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
|
||||
*.class
|
||||
|
||||
# Log file
|
||||
*.log
|
||||
|
||||
# BlueJ files
|
||||
*.ctxt
|
||||
|
||||
# Mobile Tools for Java (J2ME)
|
||||
.mtj.tmp/
|
||||
|
||||
# Package Files #
|
||||
*.jar
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
.gradle
|
||||
/build
|
||||
*.iml
|
||||
/.idea
|
||||
/local.properties
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.DS_Store
|
||||
/app/build
|
||||
/app/release
|
||||
/app/alpha
|
||||
/app/prod
|
||||
/app/alphaMainnet
|
||||
/app/prodMainnet
|
||||
/app/alphaStagenet
|
||||
/app/prodStagenet
|
||||
/app/.cxx
|
||||
/monerujo.id
|
||||
|
239
app/CMakeLists.txt
Normal file
@ -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;
|
||||
}
|
||||
}
|