1
mirror of https://github.com/m2049r/xmrwallet synced 2025-09-03 08:23:04 +02:00

Compare commits

...

22 Commits

Author SHA1 Message Date
m2049r
4ca9328949 bump version (#534) 2019-02-17 20:23:00 +01:00
m2049r
08b5a87f19 get rid of keystore exception on first call (#533) 2019-02-14 23:45:01 +01:00
m2049r
445d8acc38 open on monero: & bitcoin: uri (#532) 2019-02-14 23:34:38 +01:00
m2049r
9385ac8c31 update gradle version 2019-02-11 18:50:43 +01:00
Keksoj
4c7ebd8402 translated 'help', 'strings' and 'about' to esperanto (#531) 2019-02-11 07:41:50 +01:00
m2049r
67f3c5f948 bump version 2019-02-04 19:41:28 +01:00
m2049r
cd67a7e2bf 2019-02 height 2019-02-04 19:41:28 +01:00
0140454
fa5fe313ea Update zh-rTW translation (#529) 2019-02-03 07:54:41 +01:00
jaro Lee
ed4957a3cc sk strings update (#523) 2019-01-29 22:33:09 +01:00
jaro Lee
3e0eeebd51 sk help.xml update (#527)
sk-translation - help_send update
2019-01-29 22:32:40 +01:00
m2049r
0d213a1eb4 hide sweep amount in street mode (#526) 2019-01-28 01:16:00 +01:00
m2049r
39d048fd5e Feature bitpay (#525)
* support bitcoin payment protocol (BIP70/72)

* prep translations
2019-01-27 21:32:06 +01:00
m2049r
1a5d2d0399 Update Android Studio v3.3 (#522)
* Update Android Studio v3.3

* also update circleci script to accept licenses
2019-01-21 17:38:59 +01:00
m2049r
028057a672 bump version (#519) 2019-01-14 12:52:28 +01:00
TheFuzzStone
909ff8ca5e Ukranian translation for Monerujo (#517)
* Ukranian translation

* Update for Russian language (fixed small typos)
2019-01-12 19:56:59 +01:00
m2049r
ffd61e4495 remove getErrorString method (#516) 2019-01-11 19:24:33 +01:00
m2049r
003dee382e Merge pull request #515 from m2049r/fix_license
SwipeableRecyclerView license
2019-01-11 19:23:56 +01:00
m2049r
be0498c67d SwipeableRecyclerView license 2019-01-11 19:23:25 +01:00
m2049r
06227a4a83 Merge pull request #514 from m2049r/feature_dismissTx
Allow swipe-to-dismiss transactions in street mode
2019-01-10 19:33:26 +01:00
m2049r
4409087bd0 update dependencies 2019-01-10 19:31:34 +01:00
m2049r
965e52d8a5 allow to dismiss tx in streetmode 2019-01-10 19:31:15 +01:00
m2049r
beba0f497b Fix bech32 (#513)
* fix bech32 recognition

* bump version
2019-01-09 20:54:14 +01:00
88 changed files with 2854 additions and 332 deletions

View File

@@ -3,11 +3,13 @@ jobs:
build:
working_directory: ~/code
docker:
- image: bitriseio/android-ndk
- image: circleci/android:api-28-ndk
environment:
JVM_OPTS: -Xmx3200m
steps:
- checkout
- run: yes | sdkmanager --licenses || exit 0
- run: yes | sdkmanager --update || exit 0
- restore_cache:
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}
- run:

3
.idea/.gitignore generated vendored
View File

@@ -1,3 +0,0 @@
workspace.xml
markdown-*
misc.xml

1
.idea/.name generated
View File

@@ -1 +0,0 @@
xmrwallet

22
.idea/compiler.xml generated
View File

@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<resourceExtensions />
<wildcardResourcePatterns>
<entry name="!?*.java" />
<entry name="!?*.form" />
<entry name="!?*.class" />
<entry name="!?*.groovy" />
<entry name="!?*.scala" />
<entry name="!?*.flex" />
<entry name="!?*.kt" />
<entry name="!?*.clj" />
<entry name="!?*.aj" />
</wildcardResourcePatterns>
<annotationProcessing>
<profile default="true" name="Default" enabled="false">
<processorPath useClasspath="true" />
</profile>
</annotationProcessing>
</component>
</project>

View File

@@ -1,3 +0,0 @@
<component name="CopyrightManager">
<settings default="" />
</component>

19
.idea/gradle.xml generated
View File

@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="disableWrapperSourceDistributionNotification" value="true" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>

9
.idea/modules.xml generated
View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/app/app.iml" filepath="$PROJECT_DIR$/app/app.iml" />
<module fileurl="file://$PROJECT_DIR$/xmrwallet.iml" filepath="$PROJECT_DIR$/xmrwallet.iml" />
</modules>
</component>
</project>

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>

6
.idea/vcs.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@@ -7,8 +7,8 @@ android {
applicationId "com.m2049r.xmrwallet"
minSdkVersion 21
targetSdkVersion 28
versionCode 162
versionName "1.10.12 'Node-O-matiC'"
versionCode 166
versionName "1.10.16 'Node-O-matiC'"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild {
@@ -113,10 +113,12 @@ dependencies {
implementation 'dnsjava:dnsjava:2.1.8'
implementation 'org.jitsi:dnssecjava:1.1.3'
implementation 'org.slf4j:slf4j-nop:1.7.25'
implementation 'com.github.brnunes:swipeablerecyclerview:1.0.2'
testImplementation "junit:junit:$rootProject.ext.junitVersion"
testImplementation "org.mockito:mockito-all:$rootProject.ext.mockitoVersion"
testImplementation "com.squareup.okhttp3:mockwebserver:$rootProject.ext.okHttpVersion"
testImplementation 'org.json:json:20140107'
testImplementation 'net.jodah:concurrentunit:0.4.2'
testImplementation 'org.json:json:20180813'
testImplementation 'net.jodah:concurrentunit:0.4.4'
}

View File

@@ -42,6 +42,22 @@
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
</intent-filter>
<intent-filter android:label="@string/app_name">
<action android:name="android.intent.action.VIEW" />
<data android:scheme="monero" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
</intent-filter>
<intent-filter android:label="@string/app_name">
<action android:name="android.intent.action.VIEW" />
<data android:scheme="bitcoin" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
</intent-filter>
<meta-data
android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
android:resource="@xml/usb_device_filter" />

View File

@@ -43,6 +43,9 @@ Copyright (c) 2014 Dushyanth Maguluru
<h3>AndroidLicensesPage (https://github.com/adamsp/AndroidLicensesPage)</h3>
Copyright (c) 2013 Adam Speakman
<h3>SwipeableRecyclerView (https://github.com/brnunes/SwipeableRecyclerView)</h3>
Copyright (c) 2015 Bruno R. Nunes
<h3>Apache License, Version 2.0, January 2004</h3>
http://www.apache.org/licenses/<br/>
<br/>

View File

@@ -442,12 +442,6 @@ Java_com_m2049r_xmrwallet_model_WalletManager_findWallets(JNIEnv *env, jobject i
return cpp2java(env, walletPaths);
}
JNIEXPORT jstring JNICALL
Java_com_m2049r_xmrwallet_model_WalletManager_getErrorString(JNIEnv *env, jobject instance) {
Bitmonero::Wallet *wallet = getHandle<Bitmonero::Wallet>(env, instance);
return env->NewStringUTF(wallet->errorString().c_str());
}
//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;
JNIEXPORT void JNICALL

View File

@@ -269,7 +269,11 @@ public class LoginActivity extends BaseActivity
} else {
Timber.i("Waiting for permissions");
}
processUsbIntent(getIntent());
// try intents
Intent intent = getIntent();
if (!processUsbIntent(intent))
processUriIntent(intent);
}
boolean checkServiceRunning() {
@@ -716,6 +720,10 @@ public class LoginActivity extends BaseActivity
intent.putExtra(WalletActivity.REQUEST_PW, walletPassword);
intent.putExtra(WalletActivity.REQUEST_FINGERPRINT_USED, fingerprintUsed);
intent.putExtra(WalletActivity.REQUEST_STREETMODE, streetmode);
if (uri != null) {
intent.putExtra(WalletActivity.REQUEST_URI, uri);
uri = null; // use only once
}
startActivity(intent);
}
@@ -1385,7 +1393,7 @@ public class LoginActivity extends BaseActivity
processUsbIntent(intent);
}
private void processUsbIntent(Intent intent) {
private boolean processUsbIntent(Intent intent) {
String action = intent.getAction();
if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) {
synchronized (this) {
@@ -1398,6 +1406,21 @@ public class LoginActivity extends BaseActivity
}
}
}
return true;
}
return false;
}
private String uri = null;
private void processUriIntent(Intent intent) {
String action = intent.getAction();
if (Intent.ACTION_VIEW.equals(action)) {
synchronized (this) {
uri = intent.getDataString();
Timber.d("URI Intent %s", uri);
HelpFragment.display(getSupportFragmentManager(), R.string.help_uri);
}
}
}

View File

@@ -36,7 +36,7 @@ import com.m2049r.xmrwallet.model.TransactionInfo;
import com.m2049r.xmrwallet.model.Transfer;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.UserNotes;
import com.m2049r.xmrwallet.data.UserNotes;
import com.m2049r.xmrwallet.widget.Toolbar;
import java.text.SimpleDateFormat;

View File

@@ -47,6 +47,7 @@ import android.widget.Toast;
import com.m2049r.xmrwallet.data.BarcodeData;
import com.m2049r.xmrwallet.data.TxData;
import com.m2049r.xmrwallet.data.UserNotes;
import com.m2049r.xmrwallet.dialog.CreditsFragment;
import com.m2049r.xmrwallet.dialog.HelpFragment;
import com.m2049r.xmrwallet.fragment.send.SendAddressWizardFragment;
@@ -59,7 +60,6 @@ import com.m2049r.xmrwallet.model.WalletManager;
import com.m2049r.xmrwallet.service.WalletService;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.MoneroThreadPoolExecutor;
import com.m2049r.xmrwallet.util.UserNotes;
import com.m2049r.xmrwallet.widget.Toolbar;
import java.util.ArrayList;
@@ -81,6 +81,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
public static final String REQUEST_PW = "pw";
public static final String REQUEST_FINGERPRINT_USED = "fingerprint";
public static final String REQUEST_STREETMODE = "streetmode";
public static final String REQUEST_URI = "uri";
private NavigationView accountsView;
private DrawerLayout drawer;
@@ -92,6 +93,8 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
private String password;
private String uri = null;
private long streetMode = 0;
@Override
@@ -144,6 +147,9 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
} else {
streetMode = 0;
}
final WalletFragment walletFragment = (WalletFragment)
getSupportFragmentManager().findFragmentByTag(WalletFragment.class.getName());
if (walletFragment != null) walletFragment.resetDismissedTransactions();
updateAccountsBalance();
forceUpdate();
}
@@ -188,6 +194,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
// we can set the streetmode height AFTER opening the wallet
requestStreetMode = extras.getBoolean(REQUEST_STREETMODE);
password = extras.getString(REQUEST_PW);
uri = extras.getString(REQUEST_URI);
connectWalletService(walletId, password);
} else {
finish();
@@ -286,7 +293,6 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
showNet();
}
invalidateOptionsMenu();
}
private void onEnableStreetMode() {
@@ -510,7 +516,8 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
@Override
public void onSendRequest() {
replaceFragment(new SendFragment(), null, null);
replaceFragment(SendFragment.newInstance(uri), null, null);
uri = null; // only use uri once
}
@Override
@@ -956,8 +963,6 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
}
if (!processed || (onUriScannedListener == null)) {
Toast.makeText(this, getString(R.string.nfc_tag_read_what), Toast.LENGTH_LONG).show();
} else {
Toast.makeText(this, getString(R.string.nfc_tag_read_success), Toast.LENGTH_SHORT).show();
}
}

View File

@@ -38,6 +38,7 @@ import android.widget.ProgressBar;
import android.widget.Spinner;
import android.widget.TextView;
import com.github.brnunes.swipeablerecyclerview.SwipeableRecyclerViewTouchListener;
import com.m2049r.xmrwallet.layout.TransactionInfoAdapter;
import com.m2049r.xmrwallet.model.TransactionInfo;
import com.m2049r.xmrwallet.model.Wallet;
@@ -73,6 +74,12 @@ public class WalletFragment extends Fragment
private Spinner sCurrency;
private List<String> dismissedTransactions = new ArrayList<>();
public void resetDismissedTransactions() {
dismissedTransactions.clear();
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -116,9 +123,44 @@ public class WalletFragment extends Fragment
RecyclerView recyclerView = view.findViewById(R.id.list);
this.adapter = new TransactionInfoAdapter(getActivity(), this);
adapter = new TransactionInfoAdapter(getActivity(), this);
recyclerView.setAdapter(adapter);
SwipeableRecyclerViewTouchListener swipeTouchListener =
new SwipeableRecyclerViewTouchListener(recyclerView,
new SwipeableRecyclerViewTouchListener.SwipeListener() {
@Override
public boolean canSwipeLeft(int position) {
return activityCallback.isStreetMode();
}
@Override
public boolean canSwipeRight(int position) {
return activityCallback.isStreetMode();
}
@Override
public void onDismissedBySwipeLeft(RecyclerView recyclerView, int[] reverseSortedPositions) {
for (int position : reverseSortedPositions) {
dismissedTransactions.add(adapter.getItem(position).hash);
adapter.removeItem(position);
}
adapter.notifyDataSetChanged();
}
@Override
public void onDismissedBySwipeRight(RecyclerView recyclerView, int[] reverseSortedPositions) {
for (int position : reverseSortedPositions) {
dismissedTransactions.add(adapter.getItem(position).hash);
adapter.removeItem(position);
}
adapter.notifyDataSetChanged();
}
});
recyclerView.addOnItemTouchListener(swipeTouchListener);
bSend.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
@@ -294,7 +336,9 @@ public class WalletFragment extends Fragment
Timber.d("StreetHeight=%d", streetHeight);
for (TransactionInfo info : wallet.getHistory().getAll()) {
Timber.d("TxHeight=%d", info.blockheight);
if (info.isPending || (info.blockheight >= streetHeight)) list.add(info);
if ((info.isPending || (info.blockheight >= streetHeight))
&& !dismissedTransactions.contains(info.hash))
list.add(info);
}
adapter.setInfos(list);
adapter.notifyDataSetChanged();

View File

@@ -21,7 +21,10 @@ import android.net.Uri;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.util.BitcoinAddressValidator;
import com.m2049r.xmrwallet.util.OpenAliasHelper;
import com.m2049r.xmrwallet.util.PaymentProtocolHelper;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
@@ -37,9 +40,10 @@ public class BarcodeData {
public static final String OA_XMR_ASSET = "xmr";
public static final String OA_BTC_ASSET = "btc";
static final String BTC_SCHEME = "bitcoin:";
static final String BTC_SCHEME = "bitcoin";
static final String BTC_DESCRIPTION = "message";
static final String BTC_AMOUNT = "amount";
static final String BTC_BIP70_PARM = "r";
public enum Asset {
XMR, BTC
@@ -48,7 +52,8 @@ public class BarcodeData {
public enum Security {
NORMAL,
OA_NO_DNSSEC,
OA_DNSSEC
OA_DNSSEC,
BIP70
}
final public Asset asset;
@@ -58,57 +63,44 @@ public class BarcodeData {
final public String amount;
final public String description;
final public Security security;
final public String bip70;
public BarcodeData(Asset asset, String address) {
this.asset = asset;
this.address = address;
amount = null;
paymentId = null;
addressName = null;
description = null;
this.security = Security.NORMAL;
this(asset, address, null, null, null, null, Security.NORMAL);
}
public BarcodeData(Asset asset, String address, String amount) {
this.asset = asset;
this.address = address;
this.amount = amount;
paymentId = null;
addressName = null;
description = null;
this.security = Security.NORMAL;
this(asset, address, null, null, null, amount, Security.NORMAL);
}
public BarcodeData(Asset asset, String address, String amount, String description, Security security) {
this(asset, address, null, null, description, amount, security);
}
public BarcodeData(Asset asset, String address, String paymentId, String amount) {
this.asset = asset;
this.address = address;
this.paymentId = paymentId;
this.amount = amount;
addressName = null;
description = null;
this.security = Security.NORMAL;
this(asset, address, null, paymentId, null, amount, Security.NORMAL);
}
public BarcodeData(Asset asset, String address, String paymentId, String description, String amount) {
this.asset = asset;
this.address = address;
this.paymentId = paymentId;
this.description = description;
this.amount = amount;
addressName = null;
this.security = Security.NORMAL;
this(asset, address, null, paymentId, description, amount, Security.NORMAL);
}
public BarcodeData(Asset asset, String address, String addressName, String paymentId, String description, String amount, Security sec) {
public BarcodeData(Asset asset, String address, String addressName, String paymentId, String description, String amount, Security security) {
this(asset, address, addressName, null, paymentId, description, amount, security);
}
public BarcodeData(Asset asset, String address, String addressName, String bip70, String paymentId, String description, String amount, Security security) {
this.asset = asset;
this.address = address;
this.bip70 = bip70;
this.addressName = addressName;
this.paymentId = paymentId;
this.description = description;
this.amount = amount;
this.security = sec;
this.security = security;
}
public Uri getUri() {
return Uri.parse(getUriString());
}
@@ -146,6 +138,10 @@ public class BarcodeData {
if (bcData == null) {
bcData = parseBitcoinUri(qrCode);
}
// check for btc payment uri (like bitpay)
if (bcData == null) {
bcData = parseBitcoinPaymentUrl(qrCode);
}
// check for naked btc address
if (bcData == null) {
bcData = parseBitcoinNaked(qrCode);
@@ -228,19 +224,28 @@ public class BarcodeData {
}
// bitcoin:mpQ84J43EURZHkCnXbyQ4PpNDLLBqdsMW2?amount=0.01
static public BarcodeData parseBitcoinUri(String uri) {
Timber.d("parseBitcoinUri=%s", uri);
// bitcoin:?r=https://bitpay.com/i/xxx
static public BarcodeData parseBitcoinUri(String uriString) {
Timber.d("parseBitcoinUri=%s", uriString);
if (uri == null) return null;
if (uriString == null) return null;
URI uri;
try {
uri = new URI(uriString);
} catch (URISyntaxException ex) {
return null;
}
if (!uri.isOpaque() ||
!uri.getScheme().equals(BTC_SCHEME)) return null;
if (!uri.startsWith(BTC_SCHEME)) return null;
String noScheme = uri.substring(BTC_SCHEME.length());
Uri bitcoin = Uri.parse(noScheme);
String[] parts = uri.getRawSchemeSpecificPart().split("[?]");
if ((parts.length <= 0) || (parts.length > 2)) {
Timber.d("invalid number of parts %d", parts.length);
return null;
}
Map<String, String> parms = new HashMap<>();
String query = bitcoin.getQuery();
if (query != null) {
String[] args = query.split("&");
if (parts.length == 2) {
String[] args = parts[1].split("&");
for (String arg : args) {
String[] namevalue = arg.split("=");
if (namevalue.length == 0) {
@@ -250,10 +255,26 @@ public class BarcodeData {
namevalue.length > 1 ? Uri.decode(namevalue[1]) : "");
}
}
String address = bitcoin.getPath();
String description = parms.get(BTC_DESCRIPTION);
String address = parts[0]; // no need to decode as there can bo no special characters
if (address.isEmpty()) { // possibly a BIP72 uri
String bip70 = parms.get(BTC_BIP70_PARM);
if (bip70 == null) {
Timber.d("no address and can't find pp url");
return null;
}
if (!PaymentProtocolHelper.isHttp(bip70)) {
Timber.d("[%s] is not http url", bip70);
return null;
}
return new BarcodeData(BarcodeData.Asset.BTC, null, null, bip70, null, description, null, Security.NORMAL);
}
if (!BitcoinAddressValidator.validate(address)) {
Timber.d("BTC address (%s) invalid", address);
return null;
}
String amount = parms.get(BTC_AMOUNT);
if (amount != null) {
if ((amount != null) && (!amount.isEmpty())) {
try {
Double.parseDouble(amount);
} catch (NumberFormatException ex) {
@@ -261,11 +282,22 @@ public class BarcodeData {
return null; // we have an amount but its not a number!
}
}
if (!BitcoinAddressValidator.validate(address)) {
Timber.d("address invalid");
return new BarcodeData(BarcodeData.Asset.BTC, address, null, description, amount);
}
// https://bitpay.com/invoice?id=xxx
// https://bitpay.com/i/KbMdd4EhnLXSbpWGKsaeo6
static public BarcodeData parseBitcoinPaymentUrl(String url) {
Timber.d("parseBitcoinUri=%s", url);
if (url == null) return null;
if (!PaymentProtocolHelper.isHttp(url)) {
Timber.d("[%s] is not http url", url);
return null;
}
return new BarcodeData(BarcodeData.Asset.BTC, address, null, description, amount);
return new BarcodeData(Asset.BTC, url);
}
static public BarcodeData parseBitcoinNaked(String address) {

View File

@@ -20,9 +20,6 @@ import android.os.Parcel;
import android.os.Parcelable;
import com.m2049r.xmrwallet.model.PendingTransaction;
import com.m2049r.xmrwallet.util.UserNotes;
import timber.log.Timber;
// https://stackoverflow.com/questions/2139134/how-to-send-an-object-from-one-android-activity-to-another-using-intents
public class TxData implements Parcelable {

View File

@@ -18,12 +18,11 @@ package com.m2049r.xmrwallet.data;
import android.os.Parcel;
import com.m2049r.xmrwallet.model.PendingTransaction;
public class TxDataBtc extends TxData {
private String xmrtoUuid;
private String btcAddress;
private String bip70;
private double btcAmount;
public TxDataBtc() {
@@ -50,6 +49,14 @@ public class TxDataBtc extends TxData {
this.btcAddress = btcAddress;
}
public String getBip70() {
return bip70;
}
public void setBip70(String bip70) {
this.bip70 = bip70;
}
public double getBtcAmount() {
return btcAmount;
}
@@ -63,6 +70,7 @@ public class TxDataBtc extends TxData {
super.writeToParcel(out, flags);
out.writeString(xmrtoUuid);
out.writeString(btcAddress);
out.writeString(bip70);
out.writeDouble(btcAmount);
}
@@ -81,16 +89,19 @@ public class TxDataBtc extends TxData {
super(in);
xmrtoUuid = in.readString();
btcAddress = in.readString();
bip70 = in.readString();
btcAmount = in.readDouble();
}
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
StringBuilder sb = new StringBuilder();
sb.append(",xmrtoUuid:");
sb.append(xmrtoUuid);
sb.append(",btcAddress:");
sb.append(btcAddress);
sb.append(",bip70:");
sb.append(bip70);
sb.append(",btcAmount:");
sb.append(btcAmount);
return sb.toString();

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.m2049r.xmrwallet.util;
package com.m2049r.xmrwallet.data;
import com.m2049r.xmrwallet.xmrto.api.QueryOrderStatus;

View File

@@ -67,7 +67,7 @@ public class HelpFragment extends DialogFragment {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setView(view);
builder.setNegativeButton(R.string.about_close,
builder.setNegativeButton(R.string.help_ok,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {

View File

@@ -120,6 +120,15 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
return true;
}
private void setBip70Mode() {
TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
if (txDataBtc.getBip70() != null) {
numberPad.setVisibility(View.INVISIBLE);
} else {
numberPad.setVisibility(View.VISIBLE);
}
}
double maxBtc = 0;
double minBtc = 0;
@@ -141,12 +150,13 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
tvFunds.setText(getString(R.string.send_available,
getString(R.string.unknown_amount)));
}
if ((evAmount.getAmount() == null) || evAmount.getAmount().isEmpty()) {
final BarcodeData data = sendListener.popBarcodeData();
if ((data != null) && (data.amount != null)) {
final BarcodeData data = sendListener.popBarcodeData();
if (data != null) {
if (data.amount != null) {
evAmount.setAmount(data.amount);
}
}
setBip70Mode();
callXmrTo();
}

View File

@@ -524,8 +524,8 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
xmrtoStatus = null;
showProgress(1, getString(R.string.label_send_progress_xmrto_create));
TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
double btcAmount = txDataBtc.getBtcAmount();
getXmrToApi().createOrder(btcAmount, txDataBtc.getBtcAddress(), new XmrToCallback<CreateOrder>() {
XmrToCallback<CreateOrder> callback = new XmrToCallback<CreateOrder>() {
@Override
public void onSuccess(CreateOrder createOrder) {
if (!isResumed) return;
@@ -545,7 +545,13 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
}
processCreateOrderError(ex);
}
});
};
if (txDataBtc.getBip70() != null) {
getXmrToApi().createOrder(txDataBtc.getBip70(), callback);
} else {
getXmrToApi().createOrder(txDataBtc.getBtcAmount(), txDataBtc.getBtcAddress(), callback);
}
}
private QueryOrderStatus xmrtoStatus = null;
@@ -666,7 +672,7 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
private XmrToApi xmrToApi = null;
private final XmrToApi getXmrToApi() {
private XmrToApi getXmrToApi() {
if (xmrToApi == null) {
synchronized (this) {
if (xmrToApi == null) {

View File

@@ -36,7 +36,7 @@ import com.m2049r.xmrwallet.data.TxData;
import com.m2049r.xmrwallet.model.PendingTransaction;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.UserNotes;
import com.m2049r.xmrwallet.data.UserNotes;
import timber.log.Timber;
@@ -214,11 +214,16 @@ public class SendConfirmWizardFragment extends SendWizardFragment implements Sen
if (pendingTransaction != null) {
llConfirmSend.setVisibility(View.VISIBLE);
bSend.setEnabled(true);
tvTxAmount.setText(Wallet.getDisplayAmount(pendingTransaction.getAmount()));
tvTxFee.setText(Wallet.getDisplayAmount(pendingTransaction.getFee()));
//tvTxDust.setText(Wallet.getDisplayAmount(pendingTransaction.getDust()));
tvTxTotal.setText(Wallet.getDisplayAmount(
pendingTransaction.getFee() + pendingTransaction.getAmount()));
if (getActivityCallback().isStreetMode()
&& (sendListener.getTxData().getAmount() == Wallet.SWEEP_ALL)) {
tvTxAmount.setText(getString(R.string.street_sweep_amount));
tvTxTotal.setText(getString(R.string.street_sweep_amount));
} else {
tvTxAmount.setText(Wallet.getDisplayAmount(pendingTransaction.getAmount()));
tvTxTotal.setText(Wallet.getDisplayAmount(
pendingTransaction.getFee() + pendingTransaction.getAmount()));
}
} else {
llConfirmSend.setVisibility(View.GONE);
bSend.setEnabled(false);

View File

@@ -34,20 +34,20 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import com.m2049r.xmrwallet.OnBackPressedListener;
import com.m2049r.xmrwallet.OnUriScannedListener;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.WalletActivity;
import com.m2049r.xmrwallet.data.BarcodeData;
import com.m2049r.xmrwallet.data.PendingTx;
import com.m2049r.xmrwallet.data.TxData;
import com.m2049r.xmrwallet.data.TxDataBtc;
import com.m2049r.xmrwallet.data.UserNotes;
import com.m2049r.xmrwallet.layout.SpendViewPager;
import com.m2049r.xmrwallet.model.PendingTransaction;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.Notice;
import com.m2049r.xmrwallet.util.UserNotes;
import com.m2049r.xmrwallet.widget.DotBar;
import com.m2049r.xmrwallet.widget.Toolbar;
@@ -105,6 +105,14 @@ public class SendFragment extends Fragment
static private int MAX_FALLBACK = Integer.MAX_VALUE;
public static SendFragment newInstance(String uri) {
SendFragment f = new SendFragment();
Bundle args = new Bundle();
args.putString(WalletActivity.REQUEST_URI, uri);
f.setArguments(args);
return f;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@@ -188,6 +196,16 @@ public class SendFragment extends Fragment
etDummy.requestFocus();
Helper.hideKeyboard(getActivity());
Bundle args = getArguments();
if (args != null) {
String uri = args.getString(WalletActivity.REQUEST_URI);
Timber.d("URI: %s", uri);
if (uri != null) {
barcodeData = BarcodeData.fromQrCode(uri);
Timber.d("barcodeData: %s", barcodeData != null ? barcodeData.toString() : "null");
}
}
return view;
}

View File

@@ -27,6 +27,7 @@ import android.widget.Toast;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.PendingTx;
import com.m2049r.xmrwallet.data.TxData;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.util.Helper;
import timber.log.Timber;
@@ -54,6 +55,8 @@ public class SendSuccessWizardFragment extends SendWizardFragment {
void enableDone();
SendFragment.Mode getMode();
SendFragment.Listener getActivityCallback();
}
ImageButton bCopyTxId;
@@ -120,7 +123,13 @@ public class SendSuccessWizardFragment extends SendWizardFragment {
tvTxId.setText(committedTx.txId);
bCopyTxId.setEnabled(true);
bCopyTxId.setImageResource(R.drawable.ic_content_copy_black_24dp);
tvTxAmount.setText(getString(R.string.send_amount, Helper.getDisplayAmount(committedTx.amount)));
if (sendListener.getActivityCallback().isStreetMode()
&& (sendListener.getTxData().getAmount() == Wallet.SWEEP_ALL)) {
tvTxAmount.setText(getString(R.string.street_sweep_amount));
} else {
tvTxAmount.setText(getString(R.string.send_amount, Helper.getDisplayAmount(committedTx.amount)));
}
tvTxFee.setText(getString(R.string.send_fee, Helper.getDisplayAmount(committedTx.fee)));
}
sendListener.enableDone();

View File

@@ -28,7 +28,7 @@ import android.widget.TextView;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.model.TransactionInfo;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.UserNotes;
import com.m2049r.xmrwallet.data.UserNotes;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
@@ -91,7 +91,7 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfo
public void setInfos(List<TransactionInfo> data) {
// TODO do stuff with data so we can really recycle elements (i.e. add only new tx)
// as the TransactionInfo items are always recreated, we cannot recycle
this.infoItems.clear();
infoItems.clear();
if (data != null) {
Timber.d("setInfos %s", data.size());
infoItems.addAll(data);
@@ -102,6 +102,15 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfo
notifyDataSetChanged();
}
public void removeItem(int position) {
infoItems.remove(position);
notifyItemRemoved(position);
}
public TransactionInfo getItem(int position) {
return infoItems.get(position);
}
class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
final ImageView ivTxType;
final TextView tvAmount;

View File

@@ -264,8 +264,6 @@ public class WalletManager {
return wallets;
}
public native String getErrorString();
//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;

View File

@@ -244,32 +244,48 @@ public class KeyStoreHelper {
Timber.d("M Keys created");
}
private static KeyStore.PrivateKeyEntry getPrivateKeyEntry(String alias) {
private static PrivateKey getPrivateKey(String alias) {
try {
KeyStore ks = KeyStore
.getInstance(SecurityConstants.KEYSTORE_PROVIDER_ANDROID_KEYSTORE);
ks.load(null);
KeyStore.Entry entry = ks.getEntry(alias, null);
//KeyStore.Entry entry = ks.getEntry(alias, null);
PrivateKey privateKey = (PrivateKey) ks.getKey(alias, null);
if (entry == null) {
if (privateKey == null) {
Timber.w("No key found under alias: %s", alias);
return null;
}
if (!(entry instanceof KeyStore.PrivateKeyEntry)) {
Timber.w("Not an instance of a PrivateKeyEntry");
return null;
}
return (KeyStore.PrivateKeyEntry) entry;
return privateKey;
} catch (IOException | NoSuchAlgorithmException | CertificateException
| UnrecoverableEntryException | KeyStoreException ex) {
throw new IllegalStateException(ex);
}
}
private static PublicKey getPublicKey(String alias) {
try {
KeyStore ks = KeyStore
.getInstance(SecurityConstants.KEYSTORE_PROVIDER_ANDROID_KEYSTORE);
ks.load(null);
PublicKey publicKey = ks.getCertificate(alias).getPublicKey();
if (publicKey == null) {
Timber.w("No public key");
return null;
}
return publicKey;
} catch (IOException | NoSuchAlgorithmException | CertificateException
| KeyStoreException ex) {
throw new IllegalStateException(ex);
}
}
private static byte[] encrypt(String alias, byte[] data) {
try {
PublicKey publicKey = getPrivateKeyEntry(alias).getCertificate().getPublicKey();
PublicKey publicKey = getPublicKey(alias);
Cipher cipher = Cipher.getInstance(SecurityConstants.CIPHER_RSA_ECB_PKCS1);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
@@ -283,9 +299,8 @@ public class KeyStoreHelper {
private static byte[] decrypt(String alias, byte[] data) {
try {
KeyStore.PrivateKeyEntry pke = getPrivateKeyEntry(alias);
if (pke == null) return null;
PrivateKey privateKey = pke.getPrivateKey();
PrivateKey privateKey = getPrivateKey(alias);
if (privateKey == null) return null;
Cipher cipher = Cipher.getInstance(SecurityConstants.CIPHER_RSA_ECB_PKCS1);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
@@ -306,41 +321,14 @@ public class KeyStoreHelper {
*/
private static byte[] signData(String alias, byte[] data) throws NoSuchAlgorithmException,
InvalidKeyException, SignatureException {
KeyStore.PrivateKeyEntry pke = getPrivateKeyEntry(alias);
if (pke == null) return null;
PrivateKey privateKey = getPrivateKeyEntry(alias).getPrivateKey();
PrivateKey privateKey = getPrivateKey(alias);
if (privateKey == null) return null;
Signature s = Signature.getInstance(SecurityConstants.SIGNATURE_SHA256withRSA);
s.initSign(privateKey);
s.update(data);
return s.sign();
}
/**
* Given some data and a signature, uses the key pair stored in the Android
* Key Store to verify that the data was signed by this application, using
* that key pair.
*
* @param data The data to be verified.
* @param signature The signature provided for the data.
* @return A boolean value telling you whether the signature is valid or
* not.
*/
private static boolean verifyData(String alias, byte[] data, byte[] signature)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
// Make sure the signature string exists
if (signature == null) {
Timber.w("Invalid signature.");
return false;
}
KeyStore.PrivateKeyEntry keyEntry = getPrivateKeyEntry(alias);
Signature s = Signature.getInstance(SecurityConstants.SIGNATURE_SHA256withRSA);
s.initVerify(keyEntry.getCertificate());
s.update(data);
return s.verify(signature);
}
public interface SecurityConstants {
String KEYSTORE_PROVIDER_ANDROID_KEYSTORE = "AndroidKeyStore";
String TYPE_RSA = "RSA";

View File

@@ -0,0 +1,102 @@
/*
* 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.
*/
// Specs from https://openalias.org/
package com.m2049r.xmrwallet.util;
import com.m2049r.xmrwallet.data.BarcodeData;
import com.m2049r.xmrwallet.xmrto.api.CreateOrder;
import com.m2049r.xmrwallet.xmrto.api.XmrToApi;
import com.m2049r.xmrwallet.xmrto.api.XmrToCallback;
import com.m2049r.xmrwallet.xmrto.network.XmrToApiImpl;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import timber.log.Timber;
public class PaymentProtocolHelper {
//https://bitpay.com/i/xxx
public static boolean isHttp(String string) {
if ((string == null) || (string.isEmpty())) return false;
try {
// new URL(string).toURI() checks if its a real url
return new URL(string).toURI().getScheme().startsWith("http");
} catch (MalformedURLException | URISyntaxException ex) {
return false;
}
}
// guess if the string may be a valid payment uri
//https://bitpay.com/i/xxx
//bitcoin:?r=https://bitpay.com/i/xxx
public static String getBip70(String bip72Or70) {
if ((bip72Or70 == null) || (bip72Or70.isEmpty())) return null;
if (isHttp(bip72Or70)) return bip72Or70;
BarcodeData bc = BarcodeData.parseBitcoinUri(bip72Or70);
if (bc == null) return null;
return bc.bip70;
}
public interface OnResolvedListener {
void onResolved(BarcodeData.Asset asset, String address, double amount, String bip70);
void onFailure(Exception ex);
}
static public boolean resolve(final String bip70, final OnResolvedListener resolvedListener) {
if ((bip70 == null) || (bip70.isEmpty()))
return false; //pointless trying to lookup nothing
Timber.d("Resolving %s", bip70);
getXmrToApi().createOrder(bip70, new XmrToCallback<CreateOrder>() {
@Override
public void onSuccess(CreateOrder createOrder) {
if (resolvedListener != null) {
resolvedListener.onResolved(BarcodeData.Asset.BTC,
createOrder.getBtcDestAddress(),
createOrder.getBtcAmount(),
createOrder.getBtcBip70());
}
}
@Override
public void onError(Exception ex) {
if (resolvedListener != null) {
resolvedListener.onFailure(ex);
}
}
});
return true;
}
static private XmrToApi xmrToApi = null;
static private XmrToApi getXmrToApi() {
if (xmrToApi == null) {
synchronized (PaymentProtocolHelper.class) {
if (xmrToApi == null) {
xmrToApi = new XmrToApiImpl(OkHttpHelper.getOkHttpClient(),
Helper.getXmrToBaseUrl());
}
}
}
return xmrToApi;
}
}

Some files were not shown because too many files have changed in this diff Show More