diff --git a/.gitignore b/.gitignore index 532b316fdc..002a926ab0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,7 @@ .gradle /build *.iml -/.idea/libraries -/.idea/workspace.xml +/.idea /local.properties /captures .externalNativeBuild diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000000..30aa626c23 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000000..99202cc2d6 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index e40edcc191..5accb16c98 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,23 @@ # Monerujo -Another Android Monero Wallet +Another Android Monero Wallet for Monero + **(not + Monero Classic, + Monero-Classic, + Monero Zero, + Monero Original, + Monero C, + Monero V)** ### QUICKSTART - Download the APK for the most current release [here](https://github.com/m2049r/xmrwallet/releases) and install it +- Alternatively add our F-Droid repo https://f-droid.monerujo.io/fdroid/repo with fingerpint ```A8 2C 68 E1 4A F0 AA 6A 2E C2 0E 6B 27 2E FF 25 E5 A0 38 F3 F6 58 84 31 6E 0F 5E 0D 91 E7 B7 13``` to your F-Droid client - Run the App and select "Generate Wallet" to create a new wallet or recover a wallet - Advanced users can copy over synced wallet files (all files) onto sdcard in directory Monerujo (created first time App is started) - See the [FAQ](doc/FAQ.md) +## Translations +Help us translate Monerujo! You can find instructions [On Taiga](https://taiga.getmonero.org/project/erciccione-monero-localization/wiki/monerujo), and if you need help/support, open an issue or contact the Localization Workgroup. You can find us on the freenode channel `#monero-translations`, also relayed on [MatterMost](https://mattermost.getmonero.org/monero/channels/monero-translations), and matrix/riot. + ### Disclaimer You may lose all your Moneroj if you use this App. Be cautious when spending on the mainnet. @@ -23,6 +34,8 @@ You may lose all your Moneroj if you use this App. Be cautious when spending on - see taiga.getmonero.org & issues on github ### Issues / Pitfalls +- Users of Zenfone MAX & Zenfone 2 Laser (possibly others) **MUST** use the armeabi-v7a APK as the arm64-v8a build uses hardware AES +functionality these models don't have. - You should backup your wallet files in the "monerujo" folder periodically. - Also note, that on some devices the backups will only be visible on a PC over USB after a reboot of the device (it's an Android bug/feature) - Created wallets on a private testnet are unusable because the restore height is set to that @@ -33,8 +46,8 @@ The official monero client shows the same behaviour. No need to build. Binaries are included: - openssl-1.0.2l -- monero-v0.11.1.0 -- boost_1_64_0 +- monero-v0.12 +- boost_1_58_0 If you want to build them yourself (recommended) check out [the instructions](doc/BUILDING-external-libs.md) diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index a8adc8d7cd..7287aac148 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -60,9 +60,13 @@ set_target_properties(boost_wserialization PROPERTIES IMPORTED_LOCATION ${EXTERNAL_LIBS_DIR}/boost/lib/${ANDROID_ABI}/libboost_wserialization.a) ############# -# Monero set(libs_to_merge wallet cryptonote_core cryptonote_basic mnemonics common cncrypto ringct) +# Monero ############# +add_library(wallet_api STATIC IMPORTED) +set_target_properties(wallet_api PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/libwallet_api.a) + add_library(wallet STATIC IMPORTED) set_target_properties(wallet PROPERTIES IMPORTED_LOCATION ${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/libwallet.a) @@ -91,11 +95,9 @@ add_library(ringct STATIC IMPORTED) set_target_properties(ringct PROPERTIES IMPORTED_LOCATION ${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/libringct.a) -##### - -add_library(p2p STATIC IMPORTED) -set_target_properties(p2p PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/libp2p.a) +add_library(ringct_basic STATIC IMPORTED) +set_target_properties(ringct_basic PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/libringct_basic.a) add_library(blockchain_db STATIC IMPORTED) set_target_properties(blockchain_db PROPERTIES IMPORTED_LOCATION @@ -113,7 +115,6 @@ add_library(unbound STATIC IMPORTED) set_target_properties(unbound PROPERTIES IMPORTED_LOCATION ${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/libunbound.a) -#### add_library(epee STATIC IMPORTED) set_target_properties(epee PROPERTIES IMPORTED_LOCATION ${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/libepee.a) @@ -122,9 +123,21 @@ add_library(blocks STATIC IMPORTED) set_target_properties(blocks PROPERTIES IMPORTED_LOCATION ${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/libblocks.a) -add_library(miniupnpc STATIC IMPORTED) -set_target_properties(miniupnpc PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/libminiupnpc.a) +add_library(checkpoints STATIC IMPORTED) +set_target_properties(checkpoints PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/libcheckpoints.a) + +add_library(device STATIC IMPORTED) +set_target_properties(device PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/libdevice.a) + +add_library(multisig STATIC IMPORTED) +set_target_properties(multisig PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/libmultisig.a) + +add_library(version STATIC IMPORTED) +set_target_properties(version PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/libversion.a) ############# # System @@ -137,23 +150,26 @@ include_directories( ${EXTERNAL_LIBS_DIR}/monero/include ) message(STATUS EXTERNAL_LIBS_DIR : ${EXTERNAL_LIBS_DIR}) target_link_libraries( monerujo + + wallet_api wallet cryptonote_core cryptonote_basic mnemonics ringct + ringct_basic common cncrypto - blockchain_db lmdb easylogging unbound - p2p - epee blocks - miniupnpc + checkpoints + device + multisig + version boost_chrono boost_date_time diff --git a/app/build.gradle b/app/build.gradle index 197003029d..ab06ef4357 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,15 +1,14 @@ apply plugin: 'com.android.application' -apply plugin: 'witness' android { compileSdkVersion 25 - buildToolsVersion '26.0.2' + buildToolsVersion '27.0.3' defaultConfig { applicationId "com.m2049r.xmrwallet" minSdkVersion 21 targetSdkVersion 25 - versionCode 73 - versionName "1.3.13 'Satoshis Dream'" + versionCode 91 + versionName "1.5.1 'CrAzY Nacho'" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" externalNativeBuild { cmake { @@ -17,9 +16,6 @@ android { arguments '-DANDROID_STL=c++_shared' } } - ndk { - abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' - } } buildTypes { @@ -42,7 +38,7 @@ android { enable true reset() include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' - universalApk false + universalApk true } } @@ -52,57 +48,36 @@ android { // APKs for the same app that all have the same version information. android.applicationVariants.all { variant -> // Assigns a different version code for each output APK. - variant.outputs.each { + variant.outputs.all { output -> def abiName = output.getFilter(com.android.build.OutputFile.ABI) output.versionCodeOverride = abiCodes.get(abiName, 0) + 10 * variant.versionCode + + if (abiName == null) abiName = "universal" + def v = "${variant.versionName}".replaceFirst(" .*\$", "").replace(".", "x") + outputFileName = "$rootProject.ext.apkName-" + v + "_" + abiName + ".apk" } } } dependencies { - compile 'com.android.support:appcompat-v7:25.4.0' - compile 'com.android.support:design:25.4.0' - compile 'com.android.support:support-v4:25.4.0' - compile 'com.android.support:recyclerview-v7:25.4.0' - compile 'com.android.support:cardview-v7:25.4.0' - compile 'com.android.support.constraint:constraint-layout:1.0.2' - compile 'me.dm7.barcodescanner:zxing:1.9.8' - - compile "com.squareup.okhttp3:okhttp:$rootProject.ext.okHttpVersion" - compile "com.jakewharton.timber:timber:$rootProject.ext.timberVersion" + implementation 'com.android.support:appcompat-v7:25.4.0' + implementation 'com.android.support:design:25.4.0' + implementation 'com.android.support:support-v4:25.4.0' + implementation 'com.android.support:recyclerview-v7:25.4.0' + implementation 'com.android.support:cardview-v7:25.4.0' + implementation 'me.dm7.barcodescanner:zxing:1.9.8' - compile 'com.nulab-inc:zxcvbn:1.2.3' - - testCompile "junit:junit:$rootProject.ext.junitVersion" - testCompile "org.mockito:mockito-all:$rootProject.ext.mockitoVersion" - testCompile "com.squareup.okhttp3:mockwebserver:$rootProject.ext.okHttpVersion" - testCompile 'org.json:json:20140107' - testCompile 'net.jodah:concurrentunit:0.4.2' -} + implementation "com.squareup.okhttp3:okhttp:$rootProject.ext.okHttpVersion" + implementation "com.jakewharton.timber:timber:$rootProject.ext.timberVersion" -dependencyVerification { - verify = [ - 'com.android.support:design:3f409bf2019967ffc344cfaf11e52131fac982468a1707aaeb25bf3c52838966', - 'com.android.support:appcompat-v7:70551e62660db15b790c5275f56b9de4dd9407d1494d07c8f3dd5698f3638677', - 'com.android.support:transition:848270144fb180efd2bf928a00ed176dbbc5290badfd638390ffba90088df8b3', - 'me.dm7.barcodescanner:zxing:d43973c9527c23fa8e6d338c6a2c458e373ce1ac6bcaa3bc41d11ae49116000d', - 'me.dm7.barcodescanner:core:a5c8a704089b58029db166172ed8e55d756877d010a85a0b1c94fdc96ffb8f9a', - 'com.android.support:support-v4:ee44c481a1f4d6978568e223e8125379b52b2ececdd53450e09ebae144bd377d', - 'com.android.support:recyclerview-v7:a2fe121f9d01ed8980e97095b4a3fe9700a0aa0a7d4b0f8c594f765ad8455a0d', - 'com.android.support:cardview-v7:f3fbbe1fcfdbec7333c6a2c516c5fd511a909d1975271818e268d6fe297d8c70', - 'com.android.support.constraint:constraint-layout:b0c688cc2b7172608f8153a689d746da40f71e52d7e2fe2bfd9df2f92db77085', - 'com.android.support:animated-vector-drawable:628ab1d56a6ee4cbedf32617af8b2a1fe02964ed0628e8f898cc09ddba6e1835', - 'com.android.support:support-vector-drawable:077009d13882ee96f061e4bc2dbe7cce7ae1762d8297592a787ff741afbfb1f2', - 'com.android.support:support-fragment:316d35d4d2d2902057efad104a73e4bdb50bee260a7075678185b8cd71170945', - 'com.android.support:support-core-ui:e72ae29b823889686cff6fcb948d6745c2baf6d4c2af4fdffa1ec1e42e3833a3', - 'com.android.support:support-media-compat:566a161d9cb0083ef62a53e46b71ce5b3d455b8635b1a0a4ae28d96d4b583de8', - 'com.android.support:support-core-utils:34b8437dfa95ff28d29cf57ffa3b1354a9fa9bfe4059f0fd5ce2f5e4326a1748', - 'com.android.support:support-compat:54019c63614ce08b02d7b9605490cd2b29ba5b2505f394a9517450b5f72b30ca', - 'com.android.support:support-annotations:a774272036941b4e912eb426d70c848bde7f06a3bf5fb491f75a427dc6595270', - 'com.android.support.constraint:constraint-layout-solver:8c62525a9bc5cff5633a96cb9b32fffeccaf41b8841aa87fc22607070dea9b8d', - 'com.google.zxing:core:bba7724e02a997cec38213af77133ee8e24b0d5cf5fa7ecbc16a4fa93f11ee0d', - 'com.squareup.okio:okio:734269c3ebc5090e3b23566db558f421f0b4027277c79ad5d176b8ec168bb850', - 'com.squareup.okhttp3:okhttp:7265adbd6f028aade307f58569d814835cd02bc9beffb70c25f72c9de50d61c4', - ] + implementation 'com.nulab-inc:zxcvbn:1.2.3' + //implementation 'sunjce_provider:sunjce_provider:1.0' + compile files('lib/sunjce_provider.jar') + compile files('lib/jacksum.jar') + testImplementation "junit:junit:$rootProject.ext.junitVersion" + testImplementation "org.mockito:mockito-all:$rootProject.ext.mockitoVersion" + testImplementation "com.squareup.okhttp3:mockwebserver:$rootProject.ext.okHttpVersion" + testImplementation 'org.json:json:20140107' + testImplementation 'net.jodah:concurrentunit:0.4.2' } diff --git a/app/lib/jacksum.jar b/app/lib/jacksum.jar new file mode 100755 index 0000000000..e71161f8e4 Binary files /dev/null and b/app/lib/jacksum.jar differ diff --git a/app/lib/sunjce_provider.jar b/app/lib/sunjce_provider.jar new file mode 100755 index 0000000000..79facc3cf1 Binary files /dev/null and b/app/lib/sunjce_provider.jar differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5ff4f1c03f..aa708d26dc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,6 +7,8 @@ + + GetStringUTFChars(path, NULL); const char *_password = env->GetStringUTFChars(password, NULL); const char *_language = env->GetStringUTFChars(language, NULL); + Monero::NetworkType _networkType = static_cast(networkType); Bitmonero::Wallet *wallet = Bitmonero::WalletManagerFactory::getWalletManager()->createWallet( std::string(_path), std::string(_password), std::string(_language), - isTestNet); + _networkType); env->ReleaseStringUTFChars(path, _path); env->ReleaseStringUTFChars(password, _password); @@ -274,15 +275,16 @@ Java_com_m2049r_xmrwallet_model_WalletManager_createWalletJ(JNIEnv *env, jobject JNIEXPORT jlong JNICALL Java_com_m2049r_xmrwallet_model_WalletManager_openWalletJ(JNIEnv *env, jobject instance, jstring path, jstring password, - jboolean isTestNet) { + jint networkType) { const char *_path = env->GetStringUTFChars(path, NULL); const char *_password = env->GetStringUTFChars(password, NULL); + Monero::NetworkType _networkType = static_cast(networkType); Bitmonero::Wallet *wallet = Bitmonero::WalletManagerFactory::getWalletManager()->openWallet( std::string(_path), std::string(_password), - isTestNet); + _networkType); env->ReleaseStringUTFChars(path, _path); env->ReleaseStringUTFChars(password, _password); @@ -293,19 +295,20 @@ JNIEXPORT jlong JNICALL Java_com_m2049r_xmrwallet_model_WalletManager_recoveryWalletJ(JNIEnv *env, jobject instance, jstring path, jstring password, jstring mnemonic, - jboolean isTestNet, + jint networkType, jlong restoreHeight) { const char *_path = env->GetStringUTFChars(path, NULL); const char *_password = env->GetStringUTFChars(password, NULL); const char *_mnemonic = env->GetStringUTFChars(mnemonic, NULL); + Monero::NetworkType _networkType = static_cast(networkType); Bitmonero::Wallet *wallet = Bitmonero::WalletManagerFactory::getWalletManager()->recoveryWallet( std::string(_path), std::string(_password), std::string(_mnemonic), - isTestNet, - restoreHeight); + _networkType, + (uint64_t) restoreHeight); env->ReleaseStringUTFChars(path, _path); env->ReleaseStringUTFChars(password, _password); @@ -314,10 +317,10 @@ Java_com_m2049r_xmrwallet_model_WalletManager_recoveryWalletJ(JNIEnv *env, jobje } JNIEXPORT jlong JNICALL -Java_com_m2049r_xmrwallet_model_WalletManager_createWalletWithKeysJ(JNIEnv *env, jobject instance, +Java_com_m2049r_xmrwallet_model_WalletManager_createWalletFromKeysJ(JNIEnv *env, jobject instance, jstring path, jstring password, jstring language, - jboolean isTestNet, + jint networkType, jlong restoreHeight, jstring addressString, jstring viewKeyString, @@ -325,17 +328,18 @@ Java_com_m2049r_xmrwallet_model_WalletManager_createWalletWithKeysJ(JNIEnv *env, const char *_path = env->GetStringUTFChars(path, NULL); const char *_password = env->GetStringUTFChars(password, NULL); const char *_language = env->GetStringUTFChars(language, NULL); + Monero::NetworkType _networkType = static_cast(networkType); const char *_addressString = env->GetStringUTFChars(addressString, NULL); const char *_viewKeyString = env->GetStringUTFChars(viewKeyString, NULL); const char *_spendKeyString = env->GetStringUTFChars(spendKeyString, NULL); Bitmonero::Wallet *wallet = - Bitmonero::WalletManagerFactory::getWalletManager()->createWalletWithKeys( + Bitmonero::WalletManagerFactory::getWalletManager()->createWalletFromKeys( std::string(_path), std::string(_password), std::string(_language), - isTestNet, - restoreHeight, + _networkType, + (uint64_t) restoreHeight, std::string(_addressString), std::string(_viewKeyString), std::string(_spendKeyString)); @@ -356,7 +360,7 @@ Java_com_m2049r_xmrwallet_model_WalletManager_walletExists(JNIEnv *env, jobject bool exists = Bitmonero::WalletManagerFactory::getWalletManager()->walletExists(std::string(_path)); env->ReleaseStringUTFChars(path, _path); - return exists; + return static_cast(exists); } JNIEXPORT jboolean JNICALL @@ -371,7 +375,7 @@ Java_com_m2049r_xmrwallet_model_WalletManager_verifyWalletPassword(JNIEnv *env, std::string(_keys_file_name), std::string(_password), watch_only); env->ReleaseStringUTFChars(keys_file_name, _keys_file_name); env->ReleaseStringUTFChars(password, _password); - return passwordOk; + return static_cast(passwordOk); } @@ -440,7 +444,7 @@ Java_com_m2049r_xmrwallet_model_WalletManager_getBlockTarget(JNIEnv *env, jobjec JNIEXPORT jboolean JNICALL Java_com_m2049r_xmrwallet_model_WalletManager_isMining(JNIEnv *env, jobject instance) { - return Bitmonero::WalletManagerFactory::getWalletManager()->isMining(); + return static_cast(Bitmonero::WalletManagerFactory::getWalletManager()->isMining()); } JNIEXPORT jboolean JNICALL @@ -454,12 +458,12 @@ Java_com_m2049r_xmrwallet_model_WalletManager_startMining(JNIEnv *env, jobject i background_mining, ignore_battery); env->ReleaseStringUTFChars(address, _address); - return success; + return static_cast(success); } JNIEXPORT jboolean JNICALL Java_com_m2049r_xmrwallet_model_WalletManager_stopMining(JNIEnv *env, jobject instance) { - return Bitmonero::WalletManagerFactory::getWalletManager()->stopMining(); + return static_cast(Bitmonero::WalletManagerFactory::getWalletManager()->stopMining()); } JNIEXPORT jstring JNICALL @@ -493,7 +497,7 @@ Java_com_m2049r_xmrwallet_model_WalletManager_closeJ(JNIEnv *env, jobject instan } } LOGD("wallet closed"); - return closeSuccess; + return static_cast(closeSuccess); } @@ -543,7 +547,7 @@ Java_com_m2049r_xmrwallet_model_Wallet_setPassword(JNIEnv *env, jobject instance Bitmonero::Wallet *wallet = getHandle(env, instance); bool success = wallet->setPassword(std::string(_password)); env->ReleaseStringUTFChars(password, _password); - return success; + return static_cast(success); } JNIEXPORT jstring JNICALL @@ -558,10 +562,10 @@ Java_com_m2049r_xmrwallet_model_Wallet_getPath(JNIEnv *env, jobject instance) { return env->NewStringUTF(wallet->path().c_str()); } -JNIEXPORT jboolean JNICALL -Java_com_m2049r_xmrwallet_model_Wallet_isTestNet(JNIEnv *env, jobject instance) { +JNIEXPORT jint JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_nettype(JNIEnv *env, jobject instance) { Bitmonero::Wallet *wallet = getHandle(env, instance); - return wallet->testnet(); + return wallet->nettype(); } //TODO virtual void hardForkInfo(uint8_t &version, uint64_t &earliest_height) const = 0; @@ -599,7 +603,7 @@ Java_com_m2049r_xmrwallet_model_Wallet_store(JNIEnv *env, jobject instance, LOGE("store() %s", wallet->errorString().c_str()); } env->ReleaseStringUTFChars(path, _path); - return success; + return static_cast(success); } JNIEXPORT jstring JNICALL @@ -619,12 +623,13 @@ Java_com_m2049r_xmrwallet_model_Wallet_initJ(JNIEnv *env, jobject instance, const char *_daemon_username = env->GetStringUTFChars(daemon_username, NULL); const char *_daemon_password = env->GetStringUTFChars(daemon_password, NULL); Bitmonero::Wallet *wallet = getHandle(env, instance); - bool status = wallet->init(_daemon_address, upper_transaction_size_limit, _daemon_username, + bool status = wallet->init(_daemon_address, (uint64_t) upper_transaction_size_limit, + _daemon_username, _daemon_password); env->ReleaseStringUTFChars(daemon_address, _daemon_address); env->ReleaseStringUTFChars(daemon_username, _daemon_username); env->ReleaseStringUTFChars(daemon_password, _daemon_password); - return status; + return static_cast(status); } // virtual bool createWatchOnly(const std::string &path, const std::string &password, const std::string &language) const = 0; @@ -655,7 +660,7 @@ Java_com_m2049r_xmrwallet_model_Wallet_getUnlockedBalance(JNIEnv *env, jobject i JNIEXPORT jboolean JNICALL Java_com_m2049r_xmrwallet_model_Wallet_isWatchOnly(JNIEnv *env, jobject instance) { Bitmonero::Wallet *wallet = getHandle(env, instance); - return wallet->watchOnly(); + return static_cast(wallet->watchOnly()); } JNIEXPORT jlong JNICALL @@ -687,7 +692,24 @@ Java_com_m2049r_xmrwallet_model_Wallet_getDaemonBlockChainTargetHeight(JNIEnv *e JNIEXPORT jboolean JNICALL Java_com_m2049r_xmrwallet_model_Wallet_isSynchronized(JNIEnv *env, jobject instance) { Bitmonero::Wallet *wallet = getHandle(env, instance); - return wallet->synchronized(); + return static_cast(wallet->synchronized()); +} + +//void cn_slow_hash(const void *data, size_t length, char *hash); // from crypto/hash-ops.h +JNIEXPORT jbyteArray JNICALL +Java_com_m2049r_xmrwallet_util_KeyStoreHelper_cnSlowHash(JNIEnv *env, jobject clazz, + jbyteArray data) { + + jbyte *buffer = env->GetByteArrayElements(data, NULL); + jsize size = env->GetArrayLength(data); + char hash[HASH_SIZE]; + cn_slow_hash(buffer, (size_t) size, hash); + + env->ReleaseByteArrayElements(data, buffer, JNI_ABORT); // do not update java byte[] + + jbyteArray result = env->NewByteArray(HASH_SIZE); + env->SetByteArrayRegion(result, 0, HASH_SIZE, (jbyte *) hash); + return result; } JNIEXPORT jstring JNICALL @@ -722,16 +744,17 @@ Java_com_m2049r_xmrwallet_model_Wallet_isPaymentIdValid(JNIEnv *env, jobject cla const char *_payment_id = env->GetStringUTFChars(payment_id, NULL); bool isValid = Bitmonero::Wallet::paymentIdValid(_payment_id); env->ReleaseStringUTFChars(payment_id, _payment_id); - return isValid; + return static_cast(isValid); } JNIEXPORT jboolean JNICALL Java_com_m2049r_xmrwallet_model_Wallet_isAddressValid(JNIEnv *env, jobject clazz, - jstring address, jboolean isTestNet) { + jstring address, jint networkType) { const char *_address = env->GetStringUTFChars(address, NULL); - bool isValid = Bitmonero::Wallet::addressValid(_address, isTestNet); + Monero::NetworkType _networkType = static_cast(networkType); + bool isValid = Bitmonero::Wallet::addressValid(_address, _networkType); env->ReleaseStringUTFChars(address, _address); - return isValid; + return static_cast(isValid); } //TODO static static bool keyValid(const std::string &secret_key_string, const std::string &address_string, bool isViewKey, bool testnet, std::string &error); @@ -739,9 +762,10 @@ Java_com_m2049r_xmrwallet_model_Wallet_isAddressValid(JNIEnv *env, jobject clazz JNIEXPORT jstring JNICALL Java_com_m2049r_xmrwallet_model_Wallet_getPaymentIdFromAddress(JNIEnv *env, jobject clazz, jstring address, - jboolean isTestNet) { + jint networkType) { + Monero::NetworkType _networkType = static_cast(networkType); const char *_address = env->GetStringUTFChars(address, NULL); - std::string payment_id = Bitmonero::Wallet::paymentIdFromAddress(_address, isTestNet); + std::string payment_id = Bitmonero::Wallet::paymentIdFromAddress(_address, _networkType); env->ReleaseStringUTFChars(address, _address); return env->NewStringUTF(payment_id.c_str()); } @@ -766,7 +790,7 @@ Java_com_m2049r_xmrwallet_model_Wallet_pauseRefresh(JNIEnv *env, jobject instanc JNIEXPORT jboolean JNICALL Java_com_m2049r_xmrwallet_model_Wallet_refresh(JNIEnv *env, jobject instance) { Bitmonero::Wallet *wallet = getHandle(env, instance); - return wallet->refresh(); + return static_cast(wallet->refresh()); } JNIEXPORT void JNICALL @@ -880,7 +904,7 @@ Java_com_m2049r_xmrwallet_model_Wallet_setUserNote(JNIEnv *env, jobject instance env->ReleaseStringUTFChars(txid, _txid); env->ReleaseStringUTFChars(note, _note); - return success; + return static_cast(success); } JNIEXPORT jstring JNICALL @@ -1029,7 +1053,7 @@ Java_com_m2049r_xmrwallet_model_PendingTransaction_commit(JNIEnv *env, jobject i bool success = tx->commit(_filename, overwrite); env->ReleaseStringUTFChars(filename, _filename); - return success; + return static_cast(success); } @@ -1051,10 +1075,13 @@ Java_com_m2049r_xmrwallet_model_PendingTransaction_getFee(JNIEnv *env, jobject i // TODO this returns a vector of strings - deal with this later - for now return first one JNIEXPORT jstring JNICALL -Java_com_m2049r_xmrwallet_model_PendingTransaction_getFirstTxId(JNIEnv *env, jobject instance) { +Java_com_m2049r_xmrwallet_model_PendingTransaction_getFirstTxIdJ(JNIEnv *env, jobject instance) { Bitmonero::PendingTransaction *tx = getHandle(env, instance); std::vector txids = tx->txid(); - return env->NewStringUTF(txids.front().c_str()); + if (!txids.empty()) + return env->NewStringUTF(txids.front().c_str()); + else + return nullptr; } @@ -1065,6 +1092,85 @@ Java_com_m2049r_xmrwallet_model_PendingTransaction_getTxCount(JNIEnv *env, jobje } +// these are all in Bitmonero::Wallet - which I find wrong, so they are here! +//static void init(const char *argv0, const char *default_log_base_name); +//static void debug(const std::string &category, const std::string &str); +//static void info(const std::string &category, const std::string &str); +//static void warning(const std::string &category, const std::string &str); +//static void error(const std::string &category, const std::string &str); +JNIEXPORT void JNICALL +Java_com_m2049r_xmrwallet_model_WalletManager_initLogger(JNIEnv *env, jobject instance, + jstring argv0, + jstring default_log_base_name) { + + const char *_argv0 = env->GetStringUTFChars(argv0, NULL); + const char *_default_log_base_name = env->GetStringUTFChars(default_log_base_name, NULL); + + Bitmonero::Wallet::init(_argv0, _default_log_base_name); + + env->ReleaseStringUTFChars(argv0, _argv0); + env->ReleaseStringUTFChars(default_log_base_name, _default_log_base_name); +} + +JNIEXPORT void JNICALL +Java_com_m2049r_xmrwallet_model_WalletManager_logDebug(JNIEnv *env, jobject instance, + jstring category, jstring message) { + + const char *_category = env->GetStringUTFChars(category, NULL); + const char *_message = env->GetStringUTFChars(message, NULL); + + Bitmonero::Wallet::debug(_category, _message); + + env->ReleaseStringUTFChars(category, _category); + env->ReleaseStringUTFChars(message, _message); +} + +JNIEXPORT void JNICALL +Java_com_m2049r_xmrwallet_model_WalletManager_logInfo(JNIEnv *env, jobject instance, + jstring category, jstring message) { + + const char *_category = env->GetStringUTFChars(category, NULL); + const char *_message = env->GetStringUTFChars(message, NULL); + + Bitmonero::Wallet::info(_category, _message); + + env->ReleaseStringUTFChars(category, _category); + env->ReleaseStringUTFChars(message, _message); +} + +JNIEXPORT void JNICALL +Java_com_m2049r_xmrwallet_model_WalletManager_logWarning(JNIEnv *env, jobject instance, + jstring category, jstring message) { + + const char *_category = env->GetStringUTFChars(category, NULL); + const char *_message = env->GetStringUTFChars(message, NULL); + + Bitmonero::Wallet::warning(_category, _message); + + env->ReleaseStringUTFChars(category, _category); + env->ReleaseStringUTFChars(message, _message); +} + +JNIEXPORT void JNICALL +Java_com_m2049r_xmrwallet_model_WalletManager_logError(JNIEnv *env, jobject instance, + jstring category, jstring message) { + + const char *_category = env->GetStringUTFChars(category, NULL); + const char *_message = env->GetStringUTFChars(message, NULL); + + Bitmonero::Wallet::error(_category, _message); + + env->ReleaseStringUTFChars(category, _category); + env->ReleaseStringUTFChars(message, _message); +} + +JNIEXPORT void JNICALL +Java_com_m2049r_xmrwallet_model_WalletManager_setLogLevel(JNIEnv *env, jobject instance, + jint level) { + Bitmonero::WalletManagerFactory::setLogLevel(level); +} + + #ifdef __cplusplus } #endif diff --git a/app/src/main/cpp/monerujo.h b/app/src/main/cpp/monerujo.h index 3315e02262..de74f221a2 100644 --- a/app/src/main/cpp/monerujo.h +++ b/app/src/main/cpp/monerujo.h @@ -18,6 +18,7 @@ #define XMRWALLET_WALLET_LIB_H #include + /* #include @@ -27,13 +28,13 @@ #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__) */ -jfieldID getHandleField(JNIEnv *env, jobject obj, const char* fieldName = "handle") { +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 -T *getHandle(JNIEnv *env, jobject obj, const char* fieldName = "handle") { +template +T *getHandle(JNIEnv *env, jobject obj, const char *fieldName = "handle") { jlong handle = env->GetLongField(obj, getHandleField(env, obj, fieldName)); return reinterpret_cast(handle); } @@ -42,10 +43,27 @@ void setHandleFromLong(JNIEnv *env, jobject obj, jlong handle) { env->SetLongField(obj, getHandleField(env, obj), handle); } -template +template void setHandle(JNIEnv *env, jobject obj, T *t) { jlong handle = reinterpret_cast(t); setHandleFromLong(env, obj, handle); } +#ifdef __cplusplus +extern "C" +{ +#endif + +// 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); + +#ifdef __cplusplus +} +#endif + #endif //XMRWALLET_WALLET_LIB_H diff --git a/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java b/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java index f5aa56642b..82bc40a822 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java @@ -16,12 +16,15 @@ package com.m2049r.xmrwallet; +import android.app.AlertDialog; import android.content.Context; +import android.content.DialogInterface; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.design.widget.TextInputLayout; import android.support.v4.app.Fragment; import android.text.Editable; +import android.text.Html; import android.text.InputType; import android.text.TextWatcher; import android.view.KeyEvent; @@ -32,20 +35,23 @@ import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.Switch; import android.widget.TextView; -import com.m2049r.xmrwallet.util.RestoreHeight; -import com.m2049r.xmrwallet.widget.Toolbar; 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.widget.Toolbar; import com.nulabinc.zxcvbn.Strength; import com.nulabinc.zxcvbn.Zxcvbn; import java.io.File; import java.text.ParseException; import java.text.SimpleDateFormat; -import java.util.Calendar; import timber.log.Timber; @@ -59,6 +65,7 @@ public class GenerateFragment extends Fragment { private TextInputLayout etWalletName; private TextInputLayout etWalletPassword; + private LinearLayout llFingerprintAuth; private TextInputLayout etWalletAddress; private TextInputLayout etWalletMnemonic; private TextInputLayout etWalletViewKey; @@ -79,6 +86,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, etWalletName = (TextInputLayout) view.findViewById(R.id.etWalletName); etWalletPassword = (TextInputLayout) view.findViewById(R.id.etWalletPassword); + llFingerprintAuth = (LinearLayout) view.findViewById(R.id.llFingerprintAuth); etWalletMnemonic = (TextInputLayout) view.findViewById(R.id.etWalletMnemonic); etWalletAddress = (TextInputLayout) view.findViewById(R.id.etWalletAddress); etWalletViewKey = (TextInputLayout) view.findViewById(R.id.etWalletViewKey); @@ -146,6 +154,30 @@ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { } }); + if (FingerprintHelper.isDeviceSupported(getContext())) { + llFingerprintAuth.setVisibility(View.VISIBLE); + + final Switch swFingerprintAllowed = (Switch) llFingerprintAuth.getChildAt(0); + swFingerprintAllowed.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (!swFingerprintAllowed.isChecked()) return; + + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + 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(); + } + }); + } + if (type.equals(TYPE_NEW)) { etWalletPassword.getEditText().setImeOptions(EditorInfo.IME_ACTION_DONE); etWalletPassword.getEditText().setOnEditorActionListener(new TextView.OnEditorActionListener() { @@ -386,7 +418,7 @@ private boolean checkMnemonic() { private boolean checkAddress() { String address = etWalletAddress.getEditText().getText().toString(); - boolean ok = Wallet.isAddressValid(address, WalletManager.getInstance().isTestNet()); + boolean ok = Wallet.isAddressValid(address); if (!ok) { etWalletAddress.setError(getString(R.string.generate_check_address)); } else { @@ -423,18 +455,28 @@ private void generateWallet() { String name = etWalletName.getEditText().getText().toString(); String password = etWalletPassword.getEditText().getText().toString(); + boolean fingerprintAuthAllowed = ((Switch) llFingerprintAuth.getChildAt(0)).isChecked(); + + // create the real wallet password + String crazyPass = KeyStoreHelper.getCrazyPass(getActivity(), password); long height = getHeight(); if (height < 0) height = 0; if (type.equals(TYPE_NEW)) { bGenerate.setEnabled(false); - activityCallback.onGenerate(name, password); + if (fingerprintAuthAllowed) { + KeyStoreHelper.saveWalletUserPass(getActivity(), name, password); + } + activityCallback.onGenerate(name, crazyPass); } else if (type.equals(TYPE_SEED)) { if (!checkMnemonic()) return; String seed = etWalletMnemonic.getEditText().getText().toString(); bGenerate.setEnabled(false); - activityCallback.onGenerate(name, password, seed, height); + if (fingerprintAuthAllowed) { + KeyStoreHelper.saveWalletUserPass(getActivity(), name, password); + } + activityCallback.onGenerate(name, crazyPass, seed, height); } else if (type.equals(TYPE_KEY) || type.equals(TYPE_VIEWONLY)) { if (checkAddress() && checkViewKey() && checkSpendKey()) { bGenerate.setEnabled(false); @@ -444,7 +486,10 @@ private void generateWallet() { if (type.equals(TYPE_KEY)) { spendKey = etWalletSpendKey.getEditText().getText().toString(); } - activityCallback.onGenerate(name, password, address, viewKey, spendKey, height); + if (fingerprintAuthAllowed) { + KeyStoreHelper.saveWalletUserPass(getActivity(), name, password); + } + activityCallback.onGenerate(name, crazyPass, address, viewKey, spendKey, height); } } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java b/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java index 8ec29c7e8e..3c436df1af 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java @@ -16,29 +16,44 @@ package com.m2049r.xmrwallet; +import android.app.AlertDialog; import android.content.Context; +import android.content.DialogInterface; import android.os.AsyncTask; import android.os.Bundle; import android.support.annotation.Nullable; +import android.support.design.widget.TextInputLayout; import android.support.v4.app.Fragment; +import android.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.inputmethod.EditorInfo; import android.widget.Button; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.ScrollView; +import android.widget.Switch; import android.widget.TextView; import android.widget.Toast; -import com.m2049r.xmrwallet.widget.Toolbar; +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.Toolbar; + +import java.io.File; +import java.security.KeyStoreException; import timber.log.Timber; @@ -50,7 +65,6 @@ public class GenerateReviewFragment extends Fragment { ScrollView scrollview; ProgressBar pbProgress; - TextView tvWalletName; TextView tvWalletPassword; TextView tvWalletAddress; TextView tvWalletMnemonic; @@ -58,9 +72,18 @@ public class GenerateReviewFragment extends Fragment { TextView tvWalletSpendKey; ImageButton bCopyAddress; LinearLayout llAdvancedInfo; + LinearLayout llPassword; Button bAdvancedInfo; Button bAccept; + // TODO fix visibility of variables + String walletPath; + String walletName; + // we need to keep the password so the user is not asked again if they want to change it + // note they can only enter this fragment immediately after entering the password + // so asking them to enter it a couple of seconds later seems silly + String walletPassword = null; + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -69,7 +92,6 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, scrollview = (ScrollView) view.findViewById(R.id.scrollview); pbProgress = (ProgressBar) view.findViewById(R.id.pbProgress); - tvWalletName = (TextView) view.findViewById(R.id.tvWalletName); tvWalletPassword = (TextView) view.findViewById(R.id.tvWalletPassword); tvWalletAddress = (TextView) view.findViewById(R.id.tvWalletAddress); tvWalletViewKey = (TextView) view.findViewById(R.id.tvWalletViewKey); @@ -78,12 +100,14 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, bCopyAddress = (ImageButton) view.findViewById(R.id.bCopyAddress); bAdvancedInfo = (Button) view.findViewById(R.id.bAdvancedInfo); llAdvancedInfo = (LinearLayout) view.findViewById(R.id.llAdvancedInfo); + llPassword = (LinearLayout) view.findViewById(R.id.llPassword); bAccept = (Button) view.findViewById(R.id.bAccept); - boolean testnet = WalletManager.getInstance().isTestNet(); + boolean testnet = WalletManager.getInstance().getNetworkType() != NetworkType.NetworkType_Mainnet; tvWalletMnemonic.setTextIsSelectable(testnet); tvWalletSpendKey.setTextIsSelectable(testnet); + tvWalletPassword.setTextIsSelectable(testnet); bAccept.setOnClickListener(new View.OnClickListener() { @Override @@ -111,17 +135,20 @@ public void onClick(View v) { } }); - showProgress(); - Bundle args = getArguments(); - String path = args.getString("path"); - String password = args.getString("password"); - this.type = args.getString("type"); - new AsyncShow().executeOnExecutor(MoneroThreadPoolExecutor.MONERO_THREAD_POOL_EXECUTOR, - path, password); + type = args.getString("type"); + walletPath = args.getString("path"); + showDetails(args.getString("password")); return view; } + void showDetails(String password) { + walletPassword = password; + showProgress(); + tvWalletPassword.setText(null); + new AsyncShow().executeOnExecutor(MoneroThreadPoolExecutor.MONERO_THREAD_POOL_EXECUTOR, walletPath); + } + void copyViewKey() { Helper.clipBoardCopy(getActivity(), getString(R.string.label_copy_viewkey), tvWalletViewKey.getText().toString()); Toast.makeText(getActivity(), getString(R.string.message_copy_viewkey), Toast.LENGTH_SHORT).show(); @@ -150,15 +177,11 @@ public void run() { String type; private void acceptWallet() { - String name = tvWalletName.getText().toString(); - String password = tvWalletPassword.getText().toString(); bAccept.setEnabled(false); - acceptCallback.onAccept(name, password); + acceptCallback.onAccept(walletName, walletPassword); } private class AsyncShow extends AsyncTask { - String password; - String name; String address; String seed; @@ -169,9 +192,8 @@ private class AsyncShow extends AsyncTask { @Override protected Boolean doInBackground(String... params) { - if (params.length != 2) return false; + if (params.length != 1) return false; String walletPath = params[0]; - password = params[1]; Wallet wallet; boolean closeWallet; @@ -179,12 +201,13 @@ protected Boolean doInBackground(String... params) { wallet = GenerateReviewFragment.this.walletCallback.getWallet(); closeWallet = false; } else { - wallet = WalletManager.getInstance().openWallet(walletPath, password); + wallet = WalletManager.getInstance().openWallet(walletPath, walletPassword); closeWallet = true; } name = wallet.getName(); status = wallet.getStatus(); if (status != Wallet.Status.Status_Ok) { + Timber.e(wallet.getErrorString()); if (closeWallet) wallet.close(); return false; } @@ -202,13 +225,16 @@ protected Boolean doInBackground(String... params) { protected void onPostExecute(Boolean result) { super.onPostExecute(result); if (!isAdded()) return; // never mind - tvWalletName.setText(name); + walletName = name; if (result) { if (type.equals(GenerateReviewFragment.VIEW_TYPE_ACCEPT)) { - tvWalletPassword.setText(password); bAccept.setVisibility(View.VISIBLE); bAccept.setEnabled(true); } + if (walletPassword != null) { + llPassword.setVisibility(View.VISIBLE); + tvWalletPassword.setText(walletPassword); + } tvWalletAddress.setText(address); tvWalletMnemonic.setText(seed); tvWalletViewKey.setText(viewKey); @@ -231,6 +257,7 @@ protected void onPostExecute(Boolean result) { } Listener activityCallback = null; + ProgressListener progressCallback = null; AcceptListener acceptCallback = null; ListenerWithWallet walletCallback = null; @@ -240,6 +267,13 @@ public interface Listener { void setToolbarButton(int type); } + public interface ProgressListener { + void showProgressDialog(int msgId); + + void dismissProgressDialog(); + } + + public interface AcceptListener { void onAccept(String name, String password); } @@ -254,6 +288,9 @@ public void onAttach(Context 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; } @@ -266,9 +303,7 @@ public void onAttach(Context context) { public void onResume() { super.onResume(); Timber.d("onResume()"); - String name = tvWalletName.getText().toString(); - if (name.isEmpty()) name = null; - activityCallback.setTitle(name, getString(R.string.details_title)); + activityCallback.setTitle(walletName, getString(R.string.details_title)); activityCallback.setToolbarButton( GenerateReviewFragment.VIEW_TYPE_ACCEPT.equals(type) ? Toolbar.BUTTON_NONE : Toolbar.BUTTON_BACK); } @@ -293,7 +328,238 @@ public void onCreate(@Nullable Bundle savedInstanceState) { @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - inflater.inflate(R.menu.wallet_details_menu, menu); - super.onCreateOptionsMenu(menu, inflater); + String type = getArguments().getString("type"); + 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, walletPassword); + closeWallet = true; + } + + boolean ok = false; + if (wallet.getStatus() == Wallet.Status.Status_Ok) { + wallet.setPassword(newPassword); + wallet.store(); + ok = true; + } else { + Timber.e(wallet.getErrorString()); + } + if (closeWallet) wallet.close(); + return ok; + } + + private class AsyncChangePassword extends AsyncTask { + 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 != 4) return false; + File walletFile = Helper.getWalletFile(getActivity(), params[0]); + String oldPassword = params[1]; + String userPassword = params[2]; + boolean fingerprintAuthAllowed = Boolean.valueOf(params[3]); + newPassword = KeyStoreHelper.getCrazyPass(getActivity(), userPassword); + boolean success = changeWalletPassword(newPassword); + if (success) { + if (fingerprintAuthAllowed) { + KeyStoreHelper.saveWalletUserPass(getActivity(), walletName, userPassword); + } else { + KeyStoreHelper.removeWalletUserPass(getActivity(), walletName); + } + } + return success; + } + + @Override + protected void onPostExecute(Boolean result) { + super.onPostExecute(result); + if (getActivity().isDestroyed()) { + return; + } + if (progressCallback != null) + progressCallback.dismissProgressDialog(); + if (result) { + Toast.makeText(getActivity(), getString(R.string.changepw_success), Toast.LENGTH_SHORT).show(); + showDetails(newPassword); + } 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 AlertDialog.Builder(getActivity()); + alertDialogBuilder.setView(promptsView); + + final TextInputLayout etPasswordA = (TextInputLayout) promptsView.findViewById(R.id.etWalletPasswordA); + etPasswordA.setHint(getString(R.string.prompt_changepw, walletName)); + + final TextInputLayout etPasswordB = (TextInputLayout) promptsView.findViewById(R.id.etWalletPasswordB); + etPasswordB.setHint(getString(R.string.prompt_changepwB, walletName)); + + LinearLayout llFingerprintAuth = (LinearLayout) promptsView.findViewById(R.id.llFingerprintAuth); + final Switch swFingerprintAllowed = (Switch) llFingerprintAuth.getChildAt(0); + if (FingerprintHelper.isDeviceSupported(getActivity())) { + llFingerprintAuth.setVisibility(View.VISIBLE); + + swFingerprintAllowed.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (!swFingerprintAllowed.isChecked()) return; + + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + 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(); + } + }); + + try { + swFingerprintAllowed.setChecked(FingerprintHelper.isFingerprintAuthAllowed(walletName)); + } catch (KeyStoreException ex) { + ex.printStackTrace(); + } + } + + etPasswordA.getEditText().addTextChangedListener(new TextWatcher() { + @Override + public void afterTextChanged(Editable s) { + if (etPasswordA.getError() != null) { + etPasswordA.setError(null); + } + 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 (etPasswordA.getError() != null) { + etPasswordA.setError(null); + } + 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(getActivity()); + dialog.cancel(); + openDialog = null; + } + }); + + openDialog = alertDialogBuilder.create(); + openDialog.setOnShowListener(new DialogInterface.OnShowListener() { + @Override + public void onShow(final DialogInterface dialog) { + Button button = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE); + button.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + String newPasswordA = etPasswordA.getEditText().getText().toString(); + String newPasswordB = etPasswordB.getEditText().getText().toString(); + // disallow empty passwords + if (newPasswordA.isEmpty()) { + etPasswordA.setError(getString(R.string.generate_empty_passwordB)); + } else if (!newPasswordA.equals(newPasswordB)) { + etPasswordB.setError(getString(R.string.generate_bad_passwordB)); + } else if (newPasswordA.equals(newPasswordB)) { + new AsyncChangePassword().execute(walletName, walletPassword, newPasswordA, Boolean.toString(swFingerprintAllowed.isChecked())); + Helper.hideKeyboardAlways(getActivity()); + openDialog.dismiss(); + openDialog = null; + } + } + }); + } + }); + + // accept keyboard "ok" + etPasswordB.getEditText().setOnEditorActionListener(new TextView.OnEditorActionListener() { + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_DONE)) { + String newPasswordA = etPasswordA.getEditText().getText().toString(); + String newPasswordB = etPasswordB.getEditText().getText().toString(); + // disallow empty passwords + if (newPasswordA.isEmpty()) { + etPasswordA.setError(getString(R.string.generate_empty_passwordB)); + } else if (!newPasswordA.equals(newPasswordB)) { + etPasswordB.setError(getString(R.string.generate_bad_passwordB)); + } else if (newPasswordA.equals(newPasswordB)) { + new AsyncChangePassword().execute(walletName, walletPassword, newPasswordA, Boolean.toString(swFingerprintAllowed.isChecked())); + Helper.hideKeyboardAlways(getActivity()); + openDialog.dismiss(); + openDialog = null; + } + return true; + } + return false; + } + }); + return openDialog; + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java b/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java index 6f2aa5ce90..782afba6e2 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java +++ b/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java @@ -29,1217 +29,1185 @@ import android.os.Bundle; import android.os.Handler; import android.support.annotation.NonNull; -import android.support.design.widget.TextInputLayout; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; -import android.text.Editable; -import android.text.TextWatcher; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.inputmethod.EditorInfo; -import android.widget.Button; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; +import com.m2049r.xmrwallet.data.WalletNode; import com.m2049r.xmrwallet.dialog.AboutFragment; -import com.m2049r.xmrwallet.dialog.DonationFragment; +import com.m2049r.xmrwallet.dialog.CreditsFragment; import com.m2049r.xmrwallet.dialog.HelpFragment; import com.m2049r.xmrwallet.dialog.PrivacyFragment; +import com.m2049r.xmrwallet.model.NetworkType; import com.m2049r.xmrwallet.model.Wallet; import com.m2049r.xmrwallet.model.WalletManager; +import com.m2049r.xmrwallet.nfc.AuthenticationException; +import com.m2049r.xmrwallet.nfc.TagUtil; +import com.m2049r.xmrwallet.nfc.ThreeDES; import com.m2049r.xmrwallet.service.WalletService; +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.Toolbar; import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; -import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketAddress; import java.nio.channels.FileChannel; +import java.security.KeyStoreException; import java.util.Date; import timber.log.Timber; public class LoginActivity extends SecureActivity implements LoginFragment.Listener, GenerateFragment.Listener, - GenerateReviewFragment.Listener, GenerateReviewFragment.AcceptListener, ReceiveFragment.Listener { - private static final String GENERATE_STACK = "gen"; + GenerateReviewFragment.Listener, GenerateReviewFragment.AcceptListener, + GenerateReviewFragment.ProgressListener, ReceiveFragment.Listener { + private static final String GENERATE_STACK = "gen"; - static final int DAEMON_TIMEOUT = 500; // deamon must respond in 500ms + static final int DAEMON_TIMEOUT = 500; // deamon must respond in 500ms - private Toolbar toolbar; + private Toolbar toolbar; - @Override - public void setToolbarButton(int type) { - toolbar.setButton(type); - } - - @Override - public void setTitle(String title) { - toolbar.setTitle(title); - } - - @Override - public void setSubtitle(String subtitle) { - toolbar.setSubtitle(subtitle); - } - - @Override - public void setTitle(String title, String subtitle) { - toolbar.setTitle(title, subtitle); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - Timber.d("onCreate()"); - super.onCreate(savedInstanceState); - if (savedInstanceState != null) { - // we don't store anything ourselves - } + @Override + public void setToolbarButton(int type) { + toolbar.setButton(type); + } - setContentView(R.layout.activity_login); - toolbar = (Toolbar) findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - getSupportActionBar().setDisplayShowTitleEnabled(false); + @Override + public void setTitle(String title) { + toolbar.setTitle(title); + } - toolbar.setOnButtonListener(new Toolbar.OnButtonListener() { - @Override - public void onButton(int type) { - switch (type) { - case Toolbar.BUTTON_BACK: - onBackPressed(); - break; - case Toolbar.BUTTON_CLOSE: - finish(); - break; - case Toolbar.BUTTON_DONATE: - DonationFragment.display(getSupportFragmentManager()); - break; - case Toolbar.BUTTON_NONE: - default: - Timber.e("Button " + type + "pressed - how can this be?"); - } - } - }); + @Override + public void setSubtitle(String subtitle) { + toolbar.setSubtitle(subtitle); + } - if (Helper.getWritePermission(this)) { - if (savedInstanceState == null) startLoginFragment(); - } else { - Timber.i("Waiting for permissions"); - } - } + @Override + public void setTitle(String title, String subtitle) { + toolbar.setTitle(title, subtitle); + } - boolean checkServiceRunning() { - if (WalletService.Running) { - Toast.makeText(this, getString(R.string.service_busy), Toast.LENGTH_SHORT).show(); - return true; - } else { - return false; - } - } - - @Override - public boolean onWalletSelected(String walletName, String daemon, boolean testnet) { - if (daemon.length() == 0) { - Toast.makeText(this, getString(R.string.prompt_daemon_missing), Toast.LENGTH_SHORT).show(); - return false; - } - if (checkServiceRunning()) return false; - try { - WalletNode aWalletNode = new WalletNode(walletName, daemon, testnet); - new AsyncOpenWallet().execute(aWalletNode); - } catch (IllegalArgumentException ex) { - Timber.e(ex.getLocalizedMessage()); - Toast.makeText(this, ex.getLocalizedMessage(), Toast.LENGTH_SHORT).show(); - return false; - } - return true; - } - - @Override - public void onWalletDetails(final String walletName, boolean testnet) { - setNet(testnet); - Timber.d("details for wallet ." + walletName + "."); - if (checkServiceRunning()) return; - DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - switch (which) { - case DialogInterface.BUTTON_POSITIVE: - final File walletFile = Helper.getWalletFile(LoginActivity.this, walletName); - if (WalletManager.getInstance().walletExists(walletFile)) { - promptPassword(walletName, new PasswordAction() { - @Override - public void action(String walletName, String password) { - startDetails(walletFile, password, GenerateReviewFragment.VIEW_TYPE_DETAILS); - } - }); - } else { // this cannot really happen as we prefilter choices - Timber.e("Wallet missing: %s", walletName); - Toast.makeText(LoginActivity.this, getString(R.string.bad_wallet), Toast.LENGTH_SHORT).show(); - } - break; - - case DialogInterface.BUTTON_NEGATIVE: - // do nothing - break; - } - } - }; - - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setMessage(getString(R.string.details_alert_message)) - .setPositiveButton(getString(R.string.details_alert_yes), dialogClickListener) - .setNegativeButton(getString(R.string.details_alert_no), dialogClickListener) - .show(); - } - - @Override - public void onWalletReceive(String walletName, boolean testnet) { - setNet(testnet); - Timber.d("receive for wallet ." + walletName + "."); - if (checkServiceRunning()) return; - final File walletFile = Helper.getWalletFile(this, walletName); - if (WalletManager.getInstance().walletExists(walletFile)) { - promptPassword(walletName, new PasswordAction() { - @Override - public void action(String walletName, String password) { - startReceive(walletFile, password); - } - }); - } else { // this cannot really happen as we prefilter choices - Toast.makeText(this, getString(R.string.bad_wallet), Toast.LENGTH_SHORT).show(); - } + @Override + protected void onCreate(Bundle savedInstanceState) { + Timber.d("onCreate()"); + super.onCreate(savedInstanceState); + if (savedInstanceState != null) { + // we don't store anything ourselves } - private class AsyncRename extends AsyncTask { - @Override - protected void onPreExecute() { - super.onPreExecute(); - showProgressDialog(R.string.rename_progress); - } - - @Override - protected Boolean doInBackground(String... params) { - if (params.length != 2) return false; - File walletFile = Helper.getWalletFile(LoginActivity.this, params[0]); - String newName = params[1]; - return renameWallet(walletFile, newName); - } + setContentView(R.layout.activity_login); + toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + getSupportActionBar().setDisplayShowTitleEnabled(false); + toolbar.setOnButtonListener(new Toolbar.OnButtonListener() { @Override - protected void onPostExecute(Boolean result) { - super.onPostExecute(result); - if (isDestroyed()) { - return; - } - dismissProgressDialog(); - if (result) { - reloadWalletList(); - } else { - Toast.makeText(LoginActivity.this, getString(R.string.rename_failed), Toast.LENGTH_LONG).show(); + public void onButton(int type) { + switch (type) { + case Toolbar.BUTTON_BACK: + onBackPressed(); + break; + case Toolbar.BUTTON_CLOSE: + finish(); + break; + case Toolbar.BUTTON_CREDITS: + CreditsFragment.display(getSupportFragmentManager()); + break; + case Toolbar.BUTTON_NONE: + default: + Timber.e("Button " + type + "pressed - how can this be?"); } } - } + }); - // copy + delete seems safer than rename because we call rollback easily - boolean renameWallet(File walletFile, String newName) { - if (copyWallet(walletFile, new File(walletFile.getParentFile(), newName), false, true)) { - deleteWallet(walletFile); - return true; - } else { - return false; - } + if (Helper.getWritePermission(this)) { + if (savedInstanceState == null) startLoginFragment(); + } else { + Timber.i("Waiting for permissions"); } + } - @Override - public void onWalletRename(final String walletName) { - Timber.d("rename for wallet ." + walletName + "."); - if (checkServiceRunning()) return; - LayoutInflater li = LayoutInflater.from(this); - View promptsView = li.inflate(R.layout.prompt_rename, null); - - AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this); - alertDialogBuilder.setView(promptsView); - - final EditText etRename = (EditText) promptsView.findViewById(R.id.etRename); - final TextView tvRenameLabel = (TextView) promptsView.findViewById(R.id.tvRenameLabel); - - tvRenameLabel.setText(getString(R.string.prompt_rename, walletName)); - - // set dialog message - alertDialogBuilder - .setCancelable(false) - .setPositiveButton(getString(R.string.label_ok), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - Helper.hideKeyboardAlways(LoginActivity.this); - String newName = etRename.getText().toString(); - new AsyncRename().execute(walletName, newName); - } - }) - .setNegativeButton(getString(R.string.label_cancel), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - Helper.hideKeyboardAlways(LoginActivity.this); - dialog.cancel(); + boolean checkServiceRunning() { + if (WalletService.Running) { + Toast.makeText(this, getString(R.string.service_busy), Toast.LENGTH_SHORT).show(); + return true; + } else { + return false; + } + } + + @Override + public boolean onWalletSelected(String walletName, String daemon) { + if (daemon.length() == 0) { + Toast.makeText(this, getString(R.string.prompt_daemon_missing), Toast.LENGTH_SHORT).show(); + return false; + } + if (checkServiceRunning()) return false; + try { + WalletNode aWalletNode = new WalletNode(walletName, daemon, WalletManager.getInstance().getNetworkType()); + new AsyncOpenWallet().execute(aWalletNode); + } catch (IllegalArgumentException ex) { + Timber.e(ex.getLocalizedMessage()); + Toast.makeText(this, ex.getLocalizedMessage(), Toast.LENGTH_SHORT).show(); + return false; + } + return true; + } + + @Override + public void onWalletDetails(final String walletName) { + Timber.d("details for wallet .%s.", walletName); + if (checkServiceRunning()) return; + DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case DialogInterface.BUTTON_POSITIVE: + final File walletFile = Helper.getWalletFile(LoginActivity.this, walletName); + if (WalletManager.getInstance().walletExists(walletFile)) { + Helper.promptPassword(LoginActivity.this, walletName, true, new Helper.PasswordAction() { + @Override + public void action(String walletName, String password, boolean fingerprintUsed) { + startDetails(walletFile, password, GenerateReviewFragment.VIEW_TYPE_DETAILS); } }); + } else { // this cannot really happen as we prefilter choices + Timber.e("Wallet missing: %s", walletName); + Toast.makeText(LoginActivity.this, getString(R.string.bad_wallet), Toast.LENGTH_SHORT).show(); + } + break; - final AlertDialog dialog = alertDialogBuilder.create(); - Helper.showKeyboard(dialog); - - // accept keyboard "ok" - etRename.setOnEditorActionListener(new TextView.OnEditorActionListener() { - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_DONE)) { - Helper.hideKeyboardAlways(LoginActivity.this); - String newName = etRename.getText().toString(); - dialog.cancel(); - new AsyncRename().execute(walletName, newName); - return false; - } - return false; - } - }); - - dialog.show(); - } - - private class AsyncBackupToFile extends AsyncTask { - @Override - protected void onPreExecute() { - super.onPreExecute(); - showProgressDialog(R.string.backup_progress); - } - - @Override - protected Boolean doInBackground(String... params) { - if (params.length != 1) return false; - return backupWallet(params[0]); - } - - @Override - protected void onPostExecute(Boolean result) { - super.onPostExecute(result); - if (isDestroyed()) { - return; - } - dismissProgressDialog(); - if (!result) { - Toast.makeText(LoginActivity.this, getString(R.string.backup_failed), Toast.LENGTH_LONG).show(); + case DialogInterface.BUTTON_NEGATIVE: + // do nothing + break; } } - } - - private boolean backupWallet(String walletName) { - File backupFolder = new File(getStorageRoot(), "backups"); - if (!backupFolder.exists()) { - if (!backupFolder.mkdir()) { - Timber.e("Cannot create backup dir %s", backupFolder.getAbsolutePath()); - return false; + }; + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setMessage(getString(R.string.details_alert_message)) + .setPositiveButton(getString(R.string.details_alert_yes), dialogClickListener) + .setNegativeButton(getString(R.string.details_alert_no), dialogClickListener) + .show(); + } + + @Override + public void onWalletReceive(String walletName) { + Timber.d("receive for wallet .%s.", walletName); + if (checkServiceRunning()) return; + final File walletFile = Helper.getWalletFile(this, walletName); + if (WalletManager.getInstance().walletExists(walletFile)) { + Helper.promptPassword(LoginActivity.this, walletName, false, new Helper.PasswordAction() { + @Override + public void action(String walletName, String password, boolean fingerprintUsed) { + startReceive(walletFile, password); } - // make folder visible over USB/MTP - MediaScannerConnection.scanFile(this, new String[]{backupFolder.toString()}, null, null); - } - File walletFile = Helper.getWalletFile(LoginActivity.this, walletName); - File backupFile = new File(backupFolder, walletName); - Timber.d("backup " + walletFile.getAbsolutePath() + " to " + backupFile.getAbsolutePath()); - // TODO probably better to copy to a new file and then rename - // then if something fails we have the old backup at least - // or just create a new backup every time and keep n old backups - boolean success = copyWallet(walletFile, backupFile, true, true); - Timber.d("copyWallet is %s", success); - return success; + }); + } else { // this cannot really happen as we prefilter choices + Toast.makeText(this, getString(R.string.bad_wallet), Toast.LENGTH_SHORT).show(); } + } - /* - modified into onWalletBackupToFile + private class AsyncRename extends AsyncTask { @Override - public void onWalletBackup(String walletName) { - Timber.d("backup for wallet ." + walletName + "."); - new AsyncBackup().execute(walletName); - }*/ - - @Override - public void onWalletBackupToFile(String walletName) { - Timber.d("backup to file for wallet ." + walletName + "."); - new AsyncBackupToFile().execute(walletName); + protected void onPreExecute() { + super.onPreExecute(); + showProgressDialog(R.string.rename_progress); } @Override - public void onWalletBackupToNFC(String walletName) { - Timber.d("backup to nfc for wallet ." + walletName + "."); - - } - - private class AsyncArchive extends AsyncTask { - @Override - protected void onPreExecute() { - super.onPreExecute(); - showProgressDialog(R.string.archive_progress); - } - - @Override - protected Boolean doInBackground(String... params) { - if (params.length != 1) return false; - String walletName = params[0]; - if (backupWallet(walletName) && deleteWallet(Helper.getWalletFile(LoginActivity.this, walletName))) { - return true; - } else { - return false; - } - } - - @Override - protected void onPostExecute(Boolean result) { - super.onPostExecute(result); - if (isDestroyed()) { - return; - } - dismissProgressDialog(); - if (result) { - reloadWalletList(); - } else { - Toast.makeText(LoginActivity.this, getString(R.string.archive_failed), Toast.LENGTH_LONG).show(); + protected Boolean doInBackground(String... params) { + if (params.length != 2) return false; + File walletFile = Helper.getWalletFile(LoginActivity.this, params[0]); + String newName = params[1]; + boolean success = renameWallet(walletFile, newName); + try { + if (success && FingerprintHelper.isFingerprintAuthAllowed(params[0])) { + String savedPass = KeyStoreHelper.loadWalletUserPass(LoginActivity.this, params[0]); + KeyStoreHelper.saveWalletUserPass(LoginActivity.this, newName, savedPass); + KeyStoreHelper.removeWalletUserPass(LoginActivity.this, params[0]); } + } catch (KeyStoreException ex) { + ex.printStackTrace(); } + return success; } @Override - public void onWalletArchive(final String walletName) { - Timber.d("archive for wallet ." + walletName + "."); - if (checkServiceRunning()) return; - DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - switch (which) { - case DialogInterface.BUTTON_POSITIVE: - new AsyncArchive().execute(walletName); - break; - case DialogInterface.BUTTON_NEGATIVE: - // do nothing - break; - } - } - }; - - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setMessage(getString(R.string.archive_alert_message)) - .setTitle(walletName) - .setPositiveButton(getString(R.string.archive_alert_yes), dialogClickListener) - .setNegativeButton(getString(R.string.archive_alert_no), dialogClickListener) - .show(); - } - - void reloadWalletList() { - Timber.d("reloadWalletList()"); - try { - LoginFragment loginFragment = (LoginFragment) - getSupportFragmentManager().findFragmentById(R.id.fragment_container); - if (loginFragment != null) { - loginFragment.loadList(); - } - } catch (ClassCastException ex) { + protected void onPostExecute(Boolean result) { + super.onPostExecute(result); + if (isDestroyed()) { + return; + } + dismissProgressDialog(); + if (result) { + reloadWalletList(); + } else { + Toast.makeText(LoginActivity.this, getString(R.string.rename_failed), Toast.LENGTH_LONG).show(); } } + } - @Override - public void onAddWallet(boolean testnet, String type) { - setNet(testnet); - if (checkServiceRunning()) return; - startGenerateFragment(type); + // copy + delete seems safer than rename because we call rollback easily + boolean renameWallet(File walletFile, String newName) { + if (copyWallet(walletFile, new File(walletFile.getParentFile(), newName), false, true)) { + deleteWallet(walletFile); + return true; + } else { + return false; } + } - AlertDialog passwordDialog = null; // for preventing multiple clicks in wallet list - - void promptPassword(final String wallet, final PasswordAction action) { - if (passwordDialog != null) return; // we are already asking for password - Context context = LoginActivity.this; - LayoutInflater li = LayoutInflater.from(context); - View promptsView = li.inflate(R.layout.prompt_password, null); - - AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(context); - alertDialogBuilder.setView(promptsView); + @Override + public void onWalletRename(final String walletName) { + Timber.d("rename for wallet ." + walletName + "."); + if (checkServiceRunning()) return; + LayoutInflater li = LayoutInflater.from(this); + View promptsView = li.inflate(R.layout.prompt_rename, null); - final TextInputLayout etPassword = (TextInputLayout) promptsView.findViewById(R.id.etPassword); - etPassword.setHint(LoginActivity.this.getString(R.string.prompt_password, wallet)); + AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this); + alertDialogBuilder.setView(promptsView); - etPassword.getEditText().addTextChangedListener(new TextWatcher() { + final EditText etRename = (EditText) promptsView.findViewById(R.id.etRename); + final TextView tvRenameLabel = (TextView) promptsView.findViewById(R.id.tvRenameLabel); - @Override - public void afterTextChanged(Editable s) { - if (etPassword.getError() != null) { - etPassword.setError(null); - } - } + tvRenameLabel.setText(getString(R.string.prompt_rename, walletName)); - @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(LoginActivity.this); - dialog.cancel(); - passwordDialog = null; - } - }); - passwordDialog = alertDialogBuilder.create(); - - passwordDialog.setOnShowListener(new DialogInterface.OnShowListener() { - @Override - public void onShow(DialogInterface dialog) { - Button button = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE); - button.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - String pass = etPassword.getEditText().getText().toString(); - if (processPasswordEntry(wallet, pass, action)) { + // set dialog message + alertDialogBuilder + .setCancelable(false) + .setPositiveButton(getString(R.string.label_ok), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { Helper.hideKeyboardAlways(LoginActivity.this); - passwordDialog.dismiss(); - passwordDialog = null; - } else { - etPassword.setError(getString(R.string.bad_password)); + String newName = etRename.getText().toString(); + new AsyncRename().execute(walletName, newName); } - } - }); - } - }); - - Helper.showKeyboard(passwordDialog); - - // accept keyboard "ok" - etPassword.getEditText().setOnEditorActionListener(new TextView.OnEditorActionListener() { - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_DONE)) { - String pass = etPassword.getEditText().getText().toString(); - if (processPasswordEntry(wallet, pass, action)) { - Helper.hideKeyboardAlways(LoginActivity.this); - passwordDialog.dismiss(); - passwordDialog = null; - } else { - etPassword.setError(getString(R.string.bad_password)); - } - return true; - } + }) + .setNegativeButton(getString(R.string.label_cancel), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + Helper.hideKeyboardAlways(LoginActivity.this); + dialog.cancel(); + } + }); + + final AlertDialog dialog = alertDialogBuilder.create(); + Helper.showKeyboard(dialog); + + // accept keyboard "ok" + etRename.setOnEditorActionListener(new TextView.OnEditorActionListener() { + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_DONE)) { + Helper.hideKeyboardAlways(LoginActivity.this); + String newName = etRename.getText().toString(); + dialog.cancel(); + new AsyncRename().execute(walletName, newName); return false; } - }); - - passwordDialog.show(); - } - - private boolean checkWalletPassword(String walletName, String password) { - String walletPath = new File(Helper.getStorageRoot(getApplicationContext()), - walletName + ".keys").getAbsolutePath(); - // only test view key - return WalletManager.getInstance().verifyWalletPassword(walletPath, password, true); - } - - interface PasswordAction { - void action(String walletName, String password); - } - - private boolean processPasswordEntry(String walletName, String pass, PasswordAction action) { - if (checkWalletPassword(walletName, pass)) { - action.action(walletName, pass); - return true; - } else { return false; } - } + }); - //////////////////////////////////////// - // LoginFragment.Listener - //////////////////////////////////////// - @Override - public SharedPreferences getPrefs() { - return getPreferences(Context.MODE_PRIVATE); - } + dialog.show(); + } + private class AsyncBackup extends AsyncTask { @Override - public File getStorageRoot() { - return Helper.getStorageRoot(getApplicationContext()); + protected void onPreExecute() { + super.onPreExecute(); + showProgressDialog(R.string.backup_progress); } - //////////////////////////////////////// - //////////////////////////////////////// - @Override - public void showNet(boolean testnet) { - if (testnet) { - toolbar.setBackgroundResource(R.color.colorPrimaryDark); - } else { - toolbar.setBackgroundResource(R.drawable.backgound_toolbar_mainnet); - } - toolbar.setSubtitle(getString(testnet ? R.string.connect_testnet : R.string.connect_mainnet)); + protected Boolean doInBackground(String... params) { + if (params.length != 1) return false; + return backupWallet(params[0]); } @Override - protected void onPause() { - Timber.d("onPause()"); - super.onPause(); - } - - ProgressDialog progressDialog = null; - - private void showProgressDialog(int msgId) { - showProgressDialog(msgId, 0); - } - - private void showProgressDialog(int msgId, long delay) { - dismissProgressDialog(); // just in case - progressDialog = new MyProgressDialog(LoginActivity.this, msgId); - if (delay > 0) { - Handler handler = new Handler(); - handler.postDelayed(new Runnable() { - public void run() { - if (progressDialog != null) progressDialog.show(); - } - }, delay); - } else { - progressDialog.show(); + protected void onPostExecute(Boolean result) { + super.onPostExecute(result); + if (isDestroyed()) { + return; } - } - - private void dismissProgressDialog() { - if (progressDialog != null && progressDialog.isShowing()) { - progressDialog.dismiss(); + dismissProgressDialog(); + if (!result) { + Toast.makeText(LoginActivity.this, getString(R.string.backup_failed), Toast.LENGTH_LONG).show(); } - progressDialog = null; } + } + private class AsyncBackupToNFC extends AsyncTask { @Override - protected void onDestroy() { - dismissProgressDialog(); - super.onDestroy(); + protected void onPreExecute() { + super.onPreExecute(); + showProgressDialog(R.string.backup_progress); } @Override - protected void onResume() { - super.onResume(); - Timber.d("onResume()"); - // wait for WalletService to finish - if (WalletService.Running && (progressDialog == null)) { - // and show a progress dialog, but only if there isn't one already - new AsyncWaitForService().execute(); - } + protected Boolean doInBackground(String... params) { + if (params.length != 1) return false; + return backupWalletToNFC(params[0]); } - private class MyProgressDialog extends ProgressDialog { - Activity activity; - - public MyProgressDialog(Activity activity, int msgId) { - super(activity); - this.activity = activity; - setCancelable(false); - setMessage(activity.getString(msgId)); + @Override + protected void onPostExecute(Boolean result) { + super.onPostExecute(result); + if (isDestroyed()) { + return; } - - @Override - public void onBackPressed() { - // prevent back button + dismissProgressDialog(); + if (!result) { + Toast.makeText(LoginActivity.this, getString(R.string.backup_failed), Toast.LENGTH_LONG).show(); } } + } - - private class AsyncWaitForService extends AsyncTask { - @Override - protected void onPreExecute() { - super.onPreExecute(); - showProgressDialog(R.string.service_progress); - } - - @Override - protected Void doInBackground(Void... params) { - try { - while (WalletService.Running & !isCancelled()) { - Thread.sleep(250); - } - } catch (InterruptedException ex) { - // oh well ... - } - return null; + private boolean backupWallet(String walletName) { + File backupFolder = new File(getStorageRoot(), "backups"); + if (!backupFolder.exists()) { + if (!backupFolder.mkdir()) { + Timber.e("Cannot create backup dir %s", backupFolder.getAbsolutePath()); + return false; } + // make folder visible over USB/MTP + MediaScannerConnection.scanFile(this, new String[]{backupFolder.toString()}, null, null); + } + File walletFile = Helper.getWalletFile(LoginActivity.this, walletName); + File backupFile = new File(backupFolder, walletName); + Timber.d("backup " + walletFile.getAbsolutePath() + " to " + backupFile.getAbsolutePath()); + // TODO probably better to copy to a new file and then rename + // then if something fails we have the old backup at least + // or just create a new backup every time and keep n old backups + boolean success = copyWallet(walletFile, backupFile, true, true); + Timber.d("copyWallet is %s", success); + return success; + } + + private boolean backupWalletToNFC(String walletName) { + boolean success=false; + File walletFile = Helper.getWalletFile(LoginActivity.this, walletName); + //Timber.d("backup " + walletFile.getAbsolutePath() + " to " + backupFile.getAbsolutePath()); + Intent intent = null; + TagUtil tagUtil = null; + boolean authenticated=false; + try { + tagUtil = TagUtil.selectTag(intent, false); + authenticated = tagUtil.authentication(intent, getKey(), false); + }catch (Exception e) { + e.printStackTrace(); + } + + if(authenticated) + { + Toast toast = Toast.makeText(LoginActivity.this, "Authentication Successful", Toast.LENGTH_LONG); + try { - @Override - protected void onPostExecute(Void result) { - super.onPostExecute(result); - if (isDestroyed()) { - return; - } - dismissProgressDialog(); + byte[] bytes = new byte[(int)walletFile.length()]; + new FileInputStream(walletFile).read(bytes); + tagUtil.writeTag(intent,(byte)4,bytes,false); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (AuthenticationException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); } } - - void startWallet(String walletName, String walletPassword) { - Timber.d("startWallet()"); - Intent intent = new Intent(getApplicationContext(), WalletActivity.class); - intent.putExtra(WalletActivity.REQUEST_ID, walletName); - intent.putExtra(WalletActivity.REQUEST_PW, walletPassword); - startActivity(intent); - } - - void startDetails(File walletFile, String password, String type) { - Timber.d("startDetails()"); - Bundle b = new Bundle(); - b.putString("path", walletFile.getAbsolutePath()); - b.putString("password", password); - b.putString("type", type); - startReviewFragment(b); - } - - void startReceive(File walletFile, String password) { - Timber.d("startReceive()"); - Bundle b = new Bundle(); - b.putString("path", walletFile.getAbsolutePath()); - b.putString("password", password); - startReceiveFragment(b); + else + return false; + return success; + } + private byte[] getKey() + { + return ThreeDES.secretKey; + } + + private class AsyncBackupT extends AsyncTask { + @Override + protected void onPreExecute() { + super.onPreExecute(); + showProgressDialog(R.string.backup_progress); } @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { - Timber.d("onRequestPermissionsResult()"); - switch (requestCode) { - case Helper.PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE: - // If request is cancelled, the result arrays are empty. - if (grantResults.length > 0 - && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - startLoginFragment = true; - } else { - String msg = getString(R.string.message_strorage_not_permitted); - Timber.e(msg); - Toast.makeText(this, msg, Toast.LENGTH_LONG).show(); - } - break; - default: - } + protected Boolean doInBackground(String... params) { + if (params.length != 1) return false; + return backupWallet(params[0]); } - private boolean startLoginFragment = false; - @Override - protected void onResumeFragments() { - super.onResumeFragments(); - if (startLoginFragment) { - startLoginFragment(); - startLoginFragment = false; + protected void onPostExecute(Boolean result) { + super.onPostExecute(result); + if (isDestroyed()) { + return; + } + dismissProgressDialog(); + if (!result) { + Toast.makeText(LoginActivity.this, getString(R.string.backup_failed), Toast.LENGTH_LONG).show(); } } + } + @Override + public void onWalletBackupToFile(String walletName) { + Timber.d("backup for wallet ." + walletName + "."); + new AsyncBackup().execute(walletName); + } - void startLoginFragment() { - Fragment fragment = new LoginFragment(); - getSupportFragmentManager().beginTransaction() - .add(R.id.fragment_container, fragment).commit(); - Timber.d("LoginFragment added"); - } - - void startGenerateFragment(String type) { - Bundle extras = new Bundle(); - extras.putString(GenerateFragment.TYPE, type); - replaceFragment(new GenerateFragment(), GENERATE_STACK, extras); - Timber.d("GenerateFragment placed"); - } - - void startReviewFragment(Bundle extras) { - replaceFragment(new GenerateReviewFragment(), null, extras); - Timber.d("GenerateReviewFragment placed"); - } + @Override + public void onWalletBackupToNFC(String walletName) { + Timber.d("backup to NFC hard wallet for wallet ." + walletName + "."); + new AsyncBackupToNFC().execute(walletName); + } - void startReceiveFragment(Bundle extras) { - replaceFragment(new ReceiveFragment(), null, extras); - Timber.d("ReceiveFragment placed"); + private class AsyncArchive extends AsyncTask { + @Override + protected void onPreExecute() { + super.onPreExecute(); + showProgressDialog(R.string.archive_progress); } - void replaceFragment(Fragment newFragment, String stackName, Bundle extras) { - if (extras != null) { - newFragment.setArguments(extras); + @Override + protected Boolean doInBackground(String... params) { + if (params.length != 1) return false; + String walletName = params[0]; + if (backupWallet(walletName) && deleteWallet(Helper.getWalletFile(LoginActivity.this, walletName))) { + KeyStoreHelper.removeWalletUserPass(LoginActivity.this, walletName); + return true; + } else { + return false; } - FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); - transaction.replace(R.id.fragment_container, newFragment); - transaction.addToBackStack(stackName); - transaction.commit(); - } - - void popFragmentStack(String name) { - getSupportFragmentManager().popBackStack(name, FragmentManager.POP_BACK_STACK_INCLUSIVE); } - ////////////////////////////////////////// - // GenerateFragment.Listener - ////////////////////////////////////////// - static final String MNEMONIC_LANGUAGE = "English"; // see mnemonics/electrum-words.cpp for more - - private class AsyncCreateWallet extends AsyncTask { - final String walletName; - final String walletPassword; - final WalletCreator walletCreator; - - File newWalletFile; - - public AsyncCreateWallet(final String name, final String password, - final WalletCreator walletCreator) { - super(); - this.walletName = name; - this.walletPassword = password; - this.walletCreator = walletCreator; + @Override + protected void onPostExecute(Boolean result) { + super.onPostExecute(result); + if (isDestroyed()) { + return; } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - showProgressDialog(R.string.generate_wallet_creating); + dismissProgressDialog(); + if (result) { + reloadWalletList(); + } else { + Toast.makeText(LoginActivity.this, getString(R.string.archive_failed), Toast.LENGTH_LONG).show(); } + } + } + @Override + public void onWalletArchive(final String walletName) { + Timber.d("archive for wallet ." + walletName + "."); + if (checkServiceRunning()) return; + DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() { @Override - protected Boolean doInBackground(Void... params) { - // check if the wallet we want to create already exists - File walletFolder = getStorageRoot(); - if (!walletFolder.isDirectory()) { - Timber.e("Wallet dir " + walletFolder.getAbsolutePath() + "is not a directory"); - return false; - } - File cacheFile = new File(walletFolder, walletName); - File keysFile = new File(walletFolder, walletName + ".keys"); - File addressFile = new File(walletFolder, walletName + ".address.txt"); - - if (cacheFile.exists() || keysFile.exists() || addressFile.exists()) { - Timber.e("Some wallet files already exist for %s", cacheFile.getAbsolutePath()); - return false; - } - - newWalletFile = new File(walletFolder, walletName); - boolean success = walletCreator.createWallet(newWalletFile, walletPassword); - if (success) { - return true; - } else { - Timber.e("Could not create new wallet in %s", newWalletFile.getAbsolutePath()); - return false; + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case DialogInterface.BUTTON_POSITIVE: + new AsyncArchive().execute(walletName); + break; + case DialogInterface.BUTTON_NEGATIVE: + // do nothing + break; } } - - @Override - protected void onPostExecute(Boolean result) { - super.onPostExecute(result); - if (isDestroyed()) { - return; - } - dismissProgressDialog(); - if (result) { - startDetails(newWalletFile, walletPassword, GenerateReviewFragment.VIEW_TYPE_ACCEPT); - } else { - walletGenerateError(); - } + }; + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setMessage(getString(R.string.archive_alert_message)) + .setTitle(walletName) + .setPositiveButton(getString(R.string.archive_alert_yes), dialogClickListener) + .setNegativeButton(getString(R.string.archive_alert_no), dialogClickListener) + .show(); + } + + void reloadWalletList() { + Timber.d("reloadWalletList()"); + try { + LoginFragment loginFragment = (LoginFragment) + getSupportFragmentManager().findFragmentById(R.id.fragment_container); + if (loginFragment != null) { + loginFragment.loadList(); + } + } catch (ClassCastException ex) { + } + } + + public void onWalletChangePassword() {//final String walletName, final String walletPassword) { + try { + GenerateReviewFragment detailsFragment = (GenerateReviewFragment) + getSupportFragmentManager().findFragmentById(R.id.fragment_container); + AlertDialog dialog = detailsFragment.createChangePasswordDialog(); + if (dialog != null) { + Helper.showKeyboard(dialog); + dialog.show(); } + } catch (ClassCastException ex) { + Timber.w("onWalletChangePassword() called, but no GenerateReviewFragment active"); + } + } + + @Override + public void onAddWallet(String type) { + if (checkServiceRunning()) return; + startGenerateFragment(type); + } + + //////////////////////////////////////// + // LoginFragment.Listener + //////////////////////////////////////// + @Override + public SharedPreferences getPrefs() { + return getPreferences(Context.MODE_PRIVATE); + } + + @Override + public File getStorageRoot() { + return Helper.getWalletRoot(getApplicationContext()); + } + + //////////////////////////////////////// + //////////////////////////////////////// + + @Override + public void showNet() { + switch (WalletManager.getInstance().getNetworkType()) { + case NetworkType_Mainnet: + toolbar.setSubtitle(getString(R.string.connect_mainnet)); + toolbar.setBackgroundResource(R.drawable.backgound_toolbar_mainnet); + break; + case NetworkType_Testnet: + toolbar.setSubtitle(getString(R.string.connect_testnet)); + toolbar.setBackgroundResource(R.color.colorPrimaryDark); + break; + case NetworkType_Stagenet: + toolbar.setSubtitle(getString(R.string.connect_stagenet)); + toolbar.setBackgroundResource(R.color.colorPrimaryDark); + break; + default: + throw new IllegalStateException("NetworkType unknown: " + WalletManager.getInstance().getNetworkType()); + } + } + + @Override + protected void onPause() { + Timber.d("onPause()"); + super.onPause(); + } + + ProgressDialog progressDialog = null; + + @Override + public void showProgressDialog(int msgId) { + showProgressDialog(msgId, 0); + } + + private void showProgressDialog(int msgId, long delay) { + dismissProgressDialog(); // just in case + progressDialog = new MyProgressDialog(LoginActivity.this, msgId); + if (delay > 0) { + Handler handler = new Handler(); + handler.postDelayed(new Runnable() { + public void run() { + if (progressDialog != null) progressDialog.show(); + } + }, delay); + } else { + progressDialog.show(); } + } - public void createWallet(final String name, final String password, - final WalletCreator walletCreator) { - new AsyncCreateWallet(name, password, walletCreator) - .executeOnExecutor(MoneroThreadPoolExecutor.MONERO_THREAD_POOL_EXECUTOR); + @Override + public void dismissProgressDialog() { + if (progressDialog != null && progressDialog.isShowing()) { + progressDialog.dismiss(); } + progressDialog = null; + } - void walletGenerateError() { - try { - GenerateFragment genFragment = (GenerateFragment) - getSupportFragmentManager().findFragmentById(R.id.fragment_container); - genFragment.walletGenerateError(); - } catch (ClassCastException ex) { - Timber.e("walletGenerateError() but not in GenerateFragment"); - } + @Override + protected void onDestroy() { + dismissProgressDialog(); + super.onDestroy(); + } + + @Override + protected void onResume() { + super.onResume(); + Timber.d("onResume()"); + // wait for WalletService to finish + if (WalletService.Running && (progressDialog == null)) { + // and show a progress dialog, but only if there isn't one already + new AsyncWaitForService().execute(); } + } - interface WalletCreator { - boolean createWallet(File aFile, String password); + private class MyProgressDialog extends ProgressDialog { + Activity activity; + MyProgressDialog(Activity activity, int msgId) { + super(activity); + this.activity = activity; + setCancelable(false); + setMessage(activity.getString(msgId)); } @Override - public void onGenerate(final String name, final String password) { - createWallet(name, password, - new WalletCreator() { - public boolean createWallet(File aFile, String password) { - Wallet newWallet = WalletManager.getInstance() - .createWallet(aFile, password, MNEMONIC_LANGUAGE); - boolean success = (newWallet.getStatus() == Wallet.Status.Status_Ok); - if (!success) { - Timber.e(newWallet.getErrorString()); - toast(newWallet.getErrorString()); - } - newWallet.close(); - return success; - } - }); + public void onBackPressed() { + // prevent back button } + } + + private class AsyncWaitForService extends AsyncTask { @Override - public void onGenerate(final String name, final String password, final String seed, - final long restoreHeight) { - createWallet(name, password, - new WalletCreator() { - public boolean createWallet(File aFile, String password) { - Wallet newWallet = WalletManager.getInstance(). - recoveryWallet(aFile, password, seed, restoreHeight); - boolean success = (newWallet.getStatus() == Wallet.Status.Status_Ok); - if (!success) { - Timber.e(newWallet.getErrorString()); - toast(newWallet.getErrorString()); - } - newWallet.close(); - return success; - } - }); + protected void onPreExecute() { + super.onPreExecute(); + showProgressDialog(R.string.service_progress); } @Override - public void onGenerate(final String name, final String password, - final String address, final String viewKey, final String spendKey, - final long restoreHeight) { - createWallet(name, password, - new WalletCreator() { - public boolean createWallet(File aFile, String password) { - Wallet newWallet = WalletManager.getInstance() - .createWalletWithKeys(aFile, password, MNEMONIC_LANGUAGE, restoreHeight, - address, viewKey, spendKey); - boolean success = (newWallet.getStatus() == Wallet.Status.Status_Ok); - if (!success) { - Timber.e(newWallet.getErrorString()); - toast(newWallet.getErrorString()); - } - newWallet.close(); - return success; - } - }); + protected Void doInBackground(Void... params) { + try { + while (WalletService.Running & !isCancelled()) { + Thread.sleep(250); + } + } catch (InterruptedException ex) { + // oh well ... + } + return null; } - void toast(final String msg) { - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(LoginActivity.this, msg, Toast.LENGTH_LONG).show(); + @Override + protected void onPostExecute(Void result) { + super.onPostExecute(result); + if (isDestroyed()) { + return; + } + dismissProgressDialog(); + } + } + + void startWallet(String walletName, String walletPassword, boolean fingerprintUsed) { + Timber.d("startWallet()"); + Intent intent = new Intent(getApplicationContext(), WalletActivity.class); + intent.putExtra(WalletActivity.REQUEST_ID, walletName); + intent.putExtra(WalletActivity.REQUEST_PW, walletPassword); + intent.putExtra(WalletActivity.REQUEST_FINGERPRINT_USED, fingerprintUsed); + startActivity(intent); + } + + void startDetails(File walletFile, String password, String type) { + Timber.d("startDetails()"); + Bundle b = new Bundle(); + b.putString("path", walletFile.getAbsolutePath()); + b.putString("password", password); + b.putString("type", type); + startReviewFragment(b); + } + + void startReceive(File walletFile, String password) { + Timber.d("startReceive()"); + Bundle b = new Bundle(); + b.putString("path", walletFile.getAbsolutePath()); + b.putString("password", password); + startReceiveFragment(b); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { + Timber.d("onRequestPermissionsResult()"); + switch (requestCode) { + case Helper.PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE: + // If request is cancelled, the result arrays are empty. + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + startLoginFragment = true; + } else { + String msg = getString(R.string.message_strorage_not_permitted); + Timber.e(msg); + Toast.makeText(this, msg, Toast.LENGTH_LONG).show(); } - }); + break; + default: + } + } + + private boolean startLoginFragment = false; + + @Override + protected void onResumeFragments() { + super.onResumeFragments(); + if (startLoginFragment) { + startLoginFragment(); + startLoginFragment = false; + } + } + + void startLoginFragment() { + // we set these here because we cannot be ceratin we have permissions for storage before + Helper.setMoneroHome(this); + Helper.initLogger(this); + Fragment fragment = new LoginFragment(); + getSupportFragmentManager().beginTransaction() + .add(R.id.fragment_container, fragment).commit(); + Timber.d("LoginFragment added"); + } + + void startGenerateFragment(String type) { + Bundle extras = new Bundle(); + extras.putString(GenerateFragment.TYPE, type); + replaceFragment(new GenerateFragment(), GENERATE_STACK, extras); + Timber.d("GenerateFragment placed"); + } + + void startReviewFragment(Bundle extras) { + replaceFragment(new GenerateReviewFragment(), null, extras); + Timber.d("GenerateReviewFragment placed"); + } + + void startReceiveFragment(Bundle extras) { + replaceFragment(new ReceiveFragment(), null, extras); + Timber.d("ReceiveFragment placed"); + } + + void replaceFragment(Fragment newFragment, String stackName, Bundle extras) { + if (extras != null) { + newFragment.setArguments(extras); + } + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); + transaction.replace(R.id.fragment_container, newFragment); + transaction.addToBackStack(stackName); + transaction.commit(); + } + + void popFragmentStack(String name) { + getSupportFragmentManager().popBackStack(name, FragmentManager.POP_BACK_STACK_INCLUSIVE); + } + + ////////////////////////////////////////// + // GenerateFragment.Listener + ////////////////////////////////////////// + static final String MNEMONIC_LANGUAGE = "English"; // see mnemonics/electrum-words.cpp for more + + private class AsyncCreateWallet extends AsyncTask { + final String walletName; + final String walletPassword; + final WalletCreator walletCreator; + + File newWalletFile; + + AsyncCreateWallet(final String name, final String password, + final WalletCreator walletCreator) { + super(); + this.walletName = name; + this.walletPassword = password; + this.walletCreator = walletCreator; + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + showProgressDialog(R.string.generate_wallet_creating); } @Override - public void onAccept(final String name, final String password) { + protected Boolean doInBackground(Void... params) { + // check if the wallet we want to create already exists File walletFolder = getStorageRoot(); - File walletFile = new File(walletFolder, name); - Timber.d("New Wallet %s", walletFile.getAbsolutePath()); - walletFile.delete(); // when recovering wallets, the cache seems corrupt + if (!walletFolder.isDirectory()) { + Timber.e("Wallet dir " + walletFolder.getAbsolutePath() + "is not a directory"); + return false; + } + File cacheFile = new File(walletFolder, walletName); + File keysFile = new File(walletFolder, walletName + ".keys"); + File addressFile = new File(walletFolder, walletName + ".address.txt"); - boolean rc = testWallet(walletFile.getAbsolutePath(), password) == Wallet.Status.Status_Ok; + if (cacheFile.exists() || keysFile.exists() || addressFile.exists()) { + Timber.e("Some wallet files already exist for %s", cacheFile.getAbsolutePath()); + return false; + } - if (rc) { - popFragmentStack(GENERATE_STACK); - Toast.makeText(LoginActivity.this, - getString(R.string.generate_wallet_created), Toast.LENGTH_SHORT).show(); + newWalletFile = new File(walletFolder, walletName); + boolean success = walletCreator.createWallet(newWalletFile, walletPassword); + if (success) { + return true; } else { - Timber.e("Wallet store failed to %s", walletFile.getAbsolutePath()); - Toast.makeText(LoginActivity.this, getString(R.string.generate_wallet_create_failed), Toast.LENGTH_LONG).show(); + Timber.e("Could not create new wallet in %s", newWalletFile.getAbsolutePath()); + return false; } } - Wallet.Status testWallet(String path, String password) { - Timber.d("testing wallet %s", path); - Wallet aWallet = WalletManager.getInstance().openWallet(path, password); - if (aWallet == null) return Wallet.Status.Status_Error; // does this ever happen? - Wallet.Status status = aWallet.getStatus(); - Timber.d("wallet tested %s", aWallet.getStatus()); - aWallet.close(); - return status; - } - - boolean walletExists(File walletFile, boolean any) { - File dir = walletFile.getParentFile(); - String name = walletFile.getName(); - if (any) { - return new File(dir, name).exists() - || new File(dir, name + ".keys").exists() - || new File(dir, name + ".address.txt").exists(); + @Override + protected void onPostExecute(Boolean result) { + super.onPostExecute(result); + if (isDestroyed()) { + return; + } + dismissProgressDialog(); + if (result) { + startDetails(newWalletFile, walletPassword, GenerateReviewFragment.VIEW_TYPE_ACCEPT); } else { - return new File(dir, name).exists() - && new File(dir, name + ".keys").exists() - && new File(dir, name + ".address.txt").exists(); + walletGenerateError(); } } - - boolean copyWallet(File srcWallet, File dstWallet, boolean overwrite, boolean ignoreCacheError) { - if (walletExists(dstWallet, true) && !overwrite) return false; - boolean success = false; - File srcDir = srcWallet.getParentFile(); - String srcName = srcWallet.getName(); - File dstDir = dstWallet.getParentFile(); - String dstName = dstWallet.getName(); - try { - try { - copyFile(new File(srcDir, srcName), new File(dstDir, dstName)); - } catch (IOException ex) { - Timber.d("CACHE %s", ignoreCacheError); - if (!ignoreCacheError) { // ignore cache backup error if backing up (can be resynced) - throw ex; + } + + public void createWallet(final String name, final String password, + final WalletCreator walletCreator) { + new AsyncCreateWallet(name, password, walletCreator) + .executeOnExecutor(MoneroThreadPoolExecutor.MONERO_THREAD_POOL_EXECUTOR); + } + + void walletGenerateError() { + try { + GenerateFragment genFragment = (GenerateFragment) + getSupportFragmentManager().findFragmentById(R.id.fragment_container); + genFragment.walletGenerateError(); + } catch (ClassCastException ex) { + Timber.e("walletGenerateError() but not in GenerateFragment"); + } + } + + interface WalletCreator { + boolean createWallet(File aFile, String password); + + } + + @Override + public void onGenerate(final String name, final String password) { + createWallet(name, password, + new WalletCreator() { + public boolean createWallet(File aFile, String password) { + Wallet newWallet = WalletManager.getInstance() + .createWallet(aFile, password, MNEMONIC_LANGUAGE); + boolean success = (newWallet.getStatus() == Wallet.Status.Status_Ok); + if (!success) { + Timber.e(newWallet.getErrorString()); + toast(newWallet.getErrorString()); + } + newWallet.close(); + return success; } - } - copyFile(new File(srcDir, srcName + ".keys"), new File(dstDir, dstName + ".keys")); - copyFile(new File(srcDir, srcName + ".address.txt"), new File(dstDir, dstName + ".address.txt")); - success = true; - } catch (IOException ex) { - Timber.e("wallet copy failed: %s", ex.getMessage()); - // try to rollback - deleteWallet(dstWallet); - } - return success; - } + }); + } + + @Override + public void onGenerate(final String name, final String password, final String seed, + final long restoreHeight) { + createWallet(name, password, + new WalletCreator() { + public boolean createWallet(File aFile, String password) { + Wallet newWallet = WalletManager.getInstance(). + recoveryWallet(aFile, password, seed, restoreHeight); + boolean success = (newWallet.getStatus() == Wallet.Status.Status_Ok); + if (!success) { + Timber.e(newWallet.getErrorString()); + toast(newWallet.getErrorString()); + } + newWallet.close(); + return success; + } + }); + } + + @Override + public void onGenerate(final String name, final String password, + final String address, final String viewKey, final String spendKey, + final long restoreHeight) { + createWallet(name, password, + new WalletCreator() { + public boolean createWallet(File aFile, String password) { + Wallet newWallet = WalletManager.getInstance() + .createWalletWithKeys(aFile, password, MNEMONIC_LANGUAGE, restoreHeight, + address, viewKey, spendKey); + boolean success = (newWallet.getStatus() == Wallet.Status.Status_Ok); + if (!success) { + Timber.e(newWallet.getErrorString()); + toast(newWallet.getErrorString()); + } + newWallet.close(); + return success; + } + }); + } - // do our best to delete as much as possible of the wallet files - boolean deleteWallet(File walletFile) { - Timber.d("deleteWallet %s", walletFile.getAbsolutePath()); - File dir = walletFile.getParentFile(); - String name = walletFile.getName(); - boolean success = true; - File cacheFile = new File(dir, name); - if (cacheFile.exists()) { - success = cacheFile.delete(); - } - success = new File(dir, name + ".keys").delete() && success; - File addressFile = new File(dir, name + ".address.txt"); - if (addressFile.exists()) { - success = addressFile.delete() && success; + void toast(final String msg) { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(LoginActivity.this, msg, Toast.LENGTH_LONG).show(); } - Timber.d("deleteWallet is %s", success); - return success; - } - - void copyFile(File src, File dst) throws IOException { - FileChannel inChannel = new FileInputStream(src).getChannel(); - FileChannel outChannel = new FileOutputStream(dst).getChannel(); + }); + } + + @Override + public void onAccept(final String name, final String password) { + File walletFolder = getStorageRoot(); + File walletFile = new File(walletFolder, name); + Timber.d("New Wallet %s", walletFile.getAbsolutePath()); + walletFile.delete(); // when recovering wallets, the cache seems corrupt + + boolean rc = testWallet(walletFile.getAbsolutePath(), password) == Wallet.Status.Status_Ok; + + if (rc) { + popFragmentStack(GENERATE_STACK); + Toast.makeText(LoginActivity.this, + getString(R.string.generate_wallet_created), Toast.LENGTH_SHORT).show(); + } else { + Timber.e("Wallet store failed to %s", walletFile.getAbsolutePath()); + Toast.makeText(LoginActivity.this, getString(R.string.generate_wallet_create_failed), Toast.LENGTH_LONG).show(); + } + } + + Wallet.Status testWallet(String path, String password) { + Timber.d("testing wallet %s", path); + Wallet aWallet = WalletManager.getInstance().openWallet(path, password); + if (aWallet == null) return Wallet.Status.Status_Error; // does this ever happen? + Wallet.Status status = aWallet.getStatus(); + Timber.d("wallet tested %s", aWallet.getStatus()); + aWallet.close(); + return status; + } + + boolean walletExists(File walletFile, boolean any) { + File dir = walletFile.getParentFile(); + String name = walletFile.getName(); + if (any) { + return new File(dir, name).exists() + || new File(dir, name + ".keys").exists() + || new File(dir, name + ".address.txt").exists(); + } else { + return new File(dir, name).exists() + && new File(dir, name + ".keys").exists() + && new File(dir, name + ".address.txt").exists(); + } + } + + boolean copyWallet(File srcWallet, File dstWallet, boolean overwrite, boolean ignoreCacheError) { + if (walletExists(dstWallet, true) && !overwrite) return false; + boolean success = false; + File srcDir = srcWallet.getParentFile(); + String srcName = srcWallet.getName(); + File dstDir = dstWallet.getParentFile(); + String dstName = dstWallet.getName(); + try { try { - inChannel.transferTo(0, inChannel.size(), outChannel); - } finally { - if (inChannel != null) - inChannel.close(); - if (outChannel != null) - outChannel.close(); - } - } - - @Override - public void onBackPressed() { - Fragment f = getSupportFragmentManager().findFragmentById(R.id.fragment_container); - if (f instanceof GenerateReviewFragment) { - if (((GenerateReviewFragment) f).backOk()) { - super.onBackPressed(); - } - } else if (f instanceof LoginFragment) { - if (((LoginFragment) f).isFabOpen()) { - ((LoginFragment) f).animateFAB(); - } else { - super.onBackPressed(); + copyFile(new File(srcDir, srcName), new File(dstDir, dstName)); + } catch (IOException ex) { + Timber.d("CACHE %s", ignoreCacheError); + if (!ignoreCacheError) { // ignore cache backup error if backing up (can be resynced) + throw ex; } - } else { + } + copyFile(new File(srcDir, srcName + ".keys"), new File(dstDir, dstName + ".keys")); + copyFile(new File(srcDir, srcName + ".address.txt"), new File(dstDir, dstName + ".address.txt")); + success = true; + } catch (IOException ex) { + Timber.e("wallet copy failed: %s", ex.getMessage()); + // try to rollback + deleteWallet(dstWallet); + } + return success; + } + + // do our best to delete as much as possible of the wallet files + boolean deleteWallet(File walletFile) { + Timber.d("deleteWallet %s", walletFile.getAbsolutePath()); + File dir = walletFile.getParentFile(); + String name = walletFile.getName(); + boolean success = true; + File cacheFile = new File(dir, name); + if (cacheFile.exists()) { + success = cacheFile.delete(); + } + success = new File(dir, name + ".keys").delete() && success; + File addressFile = new File(dir, name + ".address.txt"); + if (addressFile.exists()) { + success = addressFile.delete() && success; + } + Timber.d("deleteWallet is %s", success); + return success; + } + + void copyFile(File src, File dst) throws IOException { + FileChannel inChannel = new FileInputStream(src).getChannel(); + FileChannel outChannel = new FileOutputStream(dst).getChannel(); + try { + inChannel.transferTo(0, inChannel.size(), outChannel); + } finally { + if (inChannel != null) + inChannel.close(); + if (outChannel != null) + outChannel.close(); + } + } + + @Override + public void onBackPressed() { + Fragment f = getSupportFragmentManager().findFragmentById(R.id.fragment_container); + if (f instanceof GenerateReviewFragment) { + if (((GenerateReviewFragment) f).backOk()) { super.onBackPressed(); } - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.action_create_help_new: - HelpFragment.display(getSupportFragmentManager(), R.string.help_create_new); - return true; - case R.id.action_create_help_keys: - HelpFragment.display(getSupportFragmentManager(), R.string.help_create_keys); - return true; - case R.id.action_create_help_view: - HelpFragment.display(getSupportFragmentManager(), R.string.help_create_view); - return true; - case R.id.action_create_help_seed: - HelpFragment.display(getSupportFragmentManager(), R.string.help_create_seed); - return true; - case R.id.action_details_help: - HelpFragment.display(getSupportFragmentManager(), R.string.help_details); - return true; - case R.id.action_license_info: - AboutFragment.display(getSupportFragmentManager()); - return true; - case R.id.action_help_list: - HelpFragment.display(getSupportFragmentManager(), R.string.help_list); - return true; - case R.id.action_privacy_policy: - PrivacyFragment.display(getSupportFragmentManager()); - return true; - case R.id.action_testnet: - try { - LoginFragment loginFragment = (LoginFragment) - getSupportFragmentManager().findFragmentById(R.id.fragment_container); - item.setChecked(loginFragment.onTestnetMenuItem()); - } catch (ClassCastException ex) { - } - return true; - default: - return super.onOptionsItemSelected(item); + } else if (f instanceof LoginFragment) { + if (((LoginFragment) f).isFabOpen()) { + ((LoginFragment) f).animateFAB(); + } else { + super.onBackPressed(); } + } else { + super.onBackPressed(); } + } - private void setNet(boolean testnet) { - WalletManager.getInstance().setDaemon("", testnet, "", ""); + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_create_help_new: + HelpFragment.display(getSupportFragmentManager(), R.string.help_create_new); + return true; + case R.id.action_create_help_keys: + HelpFragment.display(getSupportFragmentManager(), R.string.help_create_keys); + return true; + case R.id.action_create_help_view: + HelpFragment.display(getSupportFragmentManager(), R.string.help_create_view); + return true; + case R.id.action_create_help_seed: + HelpFragment.display(getSupportFragmentManager(), R.string.help_create_seed); + return true; + case R.id.action_details_help: + HelpFragment.display(getSupportFragmentManager(), R.string.help_details); + return true; + case R.id.action_details_changepw: + onWalletChangePassword(); + return true; + case R.id.action_license_info: + AboutFragment.display(getSupportFragmentManager()); + return true; + case R.id.action_help_list: + HelpFragment.display(getSupportFragmentManager(), R.string.help_list); + return true; + case R.id.action_privacy_policy: + PrivacyFragment.display(getSupportFragmentManager()); + return true; + case R.id.action_testnet: + try { + LoginFragment loginFragment = (LoginFragment) + getSupportFragmentManager().findFragmentById(R.id.fragment_container); + item.setChecked(loginFragment.onTestnetMenuItem()); + } catch (ClassCastException ex) { + // never mind then + } + return true; + default: + return super.onOptionsItemSelected(item); } + } - static class WalletNode { - String name = null; - String host = ""; - int port = 28081; - String user = ""; - String password = ""; - boolean isTestnet; - - WalletNode(String walletName, String daemon, boolean isTestnet) { - if ((daemon == null) || daemon.isEmpty()) return; - this.name = walletName; - String daemonAddress; - String a[] = daemon.split("@"); - if (a.length == 1) { // no credentials - daemonAddress = a[0]; - } else if (a.length == 2) { // credentials - String userPassword[] = a[0].split(":"); - if (userPassword.length != 2) - throw new IllegalArgumentException("User:Password invalid"); - user = userPassword[0]; - if (!user.isEmpty()) password = userPassword[1]; - daemonAddress = a[1]; - } else { - throw new IllegalArgumentException("Too many @"); - } + public void setNetworkType(NetworkType networkType) { + WalletManager.getInstance().setNetworkType(networkType); + } - String da[] = daemonAddress.split(":"); - if ((da.length > 2) || (da.length < 1)) - throw new IllegalArgumentException("Too many ':' or too few"); - host = da[0]; - if (da.length == 2) { - try { - port = Integer.parseInt(da[1]); - } catch (NumberFormatException ex) { - throw new IllegalArgumentException("Port not numeric"); - } - } else { - port = (isTestnet ? 28081 : 18081); - } - this.isTestnet = isTestnet; - } + private class AsyncOpenWallet extends AsyncTask { + final static int OK = 0; + final static int TIMEOUT = 1; + final static int INVALID = 2; + final static int IOEX = 3; - String getAddress() { - return host + ":" + port; - } + WalletNode walletNode; - boolean isValid() { - return !host.isEmpty(); - } + @Override + protected void onPreExecute() { + super.onPreExecute(); + showProgressDialog(R.string.open_progress, DAEMON_TIMEOUT / 4); } - private class AsyncOpenWallet extends AsyncTask { - final static int OK = 0; - final static int TIMEOUT = 1; - final static int INVALID = 2; - final static int IOEX = 3; + @Override + protected Integer doInBackground(WalletNode... params) { + if (params.length != 1) return INVALID; + this.walletNode = params[0]; + if (!walletNode.isValid()) return INVALID; - WalletNode walletNode; + Timber.d("checking %s", walletNode.getAddress()); - @Override - protected void onPreExecute() { - super.onPreExecute(); - showProgressDialog(R.string.open_progress, DAEMON_TIMEOUT / 4); + try { + long timeDA = new Date().getTime(); + SocketAddress address = walletNode.getSocketAddress(); + long timeDB = new Date().getTime(); + Timber.d("Resolving " + walletNode.getAddress() + " took " + (timeDB - timeDA) + "ms."); + Socket socket = new Socket(); + long timeA = new Date().getTime(); + socket.connect(address, LoginActivity.DAEMON_TIMEOUT); + socket.close(); + long timeB = new Date().getTime(); + long time = timeB - timeA; + Timber.d("Daemon " + walletNode.getAddress() + " is " + time + "ms away."); + return (time < LoginActivity.DAEMON_TIMEOUT ? OK : TIMEOUT); + } catch (IOException ex) { + Timber.d("Cannot reach daemon %s because %s", walletNode.getAddress(), ex.getMessage()); + return IOEX; + } catch (IllegalArgumentException ex) { + Timber.d("Cannot reach daemon %s because %s", walletNode.getAddress(), ex.getMessage()); + return INVALID; } + } - @Override - protected Integer doInBackground(WalletNode... params) { - if (params.length != 1) return INVALID; - this.walletNode = params[0]; - if (!walletNode.isValid()) return INVALID; - - Timber.d("checking %s", walletNode.getAddress()); - - try { - long timeDA = new Date().getTime(); - SocketAddress address = new InetSocketAddress(walletNode.host, walletNode.port); - long timeDB = new Date().getTime(); - Timber.d("Resolving " + walletNode.host + " took " + (timeDB - timeDA) + "ms."); - Socket socket = new Socket(); - long timeA = new Date().getTime(); - socket.connect(address, LoginActivity.DAEMON_TIMEOUT); - socket.close(); - long timeB = new Date().getTime(); - long time = timeB - timeA; - Timber.d("Daemon " + walletNode.host + " is " + time + "ms away."); - return (time < LoginActivity.DAEMON_TIMEOUT ? OK : TIMEOUT); - } catch (IOException ex) { - Timber.d("Cannot reach daemon " + walletNode.host + "/" + walletNode.port + " because " + ex.getMessage()); - return IOEX; - } catch (IllegalArgumentException ex) { - Timber.d("Cannot reach daemon " + walletNode.host + "/" + walletNode.port + " because " + ex.getMessage()); - return INVALID; - } + @Override + protected void onPostExecute(Integer result) { + super.onPostExecute(result); + if (isDestroyed()) { + return; } - - @Override - protected void onPostExecute(Integer result) { - super.onPostExecute(result); - if (isDestroyed()) { - return; - } - dismissProgressDialog(); - switch (result) { - case OK: - Timber.d("selected wallet is ." + walletNode.name + "."); - // now it's getting real, onValidateFields if wallet exists - promptAndStart(walletNode); - break; - case TIMEOUT: - Toast.makeText(LoginActivity.this, getString(R.string.status_wallet_connect_timeout), Toast.LENGTH_LONG).show(); - break; - case INVALID: - Toast.makeText(LoginActivity.this, getString(R.string.status_wallet_node_invalid), Toast.LENGTH_LONG).show(); - break; - case IOEX: - Toast.makeText(LoginActivity.this, getString(R.string.status_wallet_connect_ioex), Toast.LENGTH_LONG).show(); - break; - } + dismissProgressDialog(); + switch (result) { + case OK: + Timber.d("selected wallet is .%s.", walletNode.getName()); + // now it's getting real, onValidateFields if wallet exists + promptAndStart(walletNode); + break; + case TIMEOUT: + Toast.makeText(LoginActivity.this, getString(R.string.status_wallet_connect_timeout), Toast.LENGTH_LONG).show(); + break; + case INVALID: + Toast.makeText(LoginActivity.this, getString(R.string.status_wallet_node_invalid), Toast.LENGTH_LONG).show(); + break; + case IOEX: + Toast.makeText(LoginActivity.this, getString(R.string.status_wallet_connect_ioex), Toast.LENGTH_LONG).show(); + break; } } + } - void promptAndStart(WalletNode walletNode) { - File walletFile = Helper.getWalletFile(this, walletNode.name); - if (WalletManager.getInstance().walletExists(walletFile)) { - WalletManager.getInstance(). - setDaemon(walletNode.getAddress(), walletNode.isTestnet, walletNode.user, walletNode.password); - promptPassword(walletNode.name, new PasswordAction() { - @Override - public void action(String walletName, String password) { - startWallet(walletName, password); - } - }); - } else { // this cannot really happen as we prefilter choices - Toast.makeText(this, getString(R.string.bad_wallet), Toast.LENGTH_SHORT).show(); - } + void promptAndStart(WalletNode walletNode) { + File walletFile = Helper.getWalletFile(this, walletNode.getName()); + if (WalletManager.getInstance().walletExists(walletFile)) { + WalletManager.getInstance().setDaemon(walletNode); + Helper.promptPassword(LoginActivity.this, walletNode.getName(), false, new Helper.PasswordAction() { + @Override + public void action(String walletName, String password, boolean fingerprintUsed) { + startWallet(walletName, password, fingerprintUsed); + } + }); + } else { // this cannot really happen as we prefilter choices + Toast.makeText(this, getString(R.string.bad_wallet), Toast.LENGTH_SHORT).show(); } + } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java b/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java index 62d321eb70..4dff440174 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java @@ -18,12 +18,15 @@ import android.content.Context; import android.content.SharedPreferences; +import android.content.res.ColorStateList; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.design.widget.FloatingActionButton; import android.support.v4.app.Fragment; +import android.support.v4.content.ContextCompat; import android.support.v7.widget.RecyclerView; +import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; @@ -38,22 +41,28 @@ import android.widget.ArrayAdapter; import android.widget.EditText; import android.widget.FrameLayout; +import android.widget.ImageButton; import android.widget.ImageView; +import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; import com.m2049r.xmrwallet.dialog.HelpFragment; import com.m2049r.xmrwallet.layout.WalletInfoAdapter; +import com.m2049r.xmrwallet.model.NetworkType; import com.m2049r.xmrwallet.model.WalletManager; import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.KeyStoreHelper; import com.m2049r.xmrwallet.util.NodeList; +import com.m2049r.xmrwallet.util.Notice; import com.m2049r.xmrwallet.widget.DropDownEditText; import com.m2049r.xmrwallet.widget.Toolbar; import java.io.File; import java.util.ArrayList; import java.util.List; +import java.util.Set; import timber.log.Timber; @@ -70,9 +79,6 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter private DropDownEditText etDaemonAddress; private ArrayAdapter nodeAdapter; - private View llXmrToEnabled; - private View ibXmrToInfoClose; - private Listener activityCallback; // Container Activity must implement this interface @@ -81,11 +87,11 @@ public interface Listener { File getStorageRoot(); - boolean onWalletSelected(String wallet, String daemon, boolean testnet); + boolean onWalletSelected(String wallet, String daemon); - void onWalletDetails(String wallet, boolean testnet); + void onWalletDetails(String wallet); - void onWalletReceive(String wallet, boolean testnet); + void onWalletReceive(String wallet); void onWalletRename(String name); @@ -95,14 +101,16 @@ public interface Listener { void onWalletArchive(String walletName); - void onAddWallet(boolean testnet, String type); + void onAddWallet(String type); - void showNet(boolean testnet); + void showNet(); void setToolbarButton(int type); void setTitle(String title); + void setNetworkType(NetworkType networkType); + } @Override @@ -128,8 +136,8 @@ public void onResume() { super.onResume(); Timber.d("onResume()"); activityCallback.setTitle(null); - activityCallback.setToolbarButton(Toolbar.BUTTON_DONATE); - activityCallback.showNet(isTestnet()); + activityCallback.setToolbarButton(Toolbar.BUTTON_CREDITS); + activityCallback.showNet(); } @Override @@ -172,23 +180,8 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, etDummy = (EditText) view.findViewById(R.id.etDummy); - llXmrToEnabled = view.findViewById(R.id.llXmrToEnabled); - llXmrToEnabled.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - HelpFragment.display(getChildFragmentManager(), R.string.help_xmrto); - - } - }); - ibXmrToInfoClose = view.findViewById(R.id.ibXmrToInfoClose); - ibXmrToInfoClose.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - llXmrToEnabled.setVisibility(View.GONE); - showXmrtoEnabled = false; - saveXmrToPrefs(); - } - }); + ViewGroup llNotice = (ViewGroup) view.findViewById(R.id.llNotice); + Notice.showAll(llNotice,".*_login"); etDaemonAddress = (DropDownEditText) view.findViewById(R.id.etDaemonAddress); nodeAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_dropdown_item_1line); @@ -236,9 +229,6 @@ public void onItemClick(AdapterView parent, View arg1, int pos, long id) { }); loadPrefs(); - if (!showXmrtoEnabled) { - llXmrToEnabled.setVisibility(View.GONE); - } return view; } @@ -246,13 +236,13 @@ public void onItemClick(AdapterView parent, View arg1, int pos, long id) { // Callbacks from WalletInfoAdapter @Override public void onInteraction(final View view, final WalletManager.WalletInfo infoItem) { - String x = isTestnet() ? "9A-" : "4-"; - if (x.indexOf(infoItem.address.charAt(0)) < 0) { + String addressPrefix = addressPrefix(); + if (addressPrefix.indexOf(infoItem.address.charAt(0)) < 0) { Toast.makeText(getActivity(), getString(R.string.prompt_wrong_net), Toast.LENGTH_LONG).show(); return; } - if (activityCallback.onWalletSelected(infoItem.name, getDaemon(), isTestnet())) { + if (activityCallback.onWalletSelected(infoItem.name, getDaemon())) { savePrefs(); } } @@ -275,7 +265,7 @@ public boolean onContextInteraction(MenuItem item, WalletManager.WalletInfo list case R.id.action_backup_file: activityCallback.onWalletBackupToFile(listItem.name); break; - case R.id.action_backup_nfc: //TODO 备份到NFC + case R.id.action_backup_nfc: //backup to NFC activityCallback.onWalletBackupToNFC(listItem.name); break; case R.id.action_archive: @@ -287,11 +277,24 @@ public boolean onContextInteraction(MenuItem item, WalletManager.WalletInfo list return true; } + private String addressPrefix() { + switch (WalletManager.getInstance().getNetworkType()) { + case NetworkType_Testnet: + return "9A-"; + case NetworkType_Mainnet: + return "4-"; + case NetworkType_Stagenet: + return "5-"; + default: + throw new IllegalStateException("Unsupported Network: " + WalletManager.getInstance().getNetworkType()); + } + } + private void filterList() { displayedList.clear(); - String x = isTestnet() ? "9A" : "4"; + String addressPrefix = addressPrefix(); for (WalletManager.WalletInfo s : walletList) { - if (x.indexOf(s.address.charAt(0)) >= 0) displayedList.add(s); + if (addressPrefix.indexOf(s.address.charAt(0)) >= 0) displayedList.add(s); } } @@ -318,14 +321,25 @@ public void loadList() { ivGunther.setImageDrawable(null); } } + + // remove information of non-existent wallet + Set removedWallets = getActivity() + .getSharedPreferences(KeyStoreHelper.SecurityConstants.WALLET_PASS_PREFS_NAME, Context.MODE_PRIVATE) + .getAll().keySet(); + for (WalletManager.WalletInfo s : walletList) { + removedWallets.remove(s.name); + } + for (String name : removedWallets) { + KeyStoreHelper.removeWalletUserPass(getActivity(), name); + } } private void showInfo(@NonNull String name) { - activityCallback.onWalletDetails(name, isTestnet()); + activityCallback.onWalletDetails(name); } private void showReceive(@NonNull String name) { - activityCallback.onWalletReceive(name, isTestnet()); + activityCallback.onWalletReceive(name); } @Override @@ -337,29 +351,31 @@ public void onCreate(@Nullable Bundle savedInstanceState) { @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.list_menu, menu); - menu.findItem(R.id.action_testnet).setChecked(isTestnet()); + menu.findItem(R.id.action_testnet).setChecked(testnetCheckMenu); super.onCreateOptionsMenu(menu, inflater); } - private boolean testnet = BuildConfig.DEBUG; + private boolean testnetCheckMenu = BuildConfig.DEBUG; - boolean isTestnet() { - return testnet; - } + //boolean isTestnet() { + // return testnet; + //} public boolean onTestnetMenuItem() { - boolean lastState = testnet; + boolean lastState = testnetCheckMenu; setNet(!lastState, true); // set and save return !lastState; } - public void setNet(boolean testnet, boolean save) { - this.testnet = testnet; - activityCallback.showNet(testnet); + public void setNet(boolean testnetChecked, boolean save) { + this.testnetCheckMenu = testnetChecked; + NetworkType net = testnetChecked ? NetworkType.NetworkType_Testnet : NetworkType.NetworkType_Mainnet; + activityCallback.setNetworkType(net); + activityCallback.showNet(); if (save) { savePrefs(true); // use previous state as we just clicked it } - if (testnet) { + if (testnetChecked) { setDaemon(daemonTestNet); } else { setDaemon(daemonMainNet); @@ -369,7 +385,6 @@ public void setNet(boolean testnet, boolean save) { private static final String PREF_DAEMON_TESTNET = "daemon_testnet"; private static final String PREF_DAEMON_MAINNET = "daemon_mainnet"; - private static final String PREF_SHOW_XMRTO_ENABLED = "info_xmrto_enabled_login"; private static final String PREF_DAEMONLIST_MAINNET = "node.moneroworld.com:18089;node.xmrbackb.one;node.xmr.be"; @@ -380,23 +395,12 @@ public void setNet(boolean testnet, boolean save) { private NodeList daemonTestNet; private NodeList daemonMainNet; - boolean showXmrtoEnabled = true; - void loadPrefs() { SharedPreferences sharedPref = activityCallback.getPrefs(); daemonMainNet = new NodeList(sharedPref.getString(PREF_DAEMON_MAINNET, PREF_DAEMONLIST_MAINNET)); daemonTestNet = new NodeList(sharedPref.getString(PREF_DAEMON_TESTNET, PREF_DAEMONLIST_TESTNET)); - setNet(isTestnet(), false); - - showXmrtoEnabled = sharedPref.getBoolean(PREF_SHOW_XMRTO_ENABLED, true); - } - - void saveXmrToPrefs() { - SharedPreferences sharedPref = activityCallback.getPrefs(); - SharedPreferences.Editor editor = sharedPref.edit(); - editor.putBoolean(PREF_SHOW_XMRTO_ENABLED, showXmrtoEnabled); - editor.apply(); + setNet(testnetCheckMenu, false); } void savePrefs() { @@ -406,7 +410,7 @@ void savePrefs() { void savePrefs(boolean usePreviousTestnetState) { Timber.d("SAVE / %s", usePreviousTestnetState); // save the daemon address for the net - boolean testnet = isTestnet() ^ usePreviousTestnetState; + boolean testnet = testnetCheckMenu ^ usePreviousTestnetState; String daemon = getDaemon(); if (testnet) { daemonTestNet.setRecent(daemon); @@ -418,7 +422,6 @@ void savePrefs(boolean usePreviousTestnetState) { SharedPreferences.Editor editor = sharedPref.edit(); editor.putString(PREF_DAEMON_MAINNET, daemonMainNet.toString()); editor.putString(PREF_DAEMON_TESTNET, daemonTestNet.toString()); - editor.putBoolean(PREF_SHOW_XMRTO_ENABLED, showXmrtoEnabled); editor.apply(); } @@ -492,19 +495,19 @@ public void onClick(View v) { case R.id.fabNew: fabScreen.setVisibility(View.INVISIBLE); isFabOpen = false; - activityCallback.onAddWallet(isTestnet(), GenerateFragment.TYPE_NEW); + activityCallback.onAddWallet(GenerateFragment.TYPE_NEW); break; case R.id.fabView: animateFAB(); - activityCallback.onAddWallet(isTestnet(), GenerateFragment.TYPE_VIEWONLY); + activityCallback.onAddWallet(GenerateFragment.TYPE_VIEWONLY); break; case R.id.fabKey: animateFAB(); - activityCallback.onAddWallet(isTestnet(), GenerateFragment.TYPE_KEY); + activityCallback.onAddWallet(GenerateFragment.TYPE_KEY); break; case R.id.fabSeed: animateFAB(); - activityCallback.onAddWallet(isTestnet(), GenerateFragment.TYPE_SEED); + activityCallback.onAddWallet(GenerateFragment.TYPE_SEED); break; case R.id.fabScreen: animateFAB(); diff --git a/app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java b/app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java index e7b8dae633..2934baae8f 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java @@ -300,7 +300,7 @@ private void generateQr() { String paymentId = etPaymentId.getEditText().getText().toString(); String xmrAmount = evAmount.getAmount(); Timber.d("%s/%s/%s", xmrAmount, paymentId, address); - if ((xmrAmount == null) || !Wallet.isAddressValid(address, WalletManager.getInstance().isTestNet())) { + if ((xmrAmount == null) || !Wallet.isAddressValid(address)) { clearQR(); Timber.d("CLEARQR"); return; @@ -335,6 +335,7 @@ private void generateQr() { } public Bitmap generate(String text, int width, int height) { + if ((width <= 0) || (height <= 0)) return null; Map hints = new HashMap<>(); hints.put(EncodeHintType.CHARACTER_SET, "utf-8"); hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M); diff --git a/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java b/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java index 91ff7bd55f..abfd24f598 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java +++ b/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java @@ -36,7 +36,7 @@ import com.m2049r.xmrwallet.data.BarcodeData; import com.m2049r.xmrwallet.data.TxData; -import com.m2049r.xmrwallet.dialog.DonationFragment; +import com.m2049r.xmrwallet.dialog.CreditsFragment; import com.m2049r.xmrwallet.dialog.HelpFragment; import com.m2049r.xmrwallet.fragment.send.SendAddressWizardFragment; import com.m2049r.xmrwallet.fragment.send.SendFragment; @@ -49,8 +49,6 @@ import com.m2049r.xmrwallet.util.UserNotes; import com.m2049r.xmrwallet.widget.Toolbar; -import java.io.File; - import timber.log.Timber; public class WalletActivity extends SecureActivity implements WalletFragment.Listener, @@ -62,8 +60,10 @@ public class WalletActivity extends SecureActivity implements WalletFragment.Lis public static final String REQUEST_ID = "id"; public static final String REQUEST_PW = "pw"; + public static final String REQUEST_FINGERPRINT_USED = "fingerprint"; private Toolbar toolbar; + private boolean needVerifyIdentity; @Override public void setToolbarButton(int type) { @@ -120,6 +120,7 @@ private void startWalletService() { acquireWakeLock(); String walletId = extras.getString(REQUEST_ID); String walletPassword = extras.getString(REQUEST_PW); + needVerifyIdentity = extras.getBoolean(REQUEST_FINGERPRINT_USED); connectWalletService(walletId, walletPassword); } else { finish(); @@ -159,8 +160,8 @@ public boolean onOptionsItemSelected(MenuItem item) { case R.id.action_info: onWalletDetails(); return true; - case R.id.action_donate: - DonationFragment.display(getSupportFragmentManager()); + case R.id.action_credits: + CreditsFragment.display(getSupportFragmentManager()); return true; case R.id.action_share: onShareTxInfo(); @@ -174,6 +175,9 @@ public boolean onOptionsItemSelected(MenuItem item) { case R.id.action_details_help: HelpFragment.display(getSupportFragmentManager(), R.string.help_details); return true; + case R.id.action_details_changepw: + onWalletChangePassword(); + return true; case R.id.action_help_send: HelpFragment.display(getSupportFragmentManager(), R.string.help_send); return true; @@ -182,6 +186,20 @@ public boolean onOptionsItemSelected(MenuItem item) { } } + public void onWalletChangePassword() {//final String walletName, final String walletPassword) { + try { + GenerateReviewFragment detailsFragment = (GenerateReviewFragment) + getSupportFragmentManager().findFragmentById(R.id.fragment_container); + AlertDialog dialog = detailsFragment.createChangePasswordDialog(); + if (dialog != null) { + Helper.showKeyboard(dialog); + dialog.show(); + } + } catch (ClassCastException ex) { + Timber.w("onWalletChangePassword() called, but no GenerateReviewFragment active"); + } + } + @Override protected void onCreate(Bundle savedInstanceState) { Timber.d("onCreate()"); @@ -213,8 +231,8 @@ public void onButton(int type) { case Toolbar.BUTTON_CLOSE: finish(); break; - case Toolbar.BUTTON_DONATE: - Toast.makeText(WalletActivity.this, getString(R.string.label_donate), Toast.LENGTH_SHORT).show(); + case Toolbar.BUTTON_CREDITS: + Toast.makeText(WalletActivity.this, getString(R.string.label_credits), Toast.LENGTH_SHORT).show(); case Toolbar.BUTTON_NONE: default: Timber.e("Button " + type + "pressed - how can this be?"); @@ -222,12 +240,7 @@ public void onButton(int type) { } }); - boolean testnet = WalletManager.getInstance().isTestNet(); - if (testnet) { - toolbar.setBackgroundResource(R.color.colorPrimaryDark); - } else { - toolbar.setBackgroundResource(R.drawable.backgound_toolbar_mainnet); - } + showNet(); Fragment walletFragment = new WalletFragment(); getSupportFragmentManager().beginTransaction() @@ -238,6 +251,23 @@ public void onButton(int type) { Timber.d("onCreate() done."); } + public void showNet() { + switch (WalletManager.getInstance().getNetworkType()) { + case NetworkType_Mainnet: + toolbar.setBackgroundResource(R.drawable.backgound_toolbar_mainnet); + break; + case NetworkType_Testnet: + toolbar.setBackgroundResource(R.color.colorPrimaryDark); + break; + case NetworkType_Stagenet: + toolbar.setBackgroundResource(R.color.colorPrimaryDark); + break; + default: + throw new IllegalStateException("Unsupported Network: " + WalletManager.getInstance().getNetworkType()); + } + } + + public Wallet getWallet() { if (mBoundService == null) throw new IllegalStateException("WalletService not bound."); return mBoundService.getWallet(); @@ -368,7 +398,17 @@ public long getDaemonHeight() { @Override public void onSendRequest() { - replaceFragment(new SendFragment(), null, null); + if (needVerifyIdentity) { + Helper.promptPassword(WalletActivity.this, getWallet().getName(), true, new Helper.PasswordAction() { + @Override + public void action(String walletName, String password, boolean fingerprintUsed) { + replaceFragment(new SendFragment(), null, null); + needVerifyIdentity = false; + } + }); + } else { + replaceFragment(new SendFragment(), null, null); + } } @Override @@ -668,9 +708,22 @@ private void onWalletDetails() { public void onClick(DialogInterface dialog, int which) { switch (which) { case DialogInterface.BUTTON_POSITIVE: - Bundle extras = new Bundle(); + final Bundle extras = new Bundle(); extras.putString("type", GenerateReviewFragment.VIEW_TYPE_WALLET); - replaceFragment(new GenerateReviewFragment(), null, extras); + extras.putString("password", getIntent().getExtras().getString(REQUEST_PW)); + + if (needVerifyIdentity) { + Helper.promptPassword(WalletActivity.this, getWallet().getName(), true, new Helper.PasswordAction() { + @Override + public void action(String walletName, String password, boolean fingerprintUsed) { + replaceFragment(new GenerateReviewFragment(), null, extras); + needVerifyIdentity = false; + } + }); + } else { + replaceFragment(new GenerateReviewFragment(), null, extras); + } + break; case DialogInterface.BUTTON_NEGATIVE: // do nothing @@ -798,9 +851,8 @@ public long getTotalFunds() { @Override public boolean verifyWalletPassword(String password) { - String walletPath = new File(Helper.getStorageRoot(this), - getWalletName() + ".keys").getAbsolutePath(); - return WalletManager.getInstance().verifyWalletPassword(walletPath, password, true); + String walletPassword = Helper.getWalletPassword(getApplicationContext(), getWalletName(), password); + return walletPassword != null; } @Override diff --git a/app/src/main/java/com/m2049r/xmrwallet/WalletFragment.java b/app/src/main/java/com/m2049r/xmrwallet/WalletFragment.java index 4038e7fe53..905bc06e74 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/WalletFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/WalletFragment.java @@ -352,7 +352,7 @@ private void updateStatus(Wallet wallet) { setProgress(x); ivSynced.setVisibility(View.GONE); } else { - sync = getString(R.string.status_synced) + formatter.format(wallet.getBlockChainHeight()); + sync = getString(R.string.status_synced) + " " + formatter.format(wallet.getBlockChainHeight()); ivSynced.setVisibility(View.VISIBLE); } } else { diff --git a/app/src/main/java/com/m2049r/xmrwallet/XmrWalletApplication.java b/app/src/main/java/com/m2049r/xmrwallet/XmrWalletApplication.java index 520a817984..f6fecc59be 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/XmrWalletApplication.java +++ b/app/src/main/java/com/m2049r/xmrwallet/XmrWalletApplication.java @@ -19,13 +19,15 @@ import android.app.Application; +import com.m2049r.xmrwallet.util.Helper; + import timber.log.Timber; public class XmrWalletApplication extends Application { @Override public void onCreate() { super.onCreate(); - + if (BuildConfig.DEBUG) { Timber.plant(new Timber.DebugTree()); } diff --git a/app/src/main/java/com/m2049r/xmrwallet/data/BarcodeData.java b/app/src/main/java/com/m2049r/xmrwallet/data/BarcodeData.java index 3ff9e048f2..1471c8951a 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/data/BarcodeData.java +++ b/app/src/main/java/com/m2049r/xmrwallet/data/BarcodeData.java @@ -18,6 +18,7 @@ import android.net.Uri; +import com.m2049r.xmrwallet.model.NetworkType; import com.m2049r.xmrwallet.model.Wallet; import com.m2049r.xmrwallet.model.WalletManager; import com.m2049r.xmrwallet.util.BitcoinAddressValidator; @@ -178,7 +179,7 @@ static public BarcodeData parseBitcoinUri(String uri) { return null; // we have an amount but its not a number! } } - if (!BitcoinAddressValidator.validate(address, WalletManager.getInstance().isTestNet())) { + if (!BitcoinAddressValidator.validate(address)) { Timber.d("address invalid"); return null; } @@ -190,7 +191,7 @@ static public BarcodeData parseBitcoinNaked(String address) { if (address == null) return null; - if (!BitcoinAddressValidator.validate(address, WalletManager.getInstance().isTestNet())) { + if (!BitcoinAddressValidator.validate(address)) { Timber.d("address invalid"); return null; } diff --git a/app/src/main/java/com/m2049r/xmrwallet/data/WalletNode.java b/app/src/main/java/com/m2049r/xmrwallet/data/WalletNode.java new file mode 100644 index 0000000000..42705ab10b --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/data/WalletNode.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2018 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.data; + +import com.m2049r.xmrwallet.model.NetworkType; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; + +public class WalletNode { + private final String name; + private final String host; + private final int port; + private final String user; + private final String password; + private final NetworkType networkType; + + public WalletNode(String walletName, String daemon, NetworkType networkType) { + if ((daemon == null) || daemon.isEmpty()) + throw new IllegalArgumentException("daemon is empty"); + this.name = walletName; + String daemonAddress; + String a[] = daemon.split("@"); + if (a.length == 1) { // no credentials + daemonAddress = a[0]; + user = ""; + password = ""; + } else if (a.length == 2) { // credentials + String userPassword[] = a[0].split(":"); + if (userPassword.length != 2) + throw new IllegalArgumentException("User:Password invalid"); + user = userPassword[0]; + if (!user.isEmpty()) { + password = userPassword[1]; + } else { + password = ""; + } + daemonAddress = a[1]; + } else { + throw new IllegalArgumentException("Too many @"); + } + + String da[] = daemonAddress.split(":"); + if ((da.length > 2) || (da.length < 1)) + throw new IllegalArgumentException("Too many ':' or too few"); + host = da[0]; + if (da.length == 2) { + try { + port = Integer.parseInt(da[1]); + } catch (NumberFormatException ex) { + throw new IllegalArgumentException("Port not numeric"); + } + } else { + switch (networkType) { + case NetworkType_Mainnet: + port = 18081; + break; + case NetworkType_Testnet: + port = 28081; + break; + case NetworkType_Stagenet: + port = 38081; + break; + default: + port = 0; + } + } + this.networkType = networkType; + } + + public String getName() { + return name; + } + + public NetworkType getNetworkType() { + return networkType; + } + + public String getAddress() { + return host + ":" + port; + } + + public String getUsername() { + return user; + } + + public String getPassword() { + return password; + } + + public SocketAddress getSocketAddress() { + return new InetSocketAddress(host, port); + } + + public boolean isValid() { + return !host.isEmpty(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/dialog/DonationFragment.java b/app/src/main/java/com/m2049r/xmrwallet/dialog/CreditsFragment.java similarity index 68% rename from app/src/main/java/com/m2049r/xmrwallet/dialog/DonationFragment.java rename to app/src/main/java/com/m2049r/xmrwallet/dialog/CreditsFragment.java index f50c681f32..89b443bb3c 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/dialog/DonationFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/dialog/CreditsFragment.java @@ -28,16 +28,14 @@ import android.view.LayoutInflater; import android.view.View; import android.widget.TextView; -import android.widget.Toast; import com.m2049r.xmrwallet.R; -import com.m2049r.xmrwallet.util.Helper; -public class DonationFragment extends DialogFragment { +public class CreditsFragment extends DialogFragment { static final String TAG = "DonationFragment"; - public static DonationFragment newInstance() { - return new DonationFragment(); + public static CreditsFragment newInstance() { + return new CreditsFragment(); } public static void display(FragmentManager fm) { @@ -47,24 +45,14 @@ public static void display(FragmentManager fm) { ft.remove(prev); } - DonationFragment.newInstance().show(ft, TAG); + CreditsFragment.newInstance().show(ft, TAG); } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { - final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_donation, null); + final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_credits, null); - ((TextView) view.findViewById(R.id.tvCredits)).setText(Html.fromHtml(getString(R.string.donation_credits))); - - (view.findViewById(R.id.bCopyAddress)). - setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Helper.clipBoardCopy(getActivity(), getString(R.string.label_copy_address), - ((TextView) view.findViewById(R.id.tvWalletAddress)).getText().toString()); - Toast.makeText(getActivity(), getString(R.string.message_copy_address), Toast.LENGTH_SHORT).show(); - } - }); + ((TextView) view.findViewById(R.id.tvCredits)).setText(Html.fromHtml(getString(R.string.credits_text))); AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setView(view); diff --git a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAddressWizardFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAddressWizardFragment.java index 77ad8cd2ee..984eb11d11 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAddressWizardFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAddressWizardFragment.java @@ -212,7 +212,7 @@ public void onClick(View v) { private boolean checkAddressNoError() { String address = etAddress.getEditText().getText().toString(); return Wallet.isAddressValid(address) - || BitcoinAddressValidator.validate(address, WalletManager.getInstance().isTestNet()); + || BitcoinAddressValidator.validate(address); } private boolean checkAddress() { @@ -228,13 +228,13 @@ private boolean checkAddress() { private boolean isIntegratedAddress() { String address = etAddress.getEditText().getText().toString(); return (address.length() == INTEGRATED_ADDRESS_LENGTH) - && Wallet.isAddressValid(address, WalletManager.getInstance().isTestNet()); + && Wallet.isAddressValid(address); } private boolean isBitcoinAddress() { String address = etAddress.getEditText().getText().toString(); if ((address.length() >= 27) && (address.length() <= 34)) - return BitcoinAddressValidator.validate(address, WalletManager.getInstance().isTestNet()); + return BitcoinAddressValidator.validate(address); else return false; } diff --git a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendFragment.java index c5e61e24b6..05dcdc8f5b 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendFragment.java @@ -47,6 +47,7 @@ import com.m2049r.xmrwallet.model.PendingTransaction; import com.m2049r.xmrwallet.util.Helper; import com.m2049r.xmrwallet.util.NodeList; +import com.m2049r.xmrwallet.util.Notice; import com.m2049r.xmrwallet.util.UserNotes; import com.m2049r.xmrwallet.widget.DotBar; import com.m2049r.xmrwallet.widget.Toolbar; @@ -119,27 +120,8 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, arrowPrev = getResources().getDrawable(R.drawable.ic_navigate_prev_white_24dp); arrowNext = getResources().getDrawable(R.drawable.ic_navigate_next_white_24dp); - llXmrToEnabled = view.findViewById(R.id.llXmrToEnabled); - llXmrToEnabled.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - HelpFragment.display(getChildFragmentManager(), R.string.help_xmrto); - - } - }); - ibXmrToInfoClose = view.findViewById(R.id.ibXmrToInfoClose); - ibXmrToInfoClose.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - llXmrToEnabled.setVisibility(View.GONE); - showXmrtoEnabled = false; - saveXmrToPrefs(); - } - }); - loadPrefs(); - if (!showXmrtoEnabled) { - llXmrToEnabled.setVisibility(View.GONE); - } + ViewGroup llNotice = (ViewGroup) view.findViewById(R.id.llNotice); + Notice.showAll(llNotice,".*_send"); spendViewPager = (SpendViewPager) view.findViewById(R.id.pager); pagerAdapter = new SpendPagerAdapter(getChildFragmentManager()); @@ -291,7 +273,7 @@ public void run() { pagerAdapter.notifyDataSetChanged(); } }); - Timber.d("New Mode = " + mode.toString()); + Timber.d("New Mode = %s", mode.toString()); } } @@ -350,7 +332,7 @@ public SendWizardFragment getFragment(int position) { @Override public SendWizardFragment getItem(int position) { Timber.d("getItem(%d) CREATE", position); - Timber.d("Mode=" + mode.toString()); + Timber.d("Mode=%s", mode.toString()); if (mode == Mode.XMR) { switch (position) { case POS_ADDRESS: diff --git a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendSettingsWizardFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendSettingsWizardFragment.java index 42da4013f5..2fc78c0b94 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendSettingsWizardFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendSettingsWizardFragment.java @@ -54,7 +54,8 @@ interface Listener { TxData getTxData(); } - final static int Mixins[] = {4, 7, 12, 25}; // must match the layout XML + // Mixin = Ringsize - 1 + final static int Mixins[] = {6, 9, 12, 25}; // must match the layout XML / "@array/mixin" final static PendingTransaction.Priority Priorities[] = {PendingTransaction.Priority.Priority_Default, PendingTransaction.Priority.Priority_Low, diff --git a/app/src/main/java/com/m2049r/xmrwallet/layout/TransactionInfoAdapter.java b/app/src/main/java/com/m2049r/xmrwallet/layout/TransactionInfoAdapter.java index c0f0bdf936..c739c082ec 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/layout/TransactionInfoAdapter.java +++ b/app/src/main/java/com/m2049r/xmrwallet/layout/TransactionInfoAdapter.java @@ -137,12 +137,7 @@ void bind(int position) { ivTxType.setVisibility(View.GONE); // gives us more space for the amount } - long realAmount = infoItem.amount; - if (infoItem.isPending) { - realAmount = realAmount - infoItem.fee; - } - - String displayAmount = Helper.getDisplayAmount(realAmount, Helper.DISPLAY_DIGITS_INFO); + 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 { diff --git a/app/src/main/java/com/m2049r/xmrwallet/model/NetworkType.java b/app/src/main/java/com/m2049r/xmrwallet/model/NetworkType.java new file mode 100644 index 0000000000..ae1c84fc5e --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/model/NetworkType.java @@ -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; + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/model/PendingTransaction.java b/app/src/main/java/com/m2049r/xmrwallet/model/PendingTransaction.java index 6168d3f93a..6ad620a461 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/model/PendingTransaction.java +++ b/app/src/main/java/com/m2049r/xmrwallet/model/PendingTransaction.java @@ -84,7 +84,14 @@ public Status getStatus() { public native long getFee(); - public native String getFirstTxId(); + public String getFirstTxId() { + String id = getFirstTxIdJ(); + if (id == null) + throw new IndexOutOfBoundsException(); + return id; + } + + public native String getFirstTxIdJ(); public native long getTxCount(); diff --git a/app/src/main/java/com/m2049r/xmrwallet/model/Wallet.java b/app/src/main/java/com/m2049r/xmrwallet/model/Wallet.java index dfe8cb166c..7c205561f3 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/model/Wallet.java +++ b/app/src/main/java/com/m2049r/xmrwallet/model/Wallet.java @@ -79,7 +79,11 @@ public String getAddress() { public native String getPath(); - public native boolean isTestNet(); + 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; @@ -155,10 +159,10 @@ public ConnectionStatus getConnectionStatus() { public static native boolean isPaymentIdValid(String payment_id); public static boolean isAddressValid(String address) { - return isAddressValid(address, WalletManager.getInstance().isTestNet()); + return isAddressValid(address, WalletManager.getInstance().getNetworkType().getValue()); } - public static native boolean isAddressValid(String address, boolean isTestNet); + public static native boolean isAddressValid(String address, int networkType); //TODO static static bool keyValid(const std::string &secret_key_string, const std::string &address_string, bool isViewKey, bool testnet, std::string &error); @@ -271,5 +275,4 @@ public void setListener(WalletListener listener) { //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 &unknown_parameters, std::string &error) = 0; //virtual bool rescanSpent() = 0; - } diff --git a/app/src/main/java/com/m2049r/xmrwallet/model/WalletManager.java b/app/src/main/java/com/m2049r/xmrwallet/model/WalletManager.java index e098a273c7..5bd6a68e26 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/model/WalletManager.java +++ b/app/src/main/java/com/m2049r/xmrwallet/model/WalletManager.java @@ -16,6 +16,8 @@ package com.m2049r.xmrwallet.model; +import com.m2049r.xmrwallet.data.WalletNode; + import java.io.BufferedReader; import java.io.File; import java.io.FileReader; @@ -39,6 +41,7 @@ public static synchronized WalletManager getInstance() { if (WalletManager.Instance == null) { WalletManager.Instance = new WalletManager(); } + return WalletManager.Instance; } @@ -69,22 +72,22 @@ private void unmanageWallet(Wallet wallet) { } public Wallet createWallet(File aFile, String password, String language) { - long walletHandle = createWalletJ(aFile.getAbsolutePath(), password, language, isTestNet()); + long walletHandle = createWalletJ(aFile.getAbsolutePath(), password, language, getNetworkType().getValue()); Wallet wallet = new Wallet(walletHandle); manageWallet(wallet); return wallet; } - private native long createWalletJ(String path, String password, String language, boolean isTestNet); + private native long createWalletJ(String path, String password, String language, int networkType); public Wallet openWallet(String path, String password) { - long walletHandle = openWalletJ(path, password, isTestNet()); + long walletHandle = openWalletJ(path, password, getNetworkType().getValue()); Wallet wallet = new Wallet(walletHandle); manageWallet(wallet); return wallet; } - private native long openWalletJ(String path, String password, boolean isTestNet); + private native long openWalletJ(String path, String password, int networkType); public Wallet recoveryWallet(File aFile, String password, String mnemonic) { return recoveryWallet(aFile, password, mnemonic, 0); @@ -92,28 +95,28 @@ public Wallet recoveryWallet(File aFile, String password, String mnemonic) { public Wallet recoveryWallet(File aFile, String password, String mnemonic, long restoreHeight) { long walletHandle = recoveryWalletJ(aFile.getAbsolutePath(), password, mnemonic, - isTestNet(), restoreHeight); + getNetworkType().getValue(), restoreHeight); Wallet wallet = new Wallet(walletHandle); manageWallet(wallet); return wallet; } private native long recoveryWalletJ(String path, String password, String mnemonic, - boolean isTestNet, long restoreHeight); + int networkType, long restoreHeight); public Wallet createWalletWithKeys(File aFile, String password, String language, long restoreHeight, String addressString, String viewKeyString, String spendKeyString) { - long walletHandle = createWalletWithKeysJ(aFile.getAbsolutePath(), password, - language, isTestNet(), restoreHeight, + 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 createWalletWithKeysJ(String path, String password, + private native long createWalletFromKeysJ(String path, String password, String language, - boolean isTestNet, + int networkType, long restoreHeight, String addressString, String viewKeyString, @@ -204,24 +207,23 @@ public boolean accept(File dir, String filename) { //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 boolean testnet = true; + private NetworkType networkType = null; - public boolean isTestNet() { - if (daemonAddress == null) { - return true; - // assume testnet not explicitly initialised - //throw new IllegalStateException("use setDaemon() to initialise daemon and net first!"); - } - return testnet; + public NetworkType getNetworkType() { + return networkType; + } + + //public void setDaemon(String address, NetworkType networkType, String username, String password) { + public void setDaemon(WalletNode walletNode) { + this.daemonAddress = walletNode.getAddress(); + this.networkType = walletNode.getNetworkType(); + this.daemonUsername = walletNode.getUsername(); + this.daemonPassword = walletNode.getPassword(); + setDaemonAddressJ(daemonAddress); } - public void setDaemon(String address, boolean testnet, String username, String password) { - //Timber.d("SETDAEMON " + username + "/" + password + "/" + address); - this.daemonAddress = address; - this.testnet = testnet; - this.daemonUsername = username; - this.daemonPassword = password; - setDaemonAddressJ(address); + public void setNetworkType(NetworkType networkType) { + this.networkType = networkType; } public String getDaemonAddress() { @@ -268,5 +270,23 @@ public String getDaemonPassword() { //TODO static std::tuple 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); } \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/nfc/AuthenticationException.java b/app/src/main/java/com/m2049r/xmrwallet/nfc/AuthenticationException.java new file mode 100644 index 0000000000..a003a65eba --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/nfc/AuthenticationException.java @@ -0,0 +1,19 @@ +package com.m2049r.xmrwallet.nfc; + +public class AuthenticationException extends Exception{ + + /** + * + */ + private static final long serialVersionUID = 101001L; + + public AuthenticationException(String info) + { + super(info); + } + + public AuthenticationException(String info,Exception e) + { + super(info,e); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/nfc/TagUtil.java b/app/src/main/java/com/m2049r/xmrwallet/nfc/TagUtil.java new file mode 100644 index 0000000000..42778effc7 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/nfc/TagUtil.java @@ -0,0 +1,1598 @@ +package com.m2049r.xmrwallet.nfc; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Date; + +import jonelo.jacksum.JacksumAPI; +import jonelo.jacksum.algorithm.AbstractChecksum; +import jonelo.jacksum.ui.ExitStatus; +import jonelo.sugar.util.ExitException; +import android.content.Intent; +import android.nfc.NfcAdapter; +import android.nfc.Tag; +import android.nfc.tech.MifareClassic; +import android.nfc.tech.MifareUltralight; +import android.nfc.tech.NfcA; +import android.util.Log; + + +/** + * + * @author jianjunchu + * + */ +public class TagUtil { + + private static final int TAGUTIL_TYPE_ULTRALIGHT = 1; + private static final int TAGUTIL_TYPE_CLASSIC = 2; + private static final int TAGUTIL_NfcA = 3; + private static final byte PAGE_ADDR_AUTH0 = 42; + private static final byte PAGE_ADDR_AUTH1 = 43; + + // private static android.nfc.Tag tag; + + private static String uid; + private static String finalPage; + + private int tagType; + + private byte[] secretKey; + private byte[] ivDefault = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };// default iv + private byte[] random = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 };// radmon number + public byte[] getRandom() { + return random; + } + + public void setRandom(byte[] random) { + this.random = random; + } + + private boolean authorised = true; + private String ERR_MSG; + + private static NfcA nfcA=null; + + public TagUtil(String u,int type) + { + uid=u; + tagType = type; + } + + /** + * get the TagUtil obj, throw exception for not supported nfc tag. + * @param intent + * @param isCheckSUM: add a checksum flag at the end of the command (for some cellphone use MTK chips, we should set this param true). + * How to know mtk chip: by getprop() method + * if ro.mediatek.gemini_support=true + * then + * it is mtk + * @return + * @throws Exception: will throw this exception for unsupported nfc tag + */ + public static TagUtil selectTag(Intent intent,boolean isCheckSUM) throws Exception + { + String action = intent.getAction(); + int type=0; + if (isSupportedAction(action)) { + // get TAG in the intent + Tag tagFromIntent = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); + String[] tagTypes = tagFromIntent.getTechList();// 支持的类型集合 + String tagType = null; + for(int i=0;i0) + continue; + if (tagTypes != null && tagTypes.length > 0) { + tagType = tagFromIntent.getTechList()[i]; + } + if ("android.nfc.tech.NfcA".equals(tagType)) { + try{ + getTagUID_NfcA(tagFromIntent,isCheckSUM); + type=TAGUTIL_NfcA; + + }catch(Exception ex) + { + throw ex; + } + } + else + { + throw new Exception("unsupported action "+action +" only support ACTION_TECH_DISCOVERED or ACTION_TAG_DISCOVERED or ACTION_NDEF_DISCOVERED"); + } + } + tagFromIntent = null; + TagUtil tagUtil = new TagUtil(uid,type); + return tagUtil; +// if(checkTag(type)) +// { +// TagUtil tagUtil = new TagUtil(uid,type); +// return tagUtil; +// } +// else +// throw new Exception ("illegal tag"); + } + return null; + } + + /** + * read one page( four bytes in a page), + * @param intent + * @param addr: page address + * @param isCheckSum: + * @return 4 bytes array + * @throws AuthenticationException + * @throws Exception + */ + public byte[] readOnePage(Intent intent,byte addr,boolean isCheckSum) throws AuthenticationException,Exception + { + if(tagType==TagUtil.TAGUTIL_NfcA) + return readOnePage_NfcA( intent, addr,isCheckSum); + else + return null; + } + + + private byte[] readOnePage_NfcA(Intent intent,byte addr, boolean isCheckSum) throws AuthenticationException,Exception + { + String action = intent.getAction(); + byte[] result = null; + if (isSupportedAction(action)) { + //Tag tagFromIntent = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); + //NfcA mfc = NfcA.get(tagFromIntent); + try { + if(authorised){ + //mfc.connect(); + //accreditation(mfc,secretKey);//认证 + byte[] data0 = new byte[2]; + byte[] dataWithCheckSum = new byte[4]; + data0[0] = 0x30; + data0[1] = addr; + byte[] data1; + if(isCheckSum) + { + byte[] checkSum = getCheckSum(data0); + dataWithCheckSum[0]=data0[0]; + dataWithCheckSum[1]=data0[1]; + dataWithCheckSum[2]=checkSum[0]; + dataWithCheckSum[3]=checkSum[1]; + data1 = nfcA.transceive(dataWithCheckSum);// read 4 pages one time + } + else + data1 = nfcA.transceive(data0);// read 4 pages one time + + result = new byte[4]; + if(data1.length<16) + throw new AuthenticationException("please authenticate first!"); + else + System.arraycopy(data1, 0, result, 0, 4);// get the first page + }else{ + throw new AuthenticationException("Authenticate First!"); + } + } catch (Exception e) { + throw e; + } +// finally { +// try { +// mfc.close(); +// } catch (IOException e) { +// e.printStackTrace(); +// } +// } + } + return result; + } + + /** + * read four pages( 4 bytes in a page) + * @param intent + * @param addr: + * @param isCheckSum: + * @return 16 bytes array + * @throws AuthenticationException + * @throws Exception + */ + public byte[] readFourPage(Intent intent,byte addr, boolean isCheckSum) throws AuthenticationException,Exception + { + if(tagType==TagUtil.TAGUTIL_NfcA) + return readFourPage_NfcA( intent, addr,isCheckSum); + else + return null; + } + + + private byte[] readFourPage_NfcA(Intent intent,byte addr, boolean isCheckSum) throws AuthenticationException,Exception + { + String action = intent.getAction(); + byte[] result = null; + if (isSupportedAction(action)) { + //Tag tagFromIntent = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); + //NfcA mfc = NfcA.get(tagFromIntent); + try { + if(authorised){ + //mfc.connect(); + //accreditation(mfc,secretKey); + byte[] data0 = new byte[2]; + byte[] dataWithCheckSum = new byte[4]; + data0[0] = 0x30; + data0[1] = addr; + byte[] data1; + if(isCheckSum) + { + byte[] checkSum = getCheckSum(data0); + dataWithCheckSum[0]=data0[0]; + dataWithCheckSum[1]=data0[1]; + dataWithCheckSum[2]=checkSum[0]; + dataWithCheckSum[3]=checkSum[1]; + result = nfcA.transceive(dataWithCheckSum);// 每次读出来的数据为4page的数据 + } + else + result = nfcA.transceive(data0);// 每次读出来的数据为4page的数据 + }else{ + throw new AuthenticationException("please authenticate first!"); + } + } catch (Exception e) { + throw e; + } +// finally { +// try { +// mfc.close(); +// } catch (IOException e) { +// e.printStackTrace(); +// } +// } + } + return result; + } + + /** + * write a page(4 bytes in a page) + * @param intent + * @param addr + * @param contents 4 bytes array + * @param isCheckSum: + * @return true for success, false for failed + * @throws AuthenticationException + * @throws Exception + */ + public boolean writeTag(Intent intent, byte addr, byte[] contents, boolean isCheckSum) throws AuthenticationException, Exception + { + if(tagType==TagUtil.TAGUTIL_NfcA) + return writeAble( intent, addr,contents, isCheckSum); + else + return false; + } + + private boolean writeAble(Intent intent, byte addr, byte contents[], boolean isCheckSum) + throws AuthenticationException, Exception + { + boolean res = false; + if((new Integer(addr)).intValue() < 4) + throw new AuthenticationException("page no should bigger than 4"); + try + { + byte newByteArray[] = appendByteArray(contents); + int pageNum = newByteArray.length / 4; + byte array[] = new byte[4]; + for(int i = 0; i < pageNum; i++) + { + array[0] = newByteArray[0 + 4 * i]; + array[1] = newByteArray[1 + 4 * i]; + array[2] = newByteArray[2 + 4 * i]; + array[3] = newByteArray[3 + 4 * i]; + try + { + writeTag_NfcA(intent, (byte)(addr + i), array, false); + } + catch(Exception e) + { + throw new Exception((new StringBuilder()).append("write page ").append(addr + i).append(" failed").toString()); + } + } + + res = true; + } + catch(Exception e) + { + e.printStackTrace(); + Log.e("xxx", e.getMessage()); + } + return res; + } + + private boolean writeTag_NfcA(Intent intent, byte addr, byte[] contents, boolean isCheckSum) throws AuthenticationException, Exception + { + boolean result = false; + String action = intent.getAction(); + if (isSupportedAction(action)) { +// Tag tagFromIntent = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); +// NfcA mfc = NfcA.get(tagFromIntent); + + try { + if(authorised){ + if(contents != null && contents.length== 4){//判断输入的数据 + //mfc.connect(); + //accreditation(mfc,secretKey);//认证 + byte[] data2 = new byte[6]; + byte[] dataWithCheckSum= new byte[8]; + data2[0] = (byte) 0xA2; + data2[1] = addr; + data2[2] = contents[0]; + data2[3] = contents[1]; + data2[4] = contents[2]; + data2[5] = contents[3]; + byte[] data3; + if(isCheckSum) + { + byte[] checkSum = getCheckSum(data2); + dataWithCheckSum[0]=data2[0]; + dataWithCheckSum[1]=data2[1]; + dataWithCheckSum[2]=data2[2]; + dataWithCheckSum[3]=data2[3]; + dataWithCheckSum[4]=data2[4]; + dataWithCheckSum[5]=data2[5]; + dataWithCheckSum[6]=checkSum[0]; + dataWithCheckSum[7]=checkSum[1]; + data3 = nfcA.transceive(dataWithCheckSum);// 每次读出来的数据为4page的数据 + } + else + data3 = nfcA.transceive(data2); + result=true; + }else{ + throw new AuthenticationException("contents must be four bytes"); + } + } + else + { + throw new AuthenticationException("please authenticate first!"); + } + } catch (Exception e) { + throw e; + } +// finally { +// try { +// mfc.close(); +// } catch (IOException e) { +// e.printStackTrace(); +// } +// } + } + return result; + } + + private static boolean isSupportedAction(String action) { + return NfcAdapter.ACTION_TECH_DISCOVERED.equals(action) || NfcAdapter.ACTION_TAG_DISCOVERED.equals(action) || NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action); + } + + + /** + * 认证 + * @param intent + * @param key 秘钥 16 个字符(字母和数字)。 + * @param isCheckSum: 是否增加校验位 + * @return + * @throws AuthenticationException + */ + public boolean authentication(Intent intent, String key, boolean isCheckSum) throws AuthenticationException, Exception + { + String dexString = this.bytesToHexString(key.getBytes()); + return authentication_internal(intent,dexString,isCheckSum); + } + + /** + * 认证 + * @param intent + * @param key 秘钥 16 个字符(字母和数字)。 + * @param isCheckSum: 是否增加校验位 + * @return + * @throws AuthenticationException + */ + public boolean authentication(Intent intent, byte[] key, boolean isCheckSum) throws AuthenticationException, Exception + { + String dexString = this.bytesToHexString(key); + return authentication_internal(intent,dexString,isCheckSum); + } + /** + * 认证 + * @param intent + * @param key 秘钥 16 个字节, 用 32 个 16 进制字符的字符串表示。 + * @param isCheckSum: 是否增加校验位 + * @return + * @throws AuthenticationException + */ + public boolean authentication_internal(Intent intent, String key, boolean isCheckSum) throws AuthenticationException, Exception + { + if(tagType==TagUtil.TAGUTIL_NfcA) + return authentication_NfcA( intent, key,isCheckSum); + else + throw new Exception("unknow tag Type"+ tagType+". or SelectTag first."); + } + + private boolean authentication_NfcA(Intent intent, String key, boolean isCheckSum) throws AuthenticationException + { + boolean result = false; + String action = intent.getAction(); + if (isSupportedAction(action)) { +// Tag tagFromIntent = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); +// NfcA mfc = NfcA.get(tagFromIntent); + try { + //Log.e("aaa",key); + if(key != null && key.length() == 32){//判断输入的数据 + byte[] data = new byte[24]; + byte[] binaryKey = hexStringToBytes(key); + System.arraycopy(binaryKey, 0, data, 0, 16); + System.arraycopy(binaryKey, 0, data, 16, 8); + //mfc.connect(); + accreditation(nfcA,data,isCheckSum);//认证 + authorised=true; + secretKey = data; + return true; + }else{ + ERR_MSG = "key must be 32 hex chars,current key is "+key; + return false; + } + } catch (Exception e) { + e.printStackTrace(); + return false; + } + finally + { +// try { +// mfc.close(); +// } catch (IOException e) { +// e.printStackTrace(); +// } + } + }else + { + ERR_MSG=action+ " is not support"+ ", action must be on of ACTION_TECH_DISCOVERED or ACTION_TAG_DISCOVERED"; + return false; + } + } + + /** + * 从第 0 页开始,读取指定页数的数据,返回一个字节数组(1 页 4 个字节) + * @param intent: + * @param pageNums: 指定页数 + * @param isCheckSum: 是否增加校验位 + * @return + * @throws Exception + */ + public byte[] readAllPages(Intent intent,int pageNums,boolean isCheckSum) throws Exception{ + + if(tagType==TagUtil.TAGUTIL_NfcA) + return readAllPages_NfcA( intent, pageNums,isCheckSum); + + else + return null; + } + + + private byte[] readAllPages_NfcA(Intent intent, int pageNums,boolean isCheckSum) throws Exception{ +// Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); +// NfcA mfc = NfcA.get(tag); + int byteNum =pageNums*4; // 4 bytes a page + byte[] result= new byte[byteNum]; + try { + if(authorised){ + //mfc.connect(); + for (int i = 0x04; i < byteNum/4; i++) { + byte[] data0 = new byte[2]; + byte[] dataWithCheckSum = new byte[4]; + data0[0] = 0x30; + data0[1] = (byte) i; + + byte[] data1; + if(isCheckSum) + { + byte[] checkSum = getCheckSum(data0); + dataWithCheckSum[0]=data0[0]; + dataWithCheckSum[1]=data0[1]; + dataWithCheckSum[2]=checkSum[0]; + dataWithCheckSum[3]=checkSum[1]; + data1 = nfcA.transceive(dataWithCheckSum);// 每次读出来的数据为4page的数据 + } + else + data1 = nfcA.transceive(data0);// 4 pages + if(data1.length>=4) + System.arraycopy(data1, 0, result, 4*i, 4);// get one page + else + throw new Exception("read the" +i +"th page failed! "+data1.length +"bytes was read"); + } + return result; + }else{ + throw new AuthenticationException("please authenticate first!"); + } + } catch (Exception e) { + throw e; + } +// finally { +// try { +// mfc.close(); +// } catch (IOException e) { +// e.printStackTrace(); +// } +// } + } + + /** + * 改变秘钥 + * @param intent + * @param newKey 新的秘钥 + * @param isCheckSum: 是否增加校验位 + * @return + * @throws AuthenticationException + * @throws Exception + */ + public boolean writeNewKey(Intent intent,String newKey,boolean isCheckSum) throws AuthenticationException,Exception + { + if(newKey != null && newKey.length() == 32){ + if(tagType==TagUtil.TAGUTIL_NfcA) + return writeNewKey_NfcA( intent,newKey,isCheckSum); + else + return false; + }else + throw new Exception("key must be 32 hex chars"); + } + + + // 写入密钥 + private boolean writeNewKey_NfcA(Intent intent,String newKey, boolean isCheckSum) throws Exception{ + boolean result = false; + String action = intent.getAction(); + if (isSupportedAction(action)) { +// Tag tagFromIntent = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); +// NfcA mfc = NfcA.get(tagFromIntent); + try { + if(authorised){ + String dataString = newKey; + //判断输入的数据 + + byte[] dataX = hexStringToBytes(dataString); + byte[] dataY = new byte[16]; + for(int i=0;i<16;i++){ + dataY[i] = dataX[15-i]; + System.out.println("mi"+dataY[i]); + } + byte[] data1 = new byte[6]; + byte[] data1WithCheckSum= new byte[8]; + data1[0] = (byte) 0xA2; + data1[1] = (byte) 0x2C; + System.arraycopy(dataY, 8, data1, 2, 4); + + byte[] data2 = new byte[6]; + byte[] data2WithCheckSum= new byte[8]; + data2[0] = (byte) 0xA2; + data2[1] = (byte) 0x2D; + System.arraycopy(dataY, 12, data2, 2, 4); + + byte[] data3 = new byte[6]; + byte[] data3WithCheckSum= new byte[8]; + data3[0] = (byte) 0xA2; + data3[1] = (byte) 0x2E; + System.arraycopy(dataY, 0, data3, 2, 4); + + byte[] data4 = new byte[6]; + byte[] data4WithCheckSum= new byte[8]; + data4[0] = (byte) 0xA2; + data4[1] = (byte) 0x2F; + System.arraycopy(dataY, 4, data4, 2, 4); + +// mfc.connect(); +// accreditation(mfc,secretKeyDefault);//认证 + + + if(isCheckSum) + { + byte[] checkSum = getCheckSum(data1); + for(int i=0;i<6;i++) + data1WithCheckSum[i]=data1[i]; + data1WithCheckSum[6]=checkSum[0]; + data1WithCheckSum[7]=checkSum[1]; + nfcA.transceive(data1WithCheckSum);// 每次读出来的数据为4page的数据 + } + else + nfcA.transceive(data1); + + if(isCheckSum) + { + byte[] checkSum = getCheckSum(data2); + for(int i=0;i<6;i++) + data2WithCheckSum[i]=data2[i]; + data2WithCheckSum[6]=checkSum[0]; + data2WithCheckSum[7]=checkSum[1]; + nfcA.transceive(data2WithCheckSum);// 每次读出来的数据为4page的数据 + } + else + nfcA.transceive(data2); + + if(isCheckSum) + { + byte[] checkSum = getCheckSum(data3); + for(int i=0;i<6;i++) + data3WithCheckSum[i]=data3[i]; + data3WithCheckSum[6]=checkSum[0]; + data3WithCheckSum[7]=checkSum[1]; + nfcA.transceive(data3WithCheckSum);// 每次读出来的数据为4page的数据 + } + else + nfcA.transceive(data3); + + if(isCheckSum) + { + byte[] checkSum = getCheckSum(data4); + for(int i=0;i<6;i++) + data4WithCheckSum[i]=data4[i]; + data4WithCheckSum[6]=checkSum[0]; + data4WithCheckSum[7]=checkSum[1]; + nfcA.transceive(data4WithCheckSum);// 每次读出来的数据为4page的数据 + } + else + nfcA.transceive(data4); + result = true; + + }else{ + throw new AuthenticationException("please authenticate first!"); + } + }catch (NumberFormatException e) { + throw new Exception("new key: "+newKey+" is not correct." +" key must be 32 hex chars"); + } catch (Exception e) { + throw e; + } +// finally { +// try { +// mfc.close(); +// } catch (IOException e) { +// e.printStackTrace(); +// } +// } + } + return result; + } + +// /** +// * 设置芯片的新密码(适用于FJ8216 和 NXP216 芯片 ),在调用本方法前,要先通过 authentication216 方法进行认证。 +// * @param intent +// * @param newPWD 四个字节的 byte 数组,新的密码 +// * @param PACK 两个字节的 byte 数组,用于设置验证成功后的返回值。 +// * @param isCheckSum +// * @return +// * @throws AuthenticationException +// * @throws Exception +// */ +// public boolean writePWD216(Intent intent,byte[] newPWD,byte[] PACK, boolean isCheckSum) throws AuthenticationException,Exception +// { +// if(newPWD != null && newPWD.length == 4 && PACK.length==2){ +// if(tagType==TagUtil.TAGUTIL_NfcA) +// return writeNewKey216_NfcA( intent,newPWD,PACK,isCheckSum); +// else if (tagType==TagUtil.TAGUTIL_TYPE_ULTRALIGHT) +// return writeNewKey216_MifareUltraLight( intent,newPWD,PACK,isCheckSum); +// else if (tagType==TagUtil.TAGUTIL_TYPE_CLASSIC) +// return writeNewKey216_MifareClassic( intent,newPWD,PACK,isCheckSum); +// else +// return false; +// }else +// throw new Exception("new PWD must be 4 bytes and PACK must be 2 bytes"); +// } +// +// private boolean writeNewKey216_MifareUltraLight(Intent intent, byte[] newPWD,byte[] PACK,boolean isCheckSum) throws Exception{ +// byte[] oldE6 = readOnePage(intent, (byte)0XE6, isCheckSum); +// byte[] newE6 = new byte[4]; +// newE6[0]=PACK[0]; +// newE6[1]=PACK[1]; +// newE6[2]=oldE6[2]; +// newE6[3]=oldE6[3]; +// boolean result1 = writeTag(intent, (byte)0XE5, newPWD, isCheckSum); +// boolean result2 = writeTag(intent, (byte)0XE6, newE6, isCheckSum); +// return result1 && result2 ; +// } +// +// private boolean writeNewKey216_MifareClassic(Intent intent, byte[] newPWD,byte[] PACK,boolean isCheckSum) throws Exception{ +// throw new Exception("unimplemented"); +// } +// +// private boolean writeNewKey216_NfcA(Intent intent, byte[] newPWD,byte[] PACK, +// boolean isCheckSum) throws Exception { +// byte[] oldE6 = readOnePage(intent, (byte)0XE6, isCheckSum); +// byte[] newE6 = new byte[4]; +// newE6[0]=PACK[0]; +// newE6[1]=PACK[1]; +// newE6[2]=oldE6[2]; +// newE6[3]=oldE6[3]; +// boolean result1 = writeTag(intent, (byte)0XE5, newPWD, isCheckSum); +// boolean result2 = writeTag(intent, (byte)0XE6, newE6, isCheckSum); +// return result1 && result2 ; +// } + + /** + * 设置一个开始页面地址和访问方式, 该页面地址后的页面都需要认证后才可访问。访问方式有两种,0为读写访问,1 为写访问 + * @param intent + * @param addr: 访问地址, + * @param access: 如果设置为 0, 读写操作都需要授权. 如果设置为1 ,只有写操作需要授权。 + * @param isCheckSum: 是否增加校验位 + * @return + * @throws Exception + */ + public boolean setAccess(Intent intent,byte addr, int access,boolean isCheckSum) throws Exception + { + if(tagType==TagUtil.TAGUTIL_NfcA) + return setAccess_NfcA( intent,addr, access,isCheckSum); + else + throw new Exception("unknow tag Type"+ tagType+". or SelectTag first."); + } + + + private boolean setAccess_NfcA(Intent intent, byte addr, int access, boolean isCheckSum ) throws Exception { + if(addr< (byte)3 || addr > 0x30) + throw new Exception("address must between 03h and 30h"); + else + { + int value = (int)addr<<24; + access = access << 24; + boolean result1 = writeTag(intent, (byte)0X2A, this.getBytesArray(value), isCheckSum); + boolean result2 = writeTag(intent, (byte)0X2B, this.getBytesArray(access), isCheckSum); + if(result1 && result2) + return true; + else + return false; + } + } + + /** + * 锁定加锁位 + * @param intent + * @param isCheckSum 是否在命令后自动增加校验位。 + * @return + * @throws Exception + */ + public boolean lockLockingbits(Intent intent, boolean isCheckSum) throws Exception + { + if(tagType==TagUtil.TAGUTIL_NfcA) + return lockLockingbits_NfcA( intent,isCheckSum); + else + throw new Exception("unknow tag Type"+ tagType+". or SelectTag first."); + } + private boolean lockLockingbits_NfcA(Intent intent,boolean isCheckSum) throws Exception { + byte[] contents1= new byte[4]; + contents1[0]=(byte)0; + contents1[1]=(byte)0; + contents1[2]=(byte)7; + contents1[3]=(byte)0; + + byte[] contents2= new byte[4]; + contents2[0]=(byte)17; + contents2[1]=(byte)15; + contents2[2]=(byte)0; + contents2[3]=(byte)0; + + if (writeTag(intent, (byte)2, contents1,isCheckSum) && writeTag(intent, (byte)40, contents2,isCheckSum)) + return true; + else + return false; + } + + /** + * 锁定一个页面范围 + * @param intent + * @param addr1 要锁定的开始页面 + * @param addr2 要锁定的结束页面 + * @param isCheckSum 是否在命令后自动增加校验位。 + * @return + * @throws Exception + */ + public boolean lockPage(Intent intent,byte addr1 ,byte addr2, boolean isCheckSum) throws Exception + { + if(tagType==TagUtil.TAGUTIL_NfcA) + return lockPage_NfcA( intent,addr1, addr2, isCheckSum); + else + throw new Exception("unknow tag Type"+ tagType+". or SelectTag first."); + } + + private boolean lockPage_NfcA(Intent intent, byte startAddr1, byte endAddr, boolean isCheckSum) throws Exception { + boolean result = false; + + if(startAddr1>endAddr) + { + throw new Exception ("endAdddr must greater than or equal to startAddr"); + } + + if(startAddr1<3 || endAddr >47) + { + throw new Exception ("startAddr and endAdddr must between [3,47]"); + } + + if(endAddr>15) + { + if(lockPage_NfcA_Part1(intent,startAddr1,(byte)15, isCheckSum) && lockPage_NfcA_Part2(intent,(byte)16,endAddr, isCheckSum)) + result = true; + } + else + { + if(lockPage_NfcA_Part1(intent,startAddr1,(byte)15, isCheckSum)) + result=true; + } + return result; + } + + /** + * 锁定为在第 40 页, 可以锁定的范围是 16 到 47 页面。 + * lock in page 40, lock address between 16 and 47 + * @param intent + * @param startAddr + * @param endAddr + * @param isCheckSum 是否在命令后自动增加校验位。 + */ + private boolean lockPage_NfcA_Part2(Intent intent, byte startAddr, byte endAddr, boolean isCheckSum) throws Exception{ + byte[] contents = new byte[4]; + int value=0; + int totalValue=0; + for(int j=startAddr;j<=endAddr;j++) + { + if(j<=39) + { + if(j%4>0) + continue; + int i=(j)/4; + switch (i) + { + case 4: + value=2^25; + break; + case 5: + value=2^26; + break; + case 6: + value=2^27; + break; + case 7: + value=2^29; + break; + case 8: + value=2^30; + break; + case 9: + value=2^31; + break; + default: + break; + } + }else + { + switch (j) + { + case 41: + value=2^20; + break; + case 42: + value=2^21; + break; + case 43: + value=2^22; + break; + case 44: + value=2^23; + break; + case 40: + case 45: + case 46: + case 47: + default: + + } + } + totalValue+=value; + } + contents = getBytesArray(totalValue); + return writeTag(intent, (byte)40, contents, isCheckSum); + } + + private byte[] getBytesArray(int value) { + byte[] contents = new byte[4]; + contents[0] = (byte)(value >>> 24); + contents[1] = (byte)(value >>> 16); + contents[2] = (byte)(value >>> 8); + contents[3] = (byte)(value ); + return contents; + } + + /** + * 锁定位在第 2 页面, 可以锁定的地址范围是 3 到 15 + * lock in page 2, lock address between 3 and 15 + * @param startAddr + * @param endAddr + * @param isCheckSum 是否在命令后自动增加校验位。 + */ + private boolean lockPage_NfcA_Part1(Intent intent, byte startAddr, byte endAddr, boolean isCheckSum) throws Exception{ + byte[] contents = new byte[4]; + int value=0; + int totalValue=0; + for(int i=startAddr;i<=endAddr;i++) + { + switch (i) + { + case 3: + value=2^8; + break; + case 4: + value=2^12; + break; + case 5: + value=2^13; + break; + case 6: + value=2^14; + break; + case 7: + value=2^15; + break; + case 8: + value=2^0; + break; + case 9: + value=2^1; + break; + case 10: + value=2^2; + break; + case 11: + value=2^3; + break; + case 12: + value=2^4; + break; + case 13: + value=2^5; + break; + case 14: + value=2^6; + break; + case 15: + value=2^7; + break; + default: + } + totalValue+=value; + } + contents[0] = (byte)(totalValue >>> 24); + contents[1] = (byte)(totalValue >>> 16); + contents[2] = (byte)(totalValue >>> 8); + contents[3] = (byte)(totalValue ); + return writeTag(intent, (byte)2, contents, isCheckSum); + } + + /** + * 锁定所有页面 + * @param intent + * @param isCheckSum 是否在命令后自动增加校验位。 + * @return + * @throws Exception + */ + public boolean lockPageAll(Intent intent, boolean isCheckSum) throws Exception + { + boolean result = false; + switch(tagType) + { + case TagUtil.TAGUTIL_NfcA: + result= lockPageAll_NfcA(intent, isCheckSum); + break; + case TagUtil.TAGUTIL_TYPE_ULTRALIGHT: + result= lockPageAll_MifareUltraLight(intent); + break; + case TagUtil.TAGUTIL_TYPE_CLASSIC: + result= lockPageAll_MifareClassic(intent); + break; + default: + throw new Exception("unknow tag Type"+ tagType+". or SelectTag first."); + }return result; + } + + + private boolean lockPageAll_NfcA(Intent intent, boolean isCheckSum) throws Exception + { + byte[] contents1= new byte[4]; + contents1[0]=(byte)0; + contents1[1]=(byte)0; + contents1[2]=(byte)255; + contents1[3]=(byte)255; + + byte[] contents2= new byte[4]; + contents2[0]=(byte)255; + contents2[1]=(byte)255; + contents2[2]=(byte)0; + contents2[3]=(byte)0; + + if (writeTag(intent, (byte)2, contents1, isCheckSum) && writeTag(intent, (byte)40, contents2, isCheckSum)) + return true; + else + return false; + } + + private boolean lockPageAll_MifareUltraLight(Intent intent) throws Exception + { + throw new Exception("unimplemented"); + } + + private boolean lockPageAll_MifareClassic(Intent intent) throws Exception + { + throw new Exception("unimplemented"); + } + + /** + * 获取标签类型,目前可支持的标签类型包括 NFCA 和 UltraLight + * @return + * @throws AuthenticationException + */ + public int getTagType() throws AuthenticationException + { + return tagType; + } + + private void accreditation(NfcA mfc,byte[] secretKeys,boolean isCheckSum) throws Exception { + byte[] iv = ivDefault; + + byte[] command0 = new byte[2];// 发送认证指令的参数 + byte[] command0WithCheckSum = new byte[4];// 发送认证指令的参数(with check sum) + + byte[] command1 = null;// 发送认证后,卡片返回的密文1 + byte[] command1WithCheckSum = null;// 发送认证后,卡片返回的密文1 + + byte[] command2 = null;// 密文1去掉数组中的第1个数据,取出有效数组 + + byte[] command3 = null;// 密文1 解密后的数据 + byte[] command4 = null;// command2 加密 + byte[] command5 = null;// command3 循环左移得到的数据 + byte[] command6 = null;// 使用command5 和 command4 第二次加密后的数据RNDB + byte[] command7 = null;// + byte[] command8 = null;// + byte[] command9 = null;// + byte[] command10 = null;// + byte[] command11 = null;// + + command0[0] = (byte) 0x1A; // 命令位 + command0[1] = (byte) 0x00; // 标志位 + if(isCheckSum) + { + byte[] checkSum = getCheckSum(command0); + command0WithCheckSum[0]=command0[0]; + command0WithCheckSum[1]=command0[1]; + command0WithCheckSum[2]=checkSum[0]; + command0WithCheckSum[3]=checkSum[1]; + command1WithCheckSum = mfc.transceive(command0WithCheckSum);// 11 bytes + if(command1WithCheckSum.length != 11) + { + String str=""; + for (int i = 0 ; i> 4)); + sb.append(hexString.charAt((bytes[i] & 15) >> 0)); + } + + return sb.toString(); + } + + public static String hexStringToString(String bytes) + { + String hexString = "0123456789ABCDEF"; + ByteArrayOutputStream baos = new ByteArrayOutputStream(bytes.length() / 2); + for(int i = 0; i < bytes.length(); i += 2) + baos.write(hexString.indexOf(bytes.charAt(i)) << 4 | hexString.indexOf(bytes.charAt(i + 1))); + + return new String(baos.toByteArray()); + } + + public static byte[] StringtoBytes(String str) + { + String hexstr = StringtoHexString(str); + byte byte1[] = hexStringToBytes(hexstr); + return byte1; + } + + private byte[] appendByteArray(byte byteArray[]) + { + int length = byteArray.length; + int m = length % 4; + byte newByteArray[]; + if(m == 0) + newByteArray = new byte[length]; + else + newByteArray = new byte[length + (4 - m)]; + System.arraycopy(byteArray, 0, newByteArray, 0, length); + return newByteArray; + } + + /** + * 字节数组转化为16进制字符串 + */ + public static String bytesToHexString(byte[] src) { + StringBuilder stringBuilder = new StringBuilder(); + if (src == null || src.length <= 0) { + return null; + } + for (int i = 0; i < src.length; i++) { + int v = src[i] & 0xFF; + String hv = Integer.toHexString(v); + if (hv.length() < 2) { + stringBuilder.append(0); + } + stringBuilder.append(hv); + } + return stringBuilder.toString(); + } + + private static void getTagUID_NfcA(Tag tag,boolean isCheckSum) throws Exception + { +// byte[] datau = tag.getId(); +// uid=bytesToHexString(datau); + + nfcA = NfcA.get(tag); + try { + String metaInfo = ""; + nfcA.connect(); + byte[] datas = new byte[2]; + byte[] datasWithCheckSum = new byte[4]; + datas[0] = 0x30; + datas[1] = 0x00; + byte[] datar; + if(isCheckSum) + { + byte[] checkSum = getCheckSum(datas); + datasWithCheckSum[0]=datas[0]; + datasWithCheckSum[1]=datas[1]; + datasWithCheckSum[2]=checkSum[0]; + datasWithCheckSum[3]=checkSum[1]; + datar = nfcA.transceive(datasWithCheckSum);// 每次读出来的数据为4page的数据 + } + else + datar = nfcA.transceive(datas);// 每次读出来的数据为4page的数据 + byte[] datau = new byte[7];//uid号 + System.arraycopy(datar, 0, datau, 0, 3);// 去4page中的第1page数据 + System.arraycopy(datar, 4, datau, 3, 4);// 去4page中的第1page数据 + uid=bytesToHexString(datau); + } + catch(Exception e) + { + e.printStackTrace(); + throw e; + } +// finally +// { +// try { +// mfc.close(); +// } catch (IOException e) { +// e.printStackTrace(); +// } +// } + } + + private static void getFinalPage_NfcA(Tag tag,boolean isCheckSum) throws Exception + { + try { + //mfc.connect(); + byte[] datas = new byte[2]; + byte[] datasWithChcekSum = new byte[4]; + + datas[0] = 0x30; + datas[1] = (byte)0xFF; + byte[] checkSum; + + if(isCheckSum) + { + checkSum = getCheckSum(datas); + datasWithChcekSum[0]=datas[0]; + datasWithChcekSum[1]=datas[1]; + datasWithChcekSum[2]=checkSum[0]; + datasWithChcekSum[3]=checkSum[1]; + } + + if(nfcA==null) + { + nfcA = NfcA.get(tag); + nfcA.connect(); + } + + byte[] datar; + + if(isCheckSum) + datar = nfcA.transceive(datasWithChcekSum); + else + datar = nfcA.transceive(datas); + + byte[] datau = new byte[4];//uid号 + System.arraycopy(datar, 0, datau, 0, 3);// 4page中的第1page数据 + finalPage=bytesToHexString(datau); + } + catch(Exception e) + { + throw e; + } +// finally +// { +// try { +// mfc.close(); +// } catch (IOException e) { +// e.printStackTrace(); +// } +// } + } + + public static String getUid() { + return uid; + } + + + /** + * 访问时需要认证的页面地址范围 + * @param intent + * @return 返回一个开始地址。访问该地址后的所有页面,都需要认证。 如果返回值大于等于 48 ,则 访问(读或写)所有页面都不需要认证。如果返回值小于48,说明访问(读或写)返回值地址到 48 之间的页面时,需要认证。 + * @throws Exception + */ + public int getAuthenticationAddr(Intent intent,boolean isCheckSum) throws Exception + { + byte[] result = readOnePage(intent, (byte)PAGE_ADDR_AUTH0,isCheckSum); + int r = result[0]; + return r; + } + + /** + * 如果访问时,有地址需要认证。该方法可以获得认证的种类 + * @param intent + * @param isCheckSum 是否在命令后自动增加校验位。 + * @return 如果返回值=0 ,则读写都需要认证。如果返回值=1,只有写需要认证 + * @throws Exception + */ + public int getAuthenticationType(Intent intent,boolean isCheckSum) throws Exception + { + byte[] result = readOnePage(intent, (byte)PAGE_ADDR_AUTH1,isCheckSum); + int r = result[0]; + return r; + } + + /** + * 关闭连接 + */ + public void close() + { + try { + if(nfcA!=null) + nfcA.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private static byte[] getCheckSum(byte[] byteAyyay) throws Exception { + AbstractChecksum checksum=null; + try { + String checksumArg="crc:16,1021,c6c6,true,true,0"; + checksum = JacksumAPI.getChecksumInstance(checksumArg,false); + } catch (NoSuchAlgorithmException nsae) { + throw new ExitException(nsae.getMessage()+"\nUse -a to specify a valid one.\nFor help and a list of all supported algorithms use -h.\nExit.", ExitStatus.PARAMETER); + } + checksum.setEncoding(AbstractChecksum.HEX); + //byte[] byteAyyay = hexStringToBytes(string); + checksum.update(byteAyyay); + String hexValue = checksum.getHexValue(); + //String resultStr =checksum.toString();//d97c 02a8 + byte[] result = reverse(hexStringToBytes(hexValue)); + return result; + } + + /** + * 使用命令 + * java -jar aofei_nfc.jar + * 获取版本号 + * @param args + */ + public static void main(String[] args) + { + System.out.println("2.2.0"); + } + + private static byte[] reverse(byte[] bytes) + { + byte[] result= new byte[bytes.length]; + + for(int i=0;i 35) return false; diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/CrazyPassEncoder.java b/app/src/main/java/com/m2049r/xmrwallet/util/CrazyPassEncoder.java new file mode 100644 index 0000000000..cd884d717a --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/CrazyPassEncoder.java @@ -0,0 +1,54 @@ +package com.m2049r.xmrwallet.util; + +import com.m2049r.xmrwallet.model.WalletManager; + +import java.math.BigInteger; + +public class CrazyPassEncoder { + static final String BASE = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"; + static final int PW_CHARS = 52; + + // this takes a 32 byte buffer and converts it to 52 alphnumeric characters + // separated by blanks every 4 characters = 13 groups of 4 + // always (padding by Xs if need be + static public String encode(byte[] data) { + if (data.length != 32) throw new IllegalArgumentException("data[] is not 32 bytes long"); + BigInteger rest = new BigInteger(1, data); + BigInteger remainder; + final StringBuilder result = new StringBuilder(); + final BigInteger base = BigInteger.valueOf(BASE.length()); + int i = 0; + do { + if ((i > 0) && (i % 4 == 0)) result.append(' '); + i++; + remainder = rest.remainder(base); + rest = rest.divide(base); + result.append(BASE.charAt(remainder.intValue())); + } while (!BigInteger.ZERO.equals(rest)); + // pad it + while (i < PW_CHARS) { + if ((i > 0) && (i % 4 == 0)) result.append(' '); + result.append('2'); + i++; + } + return result.toString(); + } + + static public String reformat(String password) { + // maybe this is a CrAzYpass without blanks? or lowercase letters + String noBlanks = password.toUpperCase().replaceAll(" ", ""); + if (noBlanks.length() == PW_CHARS) { // looks like a CrAzYpass + // insert blanks every 4 characters + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < PW_CHARS; i++) { + if ((i > 0) && (i % 4 == 0)) sb.append(' '); + char c = noBlanks.charAt(i); + if (BASE.indexOf(c) < 0) return null; // invalid character found + sb.append(c); + } + return sb.toString(); + } else { + return null; // not a CrAzYpass + } + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/FingerprintHelper.java b/app/src/main/java/com/m2049r/xmrwallet/util/FingerprintHelper.java new file mode 100644 index 0000000000..46de0364ec --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/FingerprintHelper.java @@ -0,0 +1,40 @@ +package com.m2049r.xmrwallet.util; + +import android.app.KeyguardManager; +import android.content.Context; +import android.support.v4.hardware.fingerprint.FingerprintManagerCompat; +import android.support.v4.os.CancellationSignal; + +import java.security.KeyStore; +import java.security.KeyStoreException; + +public class FingerprintHelper { + + public static boolean isDeviceSupported(Context context) { + FingerprintManagerCompat fingerprintManager = FingerprintManagerCompat.from(context); + KeyguardManager keyguardManager = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); + + return keyguardManager != null && + keyguardManager.isKeyguardSecure() && + fingerprintManager.isHardwareDetected() && + fingerprintManager.hasEnrolledFingerprints(); + } + + public static boolean isFingerprintAuthAllowed(String wallet) throws KeyStoreException { + KeyStore keyStore = KeyStore.getInstance(KeyStoreHelper.SecurityConstants.KEYSTORE_PROVIDER_ANDROID_KEYSTORE); + try { + keyStore.load(null); + } catch (Exception ex) { + throw new IllegalStateException("Could not load KeyStore", ex); + } + + return keyStore.containsAlias(KeyStoreHelper.SecurityConstants.WALLET_PASS_KEY_PREFIX + wallet); + } + + public static void authenticate(Context context, CancellationSignal cancelSignal, + FingerprintManagerCompat.AuthenticationCallback callback) { + FingerprintManagerCompat manager = FingerprintManagerCompat.from(context); + manager.authenticate(null, 0, cancelSignal, callback, null); + } + +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/Helper.java b/app/src/main/java/com/m2049r/xmrwallet/util/Helper.java index b771b6c104..510134d664 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/util/Helper.java +++ b/app/src/main/java/com/m2049r/xmrwallet/util/Helper.java @@ -18,10 +18,12 @@ import android.Manifest; import android.app.Activity; +import android.app.AlertDialog; import android.app.Dialog; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; +import android.content.DialogInterface; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -30,22 +32,39 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.VectorDrawable; import android.os.Environment; +import android.support.design.widget.TextInputLayout; import android.support.v4.content.ContextCompat; +import android.support.v4.hardware.fingerprint.FingerprintManagerCompat; +import android.support.v4.os.CancellationSignal; +import android.system.ErrnoException; +import android.system.Os; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; import android.view.WindowManager; import android.view.animation.Animation; import android.view.animation.AnimationUtils; +import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.TextView; +import com.m2049r.xmrwallet.BuildConfig; import com.m2049r.xmrwallet.R; +import com.m2049r.xmrwallet.model.NetworkType; import com.m2049r.xmrwallet.model.Wallet; import com.m2049r.xmrwallet.model.WalletManager; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; +import java.math.BigInteger; import java.net.MalformedURLException; import java.net.SocketTimeoutException; import java.net.URL; +import java.security.KeyStoreException; import java.util.Locale; import javax.net.ssl.HttpsURLConnection; @@ -54,17 +73,22 @@ import timber.log.Timber; public class Helper { - static private final String WALLET_DIR = "monerujo"; + static private final String WALLET_DIR = "monerujo" + (BuildConfig.DEBUG ? "-debug" : ""); + static private final String HOME_DIR = "monero" + (BuildConfig.DEBUG ? "-debug" : ""); static public int DISPLAY_DIGITS_INFO = 5; - static public File getStorageRoot(Context context) { + static public File getWalletRoot(Context context) { + return getStorage(context, WALLET_DIR); + } + + static public File getStorage(Context context, String folderName) { if (!isExternalStorageWritable()) { String msg = context.getString(R.string.message_strorage_not_writable); Timber.e(msg); throw new IllegalStateException(msg); } - File dir = new File(Environment.getExternalStorageDirectory(), WALLET_DIR); + File dir = new File(Environment.getExternalStorageDirectory(), folderName); if (!dir.exists()) { Timber.i("Creating %s", dir.getAbsolutePath()); dir.mkdirs(); // try to make it @@ -114,9 +138,9 @@ static public boolean getCameraPermission(Activity context) { } static public File getWalletFile(Context context, String aWalletName) { - File walletDir = getStorageRoot(context); + File walletDir = getWalletRoot(context); File f = new File(walletDir, aWalletName); - Timber.d("wallet= %s size= %d", f.getAbsolutePath(), f.length()); + Timber.d("wallet=%s size= %d", f.getAbsolutePath(), f.length()); return f; } @@ -263,10 +287,217 @@ static public Animation getShakeAnimation(Context context) { } static public HttpUrl getXmrToBaseUrl() { - if ((WalletManager.getInstance() == null) || WalletManager.getInstance().isTestNet()) { + if ((WalletManager.getInstance() == null) + || (WalletManager.getInstance().getNetworkType() != NetworkType.NetworkType_Mainnet)) { return HttpUrl.parse("https://test.xmr.to/api/v2/xmr2btc/"); } else { return HttpUrl.parse("https://xmr.to/api/v2/xmr2btc/"); } } + + private final static char[] HexArray = "0123456789ABCDEF".toCharArray(); + + public static 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 void setMoneroHome(Context context) { + try { + String home = getStorage(context, HOME_DIR).getAbsolutePath(); + Os.setenv("HOME", home, true); + } catch (ErrnoException ex) { + throw new IllegalStateException(ex); + } + } + + static public void initLogger(Context context) { + if (BuildConfig.DEBUG) { + initLogger(context, WalletManager.LOGLEVEL_DEBUG); + } + // no logger if not debug + } + + // TODO make the log levels refer to the WalletManagerFactory::LogLevel enum ? + static public void initLogger(Context context, int level) { + String home = getStorage(context, HOME_DIR).getAbsolutePath(); + WalletManager.initLogger(home + "/monerujo", "monerujo.log"); + if (level >= WalletManager.LOGLEVEL_SILENT) + WalletManager.setLogLevel(level); + } + + // try to figure out what the real wallet password is given the user password + // which could be the actual wallet password or a (maybe malformed) CrAzYpass + // or the password used to derive the CrAzYpass for the wallet + static public String getWalletPassword(Context context, String walletName, String password) { + String walletPath = new File(getWalletRoot(context), walletName + ".keys").getAbsolutePath(); + + // try with entered password (which could be a legacy password or a CrAzYpass) + if (WalletManager.getInstance().verifyWalletPassword(walletPath, password, true)) { + return password; + } + + // maybe this is a malformed CrAzYpass? + String possibleCrazyPass = CrazyPassEncoder.reformat(password); + if (possibleCrazyPass != null) { // looks like a CrAzYpass + if (WalletManager.getInstance().verifyWalletPassword(walletPath, possibleCrazyPass, true)) { + return possibleCrazyPass; + } + } + + // generate & try with CrAzYpass + String crazyPass = KeyStoreHelper.getCrazyPass(context, password); + if (WalletManager.getInstance().verifyWalletPassword(walletPath, crazyPass, true)) { + return crazyPass; + } + + return null; + } + + static AlertDialog openDialog = null; // for preventing opening of multiple dialogs + + static public void promptPassword(final Context context, final String wallet, boolean fingerprintDisabled, final PasswordAction action) { + if (openDialog != null) return; // we are already asking for password + LayoutInflater li = LayoutInflater.from(context); + final View promptsView = li.inflate(R.layout.prompt_password, null); + + AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(context); + alertDialogBuilder.setView(promptsView); + + final TextInputLayout etPassword = (TextInputLayout) promptsView.findViewById(R.id.etPassword); + etPassword.setHint(context.getString(R.string.prompt_password, wallet)); + + boolean fingerprintAuthCheck; + try { + fingerprintAuthCheck = FingerprintHelper.isFingerprintAuthAllowed(wallet); + } catch (KeyStoreException ex) { + fingerprintAuthCheck = false; + } + + final boolean fingerprintAuthAllowed = !fingerprintDisabled && fingerprintAuthCheck; + final CancellationSignal cancelSignal = new CancellationSignal(); + + if (fingerprintAuthAllowed) { + promptsView.findViewById(R.id.txtFingerprintAuth).setVisibility(View.VISIBLE); + } + + etPassword.getEditText().addTextChangedListener(new TextWatcher() { + + @Override + public void afterTextChanged(Editable s) { + if (etPassword.getError() != null) { + etPassword.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(context.getString(R.string.label_ok), null) + .setNegativeButton(context.getString(R.string.label_cancel), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + Helper.hideKeyboardAlways((Activity) context); + cancelSignal.cancel(); + dialog.cancel(); + openDialog = null; + } + }); + openDialog = alertDialogBuilder.create(); + + final FingerprintManagerCompat.AuthenticationCallback fingerprintAuthCallback = new FingerprintManagerCompat.AuthenticationCallback() { + @Override + public void onAuthenticationError(int errMsgId, CharSequence errString) { + ((TextView) promptsView.findViewById(R.id.txtFingerprintAuth)).setText(errString); + } + + @Override + public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) { + String userPass = KeyStoreHelper.loadWalletUserPass(context, wallet); + if (Helper.processPasswordEntry(context, wallet, userPass, true, action)) { + Helper.hideKeyboardAlways((Activity) context); + openDialog.dismiss(); + openDialog = null; + } else { + etPassword.setError(context.getString(R.string.bad_password)); + } + } + + @Override + public void onAuthenticationFailed() { + ((TextView) promptsView.findViewById(R.id.txtFingerprintAuth)) + .setText(context.getString(R.string.bad_fingerprint)); + } + }; + + openDialog.setOnShowListener(new DialogInterface.OnShowListener() { + @Override + public void onShow(DialogInterface dialog) { + if (fingerprintAuthAllowed) { + FingerprintHelper.authenticate(context, cancelSignal, fingerprintAuthCallback); + } + Button button = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE); + button.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + String pass = etPassword.getEditText().getText().toString(); + if (processPasswordEntry(context, wallet, pass, false, action)) { + Helper.hideKeyboardAlways((Activity) context); + openDialog.dismiss(); + openDialog = null; + } else { + etPassword.setError(context.getString(R.string.bad_password)); + } + } + }); + } + }); + + // accept keyboard "ok" + etPassword.getEditText().setOnEditorActionListener(new TextView.OnEditorActionListener() { + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_DONE)) { + String pass = etPassword.getEditText().getText().toString(); + if (processPasswordEntry(context, wallet, pass, false, action)) { + Helper.hideKeyboardAlways((Activity) context); + openDialog.dismiss(); + openDialog = null; + } else { + etPassword.setError(context.getString(R.string.bad_password)); + } + return true; + } + return false; + } + }); + + Helper.showKeyboard(openDialog); + openDialog.show(); + } + + public interface PasswordAction { + void action(String walletName, String password, boolean fingerprintUsed); + } + + static private boolean processPasswordEntry(Context context, String walletName, String pass, boolean fingerprintUsed, PasswordAction action) { + String walletPassword = Helper.getWalletPassword(context, walletName, pass); + if (walletPassword != null) { + action.action(walletName, walletPassword, fingerprintUsed); + return true; + } else { + return false; + } + } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/KeyStoreHelper.java b/app/src/main/java/com/m2049r/xmrwallet/util/KeyStoreHelper.java new file mode 100644 index 0000000000..281fd257ed --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/KeyStoreHelper.java @@ -0,0 +1,283 @@ +/* + * Copyright 2018 m2049r + * Copyright 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.util; + +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build; +import android.security.KeyPairGeneratorSpec; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; +import android.util.Base64; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.util.Calendar; +import java.util.GregorianCalendar; + +import javax.crypto.Cipher; +import javax.security.auth.x500.X500Principal; + +import timber.log.Timber; + +public class KeyStoreHelper { + + static { + System.loadLibrary("monerujo"); + } + + public static native byte[] cnSlowHash(byte[] data); + + static final private String RSA_ALIAS = "MonerujoRSA"; + + public static String getCrazyPass(Context context, String password) { + // TODO we should check Locale.getDefault().getLanguage() here but for now we default to English + return getCrazyPass(context, password, "English"); + } + + public static String getCrazyPass(Context context, String password, String language) { + byte[] data = password.getBytes(StandardCharsets.UTF_8); + byte[] sig = null; + try { + KeyStoreHelper.createKeys(context, RSA_ALIAS); + sig = KeyStoreHelper.signData(RSA_ALIAS, data); + } catch (Exception ex) { + throw new IllegalStateException(ex); + } + return CrazyPassEncoder.encode(cnSlowHash(sig)); + } + + public static void saveWalletUserPass(Context context, String wallet, String password) { + String walletKeyAlias = SecurityConstants.WALLET_PASS_KEY_PREFIX + wallet; + byte[] data = password.getBytes(StandardCharsets.UTF_8); + try { + KeyStoreHelper.createKeys(context, walletKeyAlias); + byte[] encrypted = KeyStoreHelper.encrypt(walletKeyAlias, data); + context.getSharedPreferences(SecurityConstants.WALLET_PASS_PREFS_NAME, Context.MODE_PRIVATE).edit() + .putString(wallet, Base64.encodeToString(encrypted, Base64.DEFAULT)) + .apply(); + } catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + + public static String loadWalletUserPass(Context context, String wallet) { + String walletKeyAlias = SecurityConstants.WALLET_PASS_KEY_PREFIX + wallet; + String encoded = context.getSharedPreferences(SecurityConstants.WALLET_PASS_PREFS_NAME, Context.MODE_PRIVATE) + .getString(wallet, ""); + byte[] data = Base64.decode(encoded, Base64.DEFAULT); + byte[] decrypted = KeyStoreHelper.decrypt(walletKeyAlias, data); + + return new String(decrypted, StandardCharsets.UTF_8); + } + + public static void removeWalletUserPass(Context context, String wallet) { + String walletKeyAlias = SecurityConstants.WALLET_PASS_KEY_PREFIX + wallet; + try { + KeyStoreHelper.deleteKeys(walletKeyAlias); + context.getSharedPreferences(SecurityConstants.WALLET_PASS_PREFS_NAME, Context.MODE_PRIVATE).edit() + .remove(wallet).apply(); + } catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + + /** + * Creates a public and private key and stores it using the Android Key + * Store, so that only this application will be able to access the keys. + */ + public static void createKeys(Context context, String alias) throws NoSuchProviderException, + NoSuchAlgorithmException, InvalidAlgorithmParameterException, KeyStoreException { + KeyStore keyStore = KeyStore.getInstance(SecurityConstants.KEYSTORE_PROVIDER_ANDROID_KEYSTORE); + try { + keyStore.load(null); + } catch (Exception ex) { // don't care why it failed + throw new IllegalStateException("Could not load KeySotre", ex); + } + if (!keyStore.containsAlias(alias)) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + createKeysJBMR2(context, alias); + } else { + createKeysM(alias); + } + } + } + + public static void deleteKeys(String alias) throws KeyStoreException { + KeyStore keyStore = KeyStore.getInstance(SecurityConstants.KEYSTORE_PROVIDER_ANDROID_KEYSTORE); + try { + keyStore.load(null); + keyStore.deleteEntry(alias); + } catch (Exception ex) { // don't care why it failed + throw new IllegalStateException("Could not load KeySotre", ex); + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) + private static void createKeysJBMR2(Context context, String alias) throws NoSuchProviderException, + NoSuchAlgorithmException, InvalidAlgorithmParameterException { + + Calendar start = new GregorianCalendar(); + Calendar end = new GregorianCalendar(); + end.add(Calendar.YEAR, 300); + + KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(context) + .setAlias(alias) + .setSubject(new X500Principal("CN=" + alias)) + .setSerialNumber(BigInteger.valueOf(Math.abs(alias.hashCode()))) + .setStartDate(start.getTime()).setEndDate(end.getTime()) + .build(); + // defaults to 2048 bit modulus + KeyPairGenerator kpGenerator = KeyPairGenerator.getInstance( + SecurityConstants.TYPE_RSA, + SecurityConstants.KEYSTORE_PROVIDER_ANDROID_KEYSTORE); + kpGenerator.initialize(spec); + KeyPair kp = kpGenerator.generateKeyPair(); + Timber.d("preM Keys created"); + } + + @TargetApi(Build.VERSION_CODES.M) + private static void createKeysM(String alias) { + try { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance( + KeyProperties.KEY_ALGORITHM_RSA, SecurityConstants.KEYSTORE_PROVIDER_ANDROID_KEYSTORE); + keyPairGenerator.initialize( + new KeyGenParameterSpec.Builder( + alias, KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) + .setDigests(KeyProperties.DIGEST_SHA256) + .setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1) + .build()); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + Timber.d("M Keys created"); + } catch (NoSuchProviderException | NoSuchAlgorithmException | InvalidAlgorithmParameterException e) { + throw new RuntimeException(e); + } + } + + private static KeyStore.PrivateKeyEntry getPrivateKeyEntry(String alias) { + try { + KeyStore ks = KeyStore + .getInstance(SecurityConstants.KEYSTORE_PROVIDER_ANDROID_KEYSTORE); + ks.load(null); + KeyStore.Entry entry = ks.getEntry(alias, null); + + if (entry == null) { + Timber.w("No key found under alias: %s", alias); + return null; + } + + if (!(entry instanceof KeyStore.PrivateKeyEntry)) { + Timber.w("Not an instance of a PrivateKeyEntry"); + return null; + } + return (KeyStore.PrivateKeyEntry) entry; + } catch (Exception ex) { + Timber.e(ex); + return null; + } + } + + public static byte[] encrypt(String alias, byte[] data) { + try { + PublicKey publicKey = getPrivateKeyEntry(alias).getCertificate().getPublicKey(); + Cipher cipher = Cipher.getInstance(SecurityConstants.CIPHER_RSA_ECB_PKCS1); + + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + return cipher.doFinal(data); + } catch (Exception ex) { + throw new IllegalStateException("Could not initialize RSA cipher", ex); + } + } + + public static byte[] decrypt(String alias, byte[] data) { + try { + PrivateKey privateKey = getPrivateKeyEntry(alias).getPrivateKey(); + Cipher cipher = Cipher.getInstance(SecurityConstants.CIPHER_RSA_ECB_PKCS1); + + cipher.init(Cipher.DECRYPT_MODE, privateKey); + return cipher.doFinal(data); + } catch (Exception ex) { + throw new IllegalStateException("Could not initialize RSA cipher", ex); + } + } + + /** + * Signs the data using the key pair stored in the Android Key Store. This + * signature can be used with the data later to verify it was signed by this + * application. + * + * @return The data signature generated + */ + public static byte[] signData(String alias, byte[] data) throws NoSuchAlgorithmException, + InvalidKeyException, SignatureException { + + PrivateKey privateKey = getPrivateKeyEntry(alias).getPrivateKey(); + Signature s = Signature.getInstance(SecurityConstants.SIGNATURE_SHA256withRSA); + s.initSign(privateKey); + s.update(data); + return s.sign(); + } + + /** + * Given some data and a signature, uses the key pair stored in the Android + * Key Store to verify that the data was signed by this application, using + * that key pair. + * + * @param data The data to be verified. + * @param signature The signature provided for the data. + * @return A boolean value telling you whether the signature is valid or + * not. + */ + public static boolean verifyData(String alias, byte[] data, byte[] signature) + throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + + // Make sure the signature string exists + if (signature == null) { + Timber.w("Invalid signature."); + return false; + } + + KeyStore.PrivateKeyEntry keyEntry = getPrivateKeyEntry(alias); + Signature s = Signature.getInstance(SecurityConstants.SIGNATURE_SHA256withRSA); + s.initVerify(keyEntry.getCertificate()); + s.update(data); + return s.verify(signature); + } + + public interface SecurityConstants { + String KEYSTORE_PROVIDER_ANDROID_KEYSTORE = "AndroidKeyStore"; + String TYPE_RSA = "RSA"; + String SIGNATURE_SHA256withRSA = "SHA256withRSA"; + String CIPHER_RSA_ECB_PKCS1 = "RSA/ECB/PKCS1Padding"; + String WALLET_PASS_PREFS_NAME = "wallet"; + String WALLET_PASS_KEY_PREFIX = "walletKey-"; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/Notice.java b/app/src/main/java/com/m2049r/xmrwallet/util/Notice.java new file mode 100644 index 0000000000..1b0b1c8362 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/Notice.java @@ -0,0 +1,135 @@ +/* + * 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.util; + +import android.content.Context; +import android.content.SharedPreferences; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.m2049r.xmrwallet.R; +import com.m2049r.xmrwallet.dialog.HelpFragment; + +import java.util.ArrayList; +import java.util.List; + +public class Notice { + private static final String PREFS_NAME = "notice"; + private static List notices = null; + + private static final String NOTICE_SHOW_XMRTO_ENABLED_LOGIN = "notice_xmrto_enabled_login"; + private static final String NOTICE_SHOW_XMRTO_ENABLED_SEND = "notice_xmrto_enabled_send"; + private static final String NOTICE_SHOW_CRAZYPASS = "notice_crazypass_enabled_login"; + + private static void init() { + synchronized (Notice.class) { + if (notices != null) return; + notices = new ArrayList<>(); + notices.add( + new Notice(NOTICE_SHOW_XMRTO_ENABLED_SEND, + R.string.info_xmrto_enabled, + R.string.help_xmrto, + 1) + ); + notices.add( + new Notice(NOTICE_SHOW_XMRTO_ENABLED_LOGIN, + R.string.info_xmrto_enabled, + R.string.help_xmrto, + 1) + ); + notices.add( + new Notice(NOTICE_SHOW_CRAZYPASS, + R.string.info_crazypass_enabled, + R.string.help_details, + 2) + ); + } + } + + public static void showAll(ViewGroup parent, String selector) { + if (notices == null) init(); + for (Notice notice : notices) { + if (notice.id.matches(selector)) + notice.show(parent); + } + } + + private final String id; + private final int textResId; + private final int helpResId; + private final int defaultCount; + private transient int count = -1; + + private Notice(final String id, final int textResId, final int helpResId, final int defaultCount) { + this.id = id; + this.textResId = textResId; + this.helpResId = helpResId; + this.defaultCount = defaultCount; + } + + // show this notice as a child of the given parent view + // NB: it assumes the parent is in a Fragment + private void show(final ViewGroup parent) { + final Context context = parent.getContext(); + if (getCount(context) <= 0) return; // don't add it + + final LinearLayout ll = + (LinearLayout) LayoutInflater.from(context) + .inflate(R.layout.template_notice, parent, false); + + ((TextView) ll.findViewById(R.id.tvNotice)).setText(textResId); + + final FragmentManager fragmentManager = + ((FragmentActivity) context).getSupportFragmentManager(); + ll.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + HelpFragment.display(fragmentManager, helpResId); + } + }); + + ImageButton ib = (ImageButton) ll.findViewById(R.id.ibClose); + ib.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + ll.setVisibility(View.GONE); + decCount(context); + } + }); + parent.addView(ll); + } + + private int getCount(final Context context) { + count = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .getInt(id, defaultCount); + return count; + } + + private void decCount(final Context context) { + final SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + if (count < 0) // not initialized yet + count = prefs.getInt(id, defaultCount); + if (count > 0) + prefs.edit().putInt(id, count - 1).apply(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/RestoreHeight.java b/app/src/main/java/com/m2049r/xmrwallet/util/RestoreHeight.java index 29b1c53741..6d8374a149 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/util/RestoreHeight.java +++ b/app/src/main/java/com/m2049r/xmrwallet/util/RestoreHeight.java @@ -87,6 +87,8 @@ static public RestoreHeight getInstance() { blockheight.put("2017-12-01", 1454639L); blockheight.put("2018-01-01", 1477201L); blockheight.put("2018-02-01", 1499599L); + blockheight.put("2018-03-01", 1519796L); + blockheight.put("2018-04-01", 1542067L); } public long getHeight(String date) { diff --git a/app/src/main/java/com/m2049r/xmrwallet/widget/Toolbar.java b/app/src/main/java/com/m2049r/xmrwallet/widget/Toolbar.java index e06c081c6e..8f96d1262c 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/widget/Toolbar.java +++ b/app/src/main/java/com/m2049r/xmrwallet/widget/Toolbar.java @@ -45,7 +45,7 @@ public void setOnButtonListener(OnButtonListener listener) { ImageView toolbarImage; TextView toolbarTitle; TextView toolbarSubtitle; - Button bDonate; + Button bCredits; public Toolbar(Context context) { super(context); @@ -87,8 +87,8 @@ protected void onFinishInflate() { toolbarTitle = (TextView) findViewById(R.id.toolbarTitle); toolbarSubtitle = (TextView) findViewById(R.id.toolbarSubtitle); - bDonate = (Button) findViewById(R.id.bDonate); - bDonate.setOnClickListener(new View.OnClickListener() { + bCredits = (Button) findViewById(R.id.bCredits); + bCredits.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { if (onButtonListener != null) { onButtonListener.onButton(buttonType); @@ -116,43 +116,43 @@ public void setTitle(String title) { public final static int BUTTON_NONE = 0; public final static int BUTTON_BACK = 1; public final static int BUTTON_CLOSE = 2; - public final static int BUTTON_DONATE = 3; + public final static int BUTTON_CREDITS = 3; public final static int BUTTON_CANCEL = 4; - int buttonType = BUTTON_DONATE; + int buttonType = BUTTON_CREDITS; public void setButton(int type) { switch (type) { case BUTTON_BACK: Timber.d("BUTTON_BACK"); - bDonate.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_arrow_back_white_24dp, 0, 0, 0); - bDonate.setText(null); - bDonate.setVisibility(View.VISIBLE); + bCredits.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_arrow_back_white_24dp, 0, 0, 0); + bCredits.setText(null); + bCredits.setVisibility(View.VISIBLE); break; case BUTTON_CLOSE: Timber.d("BUTTON_CLOSE"); - bDonate.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_close_white_24dp, 0, 0, 0); - bDonate.setText(R.string.label_close); - bDonate.setVisibility(View.VISIBLE); + bCredits.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_close_white_24dp, 0, 0, 0); + bCredits.setText(R.string.label_close); + bCredits.setVisibility(View.VISIBLE); break; - case BUTTON_DONATE: - Timber.d("BUTTON_DONATE"); - bDonate.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_favorite_white_24dp, 0, 0, 0); - bDonate.setText(R.string.label_donate); - bDonate.setVisibility(View.VISIBLE); + case BUTTON_CREDITS: + Timber.d("BUTTON_CREDITS"); + bCredits.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_favorite_white_24dp, 0, 0, 0); + bCredits.setText(R.string.label_credits); + bCredits.setVisibility(View.VISIBLE); break; case BUTTON_CANCEL: Timber.d("BUTTON_CANCEL"); - bDonate.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_close_white_24dp, 0, 0, 0); - bDonate.setText(R.string.label_cancel); - bDonate.setVisibility(View.VISIBLE); + bCredits.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_close_white_24dp, 0, 0, 0); + bCredits.setText(R.string.label_cancel); + bCredits.setVisibility(View.VISIBLE); break; case BUTTON_NONE: default: Timber.d("BUTTON_NONE"); - bDonate.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); - bDonate.setText(null); - bDonate.setVisibility(View.INVISIBLE); + bCredits.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); + bCredits.setText(null); + bCredits.setVisibility(View.INVISIBLE); } buttonType = type; } diff --git a/app/src/main/res/drawable/backgound_amount.xml b/app/src/main/res/drawable/backgound_amount.xml deleted file mode 100644 index 8fe9ec7060..0000000000 --- a/app/src/main/res/drawable/backgound_amount.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/backgound_scan.xml b/app/src/main/res/drawable/backgound_scan.xml deleted file mode 100644 index 255a292dc0..0000000000 --- a/app/src/main/res/drawable/backgound_scan.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/gunther_donate.png b/app/src/main/res/drawable/gunther_coder.png similarity index 100% rename from app/src/main/res/drawable/gunther_donate.png rename to app/src/main/res/drawable/gunther_coder.png diff --git a/app/src/main/res/drawable/ic_add_black_24dp.xml b/app/src/main/res/drawable/ic_add_black_24dp.xml deleted file mode 100644 index 0258249cc4..0000000000 --- a/app/src/main/res/drawable/ic_add_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_arrow_right_orange_24dp.xml b/app/src/main/res/drawable/ic_arrow_right_orange_24dp.xml deleted file mode 100644 index 89be915f34..0000000000 --- a/app/src/main/res/drawable/ic_arrow_right_orange_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_favorite_white_14dp.xml b/app/src/main/res/drawable/ic_favorite_white_14dp.xml deleted file mode 100644 index 736aacd706..0000000000 --- a/app/src/main/res/drawable/ic_favorite_white_14dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_fingerprint.xml b/app/src/main/res/drawable/ic_fingerprint.xml new file mode 100644 index 0000000000..81eccc55bc --- /dev/null +++ b/app/src/main/res/drawable/ic_fingerprint.xml @@ -0,0 +1,37 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_max_sweep.xml b/app/src/main/res/drawable/ic_max_sweep.xml deleted file mode 100644 index 18ffccbbfe..0000000000 --- a/app/src/main/res/drawable/ic_max_sweep.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_monero_32dp.xml b/app/src/main/res/drawable/ic_monero_32dp.xml deleted file mode 100644 index ea68ee88e0..0000000000 --- a/app/src/main/res/drawable/ic_monero_32dp.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/ic_monero_qr.xml b/app/src/main/res/drawable/ic_monero_qr.xml deleted file mode 100644 index ceda49529f..0000000000 --- a/app/src/main/res/drawable/ic_monero_qr.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/layout/activity_test_send.xml b/app/src/main/res/layout/activity_test_send.xml deleted file mode 100644 index 4df48d5aaf..0000000000 --- a/app/src/main/res/layout/activity_test_send.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/layout/fragment_credits.xml b/app/src/main/res/layout/fragment_credits.xml new file mode 100644 index 0000000000..5d2128b96f --- /dev/null +++ b/app/src/main/res/layout/fragment_credits.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_donation.xml b/app/src/main/res/layout/fragment_donation.xml deleted file mode 100644 index 835fa82d56..0000000000 --- a/app/src/main/res/layout/fragment_donation.xml +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_generate.xml b/app/src/main/res/layout/fragment_generate.xml index 517ba60c68..7a8df46466 100644 --- a/app/src/main/res/layout/fragment_generate.xml +++ b/app/src/main/res/layout/fragment_generate.xml @@ -1,6 +1,7 @@ @@ -55,6 +56,27 @@ + + + + + + + - - - - - - + android:orientation="vertical" /> - - - - - - - - - - - - - - + android:textAlignment="center" + tools:text="49RBjxQ2zgf7t17w7So9ngcEY9obKzsrr6Dsah24MNSMiMBEeiYPP5CCTBq4GpZcEYN5Zf3upsLiwd5PezePE1i4Tf3rryY" /> + android:textAlignment="center" + tools:text="tucks slackens vehicle doctor oaks aloof balding knife rays wise haggled cuisine navy ladder suitcase dusted last thorn pixels karate ticket nibs violin zapped slackens" /> + + + + + + +