From 973472e0ef7fd9ad9150ffd6171e7b7d02c710ec Mon Sep 17 00:00:00 2001 From: m2049r Date: Tue, 20 Nov 2018 18:20:42 +0100 Subject: [PATCH] Node-o-matiC --- .gitignore | 4 + app/build.gradle | 25 +- app/src/main/AndroidManifest.xml | 5 +- .../java/com/btchip/comm/BTChipTransport.java | 6 +- .../java/com/m2049r/levin/data/Bucket.java | 145 +++++ .../java/com/m2049r/levin/data/Section.java | 125 ++++ .../com/m2049r/levin/scanner/Dispatcher.java | 197 ++++++ .../m2049r/levin/scanner/PeerRetriever.java | 226 +++++++ .../java/com/m2049r/levin/util/HexHelper.java | 42 ++ .../com/m2049r/levin/util/LevinReader.java | 182 ++++++ .../com/m2049r/levin/util/LevinWriter.java | 98 +++ .../util/LittleEndianDataInputStream.java | 564 ++++++++++++++++++ .../util/LittleEndianDataOutputStream.java | 403 +++++++++++++ .../m2049r/xmrwallet/GenerateFragment.java | 18 +- .../xmrwallet/GenerateReviewFragment.java | 36 +- .../com/m2049r/xmrwallet/LoginActivity.java | 273 ++++++--- .../com/m2049r/xmrwallet/LoginFragment.java | 306 ++++------ .../com/m2049r/xmrwallet/NodeFragment.java | 550 +++++++++++++++++ .../com/m2049r/xmrwallet/ReceiveFragment.java | 25 +- .../com/m2049r/xmrwallet/SecureActivity.java | 2 +- .../java/com/m2049r/xmrwallet/TxFragment.java | 30 +- .../com/m2049r/xmrwallet/WalletActivity.java | 12 +- .../xmrwallet/XmrWalletApplication.java | 14 + .../java/com/m2049r/xmrwallet/data/Node.java | 333 +++++++++++ .../com/m2049r/xmrwallet/data/NodeInfo.java | 266 +++++++++ .../com/m2049r/xmrwallet/data/WalletNode.java | 112 ---- .../xmrwallet/dialog/ProgressDialog.java | 6 +- .../send/SendAddressWizardFragment.java | 14 +- .../send/SendAmountWizardFragment.java | 6 +- .../send/SendBtcAmountWizardFragment.java | 15 +- .../send/SendBtcConfirmWizardFragment.java | 27 +- .../send/SendBtcSuccessWizardFragment.java | 30 +- .../send/SendConfirmWizardFragment.java | 14 +- .../xmrwallet/fragment/send/SendFragment.java | 14 +- .../send/SendSuccessWizardFragment.java | 12 +- .../xmrwallet/layout/NodeInfoAdapter.java | 195 ++++++ .../layout/TransactionInfoAdapter.java | 10 +- .../xmrwallet/layout/WalletInfoAdapter.java | 6 +- .../m2049r/xmrwallet/ledger/Instruction.java | 2 +- .../com/m2049r/xmrwallet/ledger/Ledger.java | 4 +- .../com/m2049r/xmrwallet/model/Wallet.java | 2 +- .../m2049r/xmrwallet/model/WalletManager.java | 48 +- .../coinmarketcap/ExchangeApiImpl.java | 2 +- .../com/m2049r/xmrwallet/util/Helper.java | 24 +- .../m2049r/xmrwallet/util/KeyStoreHelper.java | 6 +- .../com/m2049r/xmrwallet/util/NodeList.java | 63 -- .../com/m2049r/xmrwallet/util/Notice.java | 10 +- .../xmrwallet/util/OkHttpClientSingleton.java | 34 -- .../m2049r/xmrwallet/util/OkHttpHelper.java | 72 +++ .../xmrwallet/widget/ExchangeBtcTextView.java | 8 +- .../xmrwallet/widget/ExchangeTextView.java | 12 +- .../m2049r/xmrwallet/widget/ExchangeView.java | 12 +- .../xmrwallet/widget/SendProgressView.java | 6 +- .../com/m2049r/xmrwallet/widget/Toolbar.java | 8 +- .../xmrwallet/xmrto/network/XmrToApiImpl.java | 15 +- .../res/drawable/gradient_street_efab.xml | 10 + .../main/res/drawable/ic_bookmark_24dp.xml | 9 + .../res/drawable/ic_bookmark_border_24dp.xml | 9 + .../main/res/drawable/ic_edit_white_24dp.xml | 5 - .../main/res/drawable/ic_monero_qr_24dp.xml | 33 - .../res/drawable/ic_refresh_black_24dp.xml | 9 + .../res/drawable/ic_search_orange_24dp.xml | 9 + .../ic_signal_wifi_1_bar_black_24dp.xml | 13 + .../ic_signal_wifi_2_bar_black_24dp.xml | 13 + .../ic_signal_wifi_3_bar_black_24dp.xml | 13 + .../ic_signal_wifi_4_bar_black_24dp.xml | 9 + .../ic_signal_wifi_off_black_24dp.xml | 9 + .../res/drawable/ic_wifi_lock_black_24dp.xml | 9 + app/src/main/res/layout/fragment_login.xml | 93 ++- app/src/main/res/layout/fragment_node.xml | 72 +++ app/src/main/res/layout/fragment_receive.xml | 1 - app/src/main/res/layout/fragment_wallet.xml | 2 +- app/src/main/res/layout/item_node.xml | 56 ++ app/src/main/res/layout/prompt_editnode.xml | 153 +++++ app/src/main/res/menu/drawer_view.xml | 1 - app/src/main/res/menu/list_menu.xml | 7 - app/src/main/res/menu/node_menu.xml | 11 + app/src/main/res/values-de/strings.xml | 48 +- app/src/main/res/values-el/strings.xml | 48 +- app/src/main/res/values-es/strings.xml | 47 +- app/src/main/res/values-et/strings.xml | 48 +- app/src/main/res/values-fr/strings.xml | 48 +- app/src/main/res/values-hu/strings.xml | 48 +- app/src/main/res/values-it/strings.xml | 48 +- app/src/main/res/values-nb/strings.xml | 48 +- app/src/main/res/values-nl/strings.xml | 48 +- app/src/main/res/values-pt-rBR/strings.xml | 48 +- app/src/main/res/values-pt/strings.xml | 48 +- app/src/main/res/values-ro/strings.xml | 48 +- app/src/main/res/values-ru/strings.xml | 48 +- app/src/main/res/values-sk/strings.xml | 51 +- app/src/main/res/values-sv/strings.xml | 48 +- app/src/main/res/values-zh-rCN/strings.xml | 48 +- app/src/main/res/values-zh-rTW/strings.xml | 48 +- app/src/main/res/values/help.xml | 59 +- app/src/main/res/values/strings.xml | 57 +- app/src/main/res/values/styles.xml | 61 +- .../coinmarketcap/ExchangeRateTest.java | 12 +- .../xmrwallet/util/RestoreHeightTest.java | 3 +- .../network/XmrToApiCreateOrderTest.java | 12 +- .../network/XmrToApiOrderParameterTest.java | 8 +- .../xmrto/network/XmrToApiQueryOrderTest.java | 12 +- build.gradle | 6 +- 103 files changed, 5020 insertions(+), 1250 deletions(-) create mode 100644 app/src/main/java/com/m2049r/levin/data/Bucket.java create mode 100644 app/src/main/java/com/m2049r/levin/data/Section.java create mode 100644 app/src/main/java/com/m2049r/levin/scanner/Dispatcher.java create mode 100644 app/src/main/java/com/m2049r/levin/scanner/PeerRetriever.java create mode 100644 app/src/main/java/com/m2049r/levin/util/HexHelper.java create mode 100644 app/src/main/java/com/m2049r/levin/util/LevinReader.java create mode 100644 app/src/main/java/com/m2049r/levin/util/LevinWriter.java create mode 100644 app/src/main/java/com/m2049r/levin/util/LittleEndianDataInputStream.java create mode 100644 app/src/main/java/com/m2049r/levin/util/LittleEndianDataOutputStream.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/NodeFragment.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/data/Node.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/data/NodeInfo.java delete mode 100644 app/src/main/java/com/m2049r/xmrwallet/data/WalletNode.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/layout/NodeInfoAdapter.java delete mode 100644 app/src/main/java/com/m2049r/xmrwallet/util/NodeList.java delete mode 100644 app/src/main/java/com/m2049r/xmrwallet/util/OkHttpClientSingleton.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/util/OkHttpHelper.java create mode 100644 app/src/main/res/drawable/gradient_street_efab.xml create mode 100644 app/src/main/res/drawable/ic_bookmark_24dp.xml create mode 100644 app/src/main/res/drawable/ic_bookmark_border_24dp.xml delete mode 100644 app/src/main/res/drawable/ic_edit_white_24dp.xml delete mode 100644 app/src/main/res/drawable/ic_monero_qr_24dp.xml create mode 100644 app/src/main/res/drawable/ic_refresh_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_search_orange_24dp.xml create mode 100644 app/src/main/res/drawable/ic_signal_wifi_1_bar_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_signal_wifi_2_bar_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_signal_wifi_3_bar_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_signal_wifi_4_bar_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_signal_wifi_off_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_wifi_lock_black_24dp.xml create mode 100644 app/src/main/res/layout/fragment_node.xml create mode 100644 app/src/main/res/layout/item_node.xml create mode 100644 app/src/main/res/layout/prompt_editnode.xml create mode 100644 app/src/main/res/menu/node_menu.xml diff --git a/.gitignore b/.gitignore index 002a926a..37b8e5a9 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ .externalNativeBuild .DS_Store /app/release +/app/alphaMainnet +/app/prodMainnet +/app/alphaStagenet +/app/prodStagenet diff --git a/app/build.gradle b/app/build.gradle index de32832a..a7c8d384 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,14 +1,14 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 27 + compileSdkVersion 28 buildToolsVersion '28.0.3' defaultConfig { applicationId "com.m2049r.xmrwallet" minSdkVersion 21 - targetSdkVersion 27 - versionCode 140 - versionName "1.9.0 'We Comin' Rougher'" + targetSdkVersion 28 + versionCode 153 + versionName "1.10.3 'Node-O-matiC'" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" externalNativeBuild { @@ -19,13 +19,22 @@ android { } } - flavorDimensions "version" + flavorDimensions 'type', 'net' productFlavors { + mainnet { + dimension 'net' + } + stagenet { + dimension 'net' + applicationIdSuffix '.stage' + } alpha { - applicationIdSuffix ".alpha" - versionNameSuffix " (alpha)" + dimension 'type' + applicationIdSuffix '.alpha' + versionNameSuffix ' (alpha)' } prod { + dimension 'type' } } @@ -91,9 +100,11 @@ dependencies { implementation "com.android.support:support-v4:$rootProject.ext.supportVersion" implementation "com.android.support:recyclerview-v7:$rootProject.ext.supportVersion" implementation "com.android.support:cardview-v7:$rootProject.ext.supportVersion" + implementation "com.android.support:swiperefreshlayout:$rootProject.ext.supportVersion" implementation 'me.dm7.barcodescanner:zxing:1.9.8' implementation "com.squareup.okhttp3:okhttp:$rootProject.ext.okHttpVersion" + implementation "com.burgstaller:okhttp-digest:1.18" implementation "com.jakewharton.timber:timber:$rootProject.ext.timberVersion" implementation 'com.nulab-inc:zxcvbn:1.2.3' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 805781ce..ce7ef04a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,7 +16,8 @@ android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" - android:theme="@style/MyMaterialTheme"> + android:theme="@style/MyMaterialTheme" + android:usesCleartextTraffic="true"> - + 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(); + } +} diff --git a/app/src/main/java/com/m2049r/levin/data/Section.java b/app/src/main/java/com/m2049r/levin/data/Section.java new file mode 100644 index 00000000..9be97323 --- /dev/null +++ b/app/src/main/java/com/m2049r/levin/data/Section.java @@ -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 entries = new HashMap(); + + public void add(String key, Object entry) { + entries.put(key, entry); + } + + public int size() { + return entries.size(); + } + + public Set> 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 entry : entries.entrySet()) { + sb.append(entry.getKey()).append("="); + final Object value = entry.getValue(); + if (value instanceof List) { + @SuppressWarnings("unchecked") final List list = (List) 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(); + } + } +} diff --git a/app/src/main/java/com/m2049r/levin/scanner/Dispatcher.java b/app/src/main/java/com/m2049r/levin/scanner/Dispatcher.java new file mode 100644 index 00000000..e537a404 --- /dev/null +++ b/app/src/main/java/com/m2049r/levin/scanner/Dispatcher.java @@ -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 knownNodes = new HashSet<>(); // set of nodes to test + final private Set 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 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 nodeHeights = new TreeMap(); + 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 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 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> 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 seedNodes) { + for (NodeInfo node : seedNodes) { + node.clear(); + if (node.isFavourite()) { + rpcNodes.add(node); + if (listener != null) listener.onGet(node); + } + retrievePeer(node); + } + } +} diff --git a/app/src/main/java/com/m2049r/levin/scanner/PeerRetriever.java b/app/src/main/java/com/m2049r/levin/scanner/PeerRetriever.java new file mode 100644 index 00000000..7169264a --- /dev/null +++ b/app/src/main/java/com/m2049r/levin/scanner/PeerRetriever.java @@ -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 { + 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 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 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
peerList = (List
) 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; + } +} diff --git a/app/src/main/java/com/m2049r/levin/util/HexHelper.java b/app/src/main/java/com/m2049r/levin/util/HexHelper.java new file mode 100644 index 00000000..3c265279 --- /dev/null +++ b/app/src/main/java/com/m2049r/levin/util/HexHelper.java @@ -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); + } + } +} diff --git a/app/src/main/java/com/m2049r/levin/util/LevinReader.java b/app/src/main/java/com/m2049r/levin/util/LevinReader.java new file mode 100644 index 00000000..7175e672 --- /dev/null +++ b/app/src/main/java/com/m2049r/levin/util/LevinReader.java @@ -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 readArrayEntry(int type) throws IOException { + List list = new ArrayList(); + 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; + } + +} diff --git a/app/src/main/java/com/m2049r/levin/util/LevinWriter.java b/app/src/main/java/com/m2049r/levin/util/LevinWriter.java new file mode 100644 index 00000000..ad2fe32b --- /dev/null +++ b/app/src/main/java/com/m2049r/levin/util/LevinWriter.java @@ -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 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(); + } + } +} diff --git a/app/src/main/java/com/m2049r/levin/util/LittleEndianDataInputStream.java b/app/src/main/java/com/m2049r/levin/util/LittleEndianDataInputStream.java new file mode 100644 index 00000000..3924eebd --- /dev/null +++ b/app/src/main/java/com/m2049r/levin/util/LittleEndianDataInputStream.java @@ -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 b. 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. + * + *

+ * If b is null, a NullPointerException is thrown. + * If the length of b is zero, then no bytes are read and + * 0 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 -1 is returned; otherwise, at least one byte + * is read and stored into b. + * + *

+ * The first byte read is stored into element b[0], the next + * one into b[1], and so on. The number of bytes read is, at + * most, equal to the length of b. Let k be the + * number of bytes actually read; these bytes will be stored in elements + * b[0] through b[k-1], leaving elements + * b[k] through b[b.length-1] unaffected. + * + *

+ * The read(b) method has the same effect as:

+ * + *
+     * read(b, 0, b.length)
+     * 
+ * + *
+ * + * @param b the buffer into which the data is read. + * @return the total number of bytes read into the buffer, or + * -1 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 len bytes of data from the contained input + * stream into an array of bytes. An attempt is made to read as many as + * len bytes, but a smaller number may be read, possibly zero. + * 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. + * + *

+ * If len is zero, then no bytes are read and 0 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 + * -1 is returned; otherwise, at least one byte is read and + * stored into b. + * + *

+ * The first byte read is stored into element b[off], the next + * one into b[off+1], and so on. The number of bytes read is, + * at most, equal to len. Let k be the number of bytes + * actually read; these bytes will be stored in elements b[off] + * through b[off+k-1], leaving elements + * b[off+k] through + * b[off+len-1] unaffected. + * + *

+ * In every case, elements b[0] through b[off] and + * elements b[off+len] through b[b.length-1] are + * unaffected. + * + * @param b the buffer into which the data is read. + * @param off the start offset in the destination array b + * @param len the maximum number of bytes read. + * @return the total number of bytes read into the buffer, or + * -1 if there is no more data because the end of the + * stream has been reached. + * @throws NullPointerException If b is null. + * @throws IndexOutOfBoundsException If off is negative, len is + * negative, or len is greater than + * b.length - off + * @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 readFully method of + * DataInput. + *

+ * 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 readFully method of + * DataInput. + *

+ * 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 skipBytes method of + * DataInput. + *

+ * 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 readBoolean method of + * DataInput. + *

+ * Bytes for this operation are read from the contained input stream. + * + * @return the boolean 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 readByte method of + * DataInput. + *

+ * Bytes for this operation are read from the contained input stream. + * + * @return the next byte of this input stream as a signed 8-bit + * byte. + * @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 readUnsignedByte method of + * DataInput. + *

+ * 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 readShort method of + * DataInput. + *

+ * 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 readUnsignedShort method of + * DataInput. + *

+ * 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 readChar method of + * DataInput. + *

+ * Bytes for this operation are read from the contained input stream. + * + * @return the next two bytes of this input stream, interpreted as a + * char. + * @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 readInt method of + * DataInput. + *

+ * Bytes for this operation are read from the contained input stream. + * + * @return the next four bytes of this input stream, interpreted as an + * int. + * @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 readLong method of + * DataInput. + *

+ * Bytes for this operation are read from the contained input stream. + * + * @return the next eight bytes of this input stream, interpreted as a + * long. + * @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 readFloat method of + * DataInput. + *

+ * Bytes for this operation are read from the contained input stream. + * + * @return the next four bytes of this input stream, interpreted as a + * float. + * @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 readDouble method of + * DataInput. + *

+ * Bytes for this operation are read from the contained input stream. + * + * @return the next eight bytes of this input stream, interpreted as a + * double. + * @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 readUTF method of + * DataInput. + *

+ * 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 in a representation of a Unicode + * character string encoded in modified UTF-8 format; this + * string of characters is then returned as a String. The + * details of the modified UTF-8 representation are exactly the same as for + * the readUTF method of DataInput. + * + * @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); + } +} diff --git a/app/src/main/java/com/m2049r/levin/util/LittleEndianDataOutputStream.java b/app/src/main/java/com/m2049r/levin/util/LittleEndianDataOutputStream.java new file mode 100644 index 00000000..fbf7e0cc --- /dev/null +++ b/app/src/main/java/com/m2049r/levin/util/LittleEndianDataOutputStream.java @@ -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 written 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 + * b) to the underlying output stream. If no exception is + * thrown, the counter written is incremented by 1 + * . + *

+ * Implements the write method of OutputStream. + * + * @param b the byte 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 len bytes from the specified byte array starting at + * offset off to the underlying output stream. If no exception + * is thrown, the counter written is incremented by + * len. + * + * @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. + *

+ * The flush method of DataOutputStream calls the + * flush 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 boolean to the underlying output stream as a 1-byte + * value. The value true is written out as the value + * (byte)1; the value false is written out as the + * value (byte)0. If no exception is thrown, the counter + * written is incremented by 1. + * + * @param v a boolean 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 byte to the underlying output stream as a + * 1-byte value. If no exception is thrown, the counter written + * is incremented by 1. + * + * @param v a byte 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 short to the underlying output stream as two bytes, + * low byte first. If no exception is thrown, the counter + * written is incremented by 2. + * + * @param v a short 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 char to the underlying output stream as a 2-byte + * value, low byte first. If no exception is thrown, the counter + * written is incremented by 2. + * + * @param v a char 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 int to the underlying output stream as four bytes, + * low byte first. If no exception is thrown, the counter + * written is incremented by 4. + * + * @param v an int 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 long to the underlying output stream as eight + * bytes, low byte first. In no exception is thrown, the counter + * written is incremented by 8. + * + * @param v a long 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 int using the + * floatToIntBits method in class Float, and then + * writes that int value to the underlying output stream as a + * 4-byte quantity, low byte first. If no exception is thrown, the counter + * written is incremented by 4. + * + * @param v a float 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 long using the + * doubleToLongBits method in class Double, and + * then writes that long value to the underlying output stream + * as an 8-byte quantity, low byte first. If no exception is thrown, the + * counter written is incremented by 8. + * + * @param v a double 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 + * written is incremented by the length of s. + * + * @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 writeChar method. If no exception is thrown, the counter + * written is incremented by twice the length of s + * . + * + * @param s a String 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 modified UTF-8 encoding in a + * machine-independent manner. + *

+ * First, two bytes are written to the output stream as if by the + * writeShort 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 written is incremented by + * the total number of bytes written to the output stream. This will be at + * least two plus the length of str, and at most two plus + * thrice the length of str. + * + * @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 modified UTF-8 encoding in a + * machine-independent manner. + *

