Upload normal Monerujo code
27
.circleci/config.yml
Normal 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
@ -1,23 +1,18 @@
|
|||||||
# Compiled class file
|
.gradle
|
||||||
*.class
|
/build
|
||||||
|
*.iml
|
||||||
# Log file
|
/.idea
|
||||||
*.log
|
/local.properties
|
||||||
|
/captures
|
||||||
# BlueJ files
|
.externalNativeBuild
|
||||||
*.ctxt
|
.DS_Store
|
||||||
|
/app/build
|
||||||
# Mobile Tools for Java (J2ME)
|
/app/release
|
||||||
.mtj.tmp/
|
/app/alpha
|
||||||
|
/app/prod
|
||||||
# Package Files #
|
/app/alphaMainnet
|
||||||
*.jar
|
/app/prodMainnet
|
||||||
*.war
|
/app/alphaStagenet
|
||||||
*.nar
|
/app/prodStagenet
|
||||||
*.ear
|
/app/.cxx
|
||||||
*.zip
|
/monerujo.id
|
||||||
*.tar.gz
|
|
||||||
*.rar
|
|
||||||
|
|
||||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
|
||||||
hs_err_pid*
|
|
||||||
|
239
app/CMakeLists.txt
Normal 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
@ -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
@ -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
|
BIN
app/src/alpha/ic_launcher-web.png
Normal file
After Width: | Height: | Size: 244 KiB |
BIN
app/src/alpha/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 8.0 KiB |
BIN
app/src/alpha/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
app/src/alpha/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
app/src/alpha/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
app/src/alpha/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 47 KiB |
BIN
app/src/debug/res/mipmap-hdpi/ic_launcher.png
Executable file
After Width: | Height: | Size: 8.1 KiB |
BIN
app/src/debug/res/mipmap-mdpi/ic_launcher.png
Executable file
After Width: | Height: | Size: 4.4 KiB |
BIN
app/src/debug/res/mipmap-xhdpi/ic_launcher.png
Executable file
After Width: | Height: | Size: 14 KiB |
BIN
app/src/debug/res/mipmap-xxhdpi/ic_launcher.png
Executable file
After Width: | Height: | Size: 28 KiB |
BIN
app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png
Executable file
After Width: | Height: | Size: 48 KiB |
111
app/src/main/AndroidManifest.xml
Normal 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>
|
829
app/src/main/assets/licenses.html
Normal 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'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 'originates' from a Contributor if it was
|
||||||
|
added to the Program by such Contributor itself or anyone acting on such Contributor'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'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'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's patent(s), then such
|
||||||
|
Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation
|
||||||
|
is filed.<br/>
|
||||||
|
<br/>
|
||||||
|
All Recipient'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's
|
||||||
|
rights under this Agreement terminate, Recipient agrees to cease use and distribution of the
|
||||||
|
Program as soon as reasonably practicable. However, Recipient'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'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 'cryptographic' 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>'Poppins' 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>—————————————————————————————-<br />
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007<br />
|
||||||
|
—————————————————————————————-</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 />
|
||||||
|
“Font Software” 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>“Reserved Font Name” refers to any names specified as such after the copyright
|
||||||
|
statement(s).</p>
|
||||||
|
<p>“Original Version” refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).</p>
|
||||||
|
<p>“Modified Version” refers to any derivative made by adding to, deleting, or
|
||||||
|
substituting—in part or in whole—any of the components of the Original Version,
|
||||||
|
by changing formats or by porting the Font Software to a new environment.</p>
|
||||||
|
<p>“Author” refers to any designer, engineer, programmer, technical writer or other
|
||||||
|
person who contributed to the Font Software.</p>
|
||||||
|
<p>PERMISSION & 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 “AS IS”, 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>
|
1531
app/src/main/cpp/monerujo.cpp
Normal file
79
app/src/main/cpp/monerujo.h
Normal 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
|
BIN
app/src/main/ic_launcher-web.png
Normal file
After Width: | Height: | Size: 245 KiB |
53
app/src/main/java/com/btchip/BTChipException.java
Normal 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;
|
||||||
|
|
||||||
|
}
|
31
app/src/main/java/com/btchip/comm/BTChipTransport.java
Normal 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);
|
||||||
|
}
|
126
app/src/main/java/com/btchip/comm/LedgerHelper.java
Normal 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
62
app/src/main/java/com/btchip/utils/Dump.java
Normal 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
145
app/src/main/java/com/m2049r/levin/data/Bucket.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
125
app/src/main/java/com/m2049r/levin/data/Section.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
195
app/src/main/java/com/m2049r/levin/scanner/Dispatcher.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
39
app/src/main/java/com/m2049r/levin/scanner/LevinPeer.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
231
app/src/main/java/com/m2049r/levin/scanner/PeerRetriever.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
42
app/src/main/java/com/m2049r/levin/util/HexHelper.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
184
app/src/main/java/com/m2049r/levin/util/LevinReader.java
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
98
app/src/main/java/com/m2049r/levin/util/LevinWriter.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,564 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018 m2049r
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.m2049r.levin.util;
|
||||||
|
|
||||||
|
import java.io.DataInput;
|
||||||
|
import java.io.EOFException;
|
||||||
|
import java.io.FilterInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.UTFDataFormatException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A little endian java.io.DataInputStream (without readLine())
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class LittleEndianDataInputStream extends FilterInputStream implements
|
||||||
|
DataInput {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a DataInputStream that uses the specified underlying InputStream.
|
||||||
|
*
|
||||||
|
* @param in the specified input stream
|
||||||
|
*/
|
||||||
|
public LittleEndianDataInputStream(InputStream in) {
|
||||||
|
super(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
|
public final String readLine() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads some number of bytes from the contained input stream and stores
|
||||||
|
* them into the buffer array <code>b</code>. The number of bytes actually
|
||||||
|
* read is returned as an integer. This method blocks until input data is
|
||||||
|
* available, end of file is detected, or an exception is thrown.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* If <code>b</code> is null, a <code>NullPointerException</code> is thrown.
|
||||||
|
* If the length of <code>b</code> is zero, then no bytes are read and
|
||||||
|
* <code>0</code> is returned; otherwise, there is an attempt to read at
|
||||||
|
* least one byte. If no byte is available because the stream is at end of
|
||||||
|
* file, the value <code>-1</code> is returned; otherwise, at least one byte
|
||||||
|
* is read and stored into <code>b</code>.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The first byte read is stored into element <code>b[0]</code>, the next
|
||||||
|
* one into <code>b[1]</code>, and so on. The number of bytes read is, at
|
||||||
|
* most, equal to the length of <code>b</code>. Let <code>k</code> be the
|
||||||
|
* number of bytes actually read; these bytes will be stored in elements
|
||||||
|
* <code>b[0]</code> through <code>b[k-1]</code>, leaving elements
|
||||||
|
* <code>b[k]</code> through <code>b[b.length-1]</code> unaffected.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The <code>read(b)</code> method has the same effect as: <blockquote>
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* read(b, 0, b.length)
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* </blockquote>
|
||||||
|
*
|
||||||
|
* @param b the buffer into which the data is read.
|
||||||
|
* @return the total number of bytes read into the buffer, or
|
||||||
|
* <code>-1</code> if there is no more data because the end of the
|
||||||
|
* stream has been reached.
|
||||||
|
* @throws IOException if the first byte cannot be read for any reason other than
|
||||||
|
* end of file, the stream has been closed and the underlying
|
||||||
|
* input stream does not support reading after close, or
|
||||||
|
* another I/O error occurs.
|
||||||
|
* @see FilterInputStream#in
|
||||||
|
* @see InputStream#read(byte[], int, int)
|
||||||
|
*/
|
||||||
|
public final int read(byte b[]) throws IOException {
|
||||||
|
return in.read(b, 0, b.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads up to <code>len</code> bytes of data from the contained input
|
||||||
|
* stream into an array of bytes. An attempt is made to read as many as
|
||||||
|
* <code>len</code> bytes, but a smaller number may be read, possibly zero.
|
||||||
|
* The number of bytes actually read is returned as an integer.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This method blocks until input data is available, end of file is
|
||||||
|
* detected, or an exception is thrown.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* If <code>len</code> is zero, then no bytes are read and <code>0</code> is
|
||||||
|
* returned; otherwise, there is an attempt to read at least one byte. If no
|
||||||
|
* byte is available because the stream is at end of file, the value
|
||||||
|
* <code>-1</code> is returned; otherwise, at least one byte is read and
|
||||||
|
* stored into <code>b</code>.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The first byte read is stored into element <code>b[off]</code>, the next
|
||||||
|
* one into <code>b[off+1]</code>, and so on. The number of bytes read is,
|
||||||
|
* at most, equal to <code>len</code>. Let <i>k</i> be the number of bytes
|
||||||
|
* actually read; these bytes will be stored in elements <code>b[off]</code>
|
||||||
|
* through <code>b[off+</code><i>k</i><code>-1]</code>, leaving elements
|
||||||
|
* <code>b[off+</code><i>k</i><code>]</code> through
|
||||||
|
* <code>b[off+len-1]</code> unaffected.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* In every case, elements <code>b[0]</code> through <code>b[off]</code> and
|
||||||
|
* elements <code>b[off+len]</code> through <code>b[b.length-1]</code> are
|
||||||
|
* unaffected.
|
||||||
|
*
|
||||||
|
* @param b the buffer into which the data is read.
|
||||||
|
* @param off the start offset in the destination array <code>b</code>
|
||||||
|
* @param len the maximum number of bytes read.
|
||||||
|
* @return the total number of bytes read into the buffer, or
|
||||||
|
* <code>-1</code> if there is no more data because the end of the
|
||||||
|
* stream has been reached.
|
||||||
|
* @throws NullPointerException If <code>b</code> is <code>null</code>.
|
||||||
|
* @throws IndexOutOfBoundsException If <code>off</code> is negative, <code>len</code> is
|
||||||
|
* negative, or <code>len</code> is greater than
|
||||||
|
* <code>b.length - off</code>
|
||||||
|
* @throws IOException if the first byte cannot be read for any reason other than
|
||||||
|
* end of file, the stream has been closed and the underlying
|
||||||
|
* input stream does not support reading after close, or
|
||||||
|
* another I/O error occurs.
|
||||||
|
* @see FilterInputStream#in
|
||||||
|
* @see InputStream#read(byte[], int, int)
|
||||||
|
*/
|
||||||
|
public final int read(byte b[], int off, int len) throws IOException {
|
||||||
|
return in.read(b, off, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See the general contract of the <code>readFully</code> method of
|
||||||
|
* <code>DataInput</code>.
|
||||||
|
* <p>
|
||||||
|
* Bytes for this operation are read from the contained input stream.
|
||||||
|
*
|
||||||
|
* @param b the buffer into which the data is read.
|
||||||
|
* @throws EOFException if this input stream reaches the end before reading all
|
||||||
|
* the bytes.
|
||||||
|
* @throws IOException the stream has been closed and the contained input stream
|
||||||
|
* does not support reading after close, or another I/O error
|
||||||
|
* occurs.
|
||||||
|
* @see FilterInputStream#in
|
||||||
|
*/
|
||||||
|
public final void readFully(byte b[]) throws IOException {
|
||||||
|
readFully(b, 0, b.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See the general contract of the <code>readFully</code> method of
|
||||||
|
* <code>DataInput</code>.
|
||||||
|
* <p>
|
||||||
|
* Bytes for this operation are read from the contained input stream.
|
||||||
|
*
|
||||||
|
* @param b the buffer into which the data is read.
|
||||||
|
* @param off the start offset of the data.
|
||||||
|
* @param len the number of bytes to read.
|
||||||
|
* @throws EOFException if this input stream reaches the end before reading all
|
||||||
|
* the bytes.
|
||||||
|
* @throws IOException the stream has been closed and the contained input stream
|
||||||
|
* does not support reading after close, or another I/O error
|
||||||
|
* occurs.
|
||||||
|
* @see FilterInputStream#in
|
||||||
|
*/
|
||||||
|
public final void readFully(byte b[], int off, int len) throws IOException {
|
||||||
|
if (len < 0)
|
||||||
|
throw new IndexOutOfBoundsException();
|
||||||
|
int n = 0;
|
||||||
|
while (n < len) {
|
||||||
|
int count = in.read(b, off + n, len - n);
|
||||||
|
if (count < 0)
|
||||||
|
throw new EOFException();
|
||||||
|
n += count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See the general contract of the <code>skipBytes</code> method of
|
||||||
|
* <code>DataInput</code>.
|
||||||
|
* <p>
|
||||||
|
* Bytes for this operation are read from the contained input stream.
|
||||||
|
*
|
||||||
|
* @param n the number of bytes to be skipped.
|
||||||
|
* @return the actual number of bytes skipped.
|
||||||
|
* @throws IOException if the contained input stream does not support seek, or
|
||||||
|
* the stream has been closed and the contained input stream
|
||||||
|
* does not support reading after close, or another I/O error
|
||||||
|
* occurs.
|
||||||
|
*/
|
||||||
|
public final int skipBytes(int n) throws IOException {
|
||||||
|
int total = 0;
|
||||||
|
int cur = 0;
|
||||||
|
|
||||||
|
while ((total < n) && ((cur = (int) in.skip(n - total)) > 0)) {
|
||||||
|
total += cur;
|
||||||
|
}
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See the general contract of the <code>readBoolean</code> method of
|
||||||
|
* <code>DataInput</code>.
|
||||||
|
* <p>
|
||||||
|
* Bytes for this operation are read from the contained input stream.
|
||||||
|
*
|
||||||
|
* @return the <code>boolean</code> value read.
|
||||||
|
* @throws EOFException if this input stream has reached the end.
|
||||||
|
* @throws IOException the stream has been closed and the contained input stream
|
||||||
|
* does not support reading after close, or another I/O error
|
||||||
|
* occurs.
|
||||||
|
* @see FilterInputStream#in
|
||||||
|
*/
|
||||||
|
public final boolean readBoolean() throws IOException {
|
||||||
|
int ch = in.read();
|
||||||
|
if (ch < 0)
|
||||||
|
throw new EOFException();
|
||||||
|
return (ch != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See the general contract of the <code>readByte</code> method of
|
||||||
|
* <code>DataInput</code>.
|
||||||
|
* <p>
|
||||||
|
* Bytes for this operation are read from the contained input stream.
|
||||||
|
*
|
||||||
|
* @return the next byte of this input stream as a signed 8-bit
|
||||||
|
* <code>byte</code>.
|
||||||
|
* @throws EOFException if this input stream has reached the end.
|
||||||
|
* @throws IOException the stream has been closed and the contained input stream
|
||||||
|
* does not support reading after close, or another I/O error
|
||||||
|
* occurs.
|
||||||
|
* @see FilterInputStream#in
|
||||||
|
*/
|
||||||
|
public final byte readByte() throws IOException {
|
||||||
|
int ch = in.read();
|
||||||
|
if (ch < 0)
|
||||||
|
throw new EOFException();
|
||||||
|
return (byte) (ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See the general contract of the <code>readUnsignedByte</code> method of
|
||||||
|
* <code>DataInput</code>.
|
||||||
|
* <p>
|
||||||
|
* Bytes for this operation are read from the contained input stream.
|
||||||
|
*
|
||||||
|
* @return the next byte of this input stream, interpreted as an unsigned
|
||||||
|
* 8-bit number.
|
||||||
|
* @throws EOFException if this input stream has reached the end.
|
||||||
|
* @throws IOException the stream has been closed and the contained input stream
|
||||||
|
* does not support reading after close, or another I/O error
|
||||||
|
* occurs.
|
||||||
|
* @see FilterInputStream#in
|
||||||
|
*/
|
||||||
|
public final int readUnsignedByte() throws IOException {
|
||||||
|
int ch = in.read();
|
||||||
|
if (ch < 0)
|
||||||
|
throw new EOFException();
|
||||||
|
return ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See the general contract of the <code>readShort</code> method of
|
||||||
|
* <code>DataInput</code>.
|
||||||
|
* <p>
|
||||||
|
* Bytes for this operation are read from the contained input stream.
|
||||||
|
*
|
||||||
|
* @return the next two bytes of this input stream, interpreted as a signed
|
||||||
|
* 16-bit number.
|
||||||
|
* @throws EOFException if this input stream reaches the end before reading two
|
||||||
|
* bytes.
|
||||||
|
* @throws IOException the stream has been closed and the contained input stream
|
||||||
|
* does not support reading after close, or another I/O error
|
||||||
|
* occurs.
|
||||||
|
* @see FilterInputStream#in
|
||||||
|
*/
|
||||||
|
public final short readShort() throws IOException {
|
||||||
|
int ch1 = in.read();
|
||||||
|
int ch2 = in.read();
|
||||||
|
if ((ch1 | ch2) < 0)
|
||||||
|
throw new EOFException();
|
||||||
|
return (short) ((ch1 << 0) + (ch2 << 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See the general contract of the <code>readUnsignedShort</code> method of
|
||||||
|
* <code>DataInput</code>.
|
||||||
|
* <p>
|
||||||
|
* Bytes for this operation are read from the contained input stream.
|
||||||
|
*
|
||||||
|
* @return the next two bytes of this input stream, interpreted as an
|
||||||
|
* unsigned 16-bit integer.
|
||||||
|
* @throws EOFException if this input stream reaches the end before reading two
|
||||||
|
* bytes.
|
||||||
|
* @throws IOException the stream has been closed and the contained input stream
|
||||||
|
* does not support reading after close, or another I/O error
|
||||||
|
* occurs.
|
||||||
|
* @see FilterInputStream#in
|
||||||
|
*/
|
||||||
|
public final int readUnsignedShort() throws IOException {
|
||||||
|
int ch1 = in.read();
|
||||||
|
int ch2 = in.read();
|
||||||
|
if ((ch1 | ch2) < 0)
|
||||||
|
throw new EOFException();
|
||||||
|
return (ch1 << 0) + (ch2 << 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See the general contract of the <code>readChar</code> method of
|
||||||
|
* <code>DataInput</code>.
|
||||||
|
* <p>
|
||||||
|
* Bytes for this operation are read from the contained input stream.
|
||||||
|
*
|
||||||
|
* @return the next two bytes of this input stream, interpreted as a
|
||||||
|
* <code>char</code>.
|
||||||
|
* @throws EOFException if this input stream reaches the end before reading two
|
||||||
|
* bytes.
|
||||||
|
* @throws IOException the stream has been closed and the contained input stream
|
||||||
|
* does not support reading after close, or another I/O error
|
||||||
|
* occurs.
|
||||||
|
* @see FilterInputStream#in
|
||||||
|
*/
|
||||||
|
public final char readChar() throws IOException {
|
||||||
|
int ch1 = in.read();
|
||||||
|
int ch2 = in.read();
|
||||||
|
if ((ch1 | ch2) < 0)
|
||||||
|
throw new EOFException();
|
||||||
|
return (char) ((ch1 << 0) + (ch2 << 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See the general contract of the <code>readInt</code> method of
|
||||||
|
* <code>DataInput</code>.
|
||||||
|
* <p>
|
||||||
|
* Bytes for this operation are read from the contained input stream.
|
||||||
|
*
|
||||||
|
* @return the next four bytes of this input stream, interpreted as an
|
||||||
|
* <code>int</code>.
|
||||||
|
* @throws EOFException if this input stream reaches the end before reading four
|
||||||
|
* bytes.
|
||||||
|
* @throws IOException the stream has been closed and the contained input stream
|
||||||
|
* does not support reading after close, or another I/O error
|
||||||
|
* occurs.
|
||||||
|
* @see FilterInputStream#in
|
||||||
|
*/
|
||||||
|
public final int readInt() throws IOException {
|
||||||
|
int ch1 = in.read();
|
||||||
|
int ch2 = in.read();
|
||||||
|
int ch3 = in.read();
|
||||||
|
int ch4 = in.read();
|
||||||
|
if ((ch1 | ch2 | ch3 | ch4) < 0)
|
||||||
|
throw new EOFException();
|
||||||
|
return ((ch1 << 0) + (ch2 << 8) + (ch3 << 16) + (ch4 << 24));
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte readBuffer[] = new byte[8];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See the general contract of the <code>readLong</code> method of
|
||||||
|
* <code>DataInput</code>.
|
||||||
|
* <p>
|
||||||
|
* Bytes for this operation are read from the contained input stream.
|
||||||
|
*
|
||||||
|
* @return the next eight bytes of this input stream, interpreted as a
|
||||||
|
* <code>long</code>.
|
||||||
|
* @throws EOFException if this input stream reaches the end before reading eight
|
||||||
|
* bytes.
|
||||||
|
* @throws IOException the stream has been closed and the contained input stream
|
||||||
|
* does not support reading after close, or another I/O error
|
||||||
|
* occurs.
|
||||||
|
* @see FilterInputStream#in
|
||||||
|
*/
|
||||||
|
public final long readLong() throws IOException {
|
||||||
|
readFully(readBuffer, 0, 8);
|
||||||
|
return (((long) readBuffer[7] << 56)
|
||||||
|
+ ((long) (readBuffer[6] & 255) << 48)
|
||||||
|
+ ((long) (readBuffer[5] & 255) << 40)
|
||||||
|
+ ((long) (readBuffer[4] & 255) << 32)
|
||||||
|
+ ((long) (readBuffer[3] & 255) << 24)
|
||||||
|
+ ((readBuffer[2] & 255) << 16) + ((readBuffer[1] & 255) << 8) + ((readBuffer[0] & 255) << 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See the general contract of the <code>readFloat</code> method of
|
||||||
|
* <code>DataInput</code>.
|
||||||
|
* <p>
|
||||||
|
* Bytes for this operation are read from the contained input stream.
|
||||||
|
*
|
||||||
|
* @return the next four bytes of this input stream, interpreted as a
|
||||||
|
* <code>float</code>.
|
||||||
|
* @throws EOFException if this input stream reaches the end before reading four
|
||||||
|
* bytes.
|
||||||
|
* @throws IOException the stream has been closed and the contained input stream
|
||||||
|
* does not support reading after close, or another I/O error
|
||||||
|
* occurs.
|
||||||
|
* @see java.io.DataInputStream#readInt()
|
||||||
|
* @see Float#intBitsToFloat(int)
|
||||||
|
*/
|
||||||
|
public final float readFloat() throws IOException {
|
||||||
|
return Float.intBitsToFloat(readInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See the general contract of the <code>readDouble</code> method of
|
||||||
|
* <code>DataInput</code>.
|
||||||
|
* <p>
|
||||||
|
* Bytes for this operation are read from the contained input stream.
|
||||||
|
*
|
||||||
|
* @return the next eight bytes of this input stream, interpreted as a
|
||||||
|
* <code>double</code>.
|
||||||
|
* @throws EOFException if this input stream reaches the end before reading eight
|
||||||
|
* bytes.
|
||||||
|
* @throws IOException the stream has been closed and the contained input stream
|
||||||
|
* does not support reading after close, or another I/O error
|
||||||
|
* occurs.
|
||||||
|
* @see java.io.DataInputStream#readLong()
|
||||||
|
* @see Double#longBitsToDouble(long)
|
||||||
|
*/
|
||||||
|
public final double readDouble() throws IOException {
|
||||||
|
return Double.longBitsToDouble(readLong());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See the general contract of the <code>readUTF</code> method of
|
||||||
|
* <code>DataInput</code>.
|
||||||
|
* <p>
|
||||||
|
* Bytes for this operation are read from the contained input stream.
|
||||||
|
*
|
||||||
|
* @return a Unicode string.
|
||||||
|
* @throws EOFException if this input stream reaches the end before reading all
|
||||||
|
* the bytes.
|
||||||
|
* @throws IOException the stream has been closed and the contained input stream
|
||||||
|
* does not support reading after close, or another I/O error
|
||||||
|
* occurs.
|
||||||
|
* @throws UTFDataFormatException if the bytes do not represent a valid modified UTF-8
|
||||||
|
* encoding of a string.
|
||||||
|
* @see java.io.DataInputStream#readUTF(DataInput)
|
||||||
|
*/
|
||||||
|
public final String readUTF() throws IOException {
|
||||||
|
return readUTF(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* working arrays initialized on demand by readUTF
|
||||||
|
*/
|
||||||
|
private byte bytearr[] = new byte[80];
|
||||||
|
private char chararr[] = new char[80];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads from the stream <code>in</code> a representation of a Unicode
|
||||||
|
* character string encoded in <a
|
||||||
|
* href="DataInput.html#modified-utf-8">modified UTF-8</a> format; this
|
||||||
|
* string of characters is then returned as a <code>String</code>. The
|
||||||
|
* details of the modified UTF-8 representation are exactly the same as for
|
||||||
|
* the <code>readUTF</code> method of <code>DataInput</code>.
|
||||||
|
*
|
||||||
|
* @param in a data input stream.
|
||||||
|
* @return a Unicode string.
|
||||||
|
* @throws EOFException if the input stream reaches the end before all the bytes.
|
||||||
|
* @throws IOException the stream has been closed and the contained input stream
|
||||||
|
* does not support reading after close, or another I/O error
|
||||||
|
* occurs.
|
||||||
|
* @throws UTFDataFormatException if the bytes do not represent a valid modified UTF-8
|
||||||
|
* encoding of a Unicode string.
|
||||||
|
* @see java.io.DataInputStream#readUnsignedShort()
|
||||||
|
*/
|
||||||
|
public final static String readUTF(DataInput in) throws IOException {
|
||||||
|
int utflen = in.readUnsignedShort();
|
||||||
|
byte[] bytearr = null;
|
||||||
|
char[] chararr = null;
|
||||||
|
if (in instanceof LittleEndianDataInputStream) {
|
||||||
|
LittleEndianDataInputStream dis = (LittleEndianDataInputStream) in;
|
||||||
|
if (dis.bytearr.length < utflen) {
|
||||||
|
dis.bytearr = new byte[utflen * 2];
|
||||||
|
dis.chararr = new char[utflen * 2];
|
||||||
|
}
|
||||||
|
chararr = dis.chararr;
|
||||||
|
bytearr = dis.bytearr;
|
||||||
|
} else {
|
||||||
|
bytearr = new byte[utflen];
|
||||||
|
chararr = new char[utflen];
|
||||||
|
}
|
||||||
|
|
||||||
|
int c, char2, char3;
|
||||||
|
int count = 0;
|
||||||
|
int chararr_count = 0;
|
||||||
|
|
||||||
|
in.readFully(bytearr, 0, utflen);
|
||||||
|
|
||||||
|
while (count < utflen) {
|
||||||
|
c = (int) bytearr[count] & 0xff;
|
||||||
|
if (c > 127)
|
||||||
|
break;
|
||||||
|
count++;
|
||||||
|
chararr[chararr_count++] = (char) c;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (count < utflen) {
|
||||||
|
c = (int) bytearr[count] & 0xff;
|
||||||
|
switch (c >> 4) {
|
||||||
|
case 0:
|
||||||
|
case 1:
|
||||||
|
case 2:
|
||||||
|
case 3:
|
||||||
|
case 4:
|
||||||
|
case 5:
|
||||||
|
case 6:
|
||||||
|
case 7:
|
||||||
|
/* 0xxxxxxx */
|
||||||
|
count++;
|
||||||
|
chararr[chararr_count++] = (char) c;
|
||||||
|
break;
|
||||||
|
case 12:
|
||||||
|
case 13:
|
||||||
|
/* 110x xxxx 10xx xxxx */
|
||||||
|
count += 2;
|
||||||
|
if (count > utflen)
|
||||||
|
throw new UTFDataFormatException(
|
||||||
|
"malformed input: partial character at end");
|
||||||
|
char2 = (int) bytearr[count - 1];
|
||||||
|
if ((char2 & 0xC0) != 0x80)
|
||||||
|
throw new UTFDataFormatException(
|
||||||
|
"malformed input around byte " + count);
|
||||||
|
chararr[chararr_count++] = (char) (((c & 0x1F) << 6) | (char2 & 0x3F));
|
||||||
|
break;
|
||||||
|
case 14:
|
||||||
|
/* 1110 xxxx 10xx xxxx 10xx xxxx */
|
||||||
|
count += 3;
|
||||||
|
if (count > utflen)
|
||||||
|
throw new UTFDataFormatException(
|
||||||
|
"malformed input: partial character at end");
|
||||||
|
char2 = (int) bytearr[count - 2];
|
||||||
|
char3 = (int) bytearr[count - 1];
|
||||||
|
if (((char2 & 0xC0) != 0x80) || ((char3 & 0xC0) != 0x80))
|
||||||
|
throw new UTFDataFormatException(
|
||||||
|
"malformed input around byte " + (count - 1));
|
||||||
|
chararr[chararr_count++] = (char) (((c & 0x0F) << 12)
|
||||||
|
| ((char2 & 0x3F) << 6) | ((char3 & 0x3F) << 0));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
/* 10xx xxxx, 1111 xxxx */
|
||||||
|
throw new UTFDataFormatException("malformed input around byte "
|
||||||
|
+ count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The number of chars produced may be less than utflen
|
||||||
|
return new String(chararr, 0, chararr_count);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,403 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018 m2049r
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.m2049r.levin.util;
|
||||||
|
|
||||||
|
import java.io.DataOutput;
|
||||||
|
import java.io.FilterOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.UTFDataFormatException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A little endian java.io.DataOutputStream
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class LittleEndianDataOutputStream extends FilterOutputStream implements
|
||||||
|
DataOutput {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of bytes written to the data output stream so far. If this
|
||||||
|
* counter overflows, it will be wrapped to Integer.MAX_VALUE.
|
||||||
|
*/
|
||||||
|
protected int written;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new data output stream to write data to the specified
|
||||||
|
* underlying output stream. The counter <code>written</code> is set to
|
||||||
|
* zero.
|
||||||
|
*
|
||||||
|
* @param out the underlying output stream, to be saved for later use.
|
||||||
|
* @see FilterOutputStream#out
|
||||||
|
*/
|
||||||
|
public LittleEndianDataOutputStream(OutputStream out) {
|
||||||
|
super(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increases the written counter by the specified value until it reaches
|
||||||
|
* Integer.MAX_VALUE.
|
||||||
|
*/
|
||||||
|
private void incCount(int value) {
|
||||||
|
int temp = written + value;
|
||||||
|
if (temp < 0) {
|
||||||
|
temp = Integer.MAX_VALUE;
|
||||||
|
}
|
||||||
|
written = temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes the specified byte (the low eight bits of the argument
|
||||||
|
* <code>b</code>) to the underlying output stream. If no exception is
|
||||||
|
* thrown, the counter <code>written</code> is incremented by <code>1</code>
|
||||||
|
* .
|
||||||
|
* <p>
|
||||||
|
* Implements the <code>write</code> method of <code>OutputStream</code>.
|
||||||
|
*
|
||||||
|
* @param b the <code>byte</code> to be written.
|
||||||
|
* @throws IOException if an I/O error occurs.
|
||||||
|
* @see FilterOutputStream#out
|
||||||
|
*/
|
||||||
|
public synchronized void write(int b) throws IOException {
|
||||||
|
out.write(b);
|
||||||
|
incCount(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes <code>len</code> bytes from the specified byte array starting at
|
||||||
|
* offset <code>off</code> to the underlying output stream. If no exception
|
||||||
|
* is thrown, the counter <code>written</code> is incremented by
|
||||||
|
* <code>len</code>.
|
||||||
|
*
|
||||||
|
* @param b the data.
|
||||||
|
* @param off the start offset in the data.
|
||||||
|
* @param len the number of bytes to write.
|
||||||
|
* @throws IOException if an I/O error occurs.
|
||||||
|
* @see FilterOutputStream#out
|
||||||
|
*/
|
||||||
|
public synchronized void write(byte b[], int off, int len)
|
||||||
|
throws IOException {
|
||||||
|
out.write(b, off, len);
|
||||||
|
incCount(len);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flushes this data output stream. This forces any buffered output bytes to
|
||||||
|
* be written out to the stream.
|
||||||
|
* <p>
|
||||||
|
* The <code>flush</code> method of <code>DataOutputStream</code> calls the
|
||||||
|
* <code>flush</code> method of its underlying output stream.
|
||||||
|
*
|
||||||
|
* @throws IOException if an I/O error occurs.
|
||||||
|
* @see FilterOutputStream#out
|
||||||
|
* @see OutputStream#flush()
|
||||||
|
*/
|
||||||
|
public void flush() throws IOException {
|
||||||
|
out.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a <code>boolean</code> to the underlying output stream as a 1-byte
|
||||||
|
* value. The value <code>true</code> is written out as the value
|
||||||
|
* <code>(byte)1</code>; the value <code>false</code> is written out as the
|
||||||
|
* value <code>(byte)0</code>. If no exception is thrown, the counter
|
||||||
|
* <code>written</code> is incremented by <code>1</code>.
|
||||||
|
*
|
||||||
|
* @param v a <code>boolean</code> value to be written.
|
||||||
|
* @throws IOException if an I/O error occurs.
|
||||||
|
* @see FilterOutputStream#out
|
||||||
|
*/
|
||||||
|
public final void writeBoolean(boolean v) throws IOException {
|
||||||
|
out.write(v ? 1 : 0);
|
||||||
|
incCount(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes out a <code>byte</code> to the underlying output stream as a
|
||||||
|
* 1-byte value. If no exception is thrown, the counter <code>written</code>
|
||||||
|
* is incremented by <code>1</code>.
|
||||||
|
*
|
||||||
|
* @param v a <code>byte</code> value to be written.
|
||||||
|
* @throws IOException if an I/O error occurs.
|
||||||
|
* @see FilterOutputStream#out
|
||||||
|
*/
|
||||||
|
public final void writeByte(int v) throws IOException {
|
||||||
|
out.write(v);
|
||||||
|
incCount(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a <code>short</code> to the underlying output stream as two bytes,
|
||||||
|
* low byte first. If no exception is thrown, the counter
|
||||||
|
* <code>written</code> is incremented by <code>2</code>.
|
||||||
|
*
|
||||||
|
* @param v a <code>short</code> to be written.
|
||||||
|
* @throws IOException if an I/O error occurs.
|
||||||
|
* @see FilterOutputStream#out
|
||||||
|
*/
|
||||||
|
public final void writeShort(int v) throws IOException {
|
||||||
|
out.write((v >>> 0) & 0xFF);
|
||||||
|
out.write((v >>> 8) & 0xFF);
|
||||||
|
incCount(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a <code>char</code> to the underlying output stream as a 2-byte
|
||||||
|
* value, low byte first. If no exception is thrown, the counter
|
||||||
|
* <code>written</code> is incremented by <code>2</code>.
|
||||||
|
*
|
||||||
|
* @param v a <code>char</code> value to be written.
|
||||||
|
* @throws IOException if an I/O error occurs.
|
||||||
|
* @see FilterOutputStream#out
|
||||||
|
*/
|
||||||
|
public final void writeChar(int v) throws IOException {
|
||||||
|
out.write((v >>> 0) & 0xFF);
|
||||||
|
out.write((v >>> 8) & 0xFF);
|
||||||
|
incCount(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes an <code>int</code> to the underlying output stream as four bytes,
|
||||||
|
* low byte first. If no exception is thrown, the counter
|
||||||
|
* <code>written</code> is incremented by <code>4</code>.
|
||||||
|
*
|
||||||
|
* @param v an <code>int</code> to be written.
|
||||||
|
* @throws IOException if an I/O error occurs.
|
||||||
|
* @see FilterOutputStream#out
|
||||||
|
*/
|
||||||
|
public final void writeInt(int v) throws IOException {
|
||||||
|
out.write((v >>> 0) & 0xFF);
|
||||||
|
out.write((v >>> 8) & 0xFF);
|
||||||
|
out.write((v >>> 16) & 0xFF);
|
||||||
|
out.write((v >>> 24) & 0xFF);
|
||||||
|
incCount(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte writeBuffer[] = new byte[8];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a <code>long</code> to the underlying output stream as eight
|
||||||
|
* bytes, low byte first. In no exception is thrown, the counter
|
||||||
|
* <code>written</code> is incremented by <code>8</code>.
|
||||||
|
*
|
||||||
|
* @param v a <code>long</code> to be written.
|
||||||
|
* @throws IOException if an I/O error occurs.
|
||||||
|
* @see FilterOutputStream#out
|
||||||
|
*/
|
||||||
|
public final void writeLong(long v) throws IOException {
|
||||||
|
writeBuffer[7] = (byte) (v >>> 56);
|
||||||
|
writeBuffer[6] = (byte) (v >>> 48);
|
||||||
|
writeBuffer[5] = (byte) (v >>> 40);
|
||||||
|
writeBuffer[4] = (byte) (v >>> 32);
|
||||||
|
writeBuffer[3] = (byte) (v >>> 24);
|
||||||
|
writeBuffer[2] = (byte) (v >>> 16);
|
||||||
|
writeBuffer[1] = (byte) (v >>> 8);
|
||||||
|
writeBuffer[0] = (byte) (v >>> 0);
|
||||||
|
out.write(writeBuffer, 0, 8);
|
||||||
|
incCount(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the float argument to an <code>int</code> using the
|
||||||
|
* <code>floatToIntBits</code> method in class <code>Float</code>, and then
|
||||||
|
* writes that <code>int</code> value to the underlying output stream as a
|
||||||
|
* 4-byte quantity, low byte first. If no exception is thrown, the counter
|
||||||
|
* <code>written</code> is incremented by <code>4</code>.
|
||||||
|
*
|
||||||
|
* @param v a <code>float</code> value to be written.
|
||||||
|
* @throws IOException if an I/O error occurs.
|
||||||
|
* @see FilterOutputStream#out
|
||||||
|
* @see Float#floatToIntBits(float)
|
||||||
|
*/
|
||||||
|
public final void writeFloat(float v) throws IOException {
|
||||||
|
writeInt(Float.floatToIntBits(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the double argument to a <code>long</code> using the
|
||||||
|
* <code>doubleToLongBits</code> method in class <code>Double</code>, and
|
||||||
|
* then writes that <code>long</code> value to the underlying output stream
|
||||||
|
* as an 8-byte quantity, low byte first. If no exception is thrown, the
|
||||||
|
* counter <code>written</code> is incremented by <code>8</code>.
|
||||||
|
*
|
||||||
|
* @param v a <code>double</code> value to be written.
|
||||||
|
* @throws IOException if an I/O error occurs.
|
||||||
|
* @see FilterOutputStream#out
|
||||||
|
* @see Double#doubleToLongBits(double)
|
||||||
|
*/
|
||||||
|
public final void writeDouble(double v) throws IOException {
|
||||||
|
writeLong(Double.doubleToLongBits(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes out the string to the underlying output stream as a sequence of
|
||||||
|
* bytes. Each character in the string is written out, in sequence, by
|
||||||
|
* discarding its high eight bits. If no exception is thrown, the counter
|
||||||
|
* <code>written</code> is incremented by the length of <code>s</code>.
|
||||||
|
*
|
||||||
|
* @param s a string of bytes to be written.
|
||||||
|
* @throws IOException if an I/O error occurs.
|
||||||
|
* @see FilterOutputStream#out
|
||||||
|
*/
|
||||||
|
public final void writeBytes(String s) throws IOException {
|
||||||
|
int len = s.length();
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
out.write((byte) s.charAt(i));
|
||||||
|
}
|
||||||
|
incCount(len);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a string to the underlying output stream as a sequence of
|
||||||
|
* characters. Each character is written to the data output stream as if by
|
||||||
|
* the <code>writeChar</code> method. If no exception is thrown, the counter
|
||||||
|
* <code>written</code> is incremented by twice the length of <code>s</code>
|
||||||
|
* .
|
||||||
|
*
|
||||||
|
* @param s a <code>String</code> value to be written.
|
||||||
|
* @throws IOException if an I/O error occurs.
|
||||||
|
* @see java.io.DataOutputStream#writeChar(int)
|
||||||
|
* @see FilterOutputStream#out
|
||||||
|
*/
|
||||||
|
public final void writeChars(String s) throws IOException {
|
||||||
|
int len = s.length();
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
int v = s.charAt(i);
|
||||||
|
out.write((v >>> 0) & 0xFF);
|
||||||
|
out.write((v >>> 8) & 0xFF);
|
||||||
|
}
|
||||||
|
incCount(len * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a string to the underlying output stream using <a
|
||||||
|
* href="DataInput.html#modified-utf-8">modified UTF-8</a> encoding in a
|
||||||
|
* machine-independent manner.
|
||||||
|
* <p>
|
||||||
|
* First, two bytes are written to the output stream as if by the
|
||||||
|
* <code>writeShort</code> method giving the number of bytes to follow. This
|
||||||
|
* value is the number of bytes actually written out, not the length of the
|
||||||
|
* string. Following the length, each character of the string is output, in
|
||||||
|
* sequence, using the modified UTF-8 encoding for the character. If no
|
||||||
|
* exception is thrown, the counter <code>written</code> is incremented by
|
||||||
|
* the total number of bytes written to the output stream. This will be at
|
||||||
|
* least two plus the length of <code>str</code>, and at most two plus
|
||||||
|
* thrice the length of <code>str</code>.
|
||||||
|
*
|
||||||
|
* @param str a string to be written.
|
||||||
|
* @throws IOException if an I/O error occurs.
|
||||||
|
*/
|
||||||
|
public final void writeUTF(String str) throws IOException {
|
||||||
|
writeUTF(str, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* bytearr is initialized on demand by writeUTF
|
||||||
|
*/
|
||||||
|
private byte[] bytearr = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a string to the specified DataOutput using <a
|
||||||
|
* href="DataInput.html#modified-utf-8">modified UTF-8</a> encoding in a
|
||||||
|
* machine-independent manner.
|
||||||
|
* <p>
|
||||||
|
* First, two bytes are written to out as if by the <code>writeShort</code>
|
||||||
|
* method giving the number of bytes to follow. This value is the number of
|
||||||
|
* bytes actually written out, not the length of the string. Following the
|
||||||
|
* length, each character of the string is output, in sequence, using the
|
||||||
|
* modified UTF-8 encoding for the character. If no exception is thrown, the
|
||||||
|
* counter <code>written</code> is incremented by the total number of bytes
|
||||||
|
* written to the output stream. This will be at least two plus the length
|
||||||
|
* of <code>str</code>, and at most two plus thrice the length of
|
||||||
|
* <code>str</code>.
|
||||||
|
*
|
||||||
|
* @param str a string to be written.
|
||||||
|
* @param out destination to write to
|
||||||
|
* @return The number of bytes written out.
|
||||||
|
* @throws IOException if an I/O error occurs.
|
||||||
|
*/
|
||||||
|
static int writeUTF(String str, DataOutput out) throws IOException {
|
||||||
|
int strlen = str.length();
|
||||||
|
int utflen = 0;
|
||||||
|
int c, count = 0;
|
||||||
|
|
||||||
|
/* use charAt instead of copying String to char array */
|
||||||
|
for (int i = 0; i < strlen; i++) {
|
||||||
|
c = str.charAt(i);
|
||||||
|
if ((c >= 0x0001) && (c <= 0x007F)) {
|
||||||
|
utflen++;
|
||||||
|
} else if (c > 0x07FF) {
|
||||||
|
utflen += 3;
|
||||||
|
} else {
|
||||||
|
utflen += 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (utflen > 65535)
|
||||||
|
throw new UTFDataFormatException("encoded string too long: "
|
||||||
|
+ utflen + " bytes");
|
||||||
|
|
||||||
|
byte[] bytearr = null;
|
||||||
|
if (out instanceof LittleEndianDataOutputStream) {
|
||||||
|
LittleEndianDataOutputStream dos = (LittleEndianDataOutputStream) out;
|
||||||
|
if (dos.bytearr == null || (dos.bytearr.length < (utflen + 2)))
|
||||||
|
dos.bytearr = new byte[(utflen * 2) + 2];
|
||||||
|
bytearr = dos.bytearr;
|
||||||
|
} else {
|
||||||
|
bytearr = new byte[utflen + 2];
|
||||||
|
}
|
||||||
|
|
||||||
|
bytearr[count++] = (byte) ((utflen >>> 8) & 0xFF);
|
||||||
|
bytearr[count++] = (byte) ((utflen >>> 0) & 0xFF);
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
for (i = 0; i < strlen; i++) {
|
||||||
|
c = str.charAt(i);
|
||||||
|
if (!((c >= 0x0001) && (c <= 0x007F)))
|
||||||
|
break;
|
||||||
|
bytearr[count++] = (byte) c;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (; i < strlen; i++) {
|
||||||
|
c = str.charAt(i);
|
||||||
|
if ((c >= 0x0001) && (c <= 0x007F)) {
|
||||||
|
bytearr[count++] = (byte) c;
|
||||||
|
|
||||||
|
} else if (c > 0x07FF) {
|
||||||
|
bytearr[count++] = (byte) (0xE0 | ((c >> 12) & 0x0F));
|
||||||
|
bytearr[count++] = (byte) (0x80 | ((c >> 6) & 0x3F));
|
||||||
|
bytearr[count++] = (byte) (0x80 | ((c >> 0) & 0x3F));
|
||||||
|
} else {
|
||||||
|
bytearr[count++] = (byte) (0xC0 | ((c >> 6) & 0x1F));
|
||||||
|
bytearr[count++] = (byte) (0x80 | ((c >> 0) & 0x3F));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.write(bytearr, 0, utflen + 2);
|
||||||
|
return utflen + 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current value of the counter <code>written</code>, the number
|
||||||
|
* of bytes written to this data output stream so far. If the counter
|
||||||
|
* overflows, it will be wrapped to Integer.MAX_VALUE.
|
||||||
|
*
|
||||||
|
* @return the value of the <code>written</code> field.
|
||||||
|
* @see java.io.DataOutputStream#written
|
||||||
|
*/
|
||||||
|
public final int size() {
|
||||||
|
return written;
|
||||||
|
}
|
||||||
|
}
|
313
app/src/main/java/com/m2049r/xmrwallet/BaseActivity.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
615
app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1469
app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java
Normal file
563
app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
39
app/src/main/java/com/m2049r/xmrwallet/MainActivity.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
589
app/src/main/java/com/m2049r/xmrwallet/NodeFragment.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
469
app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
96
app/src/main/java/com/m2049r/xmrwallet/ScannerFragment.java
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
73
app/src/main/java/com/m2049r/xmrwallet/SecureActivity.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
125
app/src/main/java/com/m2049r/xmrwallet/SettingsFragment.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
241
app/src/main/java/com/m2049r/xmrwallet/SubaddressFragment.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
406
app/src/main/java/com/m2049r/xmrwallet/TxFragment.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
1220
app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java
Normal file
559
app/src/main/java/com/m2049r/xmrwallet/WalletFragment.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
232
app/src/main/java/com/m2049r/xmrwallet/data/BarcodeData.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
89
app/src/main/java/com/m2049r/xmrwallet/data/Crypto.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
371
app/src/main/java/com/m2049r/xmrwallet/data/Node.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
303
app/src/main/java/com/m2049r/xmrwallet/data/NodeInfo.java
Normal 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() ? " .onion " : ""), " " + 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);
|
||||||
|
}
|
||||||
|
}
|
39
app/src/main/java/com/m2049r/xmrwallet/data/PendingTx.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
63
app/src/main/java/com/m2049r/xmrwallet/data/Subaddress.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
149
app/src/main/java/com/m2049r/xmrwallet/data/TxData.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
101
app/src/main/java/com/m2049r/xmrwallet/data/TxDataBtc.java
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
99
app/src/main/java/com/m2049r/xmrwallet/data/UserNotes.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
115
app/src/main/java/com/m2049r/xmrwallet/dialog/HelpFragment.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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() {
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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() ? "" : (" " + 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
155
app/src/main/java/com/m2049r/xmrwallet/ledger/Instruction.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
240
app/src/main/java/com/m2049r/xmrwallet/ledger/Ledger.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
57
app/src/main/java/com/m2049r/xmrwallet/model/Transfer.java
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
507
app/src/main/java/com/m2049r/xmrwallet/model/Wallet.java
Normal 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();
|
||||||
|
|
||||||
|
}
|
@ -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();
|
||||||
|
|
||||||
|
}
|
341
app/src/main/java/com/m2049r/xmrwallet/model/WalletManager.java
Normal 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();
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|