Upload normal Monerujo code

This commit is contained in:
pokkst 2022-09-07 14:31:20 -05:00
parent ec12077c37
commit e6883a40d1
494 changed files with 68231 additions and 23 deletions

27
.circleci/config.yml Normal file
View file

@ -0,0 +1,27 @@
version: 2
jobs:
build:
working_directory: ~/code
docker:
- image: cimg/android:2022.03-ndk
environment:
JVM_OPTS: -Xmx3200m
steps:
- checkout
- restore_cache:
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}
- run:
name: Download Dependencies
command: ./gradlew androidDependencies
- save_cache:
paths:
- ~/.gradle
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}
- run:
name: Run Tests
command: ./gradlew test
- store_artifacts:
path: app/build/reports
destination: reports
- store_test_results:
path: app/build/test-results

41
.gitignore vendored
View file

@ -1,23 +1,18 @@
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
.gradle
/build
*.iml
/.idea
/local.properties
/captures
.externalNativeBuild
.DS_Store
/app/build
/app/release
/app/alpha
/app/prod
/app/alphaMainnet
/app/prodMainnet
/app/alphaStagenet
/app/prodStagenet
/app/.cxx
/monerujo.id

239
app/CMakeLists.txt Normal file
View file

@ -0,0 +1,239 @@
cmake_minimum_required(VERSION 3.4.1)
message(STATUS ABI_INFO = ${ANDROID_ABI})
add_library( monerujo
SHARED
src/main/cpp/monerujo.cpp )
set(EXTERNAL_LIBS_DIR ${CMAKE_SOURCE_DIR}/../external-libs)
############
# libsodium
############
add_library(sodium STATIC IMPORTED)
set_target_properties(sodium PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/libsodium.a)
############
# OpenSSL
############
add_library(crypto STATIC IMPORTED)
set_target_properties(crypto PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/libcrypto.a)
add_library(ssl STATIC IMPORTED)
set_target_properties(ssl PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/libssl.a)
############
# Boost
############
add_library(boost_chrono STATIC IMPORTED)
set_target_properties(boost_chrono PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/libboost_chrono.a)
add_library(boost_date_time STATIC IMPORTED)
set_target_properties(boost_date_time PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/libboost_date_time.a)
add_library(boost_filesystem STATIC IMPORTED)
set_target_properties(boost_filesystem PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/libboost_filesystem.a)
add_library(boost_program_options STATIC IMPORTED)
set_target_properties(boost_program_options PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/libboost_program_options.a)
add_library(boost_regex STATIC IMPORTED)
set_target_properties(boost_regex PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/libboost_regex.a)
add_library(boost_serialization STATIC IMPORTED)
set_target_properties(boost_serialization PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/libboost_serialization.a)
add_library(boost_system STATIC IMPORTED)
set_target_properties(boost_system PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/libboost_system.a)
add_library(boost_thread STATIC IMPORTED)
set_target_properties(boost_thread PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/libboost_thread.a)
add_library(boost_wserialization STATIC IMPORTED)
set_target_properties(boost_wserialization PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/libboost_wserialization.a)
#############
# Monero
#############
add_library(wallet_api STATIC IMPORTED)
set_target_properties(wallet_api PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libwallet_api.a)
add_library(wallet STATIC IMPORTED)
set_target_properties(wallet PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libwallet.a)
add_library(cryptonote_core STATIC IMPORTED)
set_target_properties(cryptonote_core PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libcryptonote_core.a)
add_library(cryptonote_basic STATIC IMPORTED)
set_target_properties(cryptonote_basic PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libcryptonote_basic.a)
add_library(mnemonics STATIC IMPORTED)
set_target_properties(mnemonics PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libmnemonics.a)
add_library(common STATIC IMPORTED)
set_target_properties(common PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libcommon.a)
add_library(cncrypto STATIC IMPORTED)
set_target_properties(cncrypto PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libcncrypto.a)
add_library(ringct STATIC IMPORTED)
set_target_properties(ringct PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libringct.a)
add_library(ringct_basic STATIC IMPORTED)
set_target_properties(ringct_basic PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libringct_basic.a)
add_library(blockchain_db STATIC IMPORTED)
set_target_properties(blockchain_db PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libblockchain_db.a)
add_library(lmdb STATIC IMPORTED)
set_target_properties(lmdb PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/liblmdb.a)
add_library(easylogging STATIC IMPORTED)
set_target_properties(easylogging PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libeasylogging.a)
add_library(unbound STATIC IMPORTED)
set_target_properties(unbound PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/libunbound.a)
add_library(epee STATIC IMPORTED)
set_target_properties(epee PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libepee.a)
add_library(blocks STATIC IMPORTED)
set_target_properties(blocks PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libblocks.a)
add_library(checkpoints STATIC IMPORTED)
set_target_properties(checkpoints PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libcheckpoints.a)
add_library(device STATIC IMPORTED)
set_target_properties(device PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libdevice.a)
add_library(device_trezor STATIC IMPORTED)
set_target_properties(device_trezor PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libdevice_trezor.a)
add_library(multisig STATIC IMPORTED)
set_target_properties(multisig PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libmultisig.a)
add_library(version STATIC IMPORTED)
set_target_properties(version PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libversion.a)
add_library(net STATIC IMPORTED)
set_target_properties(net PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libnet.a)
add_library(hardforks STATIC IMPORTED)
set_target_properties(hardforks PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libhardforks.a)
add_library(randomx STATIC IMPORTED)
set_target_properties(randomx PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/librandomx.a)
add_library(rpc_base STATIC IMPORTED)
set_target_properties(rpc_base PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/librpc_base.a)
add_library(wallet-crypto STATIC IMPORTED)
set_target_properties(wallet-crypto PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libwallet-crypto.a)
add_library(cryptonote_format_utils_basic STATIC IMPORTED)
set_target_properties(cryptonote_format_utils_basic PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libcryptonote_format_utils_basic.a)
#############
# System
#############
find_library( log-lib log )
include_directories( ${EXTERNAL_LIBS_DIR}/include )
message(STATUS EXTERNAL_LIBS_DIR : ${EXTERNAL_LIBS_DIR})
if(${ANDROID_ABI} STREQUAL "x86_64")
set(EXTRA_LIBS "wallet-crypto")
else()
set(EXTRA_LIBS "")
endif()
target_link_libraries( monerujo
wallet_api
wallet
cryptonote_core
cryptonote_basic
cryptonote_format_utils_basic
mnemonics
ringct
ringct_basic
net
common
cncrypto
blockchain_db
lmdb
easylogging
unbound
epee
blocks
checkpoints
device
device_trezor
multisig
version
randomx
hardforks
rpc_base
${EXTRA_LIBS}
boost_chrono
boost_date_time
boost_filesystem
boost_program_options
boost_regex
boost_serialization
boost_system
boost_thread
boost_wserialization
ssl
crypto
sodium
${log-lib}
)

159
app/build.gradle Normal file
View file

@ -0,0 +1,159 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 31
buildToolsVersion '30.0.3'
ndkVersion '17.2.4988734'
defaultConfig {
applicationId "com.m2049r.xmrwallet"
minSdkVersion 21
targetSdkVersion 31
versionCode 3002
versionName "3.0.2 'Fluorine Fermi'"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
cppFlags "-std=c++11"
arguments '-DANDROID_STL=c++_shared'
}
}
}
bundle {
language {
enableSplit = false
}
}
flavorDimensions 'type', 'net'
productFlavors {
mainnet {
dimension 'net'
}
stagenet {
dimension 'net'
applicationIdSuffix '.stage'
versionNameSuffix ' (stage)'
}
devnet {
dimension 'net'
applicationIdSuffix '.test'
versionNameSuffix ' (test)'
}
alpha {
dimension 'type'
applicationIdSuffix '.alpha'
versionNameSuffix ' (alpha)'
}
prod {
dimension 'type'
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
applicationIdSuffix ".debug"
}
applicationVariants.all { variant ->
variant.buildConfigField "String", "ID_A", "\"" + getId("ID_A") + "\""
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
splits {
abi {
enable true
reset()
include 'armeabi-v7a', 'arm64-v8a', 'x86_64'
universalApk true
}
}
// Map for the version code that gives each ABI a value.
def abiCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4]
// Enumerate translated locales
def availableLocales = ["en"]
new File("app/src/main/res/").eachFileMatch(~/^values-.*/) { file ->
def languageTag = file.name.substring(7).replace("-r", "-")
if (languageTag != "night")
availableLocales.add(languageTag)
}
// APKs for the same app that all have the same version information.
android.applicationVariants.all { variant ->
// Update string resource: available_locales
variant.resValue("string", "available_locales", availableLocales.join(","))
// Assigns a different version code for each output APK.
variant.outputs.all {
output ->
def abiName = output.getFilter(com.android.build.OutputFile.ABI)
output.versionCodeOverride = abiCodes.get(abiName, 0) + 10 * versionCode
if (abiName == null) abiName = "universal"
def v = "${variant.versionName}".replaceFirst(" '.*' ?", "")
.replace(".", "x")
.replace("(", "-")
.replace(")", "")
outputFileName = "$rootProject.ext.apkName-" + v + "_" + abiName + ".apk"
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
namespace 'com.m2049r.xmrwallet'
}
static def getId(name) {
Properties props = new Properties()
props.load(new FileInputStream(new File('monerujo.id')))
return props[name]
}
dependencies {
implementation 'androidx.core:core:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.preference:preference:1.2.0'
implementation 'com.google.android.material:material:1.6.0'
implementation 'me.dm7.barcodescanner:zxing:1.9.8'
implementation "com.squareup.okhttp3:okhttp:4.9.3"
implementation "io.github.rburgst:okhttp-digest:2.6"
implementation "com.jakewharton.timber:timber:5.0.1"
implementation 'info.guardianproject.netcipher:netcipher:2.1.0'
//implementation 'info.guardianproject.netcipher:netcipher-okhttp3:2.1.0'
implementation fileTree(dir: 'libs/classes', include: ['*.jar'])
implementation 'com.nulab-inc:zxcvbn:1.5.2'
implementation 'dnsjava:dnsjava:2.1.9'
implementation 'org.jitsi:dnssecjava:1.2.0'
implementation 'org.slf4j:slf4j-nop:1.7.36'
implementation 'com.github.brnunes:swipeablerecyclerview:1.0.2'
//noinspection GradleDependency
testImplementation "junit:junit:4.13.2"
testImplementation "org.mockito:mockito-all:1.10.19"
testImplementation "com.squareup.okhttp3:mockwebserver:4.9.3"
testImplementation 'org.json:json:20211205'
testImplementation 'net.jodah:concurrentunit:0.4.6'
compileOnly 'org.projectlombok:lombok:1.18.22'
annotationProcessor 'org.projectlombok:lombok:1.18.22'
}

25
app/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,25 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in C:\Users\Test\AppData\Local\Android\Sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View file

@ -0,0 +1,111 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<queries>
<intent>
<action android:name="org.torproject.android.intent.action.START" />
</intent>
<intent>
<action android:name="org.torproject.android.intent.action.STATUS" />
</intent>
<intent>
<action android:name="org.torproject.android.REQUEST_HS_PORT" />
</intent>
<intent>
<action android:name="org.torproject.android.REQUEST_V3_ONION_SERVICE" />
</intent>
<package android:name="org.torproject.android" />
</queries>
<application
android:name=".XmrWalletApplication"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:preserveLegacyExternalStorage="true"
android:supportsRtl="true"
android:theme="@style/MyMaterialThemeClassic"
android:usesCleartextTraffic="true">
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|uiMode"
android:exported="true"
android:launchMode="singleTop"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".WalletActivity"
android:configChanges="orientation|keyboardHidden|uiMode"
android:label="@string/wallet_activity_name"
android:launchMode="singleTask"
android:screenOrientation="behind" />
<activity
android:name=".LoginActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="true"
android:label="@string/app_name"
android:launchMode="singleTop"
android:screenOrientation="locked">
<intent-filter>
<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" />
</activity>
<activity
android:name=".onboarding.OnBoardingActivity"
android:configChanges="orientation|keyboardHidden|uiMode"
android:launchMode="singleTask"
android:screenOrientation="portrait" />
<service
android:name=".service.WalletService"
android:description="@string/service_description"
android:exported="false"
android:label="Monero Wallet Service" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
</application>
</manifest>

View file

@ -0,0 +1,829 @@
<h1>Open Source Licenses</h1>
<h2>Licensed under the Apache License, Version 2.0</h2>
<h3>monerujo (https://github.com/m2049r/xmrwallet)</h3>
Copyright (c) 2017-2018 m2049r et al.
<h3>The Android Open Source Project</h3>
<ul>
<li>com.android.support:design</li>
<li>com.android.support:support-v4</li>
<li>com.android.support:appcompat-v7</li>
<li>com.android.support:recyclerview-v7</li>
<li>com.android.support:cardview-v7</li>
<li>com.android.support.constraint:constraint-layout</li>
<li>com.android.support:support-annotations</li>
<li>com.android.support:support-vector-drawable</li>
<li>com.android.support:animated-vector-drawable</li>
<li>com.android.support:transition</li>
<li>com.android.support:support-compat</li>
<li>com.android.support:support-media-compat</li>
<li>com.android.support:support-core-utils</li>
<li>com.android.support:support-core-ui</li>
<li>com.android.support:support-fragment</li>
<li>com.android.support.constraint:constraint-layout-solver</li>
</ul>
Copyright (c) The Android Open Source Project
<h3>OkHttp</h3>
Copyright (c) 2014 Square, Inc.
<h3>Timber</h3>
Copyright (c) 2013 Jake Wharton
<h3>com.google.zxing:core</h3>
Copyright (c) 2012 ZXing authors
<h3>me.dm7.barcodescanner</h3>
<ul>
<li>me.dm7.barcodescanner:core</li>
<li>me.dm7.barcodescanner:zxing</li>
</ul>
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/>
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION<br/>
<br/>
1. Definitions.<br/>
<br/>
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.<br/>
<br/>
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.<br/>
<br/>
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.<br/>
<br/>
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.<br/>
<br/>
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.<br/>
<br/>
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.<br/>
<br/>
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).<br/>
<br/>
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.<br/>
<br/>
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."<br/>
<br/>
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.<br/>
<br/>
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.<br/>
<br/>
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.<br/>
<br/>
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:<br/>
<br/>
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and<br/>
<br/>
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and<br/>
<br/>
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and<br/>
<br/>
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.<br/>
<br/>
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.<br/>
<br/>
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.<br/>
<br/>
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.<br/>
<br/>
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.<br/>
<br/>
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.<br/>
<br/>
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
<h2>dnsjava (http://dnsjava.org/)</h2>
Copyright (c) 1998-2011, Brian Wellington. All rights reserved.<br/>
<h3>The 2-Clause BSD License</h3>
<br/>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:<br/>
<br/>
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.<br/>
<br/>
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.<br/>
<br/>
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
<h2>dnssecjava - a DNSSEC validating stub resolver for Java</h2>
Copyright (c) 2013-2015 Ingo Bauersachs
<h3>The Eclipse Public License - v 1.0</h3>
THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC
LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM
CONSTITUTES RECIPIENT&apos;S ACCEPTANCE OF THIS AGREEMENT.<br/>
<br/>
1. DEFINITIONS<br/>
<br/>
"Contribution" means:<br/>
<br/>
a) in the case of the initial Contributor, the initial code and documentation
distributed under this Agreement, and<br/>
<br/>
b) in the case of each subsequent Contributor:<br/>
<br/>
i) changes to the Program, and<br/>
<br/>
ii) additions to the Program;<br/>
<br/>
where such changes and/or additions to the Program originate from and are distributed
by that particular Contributor. A Contribution &apos;originates&apos; from a Contributor if it was
added to the Program by such Contributor itself or anyone acting on such Contributor&apos;s
behalf. Contributions do not include additions to the Program which: (i) are separate modules
of software distributed in conjunction with the Program under their own license agreement, and
(ii) are not derivative works of the Program.<br/>
<br/>
"Contributor" means any person or entity that distributes the Program.<br/>
<br/>
"Licensed Patents" mean patent claims licensable by a Contributor which are necessarily
infringed by the use or sale of its Contribution alone or when combined with the Program.<br/>
<br/>
"Program" means the Contributions distributed in accordance with this Agreement.<br/>
<br/>
"Recipient" means anyone who receives the Program under this Agreement, including all
Contributors.<br/>
<br/>
2. GRANT OF RIGHTS<br/>
<br/>
a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a
non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative
works of, publicly display, publicly perform, distribute and sublicense the Contribution of
such Contributor, if any, and such derivative works, in source code and object code form.<br/>
<br/>
b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a
non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use,
sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any,
in source code and object code form. This patent license shall apply to the combination of
the Contribution and the Program if, at the time the Contribution is added by the Contributor,
such addition of the Contribution causes such combination to be covered by the Licensed Patents.
The patent license shall not apply to any other combinations which include the Contribution.
No hardware per se is licensed hereunder.<br/>
<br/>
c) Recipient understands that although each Contributor grants the licenses to its Contributions
set forth herein, no assurances are provided by any Contributor that the Program does not
infringe the patent or other intellectual property rights of any other entity. Each Contributor
disclaims any liability to Recipient for claims brought by any other entity based on
infringement of intellectual property rights or otherwise. As a condition to exercising the
rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to
secure any other intellectual property rights needed, if any. For example, if a third party
patent license is required to allow Recipient to distribute the Program, it is Recipient&apos;s
responsibility to acquire that license before distributing the Program.<br/>
<br/>
d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its
Contribution, if any, to grant the copyright license set forth in this Agreement.<br/>
<br/>
3. REQUIREMENTS<br/>
<br/>
A Contributor may choose to distribute the Program in object code form under its own license
agreement, provided that:<br/>
<br/>
a) it complies with the terms and conditions of this Agreement; and<br/>
<br/>
b) its license agreement:<br/>
<br/>
i) effectively disclaims on behalf of all Contributors all warranties and conditions,
express and implied, including warranties or conditions of title and non-infringement,
and implied warranties or conditions of merchantability and fitness for a particular purpose;<br/>
<br/>
ii) effectively excludes on behalf of all Contributors all liability for damages, including
direct, indirect, special, incidental and consequential damages, such as lost profits;<br/>
<br/>
iii) states that any provisions which differ from this Agreement are offered by that Contributor
alone and not by any other party; and<br/>
<br/>
iv) states that source code for the Program is available from such Contributor, and informs
licensees how to obtain it in a reasonable manner on or through a medium customarily used for
software exchange.<br/>
<br/>
When the Program is made available in source code form:<br/>
<br/>
a) it must be made available under this Agreement; and<br/>
<br/>
b) a copy of this Agreement must be included with each copy of the Program.<br/>
<br/>
Contributors may not remove or alter any copyright notices contained within the Program.<br/>
<br/>
Each Contributor must identify itself as the originator of its Contribution, if any, in a
manner that reasonably allows subsequent Recipients to identify the originator of the
Contribution.<br/>
<br/>
4. COMMERCIAL DISTRIBUTION<br/>
<br/>
Commercial distributors of software may accept certain responsibilities with respect to end
users, business partners and the like. While this license is intended to facilitate the
commercial use of the Program, the Contributor who includes the Program in a commercial
product offering should do so in a manner which does not create potential liability for
other Contributors. Therefore, if a Contributor includes the Program in a commercial
product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and
indemnify every other Contributor ("Indemnified Contributor") against any losses, damages
and costs (collectively "Losses") arising from claims, lawsuits and other legal actions
brought by a third party against the Indemnified Contributor to the extent caused by the
acts or omissions of such Commercial Contributor in connection with its distribution of the
Program in a commercial product offering. The obligations in this section do not apply to
any claims or Losses relating to any actual or alleged intellectual property infringement.
In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial
Contributor in writing of such claim, and b) allow the Commercial Contributor to control,
and cooperate with the Commercial Contributor in, the defense and any related settlement
negotiations. The Indemnified Contributor may participate in any such claim at its own expense.<br/>
<br/>
For example, a Contributor might include the Program in a commercial product offering,
Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor
then makes performance claims, or offers warranties related to Product X, those performance
claims and warranties are such Commercial Contributor&apos;s responsibility alone. Under this
section, the Commercial Contributor would have to defend claims against the other Contributors
related to those performance claims and warranties, and if a court requires any other Contributor
to pay any damages as a result, the Commercial Contributor must pay those damages.<br/>
<br/>
5. NO WARRANTY<br/>
<br/>
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT
LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR
FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the
appropriateness of using and distributing the Program and assumes all risks associated with
its exercise of rights under this Agreement , including but not limited to the risks and costs
of program errors, compliance with applicable laws, damage to or loss of data, programs or
equipment, and unavailability or interruption of operations.<br/>
<br/>
6. DISCLAIMER OF LIABILITY<br/>
<br/>
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL
HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS
GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.<br/>
<br/>
7. GENERAL<br/>
<br/>
If any provision of this Agreement is invalid or unenforceable under applicable law, it shall
not affect the validity or enforceability of the remainder of the terms of this Agreement,
and without further action by the parties hereto, such provision shall be reformed to the
minimum extent necessary to make such provision valid and enforceable.<br/>
<br/>
If Recipient institutes patent litigation against any entity (including a cross-claim or
counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the
Program with other software or hardware) infringes such Recipient&apos;s patent(s), then such
Recipient&apos;s rights granted under Section 2(b) shall terminate as of the date such litigation
is filed.<br/>
<br/>
All Recipient&apos;s rights under this Agreement shall terminate if it fails to comply with any of
the material terms or conditions of this Agreement and does not cure such failure in a
reasonable period of time after becoming aware of such noncompliance. If all Recipient&apos;s
rights under this Agreement terminate, Recipient agrees to cease use and distribution of the
Program as soon as reasonably practicable. However, Recipient&apos;s obligations under this
Agreement and any licenses granted by Recipient relating to the Program shall continue
and survive.<br/>
<br/>
Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid
inconsistency the Agreement is copyrighted and may only be modified in the following manner.
The Agreement Steward reserves the right to publish new versions (including revisions)
of this Agreement from time to time. No one other than the Agreement Steward has the
right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward.
The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to
a suitable separate entity. Each new version of the Agreement will be given a distinguishing
version number. The Program (including Contributions) may always be distributed subject to
the version of the Agreement under which it was received. In addition, after a new version
of the Agreement is published, Contributor may elect to distribute the Program (including
its Contributions) under the new version. Except as expressly stated in Sections 2(a) and
2(b) above, Recipient receives no rights or licenses to the intellectual property of any
Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise.
All rights in the Program not expressly granted under this Agreement are reserved.<br/>
<br/>
This Agreement is governed by the laws of the State of New York and the intellectual property
laws of the United States of America. No party to this Agreement will bring a legal action
under this Agreement more than one year after the cause of action arose. Each party waives
its rights to a jury trial in any resulting litigation.
<h2>Licensed under the MIT License</h2>
<h3>rapidjson (https://github.com/monero-project/monero/blob/master/external/rapidjson)</h3>
Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. All rights reserved.
<h3>easylogging++ (https://github.com/monero-project/monero/tree/master/external/easylogging%2B%2B)</h3>
Copyright (c) 2017 muflihun.com
<h3>zxcvbn4j (https://github.com/nulab/zxcvbn4j)</h3>
Copyright (c) 2014 Nulab Inc
<h3>slfj-nop - Simple Logging Facade for Java no-operation binding (https://www.slf4j.org/)</h3>
Copyright (c) 2004-2017 QOS.ch
<h3>The MIT License</h3>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute,
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:<br/>
<br/>
The above copyright notice and this permission notice shall be included in all copies or
substantial portions of the Software.<br/>
<br/>
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.<br/>
<h2>Monero (https://github.com/monero-project/monero)</h2>
<h3>The Monero Project License</h3>
Copyright (c) 2014-2017, The Monero Project. All rights reserved.<br/>
<br/>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:<br/>
<br/>
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.<br/>
<br/>
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.<br/>
<br/>
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.<br/>
<br/>
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.<br/>
<br/>
Parts of the project are originally copyright (c) 2012-2013 The Cryptonote
developers
<h2>OpenSSL (https://github.com/openssl/openssl)</h2>
<h3>LICENSE ISSUES</h3>
The OpenSSL toolkit stays under a double license, i.e. both the conditions of
the OpenSSL License and the original SSLeay license apply to the toolkit.
See below for the actual license texts.
<h3>OpenSSL License</h3>
Copyright (c) 1998-2017 The OpenSSL Project. All rights reserved.<br/>
<br/>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:<br/>
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.<br/>
<br/>
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.<br/>
<br/>
3. All advertising materials mentioning features or use of this
software must display the following acknowledgment:
"This product includes software developed by the OpenSSL Project
for use in the OpenSSL Toolkit. (http://www.openssl.org/)"<br/>
<br/>
4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to
endorse or promote products derived from this software without
prior written permission. For written permission, please contact
openssl-core@openssl.org.<br/>
<br/>
5. Products derived from this software may not be called "OpenSSL"
nor may "OpenSSL" appear in their names without prior written
permission of the OpenSSL Project.<br/>
<br/>
6. Redistributions of any form whatsoever must retain the following
acknowledgment:<br/>
"This product includes software developed by the OpenSSL Project
for use in the OpenSSL Toolkit (http://www.openssl.org/)"<br/>
<br/>
THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT "AS IS" AND ANY
EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR
ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
OF THE POSSIBILITY OF SUCH DAMAGE.<br/>
<br/>
This product includes cryptographic software written by Eric Young
(eay@cryptsoft.com). This product includes software written by Tim
Hudson (tjh@cryptsoft.com).
<h3>Original SSLeay License</h3>
Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com). All rights reserved.<br/>
<br/>
This package is an SSL implementation written
by Eric Young (eay@cryptsoft.com).
The implementation was written so as to conform with Netscapes SSL.<br/>
<br/>
This library is free for commercial and non-commercial use as long as
the following conditions are aheared to. The following conditions
apply to all code found in this distribution, be it the RC4, RSA,
lhash, DES, etc., code; not just the SSL code. The SSL documentation
included with this distribution is covered by the same copyright terms
except that the holder is Tim Hudson (tjh@cryptsoft.com).<br/>
<br/>
Copyright remains Eric Young&apos;s, and as such any Copyright notices in
the code are not to be removed.
If this package is used in a product, Eric Young should be given attribution
as the author of the parts of the library used.
This can be in the form of a textual message at program startup or
in documentation (online or textual) provided with the package.<br/>
<br/>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:<br/>
1. Redistributions of source code must retain the copyright
notice, this list of conditions and the following disclaimer.<br/>
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.<br/>
3. All advertising materials mentioning features or use of this software
must display the following acknowledgement:<br/>
"This product includes cryptographic software written by
Eric Young (eay@cryptsoft.com)"
The word &apos;cryptographic&apos; can be left out if the rouines from the library
being used are not cryptographic related :-).<br/>
4. If you include any Windows specific code (or a derivative thereof) from
the apps directory (application code) you must include an acknowledgement:
"This product includes software written by Tim Hudson (tjh@cryptsoft.com)"<br/>
<br/>
THIS SOFTWARE IS PROVIDED BY ERIC YOUNG "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.<br/>
<br/>
The licence and distribution terms for any publically available version or
derivative of this code cannot be changed. i.e. this code cannot simply be
copied and put under another distribution licence
[including the GNU Public Licence.]
<h2>Boost</h2>
<ul>
<li>Boost (https://sourceforge.net/projects/boost)</li>
<li>Boost/Archive (https://github.com/monero-project/monero/tree/master/external/boost/archive)</li>
</ul>
<h3>Boost Software License - Version 1.0 - August 17th, 2003</h3>
Permission is hereby granted, free of charge, to any person or organization
obtaining a copy of the software and accompanying documentation covered by
this license (the "Software") to use, reproduce, display, distribute,
execute, and transmit the Software, and to prepare derivative works of the
Software, and to permit third-parties to whom the Software is furnished to
do so, all subject to the following:<br/>
<br/>
The copyright notices in the Software and this entire statement, including
the above license grant, this restriction and the following disclaimer,
must be included in all copies of the Software, in whole or in part, and
all derivative works of the Software, unless such copies or derivative
works are solely in the form of machine-executable object code generated by
a source language processor.<br/>
<br/>
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
<h2>Unbound (https://github.com/monero-project/monero/blob/master/external/unbound)</h2>
<h3>Unbound Software License</h3>
Copyright (c) 2007, NLnet Labs. All rights reserved.<br/>
<br/>
This software is open source.<br/>
<br/>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:<br/>
<br/>
Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.<br/>
<br/>
Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.<br/>
<br/>
Neither the name of the NLNET LABS nor the names of its contributors may
be used to endorse or promote products derived from this software without
specific prior written permission.<br/>
<br/>
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
<h2>MiniUPnPc (https://github.com/monero-project/monero/blob/master/external/miniupnpc)</h2>
Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. All rights reserved.
<h3>The MiniUPnPc License</h3>
Copyright (c) 2005-2015, Thomas BERNARD. All rights reserved.<br/>
<br/>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:<br/>
<br/>
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.<br/>
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.<br/>
* The name of the author may not be used to endorse or promote products
derived from this software without specific prior written permission.<br/>
<br/>
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
<h2>liblmdb (https://github.com/monero-project/monero/blob/master/external/db_drivers/liblmdb)</h2>
<h3>The OpenLDAP Public License, Version 2.8, 17 August 2003</h3>
Redistribution and use of this software and associated documentation
("Software"), with or without modification, are permitted provided
that the following conditions are met:<br/>
<br/>
1. Redistributions in source form must retain copyright statements
and notices,<br/>
<br/>
2. Redistributions in binary form must reproduce applicable copyright
statements and notices, this list of conditions, and the following
disclaimer in the documentation and/or other materials provided
with the distribution, and<br/>
<br/>
3. Redistributions must contain a verbatim copy of this document.<br/>
<br/>
The OpenLDAP Foundation may revise this license from time to time.
Each revision is distinguished by a version number. You may use
this Software under terms of this license revision or under the
terms of any subsequent revision of the license.<br/>
<br/>
THIS SOFTWARE IS PROVIDED BY THE OPENLDAP FOUNDATION AND ITS
CONTRIBUTORS "AS IS" AND ANY EXPRESSED OR IMPLIED WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL THE OPENLDAP FOUNDATION, ITS CONTRIBUTORS, OR THE AUTHOR(S)
OR OWNER(S) OF THE SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.<br/>
<br/>
The names of the authors and copyright holders must not be used in
advertising or otherwise to promote the sale, use or other dealing
in this Software without specific, written prior permission. Title
to copyright in this Software shall at all times remain with copyright
holders.<br/>
<br/>
OpenLDAP is a registered trademark of the OpenLDAP Foundation.<br/>
<br/>
Copyright 1999-2003 The OpenLDAP Foundation, Redwood City,
California, USA. All Rights Reserved. Permission to copy and
distribute verbatim copies of this document is granted.
<h2>epee (https://github.com/monero-project/monero/blob/master/contrib/epee)</h2>
Copyright (c) 2006-2013, Andrey N. Sabelnikov, www.sabelnikov.net. All rights reserved.
<h3>The epee License</h3>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:<br/>
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.<br/>
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.<br/>
* Neither the name of the Andrey N. Sabelnikov nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.<br/>
<br/>
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL Andrey N. Sabelnikov BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
<h2>&apos;Poppins&apos; Font</h2>
<h3>SIL Open Font License</h3>
<p>Copyright (c) 2014, Indian Type Foundry (info@indiantypefoundry.com).</p>
<p>This Font Software is licensed under the SIL Open Font License, Version 1.1.<br />
This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL</p>
<p>&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;-<br />
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007<br />
&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;-</p>
<p>PREAMBLE<br />
The goals of the Open Font License (OFL) are to stimulate worldwide development of
collaborative font projects, to support the font creation efforts of academic and
linguistic communities, and to provide a free and open framework in which fonts may be
shared and improved in partnership with others.</p>
<p>The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as
long as they are not sold by themselves. The fonts, including any derivative works, can be
bundled, embedded, redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives, however, cannot be
released under any other type of license. The requirement for fonts to remain under this
license does not apply to any document created using the fonts or their derivatives.</p>
<p>DEFINITIONS<br />
&#8220;Font Software&#8221; refers to the set of files released by the Copyright Holder(s)
under this license and clearly marked as such. This may include source files, build scripts
and documentation.</p>
<p>&#8220;Reserved Font Name&#8221; refers to any names specified as such after the copyright
statement(s).</p>
<p>&#8220;Original Version&#8221; refers to the collection of Font Software components as
distributed by the Copyright Holder(s).</p>
<p>&#8220;Modified Version&#8221; refers to any derivative made by adding to, deleting, or
substituting&#8212;in part or in whole&#8212;any of the components of the Original Version,
by changing formats or by porting the Font Software to a new environment.</p>
<p>&#8220;Author&#8221; refers to any designer, engineer, programmer, technical writer or other
person who contributed to the Font Software.</p>
<p>PERMISSION &amp; CONDITIONS<br />
Permission is hereby granted, free of charge, to any person obtaining a copy of the Font
Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and
unmodified copies of the Font Software, subject to the following conditions:</p>
<p>1) Neither the Font Software nor any of its individual components, in Original or Modified
Versions, may be sold by itself.</p>
<p>2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold
with any software, provided that each copy contains the above copyright notice and this
license. These can be included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or binary files as long as
those fields can be easily viewed by the user.</p>
<p>3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit
written permission is granted by the corresponding Copyright Holder. This restriction only
applies to the primary font name as presented to the users.</p>
<p>4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be
used to promote, endorse or advertise any Modified Version, except to acknowledge the
contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit
written permission.</p>
<p>5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely
under this license, and must not be distributed under any other license. The requirement
for fonts to remain under this license does not apply to any document created using the Font
Software.</p>
<p>TERMINATION<br />
This license becomes null and void if any of the above conditions are not met.</p>
<p>DISCLAIMER<br />
THE FONT SOFTWARE IS PROVIDED &#8220;AS IS&#8221;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN
NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN
AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE
THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.</p>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,79 @@
/**
* Copyright (c) 2017 m2049r
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.
*/
#ifndef XMRWALLET_WALLET_LIB_H
#define XMRWALLET_WALLET_LIB_H
#include <jni.h>
/*
#include <android/log.h>
#define LOG_TAG "[NDK]"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
*/
jfieldID getHandleField(JNIEnv *env, jobject obj, const char *fieldName = "handle") {
jclass c = env->GetObjectClass(obj);
return env->GetFieldID(c, fieldName, "J"); // of type long
}
template<typename T>
T *getHandle(JNIEnv *env, jobject obj, const char *fieldName = "handle") {
jlong handle = env->GetLongField(obj, getHandleField(env, obj, fieldName));
return reinterpret_cast<T *>(handle);
}
void setHandleFromLong(JNIEnv *env, jobject obj, jlong handle) {
env->SetLongField(obj, getHandleField(env, obj), handle);
}
template<typename T>
void setHandle(JNIEnv *env, jobject obj, T *t) {
jlong handle = reinterpret_cast<jlong>(t);
setHandleFromLong(env, obj, handle);
}
#ifdef __cplusplus
extern "C"
{
#endif
extern const char* const MONERO_VERSION; // the actual monero core version
// from monero-core crypto/hash-ops.h - avoid #including monero code here
enum {
HASH_SIZE = 32,
HASH_DATA_AREA = 136
};
void cn_slow_hash(const void *data, size_t length, char *hash, int variant, int prehashed, uint64_t height);
inline void slow_hash(const void *data, const size_t length, char *hash) {
cn_slow_hash(data, length, hash, 0 /*variant*/, 0 /*prehashed*/, 0 /*height*/);
}
inline void slow_hash_broken(const void *data, char *hash, int variant) {
cn_slow_hash(data, 200 /*sizeof(union hash_state)*/, hash, variant, 1 /*prehashed*/, 0 /*height*/);
}
#ifdef __cplusplus
}
#endif
#endif //XMRWALLET_WALLET_LIB_H

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

View file

@ -0,0 +1,53 @@
/*
*******************************************************************************
* BTChip Bitcoin Hardware Wallet Java API
* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn
*
* 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.btchip;
public class BTChipException extends Exception {
private static final long serialVersionUID = 5512803003827126405L;
public BTChipException(String reason) {
super(reason);
}
public BTChipException(String reason, Throwable cause) {
super(reason, cause);
}
public BTChipException(String reason, int sw) {
super(reason);
this.sw = sw;
}
public int getSW() {
return sw;
}
public String toString() {
if (sw == 0) {
return "BTChip Exception : " + getMessage();
} else {
return "BTChip Exception : " + getMessage() + " " + Integer.toHexString(sw);
}
}
private int sw;
}

View file

@ -0,0 +1,31 @@
/*
*******************************************************************************
* BTChip Bitcoin Hardware Wallet Java API
* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn
* (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.btchip.comm;
import com.btchip.BTChipException;
public interface BTChipTransport {
byte[] exchange(byte[] command);
void close();
void setDebug(boolean debugFlag);
}

View file

@ -0,0 +1,126 @@
/*
*******************************************************************************
* BTChip Bitcoin Hardware Wallet Java API
* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn
* (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.btchip.comm;
import java.io.ByteArrayOutputStream;
public class LedgerHelper {
private static final int TAG_APDU = 0x05;
public static byte[] wrapCommandAPDU(int channel, byte[] command, int packetSize) {
ByteArrayOutputStream output = new ByteArrayOutputStream();
if (packetSize < 3) {
throw new IllegalArgumentException("Can't handle Ledger framing with less than 3 bytes for the report");
}
int sequenceIdx = 0;
int offset = 0;
output.write(channel >> 8);
output.write(channel);
output.write(TAG_APDU);
output.write(sequenceIdx >> 8);
output.write(sequenceIdx);
sequenceIdx++;
output.write(command.length >> 8);
output.write(command.length);
int blockSize = (command.length > packetSize - 7 ? packetSize - 7 : command.length);
output.write(command, offset, blockSize);
offset += blockSize;
while (offset != command.length) {
output.write(channel >> 8);
output.write(channel);
output.write(TAG_APDU);
output.write(sequenceIdx >> 8);
output.write(sequenceIdx);
sequenceIdx++;
blockSize = (command.length - offset > packetSize - 5 ? packetSize - 5 : command.length - offset);
output.write(command, offset, blockSize);
offset += blockSize;
}
if ((output.size() % packetSize) != 0) {
byte[] padding = new byte[packetSize - (output.size() % packetSize)];
output.write(padding, 0, padding.length);
}
return output.toByteArray();
}
public static byte[] unwrapResponseAPDU(int channel, byte[] data, int packetSize) {
ByteArrayOutputStream response = new ByteArrayOutputStream();
int offset = 0;
int responseLength;
int sequenceIdx = 0;
if ((data == null) || (data.length < 7 + 5)) {
return null;
}
if (data[offset++] != (channel >> 8)) {
throw new IllegalArgumentException("Invalid channel");
}
if (data[offset++] != (channel & 0xff)) {
throw new IllegalArgumentException("Invalid channel");
}
if (data[offset++] != TAG_APDU) {
throw new IllegalArgumentException("Invalid tag");
}
if (data[offset++] != 0x00) {
throw new IllegalArgumentException("Invalid sequence");
}
if (data[offset++] != 0x00) {
throw new IllegalArgumentException("Invalid sequence");
}
responseLength = ((data[offset++] & 0xff) << 8);
responseLength |= (data[offset++] & 0xff);
if (data.length < 7 + responseLength) {
return null;
}
int blockSize = (responseLength > packetSize - 7 ? packetSize - 7 : responseLength);
response.write(data, offset, blockSize);
offset += blockSize;
while (response.size() != responseLength) {
sequenceIdx++;
if (offset == data.length) {
return null;
}
if (data[offset++] != (channel >> 8)) {
throw new IllegalArgumentException("Invalid channel");
}
if (data[offset++] != (channel & 0xff)) {
throw new IllegalArgumentException("Invalid channel");
}
if (data[offset++] != TAG_APDU) {
throw new IllegalArgumentException("Invalid tag");
}
if (data[offset++] != (sequenceIdx >> 8)) {
throw new IllegalArgumentException("Invalid sequence");
}
if (data[offset++] != (sequenceIdx & 0xff)) {
throw new IllegalArgumentException("Invalid sequence");
}
blockSize = (responseLength - response.size() > packetSize - 5 ? packetSize - 5 : responseLength - response.size());
if (blockSize > data.length - offset) {
return null;
}
response.write(data, offset, blockSize);
offset += blockSize;
}
return response.toByteArray();
}
}

View file

@ -0,0 +1,149 @@
/*
*******************************************************************************
* BTChip Bitcoin Hardware Wallet Java API
* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn
* (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.btchip.comm.android;
import android.hardware.usb.UsbConstants;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbEndpoint;
import android.hardware.usb.UsbInterface;
import android.hardware.usb.UsbManager;
import android.hardware.usb.UsbRequest;
import com.btchip.BTChipException;
import com.btchip.comm.BTChipTransport;
import com.btchip.comm.LedgerHelper;
import com.btchip.utils.Dump;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.HashMap;
import timber.log.Timber;
public class BTChipTransportAndroidHID implements BTChipTransport {
public static UsbDevice getDevice(UsbManager manager) {
HashMap<String, UsbDevice> deviceList = manager.getDeviceList();
for (UsbDevice device : deviceList.values()) {
Timber.d("%04X:%04X %s, %s", device.getVendorId(), device.getProductId(), device.getManufacturerName(), device.getProductName());
if (device.getVendorId() == VID) {
final int deviceProductId = device.getProductId();
for (int pid : PID_HIDS) {
if (deviceProductId == pid)
return device;
}
}
}
return null;
}
public static BTChipTransport open(UsbManager manager, UsbDevice device) throws IOException {
UsbDeviceConnection connection = manager.openDevice(device);
if (connection == null) throw new IOException("Device not connected");
// Must only be called once permission is granted (see http://developer.android.com/reference/android/hardware/usb/UsbManager.html)
// Important if enumerating, rather than being awaken by the intent notification
UsbInterface dongleInterface = device.getInterface(0);
UsbEndpoint in = null;
UsbEndpoint out = null;
for (int i = 0; i < dongleInterface.getEndpointCount(); i++) {
UsbEndpoint tmpEndpoint = dongleInterface.getEndpoint(i);
if (tmpEndpoint.getDirection() == UsbConstants.USB_DIR_IN) {
in = tmpEndpoint;
} else {
out = tmpEndpoint;
}
}
connection.claimInterface(dongleInterface, true);
return new BTChipTransportAndroidHID(connection, dongleInterface, in, out);
}
private static final int VID = 0x2C97;
private static final int[] PID_HIDS = {0x0001, 0x0004, 0x0005};
private UsbDeviceConnection connection;
private UsbInterface dongleInterface;
private UsbEndpoint in;
private UsbEndpoint out;
private byte transferBuffer[];
private boolean debug;
public BTChipTransportAndroidHID(UsbDeviceConnection connection, UsbInterface dongleInterface, UsbEndpoint in, UsbEndpoint out) {
this.connection = connection;
this.dongleInterface = dongleInterface;
this.in = in;
this.out = out;
transferBuffer = new byte[HID_BUFFER_SIZE];
}
@Override
public byte[] exchange(byte[] command) {
ByteArrayOutputStream response = new ByteArrayOutputStream();
byte[] responseData = null;
int offset = 0;
if (debug) {
Timber.d("=> %s", Dump.dump(command));
}
command = LedgerHelper.wrapCommandAPDU(LEDGER_DEFAULT_CHANNEL, command, HID_BUFFER_SIZE);
UsbRequest requestOut = new UsbRequest();
requestOut.initialize(connection, out);
while (offset != command.length) {
int blockSize = (command.length - offset > HID_BUFFER_SIZE ? HID_BUFFER_SIZE : command.length - offset);
System.arraycopy(command, offset, transferBuffer, 0, blockSize);
requestOut.queue(ByteBuffer.wrap(transferBuffer), HID_BUFFER_SIZE);
connection.requestWait();
offset += blockSize;
}
requestOut.close();
ByteBuffer responseBuffer = ByteBuffer.allocate(HID_BUFFER_SIZE);
UsbRequest requestIn = new UsbRequest();
requestIn.initialize(connection, in);
while ((responseData = LedgerHelper.unwrapResponseAPDU(LEDGER_DEFAULT_CHANNEL, response.toByteArray(), HID_BUFFER_SIZE)) == null) {
responseBuffer.clear();
requestIn.queue(responseBuffer, HID_BUFFER_SIZE);
connection.requestWait();
responseBuffer.rewind();
responseBuffer.get(transferBuffer, 0, HID_BUFFER_SIZE);
response.write(transferBuffer, 0, HID_BUFFER_SIZE);
}
requestIn.close();
if (debug) {
Timber.d("<= %s", Dump.dump(responseData));
}
return responseData;
}
@Override
public void close() {
connection.releaseInterface(dongleInterface);
connection.close();
}
@Override
public void setDebug(boolean debugFlag) {
this.debug = debugFlag;
}
private static final int HID_BUFFER_SIZE = 64;
private static final int LEDGER_DEFAULT_CHANNEL = 1;
private static final int SW1_DATA_AVAILABLE = 0x61;
}

View file

@ -0,0 +1,62 @@
/*
*******************************************************************************
* BTChip Bitcoin Hardware Wallet Java API
* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn
*
* 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.btchip.utils;
import java.io.ByteArrayOutputStream;
public class Dump {
public static String dump(byte[] buffer, int offset, int length) {
String result = "";
for (int i = 0; i < length; i++) {
String temp = Integer.toHexString((buffer[offset + i]) & 0xff);
if (temp.length() < 2) {
temp = "0" + temp;
}
result += temp;
}
return result;
}
public static String dump(byte[] buffer) {
return dump(buffer, 0, buffer.length);
}
public static byte[] hexToBin(String src) {
ByteArrayOutputStream result = new ByteArrayOutputStream();
int i = 0;
while (i < src.length()) {
char x = src.charAt(i);
if (!((x >= '0' && x <= '9') || (x >= 'A' && x <= 'F') || (x >= 'a' && x <= 'f'))) {
i++;
continue;
}
try {
result.write(Integer.valueOf("" + src.charAt(i) + src.charAt(i + 1), 16));
i += 2;
} catch (Exception e) {
return null;
}
}
return result.toByteArray();
}
}

View file

@ -0,0 +1,145 @@
/*
* Copyright (c) 2018 m2049r
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.m2049r.levin.data;
import com.m2049r.levin.util.HexHelper;
import com.m2049r.levin.util.LevinReader;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
public class Bucket {
// constants copied from monero p2p & epee
public final static int P2P_COMMANDS_POOL_BASE = 1000;
public final static int COMMAND_HANDSHAKE_ID = P2P_COMMANDS_POOL_BASE + 1;
public final static int COMMAND_TIMED_SYNC_ID = P2P_COMMANDS_POOL_BASE + 2;
public final static int COMMAND_PING_ID = P2P_COMMANDS_POOL_BASE + 3;
public final static int COMMAND_REQUEST_STAT_INFO_ID = P2P_COMMANDS_POOL_BASE + 4;
public final static int COMMAND_REQUEST_NETWORK_STATE_ID = P2P_COMMANDS_POOL_BASE + 5;
public final static int COMMAND_REQUEST_PEER_ID_ID = P2P_COMMANDS_POOL_BASE + 6;
public final static int COMMAND_REQUEST_SUPPORT_FLAGS_ID = P2P_COMMANDS_POOL_BASE + 7;
public final static long LEVIN_SIGNATURE = 0x0101010101012101L; // Bender's nightmare
public final static long LEVIN_DEFAULT_MAX_PACKET_SIZE = 100000000; // 100MB by default
public final static int LEVIN_PACKET_REQUEST = 0x00000001;
public final static int LEVIN_PACKET_RESPONSE = 0x00000002;
public final static int LEVIN_PROTOCOL_VER_0 = 0;
public final static int LEVIN_PROTOCOL_VER_1 = 1;
public final static int LEVIN_OK = 0;
public final static int LEVIN_ERROR_CONNECTION = -1;
public final static int LEVIN_ERROR_CONNECTION_NOT_FOUND = -2;
public final static int LEVIN_ERROR_CONNECTION_DESTROYED = -3;
public final static int LEVIN_ERROR_CONNECTION_TIMEDOUT = -4;
public final static int LEVIN_ERROR_CONNECTION_NO_DUPLEX_PROTOCOL = -5;
public final static int LEVIN_ERROR_CONNECTION_HANDLER_NOT_DEFINED = -6;
public final static int LEVIN_ERROR_FORMAT = -7;
public final static int P2P_SUPPORT_FLAG_FLUFFY_BLOCKS = 0x01;
public final static int P2P_SUPPORT_FLAGS = P2P_SUPPORT_FLAG_FLUFFY_BLOCKS;
final private long signature;
final private long cb;
final public boolean haveToReturnData;
final public int command;
final public int returnCode;
final private int flags;
final private int protcolVersion;
final byte[] payload;
final public Section payloadSection;
// create a request
public Bucket(int command, byte[] payload) throws IOException {
this.signature = LEVIN_SIGNATURE;
this.cb = payload.length;
this.haveToReturnData = true;
this.command = command;
this.returnCode = 0;
this.flags = LEVIN_PACKET_REQUEST;
this.protcolVersion = LEVIN_PROTOCOL_VER_1;
this.payload = payload;
payloadSection = LevinReader.readPayload(payload);
}
// create a response
public Bucket(int command, byte[] payload, int rc) throws IOException {
this.signature = LEVIN_SIGNATURE;
this.cb = payload.length;
this.haveToReturnData = false;
this.command = command;
this.returnCode = rc;
this.flags = LEVIN_PACKET_RESPONSE;
this.protcolVersion = LEVIN_PROTOCOL_VER_1;
this.payload = payload;
payloadSection = LevinReader.readPayload(payload);
}
public Bucket(DataInput in) throws IOException {
signature = in.readLong();
cb = in.readLong();
haveToReturnData = in.readBoolean();
command = in.readInt();
returnCode = in.readInt();
flags = in.readInt();
protcolVersion = in.readInt();
if (signature == Bucket.LEVIN_SIGNATURE) {
if (cb > Integer.MAX_VALUE)
throw new IllegalArgumentException();
payload = new byte[(int) cb];
in.readFully(payload);
} else
throw new IllegalStateException();
payloadSection = LevinReader.readPayload(payload);
}
public Section getPayloadSection() {
return payloadSection;
}
public void send(DataOutput out) throws IOException {
out.writeLong(signature);
out.writeLong(cb);
out.writeBoolean(haveToReturnData);
out.writeInt(command);
out.writeInt(returnCode);
out.writeInt(flags);
out.writeInt(protcolVersion);
out.write(payload);
}
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("sig: ").append(signature).append("\n");
sb.append("cb: ").append(cb).append("\n");
sb.append("call: ").append(haveToReturnData).append("\n");
sb.append("cmd: ").append(command).append("\n");
sb.append("rc: ").append(returnCode).append("\n");
sb.append("flags:").append(flags).append("\n");
sb.append("proto:").append(protcolVersion).append("\n");
sb.append(HexHelper.bytesToHex(payload)).append("\n");
sb.append(payloadSection.toString());
return sb.toString();
}
}

View file

@ -0,0 +1,125 @@
/*
* Copyright (c) 2018 m2049r
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.m2049r.levin.data;
import com.m2049r.levin.util.HexHelper;
import com.m2049r.levin.util.LevinReader;
import com.m2049r.levin.util.LevinWriter;
import com.m2049r.levin.util.LittleEndianDataOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutput;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class Section {
// constants copied from monero p2p & epee
static final public int PORTABLE_STORAGE_SIGNATUREA = 0x01011101;
static final public int PORTABLE_STORAGE_SIGNATUREB = 0x01020101;
static final public byte PORTABLE_STORAGE_FORMAT_VER = 1;
static final public byte PORTABLE_RAW_SIZE_MARK_MASK = 0x03;
static final public byte PORTABLE_RAW_SIZE_MARK_BYTE = 0;
static final public byte PORTABLE_RAW_SIZE_MARK_WORD = 1;
static final public byte PORTABLE_RAW_SIZE_MARK_DWORD = 2;
static final public byte PORTABLE_RAW_SIZE_MARK_INT64 = 3;
static final long MAX_STRING_LEN_POSSIBLE = 2000000000; // do not let string be so big
// data types
static final public byte SERIALIZE_TYPE_INT64 = 1;
static final public byte SERIALIZE_TYPE_INT32 = 2;
static final public byte SERIALIZE_TYPE_INT16 = 3;
static final public byte SERIALIZE_TYPE_INT8 = 4;
static final public byte SERIALIZE_TYPE_UINT64 = 5;
static final public byte SERIALIZE_TYPE_UINT32 = 6;
static final public byte SERIALIZE_TYPE_UINT16 = 7;
static final public byte SERIALIZE_TYPE_UINT8 = 8;
static final public byte SERIALIZE_TYPE_DUOBLE = 9;
static final public byte SERIALIZE_TYPE_STRING = 10;
static final public byte SERIALIZE_TYPE_BOOL = 11;
static final public byte SERIALIZE_TYPE_OBJECT = 12;
static final public byte SERIALIZE_TYPE_ARRAY = 13;
static final public byte SERIALIZE_FLAG_ARRAY = (byte) 0x80;
private final Map<String, Object> entries = new HashMap<String, Object>();
public void add(String key, Object entry) {
entries.put(key, entry);
}
public int size() {
return entries.size();
}
public Set<Map.Entry<String, Object>> entrySet() {
return entries.entrySet();
}
public Object get(String key) {
return entries.get(key);
}
public String toString() {
final StringBuilder sb = new StringBuilder();
sb.append("\n");
for (Map.Entry<String, Object> entry : entries.entrySet()) {
sb.append(entry.getKey()).append("=");
final Object value = entry.getValue();
if (value instanceof List) {
@SuppressWarnings("unchecked") final List<Object> list = (List<Object>) value;
for (Object listEntry : list) {
sb.append(listEntry.toString()).append("\n");
}
} else if (value instanceof String) {
sb.append("(").append(value).append(")\n");
} else if (value instanceof byte[]) {
sb.append(HexHelper.bytesToHex((byte[]) value)).append("\n");
} else {
sb.append(value.toString()).append("\n");
}
}
return sb.toString();
}
static public Section fromByteArray(byte[] buffer) {
try {
return LevinReader.readPayload(buffer);
} catch (IOException ex) {
throw new IllegalStateException();
}
}
public byte[] asByteArray() {
try {
ByteArrayOutputStream bas = new ByteArrayOutputStream();
DataOutput out = new LittleEndianDataOutputStream(bas);
LevinWriter writer = new LevinWriter(out);
writer.writePayload(this);
return bas.toByteArray();
} catch (IOException ex) {
throw new IllegalStateException();
}
}
}

View file

@ -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.levin.scanner;
import com.m2049r.xmrwallet.data.NodeInfo;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import timber.log.Timber;
public class Dispatcher implements PeerRetriever.OnGetPeers {
static final public int NUM_THREADS = 50;
static final public int MAX_PEERS = 1000;
static final public long MAX_TIME = 30000000000L; //30 seconds
private int peerCount = 0;
final private Set<NodeInfo> knownNodes = new HashSet<>(); // set of nodes to test
final private Set<NodeInfo> rpcNodes = new HashSet<>(); // set of RPC nodes we like
final private ExecutorService exeService = Executors.newFixedThreadPool(NUM_THREADS);
public interface Listener {
void onGet(NodeInfo nodeInfo);
}
private Listener listener;
public Dispatcher(Listener listener) {
this.listener = listener;
}
public Set<NodeInfo> getRpcNodes() {
return rpcNodes;
}
public int getPeerCount() {
return peerCount;
}
public boolean getMorePeers() {
return peerCount < MAX_PEERS;
}
public void awaitTermination(int nodesToFind) {
try {
final long t = System.nanoTime();
while (!jobs.isEmpty()) {
try {
Timber.d("Remaining jobs %d", jobs.size());
final PeerRetriever retrievedPeer = jobs.poll().get();
if (retrievedPeer.isGood() && getMorePeers())
retrievePeers(retrievedPeer);
final NodeInfo nodeInfo = retrievedPeer.getNodeInfo();
Timber.d("Retrieved %s", nodeInfo);
if ((nodeInfo.isValid() || nodeInfo.isFavourite())) {
nodeInfo.setDefaultName();
rpcNodes.add(nodeInfo);
Timber.d("RPC: %s", nodeInfo);
// the following is not totally correct but it works (otherwise we need to
// load much more before filtering - but we don't have time
if (listener != null) listener.onGet(nodeInfo);
if (rpcNodes.size() >= nodesToFind) {
Timber.d("are we done here?");
filterRpcNodes();
if (rpcNodes.size() >= nodesToFind) {
Timber.d("we're done here");
break;
}
}
}
if (System.nanoTime() - t > MAX_TIME) break; // watchdog
} catch (ExecutionException ex) {
Timber.d(ex); // tell us about it and continue
}
}
} catch (InterruptedException ex) {
Timber.d(ex);
} finally {
Timber.d("Shutting down!");
exeService.shutdownNow();
try {
exeService.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);
} catch (InterruptedException ex) {
Timber.d(ex);
}
}
filterRpcNodes();
}
static final public int HEIGHT_WINDOW = 1;
private boolean testHeight(long height, long consensus) {
return (height >= (consensus - HEIGHT_WINDOW))
&& (height <= (consensus + HEIGHT_WINDOW));
}
private long calcConsensusHeight() {
Timber.d("Calc Consensus height from %d nodes", rpcNodes.size());
final Map<Long, Integer> nodeHeights = new TreeMap<Long, Integer>();
for (NodeInfo info : rpcNodes) {
if (!info.isValid()) continue;
Integer h = nodeHeights.get(info.getHeight());
if (h == null)
h = 0;
nodeHeights.put(info.getHeight(), h + 1);
}
long consensusHeight = 0;
long consensusCount = 0;
for (Map.Entry<Long, Integer> entry : nodeHeights.entrySet()) {
final long entryHeight = entry.getKey();
int count = 0;
for (long i = entryHeight - HEIGHT_WINDOW; i <= entryHeight + HEIGHT_WINDOW; i++) {
Integer v = nodeHeights.get(i);
if (v == null)
v = 0;
count += v;
}
if (count >= consensusCount) {
consensusCount = count;
consensusHeight = entryHeight;
}
Timber.d("%d - %d/%d", entryHeight, count, entry.getValue());
}
return consensusHeight;
}
private void filterRpcNodes() {
long consensus = calcConsensusHeight();
Timber.d("Consensus Height = %d for %d nodes", consensus, rpcNodes.size());
for (Iterator<NodeInfo> iter = rpcNodes.iterator(); iter.hasNext(); ) {
NodeInfo info = iter.next();
// don't remove favourites
if (!info.isFavourite()) {
if (!testHeight(info.getHeight(), consensus)) {
iter.remove();
Timber.d("Removed %s", info);
}
}
}
}
// TODO: does this NEED to be a ConcurrentLinkedDeque?
private ConcurrentLinkedDeque<Future<PeerRetriever>> jobs = new ConcurrentLinkedDeque<>();
private void retrievePeer(NodeInfo nodeInfo) {
if (knownNodes.add(nodeInfo)) {
Timber.d("\t%d:%s", knownNodes.size(), nodeInfo);
jobs.add(exeService.submit(new PeerRetriever(nodeInfo, this)));
peerCount++; // jobs.size() does not perform well
}
}
private void retrievePeers(PeerRetriever peer) {
for (LevinPeer levinPeer : peer.getPeers()) {
if (getMorePeers())
retrievePeer(new NodeInfo(levinPeer));
else
break;
}
}
public void seedPeers(Collection<NodeInfo> seedNodes) {
for (NodeInfo node : seedNodes) {
if (node.isFavourite()) {
rpcNodes.add(node);
if (listener != null) listener.onGet(node);
}
retrievePeer(node);
}
}
}

View file

@ -0,0 +1,39 @@
/*
* 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 java.net.InetAddress;
import java.net.InetSocketAddress;
public class LevinPeer {
final public InetSocketAddress socketAddress;
final public int version;
final public long height;
final public String top;
public InetSocketAddress getSocketAddress() {
return socketAddress;
}
LevinPeer(InetAddress address, int port, int version, long height, String top) {
this.socketAddress = new InetSocketAddress(address, port);
this.version = version;
this.height = height;
this.top = top;
}
}

View file

@ -0,0 +1,231 @@
/*
* 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.Socket;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.concurrent.Callable;
import timber.log.Timber;
public class PeerRetriever implements Callable<PeerRetriever> {
static final public int CONNECT_TIMEOUT = 500; //ms
static final public int SOCKET_TIMEOUT = 500; //ms
static final public long PEER_ID = new Random().nextLong();
static final private byte[] HANDSHAKE = handshakeRequest().asByteArray();
static final private byte[] FLAGS_RESP = flagsResponse().asByteArray();
final private List<LevinPeer> 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<LevinPeer> 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) {
Section data = (Section) section.get("payload_data");
int topVersion = (Integer) data.get("top_version");
long currentHeight = (Long) data.get("current_height");
String topId = HexHelper.bytesToHex((byte[]) data.get("top_id"));
Timber.d("PAYLOAD_DATA %d/%d/%s", topVersion, currentHeight, topId);
@SuppressWarnings("unchecked")
List<Section> peerList = (List<Section>) section.get("local_peerlist_new");
if (peerList != null) {
for (Section peer : peerList) {
Section adr = (Section) peer.get("adr");
Integer type = (Integer) 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;
Integer sport = (Integer) 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 LevinPeer(inet, port, topVersion, currentHeight, topId));
}
}
}
}
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;
}
}

View file

@ -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);
}
}
}

View file

@ -0,0 +1,184 @@
/*
* Copyright (c) 2018 m2049r
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.m2049r.levin.util;
import com.m2049r.levin.data.Section;
import java.io.ByteArrayInputStream;
import java.io.DataInput;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
// Full Levin reader as seen on epee
public class LevinReader {
private DataInput in;
private LevinReader(byte[] buffer) {
ByteArrayInputStream bis = new ByteArrayInputStream(buffer);
in = new LittleEndianDataInputStream(bis);
}
static public Section readPayload(byte[] payload) throws IOException {
LevinReader r = new LevinReader(payload);
return r.readPayload();
}
private Section readPayload() throws IOException {
if (in.readInt() != Section.PORTABLE_STORAGE_SIGNATUREA)
throw new IllegalStateException();
if (in.readInt() != Section.PORTABLE_STORAGE_SIGNATUREB)
throw new IllegalStateException();
if (in.readByte() != Section.PORTABLE_STORAGE_FORMAT_VER)
throw new IllegalStateException();
return readSection();
}
private Section readSection() throws IOException {
Section section = new Section();
long count = readVarint();
while (count-- > 0) {
// read section name string
String sectionName = readSectionName();
section.add(sectionName, loadStorageEntry());
}
return section;
}
private Object loadStorageArrayEntry(int type) throws IOException {
type &= ~Section.SERIALIZE_FLAG_ARRAY;
return readArrayEntry(type);
}
private List<Object> readArrayEntry(int type) throws IOException {
List<Object> list = new ArrayList<Object>();
long size = readVarint();
while (size-- > 0)
list.add(read(type));
return list;
}
private Object read(int type) throws IOException {
switch (type) {
case Section.SERIALIZE_TYPE_UINT64:
case Section.SERIALIZE_TYPE_INT64:
return in.readLong();
case Section.SERIALIZE_TYPE_UINT32:
case Section.SERIALIZE_TYPE_INT32:
return in.readInt();
case Section.SERIALIZE_TYPE_UINT16:
return in.readUnsignedShort();
case Section.SERIALIZE_TYPE_INT16:
return in.readShort();
case Section.SERIALIZE_TYPE_UINT8:
return in.readUnsignedByte();
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(final int firstByte, final int bytes) throws IOException {
long result = firstByte;
for (int i = 1; i < bytes + 1; i++) {
result = result + (((long) in.readUnsignedByte()) << (8 * i));
}
return result;
}
}

View file

@ -0,0 +1,98 @@
/*
* Copyright (c) 2018 m2049r
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.m2049r.levin.util;
import com.m2049r.levin.data.Section;
import java.io.DataOutput;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Map;
// a simplified Levin Writer WITHOUT support for arrays
public class LevinWriter {
private DataOutput out;
public LevinWriter(DataOutput out) {
this.out = out;
}
public void writePayload(Section section) throws IOException {
out.writeInt(Section.PORTABLE_STORAGE_SIGNATUREA);
out.writeInt(Section.PORTABLE_STORAGE_SIGNATUREB);
out.writeByte(Section.PORTABLE_STORAGE_FORMAT_VER);
putSection(section);
}
private void writeSection(Section section) throws IOException {
out.writeByte(Section.SERIALIZE_TYPE_OBJECT);
putSection(section);
}
private void putSection(Section section) throws IOException {
writeVarint(section.size());
for (Map.Entry<String, Object> kv : section.entrySet()) {
byte[] key = kv.getKey().getBytes(StandardCharsets.US_ASCII);
out.writeByte(key.length);
out.write(key);
write(kv.getValue());
}
}
private void writeVarint(long i) throws IOException {
if (i <= 63) {
out.writeByte(((int) i << 2) | Section.PORTABLE_RAW_SIZE_MARK_BYTE);
} else if (i <= 16383) {
out.writeShort(((int) i << 2) | Section.PORTABLE_RAW_SIZE_MARK_WORD);
} else if (i <= 1073741823) {
out.writeInt(((int) i << 2) | Section.PORTABLE_RAW_SIZE_MARK_DWORD);
} else {
if (i > 4611686018427387903L)
throw new IllegalArgumentException();
out.writeLong((i << 2) | Section.PORTABLE_RAW_SIZE_MARK_INT64);
}
}
private void write(Object object) throws IOException {
if (object instanceof byte[]) {
byte[] value = (byte[]) object;
out.writeByte(Section.SERIALIZE_TYPE_STRING);
writeVarint(value.length);
out.write(value);
} else if (object instanceof String) {
byte[] value = ((String) object)
.getBytes(StandardCharsets.US_ASCII);
out.writeByte(Section.SERIALIZE_TYPE_STRING);
writeVarint(value.length);
out.write(value);
} else if (object instanceof Integer) {
out.writeByte(Section.SERIALIZE_TYPE_UINT32);
out.writeInt((int) object);
} else if (object instanceof Long) {
out.writeByte(Section.SERIALIZE_TYPE_UINT64);
out.writeLong((long) object);
} else if (object instanceof Byte) {
out.writeByte(Section.SERIALIZE_TYPE_UINT8);
out.writeByte((byte) object);
} else if (object instanceof Section) {
writeSection((Section) object);
} else {
throw new IllegalArgumentException();
}
}
}

View file

@ -0,0 +1,564 @@
/*
* Copyright (c) 2018 m2049r
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.m2049r.levin.util;
import java.io.DataInput;
import java.io.EOFException;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UTFDataFormatException;
/**
* A little endian java.io.DataInputStream (without readLine())
*/
public class LittleEndianDataInputStream extends FilterInputStream implements
DataInput {
/**
* Creates a DataInputStream that uses the specified underlying InputStream.
*
* @param in the specified input stream
*/
public LittleEndianDataInputStream(InputStream in) {
super(in);
}
@Deprecated
public final String readLine() {
throw new UnsupportedOperationException();
}
/**
* Reads some number of bytes from the contained input stream and stores
* them into the buffer array <code>b</code>. The number of bytes actually
* read is returned as an integer. This method blocks until input data is
* available, end of file is detected, or an exception is thrown.
*
* <p>
* If <code>b</code> is null, a <code>NullPointerException</code> is thrown.
* If the length of <code>b</code> is zero, then no bytes are read and
* <code>0</code> is returned; otherwise, there is an attempt to read at
* least one byte. If no byte is available because the stream is at end of
* file, the value <code>-1</code> is returned; otherwise, at least one byte
* is read and stored into <code>b</code>.
*
* <p>
* The first byte read is stored into element <code>b[0]</code>, the next
* one into <code>b[1]</code>, and so on. The number of bytes read is, at
* most, equal to the length of <code>b</code>. Let <code>k</code> be the
* number of bytes actually read; these bytes will be stored in elements
* <code>b[0]</code> through <code>b[k-1]</code>, leaving elements
* <code>b[k]</code> through <code>b[b.length-1]</code> unaffected.
*
* <p>
* The <code>read(b)</code> method has the same effect as: <blockquote>
*
* <pre>
* read(b, 0, b.length)
* </pre>
*
* </blockquote>
*
* @param b the buffer into which the data is read.
* @return the total number of bytes read into the buffer, or
* <code>-1</code> if there is no more data because the end of the
* stream has been reached.
* @throws IOException if the first byte cannot be read for any reason other than
* end of file, the stream has been closed and the underlying
* input stream does not support reading after close, or
* another I/O error occurs.
* @see FilterInputStream#in
* @see InputStream#read(byte[], int, int)
*/
public final int read(byte b[]) throws IOException {
return in.read(b, 0, b.length);
}
/**
* Reads up to <code>len</code> bytes of data from the contained input
* stream into an array of bytes. An attempt is made to read as many as
* <code>len</code> bytes, but a smaller number may be read, possibly zero.
* The number of bytes actually read is returned as an integer.
*
* <p>
* This method blocks until input data is available, end of file is
* detected, or an exception is thrown.
*
* <p>
* If <code>len</code> is zero, then no bytes are read and <code>0</code> is
* returned; otherwise, there is an attempt to read at least one byte. If no
* byte is available because the stream is at end of file, the value
* <code>-1</code> is returned; otherwise, at least one byte is read and
* stored into <code>b</code>.
*
* <p>
* The first byte read is stored into element <code>b[off]</code>, the next
* one into <code>b[off+1]</code>, and so on. The number of bytes read is,
* at most, equal to <code>len</code>. Let <i>k</i> be the number of bytes
* actually read; these bytes will be stored in elements <code>b[off]</code>
* through <code>b[off+</code><i>k</i><code>-1]</code>, leaving elements
* <code>b[off+</code><i>k</i><code>]</code> through
* <code>b[off+len-1]</code> unaffected.
*
* <p>
* In every case, elements <code>b[0]</code> through <code>b[off]</code> and
* elements <code>b[off+len]</code> through <code>b[b.length-1]</code> are
* unaffected.
*
* @param b the buffer into which the data is read.
* @param off the start offset in the destination array <code>b</code>
* @param len the maximum number of bytes read.
* @return the total number of bytes read into the buffer, or
* <code>-1</code> if there is no more data because the end of the
* stream has been reached.
* @throws NullPointerException If <code>b</code> is <code>null</code>.
* @throws IndexOutOfBoundsException If <code>off</code> is negative, <code>len</code> is
* negative, or <code>len</code> is greater than
* <code>b.length - off</code>
* @throws IOException if the first byte cannot be read for any reason other than
* end of file, the stream has been closed and the underlying
* input stream does not support reading after close, or
* another I/O error occurs.
* @see FilterInputStream#in
* @see InputStream#read(byte[], int, int)
*/
public final int read(byte b[], int off, int len) throws IOException {
return in.read(b, off, len);
}
/**
* See the general contract of the <code>readFully</code> method of
* <code>DataInput</code>.
* <p>
* Bytes for this operation are read from the contained input stream.
*
* @param b the buffer into which the data is read.
* @throws EOFException if this input stream reaches the end before reading all
* the bytes.
* @throws IOException the stream has been closed and the contained input stream
* does not support reading after close, or another I/O error
* occurs.
* @see FilterInputStream#in
*/
public final void readFully(byte b[]) throws IOException {
readFully(b, 0, b.length);
}
/**
* See the general contract of the <code>readFully</code> method of
* <code>DataInput</code>.
* <p>
* Bytes for this operation are read from the contained input stream.
*
* @param b the buffer into which the data is read.
* @param off the start offset of the data.
* @param len the number of bytes to read.
* @throws EOFException if this input stream reaches the end before reading all
* the bytes.
* @throws IOException the stream has been closed and the contained input stream
* does not support reading after close, or another I/O error
* occurs.
* @see FilterInputStream#in
*/
public final void readFully(byte b[], int off, int len) throws IOException {
if (len < 0)
throw new IndexOutOfBoundsException();
int n = 0;
while (n < len) {
int count = in.read(b, off + n, len - n);
if (count < 0)
throw new EOFException();
n += count;
}
}
/**
* See the general contract of the <code>skipBytes</code> method of
* <code>DataInput</code>.
* <p>
* Bytes for this operation are read from the contained input stream.
*
* @param n the number of bytes to be skipped.
* @return the actual number of bytes skipped.
* @throws IOException if the contained input stream does not support seek, or
* the stream has been closed and the contained input stream
* does not support reading after close, or another I/O error
* occurs.
*/
public final int skipBytes(int n) throws IOException {
int total = 0;
int cur = 0;
while ((total < n) && ((cur = (int) in.skip(n - total)) > 0)) {
total += cur;
}
return total;
}
/**
* See the general contract of the <code>readBoolean</code> method of
* <code>DataInput</code>.
* <p>
* Bytes for this operation are read from the contained input stream.
*
* @return the <code>boolean</code> value read.
* @throws EOFException if this input stream has reached the end.
* @throws IOException the stream has been closed and the contained input stream
* does not support reading after close, or another I/O error
* occurs.
* @see FilterInputStream#in
*/
public final boolean readBoolean() throws IOException {
int ch = in.read();
if (ch < 0)
throw new EOFException();
return (ch != 0);
}
/**
* See the general contract of the <code>readByte</code> method of
* <code>DataInput</code>.
* <p>
* Bytes for this operation are read from the contained input stream.
*
* @return the next byte of this input stream as a signed 8-bit
* <code>byte</code>.
* @throws EOFException if this input stream has reached the end.
* @throws IOException the stream has been closed and the contained input stream
* does not support reading after close, or another I/O error
* occurs.
* @see FilterInputStream#in
*/
public final byte readByte() throws IOException {
int ch = in.read();
if (ch < 0)
throw new EOFException();
return (byte) (ch);
}
/**
* See the general contract of the <code>readUnsignedByte</code> method of
* <code>DataInput</code>.
* <p>
* Bytes for this operation are read from the contained input stream.
*
* @return the next byte of this input stream, interpreted as an unsigned
* 8-bit number.
* @throws EOFException if this input stream has reached the end.
* @throws IOException the stream has been closed and the contained input stream
* does not support reading after close, or another I/O error
* occurs.
* @see FilterInputStream#in
*/
public final int readUnsignedByte() throws IOException {
int ch = in.read();
if (ch < 0)
throw new EOFException();
return ch;
}
/**
* See the general contract of the <code>readShort</code> method of
* <code>DataInput</code>.
* <p>
* Bytes for this operation are read from the contained input stream.
*
* @return the next two bytes of this input stream, interpreted as a signed
* 16-bit number.
* @throws EOFException if this input stream reaches the end before reading two
* bytes.
* @throws IOException the stream has been closed and the contained input stream
* does not support reading after close, or another I/O error
* occurs.
* @see FilterInputStream#in
*/
public final short readShort() throws IOException {
int ch1 = in.read();
int ch2 = in.read();
if ((ch1 | ch2) < 0)
throw new EOFException();
return (short) ((ch1 << 0) + (ch2 << 8));
}
/**
* See the general contract of the <code>readUnsignedShort</code> method of
* <code>DataInput</code>.
* <p>
* Bytes for this operation are read from the contained input stream.
*
* @return the next two bytes of this input stream, interpreted as an
* unsigned 16-bit integer.
* @throws EOFException if this input stream reaches the end before reading two
* bytes.
* @throws IOException the stream has been closed and the contained input stream
* does not support reading after close, or another I/O error
* occurs.
* @see FilterInputStream#in
*/
public final int readUnsignedShort() throws IOException {
int ch1 = in.read();
int ch2 = in.read();
if ((ch1 | ch2) < 0)
throw new EOFException();
return (ch1 << 0) + (ch2 << 8);
}
/**
* See the general contract of the <code>readChar</code> method of
* <code>DataInput</code>.
* <p>
* Bytes for this operation are read from the contained input stream.
*
* @return the next two bytes of this input stream, interpreted as a
* <code>char</code>.
* @throws EOFException if this input stream reaches the end before reading two
* bytes.
* @throws IOException the stream has been closed and the contained input stream
* does not support reading after close, or another I/O error
* occurs.
* @see FilterInputStream#in
*/
public final char readChar() throws IOException {
int ch1 = in.read();
int ch2 = in.read();
if ((ch1 | ch2) < 0)
throw new EOFException();
return (char) ((ch1 << 0) + (ch2 << 8));
}
/**
* See the general contract of the <code>readInt</code> method of
* <code>DataInput</code>.
* <p>
* Bytes for this operation are read from the contained input stream.
*
* @return the next four bytes of this input stream, interpreted as an
* <code>int</code>.
* @throws EOFException if this input stream reaches the end before reading four
* bytes.
* @throws IOException the stream has been closed and the contained input stream
* does not support reading after close, or another I/O error
* occurs.
* @see FilterInputStream#in
*/
public final int readInt() throws IOException {
int ch1 = in.read();
int ch2 = in.read();
int ch3 = in.read();
int ch4 = in.read();
if ((ch1 | ch2 | ch3 | ch4) < 0)
throw new EOFException();
return ((ch1 << 0) + (ch2 << 8) + (ch3 << 16) + (ch4 << 24));
}
private byte readBuffer[] = new byte[8];
/**
* See the general contract of the <code>readLong</code> method of
* <code>DataInput</code>.
* <p>
* Bytes for this operation are read from the contained input stream.
*
* @return the next eight bytes of this input stream, interpreted as a
* <code>long</code>.
* @throws EOFException if this input stream reaches the end before reading eight
* bytes.
* @throws IOException the stream has been closed and the contained input stream
* does not support reading after close, or another I/O error
* occurs.
* @see FilterInputStream#in
*/
public final long readLong() throws IOException {
readFully(readBuffer, 0, 8);
return (((long) readBuffer[7] << 56)
+ ((long) (readBuffer[6] & 255) << 48)
+ ((long) (readBuffer[5] & 255) << 40)
+ ((long) (readBuffer[4] & 255) << 32)
+ ((long) (readBuffer[3] & 255) << 24)
+ ((readBuffer[2] & 255) << 16) + ((readBuffer[1] & 255) << 8) + ((readBuffer[0] & 255) << 0));
}
/**
* See the general contract of the <code>readFloat</code> method of
* <code>DataInput</code>.
* <p>
* Bytes for this operation are read from the contained input stream.
*
* @return the next four bytes of this input stream, interpreted as a
* <code>float</code>.
* @throws EOFException if this input stream reaches the end before reading four
* bytes.
* @throws IOException the stream has been closed and the contained input stream
* does not support reading after close, or another I/O error
* occurs.
* @see java.io.DataInputStream#readInt()
* @see Float#intBitsToFloat(int)
*/
public final float readFloat() throws IOException {
return Float.intBitsToFloat(readInt());
}
/**
* See the general contract of the <code>readDouble</code> method of
* <code>DataInput</code>.
* <p>
* Bytes for this operation are read from the contained input stream.
*
* @return the next eight bytes of this input stream, interpreted as a
* <code>double</code>.
* @throws EOFException if this input stream reaches the end before reading eight
* bytes.
* @throws IOException the stream has been closed and the contained input stream
* does not support reading after close, or another I/O error
* occurs.
* @see java.io.DataInputStream#readLong()
* @see Double#longBitsToDouble(long)
*/
public final double readDouble() throws IOException {
return Double.longBitsToDouble(readLong());
}
/**
* See the general contract of the <code>readUTF</code> method of
* <code>DataInput</code>.
* <p>
* Bytes for this operation are read from the contained input stream.
*
* @return a Unicode string.
* @throws EOFException if this input stream reaches the end before reading all
* the bytes.
* @throws IOException the stream has been closed and the contained input stream
* does not support reading after close, or another I/O error
* occurs.
* @throws UTFDataFormatException if the bytes do not represent a valid modified UTF-8
* encoding of a string.
* @see java.io.DataInputStream#readUTF(DataInput)
*/
public final String readUTF() throws IOException {
return readUTF(this);
}
/**
* working arrays initialized on demand by readUTF
*/
private byte bytearr[] = new byte[80];
private char chararr[] = new char[80];
/**
* Reads from the stream <code>in</code> a representation of a Unicode
* character string encoded in <a
* href="DataInput.html#modified-utf-8">modified UTF-8</a> format; this
* string of characters is then returned as a <code>String</code>. The
* details of the modified UTF-8 representation are exactly the same as for
* the <code>readUTF</code> method of <code>DataInput</code>.
*
* @param in a data input stream.
* @return a Unicode string.
* @throws EOFException if the input stream reaches the end before all the bytes.
* @throws IOException the stream has been closed and the contained input stream
* does not support reading after close, or another I/O error
* occurs.
* @throws UTFDataFormatException if the bytes do not represent a valid modified UTF-8
* encoding of a Unicode string.
* @see java.io.DataInputStream#readUnsignedShort()
*/
public final static String readUTF(DataInput in) throws IOException {
int utflen = in.readUnsignedShort();
byte[] bytearr = null;
char[] chararr = null;
if (in instanceof LittleEndianDataInputStream) {
LittleEndianDataInputStream dis = (LittleEndianDataInputStream) in;
if (dis.bytearr.length < utflen) {
dis.bytearr = new byte[utflen * 2];
dis.chararr = new char[utflen * 2];
}
chararr = dis.chararr;
bytearr = dis.bytearr;
} else {
bytearr = new byte[utflen];
chararr = new char[utflen];
}
int c, char2, char3;
int count = 0;
int chararr_count = 0;
in.readFully(bytearr, 0, utflen);
while (count < utflen) {
c = (int) bytearr[count] & 0xff;
if (c > 127)
break;
count++;
chararr[chararr_count++] = (char) c;
}
while (count < utflen) {
c = (int) bytearr[count] & 0xff;
switch (c >> 4) {
case 0:
case 1:
case 2:
case 3:
case 4:
case 5:
case 6:
case 7:
/* 0xxxxxxx */
count++;
chararr[chararr_count++] = (char) c;
break;
case 12:
case 13:
/* 110x xxxx 10xx xxxx */
count += 2;
if (count > utflen)
throw new UTFDataFormatException(
"malformed input: partial character at end");
char2 = (int) bytearr[count - 1];
if ((char2 & 0xC0) != 0x80)
throw new UTFDataFormatException(
"malformed input around byte " + count);
chararr[chararr_count++] = (char) (((c & 0x1F) << 6) | (char2 & 0x3F));
break;
case 14:
/* 1110 xxxx 10xx xxxx 10xx xxxx */
count += 3;
if (count > utflen)
throw new UTFDataFormatException(
"malformed input: partial character at end");
char2 = (int) bytearr[count - 2];
char3 = (int) bytearr[count - 1];
if (((char2 & 0xC0) != 0x80) || ((char3 & 0xC0) != 0x80))
throw new UTFDataFormatException(
"malformed input around byte " + (count - 1));
chararr[chararr_count++] = (char) (((c & 0x0F) << 12)
| ((char2 & 0x3F) << 6) | ((char3 & 0x3F) << 0));
break;
default:
/* 10xx xxxx, 1111 xxxx */
throw new UTFDataFormatException("malformed input around byte "
+ count);
}
}
// The number of chars produced may be less than utflen
return new String(chararr, 0, chararr_count);
}
}

View file

@ -0,0 +1,403 @@
/*
* Copyright (c) 2018 m2049r
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.m2049r.levin.util;
import java.io.DataOutput;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UTFDataFormatException;
/**
* A little endian java.io.DataOutputStream
*/
public class LittleEndianDataOutputStream extends FilterOutputStream implements
DataOutput {
/**
* The number of bytes written to the data output stream so far. If this
* counter overflows, it will be wrapped to Integer.MAX_VALUE.
*/
protected int written;
/**
* Creates a new data output stream to write data to the specified
* underlying output stream. The counter <code>written</code> is set to
* zero.
*
* @param out the underlying output stream, to be saved for later use.
* @see FilterOutputStream#out
*/
public LittleEndianDataOutputStream(OutputStream out) {
super(out);
}
/**
* Increases the written counter by the specified value until it reaches
* Integer.MAX_VALUE.
*/
private void incCount(int value) {
int temp = written + value;
if (temp < 0) {
temp = Integer.MAX_VALUE;
}
written = temp;
}
/**
* Writes the specified byte (the low eight bits of the argument
* <code>b</code>) to the underlying output stream. If no exception is
* thrown, the counter <code>written</code> is incremented by <code>1</code>
* .
* <p>
* Implements the <code>write</code> method of <code>OutputStream</code>.
*
* @param b the <code>byte</code> to be written.
* @throws IOException if an I/O error occurs.
* @see FilterOutputStream#out
*/
public synchronized void write(int b) throws IOException {
out.write(b);
incCount(1);
}
/**
* Writes <code>len</code> bytes from the specified byte array starting at
* offset <code>off</code> to the underlying output stream. If no exception
* is thrown, the counter <code>written</code> is incremented by
* <code>len</code>.
*
* @param b the data.
* @param off the start offset in the data.
* @param len the number of bytes to write.
* @throws IOException if an I/O error occurs.
* @see FilterOutputStream#out
*/
public synchronized void write(byte b[], int off, int len)
throws IOException {
out.write(b, off, len);
incCount(len);
}
/**
* Flushes this data output stream. This forces any buffered output bytes to
* be written out to the stream.
* <p>
* The <code>flush</code> method of <code>DataOutputStream</code> calls the
* <code>flush</code> method of its underlying output stream.
*
* @throws IOException if an I/O error occurs.
* @see FilterOutputStream#out
* @see OutputStream#flush()
*/
public void flush() throws IOException {
out.flush();
}
/**
* Writes a <code>boolean</code> to the underlying output stream as a 1-byte
* value. The value <code>true</code> is written out as the value
* <code>(byte)1</code>; the value <code>false</code> is written out as the
* value <code>(byte)0</code>. If no exception is thrown, the counter
* <code>written</code> is incremented by <code>1</code>.
*
* @param v a <code>boolean</code> value to be written.
* @throws IOException if an I/O error occurs.
* @see FilterOutputStream#out
*/
public final void writeBoolean(boolean v) throws IOException {
out.write(v ? 1 : 0);
incCount(1);
}
/**
* Writes out a <code>byte</code> to the underlying output stream as a
* 1-byte value. If no exception is thrown, the counter <code>written</code>
* is incremented by <code>1</code>.
*
* @param v a <code>byte</code> value to be written.
* @throws IOException if an I/O error occurs.
* @see FilterOutputStream#out
*/
public final void writeByte(int v) throws IOException {
out.write(v);
incCount(1);
}
/**
* Writes a <code>short</code> to the underlying output stream as two bytes,
* low byte first. If no exception is thrown, the counter
* <code>written</code> is incremented by <code>2</code>.
*
* @param v a <code>short</code> to be written.
* @throws IOException if an I/O error occurs.
* @see FilterOutputStream#out
*/
public final void writeShort(int v) throws IOException {
out.write((v >>> 0) & 0xFF);
out.write((v >>> 8) & 0xFF);
incCount(2);
}
/**
* Writes a <code>char</code> to the underlying output stream as a 2-byte
* value, low byte first. If no exception is thrown, the counter
* <code>written</code> is incremented by <code>2</code>.
*
* @param v a <code>char</code> value to be written.
* @throws IOException if an I/O error occurs.
* @see FilterOutputStream#out
*/
public final void writeChar(int v) throws IOException {
out.write((v >>> 0) & 0xFF);
out.write((v >>> 8) & 0xFF);
incCount(2);
}
/**
* Writes an <code>int</code> to the underlying output stream as four bytes,
* low byte first. If no exception is thrown, the counter
* <code>written</code> is incremented by <code>4</code>.
*
* @param v an <code>int</code> to be written.
* @throws IOException if an I/O error occurs.
* @see FilterOutputStream#out
*/
public final void writeInt(int v) throws IOException {
out.write((v >>> 0) & 0xFF);
out.write((v >>> 8) & 0xFF);
out.write((v >>> 16) & 0xFF);
out.write((v >>> 24) & 0xFF);
incCount(4);
}
private byte writeBuffer[] = new byte[8];
/**
* Writes a <code>long</code> to the underlying output stream as eight
* bytes, low byte first. In no exception is thrown, the counter
* <code>written</code> is incremented by <code>8</code>.
*
* @param v a <code>long</code> to be written.
* @throws IOException if an I/O error occurs.
* @see FilterOutputStream#out
*/
public final void writeLong(long v) throws IOException {
writeBuffer[7] = (byte) (v >>> 56);
writeBuffer[6] = (byte) (v >>> 48);
writeBuffer[5] = (byte) (v >>> 40);
writeBuffer[4] = (byte) (v >>> 32);
writeBuffer[3] = (byte) (v >>> 24);
writeBuffer[2] = (byte) (v >>> 16);
writeBuffer[1] = (byte) (v >>> 8);
writeBuffer[0] = (byte) (v >>> 0);
out.write(writeBuffer, 0, 8);
incCount(8);
}
/**
* Converts the float argument to an <code>int</code> using the
* <code>floatToIntBits</code> method in class <code>Float</code>, and then
* writes that <code>int</code> value to the underlying output stream as a
* 4-byte quantity, low byte first. If no exception is thrown, the counter
* <code>written</code> is incremented by <code>4</code>.
*
* @param v a <code>float</code> value to be written.
* @throws IOException if an I/O error occurs.
* @see FilterOutputStream#out
* @see Float#floatToIntBits(float)
*/
public final void writeFloat(float v) throws IOException {
writeInt(Float.floatToIntBits(v));
}
/**
* Converts the double argument to a <code>long</code> using the
* <code>doubleToLongBits</code> method in class <code>Double</code>, and
* then writes that <code>long</code> value to the underlying output stream
* as an 8-byte quantity, low byte first. If no exception is thrown, the
* counter <code>written</code> is incremented by <code>8</code>.
*
* @param v a <code>double</code> value to be written.
* @throws IOException if an I/O error occurs.
* @see FilterOutputStream#out
* @see Double#doubleToLongBits(double)
*/
public final void writeDouble(double v) throws IOException {
writeLong(Double.doubleToLongBits(v));
}
/**
* Writes out the string to the underlying output stream as a sequence of
* bytes. Each character in the string is written out, in sequence, by
* discarding its high eight bits. If no exception is thrown, the counter
* <code>written</code> is incremented by the length of <code>s</code>.
*
* @param s a string of bytes to be written.
* @throws IOException if an I/O error occurs.
* @see FilterOutputStream#out
*/
public final void writeBytes(String s) throws IOException {
int len = s.length();
for (int i = 0; i < len; i++) {
out.write((byte) s.charAt(i));
}
incCount(len);
}
/**
* Writes a string to the underlying output stream as a sequence of
* characters. Each character is written to the data output stream as if by
* the <code>writeChar</code> method. If no exception is thrown, the counter
* <code>written</code> is incremented by twice the length of <code>s</code>
* .
*
* @param s a <code>String</code> value to be written.
* @throws IOException if an I/O error occurs.
* @see java.io.DataOutputStream#writeChar(int)
* @see FilterOutputStream#out
*/
public final void writeChars(String s) throws IOException {
int len = s.length();
for (int i = 0; i < len; i++) {
int v = s.charAt(i);
out.write((v >>> 0) & 0xFF);
out.write((v >>> 8) & 0xFF);
}
incCount(len * 2);
}
/**
* Writes a string to the underlying output stream using <a
* href="DataInput.html#modified-utf-8">modified UTF-8</a> encoding in a
* machine-independent manner.
* <p>
* First, two bytes are written to the output stream as if by the
* <code>writeShort</code> method giving the number of bytes to follow. This
* value is the number of bytes actually written out, not the length of the
* string. Following the length, each character of the string is output, in
* sequence, using the modified UTF-8 encoding for the character. If no
* exception is thrown, the counter <code>written</code> is incremented by
* the total number of bytes written to the output stream. This will be at
* least two plus the length of <code>str</code>, and at most two plus
* thrice the length of <code>str</code>.
*
* @param str a string to be written.
* @throws IOException if an I/O error occurs.
*/
public final void writeUTF(String str) throws IOException {
writeUTF(str, this);
}
/**
* bytearr is initialized on demand by writeUTF
*/
private byte[] bytearr = null;
/**
* Writes a string to the specified DataOutput using <a
* href="DataInput.html#modified-utf-8">modified UTF-8</a> encoding in a
* machine-independent manner.
* <p>
* First, two bytes are written to out as if by the <code>writeShort</code>
* method giving the number of bytes to follow. This value is the number of
* bytes actually written out, not the length of the string. Following the
* length, each character of the string is output, in sequence, using the
* modified UTF-8 encoding for the character. If no exception is thrown, the
* counter <code>written</code> is incremented by the total number of bytes
* written to the output stream. This will be at least two plus the length
* of <code>str</code>, and at most two plus thrice the length of
* <code>str</code>.
*
* @param str a string to be written.
* @param out destination to write to
* @return The number of bytes written out.
* @throws IOException if an I/O error occurs.
*/
static int writeUTF(String str, DataOutput out) throws IOException {
int strlen = str.length();
int utflen = 0;
int c, count = 0;
/* use charAt instead of copying String to char array */
for (int i = 0; i < strlen; i++) {
c = str.charAt(i);
if ((c >= 0x0001) && (c <= 0x007F)) {
utflen++;
} else if (c > 0x07FF) {
utflen += 3;
} else {
utflen += 2;
}
}
if (utflen > 65535)
throw new UTFDataFormatException("encoded string too long: "
+ utflen + " bytes");
byte[] bytearr = null;
if (out instanceof LittleEndianDataOutputStream) {
LittleEndianDataOutputStream dos = (LittleEndianDataOutputStream) out;
if (dos.bytearr == null || (dos.bytearr.length < (utflen + 2)))
dos.bytearr = new byte[(utflen * 2) + 2];
bytearr = dos.bytearr;
} else {
bytearr = new byte[utflen + 2];
}
bytearr[count++] = (byte) ((utflen >>> 8) & 0xFF);
bytearr[count++] = (byte) ((utflen >>> 0) & 0xFF);
int i = 0;
for (i = 0; i < strlen; i++) {
c = str.charAt(i);
if (!((c >= 0x0001) && (c <= 0x007F)))
break;
bytearr[count++] = (byte) c;
}
for (; i < strlen; i++) {
c = str.charAt(i);
if ((c >= 0x0001) && (c <= 0x007F)) {
bytearr[count++] = (byte) c;
} else if (c > 0x07FF) {
bytearr[count++] = (byte) (0xE0 | ((c >> 12) & 0x0F));
bytearr[count++] = (byte) (0x80 | ((c >> 6) & 0x3F));
bytearr[count++] = (byte) (0x80 | ((c >> 0) & 0x3F));
} else {
bytearr[count++] = (byte) (0xC0 | ((c >> 6) & 0x1F));
bytearr[count++] = (byte) (0x80 | ((c >> 0) & 0x3F));
}
}
out.write(bytearr, 0, utflen + 2);
return utflen + 2;
}
/**
* Returns the current value of the counter <code>written</code>, the number
* of bytes written to this data output stream so far. If the counter
* overflows, it will be wrapped to Integer.MAX_VALUE.
*
* @return the value of the <code>written</code> field.
* @see java.io.DataOutputStream#written
*/
public final int size() {
return written;
}
}

View file

@ -0,0 +1,313 @@
/*
* Copyright (c) 2017-2020 m2049r et al.
*
* 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.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.nfc.FormatException;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.nfc.tech.Ndef;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.PowerManager;
import android.widget.Toast;
import androidx.annotation.CallSuper;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.m2049r.xmrwallet.data.BarcodeData;
import com.m2049r.xmrwallet.dialog.ProgressDialog;
import com.m2049r.xmrwallet.fragment.send.SendFragment;
import com.m2049r.xmrwallet.ledger.Ledger;
import com.m2049r.xmrwallet.ledger.LedgerProgressDialog;
import java.io.IOException;
import timber.log.Timber;
public class BaseActivity extends SecureActivity
implements GenerateReviewFragment.ProgressListener, SubaddressFragment.ProgressListener {
ProgressDialog progressDialog = null;
private class SimpleProgressDialog extends ProgressDialog {
SimpleProgressDialog(Context context, int msgId) {
super(context);
setCancelable(false);
setMessage(context.getString(msgId));
}
@Override
public void onBackPressed() {
// prevent back button
}
}
@Override
public void showProgressDialog(int msgId) {
showProgressDialog(msgId, 250); // don't show dialog for fast operations
}
public void showProgressDialog(int msgId, long delayMillis) {
dismissProgressDialog(); // just in case
progressDialog = new SimpleProgressDialog(BaseActivity.this, msgId);
if (delayMillis > 0) {
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
public void run() {
if (progressDialog != null) progressDialog.show();
}
}, delayMillis);
} else {
progressDialog.show();
}
}
@Override
public void showLedgerProgressDialog(int mode) {
dismissProgressDialog(); // just in case
progressDialog = new LedgerProgressDialog(BaseActivity.this, mode);
Ledger.setListener((Ledger.Listener) progressDialog);
progressDialog.show();
}
@Override
public void dismissProgressDialog() {
if (progressDialog == null) return; // nothing to do
if (progressDialog instanceof Ledger.Listener) {
Ledger.unsetListener((Ledger.Listener) progressDialog);
}
if (progressDialog.isShowing()) {
progressDialog.dismiss();
}
progressDialog = null;
}
static final int RELEASE_WAKE_LOCK_DELAY = 5000; // millisconds
private PowerManager.WakeLock wl = null;
void acquireWakeLock() {
if ((wl != null) && wl.isHeld()) return;
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
this.wl = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, getString(R.string.app_name));
try {
wl.acquire();
Timber.d("WakeLock acquired");
} catch (SecurityException ex) {
Timber.w("WakeLock NOT acquired: %s", ex.getLocalizedMessage());
wl = null;
}
}
void releaseWakeLock(int delayMillis) {
Handler handler = new Handler(Looper.getMainLooper());
handler.postDelayed(new Runnable() {
@Override
public void run() {
releaseWakeLock();
}
}, delayMillis);
}
void releaseWakeLock() {
if ((wl == null) || !wl.isHeld()) return;
wl.release();
wl = null;
Timber.d("WakeLock released");
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initNfc();
}
@Override
protected void onPostResume() {
super.onPostResume();
if (nfcAdapter != null) {
nfcAdapter.enableForegroundDispatch(this, nfcPendingIntent, null, null);
// intercept all techs so we can tell the user their tag is no good
}
}
@Override
protected void onPause() {
Timber.d("onPause()");
if (nfcAdapter != null)
nfcAdapter.disableForegroundDispatch(this);
super.onPause();
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
processNfcIntent(intent);
}
// NFC stuff
private NfcAdapter nfcAdapter;
private PendingIntent nfcPendingIntent;
public void initNfc() {
nfcAdapter = NfcAdapter.getDefaultAdapter(this);
if (nfcAdapter == null) // no NFC support
return;
nfcPendingIntent = PendingIntent.getActivity(this, 0,
new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_IMMUTABLE : 0);
}
private void processNfcIntent(Intent intent) {
String action = intent.getAction();
Timber.d("ACTION=%s", action);
if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)
|| NfcAdapter.ACTION_TAG_DISCOVERED.equals(action)
|| NfcAdapter.ACTION_TECH_DISCOVERED.equals(action)) {
Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
Ndef ndef = Ndef.get(tag);
if (ndef == null) {
Toast.makeText(this, getString(R.string.nfc_tag_unsupported), Toast.LENGTH_LONG).show();
return;
}
Fragment f = getSupportFragmentManager().findFragmentById(R.id.fragment_container);
if (f instanceof ReceiveFragment) {
// We want to write a Tag from the ReceiveFragment
BarcodeData bc = ((ReceiveFragment) f).getBarcodeData();
if (bc != null) {
new AsyncWriteTag(ndef, bc.getUri()).execute();
} // else wallet is not loaded yet or receive is otherwise not ready - ignore
} else if (f instanceof SendFragment) {
// We want to read a Tag for the SendFragment
NdefMessage ndefMessage = ndef.getCachedNdefMessage();
if (ndefMessage == null) {
Toast.makeText(this, getString(R.string.nfc_tag_read_undef), Toast.LENGTH_LONG).show();
return;
}
NdefRecord firstRecord = ndefMessage.getRecords()[0];
Uri uri = firstRecord.toUri(); // we insist on the first record
if (uri == null) {
Toast.makeText(this, getString(R.string.nfc_tag_read_undef), Toast.LENGTH_LONG).show();
} else {
BarcodeData bc = BarcodeData.fromString(uri.toString());
if (bc == null)
Toast.makeText(this, getString(R.string.nfc_tag_read_undef), Toast.LENGTH_LONG).show();
else
onUriScanned(bc);
}
}
}
}
// this gets called only if we get data
@CallSuper
void onUriScanned(BarcodeData barcodeData) {
// do nothing by default yet
}
private BarcodeData barcodeData = null;
private BarcodeData popBarcodeData() {
BarcodeData popped = barcodeData;
barcodeData = null;
return popped;
}
private class AsyncWriteTag extends AsyncTask<Void, Void, Boolean> {
Ndef ndef;
Uri uri;
String errorMessage = null;
AsyncWriteTag(Ndef ndef, Uri uri) {
this.ndef = ndef;
this.uri = uri;
}
@Override
protected void onPreExecute() {
super.onPreExecute();
showProgressDialog(R.string.progress_nfc_write);
}
@Override
protected Boolean doInBackground(Void... params) {
if (params.length != 0) return false;
try {
writeNdef(ndef, uri);
return true;
} catch (IOException | FormatException ex) {
Timber.e(ex);
} catch (IllegalArgumentException ex) {
errorMessage = ex.getMessage();
Timber.d(errorMessage);
} finally {
try {
ndef.close();
} catch (IOException ex) {
Timber.e(ex);
}
}
return false;
}
@Override
protected void onPostExecute(Boolean result) {
super.onPostExecute(result);
if (isDestroyed()) {
return;
}
dismissProgressDialog();
if (!result) {
if (errorMessage != null)
Toast.makeText(getApplicationContext(), errorMessage, Toast.LENGTH_LONG).show();
else
Toast.makeText(getApplicationContext(), getString(R.string.nfc_write_failed), Toast.LENGTH_LONG).show();
} else {
Toast.makeText(getApplicationContext(), getString(R.string.nfc_write_successful), Toast.LENGTH_SHORT).show();
}
}
}
void writeNdef(Ndef ndef, Uri uri) throws IOException, FormatException {
NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
if (nfcAdapter == null) return; // no NFC support here
NdefRecord recordNFC = NdefRecord.createUri(uri);
NdefMessage message = new NdefMessage(recordNFC);
ndef.connect();
int tagSize = ndef.getMaxSize();
int msgSize = message.getByteArrayLength();
Timber.d("tagSize=%d, msgSIze=%d, uriSize=%d", tagSize, msgSize, uri.toString().length());
if (tagSize < msgSize)
throw new IllegalArgumentException(getString(R.string.nfc_tag_size, tagSize, msgSize));
ndef.writeNdefMessage(message);
}
}

View file

@ -0,0 +1,615 @@
/*
* 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;
import androidx.annotation.NonNull;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.text.Editable;
import android.text.Html;
import android.text.InputType;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.switchmaterial.SwitchMaterial;
import com.google.android.material.textfield.TextInputLayout;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.model.WalletManager;
import com.m2049r.xmrwallet.util.FingerprintHelper;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.KeyStoreHelper;
import com.m2049r.xmrwallet.util.RestoreHeight;
import com.m2049r.xmrwallet.util.ledger.Monero;
import com.m2049r.xmrwallet.widget.PasswordEntryView;
import com.m2049r.xmrwallet.widget.Toolbar;
import java.io.File;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import timber.log.Timber;
public class GenerateFragment extends Fragment {
static final String TYPE = "type";
static final String TYPE_NEW = "new";
static final String TYPE_KEY = "key";
static final String TYPE_SEED = "seed";
static final String TYPE_LEDGER = "ledger";
static final String TYPE_VIEWONLY = "view";
private TextInputLayout etWalletName;
private PasswordEntryView etWalletPassword;
private LinearLayout llFingerprintAuth;
private TextInputLayout etWalletAddress;
private TextInputLayout etWalletMnemonic;
private TextInputLayout etWalletViewKey;
private TextInputLayout etWalletSpendKey;
private TextInputLayout etWalletRestoreHeight;
private Button bGenerate;
private Button bSeedOffset;
private TextInputLayout etSeedOffset;
private String type = null;
private void clearErrorOnTextEntry(final TextInputLayout textInputLayout) {
textInputLayout.getEditText().addTextChangedListener(new TextWatcher() {
@Override
public void afterTextChanged(Editable editable) {
textInputLayout.setError(null);
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
});
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Bundle args = getArguments();
this.type = args.getString(TYPE);
View view = inflater.inflate(R.layout.fragment_generate, container, false);
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);
bSeedOffset = view.findViewById(R.id.bSeedOffset);
etSeedOffset = view.findViewById(R.id.etSeedOffset);
etWalletAddress.getEditText().setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
etWalletViewKey.getEditText().setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
etWalletSpendKey.getEditText().setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
etWalletName.getEditText().setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
checkName();
}
});
clearErrorOnTextEntry(etWalletName);
etWalletMnemonic.getEditText().setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
checkMnemonic();
}
});
clearErrorOnTextEntry(etWalletMnemonic);
etWalletAddress.getEditText().setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
checkAddress();
}
});
clearErrorOnTextEntry(etWalletAddress);
etWalletViewKey.getEditText().setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
checkViewKey();
}
});
clearErrorOnTextEntry(etWalletViewKey);
etWalletSpendKey.getEditText().setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
checkSpendKey();
}
});
clearErrorOnTextEntry(etWalletSpendKey);
Helper.showKeyboard(requireActivity());
etWalletName.getEditText().setOnEditorActionListener((v, actionId, event) -> {
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|| (actionId == EditorInfo.IME_ACTION_NEXT)) {
if (checkName()) {
etWalletPassword.requestFocus();
} // otherwise ignore
return true;
}
return false;
});
if (FingerprintHelper.isDeviceSupported(getContext())) {
llFingerprintAuth.setVisibility(View.VISIBLE);
final SwitchMaterial swFingerprintAllowed = (SwitchMaterial) llFingerprintAuth.getChildAt(0);
swFingerprintAllowed.setOnClickListener(view1 -> {
if (!swFingerprintAllowed.isChecked()) return;
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity());
builder.setMessage(Html.fromHtml(getString(R.string.generate_fingerprint_warn)))
.setCancelable(false)
.setPositiveButton(getString(R.string.label_ok), null)
.setNegativeButton(getString(R.string.label_cancel), (dialogInterface, i) -> swFingerprintAllowed.setChecked(false))
.show();
});
}
switch (type) {
case TYPE_NEW:
etWalletPassword.getEditText().setImeOptions(EditorInfo.IME_ACTION_UNSPECIFIED);
break;
case TYPE_LEDGER:
etWalletPassword.getEditText().setImeOptions(EditorInfo.IME_ACTION_DONE);
etWalletPassword.getEditText().setOnEditorActionListener((v, actionId, event) -> {
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|| (actionId == EditorInfo.IME_ACTION_DONE)) {
etWalletRestoreHeight.requestFocus();
return true;
}
return false;
});
break;
case TYPE_SEED:
etWalletPassword.getEditText().setOnEditorActionListener((v, actionId, event) -> {
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|| (actionId == EditorInfo.IME_ACTION_NEXT)) {
etWalletMnemonic.requestFocus();
return true;
}
return false;
});
etWalletMnemonic.setVisibility(View.VISIBLE);
etWalletMnemonic.getEditText().setOnEditorActionListener((v, actionId, event) -> {
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|| (actionId == EditorInfo.IME_ACTION_NEXT)) {
if (checkMnemonic()) {
etWalletRestoreHeight.requestFocus();
}
return true;
}
return false;
});
bSeedOffset.setVisibility(View.VISIBLE);
bSeedOffset.setOnClickListener(v -> toggleSeedOffset());
break;
case TYPE_KEY:
case TYPE_VIEWONLY:
etWalletPassword.getEditText().setOnEditorActionListener((v, actionId, event) -> {
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|| (actionId == EditorInfo.IME_ACTION_NEXT)) {
etWalletAddress.requestFocus();
return true;
}
return false;
});
etWalletAddress.setVisibility(View.VISIBLE);
etWalletAddress.getEditText().setOnEditorActionListener((v, actionId, event) -> {
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|| (actionId == EditorInfo.IME_ACTION_NEXT)) {
if (checkAddress()) {
etWalletViewKey.requestFocus();
}
return true;
}
return false;
});
etWalletViewKey.setVisibility(View.VISIBLE);
etWalletViewKey.getEditText().setOnEditorActionListener((v, actionId, event) -> {
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|| (actionId == EditorInfo.IME_ACTION_NEXT)) {
if (checkViewKey()) {
if (type.equals(TYPE_KEY)) {
etWalletSpendKey.requestFocus();
} else {
etWalletRestoreHeight.requestFocus();
}
}
return true;
}
return false;
});
break;
}
if (type.equals(TYPE_KEY)) {
etWalletSpendKey.setVisibility(View.VISIBLE);
etWalletSpendKey.getEditText().setOnEditorActionListener((v, actionId, event) -> {
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|| (actionId == EditorInfo.IME_ACTION_NEXT)) {
if (checkSpendKey()) {
etWalletRestoreHeight.requestFocus();
}
return true;
}
return false;
});
}
if (!type.equals(TYPE_NEW)) {
etWalletRestoreHeight.setVisibility(View.VISIBLE);
etWalletRestoreHeight.getEditText().setImeOptions(EditorInfo.IME_ACTION_UNSPECIFIED);
}
bGenerate.setOnClickListener(v -> {
Helper.hideKeyboard(getActivity());
generateWallet();
});
etWalletName.requestFocus();
return view;
}
void toggleSeedOffset() {
if (etSeedOffset.getVisibility() == View.VISIBLE) {
etSeedOffset.getEditText().getText().clear();
etSeedOffset.setVisibility(View.GONE);
bSeedOffset.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_keyboard_arrow_down, 0, 0, 0);
} else {
etSeedOffset.setVisibility(View.VISIBLE);
bSeedOffset.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_keyboard_arrow_up, 0, 0, 0);
etSeedOffset.requestFocusFromTouch();
}
}
private boolean checkName() {
String name = etWalletName.getEditText().getText().toString();
boolean ok = true;
if (name.length() == 0) {
etWalletName.setError(getString(R.string.generate_wallet_name));
ok = false;
} else if (name.charAt(0) == '.') {
etWalletName.setError(getString(R.string.generate_wallet_dot));
ok = false;
} else {
File walletFile = Helper.getWalletFile(getActivity(), name);
if (WalletManager.getInstance().walletExists(walletFile)) {
etWalletName.setError(getString(R.string.generate_wallet_exists));
ok = false;
}
}
if (ok) {
etWalletName.setError(null);
}
return ok;
}
private boolean checkHeight() {
long height = !type.equals(TYPE_NEW) ? getHeight() : 0;
boolean ok = true;
if (height < 0) {
etWalletRestoreHeight.setError(getString(R.string.generate_restoreheight_error));
ok = false;
}
if (ok) {
etWalletRestoreHeight.setError(null);
}
return ok;
}
private long getHeight() {
long height = -1;
String restoreHeight = etWalletRestoreHeight.getEditText().getText().toString().trim();
if (restoreHeight.isEmpty()) return -1;
try {
// is it a date?
SimpleDateFormat parser = new SimpleDateFormat("yyyy-MM-dd");
parser.setLenient(false);
height = RestoreHeight.getInstance().getHeight(parser.parse(restoreHeight));
} catch (ParseException ignored) {
}
if ((height < 0) && (restoreHeight.length() == 8))
try {
// is it a date without dashes?
SimpleDateFormat parser = new SimpleDateFormat("yyyyMMdd");
parser.setLenient(false);
height = RestoreHeight.getInstance().getHeight(parser.parse(restoreHeight));
} catch (ParseException ignored) {
}
if (height < 0)
try {
// or is it a height?
height = Long.parseLong(restoreHeight);
} catch (NumberFormatException ex) {
return -1;
}
Timber.d("Using Restore Height = %d", height);
return height;
}
private boolean checkMnemonic() {
String seed = etWalletMnemonic.getEditText().getText().toString();
boolean ok = (seed.split("\\s").length == 25); // 25 words
if (!ok) {
etWalletMnemonic.setError(getString(R.string.generate_check_mnemonic));
} else {
etWalletMnemonic.setError(null);
}
return ok;
}
private boolean checkAddress() {
String address = etWalletAddress.getEditText().getText().toString();
boolean ok = Wallet.isAddressValid(address);
if (!ok) {
etWalletAddress.setError(getString(R.string.generate_check_address));
} else {
etWalletAddress.setError(null);
}
return ok;
}
private boolean checkViewKey() {
String viewKey = etWalletViewKey.getEditText().getText().toString();
boolean ok = (viewKey.length() == 64) && (viewKey.matches("^[0-9a-fA-F]+$"));
if (!ok) {
etWalletViewKey.setError(getString(R.string.generate_check_key));
} else {
etWalletViewKey.setError(null);
}
return ok;
}
private boolean checkSpendKey() {
String spendKey = etWalletSpendKey.getEditText().getText().toString();
boolean ok = ((spendKey.length() == 0) || ((spendKey.length() == 64) && (spendKey.matches("^[0-9a-fA-F]+$"))));
if (!ok) {
etWalletSpendKey.setError(getString(R.string.generate_check_key));
} else {
etWalletSpendKey.setError(null);
}
return ok;
}
private void generateWallet() {
if (!checkName()) return;
if (!checkHeight()) return;
String name = etWalletName.getEditText().getText().toString();
String password = etWalletPassword.getEditText().getText().toString();
boolean fingerprintAuthAllowed = ((SwitchMaterial) llFingerprintAuth.getChildAt(0)).isChecked();
// create the real wallet password
String crazyPass = KeyStoreHelper.getCrazyPass(getActivity(), password);
long height = getHeight();
if (height < 0) height = 0;
switch (type) {
case TYPE_NEW:
bGenerate.setEnabled(false);
if (fingerprintAuthAllowed) {
KeyStoreHelper.saveWalletUserPass(requireActivity(), name, password);
}
activityCallback.onGenerate(name, crazyPass);
break;
case TYPE_SEED:
if (!checkMnemonic()) return;
final String seed = etWalletMnemonic.getEditText().getText().toString();
bGenerate.setEnabled(false);
if (fingerprintAuthAllowed) {
KeyStoreHelper.saveWalletUserPass(requireActivity(), name, password);
}
final String offset = etSeedOffset.getEditText().getText().toString();
activityCallback.onGenerate(name, crazyPass, seed, offset, height);
break;
case TYPE_LEDGER:
bGenerate.setEnabled(false);
if (fingerprintAuthAllowed) {
KeyStoreHelper.saveWalletUserPass(requireActivity(), name, password);
}
activityCallback.onGenerateLedger(name, crazyPass, height);
break;
case TYPE_KEY:
case TYPE_VIEWONLY:
if (checkAddress() && checkViewKey() && checkSpendKey()) {
bGenerate.setEnabled(false);
String address = etWalletAddress.getEditText().getText().toString();
String viewKey = etWalletViewKey.getEditText().getText().toString();
String spendKey = "";
if (type.equals(TYPE_KEY)) {
spendKey = etWalletSpendKey.getEditText().getText().toString();
}
if (fingerprintAuthAllowed) {
KeyStoreHelper.saveWalletUserPass(requireActivity(), name, password);
}
activityCallback.onGenerate(name, crazyPass, address, viewKey, spendKey, height);
}
break;
}
}
public void walletGenerateError() {
bGenerate.setEnabled(true);
}
@Override
public void onResume() {
super.onResume();
Timber.d("onResume()");
activityCallback.setTitle(getString(R.string.generate_title) + " - " + getType());
activityCallback.setToolbarButton(Toolbar.BUTTON_BACK);
}
String getType() {
switch (type) {
case TYPE_KEY:
return getString(R.string.generate_wallet_type_key);
case TYPE_NEW:
return getString(R.string.generate_wallet_type_new);
case TYPE_SEED:
return getString(R.string.generate_wallet_type_seed);
case TYPE_LEDGER:
return getString(R.string.generate_wallet_type_ledger);
case TYPE_VIEWONLY:
return getString(R.string.generate_wallet_type_view);
default:
Timber.e("unknown type %s", type);
return "?";
}
}
GenerateFragment.Listener activityCallback;
public interface Listener {
void onGenerate(String name, String password);
void onGenerate(String name, String password, String seed, String offset, long height);
void onGenerate(String name, String password, String address, String viewKey, String spendKey, long height);
void onGenerateLedger(String name, String password, long height);
void setTitle(String title);
void setToolbarButton(int type);
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (context instanceof GenerateFragment.Listener) {
this.activityCallback = (GenerateFragment.Listener) context;
} else {
throw new ClassCastException(context.toString()
+ " must implement Listener");
}
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
switch (type) {
case TYPE_KEY:
inflater.inflate(R.menu.create_wallet_keys, menu);
break;
case TYPE_NEW:
inflater.inflate(R.menu.create_wallet_new, menu);
break;
case TYPE_SEED:
inflater.inflate(R.menu.create_wallet_seed, menu);
break;
case TYPE_LEDGER:
inflater.inflate(R.menu.create_wallet_ledger, menu);
break;
case TYPE_VIEWONLY:
inflater.inflate(R.menu.create_wallet_view, menu);
break;
default:
}
super.onCreateOptionsMenu(menu, inflater);
}
AlertDialog ledgerDialog = null;
public void convertLedgerSeed() {
if (ledgerDialog != null) return;
final Activity activity = requireActivity();
View promptsView = getLayoutInflater().inflate(R.layout.prompt_ledger_seed, null);
MaterialAlertDialogBuilder alertDialogBuilder = new MaterialAlertDialogBuilder(activity);
alertDialogBuilder.setView(promptsView);
final TextInputLayout etSeed = promptsView.findViewById(R.id.etSeed);
final TextInputLayout etPassphrase = promptsView.findViewById(R.id.etPassphrase);
clearErrorOnTextEntry(etSeed);
alertDialogBuilder
.setCancelable(false)
.setPositiveButton(getString(R.string.label_ok), null)
.setNegativeButton(getString(R.string.label_cancel),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
Helper.hideKeyboardAlways(activity);
etWalletMnemonic.getEditText().getText().clear();
dialog.cancel();
ledgerDialog = null;
}
});
ledgerDialog = alertDialogBuilder.create();
ledgerDialog.setOnShowListener(dialog -> {
Button button = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE);
button.setOnClickListener(view -> {
String ledgerSeed = etSeed.getEditText().getText().toString();
String ledgerPassphrase = etPassphrase.getEditText().getText().toString();
String moneroSeed = Monero.convert(ledgerSeed, ledgerPassphrase);
if (moneroSeed != null) {
etWalletMnemonic.getEditText().setText(moneroSeed);
ledgerDialog.dismiss();
ledgerDialog = null;
} else {
etSeed.setError(getString(R.string.bad_ledger_seed));
}
});
});
if (Helper.preventScreenshot()) {
ledgerDialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
}
ledgerDialog.show();
}
}

View file

@ -0,0 +1,711 @@
/*
* 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;
import android.content.Context;
import android.content.DialogInterface;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.Editable;
import android.text.Html;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.switchmaterial.SwitchMaterial;
import com.google.android.material.textfield.TextInputLayout;
import com.m2049r.xmrwallet.ledger.Ledger;
import com.m2049r.xmrwallet.ledger.LedgerProgressDialog;
import com.m2049r.xmrwallet.model.NetworkType;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.model.WalletManager;
import com.m2049r.xmrwallet.util.FingerprintHelper;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.KeyStoreHelper;
import com.m2049r.xmrwallet.util.MoneroThreadPoolExecutor;
import com.m2049r.xmrwallet.widget.PasswordEntryView;
import com.m2049r.xmrwallet.widget.Toolbar;
import java.text.NumberFormat;
import timber.log.Timber;
public class GenerateReviewFragment extends Fragment {
static final public String VIEW_TYPE_DETAILS = "details";
static final public String VIEW_TYPE_ACCEPT = "accept";
static final public String VIEW_TYPE_WALLET = "wallet";
public static final String REQUEST_TYPE = "type";
public static final String REQUEST_PATH = "path";
public static final String REQUEST_PASSWORD = "password";
private ScrollView scrollview;
private ProgressBar pbProgress;
private TextView tvWalletPassword;
private TextView tvWalletAddress;
private FrameLayout flWalletMnemonic;
private TextView tvWalletMnemonic;
private TextView tvWalletHeight;
private TextView tvWalletViewKey;
private TextView tvWalletSpendKey;
private ImageButton bCopyAddress;
private LinearLayout llAdvancedInfo;
private LinearLayout llPassword;
private LinearLayout llMnemonic;
private LinearLayout llSpendKey;
private LinearLayout llViewKey;
private Button bAdvancedInfo;
private Button bAccept;
private Button bSeedOffset;
private TextInputLayout etSeedOffset;
private String walletPath;
private String walletName;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_review, container, false);
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);
flWalletMnemonic = view.findViewById(R.id.flWalletMnemonic);
tvWalletHeight = view.findViewById(R.id.tvWalletHeight);
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);
etSeedOffset = view.findViewById(R.id.etSeedOffset);
bSeedOffset = view.findViewById(R.id.bSeedOffset);
bAccept = view.findViewById(R.id.bAccept);
boolean allowCopy = WalletManager.getInstance().getNetworkType() != NetworkType.NetworkType_Mainnet;
tvWalletMnemonic.setTextIsSelectable(allowCopy);
tvWalletSpendKey.setTextIsSelectable(allowCopy);
tvWalletPassword.setTextIsSelectable(allowCopy);
bAccept.setOnClickListener(v -> acceptWallet());
view.findViewById(R.id.bCopyViewKey).setOnClickListener(v -> copyViewKey());
bCopyAddress.setEnabled(false);
bCopyAddress.setOnClickListener(v -> copyAddress());
bAdvancedInfo.setOnClickListener(v -> toggleAdvancedInfo());
bSeedOffset.setOnClickListener(v -> toggleSeedOffset());
etSeedOffset.getEditText().addTextChangedListener(new TextWatcher() {
@Override
public void afterTextChanged(Editable s) {
showSeed();
}
@Override
public void beforeTextChanged(CharSequence s, int start,
int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start,
int before, int count) {
}
});
Bundle args = getArguments();
type = args.getString(REQUEST_TYPE);
walletPath = args.getString(REQUEST_PATH);
localPassword = args.getString(REQUEST_PASSWORD);
showDetails();
return view;
}
String getSeedOffset() {
return etSeedOffset.getEditText().getText().toString();
}
boolean seedOffsetInProgress = false;
void showSeed() {
synchronized (this) {
if (seedOffsetInProgress) return;
seedOffsetInProgress = true;
}
new AsyncShowSeed().executeOnExecutor(MoneroThreadPoolExecutor.MONERO_THREAD_POOL_EXECUTOR, walletPath);
}
void showDetails() {
tvWalletPassword.setText(null);
new AsyncShow().executeOnExecutor(MoneroThreadPoolExecutor.MONERO_THREAD_POOL_EXECUTOR, walletPath);
}
void copyViewKey() {
Helper.clipBoardCopy(requireActivity(), getString(R.string.label_copy_viewkey), tvWalletViewKey.getText().toString());
Toast.makeText(getActivity(), getString(R.string.message_copy_viewkey), Toast.LENGTH_SHORT).show();
}
void copyAddress() {
Helper.clipBoardCopy(requireActivity(), getString(R.string.label_copy_address), tvWalletAddress.getText().toString());
Toast.makeText(getActivity(), getString(R.string.message_copy_address), Toast.LENGTH_SHORT).show();
}
void nocopy() {
Toast.makeText(getActivity(), getString(R.string.message_nocopy), Toast.LENGTH_SHORT).show();
}
void toggleAdvancedInfo() {
if (llAdvancedInfo.getVisibility() == View.VISIBLE) {
llAdvancedInfo.setVisibility(View.GONE);
bAdvancedInfo.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_keyboard_arrow_down, 0, 0, 0);
} else {
llAdvancedInfo.setVisibility(View.VISIBLE);
bAdvancedInfo.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_keyboard_arrow_up, 0, 0, 0);
scrollview.post(() -> scrollview.fullScroll(ScrollView.FOCUS_DOWN));
}
}
void toggleSeedOffset() {
if (etSeedOffset.getVisibility() == View.VISIBLE) {
etSeedOffset.getEditText().getText().clear();
etSeedOffset.setVisibility(View.GONE);
bSeedOffset.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_keyboard_arrow_down, 0, 0, 0);
} else {
etSeedOffset.setVisibility(View.VISIBLE);
bSeedOffset.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_keyboard_arrow_up, 0, 0, 0);
etSeedOffset.requestFocusFromTouch();
}
}
String type;
private void acceptWallet() {
bAccept.setEnabled(false);
acceptCallback.onAccept(walletName, getPassword());
}
private class AsyncShow extends AsyncTask<String, Void, Boolean> {
String name;
String address;
long height;
String seed;
String viewKey;
String spendKey;
Wallet.Status walletStatus;
boolean dialogOpened = false;
@Override
protected void onPreExecute() {
super.onPreExecute();
showProgress();
if ((walletPath != null)
&& (WalletManager.getInstance().queryWalletDevice(walletPath + ".keys", getPassword())
== Wallet.Device.Device_Ledger)
&& (progressCallback != null)) {
progressCallback.showLedgerProgressDialog(LedgerProgressDialog.TYPE_RESTORE);
dialogOpened = true;
}
}
@Override
protected Boolean doInBackground(String... params) {
if (params.length != 1) return false;
String walletPath = params[0];
Wallet wallet;
boolean closeWallet;
if (type.equals(GenerateReviewFragment.VIEW_TYPE_WALLET)) {
wallet = GenerateReviewFragment.this.walletCallback.getWallet();
closeWallet = false;
} else {
wallet = WalletManager.getInstance().openWallet(walletPath, getPassword());
closeWallet = true;
}
name = wallet.getName();
walletStatus = wallet.getStatus();
if (!walletStatus.isOk()) {
if (closeWallet) wallet.close();
return false;
}
address = wallet.getAddress();
height = wallet.getRestoreHeight();
seed = wallet.getSeed(getSeedOffset());
switch (wallet.getDeviceType()) {
case Device_Ledger:
viewKey = Ledger.Key();
break;
case Device_Software:
viewKey = wallet.getSecretViewKey();
break;
default:
throw new IllegalStateException("Hardware backing not supported. At all!");
}
spendKey = wallet.isWatchOnly() ? getActivity().getString(R.string.label_watchonly) : wallet.getSecretSpendKey();
if (closeWallet) wallet.close();
return true;
}
@Override
protected void onPostExecute(Boolean result) {
super.onPostExecute(result);
if (dialogOpened)
progressCallback.dismissProgressDialog();
if (!isAdded()) return; // never mind
walletName = name;
if (result) {
if (type.equals(GenerateReviewFragment.VIEW_TYPE_ACCEPT)) {
bAccept.setVisibility(View.VISIBLE);
bAccept.setEnabled(true);
}
llPassword.setVisibility(View.VISIBLE);
tvWalletPassword.setText(getPassword());
tvWalletAddress.setText(address);
tvWalletHeight.setText(NumberFormat.getInstance().format(height));
if (!seed.isEmpty()) {
llMnemonic.setVisibility(View.VISIBLE);
tvWalletMnemonic.setText(seed);
}
boolean showAdvanced = false;
if (isKeyValid(viewKey)) {
llViewKey.setVisibility(View.VISIBLE);
tvWalletViewKey.setText(viewKey);
showAdvanced = true;
}
if (isKeyValid(spendKey)) {
llSpendKey.setVisibility(View.VISIBLE);
tvWalletSpendKey.setText(spendKey);
showAdvanced = true;
}
if (showAdvanced) bAdvancedInfo.setVisibility(View.VISIBLE);
bCopyAddress.setEnabled(true);
activityCallback.setTitle(name, getString(R.string.details_title));
activityCallback.setToolbarButton(
GenerateReviewFragment.VIEW_TYPE_ACCEPT.equals(type) ? Toolbar.BUTTON_NONE : Toolbar.BUTTON_BACK);
} else {
// TODO show proper error message and/or end the fragment?
tvWalletAddress.setText(walletStatus.toString());
tvWalletHeight.setText(walletStatus.toString());
tvWalletMnemonic.setText(walletStatus.toString());
tvWalletViewKey.setText(walletStatus.toString());
tvWalletSpendKey.setText(walletStatus.toString());
}
hideProgress();
}
}
Listener activityCallback = null;
ProgressListener progressCallback = null;
AcceptListener acceptCallback = null;
ListenerWithWallet walletCallback = null;
PasswordChangedListener passwordCallback = null;
public interface Listener {
void setTitle(String title, String subtitle);
void setToolbarButton(int type);
}
public interface ProgressListener {
void showProgressDialog(int msgId);
void showLedgerProgressDialog(int mode);
void dismissProgressDialog();
}
public interface AcceptListener {
void onAccept(String name, String password);
}
public interface ListenerWithWallet {
Wallet getWallet();
}
public interface PasswordChangedListener {
void onPasswordChanged(String newPassword);
String getPassword();
}
private String localPassword = null;
private String getPassword() {
if (passwordCallback != null) return passwordCallback.getPassword();
return localPassword;
}
private void setPassword(String password) {
if (passwordCallback != null) passwordCallback.onPasswordChanged(password);
else localPassword = password;
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (context instanceof Listener) {
this.activityCallback = (Listener) context;
}
if (context instanceof ProgressListener) {
this.progressCallback = (ProgressListener) context;
}
if (context instanceof AcceptListener) {
this.acceptCallback = (AcceptListener) context;
}
if (context instanceof ListenerWithWallet) {
this.walletCallback = (ListenerWithWallet) context;
}
if (context instanceof PasswordChangedListener) {
this.passwordCallback = (PasswordChangedListener) context;
}
}
@Override
public void onResume() {
super.onResume();
Timber.d("onResume()");
activityCallback.setTitle(walletName, getString(R.string.details_title));
activityCallback.setToolbarButton(
GenerateReviewFragment.VIEW_TYPE_ACCEPT.equals(type) ? Toolbar.BUTTON_NONE : Toolbar.BUTTON_BACK);
}
public void showProgress() {
pbProgress.setVisibility(View.VISIBLE);
}
public void hideProgress() {
pbProgress.setVisibility(View.INVISIBLE);
}
boolean backOk() {
return !type.equals(GenerateReviewFragment.VIEW_TYPE_ACCEPT);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
String type = getArguments().getString(REQUEST_TYPE); // intance variable <type> not set yet
if (GenerateReviewFragment.VIEW_TYPE_ACCEPT.equals(type)) {
inflater.inflate(R.menu.wallet_details_help_menu, menu);
super.onCreateOptionsMenu(menu, inflater);
} else {
inflater.inflate(R.menu.wallet_details_menu, menu);
super.onCreateOptionsMenu(menu, inflater);
}
}
boolean changeWalletPassword(String newPassword) {
Wallet wallet;
boolean closeWallet;
if (type.equals(GenerateReviewFragment.VIEW_TYPE_WALLET)) {
wallet = GenerateReviewFragment.this.walletCallback.getWallet();
closeWallet = false;
} else {
wallet = WalletManager.getInstance().openWallet(walletPath, getPassword());
closeWallet = true;
}
boolean ok = false;
Wallet.Status walletStatus = wallet.getStatus();
if (walletStatus.isOk()) {
wallet.setPassword(newPassword);
ok = true;
}
if (closeWallet) wallet.close();
return ok;
}
private class AsyncChangePassword extends AsyncTask<String, Void, Boolean> {
String newPassword;
@Override
protected void onPreExecute() {
super.onPreExecute();
if (progressCallback != null)
progressCallback.showProgressDialog(R.string.changepw_progress);
}
@Override
protected Boolean doInBackground(String... params) {
if (params.length != 2) return false;
final String userPassword = params[0];
final boolean fingerPassValid = Boolean.parseBoolean(params[1]);
newPassword = KeyStoreHelper.getCrazyPass(getActivity(), userPassword);
final boolean success = changeWalletPassword(newPassword);
if (success) {
Context ctx = getActivity();
if (ctx != null)
if (fingerPassValid) {
KeyStoreHelper.saveWalletUserPass(ctx, walletName, userPassword);
} else {
KeyStoreHelper.removeWalletUserPass(ctx, walletName);
}
}
return success;
}
@Override
protected void onPostExecute(Boolean result) {
super.onPostExecute(result);
if ((getActivity() == null) || getActivity().isDestroyed()) {
return;
}
if (progressCallback != null)
progressCallback.dismissProgressDialog();
if (result) {
Toast.makeText(getActivity(), getString(R.string.changepw_success), Toast.LENGTH_SHORT).show();
setPassword(newPassword);
showDetails();
} else {
Toast.makeText(getActivity(), getString(R.string.changepw_failed), Toast.LENGTH_LONG).show();
}
}
}
AlertDialog openDialog = null; // for preventing opening of multiple dialogs
public AlertDialog createChangePasswordDialog() {
if (openDialog != null) return null; // we are already open
LayoutInflater li = LayoutInflater.from(getActivity());
View promptsView = li.inflate(R.layout.prompt_changepw, null);
AlertDialog.Builder alertDialogBuilder = new MaterialAlertDialogBuilder(requireActivity());
alertDialogBuilder.setView(promptsView);
final PasswordEntryView etPasswordA = promptsView.findViewById(R.id.etWalletPasswordA);
etPasswordA.setHint(getString(R.string.prompt_changepw, walletName));
final TextInputLayout etPasswordB = promptsView.findViewById(R.id.etWalletPasswordB);
etPasswordB.setHint(getString(R.string.prompt_changepwB, walletName));
LinearLayout llFingerprintAuth = promptsView.findViewById(R.id.llFingerprintAuth);
final SwitchMaterial swFingerprintAllowed = (SwitchMaterial) llFingerprintAuth.getChildAt(0);
if (FingerprintHelper.isDeviceSupported(getActivity())) {
llFingerprintAuth.setVisibility(View.VISIBLE);
swFingerprintAllowed.setOnClickListener(view -> {
if (!swFingerprintAllowed.isChecked()) return;
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(requireActivity());
builder.setMessage(Html.fromHtml(getString(R.string.generate_fingerprint_warn)))
.setCancelable(false)
.setPositiveButton(getString(R.string.label_ok), null)
.setNegativeButton(getString(R.string.label_cancel), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
swFingerprintAllowed.setChecked(false);
}
})
.show();
});
swFingerprintAllowed.setChecked(FingerprintHelper.isFingerPassValid(getActivity(), walletName));
}
etPasswordA.getEditText().addTextChangedListener(new TextWatcher() {
@Override
public void afterTextChanged(Editable s) {
if (etPasswordB.getError() != null) {
etPasswordB.setError(null);
}
}
@Override
public void beforeTextChanged(CharSequence s, int start,
int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start,
int before, int count) {
}
});
etPasswordB.getEditText().addTextChangedListener(new TextWatcher() {
@Override
public void afterTextChanged(Editable s) {
if (etPasswordB.getError() != null) {
etPasswordB.setError(null);
}
}
@Override
public void beforeTextChanged(CharSequence s, int start,
int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start,
int before, int count) {
}
});
// set dialog message
alertDialogBuilder
.setCancelable(false)
.setPositiveButton(getString(R.string.label_ok), null)
.setNegativeButton(getString(R.string.label_cancel),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
Helper.hideKeyboardAlways(requireActivity());
dialog.cancel();
openDialog = null;
}
});
openDialog = alertDialogBuilder.create();
openDialog.setOnShowListener(dialog -> {
Button button = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE);
button.setOnClickListener(view -> {
String newPasswordA = etPasswordA.getEditText().getText().toString();
String newPasswordB = etPasswordB.getEditText().getText().toString();
if (!newPasswordA.equals(newPasswordB)) {
etPasswordB.setError(getString(R.string.generate_bad_passwordB));
} else {
new AsyncChangePassword().execute(newPasswordA, Boolean.toString(swFingerprintAllowed.isChecked()));
Helper.hideKeyboardAlways(requireActivity());
openDialog.dismiss();
openDialog = null;
}
});
});
// accept keyboard "ok"
etPasswordB.getEditText().setOnEditorActionListener((v, actionId, event) -> {
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|| (actionId == EditorInfo.IME_ACTION_DONE)) {
String newPasswordA = etPasswordA.getEditText().getText().toString();
String newPasswordB = etPasswordB.getEditText().getText().toString();
// disallow empty passwords
if (!newPasswordA.equals(newPasswordB)) {
etPasswordB.setError(getString(R.string.generate_bad_passwordB));
} else {
new AsyncChangePassword().execute(newPasswordA, Boolean.toString(swFingerprintAllowed.isChecked()));
Helper.hideKeyboardAlways(requireActivity());
openDialog.dismiss();
openDialog = null;
}
return true;
}
return false;
});
if (Helper.preventScreenshot()) {
openDialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
}
return openDialog;
}
private boolean isKeyValid(String key) {
return (key != null) && (key.length() == 64)
&& !key.equals("0000000000000000000000000000000000000000000000000000000000000000")
&& !key.toLowerCase().equals("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
// ledger implmenetation returns the spend key as f's
}
private class AsyncShowSeed extends AsyncTask<String, Void, Boolean> {
String seed;
String offset;
Wallet.Status walletStatus;
@Override
protected void onPreExecute() {
super.onPreExecute();
offset = getSeedOffset();
flWalletMnemonic.setAlpha(0.1f);
}
@Override
protected Boolean doInBackground(String... params) {
if (params.length != 1) return false;
String walletPath = params[0];
Wallet wallet;
boolean closeWallet;
if (type.equals(GenerateReviewFragment.VIEW_TYPE_WALLET)) {
wallet = GenerateReviewFragment.this.walletCallback.getWallet();
closeWallet = false;
} else {
wallet = WalletManager.getInstance().openWallet(walletPath, getPassword());
closeWallet = true;
}
walletStatus = wallet.getStatus();
if (!walletStatus.isOk()) {
if (closeWallet) wallet.close();
return false;
}
seed = wallet.getSeed(offset);
if (closeWallet) wallet.close();
return true;
}
@Override
protected void onPostExecute(Boolean result) {
super.onPostExecute(result);
if (!isAdded()) return; // never mind
if (result) {
if (!seed.isEmpty()) {
llMnemonic.setVisibility(View.VISIBLE);
tvWalletMnemonic.setText(seed);
}
} else {
tvWalletMnemonic.setText(walletStatus.toString());
}
seedOffsetInProgress = false;
if (!getSeedOffset().equals(offset)) { // make sure we have encrypted with the correct offset
showSeed(); // seed has changed in the meantime - recalc
} else
flWalletMnemonic.setAlpha(1);
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,563 @@
/*
* 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;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.Html;
import android.text.Spanned;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.progressindicator.CircularProgressIndicator;
import com.m2049r.xmrwallet.data.NodeInfo;
import com.m2049r.xmrwallet.dialog.HelpFragment;
import com.m2049r.xmrwallet.layout.WalletInfoAdapter;
import com.m2049r.xmrwallet.model.WalletManager;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.KeyStoreHelper;
import com.m2049r.xmrwallet.util.NetCipherHelper;
import com.m2049r.xmrwallet.util.NodePinger;
import com.m2049r.xmrwallet.util.Notice;
import com.m2049r.xmrwallet.util.ThemeHelper;
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;
import timber.log.Timber;
public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInteractionListener,
View.OnClickListener {
private WalletInfoAdapter adapter;
private final List<WalletManager.WalletInfo> walletList = new ArrayList<>();
private View tvGuntherSays;
private ImageView ivGunther;
private TextView tvNodeName;
private TextView tvNodeInfo;
private ImageButton ibNetwork;
private CircularProgressIndicator pbNetwork;
private Listener activityCallback;
// Container Activity must implement this interface
public interface Listener {
File getStorageRoot();
boolean onWalletSelected(String wallet, boolean streetmode);
void onWalletDetails(String wallet);
void onWalletRename(String name);
void onWalletBackup(String name);
void onWalletRestore();
void onWalletDelete(String walletName);
void onWalletDeleteCache(String walletName);
void onAddWallet(String type);
void onNodePrefs();
void showNet();
void setToolbarButton(int type);
void setTitle(String title);
void setNode(NodeInfo node);
NodeInfo getNode();
Set<NodeInfo> getFavouriteNodes();
Set<NodeInfo> getOrPopulateFavourites();
boolean hasLedger();
void runOnNetCipher(Runnable runnable);
}
@Override
public void onAttach(@NonNull 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()");
torStatus = null;
super.onPause();
}
@Override
public void onResume() {
super.onResume();
Timber.d("onResume() %s", activityCallback.getFavouriteNodes().size());
activityCallback.setTitle(null);
activityCallback.setToolbarButton(Toolbar.BUTTON_SETTINGS);
activityCallback.showNet();
showNetwork();
//activityCallback.runOnNetCipher(this::pingSelectedNode);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Timber.d("onCreateView");
View view = inflater.inflate(R.layout.fragment_login, container, false);
tvGuntherSays = view.findViewById(R.id.tvGuntherSays);
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);
fabImport = view.findViewById(R.id.fabImport);
fabLedger = view.findViewById(R.id.fabLedger);
fabNewL = view.findViewById(R.id.fabNewL);
fabViewL = view.findViewById(R.id.fabViewL);
fabKeyL = view.findViewById(R.id.fabKeyL);
fabSeedL = view.findViewById(R.id.fabSeedL);
fabImportL = view.findViewById(R.id.fabImportL);
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);
fab_close_screen = AnimationUtils.loadAnimation(getContext(), R.anim.fab_close_screen);
fab_open = AnimationUtils.loadAnimation(getContext(), R.anim.fab_open);
fab_close = AnimationUtils.loadAnimation(getContext(), R.anim.fab_close);
rotate_forward = AnimationUtils.loadAnimation(getContext(), R.anim.rotate_forward);
rotate_backward = AnimationUtils.loadAnimation(getContext(), R.anim.rotate_backward);
fab.setOnClickListener(this);
fabNew.setOnClickListener(this);
fabView.setOnClickListener(this);
fabKey.setOnClickListener(this);
fabSeed.setOnClickListener(this);
fabImport.setOnClickListener(this);
fabLedger.setOnClickListener(this);
fabScreen.setOnClickListener(this);
RecyclerView recyclerView = view.findViewById(R.id.list);
registerForContextMenu(recyclerView);
this.adapter = new WalletInfoAdapter(getActivity(), this);
recyclerView.setAdapter(adapter);
ViewGroup llNotice = view.findViewById(R.id.llNotice);
Notice.showAll(llNotice, ".*_login");
view.findViewById(R.id.llNode).setOnClickListener(v -> startNodePrefs());
tvNodeName = view.findViewById(R.id.tvNodeName);
tvNodeInfo = view.findViewById(R.id.tvInfo);
view.findViewById(R.id.ibRenew).setOnClickListener(v -> findBestNode());
ibNetwork = view.findViewById(R.id.ibNetwork);
ibNetwork.setOnClickListener(v -> changeNetwork());
ibNetwork.setEnabled(false);
pbNetwork = view.findViewById(R.id.pbNetwork);
Helper.hideKeyboard(getActivity());
loadList();
return view;
}
// Callbacks from WalletInfoAdapter
// Wallet touched
@Override
public void onInteraction(final View view, final WalletManager.WalletInfo infoItem) {
openWallet(infoItem.getName(), false);
}
private void openWallet(String name, boolean streetmode) {
activityCallback.onWalletSelected(name, streetmode);
}
@Override
public boolean onContextInteraction(MenuItem item, WalletManager.WalletInfo listItem) {
final int id = item.getItemId();
if (id == R.id.action_streetmode) {
openWallet(listItem.getName(), true);
} else if (id == R.id.action_info) {
showInfo(listItem.getName());
} else if (id == R.id.action_rename) {
activityCallback.onWalletRename(listItem.getName());
} else if (id == R.id.action_backup) {
activityCallback.onWalletBackup(listItem.getName());
} else if (id == R.id.action_archive) {
activityCallback.onWalletDelete(listItem.getName());
} else if (id == R.id.action_deletecache) {
activityCallback.onWalletDeleteCache(listItem.getName());
} else {
return super.onContextItemSelected(item);
}
return true;
}
public void loadList() {
Timber.d("loadList()");
WalletManager mgr = WalletManager.getInstance();
walletList.clear();
walletList.addAll(mgr.findWallets(activityCallback.getStorageRoot()));
adapter.setInfos(walletList);
// deal with Gunther & FAB animation
if (walletList.isEmpty()) {
fab.startAnimation(fab_pulse);
if (ivGunther.getDrawable() == null) {
ivGunther.setImageResource(R.drawable.ic_emptygunther);
tvGuntherSays.setVisibility(View.VISIBLE);
}
} else {
fab.clearAnimation();
if (ivGunther.getDrawable() != null) {
ivGunther.setImageDrawable(null);
}
tvGuntherSays.setVisibility(View.GONE);
}
// remove information of non-existent wallet
Set<String> removedWallets = getActivity()
.getSharedPreferences(KeyStoreHelper.SecurityConstants.WALLET_PASS_PREFS_NAME, Context.MODE_PRIVATE)
.getAll().keySet();
for (WalletManager.WalletInfo s : walletList) {
removedWallets.remove(s.getName());
}
for (String name : removedWallets) {
KeyStoreHelper.removeWalletUserPass(getActivity(), name);
}
}
private void showInfo(@NonNull String name) {
activityCallback.onWalletDetails(name);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.list_menu, menu);
super.onCreateOptionsMenu(menu, inflater);
}
private boolean isFabOpen = false;
private FloatingActionButton fab, fabNew, fabView, fabKey, fabSeed, fabImport, fabLedger;
private RelativeLayout fabScreen;
private RelativeLayout fabNewL, fabViewL, fabKeyL, fabSeedL, fabImportL, fabLedgerL;
private Animation fab_open, fab_close, rotate_forward, rotate_backward, fab_open_screen, fab_close_screen;
private Animation fab_pulse;
public boolean isFabOpen() {
return isFabOpen;
}
public void animateFAB() {
if (isFabOpen) { // close the fab
fabScreen.setClickable(false);
fabScreen.startAnimation(fab_close_screen);
fab.startAnimation(rotate_backward);
if (fabLedgerL.getVisibility() == View.VISIBLE) {
fabLedgerL.startAnimation(fab_close);
fabLedger.setClickable(false);
} else {
fabNewL.startAnimation(fab_close);
fabNew.setClickable(false);
fabViewL.startAnimation(fab_close);
fabView.setClickable(false);
fabKeyL.startAnimation(fab_close);
fabKey.setClickable(false);
fabSeedL.startAnimation(fab_close);
fabSeed.setClickable(false);
fabImportL.startAnimation(fab_close);
fabImport.setClickable(false);
}
isFabOpen = false;
} else { // open the fab
fabScreen.setClickable(true);
fabScreen.startAnimation(fab_open_screen);
fab.startAnimation(rotate_forward);
if (activityCallback.hasLedger()) {
fabLedgerL.setVisibility(View.VISIBLE);
fabNewL.setVisibility(View.GONE);
fabViewL.setVisibility(View.GONE);
fabKeyL.setVisibility(View.GONE);
fabSeedL.setVisibility(View.GONE);
fabImportL.setVisibility(View.GONE);
fabLedgerL.startAnimation(fab_open);
fabLedger.setClickable(true);
} else {
fabLedgerL.setVisibility(View.GONE);
fabNewL.setVisibility(View.VISIBLE);
fabViewL.setVisibility(View.VISIBLE);
fabKeyL.setVisibility(View.VISIBLE);
fabSeedL.setVisibility(View.VISIBLE);
fabImportL.setVisibility(View.VISIBLE);
fabNewL.startAnimation(fab_open);
fabNew.setClickable(true);
fabViewL.startAnimation(fab_open);
fabView.setClickable(true);
fabKeyL.startAnimation(fab_open);
fabKey.setClickable(true);
fabSeedL.startAnimation(fab_open);
fabSeed.setClickable(true);
fabImportL.startAnimation(fab_open);
fabImport.setClickable(true);
}
isFabOpen = true;
}
}
@Override
public void onClick(View v) {
final int id = v.getId();
Timber.d("onClick %d/%d", id, R.id.fabLedger);
if (id == R.id.fab) {
animateFAB();
} else if (id == R.id.fabNew) {
fabScreen.setVisibility(View.INVISIBLE);
isFabOpen = false;
activityCallback.onAddWallet(GenerateFragment.TYPE_NEW);
} else if (id == R.id.fabView) {
animateFAB();
activityCallback.onAddWallet(GenerateFragment.TYPE_VIEWONLY);
} else if (id == R.id.fabKey) {
animateFAB();
activityCallback.onAddWallet(GenerateFragment.TYPE_KEY);
} else if (id == R.id.fabSeed) {
animateFAB();
activityCallback.onAddWallet(GenerateFragment.TYPE_SEED);
} else if (id == R.id.fabImport) {
animateFAB();
activityCallback.onWalletRestore();
} else if (id == R.id.fabLedger) {
Timber.d("FAB_LEDGER");
animateFAB();
activityCallback.onAddWallet(GenerateFragment.TYPE_LEDGER);
} else if (id == R.id.fabScreen) {
animateFAB();
}
}
public void findBestNode() {
new AsyncFindBestNode().execute(AsyncFindBestNode.FIND_BEST);
}
public void pingSelectedNode() {
new AsyncFindBestNode().execute(AsyncFindBestNode.PING_SELECTED);
}
private NodeInfo autoselect(Set<NodeInfo> nodes) {
if (nodes.isEmpty()) return null;
NodePinger.execute(nodes, null);
List<NodeInfo> nodeList = new ArrayList<>(nodes);
Collections.sort(nodeList, NodeInfo.BestNodeComparator);
return nodeList.get(0);
}
private void setSubtext(String status) {
final Context ctx = getContext();
final Spanned text = Html.fromHtml(ctx.getString(R.string.status,
Integer.toHexString(ThemeHelper.getThemedColor(ctx, R.attr.positiveColor) & 0xFFFFFF),
Integer.toHexString(ThemeHelper.getThemedColor(ctx, android.R.attr.colorBackground) & 0xFFFFFF),
status, ""));
tvNodeInfo.setText(text);
}
private class AsyncFindBestNode extends AsyncTask<Integer, Void, NodeInfo> {
final static int PING_SELECTED = 0;
final static int FIND_BEST = 1;
private boolean netState;
@Override
protected void onPreExecute() {
super.onPreExecute();
tvNodeName.setVisibility(View.GONE);
pbNetwork.setVisibility(View.VISIBLE);
netState = ibNetwork.isClickable();
ibNetwork.setClickable(false);
setSubtext(getString(R.string.node_waiting));
}
@Override
protected NodeInfo doInBackground(Integer... params) {
Set<NodeInfo> favourites = activityCallback.getOrPopulateFavourites();
NodeInfo selectedNode;
if (params[0] == FIND_BEST) {
selectedNode = autoselect(favourites);
} else if (params[0] == PING_SELECTED) {
selectedNode = activityCallback.getNode();
if (!activityCallback.getFavouriteNodes().contains(selectedNode))
selectedNode = null; // it's not in the favourites (any longer)
if (selectedNode == null)
for (NodeInfo node : favourites) {
if (node.isSelected()) {
selectedNode = node;
break;
}
}
if (selectedNode == null) { // autoselect
selectedNode = autoselect(favourites);
} else {
selectedNode.testRpcService();
}
} else throw new IllegalStateException();
if ((selectedNode != null) && selectedNode.isValid()) {
activityCallback.setNode(selectedNode);
return selectedNode;
} else {
activityCallback.setNode(null);
return null;
}
}
@Override
protected void onPostExecute(NodeInfo result) {
if (!isAdded()) return;
tvNodeName.setVisibility(View.VISIBLE);
pbNetwork.setVisibility(View.INVISIBLE);
ibNetwork.setClickable(netState);
if (result != null) {
Timber.d("found a good node %s", result.toString());
showNode(result);
} else {
tvNodeName.setText(getResources().getText(R.string.node_create_hint));
tvNodeName.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
tvNodeInfo.setText(null);
tvNodeInfo.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());
nodeInfo.showInfo(tvNodeInfo);
tvNodeInfo.setVisibility(View.VISIBLE);
}
private void startNodePrefs() {
activityCallback.onNodePrefs();
}
// Network (Tor) stuff
private void changeNetwork() {
Timber.d("S: %s", NetCipherHelper.getStatus());
final NetCipherHelper.Status status = NetCipherHelper.getStatus();
if (status == NetCipherHelper.Status.NOT_INSTALLED) {
HelpFragment.display(requireActivity().getSupportFragmentManager(), R.string.help_tor);
} else if (status == NetCipherHelper.Status.NOT_ENABLED) {
Toast.makeText(getActivity(), getString(R.string.tor_enable_background), Toast.LENGTH_LONG).show();
} else {
pbNetwork.setVisibility(View.VISIBLE);
ibNetwork.setEnabled(false);
NetCipherHelper.getInstance().toggle();
}
}
private NetCipherHelper.Status torStatus = null;
void showNetwork() {
final NetCipherHelper.Status status = NetCipherHelper.getStatus();
Timber.d("SHOW %s", status);
if (status == torStatus) return;
torStatus = status;
switch (status) {
case ENABLED:
ibNetwork.setImageResource(R.drawable.ic_network_tor_on);
ibNetwork.setEnabled(true);
ibNetwork.setClickable(true);
pbNetwork.setVisibility(View.INVISIBLE);
break;
case NOT_ENABLED:
case DISABLED:
ibNetwork.setImageResource(R.drawable.ic_network_clearnet);
ibNetwork.setEnabled(true);
ibNetwork.setClickable(true);
pbNetwork.setVisibility(View.INVISIBLE);
break;
case STARTING:
ibNetwork.setImageResource(R.drawable.ic_network_clearnet);
ibNetwork.setEnabled(false);
pbNetwork.setVisibility(View.VISIBLE);
break;
case STOPPING:
ibNetwork.setImageResource(R.drawable.ic_network_clearnet);
ibNetwork.setEnabled(false);
pbNetwork.setVisibility(View.VISIBLE);
break;
case NOT_INSTALLED:
ibNetwork.setEnabled(true);
ibNetwork.setClickable(true);
pbNetwork.setVisibility(View.INVISIBLE);
ibNetwork.setImageResource(R.drawable.ic_network_clearnet);
break;
default:
return;
}
activityCallback.runOnNetCipher(this::pingSelectedNode);
}
}

View file

@ -0,0 +1,39 @@
/*
* Copyright (c) 2018-2020 EarlOfEgo, m2049r
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.m2049r.xmrwallet;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import com.m2049r.xmrwallet.onboarding.OnBoardingActivity;
import com.m2049r.xmrwallet.onboarding.OnBoardingManager;
public class MainActivity extends BaseActivity {
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (OnBoardingManager.shouldShowOnBoarding(getApplicationContext())) {
startActivity(new Intent(this, OnBoardingActivity.class));
} else {
startActivity(new Intent(this, LoginActivity.class));
}
finish();
}
}

View file

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

View file

@ -0,0 +1,21 @@
/*
* 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;
public interface OnBackPressedListener {
boolean onBackPressed();
}

View file

@ -0,0 +1,23 @@
/*
* Copyright (c) 2021 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 com.m2049r.xmrwallet.model.Wallet;
public interface OnBlockUpdateListener {
void onBlockUpdate(final Wallet wallet);
}

View file

@ -0,0 +1,23 @@
/*
* 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;
import com.m2049r.xmrwallet.data.BarcodeData;
public interface OnUriScannedListener {
boolean onUriScanned(BarcodeData barcodeData);
}

View file

@ -0,0 +1,469 @@
/*
* 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;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.nfc.NfcManager;
import android.os.Bundle;
import android.text.Editable;
import android.text.Html;
import android.text.InputType;
import android.text.Spanned;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.FileProvider;
import androidx.fragment.app.Fragment;
import com.google.android.material.textfield.TextInputLayout;
import com.google.android.material.transition.MaterialContainerTransform;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import com.m2049r.xmrwallet.data.BarcodeData;
import com.m2049r.xmrwallet.data.Crypto;
import com.m2049r.xmrwallet.data.Subaddress;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.ThemeHelper;
import com.m2049r.xmrwallet.widget.ExchangeView;
import com.m2049r.xmrwallet.widget.Toolbar;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import timber.log.Timber;
public class ReceiveFragment extends Fragment {
private ProgressBar pbProgress;
private TextView tvAddress;
private TextInputLayout etNotes;
private ExchangeView evAmount;
private TextView tvQrCode;
private ImageView ivQrCode;
private ImageView ivQrCodeFull;
private EditText etDummy;
private ImageButton bCopyAddress;
private MenuItem shareItem;
private Wallet wallet = null;
private boolean isMyWallet = false;
public interface Listener {
void setToolbarButton(int type);
void setTitle(String title);
void setSubtitle(String subtitle);
void showSubaddresses(boolean managerMode);
Subaddress getSelectedSubaddress();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_receive, container, false);
pbProgress = view.findViewById(R.id.pbProgress);
tvAddress = view.findViewById(R.id.tvAddress);
etNotes = view.findViewById(R.id.etNotes);
evAmount = view.findViewById(R.id.evAmount);
ivQrCode = view.findViewById(R.id.qrCode);
tvQrCode = view.findViewById(R.id.tvQrCode);
ivQrCodeFull = view.findViewById(R.id.qrCodeFull);
etDummy = view.findViewById(R.id.etDummy);
bCopyAddress = view.findViewById(R.id.bCopyAddress);
etDummy.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
bCopyAddress.setOnClickListener(v -> copyAddress());
evAmount.setOnNewAmountListener(xmr -> {
Timber.d("new amount = %s", xmr);
generateQr();
if (shareRequested && (xmr != null)) share();
});
evAmount.setOnFailedExchangeListener(() -> {
if (isAdded()) {
clearQR();
Toast.makeText(getActivity(), getString(R.string.message_exchange_failed), Toast.LENGTH_LONG).show();
}
});
final EditText notesEdit = etNotes.getEditText();
notesEdit.setRawInputType(InputType.TYPE_CLASS_TEXT);
notesEdit.setOnEditorActionListener((v, actionId, event) -> {
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|| (actionId == EditorInfo.IME_ACTION_DONE)) {
generateQr();
return true;
}
return false;
});
notesEdit.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
clearQR();
}
@Override
public void afterTextChanged(Editable s) {
}
});
tvAddress.setOnClickListener(v -> {
listenerCallback.showSubaddresses(false);
});
view.findViewById(R.id.cvQrCode).setOnClickListener(v -> {
Helper.hideKeyboard(getActivity());
etDummy.requestFocus();
if (qrValid) {
ivQrCodeFull.setImageBitmap(((BitmapDrawable) ivQrCode.getDrawable()).getBitmap());
ivQrCodeFull.setVisibility(View.VISIBLE);
} else {
evAmount.doExchange();
}
});
ivQrCodeFull.setOnClickListener(v -> {
ivQrCodeFull.setImageBitmap(null);
ivQrCodeFull.setVisibility(View.GONE);
});
showProgress();
clearQR();
if (getActivity() instanceof GenerateReviewFragment.ListenerWithWallet) {
wallet = ((GenerateReviewFragment.ListenerWithWallet) getActivity()).getWallet();
show();
} else {
throw new IllegalStateException("no wallet info");
}
View tvNfc = view.findViewById(R.id.tvNfc);
NfcManager manager = (NfcManager) getContext().getSystemService(Context.NFC_SERVICE);
if ((manager != null) && (manager.getDefaultAdapter() != null))
tvNfc.setVisibility(View.VISIBLE);
return view;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
final MaterialContainerTransform transform = new MaterialContainerTransform();
transform.setDrawingViewId(R.id.fragment_container);
transform.setDuration(getResources().getInteger(R.integer.tx_item_transition_duration));
transform.setAllContainerColors(ThemeHelper.getThemedColor(getContext(), android.R.attr.colorBackground));
setSharedElementEnterTransition(transform);
}
private boolean shareRequested = false;
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, final MenuInflater inflater) {
inflater.inflate(R.menu.receive_menu, menu);
super.onCreateOptionsMenu(menu, inflater);
shareItem = menu.findItem(R.id.menu_item_share);
shareItem.setOnMenuItemClickListener(item -> {
if (shareRequested) return true;
shareRequested = true;
if (!qrValid) {
evAmount.doExchange();
} else {
share();
}
return true;
});
}
private void share() {
shareRequested = false;
if (saveQrCode()) {
final Intent sendIntent = getSendIntent();
if (sendIntent != null)
startActivity(Intent.createChooser(sendIntent, null));
} else {
Toast.makeText(getActivity(), getString(R.string.message_qr_failed), Toast.LENGTH_SHORT).show();
}
}
private boolean saveQrCode() {
if (!qrValid) throw new IllegalStateException("trying to save null qr code!");
File cachePath = new File(getActivity().getCacheDir(), "images");
if (!cachePath.exists())
if (!cachePath.mkdirs()) throw new IllegalStateException("cannot create images folder");
File png = new File(cachePath, "QR.png");
try {
FileOutputStream stream = new FileOutputStream(png);
Bitmap qrBitmap = ((BitmapDrawable) ivQrCode.getDrawable()).getBitmap();
qrBitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
stream.close();
return true;
} catch (IOException ex) {
Timber.e(ex);
// make sure we don't share an old qr code
if (!png.delete()) throw new IllegalStateException("cannot delete old qr code");
// if we manage to delete it, the URI points to nothing and the user gets a toast with the error
}
return false;
}
private Intent getSendIntent() {
File imagePath = new File(requireActivity().getCacheDir(), "images");
File png = new File(imagePath, "QR.png");
Uri contentUri = FileProvider.getUriForFile(requireActivity(), BuildConfig.APPLICATION_ID + ".fileprovider", png);
if (contentUri != null) {
Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // temp permission for receiving app to read this file
shareIntent.setTypeAndNormalize("image/png");
shareIntent.putExtra(Intent.EXTRA_STREAM, contentUri);
if (bcData != null)
shareIntent.putExtra(Intent.EXTRA_TEXT, bcData.getUriString());
return shareIntent;
}
return null;
}
void copyAddress() {
Helper.clipBoardCopy(requireActivity(), getString(R.string.label_copy_address), subaddress.getAddress());
Toast.makeText(getActivity(), getString(R.string.message_copy_address), Toast.LENGTH_SHORT).show();
}
private boolean qrValid = false;
void clearQR() {
if (qrValid) {
ivQrCode.setImageBitmap(null);
qrValid = false;
if (isLoaded)
tvQrCode.setVisibility(View.VISIBLE);
}
}
void setQR(Bitmap qr) {
ivQrCode.setImageBitmap(qr);
qrValid = true;
tvQrCode.setVisibility(View.GONE);
}
@Override
public void onResume() {
super.onResume();
Timber.d("onResume()");
listenerCallback.setToolbarButton(Toolbar.BUTTON_BACK);
if (wallet != null) {
listenerCallback.setTitle(wallet.getName());
listenerCallback.setSubtitle(wallet.getAccountLabel());
setNewSubaddress();
} else {
listenerCallback.setSubtitle(getString(R.string.status_wallet_loading));
clearQR();
}
}
private boolean isLoaded = false;
private void show() {
Timber.d("name=%s", wallet.getName());
isLoaded = true;
hideProgress();
}
public BarcodeData getBarcodeData() {
if (qrValid)
return bcData;
else
return null;
}
private BarcodeData bcData = null;
private void generateQr() {
Timber.d("GENQR");
String address = subaddress.getAddress();
String notes = etNotes.getEditText().getText().toString();
String xmrAmount = evAmount.getAmount();
Timber.d("%s/%s/%s", xmrAmount, notes, address);
if ((xmrAmount == null) || !Wallet.isAddressValid(address)) {
clearQR();
Timber.d("CLEARQR");
return;
}
bcData = new BarcodeData(Crypto.XMR, address, notes, xmrAmount);
int size = Math.max(ivQrCode.getWidth(), ivQrCode.getHeight());
Bitmap qr = generate(bcData.getUriString(), size, size);
if (qr != null) {
setQR(qr);
Timber.d("SETQR");
etDummy.requestFocus();
Helper.hideKeyboard(getActivity());
}
}
public Bitmap generate(String text, int width, int height) {
if ((width <= 0) || (height <= 0)) return null;
Map<EncodeHintType, Object> hints = new HashMap<>();
hints.put(EncodeHintType.CHARACTER_SET, "utf-8");
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
try {
BitMatrix bitMatrix = new QRCodeWriter().encode(text, BarcodeFormat.QR_CODE, width, height, hints);
int[] pixels = new int[width * height];
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
if (bitMatrix.get(j, i)) {
pixels[i * width + j] = 0x00000000;
} else {
pixels[i * height + j] = 0xffffffff;
}
}
}
Bitmap bitmap = Bitmap.createBitmap(pixels, 0, width, width, height, Bitmap.Config.RGB_565);
bitmap = addLogo(bitmap);
return bitmap;
} catch (WriterException ex) {
Timber.e(ex);
}
return null;
}
private Bitmap addLogo(Bitmap qrBitmap) {
// addume logo & qrcode are both square
Bitmap logo = getMoneroLogo();
final int qrSize = qrBitmap.getWidth();
final int logoSize = logo.getWidth();
Bitmap logoBitmap = Bitmap.createBitmap(qrSize, qrSize, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(logoBitmap);
canvas.drawBitmap(qrBitmap, 0, 0, null);
canvas.save();
final float sx = 0.2f * qrSize / logoSize;
canvas.scale(sx, sx, qrSize / 2f, qrSize / 2f);
canvas.drawBitmap(logo, (qrSize - logoSize) / 2f, (qrSize - logoSize) / 2f, null);
canvas.restore();
return logoBitmap;
}
private Bitmap logo = null;
private Bitmap getMoneroLogo() {
if (logo == null) {
logo = Helper.getBitmap(getContext(), R.drawable.ic_monero_logo_b);
}
return logo;
}
public void showProgress() {
pbProgress.setVisibility(View.VISIBLE);
}
public void hideProgress() {
pbProgress.setVisibility(View.GONE);
}
Listener listenerCallback = null;
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (context instanceof Listener) {
this.listenerCallback = (Listener) context;
} else {
throw new ClassCastException(context.toString()
+ " must implement Listener");
}
}
@Override
public void onPause() {
Timber.d("onPause()");
Helper.hideKeyboard(getActivity());
super.onPause();
}
@Override
public void onDetach() {
Timber.d("onDetach()");
if ((wallet != null) && (isMyWallet)) {
wallet.close();
wallet = null;
isMyWallet = false;
}
super.onDetach();
}
private Subaddress subaddress = null;
void setNewSubaddress() {
final Subaddress newSubaddress = listenerCallback.getSelectedSubaddress();
if (!Objects.equals(subaddress, newSubaddress)) {
final Runnable resetSize = () -> tvAddress.animate().setDuration(125).scaleX(1).scaleY(1).start();
tvAddress.animate().alpha(1).setDuration(125)
.scaleX(1.2f).scaleY(1.2f)
.withEndAction(resetSize).start();
}
subaddress = newSubaddress;
final Context context = getContext();
Spanned label = Html.fromHtml(context.getString(R.string.receive_subaddress,
Integer.toHexString(ThemeHelper.getThemedColor(context, R.attr.positiveColor) & 0xFFFFFF),
Integer.toHexString(ThemeHelper.getThemedColor(context, android.R.attr.colorBackground) & 0xFFFFFF),
subaddress.getDisplayLabel(), subaddress.getAddress()));
tvAddress.setText(label);
generateQr();
}
}

View file

@ -0,0 +1,96 @@
/*
* Copyright (c) 2017 dm77, m2049r
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.m2049r.xmrwallet;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import androidx.fragment.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.Result;
import me.dm7.barcodescanner.zxing.ZXingScannerView;
import timber.log.Timber;
public class ScannerFragment extends Fragment implements ZXingScannerView.ResultHandler {
private OnScannedListener onScannedListener;
public interface OnScannedListener {
boolean onScanned(String qrCode);
}
private ZXingScannerView mScannerView;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
Timber.d("onCreateView");
mScannerView = new ZXingScannerView(getActivity());
return mScannerView;
}
@Override
public void onResume() {
super.onResume();
Timber.d("onResume");
mScannerView.setResultHandler(this);
mScannerView.startCamera();
}
@Override
public void handleResult(Result rawResult) {
if ((rawResult.getBarcodeFormat() == BarcodeFormat.QR_CODE)) {
if (onScannedListener.onScanned(rawResult.getText())) {
return;
} else {
Toast.makeText(getActivity(), getString(R.string.send_qr_address_invalid), Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(getActivity(), getString(R.string.send_qr_invalid), Toast.LENGTH_SHORT).show();
}
// Note from dm77:
// * Wait 2 seconds to resume the preview.
// * On older devices continuously stopping and resuming camera preview can result in freezing the app.
// * I don't know why this is the case but I don't have the time to figure out.
Handler handler = new Handler();
handler.postDelayed(() -> mScannerView.resumeCameraPreview(ScannerFragment.this), 2000);
}
@Override
public void onPause() {
Timber.d("onPause");
mScannerView.stopCamera();
super.onPause();
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof OnScannedListener) {
this.onScannedListener = (OnScannedListener) context;
} else {
throw new ClassCastException(context.toString()
+ " must implement Listener");
}
}
}

View file

@ -0,0 +1,73 @@
/*
* 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;
import android.content.Context;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.LocaleHelper;
import java.util.Locale;
import static android.view.WindowManager.LayoutParams;
public abstract class SecureActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (Helper.preventScreenshot()) {
getWindow().setFlags(LayoutParams.FLAG_SECURE, LayoutParams.FLAG_SECURE);
}
}
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(newBase);
applyOverrideConfiguration(new Configuration());
}
@Override
public void applyOverrideConfiguration(Configuration newConfig) {
super.applyOverrideConfiguration(updateConfigurationIfSupported(newConfig));
}
private Configuration updateConfigurationIfSupported(Configuration config) {
// Configuration.getLocales is added after 24 and Configuration.locale is deprecated in 24
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (!config.getLocales().isEmpty()) {
return config;
}
} else {
if (config.locale != null) {
return config;
}
}
Locale locale = LocaleHelper.getPreferredLocale(this);
if (locale != null) {
config.setLocale(locale);
}
return config;
}
}

View file

@ -0,0 +1,125 @@
package com.m2049r.xmrwallet;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import androidx.annotation.StyleRes;
import androidx.preference.ListPreference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import com.m2049r.xmrwallet.dialog.AboutFragment;
import com.m2049r.xmrwallet.dialog.CreditsFragment;
import com.m2049r.xmrwallet.dialog.PrivacyFragment;
import com.m2049r.xmrwallet.util.DayNightMode;
import com.m2049r.xmrwallet.util.LocaleHelper;
import com.m2049r.xmrwallet.util.NightmodeHelper;
import com.m2049r.xmrwallet.util.ThemeHelper;
import com.m2049r.xmrwallet.widget.Toolbar;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Locale;
import timber.log.Timber;
public class SettingsFragment extends PreferenceFragmentCompat
implements SharedPreferences.OnSharedPreferenceChangeListener {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setPreferencesFromResource(R.xml.root_preferences, rootKey);
findPreference(getString(R.string.about_info)).setOnPreferenceClickListener(preference -> {
AboutFragment.display(getParentFragmentManager());
return true;
});
findPreference(getString(R.string.privacy_info)).setOnPreferenceClickListener(preference -> {
PrivacyFragment.display(getParentFragmentManager());
return true;
});
findPreference(getString(R.string.credits_info)).setOnPreferenceClickListener(preference -> {
CreditsFragment.display(getParentFragmentManager());
return true;
});
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (key.equals(getString(R.string.preferred_locale))) {
activity.recreate();
} else if (key.equals(getString(R.string.preferred_nightmode))) {
NightmodeHelper.setNightMode(DayNightMode.valueOf(sharedPreferences.getString(key, "AUTO")));
} else if (key.equals(getString(R.string.preferred_theme))) {
ThemeHelper.setTheme((Activity) activity, sharedPreferences.getString(key, "Classic"));
activity.recreate();
}
}
private SettingsFragment.Listener activity;
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof SettingsFragment.Listener) {
activity = (SettingsFragment.Listener) context;
} else {
throw new ClassCastException(context + " must implement Listener");
}
}
@Override
public void onResume() {
super.onResume();
Timber.d("onResume()");
activity.setSubtitle(getString(R.string.menu_settings));
activity.setToolbarButton(Toolbar.BUTTON_BACK);
populateLanguages();
PreferenceManager.getDefaultSharedPreferences(requireContext())
.registerOnSharedPreferenceChangeListener(this);
}
@Override
public void onPause() {
super.onPause();
PreferenceManager.getDefaultSharedPreferences(requireContext())
.unregisterOnSharedPreferenceChangeListener(this);
}
public interface Listener {
void setToolbarButton(int type);
void setSubtitle(String title);
void recreate();
void setTheme(@StyleRes final int resId);
}
public void populateLanguages() {
ListPreference language = findPreference(getString(R.string.preferred_locale));
assert language != null;
final ArrayList<Locale> availableLocales = LocaleHelper.getAvailableLocales(requireContext());
Collections.sort(availableLocales, (locale1, locale2) -> {
String localeString1 = LocaleHelper.getDisplayName(locale1, true);
String localeString2 = LocaleHelper.getDisplayName(locale2, true);
return localeString1.compareTo(localeString2);
});
String[] localeDisplayNames = new String[1 + availableLocales.size()];
localeDisplayNames[0] = getString(R.string.language_system_default);
for (int i = 1; i < localeDisplayNames.length; i++) {
localeDisplayNames[i] = LocaleHelper.getDisplayName(availableLocales.get(i - 1), true);
}
language.setEntries(localeDisplayNames);
String[] languageTags = new String[1 + availableLocales.size()];
languageTags[0] = "";
for (int i = 1; i < languageTags.length; i++) {
languageTags[i] = availableLocales.get(i - 1).toLanguageTag();
}
language.setEntryValues(languageTags);
}
}

View file

@ -0,0 +1,241 @@
/*
* 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;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.RecyclerView;
import com.m2049r.xmrwallet.data.Subaddress;
import com.m2049r.xmrwallet.layout.SubaddressInfoAdapter;
import com.m2049r.xmrwallet.ledger.LedgerProgressDialog;
import com.m2049r.xmrwallet.model.TransactionInfo;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.model.WalletManager;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.MoneroThreadPoolExecutor;
import com.m2049r.xmrwallet.widget.Toolbar;
import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import timber.log.Timber;
public class SubaddressFragment extends Fragment implements SubaddressInfoAdapter.OnInteractionListener,
View.OnClickListener, OnBlockUpdateListener {
static public final String KEY_MODE = "mode";
static public final String MODE_MANAGER = "manager";
private SubaddressInfoAdapter adapter;
private Listener activityCallback;
private Wallet wallet;
// Container Activity must implement this interface
public interface Listener {
void onSubaddressSelected(Subaddress subaddress);
void setSubtitle(String title);
void setToolbarButton(int type);
void showSubaddress(View view, final int subaddressIndex);
void saveWallet();
}
public interface ProgressListener {
void showProgressDialog(int msgId);
void showLedgerProgressDialog(int mode);
void dismissProgressDialog();
}
private ProgressListener progressCallback = null;
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (context instanceof ProgressListener) {
progressCallback = (ProgressListener) context;
}
if (context instanceof Listener) {
activityCallback = (Listener) context;
} else {
throw new ClassCastException(context.toString()
+ " must implement Listener");
}
}
@Override
public void onPause() {
Timber.d("onPause()");
super.onPause();
}
@Override
public void onResume() {
super.onResume();
activityCallback.setSubtitle(getString(R.string.subbaddress_title));
activityCallback.setToolbarButton(Toolbar.BUTTON_BACK);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Timber.d("onCreateView");
final Bundle b = getArguments();
managerMode = ((b != null) && (MODE_MANAGER.equals(b.getString(KEY_MODE))));
View view = inflater.inflate(R.layout.fragment_subaddress, container, false);
view.findViewById(R.id.fab).setOnClickListener(this);
if (managerMode) {
view.findViewById(R.id.tvInstruction).setVisibility(View.GONE);
view.findViewById(R.id.tvHint).setVisibility(View.GONE);
}
final RecyclerView list = view.findViewById(R.id.list);
adapter = new SubaddressInfoAdapter(getActivity(), this);
list.setAdapter(adapter);
adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
list.scrollToPosition(positionStart);
}
});
Helper.hideKeyboard(getActivity());
wallet = WalletManager.getInstance().getWallet();
loadList();
return view;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
}
public void loadList() {
Timber.d("loadList()");
final int numSubaddresses = wallet.getNumSubaddresses();
final List<Subaddress> list = new ArrayList<>();
for (int i = 0; i < numSubaddresses; i++) {
list.add(wallet.getSubaddressObject(i));
}
adapter.setInfos(list);
}
@Override
public void onBlockUpdate(Wallet wallet) {
loadList();
}
@Override
public void onClick(View v) {
int id = v.getId();
if (id == R.id.fab) {
getNewSubaddress();
}
}
private int lastUsedSubaddress() {
int lastUsedSubaddress = 0;
for (TransactionInfo info : wallet.getHistory().getAll()) {
if (info.addressIndex > lastUsedSubaddress)
lastUsedSubaddress = info.addressIndex;
}
return lastUsedSubaddress;
}
private void getNewSubaddress() {
final int maxSubaddresses = lastUsedSubaddress() + wallet.getDeviceType().getSubaddressLookahead();
if (wallet.getNumSubaddresses() < maxSubaddresses)
new AsyncSubaddress().executeOnExecutor(MoneroThreadPoolExecutor.MONERO_THREAD_POOL_EXECUTOR);
else
Toast.makeText(getActivity(), getString(R.string.max_subaddress_warning), Toast.LENGTH_LONG).show();
}
@SuppressLint("StaticFieldLeak")
@RequiredArgsConstructor
private class AsyncSubaddress extends AsyncTask<Void, Void, Boolean> {
boolean dialogOpened = false;
@Override
protected void onPreExecute() {
super.onPreExecute();
if ((wallet.getDeviceType() == Wallet.Device.Device_Ledger) && (progressCallback != null)) {
progressCallback.showLedgerProgressDialog(LedgerProgressDialog.TYPE_SUBADDRESS);
dialogOpened = true;
}
}
@Override
protected Boolean doInBackground(Void... params) {
if (params.length != 0) return false;
wallet.getNewSubaddress();
if (activityCallback != null) {
activityCallback.saveWallet();
}
return true;
}
@Override
protected void onPostExecute(Boolean result) {
super.onPostExecute(result);
if (dialogOpened)
progressCallback.dismissProgressDialog();
if (!isAdded()) // never mind then
return;
loadList();
}
}
boolean managerMode = false;
// Callbacks from SubaddressInfoAdapter
@Override
public void onInteraction(final View view, final Subaddress subaddress) {
if (managerMode)
activityCallback.showSubaddress(view, subaddress.getAddressIndex());
else
activityCallback.onSubaddressSelected(subaddress); // also closes the fragment with onBackpressed()
}
@Override
public boolean onLongInteraction(View view, Subaddress subaddress) {
activityCallback.showSubaddress(view, subaddress.getAddressIndex());
return false;
}
}

View file

@ -0,0 +1,173 @@
/*
* 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;
import android.content.Context;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.RecyclerView;
import androidx.transition.Transition;
import androidx.transition.TransitionInflater;
import com.google.android.material.textfield.TextInputLayout;
import com.m2049r.xmrwallet.data.Subaddress;
import com.m2049r.xmrwallet.layout.TransactionInfoAdapter;
import com.m2049r.xmrwallet.model.TransactionInfo;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.widget.Toolbar;
import java.util.ArrayList;
import java.util.List;
import timber.log.Timber;
public class SubaddressInfoFragment extends Fragment
implements TransactionInfoAdapter.OnInteractionListener, OnBlockUpdateListener {
private TransactionInfoAdapter adapter;
private Subaddress subaddress;
private TextInputLayout etName;
private TextView tvAddress;
private TextView tvTxLabel;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_subaddressinfo, container, false);
etName = view.findViewById(R.id.etName);
tvAddress = view.findViewById(R.id.tvAddress);
tvTxLabel = view.findViewById(R.id.tvTxLabel);
final RecyclerView list = view.findViewById(R.id.list);
adapter = new TransactionInfoAdapter(getActivity(), this);
list.setAdapter(adapter);
final Wallet wallet = activityCallback.getWallet();
Bundle b = getArguments();
final int subaddressIndex = b.getInt("subaddressIndex");
subaddress = wallet.getSubaddressObject(subaddressIndex);
etName.getEditText().setText(subaddress.getDisplayLabel());
tvAddress.setText(getContext().getString(R.string.subbaddress_info_subtitle,
subaddress.getAddressIndex(), subaddress.getSquashedAddress()));
etName.getEditText().setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
wallet.setSubaddressLabel(subaddressIndex, etName.getEditText().getText().toString());
}
});
etName.getEditText().setOnEditorActionListener((v, actionId, event) -> {
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|| (actionId == EditorInfo.IME_ACTION_DONE)) {
Helper.hideKeyboard(getActivity());
wallet.setSubaddressLabel(subaddressIndex, etName.getEditText().getText().toString());
onRefreshed(wallet);
return true;
}
return false;
});
onRefreshed(wallet);
return view;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Transition transform = TransitionInflater.from(requireContext())
.inflateTransition(R.transition.details);
setSharedElementEnterTransition(transform);
}
public void onRefreshed(final Wallet wallet) {
Timber.d("onRefreshed");
List<TransactionInfo> list = new ArrayList<>();
for (TransactionInfo info : wallet.getHistory().getAll()) {
if (info.addressIndex == subaddress.getAddressIndex())
list.add(info);
}
adapter.setInfos(list);
if (list.isEmpty())
tvTxLabel.setText(R.string.subaddress_notx_label);
else
tvTxLabel.setText(R.string.subaddress_tx_label);
}
@Override
public void onBlockUpdate(Wallet wallet) {
onRefreshed(wallet);
}
// Callbacks from TransactionInfoAdapter
@Override
public void onInteraction(final View view, final TransactionInfo infoItem) {
activityCallback.onTxDetailsRequest(view, infoItem);
}
Listener activityCallback;
// Container Activity must implement this interface
public interface Listener {
void onTxDetailsRequest(View view, TransactionInfo info);
Wallet getWallet();
void setToolbarButton(int type);
void setTitle(String title, String subtitle);
void setSubtitle(String subtitle);
}
@Override
public void onAttach(@NonNull 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 onResume() {
super.onResume();
Timber.d("onResume()");
activityCallback.setSubtitle(getString(R.string.subbaddress_title));
activityCallback.setToolbarButton(Toolbar.BUTTON_BACK);
}
@Override
public void onPause() {
super.onPause();
}
}

View file

@ -0,0 +1,406 @@
/*
* 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;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.graphics.Paint;
import android.net.Uri;
import android.os.Bundle;
import android.text.Html;
import android.text.InputType;
import android.text.Spanned;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.transition.Transition;
import androidx.transition.TransitionInflater;
import com.m2049r.xmrwallet.data.Subaddress;
import com.m2049r.xmrwallet.data.UserNotes;
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.ThemeHelper;
import com.m2049r.xmrwallet.widget.Toolbar;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.TimeZone;
import timber.log.Timber;
public class TxFragment extends Fragment {
static public final String ARG_INFO = "info";
private final SimpleDateFormat TS_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
public TxFragment() {
super();
Calendar cal = Calendar.getInstance();
TimeZone tz = cal.getTimeZone(); //get the local time zone.
TS_FORMATTER.setTimeZone(tz);
}
private TextView tvAccount;
private TextView tvAddress;
private TextView tvTxTimestamp;
private TextView tvTxId;
private TextView tvTxKey;
private TextView tvDestination;
private TextView tvTxPaymentId;
private TextView tvTxBlockheight;
private TextView tvTxAmount;
private TextView tvTxFee;
private TextView tvTxTransfers;
private TextView etTxNotes;
// XMRTO stuff
private View cvXmrTo;
private TextView tvTxXmrToKey;
private TextView tvDestinationBtc;
private TextView tvTxAmountBtc;
private TextView tvXmrToSupport;
private TextView tvXmrToKeyLabel;
private ImageView tvXmrToLogo;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_tx_info, container, false);
cvXmrTo = view.findViewById(R.id.cvXmrTo);
tvTxXmrToKey = view.findViewById(R.id.tvTxXmrToKey);
tvDestinationBtc = view.findViewById(R.id.tvDestinationBtc);
tvTxAmountBtc = view.findViewById(R.id.tvTxAmountBtc);
tvXmrToSupport = view.findViewById(R.id.tvXmrToSupport);
tvXmrToKeyLabel = view.findViewById(R.id.tvXmrToKeyLabel);
tvXmrToLogo = view.findViewById(R.id.tvXmrToLogo);
tvAccount = view.findViewById(R.id.tvAccount);
tvAddress = view.findViewById(R.id.tvAddress);
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);
etTxNotes.setRawInputType(InputType.TYPE_CLASS_TEXT);
tvTxXmrToKey.setOnClickListener(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();
});
info = getArguments().getParcelable(ARG_INFO);
show();
return view;
}
void shareTxInfo() {
if (this.info == null) return;
StringBuffer sb = new StringBuffer();
sb.append(getString(R.string.tx_timestamp)).append(":\n");
sb.append(TS_FORMATTER.format(new Date(info.timestamp * 1000))).append("\n\n");
sb.append(getString(R.string.tx_amount)).append(":\n");
sb.append((info.direction == TransactionInfo.Direction.Direction_In ? "+" : "-"));
sb.append(Wallet.getDisplayAmount(info.amount)).append("\n");
sb.append(getString(R.string.tx_fee)).append(":\n");
sb.append(Wallet.getDisplayAmount(info.fee)).append("\n\n");
sb.append(getString(R.string.tx_notes)).append(":\n");
String oneLineNotes = info.notes.replace("\n", " ; ");
sb.append(oneLineNotes.isEmpty() ? "-" : oneLineNotes).append("\n\n");
sb.append(getString(R.string.tx_destination)).append(":\n");
sb.append(tvDestination.getText()).append("\n\n");
sb.append(getString(R.string.tx_paymentId)).append(":\n");
sb.append(info.paymentId).append("\n\n");
sb.append(getString(R.string.tx_id)).append(":\n");
sb.append(info.hash).append("\n");
sb.append(getString(R.string.tx_key)).append(":\n");
sb.append(info.txKey.isEmpty() ? "-" : info.txKey).append("\n\n");
sb.append(getString(R.string.tx_blockheight)).append(":\n");
if (info.isFailed) {
sb.append(getString(R.string.tx_failed)).append("\n");
} else if (info.isPending) {
sb.append(getString(R.string.tx_pending)).append("\n");
} else {
sb.append(info.blockheight).append("\n");
}
sb.append("\n");
sb.append(getString(R.string.tx_transfers)).append(":\n");
if (info.transfers != null) {
boolean comma = false;
for (Transfer transfer : info.transfers) {
if (comma) {
sb.append(", ");
} else {
comma = true;
}
sb.append(transfer.address).append(": ");
sb.append(Wallet.getDisplayAmount(transfer.amount));
}
} else {
sb.append("-");
}
sb.append("\n\n");
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, sb.toString());
sendIntent.setType("text/plain");
startActivity(Intent.createChooser(sendIntent, null));
}
TransactionInfo info = null;
UserNotes userNotes = null;
void loadNotes() {
if ((userNotes == null) || (info.notes == null)) {
info.notes = activityCallback.getTxNotes(info.hash);
}
userNotes = new UserNotes(info.notes);
etTxNotes.setText(userNotes.note);
}
private void setTxColour(int clr) {
tvTxAmount.setTextColor(clr);
tvTxFee.setTextColor(clr);
}
private void showSubaddressLabel() {
final Subaddress subaddress = activityCallback.getWalletSubaddress(info.accountIndex, info.addressIndex);
final Context ctx = getContext();
Spanned label = Html.fromHtml(ctx.getString(R.string.tx_account_formatted,
info.accountIndex, info.addressIndex,
Integer.toHexString(ThemeHelper.getThemedColor(ctx, R.attr.positiveColor) & 0xFFFFFF),
Integer.toHexString(ThemeHelper.getThemedColor(ctx, android.R.attr.colorBackground) & 0xFFFFFF),
subaddress.getDisplayLabel()));
tvAccount.setText(label);
tvAccount.setOnClickListener(v -> activityCallback.showSubaddress(v, info.addressIndex));
}
private void show() {
if (info.txKey == null) {
info.txKey = activityCallback.getTxKey(info.hash);
}
if (info.address == null) {
info.address = activityCallback.getTxAddress(info.accountIndex, info.addressIndex);
}
loadNotes();
showSubaddressLabel();
tvAddress.setText(info.address);
tvTxTimestamp.setText(TS_FORMATTER.format(new Date(info.timestamp * 1000)));
tvTxId.setText(info.hash);
tvTxKey.setText(info.txKey.isEmpty() ? "-" : info.txKey);
tvTxPaymentId.setText(info.paymentId);
if (info.isFailed) {
tvTxBlockheight.setText(getString(R.string.tx_failed));
} else if (info.isPending) {
tvTxBlockheight.setText(getString(R.string.tx_pending));
} else {
tvTxBlockheight.setText("" + info.blockheight);
}
String sign = (info.direction == TransactionInfo.Direction.Direction_In ? "+" : "-");
long realAmount = info.amount;
tvTxAmount.setText(sign + Wallet.getDisplayAmount(realAmount));
if ((info.fee > 0)) {
String fee = Wallet.getDisplayAmount(info.fee);
tvTxFee.setText(getString(R.string.tx_list_fee, fee));
} else {
tvTxFee.setText(null);
tvTxFee.setVisibility(View.GONE);
}
if (info.isFailed) {
tvTxAmount.setText(getString(R.string.tx_list_amount_failed, Wallet.getDisplayAmount(info.amount)));
tvTxFee.setText(getString(R.string.tx_list_failed_text));
setTxColour(ThemeHelper.getThemedColor(getContext(), R.attr.neutralColor));
} else if (info.isPending) {
setTxColour(ThemeHelper.getThemedColor(getContext(), R.attr.neutralColor));
} else if (info.direction == TransactionInfo.Direction.Direction_In) {
setTxColour(ThemeHelper.getThemedColor(getContext(), R.attr.positiveColor));
} else {
setTxColour(ThemeHelper.getThemedColor(getContext(), R.attr.negativeColor));
}
Set<String> destinations = new HashSet<>();
StringBuilder sb = new StringBuilder();
StringBuilder dstSb = new StringBuilder();
if (info.transfers != null) {
boolean newline = false;
for (Transfer transfer : info.transfers) {
destinations.add(transfer.address);
if (newline) {
sb.append("\n");
} else {
newline = true;
}
sb.append("[").append(transfer.address.substring(0, 6)).append("] ");
sb.append(Wallet.getDisplayAmount(transfer.amount));
}
newline = false;
for (String dst : destinations) {
if (newline) {
dstSb.append("\n");
} else {
newline = true;
}
dstSb.append(dst);
}
} else {
sb.append("-");
dstSb.append(info.direction == TransactionInfo.Direction.Direction_In ?
activityCallback.getWalletSubaddress(info.accountIndex, info.addressIndex).getAddress() :
"-");
}
tvTxTransfers.setText(sb.toString());
tvDestination.setText(dstSb.toString());
showBtcInfo();
}
@SuppressLint("SetTextI18n")
void showBtcInfo() {
if (userNotes.xmrtoKey != null) {
cvXmrTo.setVisibility(View.VISIBLE);
String key = userNotes.xmrtoKey;
if ("xmrto".equals(userNotes.xmrtoTag)) { // legacy xmr.to service :(
key = "xmrto-" + key;
}
tvTxXmrToKey.setText(key);
tvDestinationBtc.setText(userNotes.xmrtoDestination);
tvTxAmountBtc.setText(userNotes.xmrtoAmount + " " + userNotes.xmrtoCurrency);
switch (userNotes.xmrtoTag) {
case "xmrto":
tvXmrToSupport.setVisibility(View.GONE);
tvXmrToKeyLabel.setVisibility(View.INVISIBLE);
tvXmrToLogo.setImageResource(R.drawable.ic_xmrto_logo);
break;
case "side": // defaults in layout - just add underline
tvXmrToSupport.setPaintFlags(tvXmrToSupport.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
tvXmrToSupport.setOnClickListener(v -> {
Uri uri = Uri.parse("https://sideshift.ai/orders/" + userNotes.xmrtoKey);
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(intent);
});
break;
default:
tvXmrToSupport.setVisibility(View.GONE);
tvXmrToKeyLabel.setVisibility(View.INVISIBLE);
tvXmrToLogo.setVisibility(View.GONE);
}
} else {
cvXmrTo.setVisibility(View.GONE);
}
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
Transition transform = TransitionInflater.from(requireContext())
.inflateTransition(R.transition.details);
setSharedElementEnterTransition(transform);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.tx_info_menu, menu);
super.onCreateOptionsMenu(menu, inflater);
}
Listener activityCallback;
public interface Listener {
Subaddress getWalletSubaddress(int accountIndex, int subaddressIndex);
String getTxKey(String hash);
String getTxNotes(String hash);
boolean setTxNotes(String txId, String txNotes);
String getTxAddress(int major, int minor);
void setToolbarButton(int type);
void setSubtitle(String subtitle);
void showSubaddress(View view, final int subaddressIndex);
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof TxFragment.Listener) {
this.activityCallback = (TxFragment.Listener) context;
} else {
throw new ClassCastException(context.toString()
+ " must implement Listener");
}
}
@Override
public void onPause() {
if (!etTxNotes.getText().toString().equals(userNotes.note)) { // notes have changed
// save them
userNotes.setNote(etTxNotes.getText().toString());
info.notes = userNotes.txNotes;
activityCallback.setTxNotes(info.hash, info.notes);
}
Helper.hideKeyboard(getActivity());
super.onPause();
}
@Override
public void onResume() {
super.onResume();
Timber.d("onResume()");
activityCallback.setSubtitle(getString(R.string.tx_title));
activityCallback.setToolbarButton(Toolbar.BUTTON_BACK);
showSubaddressLabel();
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,559 @@
/*
* 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;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.Spinner;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.RecyclerView;
import com.github.brnunes.swipeablerecyclerview.SwipeableRecyclerViewTouchListener;
import com.m2049r.xmrwallet.layout.TransactionInfoAdapter;
import com.m2049r.xmrwallet.model.TransactionInfo;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.service.exchange.api.ExchangeApi;
import com.m2049r.xmrwallet.service.exchange.api.ExchangeCallback;
import com.m2049r.xmrwallet.service.exchange.api.ExchangeRate;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.ServiceHelper;
import com.m2049r.xmrwallet.util.ThemeHelper;
import com.m2049r.xmrwallet.widget.Toolbar;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import timber.log.Timber;
public class WalletFragment extends Fragment
implements TransactionInfoAdapter.OnInteractionListener {
private TransactionInfoAdapter adapter;
private final NumberFormat formatter = NumberFormat.getInstance();
private TextView tvStreetView;
private LinearLayout llBalance;
private FrameLayout flExchange;
private TextView tvBalance;
private TextView tvUnconfirmedAmount;
private TextView tvProgress;
private ImageView ivSynced;
private ProgressBar pbProgress;
private Button bReceive;
private Button bSend;
private ImageView ivStreetGunther;
private Drawable streetGunther = null;
RecyclerView txlist;
private Spinner sCurrency;
private final List<String> dismissedTransactions = new ArrayList<>();
public void resetDismissedTransactions() {
dismissedTransactions.clear();
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
if (activityCallback.hasWallet())
inflater.inflate(R.menu.wallet_menu, menu);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_wallet, container, false);
ivStreetGunther = view.findViewById(R.id.ivStreetGunther);
tvStreetView = view.findViewById(R.id.tvStreetView);
llBalance = view.findViewById(R.id.llBalance);
flExchange = view.findViewById(R.id.flExchange);
((ProgressBar) view.findViewById(R.id.pbExchange)).getIndeterminateDrawable().
setColorFilter(
ThemeHelper.getThemedColor(getContext(), R.attr.colorPrimaryVariant),
android.graphics.PorterDuff.Mode.MULTIPLY);
tvProgress = view.findViewById(R.id.tvProgress);
pbProgress = view.findViewById(R.id.pbProgress);
tvBalance = view.findViewById(R.id.tvBalance);
showBalance(Helper.getDisplayAmount(0));
tvUnconfirmedAmount = view.findViewById(R.id.tvUnconfirmedAmount);
showUnconfirmed(0);
ivSynced = view.findViewById(R.id.ivSynced);
sCurrency = view.findViewById(R.id.sCurrency);
List<String> currencies = new ArrayList<>();
currencies.add(Helper.BASE_CRYPTO);
if (Helper.SHOW_EXCHANGERATES)
currencies.addAll(Arrays.asList(getResources().getStringArray(R.array.currency)));
ArrayAdapter<String> spinnerAdapter = new ArrayAdapter<>(requireContext(), R.layout.item_spinner_balance, currencies);
spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
sCurrency.setAdapter(spinnerAdapter);
bSend = view.findViewById(R.id.bSend);
bReceive = view.findViewById(R.id.bReceive);
txlist = view.findViewById(R.id.list);
adapter = new TransactionInfoAdapter(getActivity(), this);
txlist.setAdapter(adapter);
adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
if ((positionStart == 0) && (txlist.computeVerticalScrollOffset() == 0))
txlist.scrollToPosition(positionStart);
}
});
txlist.addOnItemTouchListener(
new SwipeableRecyclerViewTouchListener(txlist,
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);
}
}
@Override
public void onDismissedBySwipeRight(RecyclerView recyclerView, int[] reverseSortedPositions) {
for (int position : reverseSortedPositions) {
dismissedTransactions.add(adapter.getItem(position).hash);
adapter.removeItem(position);
}
}
}));
bSend.setOnClickListener(v -> activityCallback.onSendRequest(v));
bReceive.setOnClickListener(v -> activityCallback.onWalletReceive(v));
sCurrency.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parentView, View selectedItemView, int position, long id) {
refreshBalance();
}
@Override
public void onNothingSelected(AdapterView<?> parentView) {
// nothing (yet?)
}
});
if (activityCallback.isSynced()) {
onSynced();
}
activityCallback.forceUpdate();
return view;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
}
void showBalance(String balance) {
tvBalance.setText(balance);
final boolean streetMode = activityCallback.isStreetMode();
if (!streetMode) {
llBalance.setVisibility(View.VISIBLE);
tvStreetView.setVisibility(View.INVISIBLE);
} else {
llBalance.setVisibility(View.INVISIBLE);
tvStreetView.setVisibility(View.VISIBLE);
}
setStreetModeBackground(streetMode);
}
void showUnconfirmed(double unconfirmedAmount) {
if (activityCallback.isStreetMode() || unconfirmedAmount == 0) {
tvUnconfirmedAmount.setText(null);
tvUnconfirmedAmount.setVisibility(View.GONE);
} else {
String unconfirmed = Helper.getFormattedAmount(unconfirmedAmount, true);
tvUnconfirmedAmount.setText(getResources().getString(R.string.xmr_unconfirmed_amount, unconfirmed));
tvUnconfirmedAmount.setVisibility(View.VISIBLE);
}
}
void updateBalance() {
if (isExchanging) return; // wait for exchange to finish - it will fire this itself then.
// at this point selection is XMR in case of error
String displayB;
double amountA = Helper.getDecimalAmount(unlockedBalance).doubleValue();
if (!Helper.BASE_CRYPTO.equals(balanceCurrency)) { // not XMR
double amountB = amountA * balanceRate;
displayB = Helper.getFormattedAmount(amountB, false);
} else { // XMR
displayB = Helper.getFormattedAmount(amountA, true);
}
showBalance(displayB);
}
String balanceCurrency = Helper.BASE_CRYPTO;
double balanceRate = 1.0;
private final ExchangeApi exchangeApi = ServiceHelper.getExchangeApi();
void refreshBalance() {
double unconfirmedXmr = Helper.getDecimalAmount(balance - unlockedBalance).doubleValue();
showUnconfirmed(unconfirmedXmr);
if (sCurrency.getSelectedItemPosition() == 0) { // XMR
double amountXmr = Helper.getDecimalAmount(unlockedBalance).doubleValue();
showBalance(Helper.getFormattedAmount(amountXmr, true));
} else { // not XMR
String currency = (String) sCurrency.getSelectedItem();
Timber.d(currency);
if (!currency.equals(balanceCurrency) || (balanceRate <= 0)) {
showExchanging();
exchangeApi.queryExchangeRate(Helper.BASE_CRYPTO, currency,
new ExchangeCallback() {
@Override
public void onSuccess(final ExchangeRate exchangeRate) {
if (isAdded())
new Handler(Looper.getMainLooper()).post(() -> exchange(exchangeRate));
}
@Override
public void onError(final Exception e) {
Timber.e(e.getLocalizedMessage());
if (isAdded())
new Handler(Looper.getMainLooper()).post(() -> exchangeFailed());
}
});
} else {
updateBalance();
}
}
}
boolean isExchanging = false;
void showExchanging() {
isExchanging = true;
tvBalance.setVisibility(View.GONE);
flExchange.setVisibility(View.VISIBLE);
sCurrency.setEnabled(false);
}
void hideExchanging() {
isExchanging = false;
tvBalance.setVisibility(View.VISIBLE);
flExchange.setVisibility(View.GONE);
sCurrency.setEnabled(true);
}
public void exchangeFailed() {
sCurrency.setSelection(0, true); // default to XMR
double amountXmr = Helper.getDecimalAmount(unlockedBalance).doubleValue();
showBalance(Helper.getFormattedAmount(amountXmr, true));
hideExchanging();
}
public void exchange(final ExchangeRate exchangeRate) {
hideExchanging();
if (!Helper.BASE_CRYPTO.equals(exchangeRate.getBaseCurrency())) {
Timber.e("Not XMR");
sCurrency.setSelection(0, true);
balanceCurrency = Helper.BASE_CRYPTO;
balanceRate = 1.0;
} else {
int spinnerPosition = ((ArrayAdapter) sCurrency.getAdapter()).getPosition(exchangeRate.getQuoteCurrency());
if (spinnerPosition < 0) { // requested currency not in list
Timber.e("Requested currency not in list %s", exchangeRate.getQuoteCurrency());
sCurrency.setSelection(0, true);
} else {
sCurrency.setSelection(spinnerPosition, true);
}
balanceCurrency = exchangeRate.getQuoteCurrency();
balanceRate = exchangeRate.getRate();
}
updateBalance();
}
// Callbacks from TransactionInfoAdapter
@Override
public void onInteraction(final View view, final TransactionInfo infoItem) {
activityCallback.onTxDetailsRequest(view, infoItem);
}
// if account index has changed scroll to top?
private int accountIndex = 0;
public void onRefreshed(final Wallet wallet, boolean full) {
Timber.d("onRefreshed(%b)", full);
if (adapter.needsTransactionUpdateOnNewBlock()) {
wallet.refreshHistory();
full = true;
}
if (full) {
List<TransactionInfo> list = new ArrayList<>();
final long streetHeight = activityCallback.getStreetModeHeight();
Timber.d("StreetHeight=%d", streetHeight);
wallet.refreshHistory();
for (TransactionInfo info : wallet.getHistory().getAll()) {
Timber.d("TxHeight=%d, Label=%s", info.blockheight, info.subaddressLabel);
if ((info.isPending || (info.blockheight >= streetHeight))
&& !dismissedTransactions.contains(info.hash))
list.add(info);
}
adapter.setInfos(list);
if (accountIndex != wallet.getAccountIndex()) {
accountIndex = wallet.getAccountIndex();
txlist.scrollToPosition(0);
}
}
updateStatus(wallet);
}
public void onSynced() {
if (!activityCallback.isWatchOnly()) {
bSend.setVisibility(View.VISIBLE);
bSend.setEnabled(true);
}
if (isVisible()) enableAccountsList(true); //otherwise it is enabled in onResume()
}
public void unsync() {
if (!activityCallback.isWatchOnly()) {
bSend.setVisibility(View.INVISIBLE);
bSend.setEnabled(false);
}
if (isVisible()) enableAccountsList(false); //otherwise it is enabled in onResume()
firstBlock = 0;
}
boolean walletLoaded = false;
public void onLoaded() {
walletLoaded = true;
showReceive();
}
private void showReceive() {
if (walletLoaded) {
bReceive.setVisibility(View.VISIBLE);
bReceive.setEnabled(true);
}
}
private String syncText = null;
public void setProgress(final String text) {
syncText = text;
tvProgress.setText(text);
}
private int syncProgress = -1;
public void setProgress(final int n) {
syncProgress = n;
if (n > 100) {
pbProgress.setIndeterminate(true);
pbProgress.setVisibility(View.VISIBLE);
} else if (n >= 0) {
pbProgress.setIndeterminate(false);
pbProgress.setProgress(n);
pbProgress.setVisibility(View.VISIBLE);
} else { // <0
pbProgress.setVisibility(View.INVISIBLE);
}
}
void setActivityTitle(Wallet wallet) {
if (wallet == null) return;
walletTitle = wallet.getName();
walletSubtitle = wallet.getAccountLabel();
activityCallback.setTitle(walletTitle, walletSubtitle);
Timber.d("wallet title is %s", walletTitle);
}
private long firstBlock = 0;
private String walletTitle = null;
private String walletSubtitle = null;
private long unlockedBalance = 0;
private long balance = 0;
private int accountIdx = -1;
private void updateStatus(Wallet wallet) {
if (!isAdded()) return;
Timber.d("updateStatus()");
if ((walletTitle == null) || (accountIdx != wallet.getAccountIndex())) {
accountIdx = wallet.getAccountIndex();
setActivityTitle(wallet);
}
balance = wallet.getBalance();
unlockedBalance = wallet.getUnlockedBalance();
refreshBalance();
String sync;
if (!activityCallback.hasBoundService())
throw new IllegalStateException("WalletService not bound.");
Wallet.ConnectionStatus daemonConnected = activityCallback.getConnectionStatus();
if (daemonConnected == Wallet.ConnectionStatus.ConnectionStatus_Connected) {
if (!wallet.isSynchronized()) {
long daemonHeight = activityCallback.getDaemonHeight();
long walletHeight = wallet.getBlockChainHeight();
long n = daemonHeight - walletHeight;
sync = getString(R.string.status_syncing) + " " + formatter.format(n) + " " + getString(R.string.status_remaining);
if (firstBlock == 0) {
firstBlock = walletHeight;
}
int x = 100 - Math.round(100f * n / (1f * daemonHeight - firstBlock));
if (x == 0) x = 101; // indeterminate
setProgress(x);
ivSynced.setVisibility(View.GONE);
} else {
sync = getString(R.string.status_synced) + " " + formatter.format(wallet.getBlockChainHeight());
ivSynced.setVisibility(View.VISIBLE);
}
} else {
sync = getString(R.string.status_wallet_connecting);
setProgress(101);
}
setProgress(sync);
// TODO show connected status somewhere
}
Listener activityCallback;
// Container Activity must implement this interface
public interface Listener {
boolean hasBoundService();
void forceUpdate();
Wallet.ConnectionStatus getConnectionStatus();
long getDaemonHeight(); //mBoundService.getDaemonHeight();
void onSendRequest(View view);
void onTxDetailsRequest(View view, TransactionInfo info);
boolean isSynced();
boolean isStreetMode();
long getStreetModeHeight();
boolean isWatchOnly();
String getTxKey(String txId);
void onWalletReceive(View view);
boolean hasWallet();
Wallet getWallet();
void setToolbarButton(int type);
void setTitle(String title, String subtitle);
void setSubtitle(String subtitle);
}
@Override
public void onAttach(@NonNull 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 onResume() {
super.onResume();
Timber.d("onResume()");
activityCallback.setTitle(walletTitle, walletSubtitle);
activityCallback.setToolbarButton(Toolbar.BUTTON_NONE);
setProgress(syncProgress);
setProgress(syncText);
showReceive();
if (activityCallback.isSynced()) enableAccountsList(true);
}
@Override
public void onPause() {
enableAccountsList(false);
super.onPause();
}
public interface DrawerLocker {
void setDrawerEnabled(boolean enabled);
}
private void enableAccountsList(boolean enable) {
if (activityCallback instanceof DrawerLocker) {
((DrawerLocker) activityCallback).setDrawerEnabled(enable);
}
}
public void setStreetModeBackground(boolean enable) {
//TODO figure out why gunther disappears on return from send although he is still set
if (enable) {
if (streetGunther == null)
streetGunther = ContextCompat.getDrawable(requireContext(), R.drawable.ic_gunther_streetmode);
ivStreetGunther.setImageDrawable(streetGunther);
} else
ivStreetGunther.setImageDrawable(null);
}
}

View file

@ -0,0 +1,77 @@
/*
* Copyright (c) 2017 m2049r et al.
*
* 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.Application;
import android.content.Context;
import android.content.res.Configuration;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.fragment.app.FragmentManager;
import com.m2049r.xmrwallet.model.NetworkType;
import com.m2049r.xmrwallet.util.LocaleHelper;
import com.m2049r.xmrwallet.util.NetCipherHelper;
import com.m2049r.xmrwallet.util.NightmodeHelper;
import timber.log.Timber;
public class XmrWalletApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
FragmentManager.enableNewStateManager(false);
if (BuildConfig.DEBUG) {
Timber.plant(new Timber.DebugTree());
}
NightmodeHelper.setPreferredNightmode(this);
NetCipherHelper.createInstance(this);
}
@Override
protected void attachBaseContext(Context context) {
super.attachBaseContext(LocaleHelper.setPreferredLocale(context));
}
@Override
public void onConfigurationChanged(@NonNull Configuration configuration) {
super.onConfigurationChanged(configuration);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
LocaleHelper.updateSystemDefaultLocale(configuration.getLocales().get(0));
} else {
LocaleHelper.updateSystemDefaultLocale(configuration.locale);
}
LocaleHelper.setPreferredLocale(this);
}
static public NetworkType getNetworkType() {
switch (BuildConfig.FLAVOR_net) {
case "mainnet":
return NetworkType.NetworkType_Mainnet;
case "stagenet":
return NetworkType.NetworkType_Stagenet;
case "devnet": // flavors cannot start with "test"
return NetworkType.NetworkType_Testnet;
default:
throw new IllegalStateException("unknown net flavor " + BuildConfig.FLAVOR_net);
}
}
}

View file

@ -0,0 +1,232 @@
/*
* 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.data;
import android.net.Uri;
import com.m2049r.xmrwallet.util.OpenAliasHelper;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.ToString;
import timber.log.Timber;
@ToString
public class BarcodeData {
public enum Security {
NORMAL,
OA_NO_DNSSEC,
OA_DNSSEC
}
final public Crypto asset;
final public List<Crypto> ambiguousAssets;
final public String address;
final public String addressName;
final public String amount;
final public String description;
final public Security security;
public BarcodeData(List<Crypto> assets, String address) {
if (assets.isEmpty())
throw new IllegalArgumentException("no assets specified");
this.addressName = null;
this.description = null;
this.amount = null;
this.security = Security.NORMAL;
this.address = address;
if (assets.size() == 1) {
this.asset = assets.get(0);
this.ambiguousAssets = null;
} else {
this.asset = null;
this.ambiguousAssets = assets;
}
}
public BarcodeData(Crypto asset, String address, String description, String amount) {
this(asset, address, null, description, amount, Security.NORMAL);
}
public BarcodeData(Crypto asset, String address, String addressName, String description, String amount, Security security) {
this.ambiguousAssets = null;
this.asset = asset;
this.address = address;
this.addressName = addressName;
this.description = description;
this.amount = amount;
this.security = security;
}
public Uri getUri() {
return Uri.parse(getUriString());
}
public String getUriString() {
if (asset != Crypto.XMR) throw new IllegalStateException("We can only do XMR stuff!");
StringBuilder sb = new StringBuilder();
sb.append(Crypto.XMR.getUriScheme())
.append(':')
.append(address);
boolean first = true;
if ((description != null) && !description.isEmpty()) {
sb.append(first ? "?" : "&");
first = false;
sb.append(Crypto.XMR.getUriMessage()).append('=').append(Uri.encode(description));
}
if ((amount != null) && !amount.isEmpty()) {
sb.append(first ? "?" : "&");
sb.append(Crypto.XMR.getUriAmount()).append('=').append(amount);
}
return sb.toString();
}
static private BarcodeData parseNaked(String address) {
List<Crypto> possibleAssets = new ArrayList<>();
for (Crypto crypto : Crypto.values()) {
if (crypto.validate(address)) {
possibleAssets.add(crypto);
}
}
if (possibleAssets.isEmpty())
return null;
return new BarcodeData(possibleAssets, address);
}
static public BarcodeData parseUri(String uriString) {
Timber.d("parseBitUri=%s", uriString);
URI uri;
try {
uri = new URI(uriString);
} catch (URISyntaxException ex) {
return null;
}
if (!uri.isOpaque()) return null;
final String scheme = uri.getScheme();
Crypto crypto = Crypto.withScheme(scheme);
if (crypto == null) return null;
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<>();
if (parts.length == 2) {
String[] args = parts[1].split("&");
for (String arg : args) {
String[] namevalue = arg.split("=");
if (namevalue.length == 0) {
continue;
}
parms.put(Uri.decode(namevalue[0]).toLowerCase(),
namevalue.length > 1 ? Uri.decode(namevalue[1]) : "");
}
}
String addressName = parms.get(crypto.getUriLabel());
String description = parms.get(crypto.getUriMessage());
String address = parts[0]; // no need to decode as there can be no special characters
if (address.isEmpty()) {
Timber.d("no address");
return null;
}
if (!crypto.validate(address)) {
Timber.d("%s address (%s) invalid", crypto, address);
return null;
}
String amount = parms.get(crypto.getUriAmount());
if ((amount != null) && (!amount.isEmpty())) {
try {
Double.parseDouble(amount);
} catch (NumberFormatException ex) {
Timber.d(ex.getLocalizedMessage());
return null; // we have an amount but its not a number!
}
}
return new BarcodeData(crypto, address, addressName, description, amount, Security.NORMAL);
}
static public BarcodeData fromString(String qrCode) {
BarcodeData bcData = parseUri(qrCode);
if (bcData == null) {
// maybe it's naked?
bcData = parseNaked(qrCode);
}
if (bcData == null) {
// check for OpenAlias
bcData = parseOpenAlias(qrCode, false);
}
return bcData;
}
static public BarcodeData parseOpenAlias(String oaString, boolean dnssec) {
Timber.d("parseOpenAlias=%s", oaString);
if (oaString == null) return null;
Map<String, String> oaAttrs = OpenAliasHelper.parse(oaString);
if (oaAttrs == null) return null;
String oaAsset = oaAttrs.get(OpenAliasHelper.OA1_ASSET);
if (oaAsset == null) return null;
String address = oaAttrs.get(OpenAliasHelper.OA1_ADDRESS);
if (address == null) return null;
Crypto crypto = Crypto.withSymbol(oaAsset);
if (crypto == null) {
Timber.i("Unsupported OpenAlias asset %s", oaAsset);
return null;
}
if (!crypto.validate(address)) {
Timber.d("%s address invalid", crypto);
return null;
}
String description = oaAttrs.get(OpenAliasHelper.OA1_DESCRIPTION);
if (description == null) {
description = oaAttrs.get(OpenAliasHelper.OA1_NAME);
}
String amount = oaAttrs.get(OpenAliasHelper.OA1_AMOUNT);
String addressName = oaAttrs.get(OpenAliasHelper.OA1_NAME);
if (amount != null) {
try {
Double.parseDouble(amount);
} catch (NumberFormatException ex) {
Timber.d(ex.getLocalizedMessage());
return null; // we have an amount but its not a number!
}
}
Security sec = dnssec ? BarcodeData.Security.OA_DNSSEC : BarcodeData.Security.OA_NO_DNSSEC;
return new BarcodeData(crypto, address, addressName, description, amount, sec);
}
public boolean isAmbiguous() {
return ambiguousAssets != null;
}
}

View file

@ -0,0 +1,89 @@
package com.m2049r.xmrwallet.data;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.util.validator.BitcoinAddressType;
import com.m2049r.xmrwallet.util.validator.BitcoinAddressValidator;
import com.m2049r.xmrwallet.util.validator.EthAddressValidator;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public enum Crypto {
XMR("XMR", true, "monero:tx_amount:recipient_name:tx_description", R.id.ibXMR, R.drawable.ic_monero, R.drawable.ic_monero_bw, Wallet::isAddressValid),
BTC("BTC", true, "bitcoin:amount:label:message", R.id.ibBTC, R.drawable.ic_xmrto_btc, R.drawable.ic_xmrto_btc_off, address -> {
return BitcoinAddressValidator.validate(address, BitcoinAddressType.BTC);
}),
DASH("DASH", true, "dash:amount:label:message", R.id.ibDASH, R.drawable.ic_xmrto_dash, R.drawable.ic_xmrto_dash_off, address -> {
return BitcoinAddressValidator.validate(address, BitcoinAddressType.DASH);
}),
DOGE("DOGE", true, "dogecoin:amount:label:message", R.id.ibDOGE, R.drawable.ic_xmrto_doge, R.drawable.ic_xmrto_doge_off, address -> {
return BitcoinAddressValidator.validate(address, BitcoinAddressType.DOGE);
}),
ETH("ETH", false, "ethereum:amount:label:message", R.id.ibETH, R.drawable.ic_xmrto_eth, R.drawable.ic_xmrto_eth_off, EthAddressValidator::validate),
LTC("LTC", true, "litecoin:amount:label:message", R.id.ibLTC, R.drawable.ic_xmrto_ltc, R.drawable.ic_xmrto_ltc_off, address -> {
return BitcoinAddressValidator.validate(address, BitcoinAddressType.LTC);
});
@Getter
@NonNull
private final String symbol;
@Getter
private final boolean casefull;
@NonNull
private final String uriSpec;
@Getter
private final int buttonId;
@Getter
private final int iconEnabledId;
@Getter
private final int iconDisabledId;
@NonNull
private final Validator validator;
@Nullable
public static Crypto withScheme(@NonNull String scheme) {
for (Crypto crypto : values()) {
if (crypto.getUriScheme().equals(scheme)) return crypto;
}
return null;
}
@Nullable
public static Crypto withSymbol(@NonNull String symbol) {
final String upperSymbol = symbol.toUpperCase();
for (Crypto crypto : values()) {
if (crypto.symbol.equals(upperSymbol)) return crypto;
}
return null;
}
interface Validator {
boolean validate(String address);
}
// TODO maybe cache these segments
String getUriScheme() {
return uriSpec.split(":")[0];
}
String getUriAmount() {
return uriSpec.split(":")[1];
}
String getUriLabel() {
return uriSpec.split(":")[2];
}
String getUriMessage() {
return uriSpec.split(":")[3];
}
boolean validate(String address) {
return validator.validate(address);
}
}

View file

@ -0,0 +1,39 @@
/*
* Copyright (c) 2020 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 lombok.AllArgsConstructor;
import lombok.Getter;
// Nodes stolen from https://moneroworld.com/#nodes
@AllArgsConstructor
public enum DefaultNodes {
MONERUJO("nodex.monerujo.io:18081"),
XMRTO("node.xmr.to:18081"),
SUPPORTXMR("node.supportxmr.com:18081"),
HASHVAULT("nodes.hashvault.pro:18081"),
MONEROWORLD("node.moneroworld.com:18089"),
XMRTW("opennode.xmr-tw.org:18089"),
MONERUJO_ONION("monerujods7mbghwe6cobdr6ujih6c22zu5rl7zshmizz2udf7v7fsad.onion:18081/mainnet/monerujo.onion"),
Criminales78("56wl7y2ebhamkkiza4b7il4mrzwtyvpdym7bm2bkg3jrei2je646k3qd.onion:18089/mainnet/Criminales78.onion"),
xmrfail("mxcd4577fldb3ppzy7obmmhnu3tf57gbcbd4qhwr2kxyjj2qi3dnbfqd.onion:18081/mainnet/xmrfail.onion"),
boldsuck("6dsdenp6vjkvqzy4wzsnzn6wixkdzihx3khiumyzieauxuxslmcaeiad.onion:18081/mainnet/boldsuck.onion");
@Getter
private final String uri;
}

View file

@ -0,0 +1,371 @@
/*
* 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 com.m2049r.xmrwallet.util.OnionHelper;
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 lombok.Getter;
import lombok.Setter;
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";
static class Address {
final private InetAddress inet;
final private String onion;
public boolean isOnion() {
return onion != null;
}
public String getHostName() {
if (inet != null) {
return inet.getHostName();
} else {
return onion;
}
}
public String getHostAddress() {
if (inet != null) {
return inet.getHostAddress();
} else {
return onion;
}
}
private Address(InetAddress address, String onion) {
this.inet = address;
this.onion = onion;
}
static Address of(InetAddress address) {
return new Address(address, null);
}
static Address of(String host) throws UnknownHostException {
if (OnionHelper.isOnionHost(host)) {
return new Address(null, host);
} else {
return new Address(InetAddress.getByName(host), null);
}
}
@Override
public int hashCode() {
return getHostAddress().hashCode();
}
@Override
public boolean equals(Object other) {
return (other instanceof Address) && (getHostAddress().equals(((Address) other).getHostAddress()));
}
}
@Getter
private String name = null;
@Getter
final private NetworkType networkType;
Address hostAddress;
@Getter
private String host;
@Getter
@Setter
int rpcPort = 0;
private int levinPort = 0;
@Getter
@Setter
private String username = "";
@Getter
@Setter
private String password = "";
@Getter
@Setter
private boolean favourite = false;
@Getter
@Setter
private boolean selected = false;
@Override
public int hashCode() {
return hostAddress.hashCode();
}
// Nodes are equal if they are the same host address:port & 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)
&& (rpcPort == anotherNode.rpcPort)
&& (networkType == anotherNode.networkType));
}
public boolean isOnion() {
return hostAddress.isOnion();
}
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 = Address.of(socketAddress.getAddress());
this.host = socketAddress.getHostString();
this.rpcPort = 0; // unknown
this.levinPort = socketAddress.getPort();
this.username = "";
this.password = "";
}
public String getAddress() {
return getHostAddress() + ":" + rpcPort;
}
public String getHostAddress() {
return hostAddress.getHostAddress();
}
public void setHost(String host) throws UnknownHostException {
if ((host == null) || (host.isEmpty()))
throw new UnknownHostException("loopback not supported (yet?)");
this.host = host;
this.hostAddress = Address.of(host);
}
public void setDefaultName() {
if (name != null) return;
String nodeName = hostAddress.getHostName();
if (hostAddress.isOnion()) {
nodeName = nodeName.substring(0, nodeName.length() - ".onion".length());
if (nodeName.length() > 16) {
nodeName = nodeName.substring(0, 8) + "" + nodeName.substring(nodeName.length() - 6);
}
nodeName = nodeName + ".onion";
}
this.name = nodeName;
}
public void setName(String name) {
if ((name == null) || (name.isEmpty()))
setDefaultName();
else
this.name = name;
}
public void toggleFavourite() {
favourite = !favourite;
}
public 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;
}
}

View file

@ -0,0 +1,303 @@
/*
* 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 android.content.Context;
import android.text.Html;
import android.text.Spanned;
import android.widget.TextView;
import androidx.core.content.ContextCompat;
import com.m2049r.levin.scanner.LevinPeer;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.util.NetCipherHelper;
import com.m2049r.xmrwallet.util.NetCipherHelper.Request;
import com.m2049r.xmrwallet.util.NodePinger;
import com.m2049r.xmrwallet.util.ThemeHelper;
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.Calendar;
import java.util.Comparator;
import lombok.Getter;
import lombok.Setter;
import okhttp3.HttpUrl;
import okhttp3.Response;
import okhttp3.ResponseBody;
import timber.log.Timber;
public class NodeInfo extends Node {
final static public int MIN_MAJOR_VERSION = 14;
final static public String RPC_VERSION = "2.0";
@Getter
private long height = 0;
@Getter
private long timestamp = 0;
@Getter
private int majorVersion = 0;
@Getter
private double responseTime = Double.MAX_VALUE;
@Getter
private int responseCode = 0;
@Getter
private boolean tested = false;
@Getter
@Setter
private boolean selecting = false;
public void clear() {
height = 0;
majorVersion = 0;
responseTime = Double.MAX_VALUE;
responseCode = 0;
timestamp = 0;
tested = false;
}
static public NodeInfo fromString(String nodeString) {
try {
return new NodeInfo(nodeString);
} catch (IllegalArgumentException 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.getHostAddress(), 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(LevinPeer levinPeer) {
super(levinPeer.getSocketAddress());
}
public NodeInfo(InetSocketAddress address) {
super(address);
}
public NodeInfo() {
super();
}
public boolean isSuccessful() {
return (responseCode >= 200) && (responseCode < 300);
}
public boolean isUnauthorized() {
return responseCode == HttpURLConnection.HTTP_UNAUTHORIZED;
}
public boolean isValid() {
return isSuccessful() && (majorVersion >= MIN_MAJOR_VERSION) && (responseTime < Double.MAX_VALUE);
}
static public Comparator<NodeInfo> BestNodeComparator = (o1, o2) -> {
if (o1.isValid()) {
if (o2.isValid()) { // both are valid
// higher node wins
int heightDiff = (int) (o2.height - o1.height);
if (heightDiff != 0)
return heightDiff;
// if they are 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 = 1000; //ms
public static final double PING_GOOD = HTTP_TIMEOUT / 3.0; //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);
}
public boolean testRpcService(NodePinger.Listener listener) {
boolean result = testRpcService(rpcPort);
if (listener != null)
listener.publish(this);
return result;
}
private Request rpcServiceRequest(int port) {
final HttpUrl url = new HttpUrl.Builder()
.scheme("http")
.host(getHost())
.port(port)
.addPathSegment("json_rpc")
.build();
final String json = "{\"jsonrpc\":\"2.0\",\"id\":\"0\",\"method\":\"getlastblockheader\"}";
return new Request(url, json, getUsername(), getPassword());
}
private boolean testRpcService(int port) {
Timber.d("Testing %s", toNodeString());
clear();
if (hostAddress.isOnion() && !NetCipherHelper.isTor()) {
tested = true; // sortof
responseCode = 418; // I'm a teapot - or I need an Onion - who knows
return false; // autofail
}
try {
long ta = System.nanoTime();
try (Response response = rpcServiceRequest(port).execute()) {
Timber.d("%s: %s", response.code(), response.request().url());
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() < 2000)) { // sanity check
final JSONObject json = new JSONObject(respBody.string());
String rpcVersion = json.getString("jsonrpc");
if (!RPC_VERSION.equals(rpcVersion))
return false;
final JSONObject result = json.getJSONObject("result");
if (!result.has("credits")) // introduced in monero v0.15.0
return false;
final JSONObject header = result.getJSONObject("block_header");
height = header.getLong("height");
timestamp = header.getLong("timestamp");
majorVersion = header.getInt("major_version");
return true; // success
}
}
}
} catch (IOException | JSONException ex) {
Timber.d("EX: %s", ex.getMessage()); //TODO: do something here (show error?)
} finally {
tested = true;
}
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;
}
static public final int STALE_NODE_HOURS = 2;
public void showInfo(TextView view, String info, boolean isError) {
final Context ctx = view.getContext();
final Spanned text = Html.fromHtml(ctx.getString(R.string.status,
Integer.toHexString(ThemeHelper.getThemedColor(ctx, R.attr.positiveColor) & 0xFFFFFF),
Integer.toHexString(ThemeHelper.getThemedColor(ctx, android.R.attr.colorBackground) & 0xFFFFFF),
(hostAddress.isOnion() ? "&nbsp;.onion&nbsp;&nbsp;" : ""), " " + info));
view.setText(text);
if (isError)
view.setTextColor(ThemeHelper.getThemedColor(ctx, R.attr.colorError));
else
view.setTextColor(ThemeHelper.getThemedColor(ctx, android.R.attr.textColorSecondary));
}
public void showInfo(TextView view) {
if (!isTested()) {
showInfo(view, "", false);
return;
}
final Context ctx = view.getContext();
final long now = Calendar.getInstance().getTimeInMillis() / 1000;
final long secs = (now - timestamp);
final long mins = secs / 60;
final long hours = mins / 60;
final long days = hours / 24;
String info;
if (mins < 2) {
info = ctx.getString(R.string.node_updated_now, secs);
} else if (hours < 2) {
info = ctx.getString(R.string.node_updated_mins, mins);
} else if (days < 2) {
info = ctx.getString(R.string.node_updated_hours, hours);
} else {
info = ctx.getString(R.string.node_updated_days, days);
}
showInfo(view, info, hours >= STALE_NODE_HOURS);
}
}

View file

@ -0,0 +1,39 @@
/*
* 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.data;
import com.m2049r.xmrwallet.model.PendingTransaction;
public class PendingTx {
final public PendingTransaction.Status status;
final public String error;
final public long amount;
final public long dust;
final public long fee;
final public String txId;
final public long txCount;
public PendingTx(PendingTransaction pendingTransaction) {
status = pendingTransaction.getStatus();
error = pendingTransaction.getErrorString();
amount = pendingTransaction.getAmount();
dust = pendingTransaction.getDust();
fee = pendingTransaction.getFee();
txId = pendingTransaction.getFirstTxId();
txCount = pendingTransaction.getTxCount();
}
}

View file

@ -0,0 +1,63 @@
/*
* 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 java.util.regex.Pattern;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@RequiredArgsConstructor
@ToString
@EqualsAndHashCode
public class Subaddress implements Comparable<Subaddress> {
@Getter
final private int accountIndex;
@Getter
final private int addressIndex;
@Getter
final private String address;
@Getter
private final String label;
@Getter
@Setter
private long amount;
@Override
public int compareTo(Subaddress another) { // newer is <
final int compareAccountIndex = another.accountIndex - accountIndex;
if (compareAccountIndex == 0)
return another.addressIndex - addressIndex;
return compareAccountIndex;
}
public String getSquashedAddress() {
return address.substring(0, 8) + "" + address.substring(address.length() - 8);
}
public static final Pattern DEFAULT_LABEL_FORMATTER = Pattern.compile("^[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}:[0-9]{2}:[0-9]{2}$");
public String getDisplayLabel() {
if (label.isEmpty() || (DEFAULT_LABEL_FORMATTER.matcher(label).matches()))
return ("#" + addressIndex);
else
return label;
}
}

View file

@ -0,0 +1,149 @@
/*
* 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.data;
import android.os.Parcel;
import android.os.Parcelable;
import com.m2049r.xmrwallet.model.PendingTransaction;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.util.Helper;
// https://stackoverflow.com/questions/2139134/how-to-send-an-object-from-one-android-activity-to-another-using-intents
public class TxData implements Parcelable {
public TxData() {
}
public TxData(TxData txData) {
this.dstAddr = txData.dstAddr;
this.amount = txData.amount;
this.mixin = txData.mixin;
this.priority = txData.priority;
}
public TxData(String dstAddr,
long amount,
int mixin,
PendingTransaction.Priority priority) {
this.dstAddr = dstAddr;
this.amount = amount;
this.mixin = mixin;
this.priority = priority;
}
public String getDestinationAddress() {
return dstAddr;
}
public long getAmount() {
return amount;
}
public double getAmountAsDouble() {
return 1.0 * amount / Helper.ONE_XMR;
}
public int getMixin() {
return mixin;
}
public PendingTransaction.Priority getPriority() {
return priority;
}
public void setDestinationAddress(String dstAddr) {
this.dstAddr = dstAddr;
}
public void setAmount(long amount) {
this.amount = amount;
}
public void setAmount(double amount) {
this.amount = Wallet.getAmountFromDouble(amount);
}
public void setMixin(int mixin) {
this.mixin = mixin;
}
public void setPriority(PendingTransaction.Priority priority) {
this.priority = priority;
}
public UserNotes getUserNotes() {
return userNotes;
}
public void setUserNotes(UserNotes userNotes) {
this.userNotes = userNotes;
}
private String dstAddr;
private long amount;
private int mixin;
private PendingTransaction.Priority priority;
private UserNotes userNotes;
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeString(dstAddr);
out.writeLong(amount);
out.writeInt(mixin);
out.writeInt(priority.getValue());
}
// this is used to regenerate your object. All Parcelables must have a CREATOR that implements these two methods
public static final Parcelable.Creator<TxData> CREATOR = new Parcelable.Creator<TxData>() {
public TxData createFromParcel(Parcel in) {
return new TxData(in);
}
public TxData[] newArray(int size) {
return new TxData[size];
}
};
protected TxData(Parcel in) {
dstAddr = in.readString();
amount = in.readLong();
mixin = in.readInt();
priority = PendingTransaction.Priority.fromInteger(in.readInt());
}
@Override
public int describeContents() {
return 0;
}
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append("dstAddr:");
sb.append(dstAddr);
sb.append(",amount:");
sb.append(amount);
sb.append(",mixin:");
sb.append(mixin);
sb.append(",priority:");
sb.append(priority);
return sb.toString();
}
}

View file

@ -0,0 +1,101 @@
/*
* 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.data;
import android.os.Parcel;
import androidx.annotation.NonNull;
import lombok.Getter;
import lombok.Setter;
public class TxDataBtc extends TxData {
@Getter
@Setter
private String btcSymbol; // the actual non-XMR thing we're sending
@Getter
@Setter
private String xmrtoOrderId; // shown in success screen
@Getter
@Setter
private String btcAddress;
@Getter
@Setter
private double btcAmount;
public TxDataBtc() {
super();
}
public TxDataBtc(TxDataBtc txDataBtc) {
super(txDataBtc);
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeString(btcSymbol);
out.writeString(xmrtoOrderId);
out.writeString(btcAddress);
out.writeDouble(btcAmount);
}
// this is used to regenerate your object. All Parcelables must have a CREATOR that implements these two methods
public static final Creator<TxDataBtc> CREATOR = new Creator<TxDataBtc>() {
public TxDataBtc createFromParcel(Parcel in) {
return new TxDataBtc(in);
}
public TxDataBtc[] newArray(int size) {
return new TxDataBtc[size];
}
};
protected TxDataBtc(Parcel in) {
super(in);
btcSymbol = in.readString();
xmrtoOrderId = in.readString();
btcAddress = in.readString();
btcAmount = in.readDouble();
}
@NonNull
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("xmrtoOrderId:");
sb.append(xmrtoOrderId);
sb.append(",btcSymbol:");
sb.append(btcSymbol);
sb.append(",btcAddress:");
sb.append(btcAddress);
sb.append(",btcAmount:");
sb.append(btcAmount);
return sb.toString();
}
public boolean validateAddress(@NonNull String address) {
if ((btcSymbol == null) || (btcAddress == null)) return false;
final Crypto crypto = Crypto.withSymbol(btcSymbol);
if (crypto == null) return false;
if (crypto.isCasefull()) { // compare as-is
return address.equals(btcAddress);
} else { // normalize & compare (e.g. ETH with and without checksum capitals
return address.toLowerCase().equals(btcAddress.toLowerCase());
}
}
}

View file

@ -0,0 +1,99 @@
/*
* 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.data;
import com.m2049r.xmrwallet.service.shift.sideshift.api.CreateOrder;
import com.m2049r.xmrwallet.util.Helper;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class UserNotes {
public String txNotes = "";
public String note = "";
public String xmrtoTag = null;
public String xmrtoKey = null;
public String xmrtoAmount = null; // could be a double - but we are not doing any calculations
public String xmrtoCurrency = null;
public String xmrtoDestination = null;
public UserNotes(final String txNotes) {
if (txNotes == null) {
return;
}
this.txNotes = txNotes;
Pattern p = Pattern.compile("^\\{([a-z]+)-(\\w{6,}),([0-9.]*)([A-Z]+),(\\w*)\\} ?(.*)");
Matcher m = p.matcher(txNotes);
if (m.find()) {
xmrtoTag = m.group(1);
xmrtoKey = m.group(2);
xmrtoAmount = m.group(3);
xmrtoCurrency = m.group(4);
xmrtoDestination = m.group(5);
note = m.group(6);
} else {
note = txNotes;
}
}
public void setNote(String newNote) {
if (newNote != null) {
note = newNote;
} else {
note = "";
}
txNotes = buildTxNote();
}
public void setXmrtoOrder(CreateOrder order) {
if (order != null) {
xmrtoTag = order.TAG;
xmrtoKey = order.getOrderId();
xmrtoAmount = Helper.getDisplayAmount(order.getBtcAmount());
xmrtoCurrency = order.getBtcCurrency();
xmrtoDestination = order.getBtcAddress();
} else {
xmrtoTag = null;
xmrtoKey = null;
xmrtoAmount = null;
xmrtoDestination = null;
}
txNotes = buildTxNote();
}
private String buildTxNote() {
StringBuilder sb = new StringBuilder();
if (xmrtoKey != null) {
if ((xmrtoAmount == null) || (xmrtoDestination == null))
throw new IllegalArgumentException("Broken notes");
sb.append("{");
sb.append(xmrtoTag);
sb.append("-");
sb.append(xmrtoKey);
sb.append(",");
sb.append(xmrtoAmount);
sb.append(xmrtoCurrency);
sb.append(",");
sb.append(xmrtoDestination);
sb.append("}");
if ((note != null) && (!note.isEmpty()))
sb.append(" ");
}
sb.append(note);
return sb.toString();
}
}

View file

@ -0,0 +1,90 @@
/*
* 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.dialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import android.text.Html;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.m2049r.xmrwallet.BuildConfig;
import com.m2049r.xmrwallet.R;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import timber.log.Timber;
public class AboutFragment extends DialogFragment {
static final String TAG = "AboutFragment";
public static AboutFragment newInstance() {
return new AboutFragment();
}
public static void display(FragmentManager fm) {
FragmentTransaction ft = fm.beginTransaction();
Fragment prev = fm.findFragmentByTag(TAG);
if (prev != null) {
ft.remove(prev);
}
AboutFragment.newInstance().show(ft, TAG);
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_about, null);
((TextView) view.findViewById(R.id.tvHelp)).setText(Html.fromHtml(getLicencesHtml()));
((TextView) view.findViewById(R.id.tvVersion)).setText(getString(R.string.about_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE));
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity())
.setView(view)
.setNegativeButton(R.string.about_close,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
dialog.dismiss();
}
});
return builder.create();
}
private String getLicencesHtml() {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(getContext().getAssets().open("licenses.html"), StandardCharsets.UTF_8))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null)
sb.append(line);
return sb.toString();
} catch (IOException ex) {
Timber.e(ex);
return ex.getLocalizedMessage();
}
}
}

View file

@ -0,0 +1,69 @@
/*
* 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.dialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.text.Html;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.m2049r.xmrwallet.R;
public class CreditsFragment extends DialogFragment {
static final String TAG = "DonationFragment";
public static CreditsFragment newInstance() {
return new CreditsFragment();
}
public static void display(FragmentManager fm) {
FragmentTransaction ft = fm.beginTransaction();
Fragment prev = fm.findFragmentByTag(TAG);
if (prev != null) {
ft.remove(prev);
}
CreditsFragment.newInstance().show(ft, TAG);
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_credits, null);
((TextView) view.findViewById(R.id.tvCredits)).setText(Html.fromHtml(getString(R.string.credits_text)));
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity())
.setView(view)
.setNegativeButton(R.string.about_close,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
dialog.dismiss();
}
});
return builder.create();
}
}

View file

@ -0,0 +1,115 @@
/*
* Copyright (c) 2017 m2049r
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.m2049r.xmrwallet.dialog;
import android.app.Dialog;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.text.Html;
import android.text.Spanned;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.util.NetCipherHelper;
public class HelpFragment extends DialogFragment {
static final String TAG = "HelpFragment";
private static final String HELP_ID = "HELP_ID";
private static final String TOR_BUTTON = "TOR";
public static HelpFragment newInstance(int helpResourceId) {
HelpFragment fragment = new HelpFragment();
Bundle bundle = new Bundle();
bundle.putInt(HELP_ID, helpResourceId);
// a hack for the tor button
if (helpResourceId == R.string.help_tor)
bundle.putInt(TOR_BUTTON, 7);
fragment.setArguments(bundle);
return fragment;
}
public static void display(FragmentManager fm, int helpResourceId) {
FragmentTransaction ft = fm.beginTransaction();
Fragment prev = fm.findFragmentByTag(TAG);
if (prev != null) {
ft.remove(prev);
}
HelpFragment.newInstance(helpResourceId).show(ft, TAG);
}
private Spanned getHtml(String html, double textSize) {
final Html.ImageGetter imageGetter = source -> {
final int imageId = getResources().getIdentifier(source.replace("/", ""), "drawable", requireActivity().getPackageName());
// Don't die if we don't find the image - use a heart instead
final Drawable drawable = ContextCompat.getDrawable(requireActivity(), imageId > 0 ? imageId : R.drawable.ic_favorite_24dp);
final double f = textSize / drawable.getIntrinsicHeight();
drawable.setBounds(0, 0, (int) (f * drawable.getIntrinsicWidth()), (int) textSize);
return drawable;
};
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY, imageGetter, null);
} else {
return Html.fromHtml(html, imageGetter, null);
}
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_help, null);
int helpId = 0;
boolean torButton = false;
Bundle arguments = getArguments();
if (arguments != null) {
helpId = arguments.getInt(HELP_ID);
torButton = arguments.getInt(TOR_BUTTON) > 0;
}
final TextView helpTv = view.findViewById(R.id.tvHelp);
if (helpId > 0)
helpTv.setText(getHtml(getString(helpId), helpTv.getTextSize()));
MaterialAlertDialogBuilder builder =
new MaterialAlertDialogBuilder(requireActivity())
.setView(view);
if (torButton) {
builder.setNegativeButton(R.string.help_nok,
(dialog, id) -> dialog.dismiss())
.setPositiveButton(R.string.help_getorbot,
(dialog, id) -> {
dialog.dismiss();
NetCipherHelper.getInstance().installOrbot(requireActivity());
});
} else {
builder.setNegativeButton(R.string.help_ok,
(dialog, id) -> dialog.dismiss());
}
return builder.create();
}
}

View file

@ -0,0 +1,69 @@
/*
* 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.dialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.text.Html;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.m2049r.xmrwallet.R;
public class PrivacyFragment extends DialogFragment {
static final String TAG = "PrivacyFragment";
public static PrivacyFragment newInstance() {
return new PrivacyFragment();
}
public static void display(FragmentManager fm) {
FragmentTransaction ft = fm.beginTransaction();
Fragment prev = fm.findFragmentByTag(TAG);
if (prev != null) {
ft.remove(prev);
}
PrivacyFragment.newInstance().show(ft, TAG);
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_privacy_policy, null);
((TextView) view.findViewById(R.id.tvCredits)).setText(Html.fromHtml(getString(R.string.privacy_policy)));
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity())
.setView(view)
.setNegativeButton(R.string.about_close,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
dialog.dismiss();
}
});
return builder.create();
}
}

View file

@ -0,0 +1,132 @@
package com.m2049r.xmrwallet.dialog;
/*
* Copyright (C) 2007 The Android Open Source Project
* 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.
*/
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.util.Helper;
import java.util.Locale;
import timber.log.Timber;
public class ProgressDialog extends AlertDialog {
private ProgressBar pbBar;
private TextView tvMessage;
private TextView tvProgress;
private View rlProgressBar, pbCircle;
static private final String PROGRESS_FORMAT = "%1d/%2d";
private CharSequence message;
private int maxValue, progressValue;
private boolean indeterminate = true;
public ProgressDialog(Context context) {
super(context);
}
@Override
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 = view.findViewById(R.id.tvMessage);
rlProgressBar = view.findViewById(R.id.rlProgressBar);
pbBar = view.findViewById(R.id.pbBar);
tvProgress = view.findViewById(R.id.tvProgress);
setView(view);
setIndeterminate(indeterminate);
if (maxValue > 0) {
setMax(maxValue);
}
if (progressValue > 0) {
setProgress(progressValue);
}
if (message != null) {
Timber.d("msg=%s", message);
setMessage(message);
}
super.onCreate(savedInstanceState);
if (Helper.preventScreenshot()) {
getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
}
}
public void setProgress(int value, int max) {
progressValue = value;
maxValue = max;
if (pbBar != null) {
pbBar.setProgress(value);
pbBar.setMax(max);
tvProgress.setText(String.format(Locale.getDefault(), PROGRESS_FORMAT, value, maxValue));
}
}
public void setProgress(int value) {
progressValue = value;
if (pbBar != null) {
pbBar.setProgress(value);
tvProgress.setText(String.format(Locale.getDefault(), PROGRESS_FORMAT, value, maxValue));
}
}
public void setMax(int max) {
maxValue = max;
if (pbBar != null) {
pbBar.setMax(max);
}
}
public void setIndeterminate(boolean indeterminate) {
if (this.indeterminate != indeterminate) {
if (rlProgressBar != null) {
if (indeterminate) {
pbCircle.setVisibility(View.VISIBLE);
rlProgressBar.setVisibility(View.GONE);
} else {
pbCircle.setVisibility(View.GONE);
rlProgressBar.setVisibility(View.VISIBLE);
}
}
this.indeterminate = indeterminate;
}
}
@Override
public void setMessage(CharSequence message) {
this.message = message;
if (tvMessage != null) {
tvMessage.setText(message);
}
}
}

View file

@ -0,0 +1,525 @@
/*
* 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.fragment.send;
import android.content.Context;
import android.nfc.NfcManager;
import android.os.Bundle;
import android.text.Editable;
import android.text.Html;
import android.text.InputType;
import android.text.Spanned;
import android.text.TextWatcher;
import android.util.Patterns;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import com.google.android.material.textfield.TextInputLayout;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.BarcodeData;
import com.m2049r.xmrwallet.data.Crypto;
import com.m2049r.xmrwallet.data.TxData;
import com.m2049r.xmrwallet.data.TxDataBtc;
import com.m2049r.xmrwallet.data.UserNotes;
import com.m2049r.xmrwallet.model.PendingTransaction;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.OpenAliasHelper;
import com.m2049r.xmrwallet.util.ServiceHelper;
import com.m2049r.xmrwallet.util.validator.BitcoinAddressType;
import com.m2049r.xmrwallet.util.validator.BitcoinAddressValidator;
import com.m2049r.xmrwallet.util.validator.EthAddressValidator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import timber.log.Timber;
public class SendAddressWizardFragment extends SendWizardFragment {
static final int INTEGRATED_ADDRESS_LENGTH = 106;
public static SendAddressWizardFragment newInstance(Listener listener) {
SendAddressWizardFragment instance = new SendAddressWizardFragment();
instance.setSendListener(listener);
return instance;
}
Listener sendListener;
public void setSendListener(Listener listener) {
this.sendListener = listener;
}
public interface Listener {
void setBarcodeData(BarcodeData data);
BarcodeData getBarcodeData();
BarcodeData popBarcodeData();
void setMode(SendFragment.Mode mode);
TxData getTxData();
}
private EditText etDummy;
private TextInputLayout etAddress;
private TextInputLayout etNotes;
private TextView tvXmrTo;
private TextView tvTor;
private Map<Crypto, ImageButton> ibCrypto;
final private Set<Crypto> possibleCryptos = new HashSet<>();
private Crypto selectedCrypto = null;
private boolean resolvingOA = false;
OnScanListener onScanListener;
public interface OnScanListener {
void onScan();
}
private Crypto getCryptoForButton(ImageButton button) {
for (Map.Entry<Crypto, ImageButton> entry : ibCrypto.entrySet()) {
if (entry.getValue() == button) return entry.getKey();
}
return null;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Timber.d("onCreateView() %s", (String.valueOf(savedInstanceState)));
View view = inflater.inflate(R.layout.fragment_send_address, container, false);
tvTor = view.findViewById(R.id.tvTor);
tvXmrTo = view.findViewById(R.id.tvXmrTo);
ibCrypto = new HashMap<>();
for (Crypto crypto : Crypto.values()) {
final ImageButton button = view.findViewById(crypto.getButtonId());
if (Helper.ALLOW_SHIFT || (crypto == Crypto.XMR)) {
ibCrypto.put(crypto, button);
button.setOnClickListener(v -> {
if (possibleCryptos.contains(crypto)) {
selectedCrypto = crypto;
updateCryptoButtons(false);
} else {
// show help what to do:
if (button.getId() != R.id.ibXMR) {
final String name = getResources().getStringArray(R.array.cryptos)[crypto.ordinal()];
final String symbol = getCryptoForButton(button).getSymbol();
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto_help, name, symbol)));
tvXmrTo.setVisibility(View.VISIBLE);
} else {
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto_help_xmr)));
tvXmrTo.setVisibility(View.VISIBLE);
tvTor.setVisibility(View.INVISIBLE);
}
}
});
} else {
button.setImageResource(crypto.getIconDisabledId());
button.setImageAlpha(128);
button.setEnabled(false);
}
}
if (!Helper.ALLOW_SHIFT) {
tvTor.setVisibility(View.VISIBLE);
}
updateCryptoButtons(true);
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) {
// ignore ENTER
return ((event != null) && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER));
}
});
etAddress.getEditText().setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
String enteredAddress = etAddress.getEditText().getText().toString().trim();
String dnsOA = dnsFromOpenAlias(enteredAddress);
Timber.d("OpenAlias is %s", dnsOA);
if (dnsOA != null) {
processOpenAlias(dnsOA);
}
}
});
etAddress.getEditText().addTextChangedListener(new TextWatcher() {
@Override
public void afterTextChanged(Editable editable) {
Timber.d("AFTER: %s", editable.toString());
etAddress.setError(null);
possibleCryptos.clear();
selectedCrypto = null;
final String address = etAddress.getEditText().getText().toString();
if (isIntegratedAddress(address)) {
Timber.d("isIntegratedAddress");
possibleCryptos.add(Crypto.XMR);
selectedCrypto = Crypto.XMR;
etAddress.setError(getString(R.string.info_paymentid_integrated));
sendListener.setMode(SendFragment.Mode.XMR);
} else if (isStandardAddress(address)) {
Timber.d("isStandardAddress");
possibleCryptos.add(Crypto.XMR);
selectedCrypto = Crypto.XMR;
sendListener.setMode(SendFragment.Mode.XMR);
}
if (!Helper.ALLOW_SHIFT) return;
if ((selectedCrypto == null) && isEthAddress(address)) {
Timber.d("isEthAddress");
possibleCryptos.add(Crypto.ETH);
selectedCrypto = Crypto.ETH;
tvXmrTo.setVisibility(View.VISIBLE);
sendListener.setMode(SendFragment.Mode.BTC);
}
if (possibleCryptos.isEmpty()) {
Timber.d("isBitcoinAddress");
for (BitcoinAddressType type : BitcoinAddressType.values()) {
if (BitcoinAddressValidator.validate(address, type)) {
possibleCryptos.add(Crypto.valueOf(type.name()));
}
}
if (!possibleCryptos.isEmpty()) // found something in need of shifting!
sendListener.setMode(SendFragment.Mode.BTC);
if (possibleCryptos.size() == 1) {
selectedCrypto = (Crypto) possibleCryptos.toArray()[0];
}
}
if (possibleCryptos.isEmpty()) {
Timber.d("other");
tvXmrTo.setVisibility(View.INVISIBLE);
sendListener.setMode(SendFragment.Mode.XMR);
}
updateCryptoButtons(address.isEmpty());
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
});
final ImageButton bPasteAddress = view.findViewById(R.id.bPasteAddress);
bPasteAddress.setOnClickListener(v -> {
final String clip = Helper.getClipBoardText(getActivity());
if (clip == null) return;
// clean it up
final String address = clip.replaceAll("( +)|(\\r?\\n?)", "");
BarcodeData bc = BarcodeData.fromString(address);
if (bc != null) {
processScannedData(bc);
final EditText et = etAddress.getEditText();
et.setSelection(et.getText().length());
etAddress.requestFocus();
} else {
Toast.makeText(getActivity(), getString(R.string.send_address_invalid), Toast.LENGTH_SHORT).show();
}
});
etNotes = view.findViewById(R.id.etNotes);
etNotes.getEditText().setRawInputType(InputType.TYPE_CLASS_TEXT);
etNotes.getEditText().
setOnEditorActionListener((v, actionId, event) -> {
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|| (actionId == EditorInfo.IME_ACTION_DONE)) {
etDummy.requestFocus();
return true;
}
return false;
});
final View cvScan = view.findViewById(R.id.bScan);
cvScan.setOnClickListener(v -> onScanListener.onScan());
etDummy = view.findViewById(R.id.etDummy);
etDummy.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
etDummy.requestFocus();
View tvNfc = view.findViewById(R.id.tvNfc);
NfcManager manager = (NfcManager) getContext().getSystemService(Context.NFC_SERVICE);
if ((manager != null) && (manager.getDefaultAdapter() != null))
tvNfc.setVisibility(View.VISIBLE);
return view;
}
private void selectedCrypto(Crypto crypto) {
final ImageButton button = ibCrypto.get(crypto);
button.setImageResource(crypto.getIconEnabledId());
button.setImageAlpha(255);
button.setEnabled(true);
}
private void possibleCrypto(Crypto crypto) {
final ImageButton button = ibCrypto.get(crypto);
button.setImageResource(crypto.getIconDisabledId());
button.setImageAlpha(255);
button.setEnabled(true);
}
private void impossibleCrypto(Crypto crypto) {
final ImageButton button = ibCrypto.get(crypto);
button.setImageResource(crypto.getIconDisabledId());
button.setImageAlpha(128);
button.setEnabled(true);
}
private void updateCryptoButtons(boolean noAddress) {
if (!Helper.ALLOW_SHIFT) return;
for (Crypto crypto : Crypto.values()) {
if (crypto == selectedCrypto) {
selectedCrypto(crypto);
} else if (possibleCryptos.contains(crypto)) {
possibleCrypto(crypto);
} else {
impossibleCrypto(crypto);
}
}
if ((selectedCrypto != null) && (selectedCrypto != Crypto.XMR)) {
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto, selectedCrypto.getSymbol())));
tvXmrTo.setVisibility(View.VISIBLE);
} else if ((selectedCrypto == null) && (possibleCryptos.size() > 1)) {
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto_ambiguous)));
tvXmrTo.setVisibility(View.VISIBLE);
} else {
tvXmrTo.setVisibility(View.INVISIBLE);
}
if (noAddress) {
selectedCrypto(Crypto.XMR);
}
}
private void processOpenAlias(String dnsOA) {
if (resolvingOA) return; // already resolving - just wait
sendListener.popBarcodeData();
if (dnsOA != null) {
resolvingOA = true;
etAddress.setError(getString(R.string.send_address_resolve_openalias));
OpenAliasHelper.resolve(dnsOA, new OpenAliasHelper.OnResolvedListener() {
@Override
public void onResolved(Map<Crypto, BarcodeData> dataMap) {
resolvingOA = false;
BarcodeData barcodeData = dataMap.get(Crypto.XMR);
if (barcodeData == null) barcodeData = dataMap.get(Crypto.BTC);
if (barcodeData != null) {
Timber.d("Security=%s, %s", barcodeData.security.toString(), barcodeData.address);
processScannedData(barcodeData);
} else {
etAddress.setError(getString(R.string.send_address_not_openalias));
Timber.d("NO XMR OPENALIAS TXT FOUND");
}
}
@Override
public void onFailure() {
resolvingOA = false;
etAddress.setError(getString(R.string.send_address_not_openalias));
Timber.e("OA FAILED");
}
});
} // else ignore
}
private boolean checkAddressNoError() {
return selectedCrypto != null;
}
private boolean checkAddress() {
boolean ok = checkAddressNoError();
if (possibleCryptos.isEmpty()) {
etAddress.setError(getString(R.string.send_address_invalid));
} else {
etAddress.setError(null);
}
return ok;
}
private boolean isStandardAddress(String address) {
return Wallet.isAddressValid(address);
}
private boolean isIntegratedAddress(String address) {
return (address.length() == INTEGRATED_ADDRESS_LENGTH)
&& Wallet.isAddressValid(address);
}
private boolean isBitcoinishAddress(String address) {
return BitcoinAddressValidator.validate(address, BitcoinAddressType.BTC)
||
BitcoinAddressValidator.validate(address, BitcoinAddressType.LTC)
||
BitcoinAddressValidator.validate(address, BitcoinAddressType.DASH);
}
private boolean isEthAddress(String address) {
return EthAddressValidator.validate(address);
}
private void shakeAddress() {
if (possibleCryptos.size() > 1) { // address ambiguous
for (Crypto crypto : Crypto.values()) {
if (possibleCryptos.contains(crypto)) {
ibCrypto.get(crypto).startAnimation(Helper.getShakeAnimation(getContext()));
}
}
} else {
etAddress.startAnimation(Helper.getShakeAnimation(getContext()));
}
}
@Override
public boolean onValidateFields() {
if (!checkAddressNoError()) {
shakeAddress();
String enteredAddress = etAddress.getEditText().getText().toString().trim();
String dnsOA = dnsFromOpenAlias(enteredAddress);
Timber.d("OpenAlias is %s", dnsOA);
if (dnsOA != null) {
processOpenAlias(dnsOA);
}
return false;
}
if (sendListener != null) {
TxData txData = sendListener.getTxData();
if (txData instanceof TxDataBtc) {
((TxDataBtc) txData).setBtcAddress(etAddress.getEditText().getText().toString());
((TxDataBtc) txData).setBtcSymbol(selectedCrypto.getSymbol());
txData.setDestinationAddress(null);
ServiceHelper.ASSET = selectedCrypto.getSymbol().toLowerCase();
} else {
txData.setDestinationAddress(etAddress.getEditText().getText().toString());
ServiceHelper.ASSET = null;
}
txData.setUserNotes(new UserNotes(etNotes.getEditText().getText().toString()));
txData.setPriority(PendingTransaction.Priority.Priority_Default);
txData.setMixin(SendFragment.MIXIN);
}
return true;
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (context instanceof OnScanListener) {
onScanListener = (OnScanListener) context;
} else {
throw new ClassCastException(context.toString()
+ " must implement ScanListener");
}
}
// QR Scan Stuff
@Override
public void onResume() {
super.onResume();
Timber.d("onResume");
processScannedData();
}
public void processScannedData(BarcodeData barcodeData) {
sendListener.setBarcodeData(barcodeData);
if (isResumed())
processScannedData();
}
public void processScannedData() {
BarcodeData barcodeData = sendListener.getBarcodeData();
if (barcodeData != null) {
Timber.d("GOT DATA");
if (!Helper.ALLOW_SHIFT && (barcodeData.asset != Crypto.XMR)) {
Timber.d("BUT ONLY XMR SUPPORTED");
barcodeData = null;
sendListener.setBarcodeData(barcodeData);
return;
}
if (barcodeData.address != null) {
etAddress.getEditText().setText(barcodeData.address);
possibleCryptos.clear();
selectedCrypto = null;
if (barcodeData.isAmbiguous()) {
possibleCryptos.addAll(barcodeData.ambiguousAssets);
} else {
possibleCryptos.add(barcodeData.asset);
selectedCrypto = barcodeData.asset;
}
if (Helper.ALLOW_SHIFT)
updateCryptoButtons(false);
if (checkAddress()) {
if (barcodeData.security == BarcodeData.Security.OA_NO_DNSSEC)
etAddress.setError(getString(R.string.send_address_no_dnssec));
else if (barcodeData.security == BarcodeData.Security.OA_DNSSEC)
etAddress.setError(getString(R.string.send_address_openalias));
}
} else {
etAddress.getEditText().getText().clear();
etAddress.setError(null);
}
String scannedNotes = barcodeData.addressName;
if (scannedNotes == null) {
scannedNotes = barcodeData.description;
} else if (barcodeData.description != null) {
scannedNotes = scannedNotes + ": " + barcodeData.description;
}
if (scannedNotes != null) {
etNotes.getEditText().setText(scannedNotes);
} else {
etNotes.getEditText().getText().clear();
etNotes.setError(null);
}
} else
Timber.d("barcodeData=null");
}
@Override
public void onResumeFragment() {
super.onResumeFragment();
Timber.d("onResumeFragment()");
etDummy.requestFocus();
}
String dnsFromOpenAlias(String openalias) {
Timber.d("checking openalias candidate %s", openalias);
if (Patterns.DOMAIN_NAME.matcher(openalias).matches()) return openalias;
if (Patterns.EMAIL_ADDRESS.matcher(openalias).matches()) {
openalias = openalias.replaceFirst("@", ".");
if (Patterns.DOMAIN_NAME.matcher(openalias).matches()) return openalias;
}
return null; // not an openalias
}
}

View file

@ -0,0 +1,159 @@
/*
* 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.fragment.send;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.TextView;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.BarcodeData;
import com.m2049r.xmrwallet.data.TxData;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.widget.ExchangeEditText;
import timber.log.Timber;
public class SendAmountWizardFragment extends SendWizardFragment {
public static SendAmountWizardFragment newInstance(Listener listener) {
SendAmountWizardFragment instance = new SendAmountWizardFragment();
instance.setSendListener(listener);
return instance;
}
Listener sendListener;
public void setSendListener(Listener listener) {
this.sendListener = listener;
}
interface Listener {
SendFragment.Listener getActivityCallback();
TxData getTxData();
BarcodeData popBarcodeData();
}
private TextView tvFunds;
private ExchangeEditText etAmount;
private View rlSweep;
private ImageButton ibSweep;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Timber.d("onCreateView() %s", (String.valueOf(savedInstanceState)));
sendListener = (Listener) getParentFragment();
View view = inflater.inflate(R.layout.fragment_send_amount, container, false);
tvFunds = view.findViewById(R.id.tvFunds);
etAmount = view.findViewById(R.id.etAmount);
rlSweep = view.findViewById(R.id.rlSweep);
view.findViewById(R.id.ivSweep).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
sweepAll(false);
}
});
ibSweep = view.findViewById(R.id.ibSweep);
ibSweep.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
sweepAll(true);
}
});
etAmount.requestFocus();
return view;
}
private boolean spendAllMode = false;
private void sweepAll(boolean spendAllMode) {
if (spendAllMode) {
ibSweep.setVisibility(View.INVISIBLE);
etAmount.setVisibility(View.GONE);
rlSweep.setVisibility(View.VISIBLE);
} else {
ibSweep.setVisibility(View.VISIBLE);
etAmount.setVisibility(View.VISIBLE);
rlSweep.setVisibility(View.GONE);
}
this.spendAllMode = spendAllMode;
}
@Override
public boolean onValidateFields() {
if (spendAllMode) {
if (sendListener != null) {
sendListener.getTxData().setAmount(Wallet.SWEEP_ALL);
}
} else {
if (!etAmount.validate(maxFunds, 0)) {
return false;
}
if (sendListener != null) {
String xmr = etAmount.getNativeAmount();
if (xmr != null) {
sendListener.getTxData().setAmount(Wallet.getAmountFromString(xmr));
} else {
sendListener.getTxData().setAmount(0L);
}
}
}
return true;
}
double maxFunds = 0;
@Override
public void onResumeFragment() {
super.onResumeFragment();
Timber.d("onResumeFragment()");
Helper.showKeyboard(getActivity());
final long funds = getTotalFunds();
maxFunds = 1.0 * funds / Helper.ONE_XMR;
if (!sendListener.getActivityCallback().isStreetMode()) {
tvFunds.setText(getString(R.string.send_available,
Wallet.getDisplayAmount(funds)));
} else {
tvFunds.setText(getString(R.string.send_available,
getString(R.string.unknown_amount)));
}
final BarcodeData data = sendListener.popBarcodeData();
if ((data != null) && (data.amount != null)) {
etAmount.setAmount(data.amount);
}
}
long getTotalFunds() {
return sendListener.getActivityCallback().getTotalFunds();
}
}

View file

@ -0,0 +1,263 @@
/*
* 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.fragment.send;
import android.os.Bundle;
import android.text.Html;
import android.text.Spanned;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.BarcodeData;
import com.m2049r.xmrwallet.data.TxDataBtc;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
import com.m2049r.xmrwallet.service.shift.ShiftError;
import com.m2049r.xmrwallet.service.shift.ShiftException;
import com.m2049r.xmrwallet.service.shift.sideshift.api.QueryOrderParameters;
import com.m2049r.xmrwallet.service.shift.sideshift.api.SideShiftApi;
import com.m2049r.xmrwallet.service.shift.sideshift.network.SideShiftApiImpl;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.ServiceHelper;
import com.m2049r.xmrwallet.widget.ExchangeOtherEditText;
import com.m2049r.xmrwallet.widget.SendProgressView;
import java.text.NumberFormat;
import java.util.Locale;
import timber.log.Timber;
public class SendBtcAmountWizardFragment extends SendWizardFragment {
public static SendBtcAmountWizardFragment newInstance(SendAmountWizardFragment.Listener listener) {
SendBtcAmountWizardFragment instance = new SendBtcAmountWizardFragment();
instance.setSendListener(listener);
return instance;
}
SendAmountWizardFragment.Listener sendListener;
public SendBtcAmountWizardFragment setSendListener(SendAmountWizardFragment.Listener listener) {
this.sendListener = listener;
return this;
}
private TextView tvFunds;
private ExchangeOtherEditText etAmount;
private TextView tvXmrToParms;
private SendProgressView evParams;
private View llXmrToParms;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Timber.d("onCreateView() %s", (String.valueOf(savedInstanceState)));
sendListener = (SendAmountWizardFragment.Listener) getParentFragment();
View view = inflater.inflate(R.layout.fragment_send_btc_amount, container, false);
tvFunds = view.findViewById(R.id.tvFunds);
evParams = view.findViewById(R.id.evXmrToParms);
llXmrToParms = view.findViewById(R.id.llXmrToParms);
tvXmrToParms = view.findViewById(R.id.tvXmrToParms);
etAmount = view.findViewById(R.id.etAmount);
etAmount.requestFocus();
return view;
}
@Override
public boolean onValidateFields() {
Timber.i(maxBtc + "/" + minBtc);
if (!etAmount.validate(maxBtc, minBtc)) {
return false;
}
if (orderParameters == null) {
return false; // this should never happen
}
if (sendListener != null) {
TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
String btcString = etAmount.getNativeAmount();
if (btcString != null) {
try {
double btc = Double.parseDouble(btcString);
Timber.d("setBtcAmount %f", btc);
txDataBtc.setBtcAmount(btc);
txDataBtc.setAmount(btc / orderParameters.getPrice());
} catch (NumberFormatException ex) {
Timber.d(ex.getLocalizedMessage());
txDataBtc.setBtcAmount(0);
}
} else {
txDataBtc.setBtcAmount(0);
}
}
return true;
}
double maxBtc = 0;
double minBtc = 0;
@Override
public void onPauseFragment() {
llXmrToParms.setVisibility(View.INVISIBLE);
}
@Override
public void onResumeFragment() {
super.onResumeFragment();
Timber.d("onResumeFragment()");
final String btcSymbol = ((TxDataBtc) sendListener.getTxData()).getBtcSymbol();
if (!btcSymbol.toLowerCase().equals(ServiceHelper.ASSET))
throw new IllegalStateException("Asset Symbol is wrong!");
final long funds = getTotalFunds();
if (!sendListener.getActivityCallback().isStreetMode()) {
tvFunds.setText(getString(R.string.send_available,
Wallet.getDisplayAmount(funds)));
//TODO
} else {
tvFunds.setText(getString(R.string.send_available,
getString(R.string.unknown_amount)));
}
etAmount.setAmount("");
final BarcodeData data = sendListener.popBarcodeData();
if (data != null) {
if (data.amount != null) {
etAmount.setAmount(data.amount);
}
}
etAmount.setBaseCurrency(btcSymbol);
callXmrTo();
}
long getTotalFunds() {
return sendListener.getActivityCallback().getTotalFunds();
}
private QueryOrderParameters orderParameters = null;
private void processOrderParms(final QueryOrderParameters orderParameters) {
this.orderParameters = orderParameters;
getView().post(() -> {
final double price = orderParameters.getPrice();
etAmount.setExchangeRate(1 / price);
maxBtc = price * orderParameters.getUpperLimit();
minBtc = price * orderParameters.getLowerLimit();
Timber.d("minBtc=%f / maxBtc=%f", minBtc, maxBtc);
NumberFormat df = NumberFormat.getInstance(Locale.US);
df.setMaximumFractionDigits(6);
String min = df.format(minBtc);
String max = df.format(maxBtc);
String rate = df.format(price);
final TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
Spanned xmrParmText = Html.fromHtml(getString(R.string.info_send_xmrto_parms,
min, max, rate, txDataBtc.getBtcSymbol()));
tvXmrToParms.setText(xmrParmText);
final long funds = getTotalFunds();
double availableXmr = 1.0 * funds / Helper.ONE_XMR;
String availBtcString;
String availXmrString;
if (!sendListener.getActivityCallback().isStreetMode()) {
availBtcString = df.format(availableXmr * price);
availXmrString = df.format(availableXmr);
} else {
availBtcString = getString(R.string.unknown_amount);
availXmrString = availBtcString;
}
tvFunds.setText(getString(R.string.send_available_btc,
availXmrString,
availBtcString,
((TxDataBtc) sendListener.getTxData()).getBtcSymbol()));
llXmrToParms.setVisibility(View.VISIBLE);
evParams.hideProgress();
});
}
private void processOrderParmsError(final Exception ex) {
etAmount.setExchangeRate(0);
orderParameters = null;
maxBtc = 0;
minBtc = 0;
Timber.e(ex);
getView().post(() -> {
if (ex instanceof ShiftException) {
ShiftException xmrEx = (ShiftException) ex;
ShiftError xmrErr = xmrEx.getError();
if (xmrErr != null) {
if (xmrErr.isRetryable()) {
evParams.showMessage(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(),
getString(R.string.text_retry));
evParams.setOnClickListener(v -> {
evParams.setOnClickListener(null);
callXmrTo();
});
} else {
evParams.showMessage(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(),
getString(R.string.text_noretry));
}
} else {
evParams.showMessage(getString(R.string.label_generic_xmrto_error),
getString(R.string.text_generic_xmrto_error, xmrEx.getCode()),
getString(R.string.text_noretry));
}
} else {
evParams.showMessage(getString(R.string.label_generic_xmrto_error),
ex.getLocalizedMessage(),
getString(R.string.text_noretry));
}
});
}
private void callXmrTo() {
evParams.showProgress(getString(R.string.label_send_progress_queryparms));
getXmrToApi().queryOrderParameters(new ShiftCallback<QueryOrderParameters>() {
@Override
public void onSuccess(final QueryOrderParameters orderParameters) {
processOrderParms(orderParameters);
}
@Override
public void onError(final Exception e) {
processOrderParmsError(e);
}
});
}
private SideShiftApi xmrToApi = null;
private SideShiftApi getXmrToApi() {
if (xmrToApi == null) {
synchronized (this) {
if (xmrToApi == null) {
xmrToApi = new SideShiftApiImpl(ServiceHelper.getXmrToBaseUrl());
}
}
}
return xmrToApi;
}
}

View file

@ -0,0 +1,551 @@
/*
* 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.fragment.send;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.TxData;
import com.m2049r.xmrwallet.data.TxDataBtc;
import com.m2049r.xmrwallet.model.PendingTransaction;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
import com.m2049r.xmrwallet.service.shift.ShiftError;
import com.m2049r.xmrwallet.service.shift.ShiftException;
import com.m2049r.xmrwallet.service.shift.sideshift.api.CreateOrder;
import com.m2049r.xmrwallet.service.shift.sideshift.api.RequestQuote;
import com.m2049r.xmrwallet.service.shift.sideshift.api.SideShiftApi;
import com.m2049r.xmrwallet.service.shift.sideshift.network.SideShiftApiImpl;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.ServiceHelper;
import com.m2049r.xmrwallet.widget.SendProgressView;
import java.text.NumberFormat;
import java.util.Locale;
import timber.log.Timber;
public class SendBtcConfirmWizardFragment extends SendWizardFragment implements SendConfirm {
public static SendBtcConfirmWizardFragment newInstance(SendConfirmWizardFragment.Listener listener) {
SendBtcConfirmWizardFragment instance = new SendBtcConfirmWizardFragment();
instance.setSendListener(listener);
return instance;
}
SendConfirmWizardFragment.Listener sendListener;
public void setSendListener(SendConfirmWizardFragment.Listener listener) {
this.sendListener = listener;
}
private View llStageA;
private SendProgressView evStageA;
private View llStageB;
private SendProgressView evStageB;
private View llStageC;
private SendProgressView evStageC;
private TextView tvTxBtcAmount;
private TextView tvTxBtcRate;
private TextView tvTxBtcAddress;
private TextView tvTxBtcAddressLabel;
private TextView tvTxXmrToKey;
private TextView tvTxFee;
private TextView tvTxTotal;
private View llConfirmSend;
private Button bSend;
private View pbProgressSend;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Timber.d("onCreateView(%s)", (String.valueOf(savedInstanceState)));
View view = inflater.inflate(
R.layout.fragment_send_btc_confirm, container, false);
tvTxBtcAddress = view.findViewById(R.id.tvTxBtcAddress);
tvTxBtcAddressLabel = view.findViewById(R.id.tvTxBtcAddressLabel);
tvTxBtcAmount = view.findViewById(R.id.tvTxBtcAmount);
tvTxBtcRate = view.findViewById(R.id.tvTxBtcRate);
tvTxXmrToKey = view.findViewById(R.id.tvTxXmrToKey);
tvTxFee = view.findViewById(R.id.tvTxFee);
tvTxTotal = view.findViewById(R.id.tvTxTotal);
llStageA = view.findViewById(R.id.llStageA);
evStageA = view.findViewById(R.id.evStageA);
llStageB = view.findViewById(R.id.llStageB);
evStageB = view.findViewById(R.id.evStageB);
llStageC = view.findViewById(R.id.llStageC);
evStageC = view.findViewById(R.id.evStageC);
tvTxXmrToKey.setOnClickListener(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();
});
llConfirmSend = view.findViewById(R.id.llConfirmSend);
pbProgressSend = view.findViewById(R.id.pbProgressSend);
bSend = view.findViewById(R.id.bSend);
bSend.setEnabled(false);
bSend.setOnClickListener(v -> {
Timber.d("bSend.setOnClickListener");
bSend.setEnabled(false);
preSend();
});
return view;
}
int inProgress = 0;
final static int STAGE_X = 0;
final static int STAGE_A = 1;
final static int STAGE_B = 2;
final static int STAGE_C = 3;
private void showProgress(int stage, String progressText) {
Timber.d("showProgress(%d)", stage);
inProgress = stage;
switch (stage) {
case STAGE_A:
evStageA.showProgress(progressText);
break;
case STAGE_B:
evStageB.showProgress(progressText);
break;
case STAGE_C:
evStageC.showProgress(progressText);
break;
default:
throw new IllegalStateException("unknown stage " + stage);
}
}
public void hideProgress() {
Timber.d("hideProgress(%d)", inProgress);
switch (inProgress) {
case STAGE_A:
evStageA.hideProgress();
llStageA.setVisibility(View.VISIBLE);
break;
case STAGE_B:
evStageB.hideProgress();
llStageB.setVisibility(View.VISIBLE);
break;
case STAGE_C:
evStageC.hideProgress();
llStageC.setVisibility(View.VISIBLE);
break;
default:
throw new IllegalStateException("unknown stage " + inProgress);
}
inProgress = STAGE_X;
}
public void showStageError(String code, String message, String solution) {
switch (inProgress) {
case STAGE_A:
evStageA.showMessage(code, message, solution);
break;
case STAGE_B:
evStageB.showMessage(code, message, solution);
break;
case STAGE_C:
evStageC.showMessage(code, message, solution);
break;
default:
throw new IllegalStateException("unknown stage");
}
inProgress = STAGE_X;
}
PendingTransaction pendingTransaction = null;
void send() {
Timber.d("SEND @%d", sendCountdown);
if (sendCountdown <= 0) {
Timber.i("User waited too long in password dialog.");
Toast.makeText(getContext(), getString(R.string.send_xmrto_timeout), Toast.LENGTH_SHORT).show();
return;
}
sendListener.getTxData().getUserNotes().setXmrtoOrder(xmrtoOrder); // note the transaction in the TX notes
((TxDataBtc) sendListener.getTxData()).setXmrtoOrderId(xmrtoOrder.getOrderId()); // remember the order id for later
// TODO make method in TxDataBtc to set both of the above in one go
sendListener.commitTransaction();
getActivity().runOnUiThread(() -> pbProgressSend.setVisibility(View.VISIBLE));
}
@Override
public void sendFailed(String error) {
pbProgressSend.setVisibility(View.INVISIBLE);
Toast.makeText(getContext(), getString(R.string.status_transaction_failed, error), Toast.LENGTH_LONG).show();
}
@Override
// callback from wallet when PendingTransaction created (started by prepareSend() here
public void transactionCreated(final String txTag, final PendingTransaction pendingTransaction) {
if (isResumed
&& (inProgress == STAGE_C)
&& (xmrtoOrder != null)
&& (xmrtoOrder.getOrderId().equals(txTag))) {
this.pendingTransaction = pendingTransaction;
getView().post(() -> {
hideProgress();
tvTxFee.setText(Wallet.getDisplayAmount(pendingTransaction.getFee()));
tvTxTotal.setText(Wallet.getDisplayAmount(
pendingTransaction.getFee() + pendingTransaction.getAmount()));
updateSendButton();
});
} else {
this.pendingTransaction = null;
sendListener.disposeTransaction();
}
}
@Override
public void createTransactionFailed(String errorText) {
Timber.e("CREATE TX FAILED");
if (pendingTransaction != null) {
throw new IllegalStateException("pendingTransaction is not null");
}
showStageError(getString(R.string.send_create_tx_error_title),
errorText,
getString(R.string.text_noretry_monero));
}
@Override
public boolean onValidateFields() {
return true;
}
private boolean isResumed = false;
@Override
public void onPauseFragment() {
isResumed = false;
stopSendTimer();
sendListener.disposeTransaction();
pendingTransaction = null;
inProgress = STAGE_X;
updateSendButton();
super.onPauseFragment();
}
@Override
public void onResumeFragment() {
super.onResumeFragment();
Timber.d("onResumeFragment()");
if (sendListener.getMode() != SendFragment.Mode.BTC) {
throw new IllegalStateException("Mode is not BTC!");
}
if (!((TxDataBtc) sendListener.getTxData()).getBtcSymbol().toLowerCase().equals(ServiceHelper.ASSET))
throw new IllegalStateException("Asset Symbol is wrong!");
Helper.hideKeyboard(getActivity());
llStageA.setVisibility(View.INVISIBLE);
evStageA.hideProgress();
llStageB.setVisibility(View.INVISIBLE);
evStageB.hideProgress();
llStageC.setVisibility(View.INVISIBLE);
evStageC.hideProgress();
isResumed = true;
if ((pendingTransaction == null) && (inProgress == STAGE_X)) {
stageA();
} // otherwise just sit there blank
// TODO: don't sit there blank - can this happen? should we just die?
}
private int sendCountdown = 0;
private static final int XMRTO_COUNTDOWN_STEP = 1; // 1 second
Runnable updateRunnable = null;
void startSendTimer(int timeout) {
Timber.d("startSendTimer()");
sendCountdown = timeout;
updateRunnable = new Runnable() {
@Override
public void run() {
if (!isAdded())
return;
Timber.d("updateTimer()");
if (sendCountdown <= 0) {
bSend.setEnabled(false);
sendCountdown = 0;
Toast.makeText(getContext(), getString(R.string.send_xmrto_timeout), Toast.LENGTH_SHORT).show();
}
int minutes = sendCountdown / 60;
int seconds = sendCountdown % 60;
String t = String.format("%d:%02d", minutes, seconds);
bSend.setText(getString(R.string.send_send_timed_label, t));
if (sendCountdown > 0) {
sendCountdown -= XMRTO_COUNTDOWN_STEP;
getView().postDelayed(this, XMRTO_COUNTDOWN_STEP * 1000);
}
}
};
getView().post(updateRunnable);
}
void stopSendTimer() {
getView().removeCallbacks(updateRunnable);
}
void updateSendButton() {
Timber.d("updateSendButton()");
if (pendingTransaction != null) {
llConfirmSend.setVisibility(View.VISIBLE);
bSend.setEnabled(sendCountdown > 0);
} else {
llConfirmSend.setVisibility(View.GONE);
bSend.setEnabled(false);
}
}
public void preSend() {
Helper.promptPassword(getContext(), getActivityCallback().getWalletName(), false, new Helper.PasswordAction() {
@Override
public void act(String walletName, String password, boolean fingerprintUsed) {
send();
}
public void fail(String walletName) {
getActivity().runOnUiThread(() -> {
bSend.setEnabled(sendCountdown > 0); // allow to try again
});
}
});
}
// creates a pending transaction and calls us back with transactionCreated()
// or createTransactionFailed()
void prepareSend() {
if (!isResumed) return;
if ((xmrtoOrder == null)) {
throw new IllegalStateException("xmrtoOrder is null");
}
showProgress(3, getString(R.string.label_send_progress_create_tx));
final TxData txData = sendListener.getTxData();
txData.setDestinationAddress(xmrtoOrder.getXmrAddress());
txData.setAmount(xmrtoOrder.getXmrAmount());
getActivityCallback().onPrepareSend(xmrtoOrder.getOrderId(), txData);
}
SendFragment.Listener getActivityCallback() {
return sendListener.getActivityCallback();
}
private RequestQuote xmrtoQuote = null;
private void processStageA(final RequestQuote requestQuote) {
Timber.d("processCreateOrder %s", requestQuote.getId());
TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
// verify the BTC amount is correct
if (requestQuote.getBtcAmount() != txDataBtc.getBtcAmount()) {
Timber.d("Failed to get quote");
getView().post(() -> showStageError(ShiftError.Error.SERVICE.toString(),
getString(R.string.shift_noquote),
getString(R.string.shift_checkamount)));
return; // just stop for now
}
xmrtoQuote = requestQuote;
txDataBtc.setAmount(xmrtoQuote.getXmrAmount());
getView().post(() -> {
// show data from the actual quote as that is what is used to
NumberFormat df = NumberFormat.getInstance(Locale.US);
df.setMaximumFractionDigits(12);
final String btcAmount = df.format(xmrtoQuote.getBtcAmount());
final String xmrAmountTotal = df.format(xmrtoQuote.getXmrAmount());
tvTxBtcAmount.setText(getString(R.string.text_send_btc_amount,
btcAmount, xmrAmountTotal, txDataBtc.getBtcSymbol()));
final String xmrPriceBtc = df.format(xmrtoQuote.getPrice());
tvTxBtcRate.setText(getString(R.string.text_send_btc_rate, xmrPriceBtc, txDataBtc.getBtcSymbol()));
hideProgress();
});
stageB(requestQuote.getId());
}
private void processStageAError(final Exception ex) {
Timber.e("processStageAError %s", ex.getLocalizedMessage());
getView().post(() -> {
if (ex instanceof ShiftException) {
ShiftException xmrEx = (ShiftException) ex;
ShiftError xmrErr = xmrEx.getError();
if (xmrErr != null) {
if (xmrErr.isRetryable()) {
showStageError(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(),
getString(R.string.text_retry));
evStageA.setOnClickListener(v -> {
evStageA.setOnClickListener(null);
stageA();
});
} else {
showStageError(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(),
getString(R.string.text_noretry));
}
} else {
showStageError(getString(R.string.label_generic_xmrto_error),
getString(R.string.text_generic_xmrto_error, xmrEx.getCode()),
getString(R.string.text_noretry));
}
} else {
evStageA.showMessage(getString(R.string.label_generic_xmrto_error),
ex.getLocalizedMessage(),
getString(R.string.text_noretry));
}
});
}
private void stageA() {
if (!isResumed) return;
Timber.d("Request Quote");
xmrtoQuote = null;
xmrtoOrder = null;
showProgress(1, getString(R.string.label_send_progress_xmrto_create));
TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
ShiftCallback<RequestQuote> callback = new ShiftCallback<RequestQuote>() {
@Override
public void onSuccess(RequestQuote requestQuote) {
if (!isResumed) return;
if (xmrtoQuote != null) {
Timber.w("another ongoing request quote request");
return;
}
processStageA(requestQuote);
}
@Override
public void onError(Exception ex) {
if (!isResumed) return;
if (xmrtoQuote != null) {
Timber.w("another ongoing request quote request");
return;
}
processStageAError(ex);
}
};
getXmrToApi().requestQuote(txDataBtc.getBtcAmount(), callback);
}
private CreateOrder xmrtoOrder = null;
private void processStageB(final CreateOrder order) {
Timber.d("processCreateOrder %s for %s", order.getOrderId(), order.getQuoteId());
TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
// verify amount & destination
if ((order.getBtcAmount() != txDataBtc.getBtcAmount())
|| (!txDataBtc.validateAddress(order.getBtcAddress()))) {
throw new IllegalStateException("Order does not fulfill quote!"); // something is terribly wrong - die
}
xmrtoOrder = order;
getView().post(() -> {
tvTxXmrToKey.setText(order.getOrderId());
tvTxBtcAddress.setText(order.getBtcAddress());
tvTxBtcAddressLabel.setText(getString(R.string.label_send_btc_address, txDataBtc.getBtcSymbol()));
hideProgress();
Timber.d("Expires @ %s", order.getExpiresAt().toString());
final int timeout = (int) (order.getExpiresAt().getTime() - order.getCreatedAt().getTime()) / 1000 - 60; // -1 minute buffer
startSendTimer(timeout);
prepareSend();
});
}
private void processStageBError(final Exception ex) {
Timber.e("processCreateOrderError %s", ex.getLocalizedMessage());
getView().post(() -> {
if (ex instanceof ShiftException) {
ShiftException xmrEx = (ShiftException) ex;
ShiftError xmrErr = xmrEx.getError();
if (xmrErr != null) {
if (xmrErr.isRetryable()) {
showStageError(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(),
getString(R.string.text_retry));
evStageB.setOnClickListener(v -> {
evStageB.setOnClickListener(null);
stageB(xmrtoOrder.getOrderId());
});
} else {
showStageError(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(),
getString(R.string.text_noretry));
}
} else {
showStageError(getString(R.string.label_generic_xmrto_error),
getString(R.string.text_generic_xmrto_error, xmrEx.getCode()),
getString(R.string.text_noretry));
}
} else {
evStageB.showMessage(getString(R.string.label_generic_xmrto_error),
ex.getLocalizedMessage(),
getString(R.string.text_noretry));
}
});
}
private void stageB(final String quoteId) {
Timber.d("createOrder(%s)", quoteId);
if (!isResumed) return;
final String btcAddress = ((TxDataBtc) sendListener.getTxData()).getBtcAddress();
getView().post(() -> {
xmrtoOrder = null;
showProgress(2, getString(R.string.label_send_progress_xmrto_query));
getXmrToApi().createOrder(quoteId, btcAddress, new ShiftCallback<CreateOrder>() {
@Override
public void onSuccess(CreateOrder order) {
if (!isResumed) return;
if (xmrtoQuote == null) return;
if (!order.getQuoteId().equals(xmrtoQuote.getId())) {
Timber.d("Quote ID does not match");
// ignore (we got a response to a stale request)
return;
}
if (xmrtoOrder != null)
throw new IllegalStateException("xmrtoOrder must be null here!");
processStageB(order);
}
@Override
public void onError(Exception ex) {
if (!isResumed) return;
processStageBError(ex);
}
});
});
}
private SideShiftApi xmrToApi = null;
private SideShiftApi getXmrToApi() {
if (xmrToApi == null) {
synchronized (this) {
if (xmrToApi == null) {
xmrToApi = new SideShiftApiImpl(ServiceHelper.getXmrToBaseUrl());
}
}
}
return xmrToApi;
}
}

View file

@ -0,0 +1,262 @@
/*
* 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.fragment.send;
import android.content.Intent;
import android.graphics.Paint;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.Crypto;
import com.m2049r.xmrwallet.data.PendingTx;
import com.m2049r.xmrwallet.data.TxDataBtc;
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
import com.m2049r.xmrwallet.service.shift.ShiftException;
import com.m2049r.xmrwallet.service.shift.sideshift.api.QueryOrderStatus;
import com.m2049r.xmrwallet.service.shift.sideshift.api.SideShiftApi;
import com.m2049r.xmrwallet.service.shift.sideshift.network.SideShiftApiImpl;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.ServiceHelper;
import com.m2049r.xmrwallet.util.ThemeHelper;
import java.text.NumberFormat;
import java.util.Locale;
import timber.log.Timber;
public class SendBtcSuccessWizardFragment extends SendWizardFragment {
public static SendBtcSuccessWizardFragment newInstance(SendSuccessWizardFragment.Listener listener) {
SendBtcSuccessWizardFragment instance = new SendBtcSuccessWizardFragment();
instance.setSendListener(listener);
return instance;
}
SendSuccessWizardFragment.Listener sendListener;
public void setSendListener(SendSuccessWizardFragment.Listener listener) {
this.sendListener = listener;
}
ImageButton bCopyTxId;
private TextView tvTxId;
private TextView tvTxAddress;
private TextView tvTxAmount;
private TextView tvTxFee;
private TextView tvXmrToAmount;
private ImageView ivXmrToIcon;
private TextView tvXmrToStatus;
private ImageView ivXmrToStatus;
private ImageView ivXmrToStatusBig;
private ProgressBar pbXmrto;
private TextView tvTxXmrToKey;
private TextView tvXmrToSupport;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Timber.d("onCreateView() %s", (String.valueOf(savedInstanceState)));
View view = inflater.inflate(
R.layout.fragment_send_btc_success, container, false);
bCopyTxId = view.findViewById(R.id.bCopyTxId);
bCopyTxId.setEnabled(false);
bCopyTxId.setOnClickListener(v -> {
Helper.clipBoardCopy(getActivity(), getString(R.string.label_send_txid), tvTxId.getText().toString());
Toast.makeText(getActivity(), getString(R.string.message_copy_txid), Toast.LENGTH_SHORT).show();
});
tvXmrToAmount = view.findViewById(R.id.tvXmrToAmount);
ivXmrToIcon = view.findViewById(R.id.ivXmrToIcon);
tvXmrToStatus = view.findViewById(R.id.tvXmrToStatus);
ivXmrToStatus = view.findViewById(R.id.ivXmrToStatus);
ivXmrToStatusBig = view.findViewById(R.id.ivXmrToStatusBig);
tvTxId = view.findViewById(R.id.tvTxId);
tvTxAddress = view.findViewById(R.id.tvTxAddress);
tvTxAmount = view.findViewById(R.id.tvTxAmount);
tvTxFee = view.findViewById(R.id.tvTxFee);
pbXmrto = view.findViewById(R.id.pbXmrto);
pbXmrto.getIndeterminateDrawable().setColorFilter(0x61000000, android.graphics.PorterDuff.Mode.MULTIPLY);
tvTxXmrToKey = view.findViewById(R.id.tvTxXmrToKey);
tvTxXmrToKey.setOnClickListener(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();
});
tvXmrToSupport = view.findViewById(R.id.tvXmrToSupport);
tvXmrToSupport.setPaintFlags(tvXmrToSupport.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
return view;
}
@Override
public boolean onValidateFields() {
return true;
}
private boolean isResumed = false;
@Override
public void onPauseFragment() {
isResumed = false;
super.onPauseFragment();
}
TxDataBtc btcData = null;
@Override
public void onResumeFragment() {
super.onResumeFragment();
Timber.d("onResumeFragment()");
Helper.hideKeyboard(getActivity());
isResumed = true;
btcData = (TxDataBtc) sendListener.getTxData();
tvTxAddress.setText(btcData.getDestinationAddress());
final PendingTx committedTx = sendListener.getCommittedTx();
if (committedTx != null) {
tvTxId.setText(committedTx.txId);
bCopyTxId.setEnabled(true);
tvTxAmount.setText(getString(R.string.send_amount, Helper.getDisplayAmount(committedTx.amount)));
tvTxFee.setText(getString(R.string.send_fee, Helper.getDisplayAmount(committedTx.fee)));
if (btcData != null) {
NumberFormat df = NumberFormat.getInstance(Locale.US);
df.setMaximumFractionDigits(12);
String btcAmount = df.format(btcData.getBtcAmount());
tvXmrToAmount.setText(getString(R.string.info_send_xmrto_success_btc, btcAmount, btcData.getBtcSymbol()));
//TODO btcData.getBtcAddress();
tvTxXmrToKey.setText(btcData.getXmrtoOrderId());
final Crypto crypto = Crypto.withSymbol(btcData.getBtcSymbol());
ivXmrToIcon.setImageResource(crypto.getIconEnabledId());
tvXmrToSupport.setOnClickListener(v -> {
Uri orderUri = getXmrToApi().getQueryOrderUri(btcData.getXmrtoOrderId());
Intent intent = new Intent(Intent.ACTION_VIEW, orderUri);
startActivity(intent);
});
queryOrder();
} else {
throw new IllegalStateException("btcData is null");
}
}
sendListener.enableDone();
}
private void processQueryOrder(final QueryOrderStatus status) {
Timber.d("processQueryOrder %s for %s", status.getState().toString(), status.getOrderId());
if (!btcData.getXmrtoOrderId().equals(status.getOrderId()))
throw new IllegalStateException("UUIDs do not match!");
if (isResumed && (getView() != null))
getView().post(() -> {
showXmrToStatus(status);
if (!status.isTerminal()) {
getView().postDelayed(this::queryOrder, SideShiftApi.QUERY_INTERVAL);
}
});
}
private void queryOrder() {
Timber.d("queryOrder(%s)", btcData.getXmrtoOrderId());
if (!isResumed) return;
getXmrToApi().queryOrderStatus(btcData.getXmrtoOrderId(), new ShiftCallback<QueryOrderStatus>() {
@Override
public void onSuccess(QueryOrderStatus status) {
if (!isAdded()) return;
processQueryOrder(status);
}
@Override
public void onError(final Exception ex) {
if (!isResumed) return;
Timber.w(ex);
getActivity().runOnUiThread(() -> {
if (ex instanceof ShiftException) {
Toast.makeText(getActivity(), ((ShiftException) ex).getError().getErrorMsg(), Toast.LENGTH_LONG).show();
} else {
Toast.makeText(getActivity(), ex.getLocalizedMessage(), Toast.LENGTH_LONG).show();
}
});
}
});
}
void showXmrToStatus(final QueryOrderStatus status) {
int statusResource = 0;
if (status.isError()) {
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_error, status.toString()));
statusResource = R.drawable.ic_error_red_24dp;
pbXmrto.getIndeterminateDrawable().setColorFilter(
ThemeHelper.getThemedColor(getContext(), android.R.attr.colorError),
android.graphics.PorterDuff.Mode.MULTIPLY);
} else if (status.isSent() || status.isPaid()) {
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_sent, btcData.getBtcSymbol()));
statusResource = R.drawable.ic_success;
pbXmrto.getIndeterminateDrawable().setColorFilter(
ThemeHelper.getThemedColor(getContext(), R.attr.positiveColor),
android.graphics.PorterDuff.Mode.MULTIPLY);
} else if (status.isWaiting()) {
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_unpaid));
statusResource = R.drawable.ic_pending;
pbXmrto.getIndeterminateDrawable().setColorFilter(
ThemeHelper.getThemedColor(getContext(), R.attr.neutralColor),
android.graphics.PorterDuff.Mode.MULTIPLY);
} else if (status.isPending()) {
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_paid));
statusResource = R.drawable.ic_pending;
pbXmrto.getIndeterminateDrawable().setColorFilter(
ThemeHelper.getThemedColor(getContext(), R.attr.neutralColor),
android.graphics.PorterDuff.Mode.MULTIPLY);
} else {
throw new IllegalStateException("status is broken: " + status.toString());
}
ivXmrToStatus.setImageResource(statusResource);
if (status.isTerminal()) {
pbXmrto.setVisibility(View.INVISIBLE);
ivXmrToIcon.setVisibility(View.GONE);
ivXmrToStatus.setVisibility(View.GONE);
ivXmrToStatusBig.setImageResource(statusResource);
ivXmrToStatusBig.setVisibility(View.VISIBLE);
}
}
private SideShiftApi xmrToApi = null;
private SideShiftApi getXmrToApi() {
if (xmrToApi == null) {
synchronized (this) {
if (xmrToApi == null) {
xmrToApi = new SideShiftApiImpl(ServiceHelper.getXmrToBaseUrl());
}
}
}
return xmrToApi;
}
}

View file

@ -0,0 +1,27 @@
/*
* 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.fragment.send;
import com.m2049r.xmrwallet.model.PendingTransaction;
interface SendConfirm {
void sendFailed(String errorText);
void createTransactionFailed(String errorText);
void transactionCreated(String txTag, PendingTransaction pendingTransaction);
}

View file

@ -0,0 +1,247 @@
/*
* 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.fragment.send;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.textfield.TextInputLayout;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.TxData;
import com.m2049r.xmrwallet.data.UserNotes;
import com.m2049r.xmrwallet.model.PendingTransaction;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.util.Helper;
import timber.log.Timber;
public class SendConfirmWizardFragment extends SendWizardFragment implements SendConfirm {
public static SendConfirmWizardFragment newInstance(Listener listener) {
SendConfirmWizardFragment instance = new SendConfirmWizardFragment();
instance.setSendListener(listener);
return instance;
}
Listener sendListener;
public SendConfirmWizardFragment setSendListener(Listener listener) {
this.sendListener = listener;
return this;
}
interface Listener {
SendFragment.Listener getActivityCallback();
TxData getTxData();
void commitTransaction();
void disposeTransaction();
SendFragment.Mode getMode();
}
private TextView tvTxAddress;
private TextView tvTxNotes;
private TextView tvTxAmount;
private TextView tvTxFee;
private TextView tvTxTotal;
private View llProgress;
private View bSend;
private View llConfirmSend;
private View pbProgressSend;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Timber.d("onCreateView() %s", (String.valueOf(savedInstanceState)));
View view = inflater.inflate(
R.layout.fragment_send_confirm, container, false);
tvTxAddress = view.findViewById(R.id.tvTxAddress);
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);
llConfirmSend = view.findViewById(R.id.llConfirmSend);
bSend = view.findViewById(R.id.bSend);
bSend.setEnabled(false);
bSend.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Timber.d("bSend.setOnClickListener");
bSend.setEnabled(false);
preSend();
}
});
return view;
}
boolean inProgress = false;
public void hideProgress() {
llProgress.setVisibility(View.INVISIBLE);
inProgress = false;
}
public void showProgress() {
llProgress.setVisibility(View.VISIBLE);
inProgress = true;
}
PendingTransaction pendingTransaction = null;
@Override
// callback from wallet when PendingTransaction created
public void transactionCreated(String txTag, PendingTransaction pendingTransaction) {
// ignore txTag - the app flow ensures this is the correct tx
// TODO: use the txTag
hideProgress();
if (isResumed) {
this.pendingTransaction = pendingTransaction;
refreshTransactionDetails();
} else {
sendListener.disposeTransaction();
}
}
void send() {
sendListener.commitTransaction();
getActivity().runOnUiThread(() -> pbProgressSend.setVisibility(View.VISIBLE));
}
@Override
public void sendFailed(String errorText) {
pbProgressSend.setVisibility(View.INVISIBLE);
showAlert(getString(R.string.send_create_tx_error_title), errorText);
}
@Override
public void createTransactionFailed(String errorText) {
hideProgress();
showAlert(getString(R.string.send_create_tx_error_title), errorText);
}
private void showAlert(String title, String message) {
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(getActivity());
builder.setCancelable(true).
setTitle(title).
setMessage(message).
create().
show();
}
@Override
public boolean onValidateFields() {
return true;
}
private boolean isResumed = false;
@Override
public void onPauseFragment() {
isResumed = false;
pendingTransaction = null;
sendListener.disposeTransaction();
refreshTransactionDetails();
super.onPauseFragment();
}
@Override
public void onResumeFragment() {
super.onResumeFragment();
Timber.d("onResumeFragment()");
Helper.hideKeyboard(getActivity());
isResumed = true;
final TxData txData = sendListener.getTxData();
tvTxAddress.setText(txData.getDestinationAddress());
UserNotes notes = sendListener.getTxData().getUserNotes();
if ((notes != null) && (!notes.note.isEmpty())) {
tvTxNotes.setText(notes.note);
} else {
tvTxNotes.setText("-");
}
refreshTransactionDetails();
if ((pendingTransaction == null) && (!inProgress)) {
showProgress();
prepareSend(txData);
}
}
void refreshTransactionDetails() {
Timber.d("refreshTransactionDetails()");
if (pendingTransaction != null) {
llConfirmSend.setVisibility(View.VISIBLE);
bSend.setEnabled(true);
tvTxFee.setText(Wallet.getDisplayAmount(pendingTransaction.getFee()));
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);
}
}
public void preSend() {
Helper.promptPassword(getContext(), getActivityCallback().getWalletName(), false, new Helper.PasswordAction() {
@Override
public void act(String walletName, String password, boolean fingerprintUsed) {
send();
}
public void fail(String walletName) {
getActivity().runOnUiThread(() -> {
bSend.setEnabled(true); // allow to try again
});
}
});
}
// creates a pending transaction and calls us back with transactionCreated()
// or createTransactionFailed()
void prepareSend(TxData txData) {
getActivityCallback().onPrepareSend(null, txData);
}
SendFragment.Listener getActivityCallback() {
return sendListener.getActivityCallback();
}
}

View file

@ -0,0 +1,558 @@
/*
* 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.fragment.send;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.text.InputType;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.transition.MaterialContainerTransform;
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.ThemeHelper;
import com.m2049r.xmrwallet.widget.DotBar;
import com.m2049r.xmrwallet.widget.Toolbar;
import java.lang.ref.WeakReference;
import timber.log.Timber;
public class SendFragment extends Fragment
implements SendAddressWizardFragment.Listener,
SendAmountWizardFragment.Listener,
SendConfirmWizardFragment.Listener,
SendSuccessWizardFragment.Listener,
OnBackPressedListener, OnUriScannedListener {
final static public int MIXIN = 0;
private Listener activityCallback;
public interface Listener {
SharedPreferences getPrefs();
long getTotalFunds();
boolean isStreetMode();
void onPrepareSend(String tag, TxData data);
String getWalletName();
void onSend(UserNotes notes);
void onDisposeRequest();
void onFragmentDone();
void setToolbarButton(int type);
void setTitle(String title);
void setSubtitle(String subtitle);
void setOnUriScannedListener(OnUriScannedListener onUriScannedListener);
}
private View llNavBar;
private DotBar dotBar;
private Button bPrev;
private Button bNext;
private Button bDone;
static private final 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) {
final View view = inflater.inflate(R.layout.fragment_send, container, false);
llNavBar = view.findViewById(R.id.llNavBar);
bDone = view.findViewById(R.id.bDone);
dotBar = view.findViewById(R.id.dotBar);
bPrev = view.findViewById(R.id.bPrev);
bNext = view.findViewById(R.id.bNext);
ViewGroup llNotice = view.findViewById(R.id.llNotice);
Notice.showAll(llNotice, ".*_send");
spendViewPager = view.findViewById(R.id.pager);
pagerAdapter = new SpendPagerAdapter(getChildFragmentManager());
spendViewPager.setOffscreenPageLimit(pagerAdapter.getCount()); // load & keep all pages in cache
spendViewPager.setAdapter(pagerAdapter);
spendViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
private int fallbackPosition = MAX_FALLBACK;
private int currentPosition = 0;
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int newPosition) {
Timber.d("onPageSelected=%d/%d", newPosition, fallbackPosition);
if (fallbackPosition < newPosition) {
spendViewPager.setCurrentItem(fallbackPosition);
} else {
pagerAdapter.getFragment(currentPosition).onPauseFragment();
pagerAdapter.getFragment(newPosition).onResumeFragment();
updatePosition(newPosition);
currentPosition = newPosition;
fallbackPosition = MAX_FALLBACK;
}
}
@Override
public void onPageScrollStateChanged(int state) {
if (state == ViewPager.SCROLL_STATE_DRAGGING) {
if (!spendViewPager.validateFields(spendViewPager.getCurrentItem())) {
fallbackPosition = spendViewPager.getCurrentItem();
} else {
fallbackPosition = spendViewPager.getCurrentItem() + 1;
}
}
}
});
bPrev.setOnClickListener(v -> spendViewPager.previous());
bNext.setOnClickListener(v -> spendViewPager.next());
bDone.setOnClickListener(v -> {
Timber.d("bDone.onClick");
activityCallback.onFragmentDone();
});
updatePosition(0);
final EditText etDummy = view.findViewById(R.id.etDummy);
etDummy.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
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.fromString(uri);
Timber.d("barcodeData: %s", barcodeData != null ? barcodeData.toString() : "null");
}
}
return view;
}
void updatePosition(int position) {
dotBar.setActiveDot(position);
CharSequence nextLabel = pagerAdapter.getPageTitle(position + 1);
bNext.setText(nextLabel);
if (nextLabel != null) {
bNext.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_navigate_next, 0);
} else {
bNext.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
}
CharSequence prevLabel = pagerAdapter.getPageTitle(position - 1);
bPrev.setText(prevLabel);
if (prevLabel != null) {
bPrev.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_navigate_prev, 0, 0, 0);
} else {
bPrev.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
}
}
@Override
public void onResume() {
super.onResume();
Timber.d("onResume");
activityCallback.setSubtitle(getString(R.string.send_title));
if (spendViewPager.getCurrentItem() == SpendPagerAdapter.POS_SUCCESS) {
activityCallback.setToolbarButton(Toolbar.BUTTON_NONE);
} else {
activityCallback.setToolbarButton(Toolbar.BUTTON_CANCEL);
}
}
@Override
public void onAttach(@NonNull Context context) {
Timber.d("onAttach %s", context);
super.onAttach(context);
if (context instanceof Listener) {
activityCallback = (Listener) context;
activityCallback.setOnUriScannedListener(this);
} else {
throw new ClassCastException(context.toString()
+ " must implement Listener");
}
}
@Override
public void onDetach() {
activityCallback.setOnUriScannedListener(null);
super.onDetach();
}
private SpendViewPager spendViewPager;
private SpendPagerAdapter pagerAdapter;
@Override
public boolean onBackPressed() {
if (isComitted()) return true; // no going back
if (spendViewPager.getCurrentItem() == 0) {
return false;
} else {
spendViewPager.previous();
return true;
}
}
@Override
public boolean onUriScanned(BarcodeData barcodeData) {
if (spendViewPager.getCurrentItem() == SpendPagerAdapter.POS_ADDRESS) {
final SendWizardFragment fragment = pagerAdapter.getFragment(SpendPagerAdapter.POS_ADDRESS);
if (fragment instanceof SendAddressWizardFragment) {
((SendAddressWizardFragment) fragment).processScannedData(barcodeData);
return true;
}
}
return false;
}
enum Mode {
XMR, BTC
}
Mode mode = Mode.XMR;
@Override
public void setMode(Mode aMode) {
if (mode != aMode) {
mode = aMode;
switch (aMode) {
case XMR:
txData = new TxData();
break;
case BTC:
txData = new TxDataBtc();
break;
default:
throw new IllegalArgumentException("Mode " + String.valueOf(aMode) + " unknown!");
}
getView().post(() -> pagerAdapter.notifyDataSetChanged());
Timber.d("New Mode = %s", mode.toString());
}
}
@Override
public Mode getMode() {
return mode;
}
public class SpendPagerAdapter extends FragmentStatePagerAdapter {
private static final int POS_ADDRESS = 0;
private static final int POS_AMOUNT = 1;
private static final int POS_CONFIRM = 2;
private static final int POS_SUCCESS = 3;
private int numPages = 3;
SparseArray<WeakReference<SendWizardFragment>> myFragments = new SparseArray<>();
public SpendPagerAdapter(FragmentManager fm) {
super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
}
public void addSuccess() {
numPages++;
notifyDataSetChanged();
}
@Override
public int getCount() {
return numPages;
}
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
Timber.d("instantiateItem %d", position);
SendWizardFragment fragment = (SendWizardFragment) super.instantiateItem(container, position);
myFragments.put(position, new WeakReference<>(fragment));
return fragment;
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
Timber.d("destroyItem %d", position);
myFragments.remove(position);
super.destroyItem(container, position, object);
}
public SendWizardFragment getFragment(int position) {
WeakReference<SendWizardFragment> ref = myFragments.get(position);
if (ref != null)
return myFragments.get(position).get();
else
return null;
}
@NonNull
@Override
public SendWizardFragment getItem(int position) {
Timber.d("getItem(%d) CREATE", position);
Timber.d("Mode=%s", mode.toString());
if (mode == Mode.XMR) {
switch (position) {
case POS_ADDRESS:
return SendAddressWizardFragment.newInstance(SendFragment.this);
case POS_AMOUNT:
return SendAmountWizardFragment.newInstance(SendFragment.this);
case POS_CONFIRM:
return SendConfirmWizardFragment.newInstance(SendFragment.this);
case POS_SUCCESS:
return SendSuccessWizardFragment.newInstance(SendFragment.this);
default:
throw new IllegalArgumentException("no such send position(" + position + ")");
}
} else if (mode == Mode.BTC) {
switch (position) {
case POS_ADDRESS:
return SendAddressWizardFragment.newInstance(SendFragment.this);
case POS_AMOUNT:
return SendBtcAmountWizardFragment.newInstance(SendFragment.this);
case POS_CONFIRM:
return SendBtcConfirmWizardFragment.newInstance(SendFragment.this);
case POS_SUCCESS:
return SendBtcSuccessWizardFragment.newInstance(SendFragment.this);
default:
throw new IllegalArgumentException("no such send position(" + position + ")");
}
} else {
throw new IllegalStateException("Unknown mode!");
}
}
@Override
public CharSequence getPageTitle(int position) {
Timber.d("getPageTitle(%d)", position);
if (position >= numPages) return null;
switch (position) {
case POS_ADDRESS:
return getString(R.string.send_address_title);
case POS_AMOUNT:
return getString(R.string.send_amount_title);
case POS_CONFIRM:
return getString(R.string.send_confirm_title);
case POS_SUCCESS:
return getString(R.string.send_success_title);
default:
return null;
}
}
@Override
public int getItemPosition(@NonNull Object object) {
Timber.d("getItemPosition %s", String.valueOf(object));
if (object instanceof SendAddressWizardFragment) {
// keep these pages
return POSITION_UNCHANGED;
} else {
return POSITION_NONE;
}
}
}
@Override
public TxData getTxData() {
return txData;
}
private TxData txData = new TxData();
private BarcodeData barcodeData;
// Listeners
@Override
public void setBarcodeData(BarcodeData data) {
barcodeData = data;
}
@Override
public BarcodeData getBarcodeData() {
return barcodeData;
}
@Override
public BarcodeData popBarcodeData() {
Timber.d("POPPED");
BarcodeData data = barcodeData;
barcodeData = null;
return data;
}
boolean isComitted() {
return committedTx != null;
}
PendingTx committedTx;
@Override
public PendingTx getCommittedTx() {
return committedTx;
}
@Override
public void commitTransaction() {
Timber.d("REALLY SEND");
disableNavigation(); // committed - disable all navigation
activityCallback.onSend(txData.getUserNotes());
committedTx = pendingTx;
}
void disableNavigation() {
spendViewPager.allowSwipe(false);
}
void enableNavigation() {
spendViewPager.allowSwipe(true);
}
@Override
public void enableDone() {
llNavBar.setVisibility(View.INVISIBLE);
bDone.setVisibility(View.VISIBLE);
}
public Listener getActivityCallback() {
return activityCallback;
}
// callbacks from send service
public void onTransactionCreated(final String txTag, final PendingTransaction pendingTransaction) {
final SendConfirm confirm = getSendConfirm();
if (confirm != null) {
pendingTx = new PendingTx(pendingTransaction);
confirm.transactionCreated(txTag, pendingTransaction);
} else {
// not in confirm fragment => dispose & move on
disposeTransaction();
}
}
@Override
public void disposeTransaction() {
pendingTx = null;
activityCallback.onDisposeRequest();
}
PendingTx pendingTx;
public PendingTx getPendingTx() {
return pendingTx;
}
public void onCreateTransactionFailed(String errorText) {
final SendConfirm confirm = getSendConfirm();
if (confirm != null) {
confirm.createTransactionFailed(errorText);
}
}
SendConfirm getSendConfirm() {
final SendWizardFragment fragment = pagerAdapter.getFragment(SpendPagerAdapter.POS_CONFIRM);
if (fragment instanceof SendConfirm) {
return (SendConfirm) fragment;
} else {
return null;
}
}
public void onTransactionSent(final String txId) {
Timber.d("txid=%s", txId);
pagerAdapter.addSuccess();
Timber.d("numPages=%d", spendViewPager.getAdapter().getCount());
activityCallback.setToolbarButton(Toolbar.BUTTON_NONE);
spendViewPager.setCurrentItem(SpendPagerAdapter.POS_SUCCESS);
}
public void onSendTransactionFailed(final String error) {
Timber.d("error=%s", error);
committedTx = null;
final SendConfirm confirm = getSendConfirm();
if (confirm != null) {
confirm.sendFailed(getString(R.string.status_transaction_failed, error));
}
enableNavigation();
}
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
final MaterialContainerTransform transform = new MaterialContainerTransform();
transform.setDrawingViewId(R.id.fragment_container);
transform.setDuration(getResources().getInteger(R.integer.tx_item_transition_duration));
transform.setAllContainerColors(ThemeHelper.getThemedColor(getContext(), android.R.attr.colorBackground));
setSharedElementEnterTransition(transform);
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.send_menu, menu);
super.onCreateOptionsMenu(menu, inflater);
}
}

View file

@ -0,0 +1,128 @@
/*
* 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.fragment.send;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.TextView;
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;
public class SendSuccessWizardFragment extends SendWizardFragment {
public static SendSuccessWizardFragment newInstance(Listener listener) {
SendSuccessWizardFragment instance = new SendSuccessWizardFragment();
instance.setSendListener(listener);
return instance;
}
Listener sendListener;
public SendSuccessWizardFragment setSendListener(Listener listener) {
this.sendListener = listener;
return this;
}
interface Listener {
TxData getTxData();
PendingTx getCommittedTx();
void enableDone();
SendFragment.Mode getMode();
SendFragment.Listener getActivityCallback();
}
ImageButton bCopyTxId;
private TextView tvTxId;
private TextView tvTxAddress;
private TextView tvTxPaymentId;
private TextView tvTxAmount;
private TextView tvTxFee;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Timber.d("onCreateView() %s", (String.valueOf(savedInstanceState)));
View view = inflater.inflate(
R.layout.fragment_send_success, container, false);
bCopyTxId = view.findViewById(R.id.bCopyTxId);
bCopyTxId.setEnabled(false);
bCopyTxId.setOnClickListener(v -> {
Helper.clipBoardCopy(getActivity(), getString(R.string.label_send_txid), tvTxId.getText().toString());
Toast.makeText(getActivity(), getString(R.string.message_copy_txid), Toast.LENGTH_SHORT).show();
});
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;
}
@Override
public boolean onValidateFields() {
return true;
}
@Override
public void onPauseFragment() {
super.onPauseFragment();
}
@Override
public void onResumeFragment() {
super.onResumeFragment();
Timber.d("onResumeFragment()");
Helper.hideKeyboard(getActivity());
final TxData txData = sendListener.getTxData();
tvTxAddress.setText(txData.getDestinationAddress());
final PendingTx committedTx = sendListener.getCommittedTx();
if (committedTx != null) {
tvTxId.setText(committedTx.txId);
bCopyTxId.setEnabled(true);
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

@ -0,0 +1,36 @@
/*
* 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.fragment.send;
import androidx.fragment.app.Fragment;
import com.m2049r.xmrwallet.layout.SpendViewPager;
abstract public class SendWizardFragment extends Fragment
implements SpendViewPager.OnValidateFieldsListener {
@Override
public boolean onValidateFields() {
return true;
}
public void onPauseFragment() {
}
public void onResumeFragment() {
}
}

View file

@ -0,0 +1,46 @@
/*
* Copyright (c) 2021 yorha-0x
*
* 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 androidx.recyclerview.widget.DiffUtil;
import java.util.List;
public abstract class DiffCallback<T> extends DiffUtil.Callback {
protected final List<T> mOldList;
protected final List<T> mNewList;
public DiffCallback(List<T> oldList, List<T> newList) {
this.mOldList = oldList;
this.mNewList = newList;
}
@Override
public int getOldListSize() {
return mOldList.size();
}
@Override
public int getNewListSize() {
return mNewList.size();
}
public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition);
public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition);
}

View file

@ -0,0 +1,261 @@
/*
* 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.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.fragment.app.FragmentActivity;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.NodeInfo;
import com.m2049r.xmrwallet.dialog.HelpFragment;
import com.m2049r.xmrwallet.util.NetCipherHelper;
import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
public class NodeInfoAdapter extends RecyclerView.Adapter<NodeInfoAdapter.ViewHolder> {
public interface OnInteractionListener {
void onInteraction(View view, NodeInfo item);
boolean onLongInteraction(View view, NodeInfo item);
}
private final List<NodeInfo> nodeItems = new ArrayList<>();
private final OnInteractionListener listener;
private final FragmentActivity activity;
public NodeInfoAdapter(FragmentActivity activity, OnInteractionListener listener) {
this.activity = activity;
this.listener = listener;
}
public void notifyItemChanged(NodeInfo nodeInfo) {
final int pos = nodeItems.indexOf(nodeInfo);
if (pos >= 0) notifyItemChanged(pos);
}
private static class NodeDiff extends DiffCallback<NodeInfo> {
public NodeDiff(List<NodeInfo> oldList, List<NodeInfo> newList) {
super(oldList, newList);
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return mOldList.get(oldItemPosition).equals(mNewList.get(newItemPosition));
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
final NodeInfo oldItem = mOldList.get(oldItemPosition);
final NodeInfo newItem = mNewList.get(newItemPosition);
return (oldItem.getTimestamp() == newItem.getTimestamp())
&& (oldItem.isTested() == newItem.isTested())
&& (oldItem.isValid() == newItem.isValid())
&& (oldItem.getResponseTime() == newItem.getResponseTime())
&& (oldItem.isSelected() == newItem.isSelected())
&& (oldItem.getName().equals(newItem.getName()));
}
}
@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) {
List<NodeInfo> newItems = new ArrayList<>(nodeItems);
if (!nodeItems.contains(node))
newItems.add(node);
setNodes(newItems); // in case the nodeinfo has changed
}
public void setNodes(Collection<NodeInfo> newItemsCollection) {
List<NodeInfo> newItems;
if (newItemsCollection != null) {
newItems = new ArrayList<>(newItemsCollection);
Collections.sort(newItems, NodeInfo.BestNodeComparator);
} else {
newItems = new ArrayList<>();
}
final NodeInfoAdapter.NodeDiff diffCallback = new NodeInfoAdapter.NodeDiff(nodeItems, newItems);
final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback);
nodeItems.clear();
nodeItems.addAll(newItems);
diffResult.dispatchUpdatesTo(this);
}
public void setNodes() {
setNodes(nodeItems);
}
private boolean itemsClickable = true;
public void allowClick(boolean clickable) {
itemsClickable = clickable;
notifyDataSetChanged();
}
class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
final ImageButton ibBookmark;
final View pbBookmark;
final TextView tvName;
final TextView tvInfo;
final ImageView ivPing;
NodeInfo nodeItem;
ViewHolder(View itemView) {
super(itemView);
ibBookmark = itemView.findViewById(R.id.ibBookmark);
pbBookmark = itemView.findViewById(R.id.pbBookmark);
tvName = itemView.findViewById(R.id.tvName);
tvInfo = itemView.findViewById(R.id.tvInfo);
ivPing = itemView.findViewById(R.id.ivPing);
ibBookmark.setOnClickListener(v -> {
nodeItem.toggleFavourite();
showStar();
if (!nodeItem.isFavourite()) {
nodeItem.setSelected(false);
setNodes(nodeItems);
}
});
itemView.setOnClickListener(this);
itemView.setOnLongClickListener(this);
}
private void showStar() {
if (nodeItem.isFavourite()) {
ibBookmark.setImageResource(R.drawable.ic_favorite_24dp);
} else {
ibBookmark.setImageResource(R.drawable.ic_favorite_border_24dp);
}
}
void bind(int position) {
nodeItem = nodeItems.get(position);
tvName.setText(nodeItem.getName());
ivPing.setImageResource(getPingIcon(nodeItem));
if (nodeItem.isTested()) {
if (nodeItem.isValid()) {
nodeItem.showInfo(tvInfo);
} else {
nodeItem.showInfo(tvInfo, getResponseErrorText(activity, nodeItem.getResponseCode()), true);
}
} else {
nodeItem.showInfo(tvInfo);
}
itemView.setSelected(nodeItem.isSelected());
itemView.setClickable(itemsClickable);
itemView.setEnabled(itemsClickable);
ibBookmark.setClickable(itemsClickable);
pbBookmark.setVisibility(nodeItem.isSelecting() ? View.VISIBLE : View.INVISIBLE);
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
final NodeInfo node = nodeItems.get(position);
if (node.isOnion()) {
switch (NetCipherHelper.getStatus()) {
case NOT_INSTALLED:
HelpFragment.display(activity.getSupportFragmentManager(), R.string.help_tor);
return;
case DISABLED:
HelpFragment.display(activity.getSupportFragmentManager(), R.string.help_tor_enable);
return;
}
}
node.setSelecting(true);
allowClick(false);
listener.onInteraction(view, node);
}
}
}
@Override
public boolean onLongClick(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
return listener.onLongInteraction(view, nodeItems.get(position));
}
}
return false;
}
}
static public int getPingIcon(NodeInfo nodeInfo) {
if (nodeInfo.isUnauthorized()) {
return R.drawable.ic_wifi_lock;
}
if (nodeInfo.isValid()) {
final double ping = nodeInfo.getResponseTime();
if (ping < NodeInfo.PING_GOOD) {
return R.drawable.ic_wifi_4_bar;
} else if (ping < NodeInfo.PING_MEDIUM) {
return R.drawable.ic_wifi_3_bar;
} else if (ping < NodeInfo.PING_BAD) {
return R.drawable.ic_wifi_2_bar;
} else {
return R.drawable.ic_wifi_1_bar;
}
} else {
return R.drawable.ic_wifi_off;
}
}
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 if (responseCode == 418) {
return ctx.getResources().getString(R.string.node_tor_error);
} else {
return ctx.getResources().getString(R.string.node_test_error, responseCode);
}
}
}

View file

@ -0,0 +1,73 @@
/*
* 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.layout;
import android.content.Context;
import androidx.viewpager.widget.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
import com.m2049r.xmrwallet.fragment.send.SendFragment;
public class SpendViewPager extends ViewPager {
public interface OnValidateFieldsListener {
boolean onValidateFields();
}
public SpendViewPager(Context context) {
super(context);
}
public SpendViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void next() {
int pos = getCurrentItem();
if (validateFields(pos)) {
setCurrentItem(pos + 1);
}
}
public void previous() {
setCurrentItem(getCurrentItem() - 1);
}
private boolean allowSwipe = true;
public void allowSwipe(boolean allow) {
allowSwipe = allow;
}
public boolean validateFields(int position) {
OnValidateFieldsListener c = ((SendFragment.SpendPagerAdapter) getAdapter()).getFragment(position);
return c.onValidateFields();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (allowSwipe) return super.onInterceptTouchEvent(event);
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (allowSwipe) return super.onTouchEvent(event);
return false;
}
}

View file

@ -0,0 +1,164 @@
/*
* 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.layout;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.Subaddress;
import com.m2049r.xmrwallet.util.Helper;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import timber.log.Timber;
public class SubaddressInfoAdapter extends RecyclerView.Adapter<SubaddressInfoAdapter.ViewHolder> {
public interface OnInteractionListener {
void onInteraction(View view, Subaddress item);
boolean onLongInteraction(View view, Subaddress item);
}
private final List<Subaddress> items;
private final OnInteractionListener listener;
Context context;
public SubaddressInfoAdapter(Context context, OnInteractionListener listener) {
this.context = context;
this.items = new ArrayList<>();
this.listener = listener;
}
private static class SubaddressInfoDiff extends DiffCallback<Subaddress> {
public SubaddressInfoDiff(List<Subaddress> oldList, List<Subaddress> newList) {
super(oldList, newList);
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return mOldList.get(oldItemPosition).getAddress().equals(mNewList.get(newItemPosition).getAddress());
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return mOldList.get(oldItemPosition).equals(mNewList.get(newItemPosition));
}
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_subaddress, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(final ViewHolder holder, int position) {
holder.bind(position);
}
@Override
public int getItemCount() {
return items.size();
}
public Subaddress getItem(int position) {
return items.get(position);
}
public void setInfos(List<Subaddress> newItems) {
if (newItems == null) {
newItems = new ArrayList<>();
Timber.d("setInfos null");
} else {
Timber.d("setInfos %s", newItems.size());
}
Collections.sort(newItems);
final DiffCallback<Subaddress> diffCallback = new SubaddressInfoDiff(items, newItems);
final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback);
items.clear();
items.addAll(newItems);
diffResult.dispatchUpdatesTo(this);
}
class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
final TextView tvName;
final TextView tvAddress;
final TextView tvAmount;
Subaddress item;
ViewHolder(View itemView) {
super(itemView);
tvName = itemView.findViewById(R.id.tvName);
tvAddress = itemView.findViewById(R.id.tvAddress);
tvAmount = itemView.findViewById(R.id.tx_amount);
itemView.setOnClickListener(this);
itemView.setOnLongClickListener(this);
}
void bind(int position) {
item = getItem(position);
itemView.setTransitionName(context.getString(R.string.subaddress_item_transition_name, item.getAddressIndex()));
final String label = item.getDisplayLabel();
final String address = context.getString(R.string.subbaddress_info_subtitle,
item.getAddressIndex(), item.getSquashedAddress());
tvName.setText(label.isEmpty() ? address : label);
tvAddress.setText(address);
final long amount = item.getAmount();
if (amount > 0)
tvAmount.setText(context.getString(R.string.tx_list_amount_positive,
Helper.getDisplayAmount(amount, Helper.DISPLAY_DIGITS_INFO)));
else
tvAmount.setText("");
}
@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, getItem(position));
}
}
}
@Override
public boolean onLongClick(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
return listener.onLongInteraction(view, getItem(position));
}
}
return true;
}
}
}

View file

@ -0,0 +1,278 @@
/*
* 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.layout;
import android.content.Context;
import android.text.Html;
import android.text.Spanned;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.progressindicator.CircularProgressIndicator;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.Crypto;
import com.m2049r.xmrwallet.data.UserNotes;
import com.m2049r.xmrwallet.model.TransactionInfo;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.ThemeHelper;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.TimeZone;
import timber.log.Timber;
public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfoAdapter.ViewHolder> {
private final static SimpleDateFormat DATETIME_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm");
private final int outboundColour;
private final int inboundColour;
private final int pendingColour;
private final int failedColour;
public interface OnInteractionListener {
void onInteraction(View view, TransactionInfo item);
}
private final List<TransactionInfo> infoItems;
private final OnInteractionListener listener;
private final Context context;
public TransactionInfoAdapter(Context context, OnInteractionListener listener) {
this.context = context;
inboundColour = ThemeHelper.getThemedColor(context, R.attr.positiveColor);
outboundColour = ThemeHelper.getThemedColor(context, R.attr.negativeColor);
pendingColour = ThemeHelper.getThemedColor(context, R.attr.neutralColor);
failedColour = ThemeHelper.getThemedColor(context, R.attr.neutralColor);
infoItems = new ArrayList<>();
this.listener = listener;
Calendar cal = Calendar.getInstance();
TimeZone tz = cal.getTimeZone(); //get the local time zone.
DATETIME_FORMATTER.setTimeZone(tz);
}
public boolean needsTransactionUpdateOnNewBlock() {
return (infoItems.size() > 0) && !infoItems.get(0).isConfirmed();
}
private static class TransactionInfoDiff extends DiffCallback<TransactionInfo> {
public TransactionInfoDiff(List<TransactionInfo> oldList, List<TransactionInfo> newList) {
super(oldList, newList);
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return mOldList.get(oldItemPosition).hash.equals(mNewList.get(newItemPosition).hash);
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
final TransactionInfo oldItem = mOldList.get(oldItemPosition);
final TransactionInfo newItem = mNewList.get(newItemPosition);
return (oldItem.direction == newItem.direction)
&& (oldItem.isPending == newItem.isPending)
&& (oldItem.isFailed == newItem.isFailed)
&& ((oldItem.confirmations == newItem.confirmations) || (oldItem.isConfirmed()))
&& (oldItem.subaddressLabel.equals(newItem.subaddressLabel))
&& (Objects.equals(oldItem.notes, newItem.notes));
}
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_transaction, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(final ViewHolder holder, int position) {
holder.bind(position);
}
@Override
public int getItemCount() {
return infoItems.size();
}
public void setInfos(List<TransactionInfo> newItems) {
if (newItems == null) {
newItems = new ArrayList<>();
Timber.d("setInfos null");
} else {
Timber.d("setInfos %s", newItems.size());
}
Collections.sort(newItems);
final DiffCallback<TransactionInfo> diffCallback = new TransactionInfoAdapter.TransactionInfoDiff(infoItems, newItems);
final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback);
infoItems.clear();
infoItems.addAll(newItems);
diffResult.dispatchUpdatesTo(this);
}
public void removeItem(int position) {
List<TransactionInfo> newItems = new ArrayList<>(infoItems);
if (newItems.size() > position)
newItems.remove(position);
setInfos(newItems); // in case the nodeinfo has changed
}
public TransactionInfo getItem(int position) {
return infoItems.get(position);
}
class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
final ImageView ivTxType;
final TextView tvAmount;
final TextView tvFailed;
final TextView tvPaymentId;
final TextView tvDateTime;
final CircularProgressIndicator pbConfirmations;
final TextView tvConfirmations;
TransactionInfo infoItem;
ViewHolder(View itemView) {
super(itemView);
ivTxType = itemView.findViewById(R.id.ivTxType);
tvAmount = itemView.findViewById(R.id.tx_amount);
tvFailed = itemView.findViewById(R.id.tx_failed);
tvPaymentId = itemView.findViewById(R.id.tx_paymentid);
tvDateTime = itemView.findViewById(R.id.tx_datetime);
pbConfirmations = itemView.findViewById(R.id.pbConfirmations);
pbConfirmations.setMax(TransactionInfo.CONFIRMATION);
tvConfirmations = itemView.findViewById(R.id.tvConfirmations);
}
private String getDateTime(long time) {
return DATETIME_FORMATTER.format(new Date(time * 1000));
}
private void setTxColour(int clr) {
tvAmount.setTextColor(clr);
}
void bind(int position) {
infoItem = infoItems.get(position);
itemView.setTransitionName(context.getString(R.string.tx_item_transition_name, infoItem.hash));
UserNotes userNotes = new UserNotes(infoItem.notes);
if (userNotes.xmrtoKey != null) {
final Crypto crypto = Crypto.withSymbol(userNotes.xmrtoCurrency);
if (crypto != null) {
ivTxType.setImageResource(crypto.getIconEnabledId());
ivTxType.setVisibility(View.VISIBLE);
} else {// otherwirse pretend we don't know it's a shift
ivTxType.setVisibility(View.GONE);
}
} else {
ivTxType.setVisibility(View.GONE);
}
String displayAmount = Helper.getDisplayAmount(infoItem.amount, Helper.DISPLAY_DIGITS_INFO);
if (infoItem.direction == TransactionInfo.Direction.Direction_Out) {
tvAmount.setText(context.getString(R.string.tx_list_amount_negative, displayAmount));
} else {
tvAmount.setText(context.getString(R.string.tx_list_amount_positive, displayAmount));
}
tvFailed.setVisibility(View.GONE);
if (infoItem.isFailed) {
this.tvAmount.setText(context.getString(R.string.tx_list_amount_failed, displayAmount));
tvFailed.setVisibility(View.VISIBLE);
setTxColour(failedColour);
pbConfirmations.setVisibility(View.GONE);
tvConfirmations.setVisibility(View.GONE);
} else if (infoItem.isPending) {
setTxColour(pendingColour);
pbConfirmations.setVisibility(View.GONE);
pbConfirmations.setIndeterminate(true);
pbConfirmations.setVisibility(View.VISIBLE);
tvConfirmations.setVisibility(View.GONE);
} else if (infoItem.direction == TransactionInfo.Direction.Direction_In) {
setTxColour(inboundColour);
if (!infoItem.isConfirmed()) {
pbConfirmations.setVisibility(View.VISIBLE);
final int confirmations = (int) infoItem.confirmations;
pbConfirmations.setProgressCompat(confirmations, true);
final String confCount = Integer.toString(confirmations);
tvConfirmations.setText(confCount);
if (confCount.length() == 1) // we only have space for character in the progress circle
tvConfirmations.setVisibility(View.VISIBLE);
else
tvConfirmations.setVisibility(View.GONE);
} else {
pbConfirmations.setVisibility(View.GONE);
tvConfirmations.setVisibility(View.GONE);
}
} else {
setTxColour(outboundColour);
pbConfirmations.setVisibility(View.GONE);
tvConfirmations.setVisibility(View.GONE);
}
String tag = null;
String info = "";
if ((infoItem.addressIndex != 0) && (infoItem.direction == TransactionInfo.Direction.Direction_In))
tag = infoItem.getDisplayLabel();
if ((userNotes.note.isEmpty())) {
if (!infoItem.paymentId.equals("0000000000000000")) {
info = infoItem.paymentId;
}
} else {
info = userNotes.note;
}
if (tag == null) {
tvPaymentId.setText(info);
} else {
Spanned label = Html.fromHtml(context.getString(R.string.tx_details_notes,
Integer.toHexString(ThemeHelper.getThemedColor(context, R.attr.positiveColor) & 0xFFFFFF),
Integer.toHexString(ThemeHelper.getThemedColor(context, android.R.attr.colorBackground) & 0xFFFFFF),
tag, info.isEmpty() ? "" : ("&nbsp; " + info)));
tvPaymentId.setText(label);
}
this.tvDateTime.setText(getDateTime(infoItem.timestamp));
itemView.setOnClickListener(this);
}
@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, infoItems.get(position));
}
}
}
}
}

View file

@ -0,0 +1,174 @@
/*
* 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.layout;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.PopupMenu;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.model.WalletManager;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import timber.log.Timber;
public class WalletInfoAdapter extends RecyclerView.Adapter<WalletInfoAdapter.ViewHolder> {
private final SimpleDateFormat DATETIME_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm");
public interface OnInteractionListener {
void onInteraction(View view, WalletManager.WalletInfo item);
boolean onContextInteraction(MenuItem item, WalletManager.WalletInfo infoItem);
}
private final List<WalletManager.WalletInfo> infoItems;
private final OnInteractionListener listener;
Context context;
public WalletInfoAdapter(Context context, OnInteractionListener listener) {
this.context = context;
this.infoItems = new ArrayList<>();
this.listener = listener;
Calendar cal = Calendar.getInstance();
TimeZone tz = cal.getTimeZone(); //get the local time zone.
DATETIME_FORMATTER.setTimeZone(tz);
}
private static class WalletInfoDiff extends DiffCallback<WalletManager.WalletInfo> {
public WalletInfoDiff(List<WalletManager.WalletInfo> oldList, List<WalletManager.WalletInfo> newList) {
super(oldList, newList);
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return mOldList.get(oldItemPosition).getName().equals(mNewList.get(newItemPosition).getName());
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return mOldList.get(oldItemPosition).compareTo(mNewList.get(newItemPosition)) == 0;
}
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new ViewHolder(
LayoutInflater.from(parent.getContext()).inflate(R.layout.item_wallet, parent, false)
);
}
@Override
public void onBindViewHolder(final ViewHolder holder, int position) {
holder.bind(position);
}
@Override
public int getItemCount() {
return infoItems.size();
}
public WalletManager.WalletInfo getItem(int position) {
return infoItems.get(position);
}
public void setInfos(List<WalletManager.WalletInfo> newItems) {
if (newItems == null) {
newItems = new ArrayList<>();
Timber.d("setInfos null");
} else {
Timber.d("setInfos %s", newItems.size());
}
Collections.sort(newItems);
final DiffCallback<WalletManager.WalletInfo> diffCallback = new WalletInfoAdapter.WalletInfoDiff(infoItems, newItems);
final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback);
infoItems.clear();
infoItems.addAll(newItems);
diffResult.dispatchUpdatesTo(this);
}
class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
final TextView tvName;
final ImageButton ibOptions;
WalletManager.WalletInfo infoItem;
boolean popupOpen = false;
ViewHolder(View itemView) {
super(itemView);
tvName = itemView.findViewById(R.id.tvName);
ibOptions = itemView.findViewById(R.id.ibOptions);
ibOptions.setOnClickListener(view -> {
if (popupOpen) return;
//creating a popup menu
PopupMenu popup = new PopupMenu(context, ibOptions);
//inflating menu from xml resource
popup.inflate(R.menu.list_context_menu);
popupOpen = true;
//adding click listener
popup.setOnMenuItemClickListener(item -> {
if (listener != null) {
return listener.onContextInteraction(item, infoItem);
}
return false;
});
//displaying the popup
popup.show();
popup.setOnDismissListener(menu -> popupOpen = false);
});
itemView.setOnClickListener(this);
}
private String getDateTime(long time) {
return DATETIME_FORMATTER.format(new Date(time * 1000));
}
void bind(int position) {
infoItem = infoItems.get(position);
tvName.setText(infoItem.getName());
}
@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, infoItems.get(position));
}
}
}
}
}

View file

@ -0,0 +1,155 @@
/*
* 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.ledger;
public enum Instruction {
INS_NONE(0x00),
INS_RESET(0x02),
INS_GET_KEY(0x20),
INS_DISPLAY_ADDRESS(0x21),
INS_PUT_KEY(0x22),
INS_GET_CHACHA8_PREKEY(0x24),
INS_VERIFY_KEY(0x26),
INS_MANAGE_SEEDWORDS(0x28),
INS_SECRET_KEY_TO_PUBLIC_KEY(0x30),
INS_GEN_KEY_DERIVATION(0x32),
INS_DERIVATION_TO_SCALAR(0x34),
INS_DERIVE_PUBLIC_KEY(0x36),
INS_DERIVE_SECRET_KEY(0x38),
INS_GEN_KEY_IMAGE(0x3A),
INS_SECRET_KEY_ADD(0x3C),
INS_SECRET_KEY_SUB(0x3E),
INS_GENERATE_KEYPAIR(0x40),
INS_SECRET_SCAL_MUL_KEY(0x42),
INS_SECRET_SCAL_MUL_BASE(0x44),
INS_DERIVE_SUBADDRESS_PUBLIC_KEY(0x46),
INS_GET_SUBADDRESS(0x48),
INS_GET_SUBADDRESS_SPEND_PUBLIC_KEY(0x4A),
INS_GET_SUBADDRESS_SECRET_KEY(0x4C),
INS_OPEN_TX(0x70),
INS_SET_SIGNATURE_MODE(0x72),
INS_GET_ADDITIONAL_KEY(0x74),
INS_STEALTH(0x76),
INS_GEN_COMMITMENT_MASK(0x77),
INS_BLIND(0x78),
INS_UNBLIND(0x7A),
INS_GEN_TXOUT_KEYS(0x7B),
INS_VALIDATE(0x7C),
INS_PREFIX_HASH(0x7D),
INS_MLSAG(0x7E),
INS_CLOSE_TX(0x80),
INS_GET_TX_PROOF(0xA0),
INS_GET_RESPONSE(0xC0),
INS_UNDEFINED(0xFF);
public static Instruction fromByte(byte n) {
switch (n & 0xFF) {
case 0x00:
return INS_NONE;
case 0x02:
return INS_RESET;
case 0x20:
return INS_GET_KEY;
case 0x22:
return INS_PUT_KEY;
case 0x24:
return INS_GET_CHACHA8_PREKEY;
case 0x26:
return INS_VERIFY_KEY;
case 0x30:
return INS_SECRET_KEY_TO_PUBLIC_KEY;
case 0x32:
return INS_GEN_KEY_DERIVATION;
case 0x34:
return INS_DERIVATION_TO_SCALAR;
case 0x36:
return INS_DERIVE_PUBLIC_KEY;
case 0x38:
return INS_DERIVE_SECRET_KEY;
case 0x3A:
return INS_GEN_KEY_IMAGE;
case 0x3C:
return INS_SECRET_KEY_ADD;
case 0x3E:
return INS_SECRET_KEY_SUB;
case 0x40:
return INS_GENERATE_KEYPAIR;
case 0x42:
return INS_SECRET_SCAL_MUL_KEY;
case 0x44:
return INS_SECRET_SCAL_MUL_BASE;
case 0x46:
return INS_DERIVE_SUBADDRESS_PUBLIC_KEY;
case 0x48:
return INS_GET_SUBADDRESS;
case 0x4A:
return INS_GET_SUBADDRESS_SPEND_PUBLIC_KEY;
case 0x4C:
return INS_GET_SUBADDRESS_SECRET_KEY;
case 0x70:
return INS_OPEN_TX;
case 0x72:
return INS_SET_SIGNATURE_MODE;
case 0x74:
return INS_GET_ADDITIONAL_KEY;
case 0x76:
return INS_STEALTH;
case 0x78:
return INS_BLIND;
case 0x7A:
return INS_UNBLIND;
case 0x7C:
return INS_VALIDATE;
case 0x7E:
return INS_MLSAG;
case 0x80:
return INS_CLOSE_TX;
case 0xc0:
return INS_GET_RESPONSE;
default:
return INS_UNDEFINED;
}
}
public int getValue() {
return value;
}
public byte getByteValue() {
return (byte) (value & 0xFF);
}
private int value;
Instruction(int value) {
this.value = value;
}
}

View file

@ -0,0 +1,240 @@
/*
*******************************************************************************
* BTChip Bitcoin Hardware Wallet Java API
* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn
* 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.ledger;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
import com.btchip.BTChipException;
import com.btchip.comm.BTChipTransport;
import com.btchip.comm.android.BTChipTransportAndroidHID;
import com.m2049r.xmrwallet.BuildConfig;
import com.m2049r.xmrwallet.model.WalletManager;
import com.m2049r.xmrwallet.util.Helper;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import timber.log.Timber;
public class Ledger {
static final public boolean ENABLED = true;
// 5:20 is same as wallet2.cpp::restore()
static public final int LOOKAHEAD_ACCOUNTS = 5;
static public final int LOOKAHEAD_SUBADDRESSES = 20;
static public final String SUBADDRESS_LOOKAHEAD = LOOKAHEAD_ACCOUNTS + ":" + LOOKAHEAD_SUBADDRESSES;
private static final byte PROTOCOL_VERSION = 0x03;
public static final int SW_OK = 0x9000;
public static final int SW_INS_NOT_SUPPORTED = 0x6D00;
public static final int OK[] = {SW_OK};
public static final int MINIMUM_LEDGER_VERSION = (1 << 16) + (8 << 8) + (0); // 1.6.0
public static UsbDevice findDevice(UsbManager usbManager) {
if (!ENABLED) return null;
return BTChipTransportAndroidHID.getDevice(usbManager);
}
static private Ledger Instance = null;
static public String connect(UsbManager usbManager, UsbDevice usbDevice) throws IOException {
if (Instance != null) {
disconnect();
}
Instance = new Ledger(usbManager, usbDevice);
return Name();
}
static public void disconnect() {
// this is not synchronized so as to close immediately
if (Instance != null) {
Instance.close();
Instance = null;
}
}
static public boolean isConnected() {
//TODO synchronize with connect/disconnect?
return Instance != null;
}
static public String Name() {
if (Instance != null) {
return Instance.name;
} else {
return null;
}
}
static public byte[] Exchange(byte[] apdu) {
if (Instance != null) {
Timber.d("INS: %s", Instruction.fromByte(apdu[1]));
return Instance.exchangeRaw(apdu);
} else {
return null;
}
}
static public boolean check() {
if (Instance == null) return false;
byte[] moneroVersion = WalletManager.moneroVersion().getBytes(StandardCharsets.US_ASCII);
try {
byte[] resp = Instance.exchangeApduNoOpt(Instruction.INS_RESET, moneroVersion, OK);
int deviceVersion = (resp[0] << 16) + (resp[1] << 8) + (resp[2]);
if (deviceVersion < MINIMUM_LEDGER_VERSION)
return false;
} catch (BTChipException ex) { // comm error - probably wrong app started on device
return false;
}
return true;
}
final private BTChipTransport transport;
final private String name;
private int lastSW = 0;
private Ledger(UsbManager usbManager, UsbDevice usbDevice) throws IOException {
final BTChipTransport transport = BTChipTransportAndroidHID.open(usbManager, usbDevice);
Timber.d("transport opened = %s", transport.toString());
transport.setDebug(BuildConfig.DEBUG);
this.transport = transport;
this.name = usbDevice.getManufacturerName() + " " + usbDevice.getProductName();
initKey();
}
synchronized private void close() {
initKey(); // don't leak key after we disconnect
transport.close();
Timber.d("transport closed");
lastSW = 0;
}
synchronized private byte[] exchangeRaw(byte[] apdu) {
if (transport == null)
throw new IllegalStateException("No transport (probably closed previously)");
Timber.d("exchangeRaw %02x", apdu[1]);
Instruction ins = Instruction.fromByte(apdu[1]);
if (listener != null) listener.onInstructionSend(ins, apdu);
sniffOut(ins, apdu);
byte[] data = transport.exchange(apdu);
if (listener != null) listener.onInstructionReceive(ins, data);
sniffIn(data);
return data;
}
private byte[] exchange(byte[] apdu) throws BTChipException {
byte[] response = exchangeRaw(apdu);
if (response.length < 2) {
throw new BTChipException("Truncated response");
}
lastSW = ((response[response.length - 2] & 0xff) << 8) |
response[response.length - 1] & 0xff;
byte[] result = new byte[response.length - 2];
System.arraycopy(response, 0, result, 0, response.length - 2);
return result;
}
private byte[] exchangeCheck(byte[] apdu, int acceptedSW[]) throws BTChipException {
byte[] response = exchange(apdu);
if (acceptedSW == null) {
return response;
}
for (int SW : acceptedSW) {
if (lastSW == SW) {
return response;
}
}
throw new BTChipException("Invalid status", lastSW);
}
private byte[] exchangeApduNoOpt(Instruction instruction, byte[] data, int acceptedSW[])
throws BTChipException {
byte[] apdu = new byte[data.length + 6];
apdu[0] = PROTOCOL_VERSION;
apdu[1] = instruction.getByteValue();
apdu[2] = 0; // p1
apdu[3] = 0; // p2
apdu[4] = (byte) (data.length + 1); // +1 because the opt byte is part of the data
apdu[5] = 0; // opt
System.arraycopy(data, 0, apdu, 6, data.length);
return exchangeCheck(apdu, acceptedSW);
}
public interface Listener {
void onInstructionSend(Instruction ins, byte[] apdu);
void onInstructionReceive(Instruction ins, byte[] data);
}
Listener listener;
static public void setListener(Listener listener) {
if (Instance != null) {
Instance.listener = listener;
}
}
static public void unsetListener(Listener listener) {
if ((Instance != null) && (Instance.listener == listener))
Instance.listener = null;
}
// very stupid hack to extract the view key
// without messing around with monero core code
// NB: as all the ledger comm can be sniffed off the USB cable - there is no security issue here
private boolean snoopKey = false;
private byte[] key;
private void initKey() {
key = Helper.hexToBytes("0000000000000000000000000000000000000000000000000000000000000000");
}
static public String Key() {
if (Instance != null) {
return Helper.bytesToHex(Instance.key).toLowerCase();
} else {
return null;
}
}
private void sniffOut(Instruction ins, byte[] apdu) {
if (ins == Instruction.INS_GET_KEY) {
snoopKey = (apdu[2] == 2);
}
}
private void sniffIn(byte[] data) {
// stupid hack to extract the view key
// without messing around with monero core code
if (snoopKey) {
if (data.length == 34) { // 32 key + result code 9000
long sw = ((data[data.length - 2] & 0xff) << 8) |
(data[data.length - 1] & 0xff);
Timber.e("WS %d", sw);
if (sw == SW_OK) {
System.arraycopy(data, 0, key, 0, 32);
}
}
snoopKey = false;
}
}
}

View file

@ -0,0 +1,183 @@
/*
* 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.ledger;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.dialog.ProgressDialog;
import timber.log.Timber;
public class LedgerProgressDialog extends ProgressDialog implements Ledger.Listener {
static public final int TYPE_DEBUG = 0;
static public final int TYPE_RESTORE = 1;
static public final int TYPE_SUBADDRESS = 2;
static public final int TYPE_ACCOUNT = 3;
static public final int TYPE_SEND = 4;
private final int type;
private Handler uiHandler = new Handler(Looper.getMainLooper());
public LedgerProgressDialog(Context context, int type) {
super(context);
this.type = type;
setCancelable(false);
if (type == TYPE_SEND)
setMessage(context.getString(R.string.info_prepare_tx));
else
setMessage(context.getString(R.string.progress_ledger_progress));
}
@Override
public void onBackPressed() {
// prevent back button
}
private int firstSubaddress = Integer.MAX_VALUE;
private boolean validate = false;
private boolean validated = false;
@Override
public void onInstructionSend(final Instruction ins, final byte[] apdu) {
Timber.d("LedgerProgressDialog SEND %s", ins);
uiHandler.post(new Runnable() {
@Override
public void run() {
if (type > TYPE_DEBUG) {
validate = false;
switch (ins) {
case INS_RESET: // ledger may ask for confirmation - maybe a bug?
case INS_GET_KEY: // ledger asks for confirmation to send keys
case INS_DISPLAY_ADDRESS:
setIndeterminate(true);
setMessage(getContext().getString(R.string.progress_ledger_confirm));
break;
case INS_GET_SUBADDRESS_SPEND_PUBLIC_KEY: // lookahead
//00 4a 00 00 09 00 01000000 30000000
// 0 1 2 3 4 5 6 7 8 9 a b c d
int account = bytesToInteger(apdu, 6);
int subaddress = bytesToInteger(apdu, 10);
Timber.d("fetching subaddress (%d, %d)", account, subaddress);
switch (type) {
case TYPE_RESTORE:
setProgress(account * Ledger.LOOKAHEAD_SUBADDRESSES + subaddress + 1,
Ledger.LOOKAHEAD_ACCOUNTS * Ledger.LOOKAHEAD_SUBADDRESSES);
setIndeterminate(false);
break;
case TYPE_ACCOUNT:
final int requestedSubaddress = account * Ledger.LOOKAHEAD_SUBADDRESSES + subaddress;
if (firstSubaddress > requestedSubaddress) {
firstSubaddress = requestedSubaddress;
}
setProgress(requestedSubaddress - firstSubaddress + 1,
Ledger.LOOKAHEAD_ACCOUNTS * Ledger.LOOKAHEAD_SUBADDRESSES);
setIndeterminate(false);
break;
case TYPE_SUBADDRESS:
if (firstSubaddress > subaddress) {
firstSubaddress = subaddress;
}
setProgress(subaddress - firstSubaddress + 1, Ledger.LOOKAHEAD_SUBADDRESSES);
setIndeterminate(false);
break;
default:
setIndeterminate(true);
break;
}
setMessage(getContext().getString(R.string.progress_ledger_lookahead));
break;
case INS_VERIFY_KEY:
setIndeterminate(true);
setMessage(getContext().getString(R.string.progress_ledger_verify));
break;
case INS_OPEN_TX:
setIndeterminate(true);
setMessage(getContext().getString(R.string.progress_ledger_opentx));
break;
case INS_MLSAG:
if (validated) {
setIndeterminate(true);
setMessage(getContext().getString(R.string.progress_ledger_mlsag));
}
break;
case INS_PREFIX_HASH:
if ((apdu[2] != 1) || (apdu[3] != 0)) break;
setIndeterminate(true);
setMessage(getContext().getString(R.string.progress_ledger_confirm));
break;
case INS_VALIDATE:
if ((apdu[2] != 1) || (apdu[3] != 1)) break;
validate = true;
uiHandler.postDelayed(new Runnable() {
@Override
public void run() {
if (validate) {
setIndeterminate(true);
setMessage(getContext().getString(R.string.progress_ledger_confirm));
validated = true;
}
}
}, 250);
break;
default:
// ignore others and maintain state
}
} else {
setMessage(ins.name());
}
}
});
}
@Override
public void onInstructionReceive(final Instruction ins, final byte[] data) {
Timber.d("LedgerProgressDialog RECV %s", ins);
uiHandler.post(new Runnable() {
@Override
public void run() {
if (type > TYPE_DEBUG) {
switch (ins) {
case INS_GET_SUBADDRESS_SPEND_PUBLIC_KEY: // lookahead
case INS_VERIFY_KEY:
case INS_GET_CHACHA8_PREKEY:
break;
default:
if (type != TYPE_SEND)
setMessage(getContext().getString(R.string.progress_ledger_progress));
}
} else {
setMessage("Returned from " + ins.name());
}
}
});
}
// TODO: we use ints in Java but the are signed; accounts & subaddresses are unsigned ...
private int bytesToInteger(byte[] bytes, int offset) {
int result = 0;
for (int i = 3; i >= 0; i--) {
result <<= 8;
result |= (bytes[offset + i] & 0xFF);
}
return result;
}
}

View file

@ -0,0 +1,45 @@
/*
* 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.model;
public enum NetworkType {
NetworkType_Mainnet(0),
NetworkType_Testnet(1),
NetworkType_Stagenet(2);
public static NetworkType fromInteger(int n) {
switch (n) {
case 0:
return NetworkType_Mainnet;
case 1:
return NetworkType_Testnet;
case 2:
return NetworkType_Stagenet;
}
return null;
}
public int getValue() {
return value;
}
private int value;
NetworkType(int value) {
this.value = value;
}
}

View file

@ -0,0 +1,98 @@
/*
* 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.model;
public class PendingTransaction {
static {
System.loadLibrary("monerujo");
}
public long handle;
PendingTransaction(long handle) {
this.handle = handle;
}
public enum Status {
Status_Ok,
Status_Error,
Status_Critical
}
public enum Priority {
Priority_Default(0),
Priority_Low(1),
Priority_Medium(2),
Priority_High(3),
Priority_Last(4);
public static Priority fromInteger(int n) {
switch (n) {
case 0:
return Priority_Default;
case 1:
return Priority_Low;
case 2:
return Priority_Medium;
case 3:
return Priority_High;
}
return null;
}
public int getValue() {
return value;
}
private int value;
Priority(int value) {
this.value = value;
}
}
public Status getStatus() {
return Status.values()[getStatusJ()];
}
public native int getStatusJ();
public native String getErrorString();
// commit transaction or save to file if filename is provided.
public native boolean commit(String filename, boolean overwrite);
public native long getAmount();
public native long getDust();
public native long getFee();
public String getFirstTxId() {
String id = getFirstTxIdJ();
if (id == null)
throw new IndexOutOfBoundsException();
return id;
}
public native String getFirstTxIdJ();
public native long getTxCount();
}

View file

@ -0,0 +1,82 @@
/*
* 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.model;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import timber.log.Timber;
public class TransactionHistory {
static {
System.loadLibrary("monerujo");
}
private long handle;
int accountIndex;
public void setAccountFor(Wallet wallet) {
if (accountIndex != wallet.getAccountIndex()) {
this.accountIndex = wallet.getAccountIndex();
refreshWithNotes(wallet);
}
}
public TransactionHistory(long handle, int accountIndex) {
this.handle = handle;
this.accountIndex = accountIndex;
}
private void loadNotes(Wallet wallet) {
for (TransactionInfo info : transactions) {
info.notes = wallet.getUserNote(info.hash);
}
}
public native int getCount(); // over all accounts/subaddresses
//private native long getTransactionByIndexJ(int i);
//private native long getTransactionByIdJ(String id);
public List<TransactionInfo> getAll() {
return transactions;
}
private List<TransactionInfo> transactions = new ArrayList<>();
void refreshWithNotes(Wallet wallet) {
refresh();
loadNotes(wallet);
}
private void refresh() {
List<TransactionInfo> transactionInfos = refreshJ();
Timber.d("refresh size=%d", transactionInfos.size());
for (Iterator<TransactionInfo> iterator = transactionInfos.iterator(); iterator.hasNext(); ) {
TransactionInfo info = iterator.next();
if (info.accountIndex != accountIndex) {
iterator.remove();
}
}
transactions = transactionInfos;
}
private native List<TransactionInfo> refreshJ();
}

View file

@ -0,0 +1,186 @@
/*
* 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.model;
import android.os.Parcel;
import android.os.Parcelable;
import com.m2049r.xmrwallet.data.Subaddress;
import java.util.List;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
// this is not the TransactionInfo from the API as that is owned by the TransactionHistory
// this is a POJO for the TransactionInfoAdapter
public class TransactionInfo implements Parcelable, Comparable<TransactionInfo> {
public static final int CONFIRMATION = 10; // blocks
@RequiredArgsConstructor
public enum Direction {
Direction_In(0),
Direction_Out(1);
public static Direction fromInteger(int n) {
switch (n) {
case 0:
return Direction_In;
case 1:
return Direction_Out;
}
return null;
}
@Getter
private final int value;
}
public Direction direction;
public boolean isPending;
public boolean isFailed;
public long amount;
public long fee;
public long blockheight;
public String hash;
public long timestamp;
public String paymentId;
public int accountIndex;
public int addressIndex;
public long confirmations;
public String subaddressLabel;
public List<Transfer> transfers;
public String txKey = null;
public String notes = null;
public String address = null;
public TransactionInfo(
int direction,
boolean isPending,
boolean isFailed,
long amount,
long fee,
long blockheight,
String hash,
long timestamp,
String paymentId,
int accountIndex,
int addressIndex,
long confirmations,
String subaddressLabel,
List<Transfer> transfers) {
this.direction = Direction.values()[direction];
this.isPending = isPending;
this.isFailed = isFailed;
this.amount = amount;
this.fee = fee;
this.blockheight = blockheight;
this.hash = hash;
this.timestamp = timestamp;
this.paymentId = paymentId;
this.accountIndex = accountIndex;
this.addressIndex = addressIndex;
this.confirmations = confirmations;
this.subaddressLabel = subaddressLabel;
this.transfers = transfers;
}
public boolean isConfirmed() {
return confirmations >= CONFIRMATION;
}
public String getDisplayLabel() {
if (subaddressLabel.isEmpty() || (Subaddress.DEFAULT_LABEL_FORMATTER.matcher(subaddressLabel).matches()))
return ("#" + addressIndex);
else
return subaddressLabel;
}
public String toString() {
return direction + "@" + blockheight + " " + amount;
}
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeInt(direction.getValue());
out.writeByte((byte) (isPending ? 1 : 0));
out.writeByte((byte) (isFailed ? 1 : 0));
out.writeLong(amount);
out.writeLong(fee);
out.writeLong(blockheight);
out.writeString(hash);
out.writeLong(timestamp);
out.writeString(paymentId);
out.writeInt(accountIndex);
out.writeInt(addressIndex);
out.writeLong(confirmations);
out.writeString(subaddressLabel);
out.writeList(transfers);
out.writeString(txKey);
out.writeString(notes);
out.writeString(address);
}
public static final Parcelable.Creator<TransactionInfo> CREATOR = new Parcelable.Creator<TransactionInfo>() {
public TransactionInfo createFromParcel(Parcel in) {
return new TransactionInfo(in);
}
public TransactionInfo[] newArray(int size) {
return new TransactionInfo[size];
}
};
private TransactionInfo(Parcel in) {
direction = Direction.fromInteger(in.readInt());
isPending = in.readByte() != 0;
isFailed = in.readByte() != 0;
amount = in.readLong();
fee = in.readLong();
blockheight = in.readLong();
hash = in.readString();
timestamp = in.readLong();
paymentId = in.readString();
accountIndex = in.readInt();
addressIndex = in.readInt();
confirmations = in.readLong();
subaddressLabel = in.readString();
transfers = in.readArrayList(Transfer.class.getClassLoader());
txKey = in.readString();
notes = in.readString();
address = in.readString();
}
@Override
public int describeContents() {
return 0;
}
@Override
public int compareTo(TransactionInfo another) {
long b1 = this.timestamp;
long b2 = another.timestamp;
if (b1 > b2) {
return -1;
} else if (b1 < b2) {
return 1;
} else {
return this.hash.compareTo(another.hash);
}
}
}

View file

@ -0,0 +1,57 @@
/*
* 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.model;
import android.os.Parcel;
import android.os.Parcelable;
public class Transfer implements Parcelable {
public long amount;
public String address;
public Transfer(long amount, String address) {
this.amount = amount;
this.address = address;
}
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeLong(amount);
out.writeString(address);
}
public static final Parcelable.Creator<Transfer> CREATOR = new Parcelable.Creator<Transfer>() {
public Transfer createFromParcel(Parcel in) {
return new Transfer(in);
}
public Transfer[] newArray(int size) {
return new Transfer[size];
}
};
private Transfer(Parcel in) {
amount = in.readLong();
address = in.readString();
}
@Override
public int describeContents() {
return 0;
}
}

View file

@ -0,0 +1,507 @@
/*
* 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.model;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.m2049r.xmrwallet.data.Subaddress;
import com.m2049r.xmrwallet.data.TxData;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import timber.log.Timber;
public class Wallet {
final static public long SWEEP_ALL = Long.MAX_VALUE;
static {
System.loadLibrary("monerujo");
}
static public class Status {
Status(int status, String errorString) {
this.status = StatusEnum.values()[status];
this.errorString = errorString;
}
final private StatusEnum status;
final private String errorString;
@Nullable
private ConnectionStatus connectionStatus; // optional
public StatusEnum getStatus() {
return status;
}
public String getErrorString() {
return errorString;
}
public void setConnectionStatus(@Nullable ConnectionStatus connectionStatus) {
this.connectionStatus = connectionStatus;
}
@Nullable
public ConnectionStatus getConnectionStatus() {
return connectionStatus;
}
public boolean isOk() {
return (getStatus() == StatusEnum.Status_Ok)
&& ((getConnectionStatus() == null) ||
(getConnectionStatus() == ConnectionStatus.ConnectionStatus_Connected));
}
@Override
@NonNull
public String toString() {
return "Wallet.Status: " + status + "/" + errorString + "/" + connectionStatus;
}
}
private int accountIndex = 0;
public int getAccountIndex() {
return accountIndex;
}
public void setAccountIndex(int accountIndex) {
Timber.d("setAccountIndex(%d)", accountIndex);
this.accountIndex = accountIndex;
getHistory().setAccountFor(this);
}
public String getName() {
return new File(getPath()).getName();
}
private long handle = 0;
private long listenerHandle = 0;
Wallet(long handle) {
this.handle = handle;
}
Wallet(long handle, int accountIndex) {
this.handle = handle;
this.accountIndex = accountIndex;
}
@RequiredArgsConstructor
@Getter
public enum Device {
Device_Undefined(0, 0),
Device_Software(50, 200),
Device_Ledger(5, 20);
private final int accountLookahead;
private final int subaddressLookahead;
}
public enum StatusEnum {
Status_Ok,
Status_Error,
Status_Critical
}
public enum ConnectionStatus {
ConnectionStatus_Disconnected,
ConnectionStatus_Connected,
ConnectionStatus_WrongVersion
}
public native String getSeed(String offset);
public native String getSeedLanguage();
public native void setSeedLanguage(String language);
public Status getStatus() {
return statusWithErrorString();
}
public Status getFullStatus() {
Wallet.Status walletStatus = statusWithErrorString();
walletStatus.setConnectionStatus(getConnectionStatus());
return walletStatus;
}
private native Status statusWithErrorString();
public native synchronized boolean setPassword(String password);
public String getAddress() {
return getAddress(accountIndex);
}
public String getAddress(int accountIndex) {
return getAddressJ(accountIndex, 0);
}
public String getSubaddress(int addressIndex) {
return getAddressJ(accountIndex, addressIndex);
}
public String getSubaddress(int accountIndex, int addressIndex) {
return getAddressJ(accountIndex, addressIndex);
}
private native String getAddressJ(int accountIndex, int addressIndex);
public Subaddress getSubaddressObject(int accountIndex, int subAddressIndex) {
return new Subaddress(accountIndex, subAddressIndex,
getSubaddress(subAddressIndex), getSubaddressLabel(subAddressIndex));
}
public Subaddress getSubaddressObject(int subAddressIndex) {
Subaddress subaddress = getSubaddressObject(accountIndex, subAddressIndex);
long amount = 0;
for (TransactionInfo info : getHistory().getAll()) {
if ((info.addressIndex == subAddressIndex)
&& (info.direction == TransactionInfo.Direction.Direction_In)) {
amount += info.amount;
}
}
subaddress.setAmount(amount);
return subaddress;
}
public native String getPath();
public NetworkType getNetworkType() {
return NetworkType.fromInteger(nettype());
}
public native int nettype();
//TODO virtual void hardForkInfo(uint8_t &version, uint64_t &earliest_height) const = 0;
//TODO virtual bool useForkRules(uint8_t version, int64_t early_blocks) const = 0;
public native String getIntegratedAddress(String payment_id);
public native String getSecretViewKey();
public native String getSecretSpendKey();
public boolean store() {
return store("");
}
public native synchronized boolean store(String path);
public boolean close() {
disposePendingTransaction();
return WalletManager.getInstance().close(this);
}
public native String getFilename();
// virtual std::string keysFilename() const = 0;
public boolean init(long upper_transaction_size_limit) {
return initJ(WalletManager.getInstance().getDaemonAddress(), upper_transaction_size_limit,
WalletManager.getInstance().getDaemonUsername(),
WalletManager.getInstance().getDaemonPassword());
}
private native boolean initJ(String daemon_address, long upper_transaction_size_limit,
String daemon_username, String daemon_password);
// virtual bool createWatchOnly(const std::string &path, const std::string &password, const std::string &language) const = 0;
// virtual void setRefreshFromBlockHeight(uint64_t refresh_from_block_height) = 0;
public native void setRestoreHeight(long height);
public native long getRestoreHeight();
// virtual void setRecoveringFromSeed(bool recoveringFromSeed) = 0;
// virtual bool connectToDaemon() = 0;
public ConnectionStatus getConnectionStatus() {
int s = getConnectionStatusJ();
return Wallet.ConnectionStatus.values()[s];
}
private native int getConnectionStatusJ();
//TODO virtual void setTrustedDaemon(bool arg) = 0;
//TODO virtual bool trustedDaemon() const = 0;
public native boolean setProxy(String address);
public long getBalance() {
return getBalance(accountIndex);
}
public native long getBalance(int accountIndex);
public native long getBalanceAll();
public long getUnlockedBalance() {
return getUnlockedBalance(accountIndex);
}
public native long getUnlockedBalanceAll();
public native long getUnlockedBalance(int accountIndex);
public native boolean isWatchOnly();
public native long getBlockChainHeight();
public native long getApproximateBlockChainHeight();
public native long getDaemonBlockChainHeight();
public native long getDaemonBlockChainTargetHeight();
boolean synced = false;
public boolean isSynchronized() {
return synced;
}
public void setSynchronized() {
this.synced = true;
}
public static native String getDisplayAmount(long amount);
public static native long getAmountFromString(String amount);
public static native long getAmountFromDouble(double amount);
public static native String generatePaymentId();
public static native boolean isPaymentIdValid(String payment_id);
public static boolean isAddressValid(String address) {
return isAddressValid(address, WalletManager.getInstance().getNetworkType().getValue());
}
public static native boolean isAddressValid(String address, int networkType);
public static native String getPaymentIdFromAddress(String address, int networkType);
public static native long getMaximumAllowedAmount();
public native void startRefresh();
public native void pauseRefresh();
public native boolean refresh();
public native void refreshAsync();
public native void rescanBlockchainAsyncJ();
public void rescanBlockchainAsync() {
synced = false;
rescanBlockchainAsyncJ();
}
//TODO virtual void setAutoRefreshInterval(int millis) = 0;
//TODO virtual int autoRefreshInterval() const = 0;
private PendingTransaction pendingTransaction = null;
public PendingTransaction getPendingTransaction() {
return pendingTransaction;
}
public void disposePendingTransaction() {
if (pendingTransaction != null) {
disposeTransaction(pendingTransaction);
pendingTransaction = null;
}
}
public PendingTransaction createTransaction(TxData txData) {
return createTransaction(
txData.getDestinationAddress(),
txData.getAmount(),
txData.getMixin(),
txData.getPriority());
}
public PendingTransaction createTransaction(String dst_addr,
long amount, int mixin_count,
PendingTransaction.Priority priority) {
disposePendingTransaction();
int _priority = priority.getValue();
long txHandle =
(amount == SWEEP_ALL ?
createSweepTransaction(dst_addr, "", mixin_count, _priority,
accountIndex) :
createTransactionJ(dst_addr, "", amount, mixin_count, _priority,
accountIndex));
pendingTransaction = new PendingTransaction(txHandle);
return pendingTransaction;
}
private native long createTransactionJ(String dst_addr, String payment_id,
long amount, int mixin_count,
int priority, int accountIndex);
private native long createSweepTransaction(String dst_addr, String payment_id,
int mixin_count,
int priority, int accountIndex);
public PendingTransaction createSweepUnmixableTransaction() {
disposePendingTransaction();
long txHandle = createSweepUnmixableTransactionJ();
pendingTransaction = new PendingTransaction(txHandle);
return pendingTransaction;
}
private native long createSweepUnmixableTransactionJ();
//virtual UnsignedTransaction * loadUnsignedTx(const std::string &unsigned_filename) = 0;
//virtual bool submitTransaction(const std::string &fileName) = 0;
public native void disposeTransaction(PendingTransaction pendingTransaction);
//virtual bool exportKeyImages(const std::string &filename) = 0;
//virtual bool importKeyImages(const std::string &filename) = 0;
//virtual TransactionHistory * history() const = 0;
private TransactionHistory history = null;
public TransactionHistory getHistory() {
if (history == null) {
history = new TransactionHistory(getHistoryJ(), accountIndex);
}
return history;
}
private native long getHistoryJ();
public void refreshHistory() {
getHistory().refreshWithNotes(this);
}
//virtual AddressBook * addressBook() const = 0;
//virtual void setListener(WalletListener *) = 0;
private native long setListenerJ(WalletListener listener);
public void setListener(WalletListener listener) {
this.listenerHandle = setListenerJ(listener);
}
public native int getDefaultMixin();
public native void setDefaultMixin(int mixin);
public native boolean setUserNote(String txid, String note);
public native String getUserNote(String txid);
public native String getTxKey(String txid);
//virtual std::string signMessage(const std::string &message) = 0;
//virtual bool verifySignedMessage(const std::string &message, const std::string &addres, const std::string &signature) const = 0;
//virtual bool parse_uri(const std::string &uri, std::string &address, std::string &payment_id, uint64_t &tvAmount, std::string &tx_description, std::string &recipient_name, std::vector<std::string> &unknown_parameters, std::string &error) = 0;
//virtual bool rescanSpent() = 0;
private static final String NEW_ACCOUNT_NAME = "Untitled account"; // src/wallet/wallet2.cpp:941
public void addAccount() {
addAccount(NEW_ACCOUNT_NAME);
}
public native void addAccount(String label);
public String getAccountLabel() {
return getAccountLabel(accountIndex);
}
public String getAccountLabel(int accountIndex) {
String label = getSubaddressLabel(accountIndex, 0);
if (label.equals(NEW_ACCOUNT_NAME)) {
String address = getAddress(accountIndex);
int len = address.length();
label = address.substring(0, 6) +
"\u2026" + address.substring(len - 6, len);
}
return label;
}
public String getSubaddressLabel(int addressIndex) {
return getSubaddressLabel(accountIndex, addressIndex);
}
public native String getSubaddressLabel(int accountIndex, int addressIndex);
public void setAccountLabel(String label) {
setAccountLabel(accountIndex, label);
}
public void setAccountLabel(int accountIndex, String label) {
setSubaddressLabel(accountIndex, 0, label);
}
public void setSubaddressLabel(int addressIndex, String label) {
setSubaddressLabel(accountIndex, addressIndex, label);
refreshHistory();
}
public native void setSubaddressLabel(int accountIndex, int addressIndex, String label);
public native int getNumAccounts();
public int getNumSubaddresses() {
return getNumSubaddresses(accountIndex);
}
public native int getNumSubaddresses(int accountIndex);
public String getNewSubaddress() {
return getNewSubaddress(accountIndex);
}
public String getNewSubaddress(int accountIndex) {
String timeStamp = new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss", Locale.US).format(new Date());
addSubaddress(accountIndex, timeStamp);
String subaddress = getLastSubaddress(accountIndex);
Timber.d("%d: %s", getNumSubaddresses(accountIndex) - 1, subaddress);
return subaddress;
}
public native void addSubaddress(int accountIndex, String label);
public String getLastSubaddress(int accountIndex) {
return getSubaddress(accountIndex, getNumSubaddresses(accountIndex) - 1);
}
public Wallet.Device getDeviceType() {
int device = getDeviceTypeJ();
return Wallet.Device.values()[device + 1]; // mapping is monero+1=android
}
private native int getDeviceTypeJ();
}

View file

@ -0,0 +1,57 @@
/*
* 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.model;
public interface WalletListener {
/**
* moneySpent - called when money spent
* @param txId - transaction id
* @param amount - tvAmount
*/
void moneySpent(String txId, long amount);
/**
* moneyReceived - called when money received
* @param txId - transaction id
* @param amount - tvAmount
*/
void moneyReceived(String txId, long amount);
/**
* unconfirmedMoneyReceived - called when payment arrived in tx pool
* @param txId - transaction id
* @param amount - tvAmount
*/
void unconfirmedMoneyReceived(String txId, long amount);
/**
* newBlock - called when new block received
* @param height - block height
*/
void newBlock(long height);
/**
* updated - generic callback, called when any event (sent/received/block reveived/etc) happened with the wallet;
*/
void updated();
/**
* refreshed - called when wallet refreshed by background thread or explicitly refreshed by calling "refresh" synchronously
*/
void refreshed();
}

View file

@ -0,0 +1,341 @@
/*
* 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.model;
import com.m2049r.xmrwallet.XmrWalletApplication;
import com.m2049r.xmrwallet.data.Node;
import com.m2049r.xmrwallet.ledger.Ledger;
import com.m2049r.xmrwallet.util.RestoreHeight;
import java.io.File;
import java.io.FilenameFilter;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import lombok.Getter;
import timber.log.Timber;
public class WalletManager {
static {
System.loadLibrary("monerujo");
}
// no need to keep a reference to the REAL WalletManager (we get it every tvTime we need it)
private static WalletManager Instance = null;
public static synchronized WalletManager getInstance() {
if (WalletManager.Instance == null) {
WalletManager.Instance = new WalletManager();
}
return WalletManager.Instance;
}
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() {
return managedWallet;
}
private void manageWallet(Wallet wallet) {
Timber.d("Managing %s", wallet.getName());
managedWallet = wallet;
}
private void unmanageWallet(Wallet wallet) {
if (wallet == null) {
throw new IllegalArgumentException("Cannot unmanage null!");
}
if (getWallet() == null) {
throw new IllegalStateException("No wallet under management!");
}
if (getWallet() != wallet) {
throw new IllegalStateException(wallet.getName() + " not under management!");
}
Timber.d("Unmanaging %s", managedWallet.getName());
managedWallet = null;
}
public Wallet createWallet(File aFile, String password, String language, long height) {
long walletHandle = createWalletJ(aFile.getAbsolutePath(), password, language, getNetworkType().getValue());
Wallet wallet = new Wallet(walletHandle);
manageWallet(wallet);
if (wallet.getStatus().isOk()) {
// (Re-)Estimate restore height based on what we know
final long oldHeight = wallet.getRestoreHeight();
// Go back 4 days if we don't have a precise restore height
Calendar restoreDate = Calendar.getInstance();
restoreDate.add(Calendar.DAY_OF_MONTH, -4);
final long restoreHeight =
(height > -1) ? height : RestoreHeight.getInstance().getHeight(restoreDate.getTime());
wallet.setRestoreHeight(restoreHeight);
Timber.d("Changed Restore Height from %d to %d", oldHeight, wallet.getRestoreHeight());
wallet.setPassword(password); // this rewrites the keys file (which contains the restore height)
} else
Timber.e(wallet.getStatus().toString());
return wallet;
}
private native long createWalletJ(String path, String password, String language, int networkType);
public Wallet openAccount(String path, int accountIndex, String password) {
long walletHandle = openWalletJ(path, password, getNetworkType().getValue());
Wallet wallet = new Wallet(walletHandle, accountIndex);
manageWallet(wallet);
return wallet;
}
public Wallet openWallet(String path, String password) {
long walletHandle = openWalletJ(path, password, getNetworkType().getValue());
Wallet wallet = new Wallet(walletHandle);
manageWallet(wallet);
return wallet;
}
private native long openWalletJ(String path, String password, int networkType);
public Wallet recoveryWallet(File aFile, String password,
String mnemonic, String offset,
long restoreHeight) {
long walletHandle = recoveryWalletJ(aFile.getAbsolutePath(), password,
mnemonic, offset,
getNetworkType().getValue(), restoreHeight);
Wallet wallet = new Wallet(walletHandle);
manageWallet(wallet);
return wallet;
}
private native long recoveryWalletJ(String path, String password,
String mnemonic, String offset,
int networkType, long restoreHeight);
public Wallet createWalletWithKeys(File aFile, String password, String language, long restoreHeight,
String addressString, String viewKeyString, String spendKeyString) {
long walletHandle = createWalletFromKeysJ(aFile.getAbsolutePath(), password,
language, getNetworkType().getValue(), restoreHeight,
addressString, viewKeyString, spendKeyString);
Wallet wallet = new Wallet(walletHandle);
manageWallet(wallet);
return wallet;
}
private native long createWalletFromKeysJ(String path, String password,
String language,
int networkType,
long restoreHeight,
String addressString,
String viewKeyString,
String spendKeyString);
public Wallet createWalletFromDevice(File aFile, String password, long restoreHeight,
String deviceName) {
long walletHandle = createWalletFromDeviceJ(aFile.getAbsolutePath(), password,
getNetworkType().getValue(), deviceName, restoreHeight,
Ledger.SUBADDRESS_LOOKAHEAD);
Wallet wallet = new Wallet(walletHandle);
manageWallet(wallet);
return wallet;
}
private native long createWalletFromDeviceJ(String path, String password,
int networkType,
String deviceName,
long restoreHeight,
String subaddressLookahead);
public native boolean closeJ(Wallet wallet);
public boolean close(Wallet wallet) {
unmanageWallet(wallet);
boolean closed = closeJ(wallet);
if (!closed) {
// in case we could not close it
// we manage it again
manageWallet(wallet);
}
return closed;
}
public boolean walletExists(File aFile) {
return walletExists(aFile.getAbsolutePath());
}
public native boolean walletExists(String path);
public native boolean verifyWalletPassword(String keys_file_name, String password, boolean watch_only);
public boolean verifyWalletPasswordOnly(String keys_file_name, String password) {
return queryWalletDeviceJ(keys_file_name, password) >= 0;
}
public Wallet.Device queryWalletDevice(String keys_file_name, String password) {
int device = queryWalletDeviceJ(keys_file_name, password);
return Wallet.Device.values()[device + 1]; // mapping is monero+1=android
}
private native int queryWalletDeviceJ(String keys_file_name, String password);
//public native List<String> findWallets(String path); // this does not work - some error in boost
public class WalletInfo implements Comparable<WalletInfo> {
@Getter
final private File path;
@Getter
final private String name;
public WalletInfo(File wallet) {
path = wallet.getParentFile();
name = wallet.getName();
}
@Override
public int compareTo(WalletInfo another) {
return name.toLowerCase().compareTo(another.name.toLowerCase());
}
}
public List<WalletInfo> findWallets(File path) {
List<WalletInfo> wallets = new ArrayList<>();
Timber.d("Scanning: %s", path.getAbsolutePath());
File[] found = path.listFiles(new FilenameFilter() {
public boolean accept(File dir, String filename) {
return filename.endsWith(".keys");
}
});
for (int i = 0; i < found.length; i++) {
String filename = found[i].getName();
File f = new File(found[i].getParent(), filename.substring(0, filename.length() - 5)); // 5 is length of ".keys"+1
wallets.add(new WalletInfo(f));
}
return wallets;
}
//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 final NetworkType networkType = XmrWalletApplication.getNetworkType();
public NetworkType getNetworkType() {
return networkType;
}
// this should not be called on the main thread as it connects to the node (and takes a long time)
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(""); // don't disconnect as monero code blocks for many seconds!
//TODO: need to do something about that later
}
}
public String getDaemonAddress() {
if (daemonAddress == null) {
throw new IllegalStateException("use setDaemon() to initialise daemon and net first!");
}
return this.daemonAddress;
}
private native void setDaemonAddressJ(String address);
private String daemonUsername = "";
public String getDaemonUsername() {
return daemonUsername;
}
private String daemonPassword = "";
public String getDaemonPassword() {
return daemonPassword;
}
public native int getDaemonVersion();
public native long getBlockchainHeight();
public native long getBlockchainTargetHeight();
public native long getNetworkDifficulty();
public native double getMiningHashRate();
public native long getBlockTarget();
public native boolean isMining();
public native boolean startMining(String address, boolean background_mining, boolean ignore_battery);
public native boolean stopMining();
public native String resolveOpenAlias(String address, boolean dnssec_valid);
public native boolean setProxy(String address);
//TODO static std::tuple<bool, std::string, std::string, std::string, std::string> checkUpdates(const std::string &software, const std::string &subdir);
static public native void initLogger(String argv0, String defaultLogBaseName);
//TODO: maybe put these in an enum like in monero core - but why?
static public int LOGLEVEL_SILENT = -1;
static public int LOGLEVEL_WARN = 0;
static public int LOGLEVEL_INFO = 1;
static public int LOGLEVEL_DEBUG = 2;
static public int LOGLEVEL_TRACE = 3;
static public int LOGLEVEL_MAX = 4;
static public native void setLogLevel(int level);
static public native void logDebug(String category, String message);
static public native void logInfo(String category, String message);
static public native void logWarning(String category, String message);
static public native void logError(String category, String message);
static public native String moneroVersion();
}

View file

@ -0,0 +1,123 @@
/*
* Copyright (c) 2018-2020 EarlOfEgo, 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.onboarding;
import android.content.Intent;
import android.os.Bundle;
import android.util.TypedValue;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import androidx.appcompat.app.AppCompatActivity;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayout;
import com.m2049r.xmrwallet.LoginActivity;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.util.KeyStoreHelper;
public class OnBoardingActivity extends AppCompatActivity implements OnBoardingAdapter.Listener {
private OnBoardingViewPager pager;
private OnBoardingAdapter pagerAdapter;
private Button nextButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_on_boarding);
nextButton = findViewById(R.id.buttonNext);
pager = findViewById(R.id.pager);
pagerAdapter = new OnBoardingAdapter(this, this);
pager.setAdapter(pagerAdapter);
int pixels = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 16, getResources().getDisplayMetrics());
pager.setPageMargin(pixels);
pager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
@Override
public void onPageSelected(int position) {
setButtonState(position);
}
});
final TabLayout tabLayout = (TabLayout) findViewById(R.id.tabLayout);
if (pagerAdapter.getCount() > 1) {
tabLayout.setupWithViewPager(pager, true);
LinearLayout tabStrip = ((LinearLayout) tabLayout.getChildAt(0));
for (int i = 0; i < tabStrip.getChildCount(); i++) {
tabStrip.getChildAt(i).setClickable(false);
}
} else {
tabLayout.setVisibility(View.GONE);
}
nextButton.setOnClickListener(v -> {
final int item = pager.getCurrentItem();
if (item + 1 >= pagerAdapter.getCount()) {
finishOnboarding();
} else {
pager.setCurrentItem(item + 1);
}
});
// let old users who have fingerprint wallets already agree for fingerprint sending
OnBoardingScreen.FPSEND.setMustAgree(KeyStoreHelper.hasStoredPasswords(this));
for (int i = 0; i < OnBoardingScreen.values().length; i++) {
agreed[i] = !OnBoardingScreen.values()[i].isMustAgree();
}
setButtonState(0);
}
private void finishOnboarding() {
nextButton.setEnabled(false);
OnBoardingManager.setOnBoardingShown(getApplicationContext());
startActivity(new Intent(this, LoginActivity.class));
finish();
}
boolean[] agreed = new boolean[OnBoardingScreen.values().length];
@Override
public void setAgreeClicked(int position, boolean isChecked) {
agreed[position] = isChecked;
setButtonState(position);
}
@Override
public boolean isAgreeClicked(int position) {
return agreed[position];
}
@Override
public void setButtonState(int position) {
nextButton.setEnabled(agreed[position]);
if (nextButton.isEnabled())
pager.setAllowedSwipeDirection(OnBoardingViewPager.SwipeDirection.ALL);
else
pager.setAllowedSwipeDirection(OnBoardingViewPager.SwipeDirection.LEFT);
if (pager.getCurrentItem() + 1 == pagerAdapter.getCount()) { // last page
nextButton.setText(R.string.onboarding_button_ready);
} else {
nextButton.setText(R.string.onboarding_button_next);
}
}
}

View file

@ -0,0 +1,92 @@
/*
* Copyright (c) 2018-2020 EarlOfEgo, 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.onboarding;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.viewpager.widget.PagerAdapter;
import com.m2049r.xmrwallet.R;
import timber.log.Timber;
public class OnBoardingAdapter extends PagerAdapter {
interface Listener {
void setAgreeClicked(int position, boolean isChecked);
boolean isAgreeClicked(int position);
void setButtonState(int position);
}
private final Context context;
private Listener listener;
OnBoardingAdapter(final Context context, final Listener listener) {
this.context = context;
this.listener = listener;
}
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup collection, int position) {
LayoutInflater inflater = LayoutInflater.from(context);
final View view = inflater.inflate(R.layout.view_onboarding, collection, false);
final OnBoardingScreen onBoardingScreen = OnBoardingScreen.values()[position];
final Drawable drawable = ContextCompat.getDrawable(context, onBoardingScreen.getDrawable());
((ImageView) view.findViewById(R.id.onboardingImage)).setImageDrawable(drawable);
((TextView) view.findViewById(R.id.onboardingTitle)).setText(onBoardingScreen.getTitle());
((TextView) view.findViewById(R.id.onboardingInformation)).setText(onBoardingScreen.getInformation());
if (onBoardingScreen.isMustAgree()) {
final CheckBox agree = ((CheckBox) view.findViewById(R.id.onboardingAgree));
agree.setVisibility(View.VISIBLE);
agree.setChecked(listener.isAgreeClicked(position));
agree.setOnClickListener(v -> {
listener.setAgreeClicked(position, ((CheckBox) v).isChecked());
});
}
collection.addView(view);
return view;
}
@Override
public int getCount() {
return OnBoardingScreen.values().length;
}
@Override
public void destroyItem(@NonNull ViewGroup collection, int position, @NonNull Object view) {
Timber.d("destroy " + position);
collection.removeView((View) view);
}
@Override
public boolean isViewFromObject(@NonNull final View view, @NonNull final Object object) {
return view == object;
}
}

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