+ * First, two bytes are written to out as if by the writeShort + * 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 written is incremented by the total number of bytes + * written to the output stream. This will be at least two plus the length + * of str, and at most two plus thrice the length of + * str. + * + * @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 written, 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 written field. + * @see java.io.DataOutputStream#written + */ + public final int size() { + return written; + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java b/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java index 0dd4dd3a..0ded608b 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java @@ -85,15 +85,15 @@ public class GenerateFragment extends Fragment { View view = inflater.inflate(R.layout.fragment_generate, container, false); - etWalletName = (TextInputLayout) view.findViewById(R.id.etWalletName); - etWalletPassword = (TextInputLayout) view.findViewById(R.id.etWalletPassword); - llFingerprintAuth = (LinearLayout) view.findViewById(R.id.llFingerprintAuth); - etWalletMnemonic = (TextInputLayout) view.findViewById(R.id.etWalletMnemonic); - etWalletAddress = (TextInputLayout) view.findViewById(R.id.etWalletAddress); - etWalletViewKey = (TextInputLayout) view.findViewById(R.id.etWalletViewKey); - etWalletSpendKey = (TextInputLayout) view.findViewById(R.id.etWalletSpendKey); - etWalletRestoreHeight = (TextInputLayout) view.findViewById(R.id.etWalletRestoreHeight); - bGenerate = (Button) view.findViewById(R.id.bGenerate); + etWalletName = view.findViewById(R.id.etWalletName); + etWalletPassword = view.findViewById(R.id.etWalletPassword); + llFingerprintAuth = view.findViewById(R.id.llFingerprintAuth); + etWalletMnemonic = view.findViewById(R.id.etWalletMnemonic); + etWalletAddress = view.findViewById(R.id.etWalletAddress); + etWalletViewKey = view.findViewById(R.id.etWalletViewKey); + etWalletSpendKey = view.findViewById(R.id.etWalletSpendKey); + etWalletRestoreHeight = view.findViewById(R.id.etWalletRestoreHeight); + bGenerate = view.findViewById(R.id.bGenerate); etWalletMnemonic.getEditText().setRawInputType(InputType.TYPE_CLASS_TEXT); etWalletAddress.getEditText().setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); diff --git a/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java b/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java index 987a7eff..59a330fa 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java @@ -91,22 +91,22 @@ public class GenerateReviewFragment extends Fragment { View view = inflater.inflate(R.layout.fragment_review, container, false); - scrollview = (ScrollView) view.findViewById(R.id.scrollview); - pbProgress = (ProgressBar) view.findViewById(R.id.pbProgress); - tvWalletPassword = (TextView) view.findViewById(R.id.tvWalletPassword); - tvWalletAddress = (TextView) view.findViewById(R.id.tvWalletAddress); - tvWalletViewKey = (TextView) view.findViewById(R.id.tvWalletViewKey); - tvWalletSpendKey = (TextView) view.findViewById(R.id.tvWalletSpendKey); - tvWalletMnemonic = (TextView) view.findViewById(R.id.tvWalletMnemonic); - bCopyAddress = (ImageButton) view.findViewById(R.id.bCopyAddress); - bAdvancedInfo = (Button) view.findViewById(R.id.bAdvancedInfo); - llAdvancedInfo = (LinearLayout) view.findViewById(R.id.llAdvancedInfo); - llPassword = (LinearLayout) view.findViewById(R.id.llPassword); - llMnemonic = (LinearLayout) view.findViewById(R.id.llMnemonic); - llSpendKey = (LinearLayout) view.findViewById(R.id.llSpendKey); - llViewKey = (LinearLayout) view.findViewById(R.id.llViewKey); + scrollview = view.findViewById(R.id.scrollview); + pbProgress = view.findViewById(R.id.pbProgress); + tvWalletPassword = view.findViewById(R.id.tvWalletPassword); + tvWalletAddress = view.findViewById(R.id.tvWalletAddress); + tvWalletViewKey = view.findViewById(R.id.tvWalletViewKey); + tvWalletSpendKey = view.findViewById(R.id.tvWalletSpendKey); + tvWalletMnemonic = view.findViewById(R.id.tvWalletMnemonic); + bCopyAddress = view.findViewById(R.id.bCopyAddress); + bAdvancedInfo = view.findViewById(R.id.bAdvancedInfo); + llAdvancedInfo = view.findViewById(R.id.llAdvancedInfo); + llPassword = view.findViewById(R.id.llPassword); + llMnemonic = view.findViewById(R.id.llMnemonic); + llSpendKey = view.findViewById(R.id.llSpendKey); + llViewKey = view.findViewById(R.id.llViewKey); - bAccept = (Button) view.findViewById(R.id.bAccept); + bAccept = view.findViewById(R.id.bAccept); boolean allowCopy = WalletManager.getInstance().getNetworkType() != NetworkType.NetworkType_Mainnet; tvWalletMnemonic.setTextIsSelectable(allowCopy); @@ -481,13 +481,13 @@ public class GenerateReviewFragment extends Fragment { AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity()); alertDialogBuilder.setView(promptsView); - final TextInputLayout etPasswordA = (TextInputLayout) promptsView.findViewById(R.id.etWalletPasswordA); + final TextInputLayout etPasswordA = promptsView.findViewById(R.id.etWalletPasswordA); etPasswordA.setHint(getString(R.string.prompt_changepw, walletName)); - final TextInputLayout etPasswordB = (TextInputLayout) promptsView.findViewById(R.id.etWalletPasswordB); + final TextInputLayout etPasswordB = promptsView.findViewById(R.id.etWalletPasswordB); etPasswordB.setHint(getString(R.string.prompt_changepwB, walletName)); - LinearLayout llFingerprintAuth = (LinearLayout) promptsView.findViewById(R.id.llFingerprintAuth); + LinearLayout llFingerprintAuth = promptsView.findViewById(R.id.llFingerprintAuth); final Switch swFingerprintAllowed = (Switch) llFingerprintAuth.getChildAt(0); if (FingerprintHelper.isDeviceSupported(getActivity())) { llFingerprintAuth.setVisibility(View.VISIBLE); diff --git a/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java b/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java index d4fb6686..52150045 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java +++ b/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java @@ -43,7 +43,8 @@ import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; -import com.m2049r.xmrwallet.data.WalletNode; +import com.m2049r.xmrwallet.data.Node; +import com.m2049r.xmrwallet.data.NodeInfo; import com.m2049r.xmrwallet.dialog.AboutFragment; import com.m2049r.xmrwallet.dialog.CreditsFragment; import com.m2049r.xmrwallet.dialog.HelpFragment; @@ -64,25 +65,136 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; -import java.net.Socket; -import java.net.SocketAddress; import java.nio.channels.FileChannel; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; -import java.util.Date; +import java.util.HashSet; +import java.util.List; import java.util.Locale; +import java.util.Map; +import java.util.Set; import timber.log.Timber; public class LoginActivity extends BaseActivity implements LoginFragment.Listener, GenerateFragment.Listener, GenerateReviewFragment.Listener, GenerateReviewFragment.AcceptListener, - ReceiveFragment.Listener { + ReceiveFragment.Listener, NodeFragment.Listener { private static final String GENERATE_STACK = "gen"; - static final int DAEMON_TIMEOUT = 500; // deamon must respond in 500ms + private static final String NODES_PREFS_NAME = "nodes"; + private static final String PREF_DAEMON_STAGENET = "daemon_stagenet"; + private static final String PREF_DAEMON_MAINNET = "daemon_mainnet"; + + private NodeInfo node = null; + + Set favouriteNodes = new HashSet<>(); + + @Override + public void setNode(NodeInfo node) { + if ((node != null) && (node.getNetworkType() != WalletManager.getInstance().getNetworkType())) + throw new IllegalArgumentException("network type does not match"); + this.node = node; + WalletManager.getInstance().setDaemon(node); + } + + @Override + public Set getFavouriteNodes() { + return favouriteNodes; + } + + @Override + public void setFavouriteNodes(Set nodes) { + Timber.d("adding %d nodes", nodes.size()); + favouriteNodes.clear(); + for (NodeInfo node : nodes) { + Timber.d("adding %s %b", node, node.isFavourite()); + if (node.isFavourite()) + favouriteNodes.add(node); + } + if (favouriteNodes.isEmpty() && (!nodes.isEmpty())) { // no favourites - pick best ones + List nodeList = new ArrayList<>(nodes); + Collections.sort(nodeList, NodeInfo.BestNodeComparator); + int i = 0; + for (NodeInfo node : nodeList) { + Timber.d("adding %s", node); + node.setFavourite(true); + favouriteNodes.add(node); + if (++i >= 3) break; // add max first 3 nodes + } + Toast.makeText(this, getString(R.string.node_nobookmark, i), Toast.LENGTH_LONG).show(); + } + saveFavourites(); + } + + private void loadFavouritesWithNetwork() { + Helper.runWithNetwork(new Helper.Action() { + @Override + public boolean run() { + loadFavourites(); + return true; + } + }); + } + + private void loadFavourites() { + Timber.d("loadFavourites"); + favouriteNodes.clear(); + Map storedNodes = getSharedPreferences(NODES_PREFS_NAME, Context.MODE_PRIVATE).getAll(); + for (Map.Entry nodeEntry : storedNodes.entrySet()) { + if (nodeEntry != null) // just in case, ignore possible future errors + addFavourite((String) nodeEntry.getValue()); + } + if (storedNodes.isEmpty()) { // try to load legacy list & remove it (i.e. migrate the data once) + SharedPreferences sharedPref = getPreferences(Context.MODE_PRIVATE); + switch (WalletManager.getInstance().getNetworkType()) { + case NetworkType_Mainnet: + loadLegacyList(sharedPref.getString(PREF_DAEMON_MAINNET, null)); + sharedPref.edit().remove(PREF_DAEMON_MAINNET).apply(); + break; + case NetworkType_Stagenet: + loadLegacyList(sharedPref.getString(PREF_DAEMON_STAGENET, null)); + sharedPref.edit().remove(PREF_DAEMON_STAGENET).apply(); + break; + default: + throw new IllegalStateException("unsupported net " + WalletManager.getInstance().getNetworkType()); + } + } + } + + private void saveFavourites() { + List favourites = new ArrayList<>(); + Timber.d("SAVE"); + SharedPreferences.Editor editor = getSharedPreferences(NODES_PREFS_NAME, Context.MODE_PRIVATE).edit(); + editor.clear(); + int i = 1; + for (Node info : favouriteNodes) { + String nodeString = info.toNodeString(); + editor.putString(Integer.toString(i), nodeString); + Timber.d("saved %d:%s", i, nodeString); + i++; + } + editor.apply(); + } + + private void addFavourite(String nodeString) { + NodeInfo nodeInfo = NodeInfo.fromString(nodeString); + if (nodeInfo != null) { + nodeInfo.setFavourite(true); + favouriteNodes.add(nodeInfo); + } else + Timber.w("nodeString invalid: %s", nodeString); + } + + private void loadLegacyList(final String legacyListString) { + if (legacyListString == null) return; + final String[] nodeStrings = legacyListString.split(";"); + for (final String nodeString : nodeStrings) { + addFavourite(nodeString); + } + } private Toolbar toolbar; @@ -120,7 +232,7 @@ public class LoginActivity extends BaseActivity } setContentView(R.layout.activity_login); - toolbar = (Toolbar) findViewById(R.id.toolbar); + toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); getSupportActionBar().setDisplayShowTitleEnabled(false); @@ -138,12 +250,15 @@ public class LoginActivity extends BaseActivity CreditsFragment.display(getSupportFragmentManager()); break; case Toolbar.BUTTON_NONE: + break; default: Timber.e("Button " + type + "pressed - how can this be?"); } } }); + loadFavouritesWithNetwork(); + if (Helper.getWritePermission(this)) { if (savedInstanceState == null) startLoginFragment(); } else { @@ -162,15 +277,14 @@ public class LoginActivity extends BaseActivity } @Override - public boolean onWalletSelected(String walletName, String daemon, boolean streetmode) { - if (daemon.length() == 0) { + public boolean onWalletSelected(String walletName, boolean streetmode) { + if (node == null) { Toast.makeText(this, getString(R.string.prompt_daemon_missing), Toast.LENGTH_SHORT).show(); return false; } if (checkServiceRunning()) return false; try { - WalletNode aWalletNode = new WalletNode(walletName, daemon, WalletManager.getInstance().getNetworkType()); - new AsyncOpenWallet(streetmode).execute(aWalletNode); + new AsyncOpenWallet(walletName, node, streetmode).execute(); } catch (IllegalArgumentException ex) { Timber.e(ex.getLocalizedMessage()); Toast.makeText(this, ex.getLocalizedMessage(), Toast.LENGTH_SHORT).show(); @@ -211,7 +325,7 @@ public class LoginActivity extends BaseActivity }; AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setMessage(getString(R.string.details_alert_message)) + AlertDialog diag = builder.setMessage(getString(R.string.details_alert_message)) .setPositiveButton(getString(R.string.details_alert_yes), dialogClickListener) .setNegativeButton(getString(R.string.details_alert_no), dialogClickListener) .show(); @@ -298,8 +412,8 @@ public class LoginActivity extends BaseActivity AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this); alertDialogBuilder.setView(promptsView); - final EditText etRename = (EditText) promptsView.findViewById(R.id.etRename); - final TextView tvRenameLabel = (TextView) promptsView.findViewById(R.id.tvRenameLabel); + final EditText etRename = promptsView.findViewById(R.id.etRename); + final TextView tvRenameLabel = promptsView.findViewById(R.id.tvRenameLabel); tvRenameLabel.setText(getString(R.string.prompt_rename, walletName)); @@ -488,13 +602,16 @@ public class LoginActivity extends BaseActivity startGenerateFragment(type); } + @Override + public void onNodePrefs() { + Timber.d("node prefs"); + if (checkServiceRunning()) return; + startNodeFragment(); + } + //////////////////////////////////////// // LoginFragment.Listener //////////////////////////////////////// - @Override - public SharedPreferences getPrefs() { - return getPreferences(Context.MODE_PRIVATE); - } @Override public File getStorageRoot() { @@ -506,9 +623,13 @@ public class LoginActivity extends BaseActivity @Override public void showNet() { - switch (WalletManager.getInstance().getNetworkType()) { + showNet(WalletManager.getInstance().getNetworkType()); + } + + private void showNet(NetworkType net) { + switch (net) { case NetworkType_Mainnet: - toolbar.setSubtitle(getString(R.string.connect_mainnet)); + toolbar.setSubtitle(null); toolbar.setBackgroundResource(R.drawable.backgound_toolbar_mainnet); break; case NetworkType_Testnet: @@ -520,7 +641,7 @@ public class LoginActivity extends BaseActivity toolbar.setBackgroundResource(R.color.colorPrimaryDark); break; default: - throw new IllegalStateException("NetworkType unknown: " + WalletManager.getInstance().getNetworkType()); + throw new IllegalStateException("NetworkType unknown: " + net); } } @@ -609,7 +730,8 @@ public class LoginActivity extends BaseActivity } @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { + public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], + @NonNull int[] grantResults) { Timber.d("onRequestPermissionsResult()"); switch (requestCode) { case Helper.PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE: @@ -660,6 +782,11 @@ public class LoginActivity extends BaseActivity Timber.d("GenerateReviewFragment placed"); } + void startNodeFragment() { + replaceFragment(new NodeFragment(), null, null); + Timber.d("NodeFragment placed"); + } + void startReceiveFragment(Bundle extras) { replaceFragment(new ReceiveFragment(), null, extras); Timber.d("ReceiveFragment placed"); @@ -826,7 +953,8 @@ public class LoginActivity extends BaseActivity } @Override - public void onGenerateLedger(final String name, final String password, final long restoreHeight) { + public void onGenerateLedger(final String name, final String password, + final long restoreHeight) { createWallet(name, password, new WalletCreator() { @Override @@ -921,7 +1049,8 @@ public class LoginActivity extends BaseActivity } } - boolean copyWallet(File srcWallet, File dstWallet, boolean overwrite, boolean ignoreCacheError) { + boolean copyWallet(File srcWallet, File dstWallet, boolean overwrite, + boolean ignoreCacheError) { if (walletExists(dstWallet, true) && !overwrite) return false; boolean success = false; File srcDir = srcWallet.getParentFile(); @@ -1029,6 +1158,12 @@ public class LoginActivity extends BaseActivity if (((GenerateReviewFragment) f).backOk()) { super.onBackPressed(); } + } else if (f instanceof NodeFragment) { + if (!((NodeFragment) f).isRefreshing()) { + super.onBackPressed(); + } else { + Toast.makeText(LoginActivity.this, getString(R.string.node_refresh_wait), Toast.LENGTH_LONG).show(); + } } else if (f instanceof LoginFragment) { if (((LoginFragment) f).isFabOpen()) { ((LoginFragment) f).animateFAB(); @@ -1070,101 +1205,64 @@ public class LoginActivity extends BaseActivity case R.id.action_help_list: HelpFragment.display(getSupportFragmentManager(), R.string.help_list); return true; + case R.id.action_help_node: + HelpFragment.display(getSupportFragmentManager(), R.string.help_node); + return true; case R.id.action_privacy_policy: PrivacyFragment.display(getSupportFragmentManager()); return true; case R.id.action_language: onChangeLocale(); return true; - case R.id.action_stagenet: - try { - LoginFragment loginFragment = (LoginFragment) - getSupportFragmentManager().findFragmentById(R.id.fragment_container); - item.setChecked(loginFragment.onStagenetMenuItem()); - } catch (ClassCastException ex) { - // never mind then - } - return true; default: return super.onOptionsItemSelected(item); } } - public void setNetworkType(NetworkType networkType) { - WalletManager.getInstance().setNetworkType(networkType); - } - - private class AsyncOpenWallet extends AsyncTask { + // an AsyncTask which tests the node before trying to open the wallet + private class AsyncOpenWallet extends AsyncTask { final static int OK = 0; final static int TIMEOUT = 1; final static int INVALID = 2; final static int IOEX = 3; - private WalletNode walletNode; + private final String walletName; + private final NodeInfo node; private final boolean streetmode; - public AsyncOpenWallet(boolean streetmode) { + AsyncOpenWallet(String walletName, NodeInfo node, boolean streetmode) { + this.walletName = walletName; + this.node = node; this.streetmode = streetmode; } @Override protected void onPreExecute() { super.onPreExecute(); - showProgressDialog(R.string.open_progress, DAEMON_TIMEOUT / 4); } @Override - protected Integer doInBackground(WalletNode... params) { - if (params.length != 1) return INVALID; - this.walletNode = params[0]; - if (!walletNode.isValid()) return INVALID; - - Timber.d("checking %s", walletNode.getAddress()); - - try { - long timeDA = new Date().getTime(); - SocketAddress address = walletNode.getSocketAddress(); - long timeDB = new Date().getTime(); - Timber.d("Resolving " + walletNode.getAddress() + " took " + (timeDB - timeDA) + "ms."); - Socket socket = new Socket(); - long timeA = new Date().getTime(); - socket.connect(address, LoginActivity.DAEMON_TIMEOUT); - socket.close(); - long timeB = new Date().getTime(); - long time = timeB - timeA; - Timber.d("Daemon " + walletNode.getAddress() + " is " + time + "ms away."); - return (time < LoginActivity.DAEMON_TIMEOUT ? OK : TIMEOUT); - } catch (IOException ex) { - Timber.d("Cannot reach daemon %s because %s", walletNode.getAddress(), ex.getMessage()); - return IOEX; - } catch (IllegalArgumentException ex) { - Timber.d("Cannot reach daemon %s because %s", walletNode.getAddress(), ex.getMessage()); - return INVALID; - } + protected Boolean doInBackground(Void... params) { + Timber.d("checking %s", node.getAddress()); + return node.testRpcService(); } @Override - protected void onPostExecute(Integer result) { + protected void onPostExecute(Boolean result) { super.onPostExecute(result); if (isDestroyed()) { return; } - dismissProgressDialog(); - switch (result) { - case OK: - Timber.d("selected wallet is .%s.", walletNode.getName()); - // now it's getting real, onValidateFields if wallet exists - promptAndStart(walletNode, streetmode); - break; - case TIMEOUT: - Toast.makeText(LoginActivity.this, getString(R.string.status_wallet_connect_timeout), Toast.LENGTH_LONG).show(); - break; - case INVALID: + if (result) { + Timber.d("selected wallet is .%s.", node.getName()); + // now it's getting real, onValidateFields if wallet exists + promptAndStart(walletName, node, streetmode); + } else { + if (node.getResponseCode() == 0) { // IOException Toast.makeText(LoginActivity.this, getString(R.string.status_wallet_node_invalid), Toast.LENGTH_LONG).show(); - break; - case IOEX: + } else { // connected but broken Toast.makeText(LoginActivity.this, getString(R.string.status_wallet_connect_ioex), Toast.LENGTH_LONG).show(); - break; + } } } } @@ -1191,23 +1289,20 @@ public class LoginActivity extends BaseActivity return false; } - void promptAndStart(WalletNode walletNode, final boolean streetmode) { - File walletFile = Helper.getWalletFile(this, walletNode.getName()); + void promptAndStart(String walletName, Node node, final boolean streetmode) { + File walletFile = Helper.getWalletFile(this, walletName); if (WalletManager.getInstance().walletExists(walletFile)) { - WalletManager.getInstance().setDaemon(walletNode); - Helper.promptPassword(LoginActivity.this, walletNode.getName(), false, + Helper.promptPassword(LoginActivity.this, walletName, false, new Helper.PasswordAction() { @Override public void action(String walletName, String password, boolean fingerprintUsed) { if (checkDevice(walletName, password)) startWallet(walletName, password, fingerprintUsed, streetmode); - } }); } else { // this cannot really happen as we prefilter choices Toast.makeText(this, getString(R.string.bad_wallet), Toast.LENGTH_SHORT).show(); } - } // USB Stuff - (Ledger) diff --git a/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java b/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java index 13443e0f..22d93147 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java @@ -17,14 +17,13 @@ package com.m2049r.xmrwallet; import android.content.Context; -import android.content.SharedPreferences; +import android.os.AsyncTask; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.design.widget.FloatingActionButton; import android.support.v4.app.Fragment; import android.support.v7.widget.RecyclerView; -import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -33,28 +32,24 @@ import android.view.View; import android.view.ViewGroup; import android.view.animation.Animation; import android.view.animation.AnimationUtils; -import android.view.inputmethod.EditorInfo; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.EditText; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; +import com.m2049r.xmrwallet.data.NodeInfo; +import com.m2049r.xmrwallet.layout.NodeInfoAdapter; import com.m2049r.xmrwallet.layout.WalletInfoAdapter; -import com.m2049r.xmrwallet.model.NetworkType; import com.m2049r.xmrwallet.model.WalletManager; import com.m2049r.xmrwallet.util.Helper; import com.m2049r.xmrwallet.util.KeyStoreHelper; -import com.m2049r.xmrwallet.util.NodeList; import com.m2049r.xmrwallet.util.Notice; -import com.m2049r.xmrwallet.widget.DropDownEditText; import com.m2049r.xmrwallet.widget.Toolbar; import java.io.File; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Set; @@ -68,20 +63,19 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter private List walletList = new ArrayList<>(); private List displayedList = new ArrayList<>(); - private EditText etDummy; private ImageView ivGunther; - private DropDownEditText etDaemonAddress; - private ArrayAdapter nodeAdapter; + private TextView tvNodeName; + private TextView tvNodeAddress; + private View pbNode; + private View llNode; private Listener activityCallback; // Container Activity must implement this interface public interface Listener { - SharedPreferences getPrefs(); - File getStorageRoot(); - boolean onWalletSelected(String wallet, String daemon, boolean streetmode); + boolean onWalletSelected(String wallet, boolean streetmode); void onWalletDetails(String wallet); @@ -95,13 +89,17 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter void onAddWallet(String type); + void onNodePrefs(); + void showNet(); void setToolbarButton(int type); void setTitle(String title); - void setNetworkType(NetworkType networkType); + void setNode(NodeInfo node); + + Set getFavouriteNodes(); boolean hasLedger(); } @@ -120,17 +118,17 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter @Override public void onPause() { Timber.d("onPause()"); - savePrefs(); super.onPause(); } @Override public void onResume() { super.onResume(); - Timber.d("onResume()"); + Timber.d("onResume() %s", activityCallback.getFavouriteNodes().size()); activityCallback.setTitle(null); activityCallback.setToolbarButton(Toolbar.BUTTON_CREDITS); activityCallback.showNet(); + findBestNode(); } @Override @@ -139,20 +137,20 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter Timber.d("onCreateView"); View view = inflater.inflate(R.layout.fragment_login, container, false); - ivGunther = (ImageView) view.findViewById(R.id.ivGunther); - fabScreen = (FrameLayout) view.findViewById(R.id.fabScreen); - fab = (FloatingActionButton) view.findViewById(R.id.fab); - fabNew = (FloatingActionButton) view.findViewById(R.id.fabNew); - fabView = (FloatingActionButton) view.findViewById(R.id.fabView); - fabKey = (FloatingActionButton) view.findViewById(R.id.fabKey); - fabSeed = (FloatingActionButton) view.findViewById(R.id.fabSeed); - fabLedger = (FloatingActionButton) view.findViewById(R.id.fabLedger); + ivGunther = view.findViewById(R.id.ivGunther); + fabScreen = view.findViewById(R.id.fabScreen); + fab = view.findViewById(R.id.fab); + fabNew = view.findViewById(R.id.fabNew); + fabView = view.findViewById(R.id.fabView); + fabKey = view.findViewById(R.id.fabKey); + fabSeed = view.findViewById(R.id.fabSeed); + fabLedger = view.findViewById(R.id.fabLedger); - fabNewL = (RelativeLayout) view.findViewById(R.id.fabNewL); - fabViewL = (RelativeLayout) view.findViewById(R.id.fabViewL); - fabKeyL = (RelativeLayout) view.findViewById(R.id.fabKeyL); - fabSeedL = (RelativeLayout) view.findViewById(R.id.fabSeedL); - fabLedgerL = (RelativeLayout) view.findViewById(R.id.fabLedgerL); + fabNewL = view.findViewById(R.id.fabNewL); + fabViewL = view.findViewById(R.id.fabViewL); + fabKeyL = view.findViewById(R.id.fabKeyL); + fabSeedL = view.findViewById(R.id.fabSeedL); + fabLedgerL = view.findViewById(R.id.fabLedgerL); fab_pulse = AnimationUtils.loadAnimation(getContext(), R.anim.fab_pulse); fab_open_screen = AnimationUtils.loadAnimation(getContext(), R.anim.fab_open_screen); @@ -169,71 +167,48 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter fabLedger.setOnClickListener(this); fabScreen.setOnClickListener(this); - RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.list); + RecyclerView recyclerView = view.findViewById(R.id.list); registerForContextMenu(recyclerView); this.adapter = new WalletInfoAdapter(getActivity(), this); recyclerView.setAdapter(adapter); - etDummy = (EditText) view.findViewById(R.id.etDummy); - - ViewGroup llNotice = (ViewGroup) view.findViewById(R.id.llNotice); + ViewGroup llNotice = view.findViewById(R.id.llNotice); Notice.showAll(llNotice, ".*_login"); - etDaemonAddress = (DropDownEditText) view.findViewById(R.id.etDaemonAddress); - nodeAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_dropdown_item_1line); - etDaemonAddress.setAdapter(nodeAdapter); + pbNode = view.findViewById(R.id.pbNode); + llNode = view.findViewById(R.id.llNode); + llNode.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (activityCallback.getFavouriteNodes().isEmpty()) + activityCallback.onNodePrefs(); + else + findBestNode(); + } + }); + tvNodeName = view.findViewById(R.id.tvNodeName); + tvNodeAddress = view.findViewById(R.id.tvNodeAddress); + view.findViewById(R.id.ibOption).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (activityCallback != null) + activityCallback.onNodePrefs(); + } + }); Helper.hideKeyboard(getActivity()); - etDaemonAddress.setThreshold(0); - etDaemonAddress.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - etDaemonAddress.showDropDown(); - Helper.showKeyboard(getActivity()); - } - }); - - etDaemonAddress.setOnFocusChangeListener(new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View v, boolean hasFocus) { - if (hasFocus && !getActivity().isFinishing() && etDaemonAddress.isLaidOut()) { - etDaemonAddress.showDropDown(); - Helper.showKeyboard(getActivity()); - } - } - }); - - etDaemonAddress.setOnEditorActionListener(new TextView.OnEditorActionListener() { - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN)) - || (actionId == EditorInfo.IME_ACTION_DONE)) { - Helper.hideKeyboard(getActivity()); - etDummy.requestFocus(); - return true; - } - return false; - } - }); - - etDaemonAddress.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View arg1, int pos, long id) { - Helper.hideKeyboard(getActivity()); - etDummy.requestFocus(); - - } - }); - - loadPrefs(); + loadList(); return view; } // Callbacks from WalletInfoAdapter + + // Wallet touched @Override public void onInteraction(final View view, final WalletManager.WalletInfo infoItem) { - String addressPrefix = addressPrefix(); + String addressPrefix = WalletManager.getInstance().addressPrefix(); if (addressPrefix.indexOf(infoItem.address.charAt(0)) < 0) { Toast.makeText(getActivity(), getString(R.string.prompt_wrong_net), Toast.LENGTH_LONG).show(); return; @@ -242,9 +217,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter } private void openWallet(String name, boolean streetmode) { - if (activityCallback.onWalletSelected(name, getDaemon(), streetmode)) { - savePrefs(); - } + activityCallback.onWalletSelected(name, streetmode); } @Override @@ -274,22 +247,9 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter return true; } - private String addressPrefix() { - switch (WalletManager.getInstance().getNetworkType()) { - case NetworkType_Testnet: - return "9A-"; - case NetworkType_Mainnet: - return "4-"; - case NetworkType_Stagenet: - return "5-"; - default: - throw new IllegalStateException("Unsupported Network: " + WalletManager.getInstance().getNetworkType()); - } - } - private void filterList() { displayedList.clear(); - String addressPrefix = addressPrefix(); + String addressPrefix = WalletManager.getInstance().addressPrefix(); for (WalletManager.WalletInfo s : walletList) { if (addressPrefix.indexOf(s.address.charAt(0)) >= 0) displayedList.add(s); } @@ -348,94 +308,9 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.list_menu, menu); - menu.findItem(R.id.action_stagenet).setChecked(stagenetCheckMenu); super.onCreateOptionsMenu(menu, inflater); } - private boolean stagenetCheckMenu = BuildConfig.DEBUG; - - public boolean onStagenetMenuItem() { - boolean lastState = stagenetCheckMenu; - setNet(!lastState, true); // set and save - return !lastState; - } - - public void setNet(boolean stagenetChecked, boolean save) { - this.stagenetCheckMenu = stagenetChecked; - NetworkType net = stagenetChecked ? NetworkType.NetworkType_Stagenet : NetworkType.NetworkType_Mainnet; - activityCallback.setNetworkType(net); - activityCallback.showNet(); - if (save) { - savePrefs(true); // use previous state as we just clicked it - } - if (stagenetChecked) { - setDaemon(daemonStageNet); - } else { - setDaemon(daemonMainNet); - } - loadList(); - } - - private static final String PREF_DAEMON_STAGENET = "daemon_stagenet"; - private static final String PREF_DAEMON_MAINNET = "daemon_mainnet"; - - private static final String PREF_DAEMONLIST_MAINNET = - "node.moneroworld.com:18089;node.xmrbackb.one;node.xmr.be"; - - private static final String PREF_DAEMONLIST_STAGENET = - "stagenet.monerujo.io;stagenet.xmr-tw.org"; - - private NodeList daemonStageNet; - private NodeList daemonMainNet; - - void loadPrefs() { - SharedPreferences sharedPref = activityCallback.getPrefs(); - - daemonMainNet = new NodeList(sharedPref.getString(PREF_DAEMON_MAINNET, PREF_DAEMONLIST_MAINNET)); - daemonStageNet = new NodeList(sharedPref.getString(PREF_DAEMON_STAGENET, PREF_DAEMONLIST_STAGENET)); - setNet(stagenetCheckMenu, false); - } - - void savePrefs() { - savePrefs(false); - } - - void savePrefs(boolean usePreviousNetState) { - Timber.d("SAVE / %s", usePreviousNetState); - // save the daemon address for the net - boolean stagenet = stagenetCheckMenu ^ usePreviousNetState; - String daemon = getDaemon(); - if (stagenet) { - daemonStageNet.setRecent(daemon); - } else { - daemonMainNet.setRecent(daemon); - } - - SharedPreferences sharedPref = activityCallback.getPrefs(); - SharedPreferences.Editor editor = sharedPref.edit(); - editor.putString(PREF_DAEMON_MAINNET, daemonMainNet.toString()); - editor.putString(PREF_DAEMON_STAGENET, daemonStageNet.toString()); - editor.apply(); - } - - String getDaemon() { - return etDaemonAddress.getText().toString().trim(); - } - - void setDaemon(NodeList nodeList) { - Timber.d("setDaemon() %s", nodeList.toString()); - String[] nodes = nodeList.getNodes().toArray(new String[0]); - nodeAdapter.clear(); - nodeAdapter.addAll(nodes); - etDaemonAddress.getText().clear(); - if (nodes.length > 0) { - etDaemonAddress.setText(nodes[0]); - } - etDaemonAddress.dismissDropDown(); - etDummy.requestFocus(); - Helper.hideKeyboard(getActivity()); - } - private boolean isFabOpen = false; private FloatingActionButton fab, fabNew, fabView, fabKey, fabSeed, fabLedger; private FrameLayout fabScreen; @@ -534,4 +409,71 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter break; } } + + public void findBestNode() { + new AsyncFindBestNode().execute(); + } + + private class AsyncFindBestNode extends AsyncTask { + @Override + protected void onPreExecute() { + super.onPreExecute(); + pbNode.setVisibility(View.VISIBLE); + llNode.setVisibility(View.INVISIBLE); + activityCallback.setNode(null); + } + + @Override + protected NodeInfo doInBackground(Void... params) { + List nodesToTest = new ArrayList<>(activityCallback.getFavouriteNodes()); + Timber.d("testing best node from %d", nodesToTest.size()); + if (nodesToTest.isEmpty()) return null; + for (NodeInfo node : nodesToTest) { + node.testRpcService(); // TODO: do this in parallel? + // no: it's better if it looks like it's doing something + } + Collections.sort(nodesToTest, NodeInfo.BestNodeComparator); + NodeInfo bestNode = nodesToTest.get(0); + if (bestNode.isValid()) + return nodesToTest.get(0); + else + return null; + } + + @Override + protected void onPostExecute(NodeInfo result) { + if (!isAdded()) return; + pbNode.setVisibility(View.INVISIBLE); + llNode.setVisibility(View.VISIBLE); + activityCallback.setNode(result); + if (result != null) { + Timber.d("found a good node %s", result.toString()); + showNode(result); + } else { + if (!activityCallback.getFavouriteNodes().isEmpty()) { + tvNodeName.setText(getResources().getText(R.string.node_refresh_hint)); + tvNodeName.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_refresh_black_24dp, 0, 0, 0); + tvNodeAddress.setText(null); + tvNodeAddress.setVisibility(View.GONE); + } else { + tvNodeName.setText(getResources().getText(R.string.node_create_hint)); + tvNodeName.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); + tvNodeAddress.setText(null); + tvNodeAddress.setVisibility(View.GONE); + } + } + } + + @Override + protected void onCancelled(NodeInfo result) { //TODO: cancel this on exit from fragment + Timber.d("cancelled with %s", result); + } + } + + private void showNode(NodeInfo nodeInfo) { + tvNodeName.setText(nodeInfo.getName()); + tvNodeName.setCompoundDrawablesWithIntrinsicBounds(NodeInfoAdapter.getPingIcon(nodeInfo), 0, 0, 0); + tvNodeAddress.setText(nodeInfo.getAddress()); + tvNodeAddress.setVisibility(View.VISIBLE); + } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/NodeFragment.java b/app/src/main/java/com/m2049r/xmrwallet/NodeFragment.java new file mode 100644 index 00000000..13fefa75 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/NodeFragment.java @@ -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 nodeList = new HashSet<>(); + + private NodeInfoAdapter nodesAdapter; + + private Listener activityCallback; + + public interface Listener { + File getStorageRoot(); + + void setToolbarButton(int type); + + void setSubtitle(String title); + + Set getFavouriteNodes(); + + void setFavouriteNodes(Set favouriteNodes); + } + + void filterFavourites() { + for (Iterator 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 { + @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 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 { + @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(); + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java b/app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java index 795bd414..fb48a05c 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java @@ -92,17 +92,17 @@ public class ReceiveFragment extends Fragment { View view = inflater.inflate(R.layout.fragment_receive, container, false); - pbProgress = (ProgressBar) view.findViewById(R.id.pbProgress); - tvAddressLabel = (TextView) view.findViewById(R.id.tvAddressLabel); - tvAddress = (TextView) view.findViewById(R.id.tvAddress); - etNotes = (TextInputLayout) view.findViewById(R.id.etNotes); - evAmount = (ExchangeView) view.findViewById(R.id.evAmount); - qrCode = (ImageView) view.findViewById(R.id.qrCode); - tvQrCode = (TextView) view.findViewById(R.id.tvQrCode); - qrCodeFull = (ImageView) view.findViewById(R.id.qrCodeFull); - etDummy = (EditText) view.findViewById(R.id.etDummy); - bCopyAddress = (ImageButton) view.findViewById(R.id.bCopyAddress); - bSubaddress = (Button) view.findViewById(R.id.bSubaddress); + pbProgress = view.findViewById(R.id.pbProgress); + tvAddressLabel = view.findViewById(R.id.tvAddressLabel); + tvAddress = view.findViewById(R.id.tvAddress); + etNotes = view.findViewById(R.id.etNotes); + evAmount = view.findViewById(R.id.evAmount); + qrCode = view.findViewById(R.id.qrCode); + tvQrCode = view.findViewById(R.id.tvQrCode); + qrCodeFull = view.findViewById(R.id.qrCodeFull); + etDummy = view.findViewById(R.id.etDummy); + bCopyAddress = view.findViewById(R.id.bCopyAddress); + bSubaddress = view.findViewById(R.id.bSubaddress); etDummy.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); @@ -438,7 +438,7 @@ public class ReceiveFragment extends Fragment { Bitmap logoBitmap = Bitmap.createBitmap(qrWidth, qrHeight, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(logoBitmap); canvas.drawBitmap(qrBitmap, 0, 0, null); - canvas.save(Canvas.ALL_SAVE_FLAG); + canvas.save(); // figure out how to scale the logo float scaleSize = 1.0f; while ((logoWidth / scaleSize) > (qrWidth / 5) || (logoHeight / scaleSize) > (qrHeight / 5)) { @@ -482,7 +482,6 @@ public class ReceiveFragment extends Fragment { if (context instanceof GenerateReviewFragment.ProgressListener) { this.progressCallback = (GenerateReviewFragment.ProgressListener) context; } - } @Override diff --git a/app/src/main/java/com/m2049r/xmrwallet/SecureActivity.java b/app/src/main/java/com/m2049r/xmrwallet/SecureActivity.java index 36728956..c3e9b4c9 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/SecureActivity.java +++ b/app/src/main/java/com/m2049r/xmrwallet/SecureActivity.java @@ -31,7 +31,7 @@ public abstract class SecureActivity extends AppCompatActivity { super.onCreate(savedInstanceState); // set FLAG_SECURE to prevent screenshots in Release Mode - if (!BuildConfig.DEBUG) { + if (!BuildConfig.DEBUG && !BuildConfig.FLAVOR_type.equals("alpha")) { getWindow().setFlags(LayoutParams.FLAG_SECURE, LayoutParams.FLAG_SECURE); } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/TxFragment.java b/app/src/main/java/com/m2049r/xmrwallet/TxFragment.java index ea86aa55..137e6a56 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/TxFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/TxFragment.java @@ -85,22 +85,22 @@ public class TxFragment extends Fragment { View view = inflater.inflate(R.layout.fragment_tx_info, container, false); cvXmrTo = view.findViewById(R.id.cvXmrTo); - tvTxXmrToKey = (TextView) view.findViewById(R.id.tvTxXmrToKey); - tvDestinationBtc = (TextView) view.findViewById(R.id.tvDestinationBtc); - tvTxAmountBtc = (TextView) view.findViewById(R.id.tvTxAmountBtc); + tvTxXmrToKey = view.findViewById(R.id.tvTxXmrToKey); + tvDestinationBtc = view.findViewById(R.id.tvDestinationBtc); + tvTxAmountBtc = view.findViewById(R.id.tvTxAmountBtc); - tvAccount = (TextView) view.findViewById(R.id.tvAccount); - tvTxTimestamp = (TextView) view.findViewById(R.id.tvTxTimestamp); - tvTxId = (TextView) view.findViewById(R.id.tvTxId); - tvTxKey = (TextView) view.findViewById(R.id.tvTxKey); - tvDestination = (TextView) view.findViewById(R.id.tvDestination); - tvTxPaymentId = (TextView) view.findViewById(R.id.tvTxPaymentId); - tvTxBlockheight = (TextView) view.findViewById(R.id.tvTxBlockheight); - tvTxAmount = (TextView) view.findViewById(R.id.tvTxAmount); - tvTxFee = (TextView) view.findViewById(R.id.tvTxFee); - tvTxTransfers = (TextView) view.findViewById(R.id.tvTxTransfers); - etTxNotes = (TextView) view.findViewById(R.id.etTxNotes); - bTxNotes = (Button) view.findViewById(R.id.bTxNotes); + tvAccount = view.findViewById(R.id.tvAccount); + tvTxTimestamp = view.findViewById(R.id.tvTxTimestamp); + tvTxId = view.findViewById(R.id.tvTxId); + tvTxKey = view.findViewById(R.id.tvTxKey); + tvDestination = view.findViewById(R.id.tvDestination); + tvTxPaymentId = view.findViewById(R.id.tvTxPaymentId); + tvTxBlockheight = view.findViewById(R.id.tvTxBlockheight); + tvTxAmount = view.findViewById(R.id.tvTxAmount); + tvTxFee = view.findViewById(R.id.tvTxFee); + tvTxTransfers = view.findViewById(R.id.tvTxTransfers); + etTxNotes = view.findViewById(R.id.etTxNotes); + bTxNotes = view.findViewById(R.id.bTxNotes); etTxNotes.setRawInputType(InputType.TYPE_CLASS_TEXT); diff --git a/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java b/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java index 045c523b..72bb05aa 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java +++ b/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java @@ -349,7 +349,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste } setContentView(R.layout.activity_wallet); - toolbar = (Toolbar) findViewById(R.id.toolbar); + toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); getSupportActionBar().setDisplayShowTitleEnabled(false); @@ -378,13 +378,13 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste } }); - drawer = (DrawerLayout) findViewById(R.id.drawer_layout); + drawer = findViewById(R.id.drawer_layout); drawerToggle = new ActionBarDrawerToggle(this, drawer, toolbar, 0, 0); drawer.addDrawerListener(drawerToggle); drawerToggle.syncState(); setDrawerEnabled(false); // disable until synced - accountsView = (NavigationView) findViewById(R.id.accounts_nav); + accountsView = findViewById(R.id.accounts_nav); accountsView.setNavigationItemSelectedListener(this); showNet(); @@ -1074,7 +1074,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste void updateAccountsHeader() { final Wallet wallet = getWallet(); - final TextView tvName = (TextView) accountsView.getHeaderView(0).findViewById(R.id.tvName); + final TextView tvName = accountsView.getHeaderView(0).findViewById(R.id.tvName); tvName.setText(wallet.getName()); } @@ -1115,8 +1115,8 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this); alertDialogBuilder.setView(promptsView); - final EditText etRename = (EditText) promptsView.findViewById(R.id.etRename); - final TextView tvRenameLabel = (TextView) promptsView.findViewById(R.id.tvRenameLabel); + final EditText etRename = promptsView.findViewById(R.id.etRename); + final TextView tvRenameLabel = promptsView.findViewById(R.id.tvRenameLabel); final Wallet wallet = getWallet(); tvRenameLabel.setText(getString(R.string.prompt_rename, wallet.getAccountLabel())); diff --git a/app/src/main/java/com/m2049r/xmrwallet/XmrWalletApplication.java b/app/src/main/java/com/m2049r/xmrwallet/XmrWalletApplication.java index d7178b81..389996dc 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/XmrWalletApplication.java +++ b/app/src/main/java/com/m2049r/xmrwallet/XmrWalletApplication.java @@ -21,6 +21,7 @@ import android.app.Application; import android.content.Context; import android.content.res.Configuration; +import com.m2049r.xmrwallet.model.NetworkType; import com.m2049r.xmrwallet.util.LocaleHelper; import timber.log.Timber; @@ -46,4 +47,17 @@ public class XmrWalletApplication extends Application { LocaleHelper.updateSystemDefaultLocale(configuration.locale); LocaleHelper.setLocale(XmrWalletApplication.this, LocaleHelper.getLocale(XmrWalletApplication.this)); } + + static public NetworkType getNetworkType() { + switch (BuildConfig.FLAVOR_net) { + case "mainnet": + return NetworkType.NetworkType_Mainnet; + case "stagenet": + return NetworkType.NetworkType_Stagenet; + case "testnet": + return NetworkType.NetworkType_Testnet; + default: + throw new IllegalStateException("unknown net flavor " + BuildConfig.FLAVOR_net); + } + } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/data/Node.java b/app/src/main/java/com/m2049r/xmrwallet/data/Node.java new file mode 100644 index 00000000..4eb1d335 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/data/Node.java @@ -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; + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/data/NodeInfo.java b/app/src/main/java/com/m2049r/xmrwallet/data/NodeInfo.java new file mode 100644 index 00000000..63f00d7b --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/data/NodeInfo.java @@ -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 BestNodeComparator = new Comparator() { + @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 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; + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/data/WalletNode.java b/app/src/main/java/com/m2049r/xmrwallet/data/WalletNode.java deleted file mode 100644 index 42705ab1..00000000 --- a/app/src/main/java/com/m2049r/xmrwallet/data/WalletNode.java +++ /dev/null @@ -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(); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/dialog/ProgressDialog.java b/app/src/main/java/com/m2049r/xmrwallet/dialog/ProgressDialog.java index aa73f740..73713b06 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/dialog/ProgressDialog.java +++ b/app/src/main/java/com/m2049r/xmrwallet/dialog/ProgressDialog.java @@ -57,10 +57,10 @@ public class ProgressDialog extends AlertDialog { protected void onCreate(Bundle savedInstanceState) { final View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_ledger_progress, null); pbCircle = view.findViewById(R.id.pbCircle); - tvMessage = (TextView) view.findViewById(R.id.tvMessage); + tvMessage = view.findViewById(R.id.tvMessage); rlProgressBar = view.findViewById(R.id.rlProgressBar); - pbBar = (ProgressBar) view.findViewById(R.id.pbBar); - tvProgress = (TextView) view.findViewById(R.id.tvProgress); + pbBar = view.findViewById(R.id.pbBar); + tvProgress = view.findViewById(R.id.tvProgress); setView(view); //setTitle("blabla"); //super.setMessage("bubbu"); diff --git a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAddressWizardFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAddressWizardFragment.java index 939f8beb..587d59f5 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAddressWizardFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAddressWizardFragment.java @@ -106,10 +106,10 @@ public class SendAddressWizardFragment extends SendWizardFragment { tvPaymentIdIntegrated = view.findViewById(R.id.tvPaymentIdIntegrated); llPaymentId = view.findViewById(R.id.llPaymentId); llXmrTo = view.findViewById(R.id.llXmrTo); - tvXmrTo = (TextView) view.findViewById(R.id.tvXmrTo); + tvXmrTo = view.findViewById(R.id.tvXmrTo); tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto))); - etAddress = (TextInputLayout) view.findViewById(R.id.etAddress); + etAddress = view.findViewById(R.id.etAddress); etAddress.getEditText().setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); etAddress.getEditText().setOnEditorActionListener(new TextView.OnEditorActionListener() { public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { @@ -168,7 +168,7 @@ public class SendAddressWizardFragment extends SendWizardFragment { } }); - etPaymentId = (TextInputLayout) view.findViewById(R.id.etPaymentId); + etPaymentId = view.findViewById(R.id.etPaymentId); etPaymentId.getEditText().setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); etPaymentId.getEditText().setOnEditorActionListener(new TextView.OnEditorActionListener() { public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { @@ -197,7 +197,7 @@ public class SendAddressWizardFragment extends SendWizardFragment { } }); - bPaymentId = (Button) view.findViewById(R.id.bPaymentId); + bPaymentId = view.findViewById(R.id.bPaymentId); bPaymentId.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -205,7 +205,7 @@ public class SendAddressWizardFragment extends SendWizardFragment { } }); - etNotes = (TextInputLayout) view.findViewById(R.id.etNotes); + etNotes = view.findViewById(R.id.etNotes); etNotes.getEditText().setRawInputType(InputType.TYPE_CLASS_TEXT); etNotes.getEditText().setOnEditorActionListener(new TextView.OnEditorActionListener() { public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { @@ -219,7 +219,7 @@ public class SendAddressWizardFragment extends SendWizardFragment { } }); - cvScan = (CardView) view.findViewById(R.id.bScan); + cvScan = view.findViewById(R.id.bScan); cvScan.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -228,7 +228,7 @@ public class SendAddressWizardFragment extends SendWizardFragment { }); - etDummy = (EditText) view.findViewById(R.id.etDummy); + etDummy = view.findViewById(R.id.etDummy); etDummy.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); etDummy.requestFocus(); Helper.hideKeyboard(getActivity()); diff --git a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAmountWizardFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAmountWizardFragment.java index c7b901f5..c0b5fb5b 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAmountWizardFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAmountWizardFragment.java @@ -74,9 +74,9 @@ public class SendAmountWizardFragment extends SendWizardFragment { View view = inflater.inflate(R.layout.fragment_send_amount, container, false); - tvFunds = (TextView) view.findViewById(R.id.tvFunds); + tvFunds = view.findViewById(R.id.tvFunds); - evAmount = (ExchangeTextView) view.findViewById(R.id.evAmount); + evAmount = view.findViewById(R.id.evAmount); ((NumberPadView) view.findViewById(R.id.numberPad)).setListener(evAmount); rlSweep = view.findViewById(R.id.rlSweep); @@ -88,7 +88,7 @@ public class SendAmountWizardFragment extends SendWizardFragment { } }); - ibSweep = (ImageButton) view.findViewById(R.id.ibSweep); + ibSweep = view.findViewById(R.id.ibSweep); ibSweep.setOnClickListener(new View.OnClickListener() { @Override diff --git a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcAmountWizardFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcAmountWizardFragment.java index d7e4dd25..d8708b61 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcAmountWizardFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcAmountWizardFragment.java @@ -30,7 +30,7 @@ import com.m2049r.xmrwallet.data.BarcodeData; import com.m2049r.xmrwallet.data.TxDataBtc; import com.m2049r.xmrwallet.model.Wallet; import com.m2049r.xmrwallet.util.Helper; -import com.m2049r.xmrwallet.util.OkHttpClientSingleton; +import com.m2049r.xmrwallet.util.OkHttpHelper; import com.m2049r.xmrwallet.widget.ExchangeBtcTextView; import com.m2049r.xmrwallet.widget.NumberPadView; import com.m2049r.xmrwallet.widget.SendProgressView; @@ -44,7 +44,6 @@ import com.m2049r.xmrwallet.xmrto.network.XmrToApiImpl; import java.text.NumberFormat; import java.util.Locale; -import okhttp3.HttpUrl; import timber.log.Timber; public class SendBtcAmountWizardFragment extends SendWizardFragment { @@ -80,15 +79,15 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment { View view = inflater.inflate(R.layout.fragment_send_btc_amount, container, false); - tvFunds = (TextView) view.findViewById(R.id.tvFunds); + tvFunds = view.findViewById(R.id.tvFunds); - evParams = (SendProgressView) view.findViewById(R.id.evXmrToParms); + evParams = view.findViewById(R.id.evXmrToParms); llXmrToParms = view.findViewById(R.id.llXmrToParms); - tvXmrToParms = (TextView) view.findViewById(R.id.tvXmrToParms); + tvXmrToParms = view.findViewById(R.id.tvXmrToParms); - evAmount = (ExchangeBtcTextView) view.findViewById(R.id.evAmount); - numberPad = (NumberPadView) view.findViewById(R.id.numberPad); + evAmount = view.findViewById(R.id.evAmount); + numberPad = view.findViewById(R.id.numberPad); numberPad.setListener(evAmount); Helper.hideKeyboard(getActivity()); @@ -263,7 +262,7 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment { if (xmrToApi == null) { synchronized (this) { if (xmrToApi == null) { - xmrToApi = new XmrToApiImpl(OkHttpClientSingleton.getOkHttpClient(), + xmrToApi = new XmrToApiImpl(OkHttpHelper.getOkHttpClient(), Helper.getXmrToBaseUrl()); } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcConfirmWizardFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcConfirmWizardFragment.java index 2697b336..6e8a2009 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcConfirmWizardFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcConfirmWizardFragment.java @@ -38,7 +38,7 @@ import com.m2049r.xmrwallet.data.TxDataBtc; import com.m2049r.xmrwallet.model.PendingTransaction; import com.m2049r.xmrwallet.model.Wallet; import com.m2049r.xmrwallet.util.Helper; -import com.m2049r.xmrwallet.util.OkHttpClientSingleton; +import com.m2049r.xmrwallet.util.OkHttpHelper; import com.m2049r.xmrwallet.widget.SendProgressView; import com.m2049r.xmrwallet.xmrto.XmrToError; import com.m2049r.xmrwallet.xmrto.XmrToException; @@ -51,7 +51,6 @@ import com.m2049r.xmrwallet.xmrto.network.XmrToApiImpl; import java.text.NumberFormat; import java.util.Locale; -import okhttp3.HttpUrl; import timber.log.Timber; public class SendBtcConfirmWizardFragment extends SendWizardFragment implements SendConfirm { @@ -95,21 +94,21 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements View view = inflater.inflate( R.layout.fragment_send_btc_confirm, container, false); - tvTxBtcAddress = (TextView) view.findViewById(R.id.tvTxBtcAddress); - tvTxBtcAmount = ((TextView) view.findViewById(R.id.tvTxBtcAmount)); - tvTxBtcRate = (TextView) view.findViewById(R.id.tvTxBtcRate); - tvTxXmrToKey = (TextView) view.findViewById(R.id.tvTxXmrToKey); + tvTxBtcAddress = view.findViewById(R.id.tvTxBtcAddress); + tvTxBtcAmount = view.findViewById(R.id.tvTxBtcAmount); + tvTxBtcRate = view.findViewById(R.id.tvTxBtcRate); + tvTxXmrToKey = view.findViewById(R.id.tvTxXmrToKey); - tvTxFee = (TextView) view.findViewById(R.id.tvTxFee); - tvTxTotal = (TextView) view.findViewById(R.id.tvTxTotal); + tvTxFee = view.findViewById(R.id.tvTxFee); + tvTxTotal = view.findViewById(R.id.tvTxTotal); llStageA = view.findViewById(R.id.llStageA); - evStageA = (SendProgressView) view.findViewById(R.id.evStageA); + evStageA = view.findViewById(R.id.evStageA); llStageB = view.findViewById(R.id.llStageB); - evStageB = (SendProgressView) view.findViewById(R.id.evStageB); + evStageB = view.findViewById(R.id.evStageB); llStageC = view.findViewById(R.id.llStageC); - evStageC = (SendProgressView) view.findViewById(R.id.evStageC); + evStageC = view.findViewById(R.id.evStageC); tvTxXmrToKey.setOnClickListener(new View.OnClickListener() { @Override @@ -122,7 +121,7 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements llConfirmSend = view.findViewById(R.id.llConfirmSend); pbProgressSend = view.findViewById(R.id.pbProgressSend); - bSend = (Button) view.findViewById(R.id.bSend); + bSend = view.findViewById(R.id.bSend); bSend.setEnabled(false); bSend.setOnClickListener(new View.OnClickListener() { @@ -350,7 +349,7 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements android.app.AlertDialog.Builder alertDialogBuilder = new android.app.AlertDialog.Builder(activity); alertDialogBuilder.setView(promptsView); - final TextInputLayout etPassword = (TextInputLayout) promptsView.findViewById(R.id.etPassword); + final TextInputLayout etPassword = promptsView.findViewById(R.id.etPassword); etPassword.setHint(getString(R.string.prompt_send_password)); etPassword.getEditText().addTextChangedListener(new TextWatcher() { @@ -671,7 +670,7 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements if (xmrToApi == null) { synchronized (this) { if (xmrToApi == null) { - xmrToApi = new XmrToApiImpl(OkHttpClientSingleton.getOkHttpClient(), + xmrToApi = new XmrToApiImpl(OkHttpHelper.getOkHttpClient(), Helper.getXmrToBaseUrl()); } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcSuccessWizardFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcSuccessWizardFragment.java index ca56e2a7..fc698b42 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcSuccessWizardFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcSuccessWizardFragment.java @@ -30,7 +30,7 @@ import com.m2049r.xmrwallet.R; import com.m2049r.xmrwallet.data.PendingTx; import com.m2049r.xmrwallet.data.TxDataBtc; import com.m2049r.xmrwallet.util.Helper; -import com.m2049r.xmrwallet.util.OkHttpClientSingleton; +import com.m2049r.xmrwallet.util.OkHttpHelper; import com.m2049r.xmrwallet.xmrto.XmrToException; import com.m2049r.xmrwallet.xmrto.api.QueryOrderStatus; import com.m2049r.xmrwallet.xmrto.api.XmrToApi; @@ -40,7 +40,6 @@ import com.m2049r.xmrwallet.xmrto.network.XmrToApiImpl; import java.text.NumberFormat; import java.util.Locale; -import okhttp3.HttpUrl; import timber.log.Timber; public class SendBtcSuccessWizardFragment extends SendWizardFragment { @@ -80,7 +79,7 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment { View view = inflater.inflate( R.layout.fragment_send_btc_success, container, false); - bCopyTxId = (ImageButton) view.findViewById(R.id.bCopyTxId); + bCopyTxId = view.findViewById(R.id.bCopyTxId); bCopyTxId.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -89,27 +88,26 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment { } }); - tvXmrToAmount = (TextView) view.findViewById(R.id.tvXmrToAmount); - tvXmrToStatus = (TextView) view.findViewById(R.id.tvXmrToStatus); - ivXmrToStatus = (ImageView) view.findViewById(R.id.ivXmrToStatus); - ivXmrToStatusBig = (ImageView) view.findViewById(R.id.ivXmrToStatusBig); + tvXmrToAmount = view.findViewById(R.id.tvXmrToAmount); + tvXmrToStatus = view.findViewById(R.id.tvXmrToStatus); + ivXmrToStatus = view.findViewById(R.id.ivXmrToStatus); + ivXmrToStatusBig = view.findViewById(R.id.ivXmrToStatusBig); - tvTxId = (TextView) view.findViewById(R.id.tvTxId); - tvTxAddress = (TextView) view.findViewById(R.id.tvTxAddress); - tvTxPaymentId = (TextView) view.findViewById(R.id.tvTxPaymentId); - tvTxAmount = ((TextView) view.findViewById(R.id.tvTxAmount)); - tvTxFee = (TextView) view.findViewById(R.id.tvTxFee); + tvTxId = view.findViewById(R.id.tvTxId); + tvTxAddress = view.findViewById(R.id.tvTxAddress); + tvTxPaymentId = view.findViewById(R.id.tvTxPaymentId); + tvTxAmount = view.findViewById(R.id.tvTxAmount); + tvTxFee = view.findViewById(R.id.tvTxFee); - pbXmrto = (ProgressBar) view.findViewById(R.id.pbXmrto); + pbXmrto = view.findViewById(R.id.pbXmrto); pbXmrto.getIndeterminateDrawable().setColorFilter(0x61000000, android.graphics.PorterDuff.Mode.MULTIPLY); - tvTxXmrToKey = (TextView) view.findViewById(R.id.tvTxXmrToKey); + tvTxXmrToKey = view.findViewById(R.id.tvTxXmrToKey); tvTxXmrToKey.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Helper.clipBoardCopy(getActivity(), getString(R.string.label_copy_xmrtokey), tvTxXmrToKey.getText().toString()); Toast.makeText(getActivity(), getString(R.string.message_copy_xmrtokey), Toast.LENGTH_SHORT).show(); - ; } }); @@ -257,7 +255,7 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment { if (xmrToApi == null) { synchronized (this) { if (xmrToApi == null) { - xmrToApi = new XmrToApiImpl(OkHttpClientSingleton.getOkHttpClient(), + xmrToApi = new XmrToApiImpl(OkHttpHelper.getOkHttpClient(), Helper.getXmrToBaseUrl()); } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendConfirmWizardFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendConfirmWizardFragment.java index 5299d3bc..f0eba4db 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendConfirmWizardFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendConfirmWizardFragment.java @@ -87,12 +87,12 @@ public class SendConfirmWizardFragment extends SendWizardFragment implements Sen View view = inflater.inflate( R.layout.fragment_send_confirm, container, false); - tvTxAddress = (TextView) view.findViewById(R.id.tvTxAddress); - tvTxPaymentId = (TextView) view.findViewById(R.id.tvTxPaymentId); - tvTxNotes = (TextView) view.findViewById(R.id.tvTxNotes); - tvTxAmount = ((TextView) view.findViewById(R.id.tvTxAmount)); - tvTxFee = (TextView) view.findViewById(R.id.tvTxFee); - tvTxTotal = (TextView) view.findViewById(R.id.tvTxTotal); + tvTxAddress = view.findViewById(R.id.tvTxAddress); + tvTxPaymentId = view.findViewById(R.id.tvTxPaymentId); + tvTxNotes = view.findViewById(R.id.tvTxNotes); + tvTxAmount = view.findViewById(R.id.tvTxAmount); + tvTxFee = view.findViewById(R.id.tvTxFee); + tvTxTotal = view.findViewById(R.id.tvTxTotal); llProgress = view.findViewById(R.id.llProgress); pbProgressSend = view.findViewById(R.id.pbProgressSend); @@ -231,7 +231,7 @@ public class SendConfirmWizardFragment extends SendWizardFragment implements Sen android.app.AlertDialog.Builder alertDialogBuilder = new android.app.AlertDialog.Builder(activity); alertDialogBuilder.setView(promptsView); - final TextInputLayout etPassword = (TextInputLayout) promptsView.findViewById(R.id.etPassword); + final TextInputLayout etPassword = promptsView.findViewById(R.id.etPassword); etPassword.setHint(getString(R.string.prompt_send_password)); etPassword.getEditText().addTextChangedListener(new TextWatcher() { diff --git a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendFragment.java index 9c7f8e18..a9f0a541 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendFragment.java @@ -112,18 +112,18 @@ public class SendFragment extends Fragment final View view = inflater.inflate(R.layout.fragment_send, container, false); llNavBar = view.findViewById(R.id.llNavBar); - bDone = (Button) view.findViewById(R.id.bDone); + bDone = view.findViewById(R.id.bDone); - dotBar = (DotBar) view.findViewById(R.id.dotBar); - bPrev = (Button) view.findViewById(R.id.bPrev); - bNext = (Button) view.findViewById(R.id.bNext); + dotBar = view.findViewById(R.id.dotBar); + bPrev = view.findViewById(R.id.bPrev); + bNext = view.findViewById(R.id.bNext); arrowPrev = getResources().getDrawable(R.drawable.ic_navigate_prev_white_24dp); arrowNext = getResources().getDrawable(R.drawable.ic_navigate_next_white_24dp); - ViewGroup llNotice = (ViewGroup) view.findViewById(R.id.llNotice); + ViewGroup llNotice = view.findViewById(R.id.llNotice); Notice.showAll(llNotice, ".*_send"); - spendViewPager = (SpendViewPager) view.findViewById(R.id.pager); + spendViewPager = view.findViewById(R.id.pager); pagerAdapter = new SpendPagerAdapter(getChildFragmentManager()); spendViewPager.setOffscreenPageLimit(pagerAdapter.getCount()); // load & keep all pages in cache spendViewPager.setAdapter(pagerAdapter); @@ -183,7 +183,7 @@ public class SendFragment extends Fragment updatePosition(0); - etDummy = (EditText) view.findViewById(R.id.etDummy); + etDummy = view.findViewById(R.id.etDummy); etDummy.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); etDummy.requestFocus(); Helper.hideKeyboard(getActivity()); diff --git a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendSuccessWizardFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendSuccessWizardFragment.java index 2d2588a7..0e09a0c6 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendSuccessWizardFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendSuccessWizardFragment.java @@ -72,7 +72,7 @@ public class SendSuccessWizardFragment extends SendWizardFragment { View view = inflater.inflate( R.layout.fragment_send_success, container, false); - bCopyTxId = (ImageButton) view.findViewById(R.id.bCopyTxId); + bCopyTxId = view.findViewById(R.id.bCopyTxId); bCopyTxId.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -81,11 +81,11 @@ public class SendSuccessWizardFragment extends SendWizardFragment { } }); - tvTxId = (TextView) view.findViewById(R.id.tvTxId); - tvTxAddress = (TextView) view.findViewById(R.id.tvTxAddress); - tvTxPaymentId = (TextView) view.findViewById(R.id.tvTxPaymentId); - tvTxAmount = ((TextView) view.findViewById(R.id.tvTxAmount)); - tvTxFee = (TextView) view.findViewById(R.id.tvTxFee); + tvTxId = view.findViewById(R.id.tvTxId); + tvTxAddress = view.findViewById(R.id.tvTxAddress); + tvTxPaymentId = view.findViewById(R.id.tvTxPaymentId); + tvTxAmount = view.findViewById(R.id.tvTxAmount); + tvTxFee = view.findViewById(R.id.tvTxFee); return view; } diff --git a/app/src/main/java/com/m2049r/xmrwallet/layout/NodeInfoAdapter.java b/app/src/main/java/com/m2049r/xmrwallet/layout/NodeInfoAdapter.java new file mode 100644 index 00000000..25b7dacf --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/layout/NodeInfoAdapter.java @@ -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 { + private final SimpleDateFormat TS_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + + public interface OnInteractionListener { + void onInteraction(View view, NodeInfo item); + } + + private final List 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 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); + } + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/layout/TransactionInfoAdapter.java b/app/src/main/java/com/m2049r/xmrwallet/layout/TransactionInfoAdapter.java index 0fa41094..8d1e508a 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/layout/TransactionInfoAdapter.java +++ b/app/src/main/java/com/m2049r/xmrwallet/layout/TransactionInfoAdapter.java @@ -112,11 +112,11 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter managedWallets; + public String addressPrefix() { + return addressPrefix(getNetworkType()); + } + + static public String addressPrefix(NetworkType networkType) { + switch (networkType) { + case NetworkType_Testnet: + return "9A-"; + case NetworkType_Mainnet: + return "4-"; + case NetworkType_Stagenet: + return "5-"; + default: + throw new IllegalStateException("Unsupported Network: " + networkType); + } + } + private Wallet managedWallet = null; public Wallet getWallet() { @@ -252,23 +269,26 @@ public class WalletManager { //TODO virtual bool checkPayment(const std::string &address, const std::string &txid, const std::string &txkey, const std::string &daemon_address, uint64_t &received, uint64_t &height, std::string &error) const = 0; private String daemonAddress = null; - private NetworkType networkType = null; + private final NetworkType networkType = XmrWalletApplication.getNetworkType(); public NetworkType getNetworkType() { return networkType; } - //public void setDaemon(String address, NetworkType networkType, String username, String password) { - public void setDaemon(WalletNode walletNode) { - this.daemonAddress = walletNode.getAddress(); - this.networkType = walletNode.getNetworkType(); - this.daemonUsername = walletNode.getUsername(); - this.daemonPassword = walletNode.getPassword(); - setDaemonAddressJ(daemonAddress); - } - - public void setNetworkType(NetworkType networkType) { - this.networkType = networkType; + public void setDaemon(Node node) { + if (node != null) { + this.daemonAddress = node.getAddress(); + if (networkType != node.getNetworkType()) + throw new IllegalArgumentException("network type does not match"); + this.daemonUsername = node.getUsername(); + this.daemonPassword = node.getPassword(); + setDaemonAddressJ(daemonAddress); + } else { + this.daemonAddress = null; + this.daemonUsername = ""; + this.daemonPassword = ""; + setDaemonAddressJ(""); + } } public String getDaemonAddress() { diff --git a/app/src/main/java/com/m2049r/xmrwallet/service/exchange/coinmarketcap/ExchangeApiImpl.java b/app/src/main/java/com/m2049r/xmrwallet/service/exchange/coinmarketcap/ExchangeApiImpl.java index 50087158..0babfdd8 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/service/exchange/coinmarketcap/ExchangeApiImpl.java +++ b/app/src/main/java/com/m2049r/xmrwallet/service/exchange/coinmarketcap/ExchangeApiImpl.java @@ -107,7 +107,7 @@ public class ExchangeApiImpl implements ExchangeApi { final JSONObject metadata = json.getJSONObject("metadata"); if (!metadata.isNull("error")) { final String errorMsg = metadata.getString("error"); - callback.onError(new ExchangeException(response.code(), (String) errorMsg)); + callback.onError(new ExchangeException(response.code(), errorMsg)); } else { final JSONObject jsonResult = json.getJSONObject("data"); reportSuccess(jsonResult, swapAssets, callback); diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/Helper.java b/app/src/main/java/com/m2049r/xmrwallet/util/Helper.java index 1ab98a97..6022cd04 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/util/Helper.java +++ b/app/src/main/java/com/m2049r/xmrwallet/util/Helper.java @@ -36,6 +36,7 @@ import android.os.AsyncTask; import android.os.Build; import android.os.CancellationSignal; import android.os.Environment; +import android.os.StrictMode; import android.support.design.widget.TextInputLayout; import android.support.v4.content.ContextCompat; import android.system.ErrnoException; @@ -77,7 +78,7 @@ import timber.log.Timber; public class Helper { static private final String FLAVOR_SUFFIX = - (BuildConfig.FLAVOR.equals("prod") ? "" : "." + BuildConfig.FLAVOR) + (BuildConfig.FLAVOR.startsWith("prod") ? "" : "." + BuildConfig.FLAVOR) + (BuildConfig.DEBUG ? "-debug" : ""); static public final String CRYPTO = "XMR"; @@ -397,10 +398,10 @@ public class Helper { AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(context); alertDialogBuilder.setView(promptsView); - final TextInputLayout etPassword = (TextInputLayout) promptsView.findViewById(R.id.etPassword); + final TextInputLayout etPassword = promptsView.findViewById(R.id.etPassword); etPassword.setHint(context.getString(R.string.prompt_password, wallet)); - final TextView tvOpenPrompt = (TextView) promptsView.findViewById(R.id.tvOpenPrompt); + final TextView tvOpenPrompt = promptsView.findViewById(R.id.tvOpenPrompt); final Drawable icFingerprint = context.getDrawable(R.drawable.ic_fingerprint); final Drawable icError = context.getDrawable(R.drawable.ic_error_red_36dp); final Drawable icInfo = context.getDrawable(R.drawable.ic_info_green_36dp); @@ -598,6 +599,21 @@ public class Helper { } static public ExchangeApi getExchangeApi() { - return new com.m2049r.xmrwallet.service.exchange.coinmarketcap.ExchangeApiImpl(OkHttpClientSingleton.getOkHttpClient()); + return new com.m2049r.xmrwallet.service.exchange.coinmarketcap.ExchangeApiImpl(OkHttpHelper.getOkHttpClient()); + } + + public interface Action { + boolean run(); + } + + static public boolean runWithNetwork(Action action) { + StrictMode.ThreadPolicy currentPolicy = StrictMode.getThreadPolicy(); + StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitNetwork().build(); + StrictMode.setThreadPolicy(policy); + try { + return action.run(); + } finally { + StrictMode.setThreadPolicy(currentPolicy); + } } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/KeyStoreHelper.java b/app/src/main/java/com/m2049r/xmrwallet/util/KeyStoreHelper.java index 2e3521ca..4212887f 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/util/KeyStoreHelper.java +++ b/app/src/main/java/com/m2049r/xmrwallet/util/KeyStoreHelper.java @@ -102,11 +102,7 @@ public class KeyStoreHelper { if (isArm32 != null) return isArm32; synchronized (KeyStoreException.class) { if (isArm32 != null) return isArm32; - if (Build.SUPPORTED_ABIS[0].equals("armeabi-v7a")) { - isArm32 = true; - } else { - isArm32 = false; - } + isArm32 = Build.SUPPORTED_ABIS[0].equals("armeabi-v7a"); return isArm32; } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/NodeList.java b/app/src/main/java/com/m2049r/xmrwallet/util/NodeList.java deleted file mode 100644 index c73643e1..00000000 --- a/app/src/main/java/com/m2049r/xmrwallet/util/NodeList.java +++ /dev/null @@ -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 nodes = new ArrayList<>(); - - public List 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(); - } -} diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/Notice.java b/app/src/main/java/com/m2049r/xmrwallet/util/Notice.java index 350b5a0e..8985fba5 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/util/Notice.java +++ b/app/src/main/java/com/m2049r/xmrwallet/util/Notice.java @@ -40,16 +40,16 @@ public class Notice { private static final String NOTICE_SHOW_XMRTO_ENABLED_LOGIN = "notice_xmrto_enabled_login"; private static final String NOTICE_SHOW_XMRTO_ENABLED_SEND = "notice_xmrto_enabled_send"; private static final String NOTICE_SHOW_LEDGER = "notice_ledger_enabled_login"; - private static final String NOTICE_SHOW_STREET = "notice_streetmode_login"; + private static final String NOTICE_SHOW_NODES = "notice_nodes"; private static void init() { synchronized (Notice.class) { if (notices != null) return; notices = new ArrayList<>(); notices.add( - new Notice(NOTICE_SHOW_STREET, - R.string.info_streetmode_enabled, - R.string.help_wallet, + new Notice(NOTICE_SHOW_NODES, + R.string.info_nodes_enabled, + R.string.help_node, 1) ); notices.add( @@ -115,7 +115,7 @@ public class Notice { } }); - ImageButton ib = (ImageButton) ll.findViewById(R.id.ibClose); + ImageButton ib = ll.findViewById(R.id.ibClose); ib.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/OkHttpClientSingleton.java b/app/src/main/java/com/m2049r/xmrwallet/util/OkHttpClientSingleton.java deleted file mode 100644 index f1dae5c9..00000000 --- a/app/src/main/java/com/m2049r/xmrwallet/util/OkHttpClientSingleton.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/OkHttpHelper.java b/app/src/main/java/com/m2049r/xmrwallet/util/OkHttpHelper.java new file mode 100644 index 00000000..ecaa5e6b --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/OkHttpHelper.java @@ -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(); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeBtcTextView.java b/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeBtcTextView.java index 5a86ed1e..7f1735b7 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeBtcTextView.java +++ b/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeBtcTextView.java @@ -131,10 +131,10 @@ public class ExchangeBtcTextView extends LinearLayout @Override protected void onFinishInflate() { super.onFinishInflate(); - tvAmountA = (TextView) findViewById(R.id.tvAmountA); - tvAmountB = (TextView) findViewById(R.id.tvAmountB); - sCurrencyA = (Spinner) findViewById(R.id.sCurrencyA); - sCurrencyB = (Spinner) findViewById(R.id.sCurrencyB); + tvAmountA = findViewById(R.id.tvAmountA); + tvAmountB = findViewById(R.id.tvAmountB); + sCurrencyA = findViewById(R.id.sCurrencyA); + sCurrencyB = findViewById(R.id.sCurrencyB); ArrayAdapter btcAdapter = new ArrayAdapter(getContext(), android.R.layout.simple_spinner_item, diff --git a/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeTextView.java b/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeTextView.java index 6043c0ba..f0db87ab 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeTextView.java +++ b/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeTextView.java @@ -177,12 +177,12 @@ public class ExchangeTextView extends LinearLayout @Override protected void onFinishInflate() { super.onFinishInflate(); - tvAmountA = (TextView) findViewById(R.id.tvAmountA); - tvAmountB = (TextView) findViewById(R.id.tvAmountB); - sCurrencyA = (Spinner) findViewById(R.id.sCurrencyA); - sCurrencyB = (Spinner) findViewById(R.id.sCurrencyB); - evExchange = (ImageView) findViewById(R.id.evExchange); - pbExchange = (ProgressBar) findViewById(R.id.pbExchange); + tvAmountA = findViewById(R.id.tvAmountA); + tvAmountB = findViewById(R.id.tvAmountB); + sCurrencyA = findViewById(R.id.sCurrencyA); + sCurrencyB = findViewById(R.id.sCurrencyB); + evExchange = findViewById(R.id.evExchange); + pbExchange = findViewById(R.id.pbExchange); // make progress circle gray pbExchange.getIndeterminateDrawable(). diff --git a/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeView.java b/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeView.java index a913baad..54d59246 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeView.java +++ b/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeView.java @@ -162,16 +162,16 @@ public class ExchangeView extends LinearLayout @Override protected void onFinishInflate() { super.onFinishInflate(); - etAmount = (TextInputLayout) findViewById(R.id.etAmount); - tvAmountB = (TextView) findViewById(R.id.tvAmountB); - sCurrencyA = (Spinner) findViewById(R.id.sCurrencyA); + etAmount = findViewById(R.id.etAmount); + tvAmountB = findViewById(R.id.tvAmountB); + sCurrencyA = findViewById(R.id.sCurrencyA); ArrayAdapter adapter = ArrayAdapter.createFromResource(getContext(), R.array.currency, R.layout.item_spinner); adapter.setDropDownViewResource(R.layout.item_spinner_dropdown_item); sCurrencyA.setAdapter(adapter); - sCurrencyB = (Spinner) findViewById(R.id.sCurrencyB); + sCurrencyB = findViewById(R.id.sCurrencyB); sCurrencyB.setAdapter(adapter); - evExchange = (ImageView) findViewById(R.id.evExchange); - pbExchange = (ProgressBar) findViewById(R.id.pbExchange); + evExchange = findViewById(R.id.evExchange); + pbExchange = findViewById(R.id.pbExchange); // make progress circle gray pbExchange.getIndeterminateDrawable(). diff --git a/app/src/main/java/com/m2049r/xmrwallet/widget/SendProgressView.java b/app/src/main/java/com/m2049r/xmrwallet/widget/SendProgressView.java index 1ffcdab4..11d53559 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/widget/SendProgressView.java +++ b/app/src/main/java/com/m2049r/xmrwallet/widget/SendProgressView.java @@ -57,9 +57,9 @@ public class SendProgressView extends LinearLayout { super.onFinishInflate(); pbProgress = findViewById(R.id.pbProgress); llMessage = findViewById(R.id.llMessage); - tvCode = (TextView) findViewById(R.id.tvCode); - tvMessage = (TextView) findViewById(R.id.tvMessage); - tvSolution = (TextView) findViewById(R.id.tvSolution); + tvCode = findViewById(R.id.tvCode); + tvMessage = findViewById(R.id.tvMessage); + tvSolution = findViewById(R.id.tvSolution); } public void showProgress(String progressText) { diff --git a/app/src/main/java/com/m2049r/xmrwallet/widget/Toolbar.java b/app/src/main/java/com/m2049r/xmrwallet/widget/Toolbar.java index 8f96d126..6e5e9a96 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/widget/Toolbar.java +++ b/app/src/main/java/com/m2049r/xmrwallet/widget/Toolbar.java @@ -78,16 +78,16 @@ public class Toolbar extends android.support.v7.widget.Toolbar { @Override protected void onFinishInflate() { super.onFinishInflate(); - toolbarImage = (ImageView) findViewById(R.id.toolbarImage); + toolbarImage = findViewById(R.id.toolbarImage); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { // the vector image does not work well for androis < Nougat toolbarImage.getLayoutParams().width = (int) getResources().getDimension(R.dimen.logo_width); toolbarImage.setImageResource(R.drawable.logo_horizontol_xmrujo); } - toolbarTitle = (TextView) findViewById(R.id.toolbarTitle); - toolbarSubtitle = (TextView) findViewById(R.id.toolbarSubtitle); - bCredits = (Button) findViewById(R.id.bCredits); + toolbarTitle = findViewById(R.id.toolbarTitle); + toolbarSubtitle = findViewById(R.id.toolbarSubtitle); + bCredits = findViewById(R.id.bCredits); bCredits.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { if (onButtonListener != null) { diff --git a/app/src/main/java/com/m2049r/xmrwallet/xmrto/network/XmrToApiImpl.java b/app/src/main/java/com/m2049r/xmrwallet/xmrto/network/XmrToApiImpl.java index 8d8946b5..3d4fa73a 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/xmrto/network/XmrToApiImpl.java +++ b/app/src/main/java/com/m2049r/xmrwallet/xmrto/network/XmrToApiImpl.java @@ -17,15 +17,15 @@ package com.m2049r.xmrwallet.xmrto.network; import android.support.annotation.NonNull; -import android.support.annotation.VisibleForTesting; -import com.m2049r.xmrwallet.xmrto.api.XmrToCallback; +import com.m2049r.xmrwallet.util.OkHttpHelper; import com.m2049r.xmrwallet.xmrto.XmrToError; import com.m2049r.xmrwallet.xmrto.XmrToException; import com.m2049r.xmrwallet.xmrto.api.CreateOrder; import com.m2049r.xmrwallet.xmrto.api.QueryOrderParameters; import com.m2049r.xmrwallet.xmrto.api.QueryOrderStatus; import com.m2049r.xmrwallet.xmrto.api.XmrToApi; +import com.m2049r.xmrwallet.xmrto.api.XmrToCallback; import org.json.JSONException; import org.json.JSONObject; @@ -128,16 +128,9 @@ public class XmrToApiImpl implements XmrToApi, XmrToApiCall { if (request != null) { final RequestBody body = RequestBody.create( MediaType.parse("application/json"), request.toString()); - - return new Request.Builder() - .url(url) - .post(body) - .build(); + return OkHttpHelper.getPostRequest(url, body); } else { - return new Request.Builder() - .url(url) - .get() - .build(); + return OkHttpHelper.getGetRequest(url); } } } diff --git a/app/src/main/res/drawable/gradient_street_efab.xml b/app/src/main/res/drawable/gradient_street_efab.xml new file mode 100644 index 00000000..4f5b0728 --- /dev/null +++ b/app/src/main/res/drawable/gradient_street_efab.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_bookmark_24dp.xml b/app/src/main/res/drawable/ic_bookmark_24dp.xml new file mode 100644 index 00000000..cf64e057 --- /dev/null +++ b/app/src/main/res/drawable/ic_bookmark_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_bookmark_border_24dp.xml b/app/src/main/res/drawable/ic_bookmark_border_24dp.xml new file mode 100644 index 00000000..1cae0eb2 --- /dev/null +++ b/app/src/main/res/drawable/ic_bookmark_border_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_edit_white_24dp.xml b/app/src/main/res/drawable/ic_edit_white_24dp.xml deleted file mode 100644 index 46462b57..00000000 --- a/app/src/main/res/drawable/ic_edit_white_24dp.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_monero_qr_24dp.xml b/app/src/main/res/drawable/ic_monero_qr_24dp.xml deleted file mode 100644 index 54af39d1..00000000 --- a/app/src/main/res/drawable/ic_monero_qr_24dp.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_refresh_black_24dp.xml b/app/src/main/res/drawable/ic_refresh_black_24dp.xml new file mode 100644 index 00000000..1f9072a3 --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_search_orange_24dp.xml b/app/src/main/res/drawable/ic_search_orange_24dp.xml new file mode 100644 index 00000000..cec9af76 --- /dev/null +++ b/app/src/main/res/drawable/ic_search_orange_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_signal_wifi_1_bar_black_24dp.xml b/app/src/main/res/drawable/ic_signal_wifi_1_bar_black_24dp.xml new file mode 100644 index 00000000..fe9c7a93 --- /dev/null +++ b/app/src/main/res/drawable/ic_signal_wifi_1_bar_black_24dp.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_signal_wifi_2_bar_black_24dp.xml b/app/src/main/res/drawable/ic_signal_wifi_2_bar_black_24dp.xml new file mode 100644 index 00000000..caa79d64 --- /dev/null +++ b/app/src/main/res/drawable/ic_signal_wifi_2_bar_black_24dp.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_signal_wifi_3_bar_black_24dp.xml b/app/src/main/res/drawable/ic_signal_wifi_3_bar_black_24dp.xml new file mode 100644 index 00000000..24960e6a --- /dev/null +++ b/app/src/main/res/drawable/ic_signal_wifi_3_bar_black_24dp.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_signal_wifi_4_bar_black_24dp.xml b/app/src/main/res/drawable/ic_signal_wifi_4_bar_black_24dp.xml new file mode 100644 index 00000000..254f1609 --- /dev/null +++ b/app/src/main/res/drawable/ic_signal_wifi_4_bar_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_signal_wifi_off_black_24dp.xml b/app/src/main/res/drawable/ic_signal_wifi_off_black_24dp.xml new file mode 100644 index 00000000..c30cb7c5 --- /dev/null +++ b/app/src/main/res/drawable/ic_signal_wifi_off_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_wifi_lock_black_24dp.xml b/app/src/main/res/drawable/ic_wifi_lock_black_24dp.xml new file mode 100644 index 00000000..8a643eaf --- /dev/null +++ b/app/src/main/res/drawable/ic_wifi_lock_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/fragment_login.xml b/app/src/main/res/layout/fragment_login.xml index c52d2037..8273dd5a 100644 --- a/app/src/main/res/layout/fragment_login.xml +++ b/app/src/main/res/layout/fragment_login.xml @@ -1,6 +1,7 @@ @@ -16,35 +17,85 @@ android:layout_height="wrap_content" android:orientation="vertical" /> + + + + + + + + + + + + + + + + + + + - - - - - @@ -72,7 +123,7 @@ android:layout_height="match_parent" android:clipToPadding="false" android:paddingBottom="72dp" - app:layoutManager="LinearLayoutManager" + app:layoutManager="android.support.v7.widget.LinearLayoutManager" tools:listitem="@layout/item_wallet" /> diff --git a/app/src/main/res/layout/fragment_node.xml b/app/src/main/res/layout/fragment_node.xml new file mode 100644 index 00000000..12b83aa7 --- /dev/null +++ b/app/src/main/res/layout/fragment_node.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_receive.xml b/app/src/main/res/layout/fragment_receive.xml index 7174d665..ee91ffdd 100644 --- a/app/src/main/res/layout/fragment_receive.xml +++ b/app/src/main/res/layout/fragment_receive.xml @@ -25,7 +25,6 @@ android:visibility="gone" /> diff --git a/app/src/main/res/layout/fragment_wallet.xml b/app/src/main/res/layout/fragment_wallet.xml index cd319f38..15bc942b 100644 --- a/app/src/main/res/layout/fragment_wallet.xml +++ b/app/src/main/res/layout/fragment_wallet.xml @@ -22,7 +22,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" - android:text="@string/text_streetmode" + android:text="@string/menu_streetmode" android:visibility="invisible" /> + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/prompt_editnode.xml b/app/src/main/res/layout/prompt_editnode.xml new file mode 100644 index 00000000..83ec991d --- /dev/null +++ b/app/src/main/res/layout/prompt_editnode.xml @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/drawer_view.xml b/app/src/main/res/menu/drawer_view.xml index 197047d6..ec9557da 100644 --- a/app/src/main/res/menu/drawer_view.xml +++ b/app/src/main/res/menu/drawer_view.xml @@ -5,7 +5,6 @@ android:checkableBehavior="single" android:orderInCategory="100" /> - - +

+ + + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 00b8fe76..e5f11e78 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -29,10 +29,8 @@ Fertig Berühren für QR-Code - Höhere Priorität = Höhere Gebühr BTC Zahlung aktiviert - Tippe für mehr Infos. - CrAzYpass aktiviert - Tippe für mehr Infos. Ledger aktiviert - Tippe für mehr Infos. %1$s BTC = %2$s XMR (Kurs: %1$s BTC/XMR) - Fortgeschritten: - Besuche XMR.TO für Support & Nachverfolgung Geheimer Schlüssel\nXMR.TO XMR.TO Geheimer Schlüssel @@ -96,7 +92,6 @@ Backup wird erstellt Archivierung wird ausgeführt Umbenennung wird ausgeführt - Daemon Verbindung wird getestet Ändere Passwort Fertigstellen …\nDies kann einen Moment dauern! @@ -108,13 +103,11 @@ Passwort geändert Node - ([<user>:<pass>@]<daemon>[:<port>]) Lade Wallet … Wallet gespeichert Walletspeicherung fehlgeschlagen! Verbinde … Verbindung zum Node fehlgeschlagen!\nPrüfe Username/Passwort - Node Zeitüberschreitung!\nNochmal oder anderen Node versuchen. Node ungültig!\nVersuche anderen. Kann Node nicht erreichen!\nNochmal oder anderen Node versuchen. @@ -194,7 +187,6 @@ Kann nicht mit einem . beginnen Erstelle Wallet Wallet erstellt - Walleterstellung fehlgeschlagen Gib eine Blocknummer oder ein Datum (JJJJ-MM-TT) ein @@ -274,7 +266,6 @@ AUSSTEHEND FEHLGESCHLAGEN - Zahlungs-ID (optional) Betrag Konnte Wallet nicht öffnen! 16 oder 64 Hex-Zeichen (0–9,a–f) @@ -284,8 +275,6 @@ Min. 0 XMR keine Nummer - Empfangen - Sensible Daten werden angezeigt.\nSchau über deine Schulter! Ich bin sicher Nein, doch nicht! @@ -295,20 +284,12 @@ Ja, mach das! Nein, danke! - - Priorität Standard - Priorität Gering - Priorität Mittel - Priorität Hoch - - Neues Wallet erstellen View Only Wallet wiederherstellen Mit privaten Schlüsseln wiederherstellen Mit 25 Wörter Seed wiederherstellen Konto erstellen - Konten Neues Konto #%1$d hinzugefügt Konto # @@ -330,7 +311,6 @@ Bitte Ledger (wieder-)anschließen! Erzeuge Konto - Aktualisiere Wallet %1$s angesteckt %1$s abgesteckt @@ -357,5 +337,31 @@ Details Street Mode - Street Mode enabled, tap for more info. + + Node-o-matiC enabled, tap for more info. + Last block updated: %1$s + Nodes + Node Name (Optional) + Hostname + Port + Username (Optional) + Password (Optional) + Cannot resolve host + We need this! + Must be numeric + Must be 1–65535 + Add Node + Touch to refresh! + CONNECTION ERROR %1$d + CONNECTION ERROR + AUTHENTICATION FAILED + Test Result: + Height: %1$s (v%2$d), Ping: %3$.0fms, IP: %4$s + Testing IP: %1$s … + Please wait for scan to finish + Touch to select or add nodes + Add nodes manually or pull down to scan + Scanning network… + Automatically bookmarked best %1$d nodes + Test diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 7307f05e..f52db02e 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -28,10 +28,8 @@ Έγινε Πατήστε για QR κωδικό - Υψηλότερη προτεραιότητα = Υψηλότερα Κόμιστρα Συναλλαγή BTC ενεργοποιήθηκε, πάτα για περισσότερες πληροφορείες. - CrAzYpass ενεργοποιήθηκε, πάτα για περισσότερες πληροφορείες. Ledger ενεργοποιήθηκε, πάτα για περισσότερες πληροφορείες. %1$s BTC = %2$s XMR (Rate: %1$s BTC/XMR) - Για προχωρημένους: - Επίσκεψη στο xmr.to για υποστήριξη & με εντοπισμό συναλλαγής Μυστικό Κλειδί\nXMR.TO XMR.TO Μυστικό Κλειδί @@ -94,7 +90,6 @@ Δημιουργία αντίγραφου ασφαλείας σε εξέλιξη Αρχειοθέτηση σε εξέλιξη Μετονομασία σε εξέλιξη - Έλεγχος σύνδεσης δαίμονα Μάζεμα των πραγμάτων …\nΑυτό μπορεί να διαρκέσει λίγο! @@ -103,13 +98,11 @@ Η μετονομασία απέτυχε! Κόμβος(Δαίμονας) - ([<χρήστης>:<κωδικός>@]<δαίμονας>[:<πόρτα>]) Φόρτωση Πορτοφολιού … Πορτοφόλι αποθηκεύτηκε Η αποθήκευση του πορτοφολιού απέτυχε! Σύνδεση … Η σύνδεση με τον κόμβο απέτυχε!\nΈλεγξε χρήστη/κωδικό - Η σύνδεση με τον κόμβο έχει λήξει!\nΠροσπάθησε πάλι ή με άλλον. Ο κόμβος είναι μη έγκυρος!\nΠροσπάθησε με άλλον. Δεν είναι δυνατή η πρόσβαση στον κόμβο!\nΠροσπάθησε πάλι ή με άλλον. @@ -169,7 +162,6 @@ Δεν γίνεται να ξεκινάει με . Το πορτοφόλι δημιουργείται Το Πορτοφόλι δημιουργήθηκε - Η δημιουργία πορτοφολιού απέτυχε Βάλε αριθμό ή ημερομηνία (YYYY-MM-DD δλδ χρονιά-μήνας-μέρα) @@ -248,7 +240,6 @@ ΕΚΚΡΕΜΗ ΑΠΕΤΥΧΕ - ID Πληρωμής(Payment ID) (προαιρετικό) Ποσό Δεν ήταν δυνατό το άνοιγμα του πορτοφολιού! 16 ή 64 χαρακτήρες Hex (0–9,a–f) @@ -258,8 +249,6 @@ Ελάχιστο 0 XMR δεν υπάρχει αριθμός - Λήψη - Θα εμφανιστούν τώρα ευαίσθητα δεδομένα. \nΠρόσεχε ποιος είναι πίσω σου! Είμαι ασφαλής Πήγαινε με πίσω! @@ -269,13 +258,6 @@ Ναι, κάνε αυτό! Όχι ευχαριστώ! - - Προεπιλεγμένη Προτεραιότητα - Χαμηλή Προτεραιότητα - Μέτρια Προτεραιότητα - Υψηλή Προτεραιότητα - - Δημιουργία νέου πορτοφολιού Επαναφορά πορτοφολιού προβολής-μόνο Επαναφορά πορτοφολιού από ιδιωτικά κλειδιά @@ -308,7 +290,6 @@ Passphrase may not be empty Wallet Files Restore Password Create Account - Accounts Added new account #%1$d Account # Send all confirmed funds in this account! @@ -329,7 +310,6 @@ Please (re)connect Ledger device Creating account - Updating wallet %1$s attached %1$s detached @@ -356,5 +336,31 @@ Λεπτομέριες Street Mode - Street Mode enabled, tap for more info. + + Node-o-matiC enabled, tap for more info. + Last block updated: %1$s + Nodes + Node Name (Optional) + Hostname + Port + Username (Optional) + Password (Optional) + Cannot resolve host + We need this! + Must be numeric + Must be 1–65535 + Add Node + Touch to refresh! + CONNECTION ERROR %1$d + CONNECTION ERROR + AUTHENTICATION FAILED + Test Result: + Height: %1$s (v%2$d), Ping: %3$.0fms, IP: %4$s + Testing IP: %1$s … + Please wait for scan to finish + Touch to select or add nodes + Add nodes manually or pull down to scan + Scanning network… + Automatically bookmarked best %1$d nodes + Test diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 9e753fa7..244226fd 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -29,7 +29,6 @@ Hecho Toca para mostrar código QR - Mayor Prioridad = Mayor Comisión ID de pago integrado Preparando tu transacción @@ -44,7 +43,6 @@ Copia de seguridad en progreso Archivado en progreso Cambio de nombre en progreso - Comprobando conexión con el daemon Cambiando contraseña en progreso Guardando todo\n¡Puede llevar un tiempo! @@ -56,13 +54,11 @@ Contraseña cambiada Nodo - ([<usuario>:<contraseña>@]<daemon>[:<puerto>]) Cargando monedero… Monedero guardada ¡Guardado de monedero fallido! Conectando… ¡Conexión con el nodo fallida!\nComprueba el usuario/contraseña - ¡Conexión con el nodo ha expirado!\nInténtalo de nuevo o prueba otro. ¡Nodo inválido!\nInténtalo con otro. ¡No se puede alcanzar el nodo!\nInténtalo de nuevo o prueba otro. @@ -138,7 +134,6 @@ No puede empezar con . Creando monedero Monedero creada - Creación de monedero fallida Introduce un número o una fecha (AAAA-MM-DD) @@ -206,7 +201,6 @@ PENDIENTE FALLIDO - ID de Pago (opcional) Monto ¡No se ha podido abrir el monedero! 16 o 64 caracteres hexadecimales (0–9,a–f) @@ -216,8 +210,6 @@ Min. 0 XMR no es un número - Recibir - Se va a mostrar información delicada.\n¡Mira por encima del hombro! Estoy seguro ¡Llévame de vuelta! @@ -227,13 +219,6 @@ ¡Sí, haz eso! ¡No gracias! - - Prioridad por Defecto - Prioridad Baja - Prioridad Media - Prioridad Alta - - Crear nuevo monedero Restaurar monedero de sólo vista Restaurar monedero con claves privadas @@ -267,7 +252,6 @@ Oh-oh, parece que XMR.TO no está disponible ahora! %1$s BTC = %2$s XMR (Cambio: %1$s BTC/XMR) - Avanzado Visita https://xmr.to para soporte y rastreo Clave secreta\nXMR.TO Clave secreta XMR.TO @@ -290,11 +274,9 @@ Orden XMR.TO Pago en BTC activado, toca para más info. - CrAzYpass activado, toca para más info. Ledger activado, toca para más info. Crear Cuenta - Cuentas Nueva cuenta agregada #%1$d # de cuenta @@ -316,7 +298,6 @@ Please (re)connect Ledger device Creating account - Updating wallet %1$s attached %1$s detached @@ -343,5 +324,31 @@ Detalles Street Mode - Street Mode enabled, tap for more info. + + Node-o-matiC enabled, tap for more info. + Last block updated: %1$s + Nodes + Node Name (Optional) + Hostname + Port + Username (Optional) + Password (Optional) + Cannot resolve host + We need this! + Must be numeric + Must be 1–65535 + Add Node + Touch to refresh! + CONNECTION ERROR %1$d + CONNECTION ERROR + AUTHENTICATION FAILED + Test Result: + Height: %1$s (v%2$d), Ping: %3$.0fms, IP: %4$s + Testing IP: %1$s … + Please wait for scan to finish + Touch to select or add nodes + Add nodes manually or pull down to scan + Scanning network… + Automatically bookmarked best %1$d nodes + Test diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index de4f8edc..82f2c2d5 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -29,10 +29,8 @@ Tehtud Puuduta QR koodi saamiseks - Suurem tähtsus = kõrgemad teenustasud Bitcoini maksed sisse lülitatud, puuduta lisainfo saamiseks. - CrAzYpass sisse lülitatud, puuduta lisainfo saamiseks. Ledger\'i tugi sisse lülitatud, puuduta lisainfo saamiseks. %1$s BTC = %2$s XMR (Kurss: %1$s BTC/XMR) - Edasijõudnutele: - Külasta xmr.to lisainfo saamiseks & jälgin Privaatvõti\nXMR.TO XMR.TO privaatvõti @@ -95,7 +91,6 @@ Teostan tagavarakoopiat Arhiveerin Nimetan ümber - Kontrollin ühendust serveriga Vahetan parooli Teen ettevalmistusi …\nSee võib aega võtta! @@ -107,13 +102,11 @@ Parool vahetatud Server - ([<user>:<pass>@]<daemon>[:<port>]) Laen rahakotti … Rahakott salvestatud Rahakoti salvestamine ebaõnnestus! Ühendun … Serveriga ühendumine ebaõnnestus!\nKontrolli kasutajanime/parooli - Serveriga ühendumine võttis liiga kaua aega!\nProovi uuesti või vali teine server. Ebasobilik server!\nVali mõni teine. Server ei vastanud!\nProovi uuesti või vali teine server. @@ -194,7 +187,6 @@ Nimi ei saa alata märgiga . Loon rahakotti Rahakott loodud - Rahakoti loomine ebaõnnestus Sisesta plokinumber või kuupäev (YYYY-MM-DD) @@ -279,7 +271,6 @@ OOTEL EBAÕNNESTUS - Makse ID (valikuline) Kogus Kommentaarid (valikuline) Rahakotti ei õnnestunud avada! @@ -290,8 +281,6 @@ Vähemalt 0 XMR pole arv - Küsi raha - Nüüd näidatakse tundlikku infot.\nPiilu oma seljataha! Olen turvalises kohas Vii mind tagasi! @@ -301,20 +290,12 @@ Täpselt nii! Ei, tänan! - - Tavaline tähtsus - Väike tähtsus - Keskmine tähtsus - Suur tähtsus - - Loo uus rahakott Taasta rahakott vaatamiseks Taasta rahakott privaatvõtmetest Taasta rahakott 25-sõnalisest seemnest Loo konto - Kontod Uus konto lisatud #%1$d Konto # @@ -336,7 +317,6 @@ Palun (taas)ühenda seade Ledger Loon kontot - Uuendan rahakotti %1$s lisatud %1$s eemaldatud @@ -355,5 +335,31 @@ Näita salajast infot Avalik režiim - Avalik režiim sisse lülitatud, puuduta lisainfo saamiseks. + + Node-o-matiC enabled, tap for more info. + Last block updated: %1$s + Nodes + Node Name (Optional) + Hostname + Port + Username (Optional) + Password (Optional) + Cannot resolve host + We need this! + Must be numeric + Must be 1–65535 + Add Node + Touch to refresh! + CONNECTION ERROR %1$d + CONNECTION ERROR + AUTHENTICATION FAILED + Test Result: + Height: %1$s (v%2$d), Ping: %3$.0fms, IP: %4$s + Testing IP: %1$s … + Please wait for scan to finish + Touch to select or add nodes + Add nodes manually or pull down to scan + Scanning network… + Automatically bookmarked best %1$d nodes + Test diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index b99f7a02..da656eaf 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -29,10 +29,8 @@ Fait Toucher pour le QR Code - Plus Prioritaire = Plus de Frais Paiement BTC activé, tapez pour plus d\'infos. - CrAzYpass activé, tapez pour plus d\'infos. Ledger activé, tapez pour plus d\'infos. %1$s BTC = %2$s XMR (Taux : %1$s BTC/XMR) - Avancé : - Visitez xmr.to pour l\'assistance & le suivi Clef Secrète\nXMR.TO Clef Secrète XMR.TO @@ -96,7 +92,6 @@ Sauvegarde en cours Archivage en cours Renommage en cours - Vérification de la connexion au démon Modification du mot de passe en cours Remise en ordre …\nCela peut prendre un moment ! @@ -108,13 +103,11 @@ Modification du mot de passe réussie Nœud - ([<utilisateur>:<mdp>@]<démon>[:<port>]) Chargement du Portefeuille … Portefeuille Sauvegardé Sauvegarde du Portefeuille Échouée ! Connexion … Connexion au nœud échouée !\nVérifiez utilisateur/mot de passe - Connexion au nœud sans réponse !\nEssayez à nouveau ou un autre. Nœud invalide !\nEssayez un autre. Nœud injoignable !\nEssayez à nouveau ou un autre. @@ -196,7 +189,6 @@ Ne peut pas commencer par . Création du Portefeuille Portefeuille créé - Création du Portefeuille échouée Entrer un nombre ou une date (YYYY-MM-DD) @@ -276,7 +268,6 @@ EN ATTENTE ÉCHOUÉ - ID de Paiement (optionnel) Montant Ouverture du portefeuille impossible ! 16 ou 64 caractères héxadécimaux (0–9,a–f) @@ -286,8 +277,6 @@ Min. 0 XMR pas un nombre - Recevoir - Des données sensible vont être affichées.\nRegardez autour de vous ! C\'est bon Non merci ! @@ -297,20 +286,12 @@ Oui, procéder ! Non merci ! - - Priority Défaut - Priority Faible - Priority Moyenne - Priority Élevée - - Créer un nouveau portefeuille Restaurer un portefeuille d\'audit Restaurer depuis la clef privée Restaurer depuis la phrase mnémonique Créer un Compte - Comptes Nouveau Compte #%1$d ajouté Compte # @@ -329,7 +310,6 @@ Merci de (re)connecter le Ledger Création du compte - Mise à jour du portefeuille %1$s connecté %1$s déconnecté @@ -359,5 +339,31 @@ Montrer les secrets ! Mode Urbain - Mode Urbain activé, tapez pour plus d\'infos. + + Node-o-matiC enabled, tap for more info. + Last block updated: %1$s + Nodes + Node Name (Optional) + Hostname + Port + Username (Optional) + Password (Optional) + Cannot resolve host + We need this! + Must be numeric + Must be 1–65535 + Add Node + Touch to refresh! + CONNECTION ERROR %1$d + CONNECTION ERROR + AUTHENTICATION FAILED + Test Result: + Height: %1$s (v%2$d), Ping: %3$.0fms, IP: %4$s + Testing IP: %1$s … + Please wait for scan to finish + Touch to select or add nodes + Add nodes manually or pull down to scan + Scanning network… + Automatically bookmarked best %1$d nodes + Test diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index a380c22a..84b3eb70 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -29,10 +29,8 @@ Kész Koppints a QR-kódért - Magasabb prioritás = magasabb tranzakciós díj BTC fizetés engedélyezve, koppints ide a részletekért. - CrAzYpass engedélyezve, koppints ide a részletekért Ledger engedélyezve, koppints ide a részletekért %1$s BTC = %2$s XMR (Arány: %1$s BTC/XMR) - Haladó: - Segítségért és nyomonkövetésért látogass el az XMR.TO weboldalra Titkos kulcs\nXMR.TO XMR.TO titkos kulcs @@ -95,7 +91,6 @@ Biztonsági mentés folyamatban Archiválás folyamatban Átnevezés folyamatban - Daemon-kapcsolat ellenőrzése Jelszómódosítás folyamatban Műveletek befejezése…\nEz eltarthat egy ideig! @@ -107,13 +102,11 @@ Jelszó megváltoztatva Csomópont - ([<felhasználó>:<jelszó>@]<daemon>[:<port>]) Tárca betöltése… Tárca mentve Sikertelen mentés! Kapcsolódás… Sikertelen kapcsolódás.\nEllenőrizd a felhasználónevet és jelszót. - A kapcsolódás időtúllépés miatt megszakadt.\nPróbáld újra vagy próbálkozz egy másikkal. Érvénytelen csomópont!\nPróbálkozz egy másikkal. Nem lehet elérni a csomópontot!\nPróbáld újra vagy próbálkozz egy másikkal. @@ -194,7 +187,6 @@ Nem kezdődhet ponttal. Tárca létrehozása Tárca létrehozva - Sikertelen tárcalétrehozás Számot vagy dátumot (ÉÉÉÉ-HH-NN) adj meg @@ -274,7 +266,6 @@ FÜGGŐBEN SIKERTELEN - Fizetési azonosító (opcionális) Mennyiség Nem sikerült megnyitni a tárcát! 16 vagy 64 hexadecimális karakter (0–9,a–f) @@ -284,8 +275,6 @@ Min. 0 XMR nem egy szám - Fogadás - Bizalmas adatok kerülnek megjelenítésre.\nBizonyosodj meg róla, hogy más nem látja! Mehet Inkább ne! @@ -295,20 +284,12 @@ Igen, mehet! Inkább ne! - - Alapértelmezett prioritás - Alacsony prioritás - Közepes prioritás - Magas prioritás - - Új tárca létrehozása Figyelőtárca visszaállítása Visszaállítás privát kulcsokkal Visszaállítás mnemonikus maggal Számla létrehozása - Számlák Új számla hozzáadva (#%1$d) Számla # @@ -330,7 +311,6 @@ Csatlakoztasd (újra) a Ledgert Számla létrehozása - Tárca frissítése %1$s csatlakoztatva %1$s leválasztva @@ -357,5 +337,31 @@ Részletek Street Mode - Street Mode enabled, tap for more info. + + Node-o-matiC enabled, tap for more info. + Last block updated: %1$s + Nodes + Node Name (Optional) + Hostname + Port + Username (Optional) + Password (Optional) + Cannot resolve host + We need this! + Must be numeric + Must be 1–65535 + Add Node + Touch to refresh! + CONNECTION ERROR %1$d + CONNECTION ERROR + AUTHENTICATION FAILED + Test Result: + Height: %1$s (v%2$d), Ping: %3$.0fms, IP: %4$s + Testing IP: %1$s … + Please wait for scan to finish + Touch to select or add nodes + Add nodes manually or pull down to scan + Scanning network… + Automatically bookmarked best %1$d nodes + Test diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index f323f08f..4c9bfa92 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -29,10 +29,8 @@ Fatto Tocca per il codice QR - Priorità più alta = Commissioni più alte Pagamento BTC abilitato, tocca per maggiori informazioni. - CrAzYpass abilitato, tocca per maggiori informazioni. Ledger abilitato, tocca per maggiori informazioni. %1$s BTC = %2$s XMR (Tasso: %1$s BTC/XMR) - Avanzate: - Visita xmr.to per supporto e tracciamento Chiave segreta\nXMR.TO Chiave segreta XMR.TO @@ -96,7 +92,6 @@ Backup in corso Archiviazione in corso Rinomina in corso - Controllando connessione con daemon Modifica password in corso Rimettendo le cose a posto …\nPuò richiedere del tempo! @@ -108,13 +103,11 @@ Password cambiata Nodo - ([<utente>:<password>@]<daemon>[:<porta>]) Caricando portafoglio … Portafoglio salvato Salvataggio portafoglio fallito! In connessione … Connessione con il nodo fallita!\nControlla username/password - Connessione con il nodo scaduta!\nProva di nuovo o provane un altro. Nodo invalido!\nProvane un altro. Impossibile raggiungere il nodo!\nProva di nuovo o provane un altro. @@ -195,7 +188,6 @@ Non può iniziare con . Portafoglio in creazione Portafoglio creato - Creazione del portafoglio fallita Inserisci un numero o una data (AAAA-MM-GG) @@ -275,7 +267,6 @@ IN ATTESA FALLITA - ID pagamento (opzionale) Ammontare Impossibile aprire il portafoglio! 16 o 64 caratteri esadecimali (0–9,a–f) @@ -285,8 +276,6 @@ Min. 0 XMR non è un numero - Ricevi - Verranno ora mostrati dati sensibili.\nGuardati alle spalle! Sono al sicuro Torna indietro! @@ -296,20 +285,12 @@ Sì, procedi! No grazie! - - Priority Default - Priority Bassa - Priority Media - Priority Alta - - Crea un nuovo portafoglio Recupera un portafoglio solo-visualizzazione Recupera un portafoglio dalle chiavi private Recupera un portafoglio da un seed di 25 parole Crea Account - Accounts Aggiunto nuovo account #%1$d Account # @@ -331,7 +312,6 @@ (Ri)connetti il dispositivo Ledger Creando account - Aggiornando portafoglio %1$s allegati %1$s allegati @@ -358,5 +338,31 @@ Mostra i segreti! Modalità strada - Modalità strada abilitata, tocca per maggiori informazioni. + + Node-o-matiC enabled, tap for more info. + Last block updated: %1$s + Nodes + Node Name (Optional) + Hostname + Port + Username (Optional) + Password (Optional) + Cannot resolve host + We need this! + Must be numeric + Must be 1–65535 + Add Node + Touch to refresh! + CONNECTION ERROR %1$d + CONNECTION ERROR + AUTHENTICATION FAILED + Test Result: + Height: %1$s (v%2$d), Ping: %3$.0fms, IP: %4$s + Testing IP: %1$s … + Please wait for scan to finish + Touch to select or add nodes + Add nodes manually or pull down to scan + Scanning network… + Automatically bookmarked best %1$d nodes + Test diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 0e651601..32cdfb16 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -29,10 +29,8 @@ Ferdig Trykk for QR kode - Høyere prioritet = høyere avgifter BTC betaling tilgjengelig, trykk for mer info. - CrAzYpass tilgjengelig, trykk for mer info. Ledger tilgjengelig, trykk for mer info. %1$s BTC = %2$s XMR (Rate: %1$s BTC/XMR) - Avansert: - Besøk xmr.to for støtte og sporing Hemmelig nøkkel\nXMR.TO XMR.TO Hemmelig nøkkel @@ -94,7 +90,6 @@ Backup pågår Arkivering pågår Nytt navn gis - Sjekker daemon-tilkobling [Passordforandring i gang] Gjør ting ferdig …\nDette kan ta si tid! @@ -106,13 +101,11 @@ [Passord forandra] Node - ([<bruker>:<pass>@]<daemon>[:<port>]) Laster lommebok … Lommebok lagra Lommeboklagring feila! Kobler til … Node-tiklobling feila!\nSjekk brukernavn/passord - Node-tilkobling tok for lang tid!\nPrøv på nytt eller med en annen. Noden er ugyldig!\nPrøv en annen. Kan ikke nå node!\nPrøv på nytt eller med en annen. @@ -193,7 +186,6 @@ Kan ikke begynne med . Lager lommebok Lommebok lagd - Klarte ikke å lage lommebok Skriv inn nummer eller dato (ÅÅÅÅ-MM-DD) @@ -273,7 +265,6 @@ VENTENDE FEILA - Betalings-ID (valgfritt) Mengde Kunne ikke åpne lommebok! 16 eller 64 heks-karakterer (0–9,a–f) @@ -283,8 +274,6 @@ Min. 0 XMR ikke et tall - Motta - Sensitive data vil nå bli vist.\nSe over skulderen din! Jeg er trygg Ta meg tilbake! @@ -294,20 +283,12 @@ Ja, gjør det! Nei takk! - - Prioritet Standard - Prioritet Lav - Prioritet Middels - Prioritet Høy - - Lag ny lommebok Gjenoprett bare-se lommebok Gjenoprett lommebok fra private nøkler Gjenoprett lommebok fra 25-ord seed Create Account - Accounts Added new account #%1$d Account # @@ -329,7 +310,6 @@ Please (re)connect Ledger device Creating account - Updating wallet %1$s attached %1$s detached @@ -356,5 +336,31 @@ Detaljer Street Mode - Street Mode enabled, tap for more info. + + Node-o-matiC enabled, tap for more info. + Last block updated: %1$s + Nodes + Node Name (Optional) + Hostname + Port + Username (Optional) + Password (Optional) + Cannot resolve host + We need this! + Must be numeric + Must be 1–65535 + Add Node + Touch to refresh! + CONNECTION ERROR %1$d + CONNECTION ERROR + AUTHENTICATION FAILED + Test Result: + Height: %1$s (v%2$d), Ping: %3$.0fms, IP: %4$s + Testing IP: %1$s … + Please wait for scan to finish + Touch to select or add nodes + Add nodes manually or pull down to scan + Scanning network… + Automatically bookmarked best %1$d nodes + Test diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 8bf3c6d7..edf8129a 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -29,10 +29,8 @@ Klaar Tik voor QR-code - Hogere prioriteit = hogere kosten BTC-betaling ingeschakeld. Tik voor meer info. - CrAzYpass ingeschakeld. Tik voor meer info. Ledger ingeschakeld. Tik voor meer info. %1$s BTC = %2$s XMR (Koers: %1$s BTC/XMR) - Geavanceerd: - Ga naar xmr.to voor hulp en traceren Geheime sleutel\nXMR.TO Geheime sleutel XMR.TO @@ -95,7 +91,6 @@ Back-up wordt gemaakt Wordt gearchiveerd Naam wordt gewijzigd - Verbinding met node controleren Wachtwoord wordt gewijzigd Bezig met voltooien…\nDit kan even duren. @@ -107,13 +102,11 @@ Wachtwoord is gewijzigd Node - ([<user>:<pass>@]<daemon>[:<port>]) Portemonnee laden… Portemonnee opgeslagen Portemonnee opslaan mislukt! Verbinden… Geen verbinding met node.\nControleer naam/wachtwoord. - Time-out verbinding met node.\nProbeer opnieuw of andere. Node ongeldig.\nProbeer een andere. Kan node niet bereiken.\nProbeer opnieuw of andere. @@ -191,7 +184,6 @@ Mag niet beginnen met . Portemonnee wordt gemaakt Portemonnee is gemaakt - Portemonnee maken mislukt! Voer getal of datum (JJJJ-MM-DD) in @@ -272,7 +264,6 @@ WACHTEN MISLUKT - Betalings-ID (optioneel) Bedrag Kan portemonnee niet openen! 16 of 64 hexadecimale tekens (0–9, a–f) @@ -282,8 +273,6 @@ Min. 0 XMR geen getal - Ontvangen - Nu worden vertrouwelijke gegevens getoond.\nKijk achter je! Het is veilig. Ga terug! @@ -293,20 +282,12 @@ Ja, doe dat! Nee, niet doen! - - Prioriteit Normaal - Prioriteit Laag - Prioriteit Normaal - Prioriteit Hoog - - Nieuwe portemonnee Alleen-lezen portemonnee herstellen Portemonnee herstellen met privésleutels Voer je 25 woorden in Account maken - Accounts Nieuw account #%1$d toegevoegd Accountnr. @@ -328,7 +309,6 @@ Maak (opnieuw) verbinding met Ledger Account maken… - Portemonnee bijwerken… %1$s gekoppeld %1$s losgemaakt @@ -354,5 +334,31 @@ Details Street Mode - Street Mode enabled, tap for more info. + + Node-o-matiC enabled, tap for more info. + Last block updated: %1$s + Nodes + Node Name (Optional) + Hostname + Port + Username (Optional) + Password (Optional) + Cannot resolve host + We need this! + Must be numeric + Must be 1–65535 + Add Node + Touch to refresh! + CONNECTION ERROR %1$d + CONNECTION ERROR + AUTHENTICATION FAILED + Test Result: + Height: %1$s (v%2$d), Ping: %3$.0fms, IP: %4$s + Testing IP: %1$s … + Please wait for scan to finish + Touch to select or add nodes + Add nodes manually or pull down to scan + Scanning network… + Automatically bookmarked best %1$d nodes + Test diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 4fcced95..e316346f 100755 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -29,10 +29,8 @@ Concluído Toque para ver o código QR - Maior Prioridade = Maiores Taxas Pagamento em BTC ativado, toque para mais informações. - SeNHaLoUCa ativada, toque para mais informações. Ledger ativada, toque para mais informações. %1$s BTC = %2$s XMR (Cotação: %1$s BTC/XMR) - Avançado: - Visite o XMR.TO para suporte & rastreio da ordem Chave secreta\nXMR.TO Chave secreta XMR.TO @@ -95,7 +91,6 @@ Criando backup Arquivando Renomeando - Verificando conexão Alterando senha Finalizando o processo …\nPode demorar um pouco @@ -107,14 +102,12 @@ Senha alterada - ([<usuário>:<senha>@]<daemon>[:<porta>]) Carregando … Carteira salva Erro ao salvar carteira Conectando … Erro ao conectar!\Verifique o usuário e senha Versão do nó incompatível - por favor faça a atualização! - Tempo limite de conexão ao nó!\nTente novamente ou escolha outro. Nó inválido!\nEscolha outro. Não foi possível conectar ao nó!\nTente novamente ou escolha outro. @@ -193,7 +186,6 @@ Não pode começar com . Criando carteira Carteira criada - Erro ao criar carteira Escreva a altura ou data (AAAA-MM-DD) @@ -274,7 +266,6 @@ PENDENTE FALHOU - ID do Pagamento (opcional) Valor Não foi possível abrir a carteira! 16 ou 64 caracteres em hexadecimal (0–9,a–f) @@ -284,8 +275,6 @@ Mín. 0 O valor não é numérico - Receber - Dados importantes serão exibidos.\nCertifique-se que ninguém está espiando! Estou seguro Tire-me daqui! @@ -295,20 +284,12 @@ Sim, pode fazer! Não, obrigado! - - Prioridade padrão - Prioridade baixa - Prioridade média - Prioridade alta - - Criar nova carteira Restaurar carteira \"Somente leitura\" Restaurar carteira via chaves privadas Restaurar carteira via semente mnemônica Criar conta - Contas Conta adicionada #%1$d Conta # @@ -330,7 +311,6 @@ Favor (re)conectar a Ledger Criando conta - Atualizando carteira %1$s conectado %1$s desconectado @@ -347,5 +327,31 @@ Detalhes Street Mode - Street Mode enabled, tap for more info. + + Node-o-matiC enabled, tap for more info. + Last block updated: %1$s + Nodes + Node Name (Optional) + Hostname + Port + Username (Optional) + Password (Optional) + Cannot resolve host + We need this! + Must be numeric + Must be 1–65535 + Add Node + Touch to refresh! + CONNECTION ERROR %1$d + CONNECTION ERROR + AUTHENTICATION FAILED + Test Result: + Height: %1$s (v%2$d), Ping: %3$.0fms, IP: %4$s + Testing IP: %1$s … + Please wait for scan to finish + Touch to select or add nodes + Add nodes manually or pull down to scan + Scanning network… + Automatically bookmarked best %1$d nodes + Test diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 87cf9b72..b8c2ab44 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -29,10 +29,8 @@ Feito Toca para código QR - Prioridade Alta = Taxas Altas Pagamento em BTC activado, toca para mais informação. - passLoUCa activa, toca para mais informação. Ledger activa, toca para mais informação. %1$s BTC = %2$s XMR (Rácio: %1$s BTC/XMR) - Avançado: - Vai a xmr.to para suporte & seguimento Chave secreta\nXMR.TO XMR.TO Chave secreta @@ -95,7 +91,6 @@ A efectuar cópia de segurança A arquivar A renomear - A verificar a conecção A altear a palavra passe A concluir o processamento …\nIsto pode demorar! @@ -107,13 +102,11 @@ Palavra passe alterada - ([<utilizador>:<passe>@]<serviço>[:<porta>]) A carregar a carteira … Carteira guardada Erro ao gravar a carteira! A conectar … Erro ao conectar!\nConfirma o utilizador/palavra passe - O tempo para a conecção expirou!\nTenta novamente ou outro nó. Nó inválido!\nTenta outro. Não foi possível chegar ao nó!\nTenta novamente ou outro. @@ -193,7 +186,6 @@ Não pode começar com . A criar carteira Carteira criada - Erro ao criar a carteira Introduzir número ou data (AAAA-MM-DD) @@ -273,7 +265,6 @@ PENDENTE FALHADA - ID Pagamento (opcional) Quantidade Não foi possível abrir a carteira! 16 ou 64 caracteres em hexadecimal (0–9,a–f) @@ -283,8 +274,6 @@ Mín. 0 XMR não é um número - Receber - Dados sensíveis vão ser mostrados.\nOlha à tua volta! Estou seguro Volta atrás! @@ -294,13 +283,6 @@ Sim, faz isso! Não obrigado! - - Prioridade por Defeito - Prioridade Baixa - Prioridade Média - Prioridade Alta - - Criar nova carteira Restaurar carteira apenas de visualização Restaurar carteira a partir de chaves privadas @@ -310,7 +292,6 @@ Opening the wallet… Create Account - Accounts Added new account #%1$d Account # @@ -332,7 +313,6 @@ Please (re)connect Ledger device Creating account - Updating wallet %1$s attached %1$s detached @@ -359,5 +339,31 @@ Detalhes Street Mode - Street Mode enabled, tap for more info. + + Node-o-matiC enabled, tap for more info. + Last block updated: %1$s + Nodes + Node Name (Optional) + Hostname + Port + Username (Optional) + Password (Optional) + Cannot resolve host + We need this! + Must be numeric + Must be 1–65535 + Add Node + Touch to refresh! + CONNECTION ERROR %1$d + CONNECTION ERROR + AUTHENTICATION FAILED + Test Result: + Height: %1$s (v%2$d), Ping: %3$.0fms, IP: %4$s + Testing IP: %1$s … + Please wait for scan to finish + Touch to select or add nodes + Add nodes manually or pull down to scan + Scanning network… + Automatically bookmarked best %1$d nodes + Test diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 9dcd1e90..8e27c99c 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -28,10 +28,8 @@ Gata Atinge pentru codul QR - Prioritate mare = Comision mare Plată BTC activată, apasă pentru mai multe informații. - CrAzYpass activată, apasă pentru mai multe informații. Ledger activată, apasă pentru mai multe informații. %1$s BTC = %2$s XMR (Rată: %1$s BTC/XMR) - Avansat: - Vizitează xmr.to pentru suport & interogare Cheia secretă \nXMR.TO Cheia secretă XMR.TO @@ -94,7 +90,6 @@ Copie de rezervă în curs Arhivare în curs Redenumire în curd - Se verifică conexiunea cu nodul Impachetăm lucrurile …\nPoate dura un pic! @@ -103,13 +98,11 @@ Redenumirea a eșuat! Nodul - ([<user>:<pass>@]<daemon>[:<port>]) Se încarcă portofelul … Portofelul a fost salvat Salvarea portofelului a eșuat! Se conecteaza … Conexiunea la nod a eșuat!\nVerifică utilizator/parolă - Conexiunea la nod a expirat!\nÎncearca din nou sau un altul. Nodul este invalid!\nÎncearcă altul. Nu se poate ajunge la nod!\nÎncearca din nou sau un altul. @@ -169,7 +162,6 @@ Nu pot începe cu . Se creează portofel Portofel creat - Crearea portofelului nereușită Introdu număr sau dată (YYYY-MM-ZZ) @@ -248,7 +240,6 @@ ÎN CURS EȘUAT - Payment ID (facultativ) Sumă Nu pot deschide portofel! 16 sau 64 caractere Hex (0–9,a–f) @@ -258,8 +249,6 @@ Min. 0 XMR fără valoare - Primește - Acum vor fi afișate date sensibile.\nUită-te peste umărul tău! Sunt în siguranță Du-mă înapoi! @@ -269,13 +258,6 @@ Da, fă asta! Nu mersi! - - Prioritate Implicită - Prioritate mică - Prioritate medie - Prioritate mare - - Creeaza portofel nou Restaurează portofel-vizualizare Restaurează portofel folosind cheie privată @@ -308,7 +290,6 @@ Passphrase may not be empty Wallet Files Restore Password Create Account - Accounts Added new account #%1$d Account # Send all confirmed funds in this account! @@ -329,7 +310,6 @@ Please (re)connect Ledger device Creating account - Updating wallet %1$s attached %1$s detached @@ -356,5 +336,31 @@ Detalii Street Mode - Street Mode enabled, tap for more info. + + Node-o-matiC enabled, tap for more info. + Last block updated: %1$s + Nodes + Node Name (Optional) + Hostname + Port + Username (Optional) + Password (Optional) + Cannot resolve host + We need this! + Must be numeric + Must be 1–65535 + Add Node + Touch to refresh! + CONNECTION ERROR %1$d + CONNECTION ERROR + AUTHENTICATION FAILED + Test Result: + Height: %1$s (v%2$d), Ping: %3$.0fms, IP: %4$s + Testing IP: %1$s … + Please wait for scan to finish + Touch to select or add nodes + Add nodes manually or pull down to scan + Scanning network… + Automatically bookmarked best %1$d nodes + Test diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index f30c733b..b7e2f255 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -29,10 +29,8 @@ Готово Нажмите для использования QR-кода - Высокий приоритет = Высокие комиссии Доступны переводы в BTC, нажмите для доп. информации - Доступен CrAzYpass, нажмите для доп. информации Доступен Ledger, нажмите для доп. информации %1$s BTC = %2$s XMR (Курс: %1$s BTC/XMR) - Дополнительно: - Посетите xmr.to для получения помощи & Секретный ключ\nXMR.TO XMR.TO секретный ключ @@ -96,7 +92,6 @@ Выполняется резервное копирование Выполняется архивирование Выполняется переименование - Проверка подключения Выполняется изменение пароля Для данного действия …\nМожет потребоваться некоторое время! @@ -108,13 +103,11 @@ Изменение пароля выполено успешно Удаленный узел - ([<user>:<pass>@]<daemon>[:<port>]) Загрузка кошелека … Кошелек записан Ошибка записи кошелека! Подключение … Ошибка подключения к удаленному узлу!\nПроверьте username/password - Время ожидания соединения превышено!\nПопробуйте еще раз. Ошибка удаленного узла!\nПопробуйте еще раз. Не удается подключится к удаленному узлу!\nПопробуйте позже. @@ -195,7 +188,6 @@ Не может начинаться с . Создание кошелька Кошелек создан - Не удалось создать кошелек Введите номер блока или дату (YYYY-MM-DD) @@ -275,7 +267,6 @@ ОЖИДАНИЕ ОШИБКА - ID платежа (необязательно) Сумма Не удалось открыть кошелек! Система счисления - 16 или 64(0–9,a–f) @@ -285,8 +276,6 @@ Min. 0 Не числовое значение XMR - Получить - Сейчас будут показаны конфиденциальные данные. Оглянись вокруг! Я в безопасности Верните меня обратно! @@ -296,20 +285,12 @@ Да, сделай это! Нет, спасибо! - - Приоритет - Стандартный - Приоритет - Низкий - Приоритет - Средний - Приоритет - Высокий - - Создать новый кошелек Восстановить кошелек только для просмотра Восстановить кошелек из ключей Восстановить кошелек из мнемонической фразы Создать учетную запись - Учетная запись Добавить новую учетную запись #%1$d Учетная запись # @@ -331,7 +312,6 @@ Подключите (переподключите) устройство Ledger Создание аккаунта - Обновление кошелька %1$s прикреплен %1$s откреплён @@ -358,5 +338,31 @@ Информация Street Mode - Street Mode enabled, tap for more info. + + Node-o-matiC enabled, tap for more info. + Last block updated: %1$s + Nodes + Node Name (Optional) + Hostname + Port + Username (Optional) + Password (Optional) + Cannot resolve host + We need this! + Must be numeric + Must be 1–65535 + Add Node + Touch to refresh! + CONNECTION ERROR %1$d + CONNECTION ERROR + AUTHENTICATION FAILED + Test Result: + Height: %1$s (v%2$d), Ping: %3$.0fms, IP: %4$s + Testing IP: %1$s … + Please wait for scan to finish + Touch to select or add nodes + Add nodes manually or pull down to scan + Scanning network… + Automatically bookmarked best %1$d nodes + Test diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 8a7f287d..2ac8c872 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -29,10 +29,8 @@ Hotovo Klepni pre QR kód - Vyššia priorita = Vyšší poplatok BTC platby aktivované, klepni pre viac info. - CrAzYpass aktivovaný, klepni pre viac info. Ledger aktivovaný, klepni pre viac info. %1$s BTC = %2$s XMR (Kurz: %1$s BTC/XMR) - Pokročilé: - Navštív XMR.TO pre podporu & tracking Tajný kľúč\nXMR.TO XMR.TO Tajný kľúč @@ -96,7 +92,6 @@ Prebieha Záloha Prebieha Archivácia Prebieha Premenovanie - Skúšam spojenie so serverom Prebieha zmena hesla Balím si veci …\nMôže to chvíľu trvať! @@ -108,13 +103,11 @@ Heslo zmenené Uzol - ([<meno>:<heslo>@]<server>[:<port>]) Načítavam peňaženku … Peňaženka uložená Uloženie zlyhalo! pripájam sa … Spojenie s uzlom zlyhalo!\nSkontroluj užívateľa/heslo - Pripojenie uzla vypršalo!\nSkús znova, alebo skús iný uzol. Neplatný uzol!\nSkús iný. Neviem sa spojiť s uzlom!\nSkús znova, alebo skús iný uzol. @@ -192,7 +185,6 @@ Neslobodno začať s . Vytváranie peňaženky Peňaženka vytvorená - Vytvorenie peňaženky zlyhalo Vlož číslo alebo dátum (YYYY-MM-DD) @@ -272,7 +264,6 @@ ČAKAJÚCI ZLYHAL - ID Platby (voliteľné) Suma Nemohol som otvoriť peňaženku! 16 alebo 64 hexa znakov (0–9,a–f) @@ -282,8 +273,6 @@ Min. 0 XMR nie číslo - Prijať - Budú zobrazené citlivé dáta.\nPozri cez plece! Som v bezpečí Naspäť! @@ -293,20 +282,12 @@ Áno, poďme na to! Nie, díky! - - Priorita Štandard - Priorita Nízka - Priorita Medium - Priorita Vysoká - - Vytvor novú peňaženku Obnoviť prezeraciu peňaženku Obnoviť peňaženku zo súkromných kľúčov Obnoviť peňaženku z 25-slovného seedu Vytvor Účet - Účty Pridaný nový účet #%1$d Účet # @@ -328,7 +309,6 @@ (znovu) zapoj Ledger Vytváram účet - Aktualizujem peňaženku %1$s pripojený %1$s odpojený @@ -350,11 +330,36 @@ Prekladám OpenAlias… OpenAlias bez DNSSEC - adresa môže byť zneužitá Prijímateľova XMR/BTC adresa alebo OpenAlias - - vyžaduje sa uzol V9 + Nekompatibilný s verziou uzla - nutná aktualizácia! Detaily Street Mode - Street Mode enabled, tap for more info. + + Node-o-matiC enabled, tap for more info. + Last block updated: %1$s + Nodes + Node Name (Optional) + Hostname + Port + Username (Optional) + Password (Optional) + Cannot resolve host + We need this! + Must be numeric + Must be 1–65535 + Add Node + Touch to refresh! + CONNECTION ERROR %1$d + CONNECTION ERROR + AUTHENTICATION FAILED + Test Result: + Height: %1$s (v%2$d), Ping: %3$.0fms, IP: %4$s + Testing IP: %1$s … + Please wait for scan to finish + Touch to select or add nodes + Add nodes manually or pull down to scan + Scanning network… + Automatically bookmarked best %1$d nodes + Test diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 2482ae6e..2600a7e8 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -30,10 +30,8 @@ Färdig Tryck för QR-kod - Högre prioritet = Högre avgift BTC-betalning aktiverad, tryck för mer info. - CrAzYpass aktiverat, tryck för mer info. Ledger aktiverat, tryck för mer info. Du har angivit en Bitcoin-adress.
Du kommer att skicka XMR och mottagaren får BTC via tjänsten XMR.TO.]]>
@@ -72,8 +70,6 @@ %1$s BTC = %2$s XMR (Kurs: %1$s BTC/XMR) - Avancerat: - Besök xmr.to för support & spårning Hemlig nyckel\nXMR.TO XMR.TO hemlig nyckel @@ -88,7 +84,6 @@ Säkerhetskopiering pågår Arkivering pågår Namnbyte pågår - Kontrollerar anslutning till daemon Ändring av lösenord pågår Gör klart några saker …\nDetta kan ta lite tid! @@ -100,13 +95,11 @@ Lösenordet har ändrats Nod - ([<användare>:<lösenord>@]<daemon>[:<port>]) Läser in plånbok … Plånbok sparad Det gick inte att spara plånbok! Ansluter … Kunde inte ansluta till nod!\nKontrollera användarnamn/lösenord - Fel: Anslutning till nod uppnådde en tidsgräns!\nFörsök igen eller en annan nod. Ogiltig nod!\nPröva någon annan. Noden kan inte nås!\nFörsök igen eller en annan nod. @@ -174,7 +167,6 @@ Får inte börja med . Skapar plånbok Plånboken skapades - Det gick inte att skapa plånbok Ange ett tal eller ett datum (ÅÅÅÅ-MM-DD) @@ -254,7 +246,6 @@ VÄNTANDE MISSLYCKAD - Betalnings-ID (valfritt) Belopp Det gick inte att öppna plånboken! 16 eller 64 hexadecimala tecken (0–9, a–f) @@ -264,8 +255,6 @@ Min. 0 XMR är inte ett tal - Ta emot - Nu kommer känsliga data att visas.\nTitta över axeln! Det är OK Ta mig tillbaka! @@ -275,13 +264,6 @@ Ja, gör det! Nej tack! - - Prioritet standard - Prioritet låg - Prioritet medel - Prioritet hög - - Skapa ny plånbok Återställ granskningsplånbok Återställ plånbok från privata nycklar @@ -291,7 +273,6 @@ Det sparade lösenordet är inkorrekt.\nSkriv lösenordet manuellt. Skapa konto - Konton Nytt konto skapat #%1$d Konto # @@ -313,7 +294,6 @@ Vänligen (åter)anslut Ledger Skapar konto - Uppdaterar blånbok %1$s ansluten %1$s frånkopplad @@ -340,5 +320,31 @@ Detaljer Street Mode - Street Mode enabled, tap for more info. + + Node-o-matiC enabled, tap for more info. + Last block updated: %1$s + Nodes + Node Name (Optional) + Hostname + Port + Username (Optional) + Password (Optional) + Cannot resolve host + We need this! + Must be numeric + Must be 1–65535 + Add Node + Touch to refresh! + CONNECTION ERROR %1$d + CONNECTION ERROR + AUTHENTICATION FAILED + Test Result: + Height: %1$s (v%2$d), Ping: %3$.0fms, IP: %4$s + Testing IP: %1$s … + Please wait for scan to finish + Touch to select or add nodes + Add nodes manually or pull down to scan + Scanning network… + Automatically bookmarked best %1$d nodes + Test diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 41f97b5b..74866931 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -29,10 +29,8 @@ 完成 点选显示QR码 - 较高优先权 = 较高手续费 BTC付款已启用, 点选了解更多 - CrAzYpass已启用, 点选了解更多 Ledger已启用, 点选了解更多 %1$s BTC = %2$s XMR (汇率: %1$s BTC/XMR) - 高级选项: - 参访 xmr.to 以获得支援及追踪交易 私钥\nXMR.TO XMR.TO 私钥 @@ -95,7 +91,6 @@ 备份中 封存中 重新命名中 - 检查背景程序连网中 更改密码中 处理中 …\n可能会花费一点时间! @@ -107,13 +102,11 @@ 密码更改成功 节点 - ([<使用者>:<密码>@]<位置>[:<port>]) 载入钱包中 … 钱包已储存 储存钱包失败! 连接中 … 连接至节点失败!\n请检查使用者/密码。 - 节点连接逾时!\n请重试或更換节点。 节点无效!\n请试试其他节点。 无法连接至节点!\n请试试其他节点。 @@ -191,7 +184,6 @@ 无法使用.作为开头 建立钱包 钱包已建立 - 钱包建立失败 输入区块高度或日期(YYYY-MM-DD) @@ -271,7 +263,6 @@ 等待确认中 失败 - 付款ID (选填) 金额 无法开启钱包! 16 或 64 Hex 字元 (0–9,a–f) @@ -281,8 +272,6 @@ 最小值 0 输入的XMR不是数字 - 接收 - 将会选示敏感信息。\n请注意周遭安全! 我现在很安全 不看了! @@ -292,20 +281,12 @@ 好的! 不了! - - 预设优先权 - 低优先权 - 中优先权 - 高优先权 - - 建立新钱包 恢复只读钱包 从私钥恢复钱包 从25字种子码恢复钱包 开新户口 - 户口 已新增户口 #%1$d 户口 # @@ -327,7 +308,6 @@ Please (re)connect Ledger device Creating account - Updating wallet %1$s attached %1$s detached @@ -354,5 +334,31 @@ 详细信息 Street Mode - Street Mode enabled, tap for more info. + + Node-o-matiC enabled, tap for more info. + Last block updated: %1$s + Nodes + Node Name (Optional) + Hostname + Port + Username (Optional) + Password (Optional) + Cannot resolve host + We need this! + Must be numeric + Must be 1–65535 + Add Node + Touch to refresh! + CONNECTION ERROR %1$d + CONNECTION ERROR + AUTHENTICATION FAILED + Test Result: + Height: %1$s (v%2$d), Ping: %3$.0fms, IP: %4$s + Testing IP: %1$s … + Please wait for scan to finish + Touch to select or add nodes + Add nodes manually or pull down to scan + Scanning network… + Automatically bookmarked best %1$d nodes + Test diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index ba4decd8..8b9a6655 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -29,10 +29,8 @@ 完成 點選顯示 QR 碼 - 較高優先權 = 較高手續費 BTC 付款已啟用,點選了解更多 - CrAzYpass 已啟用,點選了解更多 Ledger 已啟用,點選了解更多 %1$s BTC = %2$s XMR (匯率:%1$s BTC/XMR) - 進階: - 參訪 XMR.TO 以獲得支援及追蹤交易 私鑰\nXMR.TO XMR.TO 私鑰 @@ -96,7 +92,6 @@ 備份中 封存中 重新命名中 - 檢查背景程式連線中 更改密碼中 處理中 …\n可能需要些許時間! @@ -108,13 +103,11 @@ 密碼更改成功 節點 - ([<使用者>:<密碼>@]<位置>[:<port>]) 載入錢包中 … 錢包已儲存 儲存錢包失敗! 連接中 … 連接至節點失敗!\n請檢查使用者/密碼。 - 節點連接逾時!\n請重試或更換節點。 節點無效!\n請試試其他節點。 無法連接至節點!\n請試試其他節點。 @@ -192,7 +185,6 @@ 無法用.作為開頭 建立錢包 錢包已建立 - 錢包建立失敗 輸入區塊高度或日期 (YYYY-MM-DD) @@ -272,7 +264,6 @@ 等待確認中 失敗 - 付款 ID (選填) 金額 無法開啟錢包! 16 或 64 Hex 字元 (0–9,a–f) @@ -282,8 +273,6 @@ 最小值 0 輸入的 XMR 不是數字 - 接收 - 將會顯示敏感資訊。\n請注意周遭安全! 我現在很安全 不看了! @@ -293,20 +282,12 @@ 好的! 不了! - - 預設優先權 - 低優先權 - 中優先權 - 高優先權 - - 建立新錢包 回復唯讀錢包 從私鑰回復錢包 從 25 字種子碼回復錢包 新增帳戶 - 帳戶 已新增帳戶 #%1$d 帳戶 # @@ -328,7 +309,6 @@ 請(重新)連接 Ledger 裝置 正在建立帳戶 - 正在更新帳戶 %1$s 已連接 %1$s 已斷開連接 @@ -355,5 +335,31 @@ 詳細資訊 Street Mode - Street Mode enabled, tap for more info. + + Node-o-matiC enabled, tap for more info. + Last block updated: %1$s + Nodes + Node Name (Optional) + Hostname + Port + Username (Optional) + Password (Optional) + Cannot resolve host + We need this! + Must be numeric + Must be 1–65535 + Add Node + Touch to refresh! + CONNECTION ERROR %1$d + CONNECTION ERROR + AUTHENTICATION FAILED + Test Result: + Height: %1$s (v%2$d), Ping: %3$.0fms, IP: %4$s + Testing IP: %1$s … + Please wait for scan to finish + Touch to select or add nodes + Add nodes manually or pull down to scan + Scanning network… + Automatically bookmarked best %1$d nodes + Test diff --git a/app/src/main/res/values/help.xml b/app/src/main/res/values/help.xml index ea9aa54b..fb8f69ee 100644 --- a/app/src/main/res/values/help.xml +++ b/app/src/main/res/values/help.xml @@ -216,16 +216,6 @@ previous step and then coming back to the \"Confirm\" screen.

]]> - October 2018 Network Upgrade -

On or around 18th October 2018, the Monero Network will be upgraded. Clients and Servers - (Nodes) need to be upgraded simultaneously as old & new versions are not compatible.

-

You are running a new V9 client. You need to run or find a V9 node to connect to.

-

Check - https://www.reddit.com/r/Monerujo/comments/9n70ys/does_anyone_have_a_list_of_updated_nodes/ - for more info & support.

- ]]>
- The Wallet

Street Mode

@@ -247,4 +237,53 @@

Transaction List

A list of the wallet transactions. In view wallets, only incoming transactions are shown.

]]>
+ + Nodes +

TL;DR

+

Refresh the nodes list by pulling down & bookmark 3–5 nodes to allow Monerujo + to choose the best one for you!

+

What's a Node?

+

Monerujo uses a Remote Node (sometimes also called Daemon) to communicate with + the Monero Network without having to download and store a copy of the + whole blockchain itself.

+

Node List

+

If the list is empty, you can either add new nodes manually or let Monerujo + scan the network for you. Or both. Read on…

+

The node list shows all currently known nodes. Additionally, the timestamp + of the latest block known to each node is shown under the node name. An icon + representing the node's response behaviour + (which indicates the level of connectivity to be expected) + is shown next to each node.

+

Any node in the list can be bookmarked for later use. + Nodes which are not bookmarked will be forgotten.

+

Monerujo will choose the optimal bookmarked node each time you use it. + It does this by checking the blockheight (how up-to-date + is the node?) as well as the response behaviour (how fast does the node respond to requests?).

+

The list is sorted by these characteristics, so the top node would be the one Monerujo + would choose right now. The bottom of the list would show very slow or unavailable nodes.

+

Add a Node

+

By touching the "Add Node" button at the bottom, you will be asked to + enter the node details in the following dialog. + The "Address" is the hostname or IP-address of the node - this is the only + mandatory entry. + Enter the "Port" if the node runs on a non-default port (e.g. 18089). + You can also optionally name the node, so you can identify it easier later on. + Some nodes require credentials to use them. Enter the provided username & + password in the appropriate fields. Now you can "Test" these setting. + The "Test Results" will display the blockheight, response time and actual IP used. + The result may also be an error - usually because the hostname provided is + not reachable in a sensible amount of time or the credentials are incorrect. + Or the hostname/port combination does not point to an actual Monero Node! + Once the test passes (no error) - you're set to press "OK" to save & + bookmark this node.

+

Scan for Nodes

+

Additionally, you can scan the network for nodes. Monerujo will start + scanning the network for Remote Nodes on port 18089. It begins by asking your + bookmarked nodes for other peers in the Monero P2P network and then continues + by asking those for their peers, and so on. If you have no bookmarked nodes + (or they don't tell us about their peers), + Monerujo will go straight to the Monero seed nodes hardcoded into Monero. The + scan stops when it finds 10 remote nodes in total.

+ ]]>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0665913e..52f0dd4c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,7 +2,6 @@ monerujo Wallet - Stagenet About Privacy Policy @@ -31,10 +30,8 @@ Done Touch for QR Code - Higher Priority = Higher Fees BTC payment enabled, tap for more info. - CrAzYpass enabled, tap for more info. Ledger enabled, tap for more info. %1$s BTC = %2$s XMR (Rate: %1$s BTC/XMR) - Advanced: - Visit xmr.to for support & tracking Secret Key\nXMR.TO XMR.TO Secret Key @@ -97,7 +92,6 @@ Backup in progress Archive in progress Rename in progress - Checking daemon connection Change Password in progress Wrapping things up …\nThis can take a while! @@ -109,17 +103,14 @@ Password changed Node - ([<user>:<pass>@]<daemon>[:<port>]) Stagenet Testnet - Mainnet Loading Wallet … Wallet saved Wallet save failed! Connecting … Node connection failed!\nCheck username/password Node version incompatible - please upgrade! - Node connection timed out!\nTry again or another. Node invalid!\nTry another. Cannot reach node!\nTry again or another. @@ -143,7 +134,7 @@ Incorrect password! Saved password is incorrect.\nPlease enter password manually. Wallet does not exist! - Daemon address must be set! + Node must be set! Wallet does not match selected net (Watch Only) @@ -200,7 +191,6 @@ Cannot begin with . Creating wallet Wallet created - Wallet create failed Enter Number or Date (YYYY-MM-DD) @@ -287,7 +277,6 @@ PENDING FAILED - Payment ID (optional) Amount Description (optional) Could not open wallet! @@ -298,8 +287,6 @@ Min. 0 XMR not a number - Receive - Sensitive data will now be shown.\nLook over your shoulder! I\'m safe Take me back! @@ -309,13 +296,6 @@ Yes, do that! No thanks! - - Priority Default - Priority Low - Priority Medium - Priority High - - XMR EUR @@ -362,7 +342,6 @@ (%1$d, %2$d) Create Account - Accounts Added new account #%1$d Account # @@ -384,7 +363,6 @@ Please (re)connect Ledger device Creating account - Updating wallet %1$s attached %1$s detached @@ -399,12 +377,37 @@ Reading Tag successful NFC Available! - V9 node required - \?\?\? - Street Mode Show Secrets! Street Mode - Street Mode enabled, tap for more info. + + Node scanning only for mainnet! + + Node-o-matiC enabled, tap for more info. + Last block updated: %1$s + Nodes + Node Name (Optional) + Hostname + Port + Username (Optional) + Password (Optional) + Cannot resolve host + We need this! + Must be numeric + Must be 1–65535 + Add Node + Touch to refresh! + CONNECTION ERROR %1$d + CONNECTION ERROR + AUTHENTICATION FAILED + Test Result: + Height: %1$s (v%2$d), Ping: %3$.0fms, IP: %4$s + Testing IP: %1$s … + Please wait for scan to finish + Touch to select or add nodes + Add nodes manually or pull down to scan + Scanning network… + Automatically bookmarked best %1$d nodes + Test diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 9aa7aeb7..161e2cd2 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -60,14 +60,6 @@ @color/moneroBlack - - - - - - - - @@ -208,10 +191,6 @@ 12sp - - @@ -254,13 +233,6 @@ 11sp - - - - @@ -274,31 +246,6 @@ @color/gradientPink - - - - - - - - - - - - - diff --git a/app/src/test/java/com/m2049r/xmrwallet/service/exchange/coinmarketcap/ExchangeRateTest.java b/app/src/test/java/com/m2049r/xmrwallet/service/exchange/coinmarketcap/ExchangeRateTest.java index 2b7baf5a..76ed1417 100644 --- a/app/src/test/java/com/m2049r/xmrwallet/service/exchange/coinmarketcap/ExchangeRateTest.java +++ b/app/src/test/java/com/m2049r/xmrwallet/service/exchange/coinmarketcap/ExchangeRateTest.java @@ -71,7 +71,7 @@ public class ExchangeRateTest { @Test public void queryExchangeRate_shouldBeGetMethod() - throws InterruptedException, TimeoutException { + throws InterruptedException { exchangeApi.queryExchangeRate("XMR", "EUR", mockExchangeCallback); @@ -81,7 +81,7 @@ public class ExchangeRateTest { @Test public void queryExchangeRate_shouldHavePairInUrl() - throws InterruptedException, TimeoutException { + throws InterruptedException { exchangeApi.queryExchangeRate("XMR", "EUR", mockExchangeCallback); @@ -91,7 +91,7 @@ public class ExchangeRateTest { @Test public void queryExchangeRate_wasSuccessfulShouldRespondWithRate() - throws InterruptedException, JSONException, TimeoutException { + throws TimeoutException { final String base = "XMR"; final String quote = "EUR"; final double rate = 1.56; @@ -119,7 +119,7 @@ public class ExchangeRateTest { @Test public void queryExchangeRate_wasSuccessfulShouldRespondWithRateUSD() - throws InterruptedException, JSONException, TimeoutException { + throws TimeoutException { final String base = "XMR"; final String quote = "USD"; final double rate = 1.56; @@ -147,7 +147,7 @@ public class ExchangeRateTest { @Test public void queryExchangeRate_wasNotSuccessfulShouldCallOnError() - throws InterruptedException, JSONException, TimeoutException { + throws TimeoutException { mockWebServer.enqueue(new MockResponse().setResponseCode(500)); exchangeApi.queryExchangeRate("XMR", "USD", new ExchangeCallback() { @@ -170,7 +170,7 @@ public class ExchangeRateTest { @Test public void queryExchangeRate_unknownAssetShouldCallOnError() - throws InterruptedException, JSONException, TimeoutException { + throws TimeoutException { MockResponse jsonMockResponse = new MockResponse().setBody( createMockExchangeRateErrorResponse()); mockWebServer.enqueue(jsonMockResponse); diff --git a/app/src/test/java/com/m2049r/xmrwallet/util/RestoreHeightTest.java b/app/src/test/java/com/m2049r/xmrwallet/util/RestoreHeightTest.java index 51185d94..58ac8f54 100644 --- a/app/src/test/java/com/m2049r/xmrwallet/util/RestoreHeightTest.java +++ b/app/src/test/java/com/m2049r/xmrwallet/util/RestoreHeightTest.java @@ -124,8 +124,7 @@ public class RestoreHeightTest { private boolean isInRange(long n, long min, long max) { if (n > max) return false; - if (n < min) return false; - return true; + return n >= min; } private long getHeight(String date) { diff --git a/app/src/test/java/com/m2049r/xmrwallet/xmrto/network/XmrToApiCreateOrderTest.java b/app/src/test/java/com/m2049r/xmrwallet/xmrto/network/XmrToApiCreateOrderTest.java index db585748..0d342cd5 100644 --- a/app/src/test/java/com/m2049r/xmrwallet/xmrto/network/XmrToApiCreateOrderTest.java +++ b/app/src/test/java/com/m2049r/xmrwallet/xmrto/network/XmrToApiCreateOrderTest.java @@ -71,7 +71,7 @@ public class XmrToApiCreateOrderTest { @Test public void createOrder_shouldBePostMethod() - throws InterruptedException, TimeoutException { + throws InterruptedException { xmrToApi.createOrder(0.5, "btcsomething", mockOrderXmrToCallback); @@ -81,7 +81,7 @@ public class XmrToApiCreateOrderTest { @Test public void createOrder_shouldBeContentTypeJson() - throws InterruptedException, TimeoutException { + throws InterruptedException { xmrToApi.createOrder(0.5, "19y91nJyzXsLEuR7Nj9pc3o5SeHNc8A9RW", mockOrderXmrToCallback); @@ -91,7 +91,7 @@ public class XmrToApiCreateOrderTest { @Test public void createOrder_shouldContainValidBody() - throws InterruptedException, TimeoutException { + throws InterruptedException { final String validBody = "{\"btc_amount\":0.1,\"btc_dest_address\":\"19y91nJyzXsLEuR7Nj9pc3o5SeHNc8A9RW\"}"; @@ -104,7 +104,7 @@ public class XmrToApiCreateOrderTest { @Test public void createOrder_wasSuccessfulShouldRespondWithOrder() - throws InterruptedException, JSONException, TimeoutException { + throws TimeoutException { final double amount = 1.23456789; final String address = "19y91nJyzXsLEuR7Nj9pc3o5SeHNc8A9RW"; final String uuid = "xmrto-abcdef"; @@ -134,7 +134,7 @@ public class XmrToApiCreateOrderTest { @Test public void createOrder_wasNotSuccessfulShouldCallOnError() - throws InterruptedException, JSONException, TimeoutException { + throws TimeoutException { mockWebServer.enqueue(new MockResponse().setResponseCode(500)); xmrToApi.createOrder(0.5, "19y91nJyzXsLEuR7Nj9pc3o5SeHNc8A9RW", new XmrToCallback() { @Override @@ -156,7 +156,7 @@ public class XmrToApiCreateOrderTest { @Test public void createOrder_malformedAddressShouldCallOnError() - throws InterruptedException, JSONException, TimeoutException { + throws TimeoutException { mockWebServer.enqueue(new MockResponse(). setResponseCode(400). setBody("{\"error_msg\":\"malformed bitcoin address\",\"error\":\"XMRTO-ERROR-002\"}")); diff --git a/app/src/test/java/com/m2049r/xmrwallet/xmrto/network/XmrToApiOrderParameterTest.java b/app/src/test/java/com/m2049r/xmrwallet/xmrto/network/XmrToApiOrderParameterTest.java index f554e0a1..c4a9f98c 100644 --- a/app/src/test/java/com/m2049r/xmrwallet/xmrto/network/XmrToApiOrderParameterTest.java +++ b/app/src/test/java/com/m2049r/xmrwallet/xmrto/network/XmrToApiOrderParameterTest.java @@ -71,7 +71,7 @@ public class XmrToApiOrderParameterTest { @Test public void orderParameter_shouldBeGetMethod() - throws InterruptedException, TimeoutException { + throws InterruptedException { xmrToApi.queryOrderParameters(mockParametersXmrToCallback); @@ -81,7 +81,7 @@ public class XmrToApiOrderParameterTest { @Test public void orderParameter_wasSuccessfulShouldRespondWithParameters() - throws InterruptedException, JSONException, TimeoutException { + throws TimeoutException { final boolean isZeroConfEnabled = true; final double price = 0.015537; final double upperLimit = 20.0; @@ -114,7 +114,7 @@ public class XmrToApiOrderParameterTest { @Test public void orderParameter_wasNotSuccessfulShouldCallOnError() - throws InterruptedException, JSONException, TimeoutException { + throws TimeoutException { mockWebServer.enqueue(new MockResponse().setResponseCode(500)); xmrToApi.queryOrderParameters(new XmrToCallback() { @Override @@ -136,7 +136,7 @@ public class XmrToApiOrderParameterTest { @Test public void orderParameter_thirdPartyServiceNotAvailableShouldCallOnError() - throws InterruptedException, JSONException, TimeoutException { + throws TimeoutException { mockWebServer.enqueue(new MockResponse(). setResponseCode(503). setBody("{\"error_msg\":\"third party service not available\",\"error\":\"XMRTO-ERROR-007\"}")); diff --git a/app/src/test/java/com/m2049r/xmrwallet/xmrto/network/XmrToApiQueryOrderTest.java b/app/src/test/java/com/m2049r/xmrwallet/xmrto/network/XmrToApiQueryOrderTest.java index c97554a5..dac52600 100644 --- a/app/src/test/java/com/m2049r/xmrwallet/xmrto/network/XmrToApiQueryOrderTest.java +++ b/app/src/test/java/com/m2049r/xmrwallet/xmrto/network/XmrToApiQueryOrderTest.java @@ -81,7 +81,7 @@ public class XmrToApiQueryOrderTest { @Test public void orderStatus_shouldBePostMethod() - throws InterruptedException, TimeoutException { + throws InterruptedException { xmrToApi.queryOrderStatus("xmrto - efMsiU", mockQueryXmrToCallback); @@ -91,7 +91,7 @@ public class XmrToApiQueryOrderTest { @Test public void orderStatus_shouldBeContentTypeJson() - throws InterruptedException, TimeoutException { + throws InterruptedException { xmrToApi.queryOrderStatus("xmrto - efMsiU", mockQueryXmrToCallback); @@ -101,7 +101,7 @@ public class XmrToApiQueryOrderTest { @Test public void orderStatus_shouldContainValidBody() - throws InterruptedException, TimeoutException { + throws InterruptedException { final String validBody = "{\"uuid\":\"xmrto - efMsiU\"}"; @@ -114,7 +114,7 @@ public class XmrToApiQueryOrderTest { @Test public void orderStatus_wasSuccessfulShouldRespondWithOrder() - throws InterruptedException, JSONException, TimeoutException { + throws TimeoutException { //TODO: state enum // TODO dates are dates @@ -204,7 +204,7 @@ public class XmrToApiQueryOrderTest { @Test public void orderStatus_wasNotSuccessfulShouldCallOnError() - throws InterruptedException, JSONException, TimeoutException { + throws TimeoutException { mockWebServer.enqueue(new MockResponse().setResponseCode(500)); xmrToApi.queryOrderStatus("xmrto - efMsiU", new XmrToCallback() { @Override @@ -226,7 +226,7 @@ public class XmrToApiQueryOrderTest { @Test public void orderStatus_orderNotFoundShouldCallOnError() - throws InterruptedException, JSONException, TimeoutException { + throws TimeoutException { mockWebServer.enqueue(new MockResponse(). setResponseCode(404). setBody("{\"error_msg\":\"requested order not found\",\"error\":\"XMRTO-ERROR-006\"}")); diff --git a/build.gradle b/build.gradle index 53dec4ef..6f144406 100644 --- a/build.gradle +++ b/build.gradle @@ -26,9 +26,9 @@ task clean(type: Delete) { ext { apkName = 'monerujo' - okHttpVersion = '3.9.0' + okHttpVersion = '3.12.0' junitVersion = '4.12' mockitoVersion = '1.10.19' - timberVersion = '4.7.0' - supportVersion = '27.1.1' + timberVersion = '4.7.1' + supportVersion = '28.0.0' }