From e6883a40d173bd21054b03eadd3178ec9b23db46 Mon Sep 17 00:00:00 2001 From: pokkst Date: Wed, 7 Sep 2022 14:31:20 -0500 Subject: [PATCH] Upload normal Monerujo code --- .circleci/config.yml | 27 + .gitignore | 41 +- app/CMakeLists.txt | 239 +++ app/build.gradle | 159 ++ app/proguard-rules.pro | 25 + app/src/alpha/ic_launcher-web.png | Bin 0 -> 249717 bytes app/src/alpha/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 8175 bytes app/src/alpha/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 4506 bytes .../alpha/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 14096 bytes .../alpha/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 28536 bytes .../alpha/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 47939 bytes app/src/debug/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 8336 bytes app/src/debug/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 4527 bytes .../debug/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 14365 bytes .../debug/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 28937 bytes .../debug/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 49047 bytes app/src/main/AndroidManifest.xml | 111 + app/src/main/assets/licenses.html | 829 ++++++++ app/src/main/cpp/monerujo.cpp | 1531 ++++++++++++++ app/src/main/cpp/monerujo.h | 79 + app/src/main/ic_launcher-web.png | Bin 0 -> 251291 bytes .../main/java/com/btchip/BTChipException.java | 53 + .../java/com/btchip/comm/BTChipTransport.java | 31 + .../java/com/btchip/comm/LedgerHelper.java | 126 ++ .../android/BTChipTransportAndroidHID.java | 149 ++ app/src/main/java/com/btchip/utils/Dump.java | 62 + .../java/com/m2049r/levin/data/Bucket.java | 145 ++ .../java/com/m2049r/levin/data/Section.java | 125 ++ .../com/m2049r/levin/scanner/Dispatcher.java | 195 ++ .../com/m2049r/levin/scanner/LevinPeer.java | 39 + .../m2049r/levin/scanner/PeerRetriever.java | 231 ++ .../java/com/m2049r/levin/util/HexHelper.java | 42 + .../com/m2049r/levin/util/LevinReader.java | 184 ++ .../com/m2049r/levin/util/LevinWriter.java | 98 + .../util/LittleEndianDataInputStream.java | 564 +++++ .../util/LittleEndianDataOutputStream.java | 403 ++++ .../com/m2049r/xmrwallet/BaseActivity.java | 313 +++ .../m2049r/xmrwallet/GenerateFragment.java | 615 ++++++ .../xmrwallet/GenerateReviewFragment.java | 711 +++++++ .../com/m2049r/xmrwallet/LoginActivity.java | 1469 +++++++++++++ .../com/m2049r/xmrwallet/LoginFragment.java | 563 +++++ .../com/m2049r/xmrwallet/MainActivity.java | 39 + .../com/m2049r/xmrwallet/NodeFragment.java | 589 ++++++ .../xmrwallet/OnBackPressedListener.java | 21 + .../xmrwallet/OnBlockUpdateListener.java | 23 + .../xmrwallet/OnUriScannedListener.java | 23 + .../com/m2049r/xmrwallet/ReceiveFragment.java | 469 +++++ .../com/m2049r/xmrwallet/ScannerFragment.java | 96 + .../com/m2049r/xmrwallet/SecureActivity.java | 73 + .../m2049r/xmrwallet/SettingsFragment.java | 125 ++ .../m2049r/xmrwallet/SubaddressFragment.java | 241 +++ .../xmrwallet/SubaddressInfoFragment.java | 173 ++ .../java/com/m2049r/xmrwallet/TxFragment.java | 406 ++++ .../com/m2049r/xmrwallet/WalletActivity.java | 1220 +++++++++++ .../com/m2049r/xmrwallet/WalletFragment.java | 559 +++++ .../xmrwallet/XmrWalletApplication.java | 77 + .../m2049r/xmrwallet/data/BarcodeData.java | 232 ++ .../com/m2049r/xmrwallet/data/Crypto.java | 89 + .../m2049r/xmrwallet/data/DefaultNodes.java | 39 + .../java/com/m2049r/xmrwallet/data/Node.java | 371 ++++ .../com/m2049r/xmrwallet/data/NodeInfo.java | 303 +++ .../com/m2049r/xmrwallet/data/PendingTx.java | 39 + .../com/m2049r/xmrwallet/data/Subaddress.java | 63 + .../com/m2049r/xmrwallet/data/TxData.java | 149 ++ .../com/m2049r/xmrwallet/data/TxDataBtc.java | 101 + .../com/m2049r/xmrwallet/data/UserNotes.java | 99 + .../xmrwallet/dialog/AboutFragment.java | 90 + .../xmrwallet/dialog/CreditsFragment.java | 69 + .../m2049r/xmrwallet/dialog/HelpFragment.java | 115 + .../xmrwallet/dialog/PrivacyFragment.java | 69 + .../xmrwallet/dialog/ProgressDialog.java | 132 ++ .../send/SendAddressWizardFragment.java | 525 +++++ .../send/SendAmountWizardFragment.java | 159 ++ .../send/SendBtcAmountWizardFragment.java | 263 +++ .../send/SendBtcConfirmWizardFragment.java | 551 +++++ .../send/SendBtcSuccessWizardFragment.java | 262 +++ .../xmrwallet/fragment/send/SendConfirm.java | 27 + .../send/SendConfirmWizardFragment.java | 247 +++ .../xmrwallet/fragment/send/SendFragment.java | 558 +++++ .../send/SendSuccessWizardFragment.java | 128 ++ .../fragment/send/SendWizardFragment.java | 36 + .../m2049r/xmrwallet/layout/DiffCallback.java | 46 + .../xmrwallet/layout/NodeInfoAdapter.java | 261 +++ .../xmrwallet/layout/SpendViewPager.java | 73 + .../layout/SubaddressInfoAdapter.java | 164 ++ .../layout/TransactionInfoAdapter.java | 278 +++ .../xmrwallet/layout/WalletInfoAdapter.java | 174 ++ .../m2049r/xmrwallet/ledger/Instruction.java | 155 ++ .../com/m2049r/xmrwallet/ledger/Ledger.java | 240 +++ .../ledger/LedgerProgressDialog.java | 183 ++ .../m2049r/xmrwallet/model/NetworkType.java | 45 + .../xmrwallet/model/PendingTransaction.java | 98 + .../xmrwallet/model/TransactionHistory.java | 82 + .../xmrwallet/model/TransactionInfo.java | 186 ++ .../com/m2049r/xmrwallet/model/Transfer.java | 57 + .../com/m2049r/xmrwallet/model/Wallet.java | 507 +++++ .../xmrwallet/model/WalletListener.java | 57 + .../m2049r/xmrwallet/model/WalletManager.java | 341 +++ .../onboarding/OnBoardingActivity.java | 123 ++ .../onboarding/OnBoardingAdapter.java | 92 + .../onboarding/OnBoardingManager.java | 51 + .../onboarding/OnBoardingScreen.java | 59 + .../onboarding/OnBoardingViewPager.java | 86 + .../service/MoneroHandlerThread.java | 161 ++ .../xmrwallet/service/WalletService.java | 595 ++++++ .../service/exchange/api/ExchangeApi.java | 37 + .../exchange/api/ExchangeCallback.java | 26 + .../exchange/api/ExchangeException.java | 48 + .../service/exchange/api/ExchangeRate.java | 29 + .../service/exchange/ecb/ExchangeApiImpl.java | 203 ++ .../exchange/ecb/ExchangeRateImpl.java | 57 + .../exchange/kraken/ExchangeApiImpl.java | 124 ++ .../exchange/kraken/ExchangeRateImpl.java | 94 + .../exchange/krakenEcb/ExchangeApiImpl.java | 92 + .../exchange/krakenEcb/ExchangeRateImpl.java | 54 + .../service/shift/NetworkCallback.java | 27 + .../xmrwallet/service/shift/ShiftApiCall.java | 28 + .../service/shift/ShiftCallback.java | 24 + .../xmrwallet/service/shift/ShiftError.java | 54 + .../service/shift/ShiftException.java | 33 + .../shift/sideshift/api/CreateOrder.java | 42 + .../sideshift/api/QueryOrderParameters.java | 27 + .../shift/sideshift/api/QueryOrderStatus.java | 65 + .../shift/sideshift/api/RequestQuote.java | 34 + .../shift/sideshift/api/SideShiftApi.java | 64 + .../sideshift/network/CreateOrderImpl.java | 122 ++ .../network/QueryOrderParametersImpl.java | 73 + .../network/QueryOrderStatusImpl.java | 146 ++ .../sideshift/network/RequestQuoteImpl.java | 126 ++ .../sideshift/network/SideShiftApiImpl.java | 122 ++ .../xmrwallet/util/CrazyPassEncoder.java | 68 + .../com/m2049r/xmrwallet/util/DateHelper.java | 29 + .../m2049r/xmrwallet/util/DayNightMode.java | 45 + .../xmrwallet/util/FingerprintHelper.java | 61 + .../com/m2049r/xmrwallet/util/Helper.java | 609 ++++++ .../m2049r/xmrwallet/util/KeyStoreHelper.java | 351 ++++ .../xmrwallet/util/LegacyStorageHelper.java | 170 ++ .../m2049r/xmrwallet/util/LocaleHelper.java | 104 + .../util/MoneroThreadPoolExecutor.java | 56 + .../xmrwallet/util/NetCipherHelper.java | 393 ++++ .../xmrwallet/util/NightmodeHelper.java | 53 + .../com/m2049r/xmrwallet/util/NodePinger.java | 53 + .../com/m2049r/xmrwallet/util/Notice.java | 127 ++ .../m2049r/xmrwallet/util/OnionHelper.java | 24 + .../xmrwallet/util/OpenAliasHelper.java | 244 +++ .../m2049r/xmrwallet/util/RestoreHeight.java | 211 ++ .../m2049r/xmrwallet/util/ServiceHelper.java | 24 + .../m2049r/xmrwallet/util/ThemeHelper.java | 65 + .../com/m2049r/xmrwallet/util/ZipBackup.java | 64 + .../com/m2049r/xmrwallet/util/ZipRestore.java | 139 ++ .../xmrwallet/util/ledger/ECsecp256k1.java | 81 + .../m2049r/xmrwallet/util/ledger/Monero.java | 1866 +++++++++++++++++ .../util/validator/BitcoinAddressType.java | 51 + .../validator/BitcoinAddressValidator.java | 220 ++ .../util/validator/EthAddressValidator.java | 64 + .../xmrwallet/widget/CTextInputLayout.java | 44 + .../com/m2049r/xmrwallet/widget/DotBar.java | 156 ++ .../xmrwallet/widget/DropDownEditText.java | 54 + .../xmrwallet/widget/ExchangeEditText.java | 421 ++++ .../widget/ExchangeOtherEditText.java | 196 ++ .../m2049r/xmrwallet/widget/ExchangeView.java | 469 +++++ .../xmrwallet/widget/PasswordEntryView.java | 73 + .../xmrwallet/widget/SendProgressView.java | 87 + .../com/m2049r/xmrwallet/widget/Toolbar.java | 161 ++ .../main/java/com/theromus/sha/Keccak.java | 170 ++ .../java/com/theromus/sha/Parameters.java | 51 + .../java/com/theromus/utils/HexUtils.java | 97 + .../client/StrongOkHttpClientBuilder.java | 115 + app/src/main/res/anim/cycle_7.xml | 3 + app/src/main/res/anim/fab_close.xml | 18 + app/src/main/res/anim/fab_close_screen.xml | 9 + app/src/main/res/anim/fab_open.xml | 18 + app/src/main/res/anim/fab_open_screen.xml | 9 + app/src/main/res/anim/fab_pulse.xml | 15 + app/src/main/res/anim/rotate_backward.xml | 11 + app/src/main/res/anim/rotate_forward.xml | 11 + app/src/main/res/anim/shake.xml | 6 + app/src/main/res/color/btn_color_selector.xml | 5 + .../res/drawable-night/ic_emptygunther.xml | 1083 ++++++++++ .../drawable-night/ic_gunther_streetmode.xml | 180 ++ .../ic_onboarding_fingerprint.xml | 72 + .../drawable-night/ic_onboarding_nodes.xml | 231 ++ .../res/drawable-night/ic_onboarding_seed.xml | 393 ++++ .../drawable-night/ic_onboarding_welcome.xml | 75 + .../drawable-night/ic_onboarding_xmrto.xml | 138 ++ .../main/res/drawable-v24/ic_check_circle.xml | 24 + .../res/drawable-v24/ic_check_circle_xmr.xml | 46 + .../res/drawable-v24/ic_xmrto_btc_off.xml | 14 + .../res/drawable-v24/ic_xmrto_dash_off.xml | 15 + .../res/drawable-v24/ic_xmrto_doge_off.xml | 14 + .../res/drawable-v24/ic_xmrto_eth_off.xml | 38 + .../res/drawable-v24/ic_xmrto_ltc_off.xml | 14 + app/src/main/res/drawable/backgound_all.xml | 352 ++++ app/src/main/res/drawable/backgound_seed.xml | 14 + .../drawable/backgound_toolbar_mainnet.xml | 4 + .../drawable/backgound_toolbar_streetmode.xml | 4 + app/src/main/res/drawable/button_green.xml | 4 + .../res/drawable/button_selector_green.xml | 5 + app/src/main/res/drawable/dot_dark.xml | 12 + app/src/main/res/drawable/dot_light.xml | 12 + app/src/main/res/drawable/gradient_all.xml | 9 + app/src/main/res/drawable/gradient_oval.xml | 9 + app/src/main/res/drawable/gradient_street.xml | 9 + .../res/drawable/gradient_street_efab.xml | 10 + app/src/main/res/drawable/gunther_24dp.png | Bin 0 -> 1294 bytes app/src/main/res/drawable/gunther_coder.png | Bin 0 -> 3622 bytes .../main/res/drawable/gunther_csi_24dp.png | Bin 0 -> 1296 bytes .../main/res/drawable/gunther_desaturated.png | Bin 0 -> 5302 bytes .../ic_account_balance_wallet_black_24dp.xml | 10 + app/src/main/res/drawable/ic_add.xml | 9 + app/src/main/res/drawable/ic_add_circle.xml | 9 + .../main/res/drawable/ic_all_inclusive.xml | 9 + app/src/main/res/drawable/ic_arrow_back.xml | 10 + app/src/main/res/drawable/ic_check_circle.xml | 9 + .../main/res/drawable/ic_check_circle_xmr.xml | 26 + .../main/res/drawable/ic_close_white_24dp.xml | 9 + .../res/drawable/ic_content_copy_24dp.xml | 9 + .../res/drawable/ic_content_paste_24dp.xml | 9 + app/src/main/res/drawable/ic_done_all.xml | 13 + app/src/main/res/drawable/ic_emptygunther.xml | 1113 ++++++++++ .../main/res/drawable/ic_error_red_24dp.xml | 9 + .../main/res/drawable/ic_error_red_36dp.xml | 12 + app/src/main/res/drawable/ic_eye.xml | 19 + .../main/res/drawable/ic_eye_black_24dp.xml | 9 + .../main/res/drawable/ic_favorite_24dp.xml | 9 + .../res/drawable/ic_favorite_border_24dp.xml | 9 + app/src/main/res/drawable/ic_fingerprint.xml | 25 + .../res/drawable/ic_gunther_streetmode.xml | 105 + app/src/main/res/drawable/ic_hand.xml | 21 + .../main/res/drawable/ic_help_white_24dp.xml | 10 + app/src/main/res/drawable/ic_import.xml | 10 + .../drawable/ic_info_outline_black_24dp.xml | 9 + .../drawable/ic_info_outline_gray_24dp.xml | 9 + .../main/res/drawable/ic_info_white_24dp.xml | 9 + app/src/main/res/drawable/ic_key.xml | 31 + .../res/drawable/ic_keyboard_arrow_down.xml | 10 + .../res/drawable/ic_keyboard_arrow_up.xml | 10 + .../main/res/drawable/ic_launch_external.xml | 10 + .../res/drawable/ic_launcher_background.xml | 29 + .../res/drawable/ic_launcher_foreground.xml | 12 + .../main/res/drawable/ic_ledger_restore.xml | 15 + .../drawable/ic_logo_horizontol_xmrujo.xml | 34 + app/src/main/res/drawable/ic_monero.xml | 12 + app/src/main/res/drawable/ic_monero_bw.xml | 12 + .../main/res/drawable/ic_monero_logo_b.png | Bin 0 -> 1246 bytes app/src/main/res/drawable/ic_monerujo.xml | 32 + .../main/res/drawable/ic_navigate_next.xml | 9 + .../main/res/drawable/ic_navigate_prev.xml | 13 + .../main/res/drawable/ic_network_clearnet.xml | 9 + .../main/res/drawable/ic_network_tor_on.xml | 10 + app/src/main/res/drawable/ic_new.xml | 32 + app/src/main/res/drawable/ic_nfc.xml | 9 + .../drawable/ic_onboarding_fingerprint.xml | 72 + .../main/res/drawable/ic_onboarding_nodes.xml | 279 +++ .../main/res/drawable/ic_onboarding_seed.xml | 393 ++++ .../res/drawable/ic_onboarding_welcome.xml | 75 + .../main/res/drawable/ic_onboarding_xmrto.xml | 138 ++ app/src/main/res/drawable/ic_pending.xml | 9 + app/src/main/res/drawable/ic_renew.xml | 9 + app/src/main/res/drawable/ic_scan.xml | 9 + app/src/main/res/drawable/ic_seed.xml | 21 + app/src/main/res/drawable/ic_send.xml | 9 + app/src/main/res/drawable/ic_settings.xml | 10 + app/src/main/res/drawable/ic_share.xml | 10 + .../main/res/drawable/ic_sideshift_circle.xml | 12 + .../main/res/drawable/ic_sideshift_white.xml | 48 + .../drawable/ic_smiley_ecstatic_filled.xml | 9 + .../res/drawable/ic_smiley_gunther_filled.xml | 12 + .../res/drawable/ic_smiley_happy_filled.xml | 9 + .../res/drawable/ic_smiley_meh_filled.xml | 9 + .../res/drawable/ic_smiley_neutral_filled.xml | 9 + .../res/drawable/ic_smiley_sad_filled.xml | 9 + app/src/main/res/drawable/ic_statsup.xml | 13 + app/src/main/res/drawable/ic_success.xml | 9 + app/src/main/res/drawable/ic_wifi_1_bar.xml | 13 + app/src/main/res/drawable/ic_wifi_2_bar.xml | 13 + app/src/main/res/drawable/ic_wifi_3_bar.xml | 13 + app/src/main/res/drawable/ic_wifi_4_bar.xml | 9 + app/src/main/res/drawable/ic_wifi_lock.xml | 9 + app/src/main/res/drawable/ic_wifi_off.xml | 9 + app/src/main/res/drawable/ic_xmrto_btc.xml | 14 + .../main/res/drawable/ic_xmrto_btc_off.xml | 14 + app/src/main/res/drawable/ic_xmrto_dash.xml | 15 + .../main/res/drawable/ic_xmrto_dash_off.xml | 15 + app/src/main/res/drawable/ic_xmrto_doge.xml | 14 + .../main/res/drawable/ic_xmrto_doge_off.xml | 14 + app/src/main/res/drawable/ic_xmrto_eth.xml | 38 + .../main/res/drawable/ic_xmrto_eth_off.xml | 38 + app/src/main/res/drawable/ic_xmrto_logo.xml | 51 + app/src/main/res/drawable/ic_xmrto_ltc.xml | 14 + .../main/res/drawable/ic_xmrto_ltc_off.xml | 14 + .../res/drawable/logo_horizontol_xmrujo.png | Bin 0 -> 9814 bytes app/src/main/res/drawable/onboarding_dots.xml | 5 + app/src/main/res/drawable/selector_login.xml | 20 + app/src/main/res/layout/activity_login.xml | 21 + .../main/res/layout/activity_on_boarding.xml | 57 + app/src/main/res/layout/activity_wallet.xml | 40 + app/src/main/res/layout/checkbox_confirm.xml | 13 + .../res/layout/dialog_ledger_progress.xml | 54 + app/src/main/res/layout/fragment_about.xml | 47 + app/src/main/res/layout/fragment_credits.xml | 30 + app/src/main/res/layout/fragment_generate.xml | 214 ++ app/src/main/res/layout/fragment_help.xml | 20 + app/src/main/res/layout/fragment_login.xml | 158 ++ app/src/main/res/layout/fragment_node.xml | 74 + .../res/layout/fragment_privacy_policy.xml | 31 + app/src/main/res/layout/fragment_receive.xml | 145 ++ app/src/main/res/layout/fragment_review.xml | 259 +++ app/src/main/res/layout/fragment_send.xml | 78 + .../main/res/layout/fragment_send_address.xml | 220 ++ .../main/res/layout/fragment_send_amount.xml | 69 + .../res/layout/fragment_send_btc_amount.xml | 65 + .../res/layout/fragment_send_btc_confirm.xml | 257 +++ .../res/layout/fragment_send_btc_success.xml | 230 ++ .../main/res/layout/fragment_send_confirm.xml | 178 ++ .../main/res/layout/fragment_send_success.xml | 110 + .../main/res/layout/fragment_subaddress.xml | 75 + .../res/layout/fragment_subaddressinfo.xml | 56 + app/src/main/res/layout/fragment_tx_info.xml | 362 ++++ app/src/main/res/layout/fragment_wallet.xml | 198 ++ app/src/main/res/layout/item_node.xml | 71 + app/src/main/res/layout/item_spinner.xml | 11 + .../main/res/layout/item_spinner_balance.xml | 11 + .../res/layout/item_spinner_dropdown_item.xml | 11 + app/src/main/res/layout/item_subaddress.xml | 49 + app/src/main/res/layout/item_transaction.xml | 116 + app/src/main/res/layout/item_wallet.xml | 35 + app/src/main/res/layout/layout_fabmenu.xml | 232 ++ app/src/main/res/layout/nav_header.xml | 28 + app/src/main/res/layout/prompt_changepw.xml | 74 + app/src/main/res/layout/prompt_editnode.xml | 159 ++ .../main/res/layout/prompt_ledger_seed.xml | 68 + app/src/main/res/layout/prompt_password.xml | 34 + app/src/main/res/layout/prompt_rename.xml | 32 + app/src/main/res/layout/template_notice.xml | 36 + app/src/main/res/layout/view_exchange.xml | 112 + .../main/res/layout/view_exchange_edit.xml | 109 + app/src/main/res/layout/view_onboarding.xml | 65 + .../main/res/layout/view_send_progress.xml | 47 + app/src/main/res/layout/view_toolbar.xml | 59 + app/src/main/res/menu/create_wallet_keys.xml | 12 + .../main/res/menu/create_wallet_ledger.xml | 12 + app/src/main/res/menu/create_wallet_new.xml | 12 + app/src/main/res/menu/create_wallet_seed.xml | 19 + app/src/main/res/menu/create_wallet_view.xml | 12 + app/src/main/res/menu/drawer_view.xml | 15 + app/src/main/res/menu/list_context_menu.xml | 33 + app/src/main/res/menu/list_menu.xml | 11 + app/src/main/res/menu/node_menu.xml | 17 + app/src/main/res/menu/receive_menu.xml | 12 + app/src/main/res/menu/send_menu.xml | 12 + app/src/main/res/menu/tx_info_menu.xml | 19 + .../res/menu/wallet_details_help_menu.xml | 12 + app/src/main/res/menu/wallet_details_menu.xml | 18 + app/src/main/res/menu/wallet_menu.xml | 48 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2667 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 4325 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1917 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2800 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 3701 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 6277 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 5431 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 9265 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 7132 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 13045 bytes app/src/main/res/transition/details.xml | 3 + app/src/main/res/values-cat/about.xml | 54 + app/src/main/res/values-cat/help.xml | 254 +++ app/src/main/res/values-cat/strings.xml | 450 ++++ app/src/main/res/values-de/about.xml | 61 + app/src/main/res/values-de/help.xml | 314 +++ app/src/main/res/values-de/strings.xml | 451 ++++ app/src/main/res/values-el/about.xml | 61 + app/src/main/res/values-el/help.xml | 296 +++ app/src/main/res/values-el/strings.xml | 452 ++++ app/src/main/res/values-eo/about.xml | 57 + app/src/main/res/values-eo/help.xml | 340 +++ app/src/main/res/values-eo/strings.xml | 452 ++++ app/src/main/res/values-es/about.xml | 58 + app/src/main/res/values-es/help.xml | 336 +++ app/src/main/res/values-es/strings.xml | 443 ++++ app/src/main/res/values-et/about.xml | 61 + app/src/main/res/values-et/help.xml | 316 +++ app/src/main/res/values-et/strings.xml | 450 ++++ app/src/main/res/values-fa/about.xml | 59 + app/src/main/res/values-fa/help.xml | 425 ++++ app/src/main/res/values-fa/strings.xml | 703 +++++++ app/src/main/res/values-fr/about.xml | 66 + app/src/main/res/values-fr/help.xml | 334 +++ app/src/main/res/values-fr/strings.xml | 456 ++++ app/src/main/res/values-hu/about.xml | 61 + app/src/main/res/values-hu/help.xml | 318 +++ app/src/main/res/values-hu/strings.xml | 454 ++++ app/src/main/res/values-it/about.xml | 49 + app/src/main/res/values-it/help.xml | 319 +++ app/src/main/res/values-it/strings.xml | 455 ++++ app/src/main/res/values-ja/about.xml | 63 + app/src/main/res/values-ja/help.xml | 447 ++++ app/src/main/res/values-ja/strings.xml | 455 ++++ app/src/main/res/values-nb/about.xml | 61 + app/src/main/res/values-nb/help.xml | 316 +++ app/src/main/res/values-nb/strings.xml | 452 ++++ app/src/main/res/values-night-v23/styles.xml | 11 + app/src/main/res/values-night-v27/styles.xml | 12 + app/src/main/res/values-night/colors.xml | 63 + app/src/main/res/values-night/styles.xml | 13 + app/src/main/res/values-nl/about.xml | 49 + app/src/main/res/values-nl/help.xml | 254 +++ app/src/main/res/values-nl/strings.xml | 452 ++++ app/src/main/res/values-pt-rBR/about.xml | 62 + app/src/main/res/values-pt-rBR/help.xml | 315 +++ app/src/main/res/values-pt-rBR/strings.xml | 445 ++++ app/src/main/res/values-pt/about.xml | 62 + app/src/main/res/values-pt/help.xml | 315 +++ app/src/main/res/values-pt/strings.xml | 456 ++++ app/src/main/res/values-ro/about.xml | 62 + app/src/main/res/values-ro/help.xml | 322 +++ app/src/main/res/values-ro/strings.xml | 452 ++++ app/src/main/res/values-ru/about.xml | 62 + app/src/main/res/values-ru/help.xml | 319 +++ app/src/main/res/values-ru/strings.xml | 456 ++++ app/src/main/res/values-sk/about.xml | 52 + app/src/main/res/values-sk/help.xml | 283 +++ app/src/main/res/values-sk/strings.xml | 453 ++++ app/src/main/res/values-sr/about.xml | 61 + app/src/main/res/values-sr/help.xml | 314 +++ app/src/main/res/values-sr/strings.xml | 451 ++++ app/src/main/res/values-sv/about.xml | 61 + app/src/main/res/values-sv/help.xml | 303 +++ app/src/main/res/values-sv/strings.xml | 444 ++++ app/src/main/res/values-ta/about.xml | 55 + app/src/main/res/values-ta/help.xml | 322 +++ app/src/main/res/values-ta/strings.xml | 450 ++++ app/src/main/res/values-uk/about.xml | 62 + app/src/main/res/values-uk/help.xml | 314 +++ app/src/main/res/values-uk/strings.xml | 456 ++++ app/src/main/res/values-v23/styles.xml | 11 + app/src/main/res/values-v27/styles.xml | 12 + app/src/main/res/values-w1240dp/dimens.xml | 3 + app/src/main/res/values-w600dp/dimens.xml | 3 + app/src/main/res/values-zh-rCN/about.xml | 46 + app/src/main/res/values-zh-rCN/help.xml | 254 +++ app/src/main/res/values-zh-rCN/strings.xml | 376 ++++ app/src/main/res/values-zh-rTW/about.xml | 47 + app/src/main/res/values-zh-rTW/help.xml | 255 +++ app/src/main/res/values-zh-rTW/strings.xml | 451 ++++ app/src/main/res/values/about.xml | 61 + app/src/main/res/values/arrays.xml | 13 + app/src/main/res/values/attrs.xml | 25 + app/src/main/res/values/colors.xml | 63 + app/src/main/res/values/dimens.xml | 10 + app/src/main/res/values/help.xml | 314 +++ app/src/main/res/values/ids.xml | 5 + app/src/main/res/values/integers.xml | 4 + app/src/main/res/values/strings.xml | 529 +++++ app/src/main/res/values/styles.xml | 367 ++++ app/src/main/res/values/themes.xml | 11 + app/src/main/res/xml/filepaths.xml | 6 + app/src/main/res/xml/root_preferences.xml | 35 + app/src/main/res/xml/usb_device_filter.xml | 19 + app/src/stagenet/res/values/strings.xml | 4 + .../exchange/ecb/ExchangeRateTest.java | 301 +++ .../exchange/kraken/ExchangeRateTest.java | 186 ++ .../SideShiftApiCreateOrderTest.java | 213 ++ .../SideShiftApiOrderParameterTest.java | 169 ++ .../SideShiftApiQueryOrderStatusTest.java | 192 ++ .../SideShiftApiRequestQuoteTest.java | 198 ++ .../com/m2049r/xmrwallet/util/HelperTest.java | 99 + .../xmrwallet/util/OpenAliasHelperTest.java | 168 ++ .../xmrwallet/util/RestoreHeightTest.java | 133 ++ .../m2049r/xmrwallet/util/UserNoteTest.java | 98 + .../xmrwallet/util/ledger/MoneroTest.java | 104 + .../BitcoinAddressValidatorTest.java | 139 ++ .../validator/EthAddressValidatorTest.java | 46 + build.gradle | 25 + doc/BUILDING-external-libs.md | 25 + doc/FAQ.md | 106 + external-libs/.gitignore | 5 + external-libs/Makefile | 68 + external-libs/VERSION | 1 + external-libs/android32.Dockerfile | 172 ++ external-libs/android32_x86.Dockerfile | 172 ++ external-libs/android64.Dockerfile | 172 ++ external-libs/android64_x86.Dockerfile | 172 ++ external-libs/include/wallet2_api.h | 1376 ++++++++++++ gradle.properties | 20 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 185 ++ gradlew.bat | 89 + privacy-policy.md | 11 + settings.gradle | 1 + 494 files changed, 68231 insertions(+), 23 deletions(-) create mode 100644 .circleci/config.yml create mode 100644 app/CMakeLists.txt create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/alpha/ic_launcher-web.png create mode 100644 app/src/alpha/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/alpha/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/alpha/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/alpha/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/alpha/res/mipmap-xxxhdpi/ic_launcher.png create mode 100755 app/src/debug/res/mipmap-hdpi/ic_launcher.png create mode 100755 app/src/debug/res/mipmap-mdpi/ic_launcher.png create mode 100755 app/src/debug/res/mipmap-xhdpi/ic_launcher.png create mode 100755 app/src/debug/res/mipmap-xxhdpi/ic_launcher.png create mode 100755 app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/assets/licenses.html create mode 100644 app/src/main/cpp/monerujo.cpp create mode 100644 app/src/main/cpp/monerujo.h create mode 100644 app/src/main/ic_launcher-web.png create mode 100644 app/src/main/java/com/btchip/BTChipException.java create mode 100644 app/src/main/java/com/btchip/comm/BTChipTransport.java create mode 100644 app/src/main/java/com/btchip/comm/LedgerHelper.java create mode 100644 app/src/main/java/com/btchip/comm/android/BTChipTransportAndroidHID.java create mode 100644 app/src/main/java/com/btchip/utils/Dump.java create mode 100644 app/src/main/java/com/m2049r/levin/data/Bucket.java create mode 100644 app/src/main/java/com/m2049r/levin/data/Section.java create mode 100644 app/src/main/java/com/m2049r/levin/scanner/Dispatcher.java create mode 100644 app/src/main/java/com/m2049r/levin/scanner/LevinPeer.java create mode 100644 app/src/main/java/com/m2049r/levin/scanner/PeerRetriever.java create mode 100644 app/src/main/java/com/m2049r/levin/util/HexHelper.java create mode 100644 app/src/main/java/com/m2049r/levin/util/LevinReader.java create mode 100644 app/src/main/java/com/m2049r/levin/util/LevinWriter.java create mode 100644 app/src/main/java/com/m2049r/levin/util/LittleEndianDataInputStream.java create mode 100644 app/src/main/java/com/m2049r/levin/util/LittleEndianDataOutputStream.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/BaseActivity.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/MainActivity.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/NodeFragment.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/OnBackPressedListener.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/OnBlockUpdateListener.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/OnUriScannedListener.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/ScannerFragment.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/SecureActivity.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/SettingsFragment.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/SubaddressFragment.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/SubaddressInfoFragment.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/TxFragment.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/WalletFragment.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/XmrWalletApplication.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/data/BarcodeData.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/data/Crypto.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/data/DefaultNodes.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/data/Node.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/data/NodeInfo.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/data/PendingTx.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/data/Subaddress.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/data/TxData.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/data/TxDataBtc.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/data/UserNotes.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/dialog/AboutFragment.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/dialog/CreditsFragment.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/dialog/HelpFragment.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/dialog/PrivacyFragment.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/dialog/ProgressDialog.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAddressWizardFragment.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAmountWizardFragment.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcAmountWizardFragment.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcConfirmWizardFragment.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcSuccessWizardFragment.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendConfirm.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendConfirmWizardFragment.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendFragment.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendSuccessWizardFragment.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendWizardFragment.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/layout/DiffCallback.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/layout/NodeInfoAdapter.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/layout/SpendViewPager.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/layout/SubaddressInfoAdapter.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/layout/TransactionInfoAdapter.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/layout/WalletInfoAdapter.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/ledger/Instruction.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/ledger/Ledger.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/ledger/LedgerProgressDialog.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/model/NetworkType.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/model/PendingTransaction.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/model/TransactionHistory.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/model/TransactionInfo.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/model/Transfer.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/model/Wallet.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/model/WalletListener.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/model/WalletManager.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/onboarding/OnBoardingActivity.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/onboarding/OnBoardingAdapter.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/onboarding/OnBoardingManager.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/onboarding/OnBoardingScreen.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/onboarding/OnBoardingViewPager.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/service/MoneroHandlerThread.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/service/WalletService.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/service/exchange/api/ExchangeApi.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/service/exchange/api/ExchangeCallback.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/service/exchange/api/ExchangeException.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/service/exchange/api/ExchangeRate.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/service/exchange/ecb/ExchangeApiImpl.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/service/exchange/ecb/ExchangeRateImpl.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/service/exchange/kraken/ExchangeApiImpl.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/service/exchange/kraken/ExchangeRateImpl.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/service/exchange/krakenEcb/ExchangeApiImpl.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/service/exchange/krakenEcb/ExchangeRateImpl.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/service/shift/NetworkCallback.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/service/shift/ShiftApiCall.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/service/shift/ShiftCallback.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/service/shift/ShiftError.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/service/shift/ShiftException.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/api/CreateOrder.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/api/QueryOrderParameters.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/api/QueryOrderStatus.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/api/RequestQuote.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/api/SideShiftApi.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/network/CreateOrderImpl.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/network/QueryOrderParametersImpl.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/network/QueryOrderStatusImpl.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/network/RequestQuoteImpl.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/network/SideShiftApiImpl.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/util/CrazyPassEncoder.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/util/DateHelper.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/util/DayNightMode.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/util/FingerprintHelper.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/util/Helper.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/util/KeyStoreHelper.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/util/LegacyStorageHelper.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/util/LocaleHelper.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/util/MoneroThreadPoolExecutor.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/util/NetCipherHelper.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/util/NightmodeHelper.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/util/NodePinger.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/util/Notice.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/util/OnionHelper.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/util/OpenAliasHelper.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/util/RestoreHeight.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/util/ServiceHelper.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/util/ThemeHelper.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/util/ZipBackup.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/util/ZipRestore.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/util/ledger/ECsecp256k1.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/util/ledger/Monero.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/util/validator/BitcoinAddressType.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/util/validator/BitcoinAddressValidator.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/util/validator/EthAddressValidator.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/widget/CTextInputLayout.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/widget/DotBar.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/widget/DropDownEditText.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeEditText.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeOtherEditText.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeView.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/widget/PasswordEntryView.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/widget/SendProgressView.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/widget/Toolbar.java create mode 100644 app/src/main/java/com/theromus/sha/Keccak.java create mode 100644 app/src/main/java/com/theromus/sha/Parameters.java create mode 100644 app/src/main/java/com/theromus/utils/HexUtils.java create mode 100644 app/src/main/java/info/guardianproject/netcipher/client/StrongOkHttpClientBuilder.java create mode 100644 app/src/main/res/anim/cycle_7.xml create mode 100644 app/src/main/res/anim/fab_close.xml create mode 100644 app/src/main/res/anim/fab_close_screen.xml create mode 100644 app/src/main/res/anim/fab_open.xml create mode 100644 app/src/main/res/anim/fab_open_screen.xml create mode 100644 app/src/main/res/anim/fab_pulse.xml create mode 100644 app/src/main/res/anim/rotate_backward.xml create mode 100644 app/src/main/res/anim/rotate_forward.xml create mode 100644 app/src/main/res/anim/shake.xml create mode 100644 app/src/main/res/color/btn_color_selector.xml create mode 100644 app/src/main/res/drawable-night/ic_emptygunther.xml create mode 100644 app/src/main/res/drawable-night/ic_gunther_streetmode.xml create mode 100644 app/src/main/res/drawable-night/ic_onboarding_fingerprint.xml create mode 100644 app/src/main/res/drawable-night/ic_onboarding_nodes.xml create mode 100644 app/src/main/res/drawable-night/ic_onboarding_seed.xml create mode 100644 app/src/main/res/drawable-night/ic_onboarding_welcome.xml create mode 100644 app/src/main/res/drawable-night/ic_onboarding_xmrto.xml create mode 100644 app/src/main/res/drawable-v24/ic_check_circle.xml create mode 100644 app/src/main/res/drawable-v24/ic_check_circle_xmr.xml create mode 100644 app/src/main/res/drawable-v24/ic_xmrto_btc_off.xml create mode 100644 app/src/main/res/drawable-v24/ic_xmrto_dash_off.xml create mode 100644 app/src/main/res/drawable-v24/ic_xmrto_doge_off.xml create mode 100644 app/src/main/res/drawable-v24/ic_xmrto_eth_off.xml create mode 100644 app/src/main/res/drawable-v24/ic_xmrto_ltc_off.xml create mode 100644 app/src/main/res/drawable/backgound_all.xml create mode 100644 app/src/main/res/drawable/backgound_seed.xml create mode 100644 app/src/main/res/drawable/backgound_toolbar_mainnet.xml create mode 100644 app/src/main/res/drawable/backgound_toolbar_streetmode.xml create mode 100644 app/src/main/res/drawable/button_green.xml create mode 100644 app/src/main/res/drawable/button_selector_green.xml create mode 100644 app/src/main/res/drawable/dot_dark.xml create mode 100644 app/src/main/res/drawable/dot_light.xml create mode 100644 app/src/main/res/drawable/gradient_all.xml create mode 100644 app/src/main/res/drawable/gradient_oval.xml create mode 100644 app/src/main/res/drawable/gradient_street.xml create mode 100644 app/src/main/res/drawable/gradient_street_efab.xml create mode 100644 app/src/main/res/drawable/gunther_24dp.png create mode 100644 app/src/main/res/drawable/gunther_coder.png create mode 100644 app/src/main/res/drawable/gunther_csi_24dp.png create mode 100644 app/src/main/res/drawable/gunther_desaturated.png create mode 100644 app/src/main/res/drawable/ic_account_balance_wallet_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_add.xml create mode 100644 app/src/main/res/drawable/ic_add_circle.xml create mode 100644 app/src/main/res/drawable/ic_all_inclusive.xml create mode 100644 app/src/main/res/drawable/ic_arrow_back.xml create mode 100644 app/src/main/res/drawable/ic_check_circle.xml create mode 100644 app/src/main/res/drawable/ic_check_circle_xmr.xml create mode 100644 app/src/main/res/drawable/ic_close_white_24dp.xml create mode 100644 app/src/main/res/drawable/ic_content_copy_24dp.xml create mode 100644 app/src/main/res/drawable/ic_content_paste_24dp.xml create mode 100644 app/src/main/res/drawable/ic_done_all.xml create mode 100644 app/src/main/res/drawable/ic_emptygunther.xml create mode 100644 app/src/main/res/drawable/ic_error_red_24dp.xml create mode 100644 app/src/main/res/drawable/ic_error_red_36dp.xml create mode 100644 app/src/main/res/drawable/ic_eye.xml create mode 100644 app/src/main/res/drawable/ic_eye_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_favorite_24dp.xml create mode 100644 app/src/main/res/drawable/ic_favorite_border_24dp.xml create mode 100644 app/src/main/res/drawable/ic_fingerprint.xml create mode 100644 app/src/main/res/drawable/ic_gunther_streetmode.xml create mode 100644 app/src/main/res/drawable/ic_hand.xml create mode 100644 app/src/main/res/drawable/ic_help_white_24dp.xml create mode 100644 app/src/main/res/drawable/ic_import.xml create mode 100644 app/src/main/res/drawable/ic_info_outline_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_info_outline_gray_24dp.xml create mode 100644 app/src/main/res/drawable/ic_info_white_24dp.xml create mode 100644 app/src/main/res/drawable/ic_key.xml create mode 100644 app/src/main/res/drawable/ic_keyboard_arrow_down.xml create mode 100644 app/src/main/res/drawable/ic_keyboard_arrow_up.xml create mode 100644 app/src/main/res/drawable/ic_launch_external.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/ic_ledger_restore.xml create mode 100644 app/src/main/res/drawable/ic_logo_horizontol_xmrujo.xml create mode 100644 app/src/main/res/drawable/ic_monero.xml create mode 100644 app/src/main/res/drawable/ic_monero_bw.xml create mode 100644 app/src/main/res/drawable/ic_monero_logo_b.png create mode 100644 app/src/main/res/drawable/ic_monerujo.xml create mode 100644 app/src/main/res/drawable/ic_navigate_next.xml create mode 100644 app/src/main/res/drawable/ic_navigate_prev.xml create mode 100644 app/src/main/res/drawable/ic_network_clearnet.xml create mode 100644 app/src/main/res/drawable/ic_network_tor_on.xml create mode 100644 app/src/main/res/drawable/ic_new.xml create mode 100644 app/src/main/res/drawable/ic_nfc.xml create mode 100644 app/src/main/res/drawable/ic_onboarding_fingerprint.xml create mode 100644 app/src/main/res/drawable/ic_onboarding_nodes.xml create mode 100644 app/src/main/res/drawable/ic_onboarding_seed.xml create mode 100644 app/src/main/res/drawable/ic_onboarding_welcome.xml create mode 100644 app/src/main/res/drawable/ic_onboarding_xmrto.xml create mode 100644 app/src/main/res/drawable/ic_pending.xml create mode 100644 app/src/main/res/drawable/ic_renew.xml create mode 100644 app/src/main/res/drawable/ic_scan.xml create mode 100644 app/src/main/res/drawable/ic_seed.xml create mode 100644 app/src/main/res/drawable/ic_send.xml create mode 100644 app/src/main/res/drawable/ic_settings.xml create mode 100644 app/src/main/res/drawable/ic_share.xml create mode 100644 app/src/main/res/drawable/ic_sideshift_circle.xml create mode 100644 app/src/main/res/drawable/ic_sideshift_white.xml create mode 100644 app/src/main/res/drawable/ic_smiley_ecstatic_filled.xml create mode 100644 app/src/main/res/drawable/ic_smiley_gunther_filled.xml create mode 100644 app/src/main/res/drawable/ic_smiley_happy_filled.xml create mode 100644 app/src/main/res/drawable/ic_smiley_meh_filled.xml create mode 100644 app/src/main/res/drawable/ic_smiley_neutral_filled.xml create mode 100644 app/src/main/res/drawable/ic_smiley_sad_filled.xml create mode 100644 app/src/main/res/drawable/ic_statsup.xml create mode 100644 app/src/main/res/drawable/ic_success.xml create mode 100644 app/src/main/res/drawable/ic_wifi_1_bar.xml create mode 100644 app/src/main/res/drawable/ic_wifi_2_bar.xml create mode 100644 app/src/main/res/drawable/ic_wifi_3_bar.xml create mode 100644 app/src/main/res/drawable/ic_wifi_4_bar.xml create mode 100644 app/src/main/res/drawable/ic_wifi_lock.xml create mode 100644 app/src/main/res/drawable/ic_wifi_off.xml create mode 100644 app/src/main/res/drawable/ic_xmrto_btc.xml create mode 100644 app/src/main/res/drawable/ic_xmrto_btc_off.xml create mode 100644 app/src/main/res/drawable/ic_xmrto_dash.xml create mode 100644 app/src/main/res/drawable/ic_xmrto_dash_off.xml create mode 100644 app/src/main/res/drawable/ic_xmrto_doge.xml create mode 100644 app/src/main/res/drawable/ic_xmrto_doge_off.xml create mode 100644 app/src/main/res/drawable/ic_xmrto_eth.xml create mode 100644 app/src/main/res/drawable/ic_xmrto_eth_off.xml create mode 100644 app/src/main/res/drawable/ic_xmrto_logo.xml create mode 100644 app/src/main/res/drawable/ic_xmrto_ltc.xml create mode 100644 app/src/main/res/drawable/ic_xmrto_ltc_off.xml create mode 100644 app/src/main/res/drawable/logo_horizontol_xmrujo.png create mode 100644 app/src/main/res/drawable/onboarding_dots.xml create mode 100644 app/src/main/res/drawable/selector_login.xml create mode 100644 app/src/main/res/layout/activity_login.xml create mode 100644 app/src/main/res/layout/activity_on_boarding.xml create mode 100644 app/src/main/res/layout/activity_wallet.xml create mode 100644 app/src/main/res/layout/checkbox_confirm.xml create mode 100644 app/src/main/res/layout/dialog_ledger_progress.xml create mode 100644 app/src/main/res/layout/fragment_about.xml create mode 100644 app/src/main/res/layout/fragment_credits.xml create mode 100644 app/src/main/res/layout/fragment_generate.xml create mode 100644 app/src/main/res/layout/fragment_help.xml create mode 100644 app/src/main/res/layout/fragment_login.xml create mode 100644 app/src/main/res/layout/fragment_node.xml create mode 100644 app/src/main/res/layout/fragment_privacy_policy.xml create mode 100644 app/src/main/res/layout/fragment_receive.xml create mode 100644 app/src/main/res/layout/fragment_review.xml create mode 100644 app/src/main/res/layout/fragment_send.xml create mode 100644 app/src/main/res/layout/fragment_send_address.xml create mode 100644 app/src/main/res/layout/fragment_send_amount.xml create mode 100644 app/src/main/res/layout/fragment_send_btc_amount.xml create mode 100644 app/src/main/res/layout/fragment_send_btc_confirm.xml create mode 100644 app/src/main/res/layout/fragment_send_btc_success.xml create mode 100644 app/src/main/res/layout/fragment_send_confirm.xml create mode 100644 app/src/main/res/layout/fragment_send_success.xml create mode 100644 app/src/main/res/layout/fragment_subaddress.xml create mode 100644 app/src/main/res/layout/fragment_subaddressinfo.xml create mode 100644 app/src/main/res/layout/fragment_tx_info.xml create mode 100644 app/src/main/res/layout/fragment_wallet.xml create mode 100644 app/src/main/res/layout/item_node.xml create mode 100644 app/src/main/res/layout/item_spinner.xml create mode 100644 app/src/main/res/layout/item_spinner_balance.xml create mode 100644 app/src/main/res/layout/item_spinner_dropdown_item.xml create mode 100644 app/src/main/res/layout/item_subaddress.xml create mode 100644 app/src/main/res/layout/item_transaction.xml create mode 100644 app/src/main/res/layout/item_wallet.xml create mode 100644 app/src/main/res/layout/layout_fabmenu.xml create mode 100644 app/src/main/res/layout/nav_header.xml create mode 100644 app/src/main/res/layout/prompt_changepw.xml create mode 100644 app/src/main/res/layout/prompt_editnode.xml create mode 100644 app/src/main/res/layout/prompt_ledger_seed.xml create mode 100644 app/src/main/res/layout/prompt_password.xml create mode 100644 app/src/main/res/layout/prompt_rename.xml create mode 100644 app/src/main/res/layout/template_notice.xml create mode 100644 app/src/main/res/layout/view_exchange.xml create mode 100644 app/src/main/res/layout/view_exchange_edit.xml create mode 100644 app/src/main/res/layout/view_onboarding.xml create mode 100644 app/src/main/res/layout/view_send_progress.xml create mode 100644 app/src/main/res/layout/view_toolbar.xml create mode 100644 app/src/main/res/menu/create_wallet_keys.xml create mode 100644 app/src/main/res/menu/create_wallet_ledger.xml create mode 100644 app/src/main/res/menu/create_wallet_new.xml create mode 100644 app/src/main/res/menu/create_wallet_seed.xml create mode 100644 app/src/main/res/menu/create_wallet_view.xml create mode 100644 app/src/main/res/menu/drawer_view.xml create mode 100644 app/src/main/res/menu/list_context_menu.xml create mode 100644 app/src/main/res/menu/list_menu.xml create mode 100644 app/src/main/res/menu/node_menu.xml create mode 100644 app/src/main/res/menu/receive_menu.xml create mode 100644 app/src/main/res/menu/send_menu.xml create mode 100644 app/src/main/res/menu/tx_info_menu.xml create mode 100644 app/src/main/res/menu/wallet_details_help_menu.xml create mode 100644 app/src/main/res/menu/wallet_details_menu.xml create mode 100644 app/src/main/res/menu/wallet_menu.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/transition/details.xml create mode 100644 app/src/main/res/values-cat/about.xml create mode 100644 app/src/main/res/values-cat/help.xml create mode 100644 app/src/main/res/values-cat/strings.xml create mode 100644 app/src/main/res/values-de/about.xml create mode 100644 app/src/main/res/values-de/help.xml create mode 100644 app/src/main/res/values-de/strings.xml create mode 100644 app/src/main/res/values-el/about.xml create mode 100644 app/src/main/res/values-el/help.xml create mode 100644 app/src/main/res/values-el/strings.xml create mode 100644 app/src/main/res/values-eo/about.xml create mode 100644 app/src/main/res/values-eo/help.xml create mode 100644 app/src/main/res/values-eo/strings.xml create mode 100644 app/src/main/res/values-es/about.xml create mode 100644 app/src/main/res/values-es/help.xml create mode 100644 app/src/main/res/values-es/strings.xml create mode 100644 app/src/main/res/values-et/about.xml create mode 100644 app/src/main/res/values-et/help.xml create mode 100644 app/src/main/res/values-et/strings.xml create mode 100644 app/src/main/res/values-fa/about.xml create mode 100644 app/src/main/res/values-fa/help.xml create mode 100644 app/src/main/res/values-fa/strings.xml create mode 100644 app/src/main/res/values-fr/about.xml create mode 100644 app/src/main/res/values-fr/help.xml create mode 100644 app/src/main/res/values-fr/strings.xml create mode 100644 app/src/main/res/values-hu/about.xml create mode 100644 app/src/main/res/values-hu/help.xml create mode 100644 app/src/main/res/values-hu/strings.xml create mode 100644 app/src/main/res/values-it/about.xml create mode 100644 app/src/main/res/values-it/help.xml create mode 100644 app/src/main/res/values-it/strings.xml create mode 100644 app/src/main/res/values-ja/about.xml create mode 100644 app/src/main/res/values-ja/help.xml create mode 100644 app/src/main/res/values-ja/strings.xml create mode 100644 app/src/main/res/values-nb/about.xml create mode 100644 app/src/main/res/values-nb/help.xml create mode 100644 app/src/main/res/values-nb/strings.xml create mode 100644 app/src/main/res/values-night-v23/styles.xml create mode 100644 app/src/main/res/values-night-v27/styles.xml create mode 100644 app/src/main/res/values-night/colors.xml create mode 100644 app/src/main/res/values-night/styles.xml create mode 100644 app/src/main/res/values-nl/about.xml create mode 100644 app/src/main/res/values-nl/help.xml create mode 100644 app/src/main/res/values-nl/strings.xml create mode 100755 app/src/main/res/values-pt-rBR/about.xml create mode 100755 app/src/main/res/values-pt-rBR/help.xml create mode 100755 app/src/main/res/values-pt-rBR/strings.xml create mode 100644 app/src/main/res/values-pt/about.xml create mode 100644 app/src/main/res/values-pt/help.xml create mode 100644 app/src/main/res/values-pt/strings.xml create mode 100644 app/src/main/res/values-ro/about.xml create mode 100644 app/src/main/res/values-ro/help.xml create mode 100644 app/src/main/res/values-ro/strings.xml create mode 100644 app/src/main/res/values-ru/about.xml create mode 100644 app/src/main/res/values-ru/help.xml create mode 100644 app/src/main/res/values-ru/strings.xml create mode 100644 app/src/main/res/values-sk/about.xml create mode 100644 app/src/main/res/values-sk/help.xml create mode 100644 app/src/main/res/values-sk/strings.xml create mode 100644 app/src/main/res/values-sr/about.xml create mode 100644 app/src/main/res/values-sr/help.xml create mode 100644 app/src/main/res/values-sr/strings.xml create mode 100644 app/src/main/res/values-sv/about.xml create mode 100644 app/src/main/res/values-sv/help.xml create mode 100644 app/src/main/res/values-sv/strings.xml create mode 100644 app/src/main/res/values-ta/about.xml create mode 100644 app/src/main/res/values-ta/help.xml create mode 100644 app/src/main/res/values-ta/strings.xml create mode 100644 app/src/main/res/values-uk/about.xml create mode 100644 app/src/main/res/values-uk/help.xml create mode 100644 app/src/main/res/values-uk/strings.xml create mode 100644 app/src/main/res/values-v23/styles.xml create mode 100644 app/src/main/res/values-v27/styles.xml create mode 100644 app/src/main/res/values-w1240dp/dimens.xml create mode 100644 app/src/main/res/values-w600dp/dimens.xml create mode 100644 app/src/main/res/values-zh-rCN/about.xml create mode 100644 app/src/main/res/values-zh-rCN/help.xml create mode 100644 app/src/main/res/values-zh-rCN/strings.xml create mode 100644 app/src/main/res/values-zh-rTW/about.xml create mode 100644 app/src/main/res/values-zh-rTW/help.xml create mode 100644 app/src/main/res/values-zh-rTW/strings.xml create mode 100644 app/src/main/res/values/about.xml create mode 100644 app/src/main/res/values/arrays.xml create mode 100644 app/src/main/res/values/attrs.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/dimens.xml create mode 100644 app/src/main/res/values/help.xml create mode 100644 app/src/main/res/values/ids.xml create mode 100644 app/src/main/res/values/integers.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/main/res/xml/filepaths.xml create mode 100644 app/src/main/res/xml/root_preferences.xml create mode 100644 app/src/main/res/xml/usb_device_filter.xml create mode 100644 app/src/stagenet/res/values/strings.xml create mode 100644 app/src/test/java/com/m2049r/xmrwallet/service/exchange/ecb/ExchangeRateTest.java create mode 100644 app/src/test/java/com/m2049r/xmrwallet/service/exchange/kraken/ExchangeRateTest.java create mode 100644 app/src/test/java/com/m2049r/xmrwallet/service/shift/sideshift/SideShiftApiCreateOrderTest.java create mode 100644 app/src/test/java/com/m2049r/xmrwallet/service/shift/sideshift/SideShiftApiOrderParameterTest.java create mode 100644 app/src/test/java/com/m2049r/xmrwallet/service/shift/sideshift/SideShiftApiQueryOrderStatusTest.java create mode 100644 app/src/test/java/com/m2049r/xmrwallet/service/shift/sideshift/SideShiftApiRequestQuoteTest.java create mode 100644 app/src/test/java/com/m2049r/xmrwallet/util/HelperTest.java create mode 100644 app/src/test/java/com/m2049r/xmrwallet/util/OpenAliasHelperTest.java create mode 100644 app/src/test/java/com/m2049r/xmrwallet/util/RestoreHeightTest.java create mode 100644 app/src/test/java/com/m2049r/xmrwallet/util/UserNoteTest.java create mode 100644 app/src/test/java/com/m2049r/xmrwallet/util/ledger/MoneroTest.java create mode 100644 app/src/test/java/com/m2049r/xmrwallet/util/validator/BitcoinAddressValidatorTest.java create mode 100644 app/src/test/java/com/m2049r/xmrwallet/util/validator/EthAddressValidatorTest.java create mode 100644 build.gradle create mode 100644 doc/BUILDING-external-libs.md create mode 100644 doc/FAQ.md create mode 100644 external-libs/.gitignore create mode 100644 external-libs/Makefile create mode 100644 external-libs/VERSION create mode 100644 external-libs/android32.Dockerfile create mode 100644 external-libs/android32_x86.Dockerfile create mode 100644 external-libs/android64.Dockerfile create mode 100644 external-libs/android64_x86.Dockerfile create mode 100644 external-libs/include/wallet2_api.h create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 privacy-policy.md create mode 100644 settings.gradle diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..b939051 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,27 @@ +version: 2 +jobs: + build: + working_directory: ~/code + docker: + - image: cimg/android:2022.03-ndk + environment: + JVM_OPTS: -Xmx3200m + steps: + - checkout + - restore_cache: + key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }} + - run: + name: Download Dependencies + command: ./gradlew androidDependencies + - save_cache: + paths: + - ~/.gradle + key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }} + - run: + name: Run Tests + command: ./gradlew test + - store_artifacts: + path: app/build/reports + destination: reports + - store_test_results: + path: app/build/test-results diff --git a/.gitignore b/.gitignore index a1c2a23..679ff4d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,18 @@ -# Compiled class file -*.class - -# Log file -*.log - -# BlueJ files -*.ctxt - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar - -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* +.gradle +/build +*.iml +/.idea +/local.properties +/captures +.externalNativeBuild +.DS_Store +/app/build +/app/release +/app/alpha +/app/prod +/app/alphaMainnet +/app/prodMainnet +/app/alphaStagenet +/app/prodStagenet +/app/.cxx +/monerujo.id diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt new file mode 100644 index 0000000..08904bf --- /dev/null +++ b/app/CMakeLists.txt @@ -0,0 +1,239 @@ +cmake_minimum_required(VERSION 3.4.1) +message(STATUS ABI_INFO = ${ANDROID_ABI}) + +add_library( monerujo + SHARED + src/main/cpp/monerujo.cpp ) + +set(EXTERNAL_LIBS_DIR ${CMAKE_SOURCE_DIR}/../external-libs) + +############ +# libsodium +############ + +add_library(sodium STATIC IMPORTED) +set_target_properties(sodium PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/libsodium.a) + +############ +# OpenSSL +############ + +add_library(crypto STATIC IMPORTED) +set_target_properties(crypto PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/libcrypto.a) + +add_library(ssl STATIC IMPORTED) +set_target_properties(ssl PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/libssl.a) + +############ +# Boost +############ + +add_library(boost_chrono STATIC IMPORTED) +set_target_properties(boost_chrono PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/libboost_chrono.a) + +add_library(boost_date_time STATIC IMPORTED) +set_target_properties(boost_date_time PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/libboost_date_time.a) + +add_library(boost_filesystem STATIC IMPORTED) +set_target_properties(boost_filesystem PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/libboost_filesystem.a) + +add_library(boost_program_options STATIC IMPORTED) +set_target_properties(boost_program_options PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/libboost_program_options.a) + +add_library(boost_regex STATIC IMPORTED) +set_target_properties(boost_regex PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/libboost_regex.a) + +add_library(boost_serialization STATIC IMPORTED) +set_target_properties(boost_serialization PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/libboost_serialization.a) + +add_library(boost_system STATIC IMPORTED) +set_target_properties(boost_system PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/libboost_system.a) + +add_library(boost_thread STATIC IMPORTED) +set_target_properties(boost_thread PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/libboost_thread.a) + +add_library(boost_wserialization STATIC IMPORTED) +set_target_properties(boost_wserialization PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/libboost_wserialization.a) + +############# +# Monero +############# + +add_library(wallet_api STATIC IMPORTED) +set_target_properties(wallet_api PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libwallet_api.a) + +add_library(wallet STATIC IMPORTED) +set_target_properties(wallet PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libwallet.a) + +add_library(cryptonote_core STATIC IMPORTED) +set_target_properties(cryptonote_core PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libcryptonote_core.a) + +add_library(cryptonote_basic STATIC IMPORTED) +set_target_properties(cryptonote_basic PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libcryptonote_basic.a) + +add_library(mnemonics STATIC IMPORTED) +set_target_properties(mnemonics PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libmnemonics.a) + +add_library(common STATIC IMPORTED) +set_target_properties(common PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libcommon.a) + +add_library(cncrypto STATIC IMPORTED) +set_target_properties(cncrypto PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libcncrypto.a) + +add_library(ringct STATIC IMPORTED) +set_target_properties(ringct PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libringct.a) + +add_library(ringct_basic STATIC IMPORTED) +set_target_properties(ringct_basic PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libringct_basic.a) + +add_library(blockchain_db STATIC IMPORTED) +set_target_properties(blockchain_db PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libblockchain_db.a) + +add_library(lmdb STATIC IMPORTED) +set_target_properties(lmdb PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/liblmdb.a) + +add_library(easylogging STATIC IMPORTED) +set_target_properties(easylogging PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libeasylogging.a) + +add_library(unbound STATIC IMPORTED) +set_target_properties(unbound PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/libunbound.a) + +add_library(epee STATIC IMPORTED) +set_target_properties(epee PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libepee.a) + +add_library(blocks STATIC IMPORTED) +set_target_properties(blocks PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libblocks.a) + +add_library(checkpoints STATIC IMPORTED) +set_target_properties(checkpoints PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libcheckpoints.a) + +add_library(device STATIC IMPORTED) +set_target_properties(device PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libdevice.a) + +add_library(device_trezor STATIC IMPORTED) +set_target_properties(device_trezor PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libdevice_trezor.a) + +add_library(multisig STATIC IMPORTED) +set_target_properties(multisig PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libmultisig.a) + +add_library(version STATIC IMPORTED) +set_target_properties(version PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libversion.a) + +add_library(net STATIC IMPORTED) +set_target_properties(net PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libnet.a) + +add_library(hardforks STATIC IMPORTED) +set_target_properties(hardforks PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libhardforks.a) + +add_library(randomx STATIC IMPORTED) +set_target_properties(randomx PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/librandomx.a) + +add_library(rpc_base STATIC IMPORTED) +set_target_properties(rpc_base PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/librpc_base.a) + +add_library(wallet-crypto STATIC IMPORTED) +set_target_properties(wallet-crypto PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libwallet-crypto.a) + +add_library(cryptonote_format_utils_basic STATIC IMPORTED) +set_target_properties(cryptonote_format_utils_basic PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libcryptonote_format_utils_basic.a) + +############# +# System +############# + +find_library( log-lib log ) + +include_directories( ${EXTERNAL_LIBS_DIR}/include ) + +message(STATUS EXTERNAL_LIBS_DIR : ${EXTERNAL_LIBS_DIR}) + +if(${ANDROID_ABI} STREQUAL "x86_64") + set(EXTRA_LIBS "wallet-crypto") +else() + set(EXTRA_LIBS "") +endif() + +target_link_libraries( monerujo + + wallet_api + wallet + cryptonote_core + cryptonote_basic + cryptonote_format_utils_basic + mnemonics + ringct + ringct_basic + net + common + cncrypto + blockchain_db + lmdb + easylogging + unbound + epee + blocks + checkpoints + device + device_trezor + multisig + version + randomx + hardforks + rpc_base + ${EXTRA_LIBS} + + boost_chrono + boost_date_time + boost_filesystem + boost_program_options + boost_regex + boost_serialization + boost_system + boost_thread + boost_wserialization + + ssl + crypto + + sodium + + ${log-lib} + ) diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..40e250f --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,159 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 31 + buildToolsVersion '30.0.3' + ndkVersion '17.2.4988734' + defaultConfig { + applicationId "com.m2049r.xmrwallet" + minSdkVersion 21 + targetSdkVersion 31 + versionCode 3002 + versionName "3.0.2 'Fluorine Fermi'" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + externalNativeBuild { + cmake { + cppFlags "-std=c++11" + arguments '-DANDROID_STL=c++_shared' + } + } + } + bundle { + language { + enableSplit = false + } + } + + flavorDimensions 'type', 'net' + productFlavors { + mainnet { + dimension 'net' + } + stagenet { + dimension 'net' + applicationIdSuffix '.stage' + versionNameSuffix ' (stage)' + } + devnet { + dimension 'net' + applicationIdSuffix '.test' + versionNameSuffix ' (test)' + } + alpha { + dimension 'type' + applicationIdSuffix '.alpha' + versionNameSuffix ' (alpha)' + } + prod { + dimension 'type' + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + debug { + applicationIdSuffix ".debug" + } + applicationVariants.all { variant -> + variant.buildConfigField "String", "ID_A", "\"" + getId("ID_A") + "\"" + } + } + + externalNativeBuild { + cmake { + path "CMakeLists.txt" + } + } + + splits { + abi { + enable true + reset() + include 'armeabi-v7a', 'arm64-v8a', 'x86_64' + universalApk true + } + } + + // Map for the version code that gives each ABI a value. + def abiCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4] + + // Enumerate translated locales + def availableLocales = ["en"] + new File("app/src/main/res/").eachFileMatch(~/^values-.*/) { file -> + def languageTag = file.name.substring(7).replace("-r", "-") + if (languageTag != "night") + availableLocales.add(languageTag) + } + + // APKs for the same app that all have the same version information. + android.applicationVariants.all { variant -> + // Update string resource: available_locales + variant.resValue("string", "available_locales", availableLocales.join(",")) + // Assigns a different version code for each output APK. + variant.outputs.all { + output -> + def abiName = output.getFilter(com.android.build.OutputFile.ABI) + output.versionCodeOverride = abiCodes.get(abiName, 0) + 10 * versionCode + + if (abiName == null) abiName = "universal" + def v = "${variant.versionName}".replaceFirst(" '.*' ?", "") + .replace(".", "x") + .replace("(", "-") + .replace(")", "") + outputFileName = "$rootProject.ext.apkName-" + v + "_" + abiName + ".apk" + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + namespace 'com.m2049r.xmrwallet' +} + +static def getId(name) { + Properties props = new Properties() + props.load(new FileInputStream(new File('monerujo.id'))) + return props[name] +} + +dependencies { + implementation 'androidx.core:core:1.7.0' + implementation 'androidx.appcompat:appcompat:1.4.1' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'androidx.recyclerview:recyclerview:1.2.1' + implementation 'androidx.cardview:cardview:1.0.0' + implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.3' + implementation 'androidx.preference:preference:1.2.0' + + implementation 'com.google.android.material:material:1.6.0' + + implementation 'me.dm7.barcodescanner:zxing:1.9.8' + implementation "com.squareup.okhttp3:okhttp:4.9.3" + implementation "io.github.rburgst:okhttp-digest:2.6" + implementation "com.jakewharton.timber:timber:5.0.1" + + implementation 'info.guardianproject.netcipher:netcipher:2.1.0' + //implementation 'info.guardianproject.netcipher:netcipher-okhttp3:2.1.0' + implementation fileTree(dir: 'libs/classes', include: ['*.jar']) + implementation 'com.nulab-inc:zxcvbn:1.5.2' + + implementation 'dnsjava:dnsjava:2.1.9' + implementation 'org.jitsi:dnssecjava:1.2.0' + implementation 'org.slf4j:slf4j-nop:1.7.36' + implementation 'com.github.brnunes:swipeablerecyclerview:1.0.2' + + //noinspection GradleDependency + testImplementation "junit:junit:4.13.2" + testImplementation "org.mockito:mockito-all:1.10.19" + testImplementation "com.squareup.okhttp3:mockwebserver:4.9.3" + testImplementation 'org.json:json:20211205' + testImplementation 'net.jodah:concurrentunit:0.4.6' + + compileOnly 'org.projectlombok:lombok:1.18.22' + annotationProcessor 'org.projectlombok:lombok:1.18.22' +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..cb4574a --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,25 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in C:\Users\Test\AppData\Local\Android\Sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/src/alpha/ic_launcher-web.png b/app/src/alpha/ic_launcher-web.png new file mode 100644 index 0000000000000000000000000000000000000000..f34cb0a4df3d23219bc08ff72d1244a2c6a771be GIT binary patch literal 249717 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%0Dkj*kb+jUx7k`Gs^G>5?bo(%hJ4Qc&q4jth#yp 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

Open Source Licenses

+

Licensed under the Apache License, Version 2.0

+

monerujo (https://github.com/m2049r/xmrwallet)

+Copyright (c) 2017-2018 m2049r et al. + +

The Android Open Source Project

+
    +
  • com.android.support:design
  • +
  • com.android.support:support-v4
  • +
  • com.android.support:appcompat-v7
  • +
  • com.android.support:recyclerview-v7
  • +
  • com.android.support:cardview-v7
  • +
  • com.android.support.constraint:constraint-layout
  • +
  • com.android.support:support-annotations
  • +
  • com.android.support:support-vector-drawable
  • +
  • com.android.support:animated-vector-drawable
  • +
  • com.android.support:transition
  • +
  • com.android.support:support-compat
  • +
  • com.android.support:support-media-compat
  • +
  • com.android.support:support-core-utils
  • +
  • com.android.support:support-core-ui
  • +
  • com.android.support:support-fragment
  • +
  • com.android.support.constraint:constraint-layout-solver
  • +
+Copyright (c) The Android Open Source Project + +

OkHttp

+Copyright (c) 2014 Square, Inc. + +

Timber

+Copyright (c) 2013 Jake Wharton + +

com.google.zxing:core

+Copyright (c) 2012 ZXing authors + +

me.dm7.barcodescanner

+
    +
  • me.dm7.barcodescanner:core
  • +
  • me.dm7.barcodescanner:zxing
  • +
+Copyright (c) 2014 Dushyanth Maguluru + +

AndroidLicensesPage (https://github.com/adamsp/AndroidLicensesPage)

+Copyright (c) 2013 Adam Speakman + +

SwipeableRecyclerView (https://github.com/brnunes/SwipeableRecyclerView)

+Copyright (c) 2015 Bruno R. Nunes + +

Apache License, Version 2.0, January 2004

+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction, +and distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by +the copyright owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all +other entities that control, are controlled by, or are under common +control with that entity. For the purposes of this definition, +"control" means (i) the power, direct or indirect, to cause the +direction or management of such entity, whether by contract or +otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity +exercising permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications, +including but not limited to software source code, documentation +source, and configuration files.
+
+"Object" form shall mean any form resulting from mechanical +transformation or translation of a Source form, including but +not limited to compiled object code, generated documentation, +and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or +Object form, made available under the License, as indicated by a +copyright notice that is included in or attached to the work +(an example is provided in the Appendix below).
+
+"Derivative Works" shall mean any work, whether in Source or Object +form, that is based on (or derived from) the Work and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. For the purposes +of this License, Derivative Works shall not include works that remain +separable from, or merely link (or bind by name) to the interfaces of, +the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including +the original version of the Work and any modifications or additions +to that Work or Derivative Works thereof, that is intentionally +submitted to Licensor for inclusion in the Work by the copyright owner +or by an individual or Legal Entity authorized to submit on behalf of +the copyright owner. For the purposes of this definition, "submitted" +means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, +and issue tracking systems that are managed by, or on behalf of, the +Licensor for the purpose of discussing and improving the Work, but +excluding communication that is conspicuously marked or otherwise +designated in writing by the copyright owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity +on behalf of whom a Contribution has been received by Licensor and +subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the +Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +(except as stated in this section) patent license to make, have made, +use, offer to sell, sell, import, and otherwise transfer the Work, +where such license applies only to those patent claims licensable +by such Contributor that are necessarily infringed by their +Contribution(s) alone or by combination of their Contribution(s) +with the Work to which such Contribution(s) was submitted. If You +institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work +or a Contribution incorporated within the Work constitutes direct +or contributory patent infringement, then any patent licenses +granted to You under this License for that Work shall terminate +as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the +Work or Derivative Works thereof in any medium, with or without +modifications, and in Source or Object form, provided that You +meet the following conditions:
+
+(a) You must give any other recipients of the Work or +Derivative Works a copy of this License; and
+
+(b) You must cause any modified files to carry prominent notices +stating that You changed the files; and
+
+(c) You must retain, in the Source form of any Derivative Works +that You distribute, all copyright, patent, trademark, and +attribution notices from the Source form of the Work, +excluding those notices that do not pertain to any part of +the Derivative Works; and
+
+(d) If the Work includes a "NOTICE" text file as part of its +distribution, then any Derivative Works that You distribute must +include a readable copy of the attribution notices contained +within such NOTICE file, excluding those notices that do not +pertain to any part of the Derivative Works, in at least one +of the following places: within a NOTICE text file distributed +as part of the Derivative Works; within the Source form or +documentation, if provided along with the Derivative Works; or, +within a display generated by the Derivative Works, if and +wherever such third-party notices normally appear. The contents +of the NOTICE file are for informational purposes only and +do not modify the License. You may add Your own attribution +notices within Derivative Works that You distribute, alongside +or as an addendum to the NOTICE text from the Work, provided +that such additional attribution notices cannot be construed +as modifying the License.
+
+You may add Your own copyright statement to Your modifications and +may provide additional or different license terms and conditions +for use, reproduction, or distribution of Your modifications, or +for any such Derivative Works as a whole, provided Your use, +reproduction, and distribution of the Work otherwise complies with +the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise, +any Contribution intentionally submitted for inclusion in the Work +by You to the Licensor shall be under the terms and conditions of +this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify +the terms of any separate license agreement you may have executed +with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade +names, trademarks, service marks, or product names of the Licensor, +except as required for reasonable and customary use in describing the +origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or +agreed to in writing, Licensor provides the Work (and each +Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied, including, without limitation, any warranties or conditions +of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +PARTICULAR PURPOSE. You are solely responsible for determining the +appropriateness of using or redistributing the Work and assume any +risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory, +whether in tort (including negligence), contract, or otherwise, +unless required by applicable law (such as deliberate and grossly +negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, +incidental, or consequential damages of any character arising as a +result of this License or out of the use or inability to use the +Work (including but not limited to damages for loss of goodwill, +work stoppage, computer failure or malfunction, or any and all +other commercial damages or losses), even if such Contributor +has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing +the Work or Derivative Works thereof, You may choose to offer, +and charge a fee for, acceptance of support, warranty, indemnity, +or other liability obligations and/or rights consistent with this +License. However, in accepting such obligations, You may act only +on Your own behalf and on Your sole responsibility, not on behalf +of any other Contributor, and only if You agree to indemnify, +defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason +of your accepting any such warranty or additional liability. + +

dnsjava (http://dnsjava.org/)

+Copyright (c) 1998-2011, Brian Wellington. All rights reserved.
+

The 2-Clause BSD License

+
+Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +

dnssecjava - a DNSSEC validating stub resolver for Java

+Copyright (c) 2013-2015 Ingo Bauersachs +

The Eclipse Public License - v 1.0

+THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC +LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM +CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
+
+1. DEFINITIONS
+
+"Contribution" means:
+
+a) in the case of the initial Contributor, the initial code and documentation +distributed under this Agreement, and
+
+b) in the case of each subsequent Contributor:
+
+i) changes to the Program, and
+
+ii) additions to the Program;
+
+where such changes and/or additions to the Program originate from and are distributed +by that particular Contributor. A Contribution 'originates' from a Contributor if it was +added to the Program by such Contributor itself or anyone acting on such Contributor's +behalf. Contributions do not include additions to the Program which: (i) are separate modules +of software distributed in conjunction with the Program under their own license agreement, and +(ii) are not derivative works of the Program.
+
+"Contributor" means any person or entity that distributes the Program.
+
+"Licensed Patents" mean patent claims licensable by a Contributor which are necessarily +infringed by the use or sale of its Contribution alone or when combined with the Program.
+
+"Program" means the Contributions distributed in accordance with this Agreement.
+
+"Recipient" means anyone who receives the Program under this Agreement, including all +Contributors.
+
+2. GRANT OF RIGHTS
+
+a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a +non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative +works of, publicly display, publicly perform, distribute and sublicense the Contribution of +such Contributor, if any, and such derivative works, in source code and object code form.
+
+b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a +non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, +sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, +in source code and object code form. This patent license shall apply to the combination of +the Contribution and the Program if, at the time the Contribution is added by the Contributor, +such addition of the Contribution causes such combination to be covered by the Licensed Patents. +The patent license shall not apply to any other combinations which include the Contribution. +No hardware per se is licensed hereunder.
+
+c) Recipient understands that although each Contributor grants the licenses to its Contributions +set forth herein, no assurances are provided by any Contributor that the Program does not +infringe the patent or other intellectual property rights of any other entity. Each Contributor +disclaims any liability to Recipient for claims brought by any other entity based on +infringement of intellectual property rights or otherwise. As a condition to exercising the +rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to +secure any other intellectual property rights needed, if any. For example, if a third party +patent license is required to allow Recipient to distribute the Program, it is Recipient's +responsibility to acquire that license before distributing the Program.
+
+d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its +Contribution, if any, to grant the copyright license set forth in this Agreement.
+
+3. REQUIREMENTS
+
+A Contributor may choose to distribute the Program in object code form under its own license +agreement, provided that:
+
+a) it complies with the terms and conditions of this Agreement; and
+
+b) its license agreement:
+
+i) effectively disclaims on behalf of all Contributors all warranties and conditions, +express and implied, including warranties or conditions of title and non-infringement, +and implied warranties or conditions of merchantability and fitness for a particular purpose;
+
+ii) effectively excludes on behalf of all Contributors all liability for damages, including +direct, indirect, special, incidental and consequential damages, such as lost profits;
+
+iii) states that any provisions which differ from this Agreement are offered by that Contributor +alone and not by any other party; and
+
+iv) states that source code for the Program is available from such Contributor, and informs +licensees how to obtain it in a reasonable manner on or through a medium customarily used for +software exchange.
+
+When the Program is made available in source code form:
+
+a) it must be made available under this Agreement; and
+
+b) a copy of this Agreement must be included with each copy of the Program.
+
+Contributors may not remove or alter any copyright notices contained within the Program.
+
+Each Contributor must identify itself as the originator of its Contribution, if any, in a +manner that reasonably allows subsequent Recipients to identify the originator of the +Contribution.
+
+4. COMMERCIAL DISTRIBUTION
+
+Commercial distributors of software may accept certain responsibilities with respect to end +users, business partners and the like. While this license is intended to facilitate the +commercial use of the Program, the Contributor who includes the Program in a commercial +product offering should do so in a manner which does not create potential liability for +other Contributors. Therefore, if a Contributor includes the Program in a commercial +product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and +indemnify every other Contributor ("Indemnified Contributor") against any losses, damages +and costs (collectively "Losses") arising from claims, lawsuits and other legal actions +brought by a third party against the Indemnified Contributor to the extent caused by the +acts or omissions of such Commercial Contributor in connection with its distribution of the +Program in a commercial product offering. The obligations in this section do not apply to +any claims or Losses relating to any actual or alleged intellectual property infringement. +In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial +Contributor in writing of such claim, and b) allow the Commercial Contributor to control, +and cooperate with the Commercial Contributor in, the defense and any related settlement +negotiations. The Indemnified Contributor may participate in any such claim at its own expense.
+
+For example, a Contributor might include the Program in a commercial product offering, +Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor +then makes performance claims, or offers warranties related to Product X, those performance +claims and warranties are such Commercial Contributor's responsibility alone. Under this +section, the Commercial Contributor would have to defend claims against the other Contributors +related to those performance claims and warranties, and if a court requires any other Contributor +to pay any damages as a result, the Commercial Contributor must pay those damages.
+
+5. NO WARRANTY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT +LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR +FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the +appropriateness of using and distributing the Program and assumes all risks associated with +its exercise of rights under this Agreement , including but not limited to the risks and costs +of program errors, compliance with applicable laws, damage to or loss of data, programs or +equipment, and unavailability or interruption of operations.
+
+6. DISCLAIMER OF LIABILITY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL +HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS +GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+7. GENERAL
+
+If any provision of this Agreement is invalid or unenforceable under applicable law, it shall +not affect the validity or enforceability of the remainder of the terms of this Agreement, +and without further action by the parties hereto, such provision shall be reformed to the +minimum extent necessary to make such provision valid and enforceable.
+
+If Recipient institutes patent litigation against any entity (including a cross-claim or +counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the +Program with other software or hardware) infringes such Recipient's patent(s), then such +Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation +is filed.
+
+All Recipient's rights under this Agreement shall terminate if it fails to comply with any of +the material terms or conditions of this Agreement and does not cure such failure in a +reasonable period of time after becoming aware of such noncompliance. If all Recipient's +rights under this Agreement terminate, Recipient agrees to cease use and distribution of the +Program as soon as reasonably practicable. However, Recipient's obligations under this +Agreement and any licenses granted by Recipient relating to the Program shall continue +and survive.
+
+Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid +inconsistency the Agreement is copyrighted and may only be modified in the following manner. +The Agreement Steward reserves the right to publish new versions (including revisions) +of this Agreement from time to time. No one other than the Agreement Steward has the +right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. +The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to +a suitable separate entity. Each new version of the Agreement will be given a distinguishing +version number. The Program (including Contributions) may always be distributed subject to +the version of the Agreement under which it was received. In addition, after a new version +of the Agreement is published, Contributor may elect to distribute the Program (including +its Contributions) under the new version. Except as expressly stated in Sections 2(a) and +2(b) above, Recipient receives no rights or licenses to the intellectual property of any +Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. +All rights in the Program not expressly granted under this Agreement are reserved.
+
+This Agreement is governed by the laws of the State of New York and the intellectual property +laws of the United States of America. No party to this Agreement will bring a legal action +under this Agreement more than one year after the cause of action arose. Each party waives +its rights to a jury trial in any resulting litigation. + +

Licensed under the MIT License

+

rapidjson (https://github.com/monero-project/monero/blob/master/external/rapidjson)

+Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. All rights reserved. +

easylogging++ (https://github.com/monero-project/monero/tree/master/external/easylogging%2B%2B)

+Copyright (c) 2017 muflihun.com +

zxcvbn4j (https://github.com/nulab/zxcvbn4j)

+Copyright (c) 2014 Nulab Inc +

slfj-nop - Simple Logging Facade for Java no-operation binding (https://www.slf4j.org/)

+Copyright (c) 2004-2017 QOS.ch +

The MIT License

+Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ +

Monero (https://github.com/monero-project/monero)

+

The Monero Project License

+Copyright (c) 2014-2017, The Monero Project. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+Parts of the project are originally copyright (c) 2012-2013 The Cryptonote +developers + +

OpenSSL (https://github.com/openssl/openssl)

+

LICENSE ISSUES

+The OpenSSL toolkit stays under a double license, i.e. both the conditions of +the OpenSSL License and the original SSLeay license apply to the toolkit. +See below for the actual license texts. +

OpenSSL License

+Copyright (c) 1998-2017 The OpenSSL Project. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met:
+1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in +the documentation and/or other materials provided with the +distribution.
+
+3. All advertising materials mentioning features or use of this +software must display the following acknowledgment: +"This product includes software developed by the OpenSSL Project +for use in the OpenSSL Toolkit. (http://www.openssl.org/)"
+
+4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to +endorse or promote products derived from this software without +prior written permission. For written permission, please contact +openssl-core@openssl.org.
+
+5. Products derived from this software may not be called "OpenSSL" +nor may "OpenSSL" appear in their names without prior written +permission of the OpenSSL Project.
+
+6. Redistributions of any form whatsoever must retain the following +acknowledgment:
+"This product includes software developed by the OpenSSL Project +for use in the OpenSSL Toolkit (http://www.openssl.org/)"
+
+THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT "AS IS" AND ANY +EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR +ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED +OF THE POSSIBILITY OF SUCH DAMAGE.
+
+This product includes cryptographic software written by Eric Young +(eay@cryptsoft.com). This product includes software written by Tim +Hudson (tjh@cryptsoft.com). +

Original SSLeay License

+Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com). All rights reserved.
+
+This package is an SSL implementation written +by Eric Young (eay@cryptsoft.com). +The implementation was written so as to conform with Netscapes SSL.
+
+This library is free for commercial and non-commercial use as long as +the following conditions are aheared to. The following conditions +apply to all code found in this distribution, be it the RC4, RSA, +lhash, DES, etc., code; not just the SSL code. The SSL documentation +included with this distribution is covered by the same copyright terms +except that the holder is Tim Hudson (tjh@cryptsoft.com).
+
+Copyright remains Eric Young's, and as such any Copyright notices in +the code are not to be removed. +If this package is used in a product, Eric Young should be given attribution +as the author of the parts of the library used. +This can be in the form of a textual message at program startup or +in documentation (online or textual) provided with the package.
+
+Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met:
+1. Redistributions of source code must retain the copyright +notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution.
+3. All advertising materials mentioning features or use of this software +must display the following acknowledgement:
+"This product includes cryptographic software written by +Eric Young (eay@cryptsoft.com)" +The word 'cryptographic' can be left out if the rouines from the library +being used are not cryptographic related :-).
+4. If you include any Windows specific code (or a derivative thereof) from +the apps directory (application code) you must include an acknowledgement: +"This product includes software written by Tim Hudson (tjh@cryptsoft.com)"
+
+THIS SOFTWARE IS PROVIDED BY ERIC YOUNG "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE.
+
+The licence and distribution terms for any publically available version or +derivative of this code cannot be changed. i.e. this code cannot simply be +copied and put under another distribution licence +[including the GNU Public Licence.] + +

Boost

+
    +
  • Boost (https://sourceforge.net/projects/boost)
  • +
  • Boost/Archive (https://github.com/monero-project/monero/tree/master/external/boost/archive)
  • +
+

Boost Software License - Version 1.0 - August 17th, 2003

+Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following:
+
+The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +

Unbound (https://github.com/monero-project/monero/blob/master/external/unbound)

+

Unbound Software License

+Copyright (c) 2007, NLnet Labs. All rights reserved.
+
+This software is open source.
+
+Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met:
+
+Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution.
+
+Neither the name of the NLNET LABS nor the names of its contributors may +be used to endorse or promote products derived from this software without +specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +

MiniUPnPc (https://github.com/monero-project/monero/blob/master/external/miniupnpc)

+Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. All rights reserved. +

The MiniUPnPc License

+Copyright (c) 2005-2015, Thomas BERNARD. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution.
+* The name of the author may not be used to endorse or promote products +derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +

liblmdb (https://github.com/monero-project/monero/blob/master/external/db_drivers/liblmdb)

+

The OpenLDAP Public License, Version 2.8, 17 August 2003

+Redistribution and use of this software and associated documentation +("Software"), with or without modification, are permitted provided +that the following conditions are met:
+
+1. Redistributions in source form must retain copyright statements +and notices,
+
+2. Redistributions in binary form must reproduce applicable copyright +statements and notices, this list of conditions, and the following +disclaimer in the documentation and/or other materials provided +with the distribution, and
+
+3. Redistributions must contain a verbatim copy of this document.
+
+The OpenLDAP Foundation may revise this license from time to time. +Each revision is distinguished by a version number. You may use +this Software under terms of this license revision or under the +terms of any subsequent revision of the license.
+
+THIS SOFTWARE IS PROVIDED BY THE OPENLDAP FOUNDATION AND ITS +CONTRIBUTORS "AS IS" AND ANY EXPRESSED OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT +SHALL THE OPENLDAP FOUNDATION, ITS CONTRIBUTORS, OR THE AUTHOR(S) +OR OWNER(S) OF THE SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE.
+
+The names of the authors and copyright holders must not be used in +advertising or otherwise to promote the sale, use or other dealing +in this Software without specific, written prior permission. Title +to copyright in this Software shall at all times remain with copyright +holders.
+
+OpenLDAP is a registered trademark of the OpenLDAP Foundation.
+
+Copyright 1999-2003 The OpenLDAP Foundation, Redwood City, +California, USA. All Rights Reserved. Permission to copy and +distribute verbatim copies of this document is granted. + +

epee (https://github.com/monero-project/monero/blob/master/contrib/epee)

+Copyright (c) 2006-2013, Andrey N. Sabelnikov, www.sabelnikov.net. All rights reserved. +

The epee License

+Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met:
+* Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution.
+* Neither the name of the Andrey N. Sabelnikov nor the +names of its contributors may be used to endorse or promote products +derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL Andrey N. Sabelnikov BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +

'Poppins' Font

+

SIL Open Font License

+

Copyright (c) 2014, Indian Type Foundry (info@indiantypefoundry.com).

+

This Font Software is licensed under the SIL Open Font License, Version 1.1.
+ This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL

+

—————————————————————————————-
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+ —————————————————————————————-

+

PREAMBLE
+ The goals of the Open Font License (OFL) are to stimulate worldwide development of + collaborative font projects, to support the font creation efforts of academic and + linguistic communities, and to provide a free and open framework in which fonts may be + shared and improved in partnership with others.

+

The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as + long as they are not sold by themselves. The fonts, including any derivative works, can be + bundled, embedded, redistributed and/or sold with any software provided that any reserved + names are not used by derivative works. The fonts and derivatives, however, cannot be + released under any other type of license. The requirement for fonts to remain under this + license does not apply to any document created using the fonts or their derivatives.

+

DEFINITIONS
+ “Font Software” refers to the set of files released by the Copyright Holder(s) + under this license and clearly marked as such. This may include source files, build scripts + and documentation.

+

“Reserved Font Name” refers to any names specified as such after the copyright + statement(s).

+

“Original Version” refers to the collection of Font Software components as + distributed by the Copyright Holder(s).

+

“Modified Version” refers to any derivative made by adding to, deleting, or + substituting—in part or in whole—any of the components of the Original Version, + by changing formats or by porting the Font Software to a new environment.

+

“Author” refers to any designer, engineer, programmer, technical writer or other + person who contributed to the Font Software.

+

PERMISSION & CONDITIONS
+ Permission is hereby granted, free of charge, to any person obtaining a copy of the Font + Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and + unmodified copies of the Font Software, subject to the following conditions:

+

1) Neither the Font Software nor any of its individual components, in Original or Modified + Versions, may be sold by itself.

+

2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold + with any software, provided that each copy contains the above copyright notice and this + license. These can be included either as stand-alone text files, human-readable headers or + in the appropriate machine-readable metadata fields within text or binary files as long as + those fields can be easily viewed by the user.

+

3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit + written permission is granted by the corresponding Copyright Holder. This restriction only + applies to the primary font name as presented to the users.

+

4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be + used to promote, endorse or advertise any Modified Version, except to acknowledge the + contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit + written permission.

+

5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely + under this license, and must not be distributed under any other license. The requirement + for fonts to remain under this license does not apply to any document created using the Font + Software.

+

TERMINATION
+ This license becomes null and void if any of the above conditions are not met.

+

DISCLAIMER
+ THE FONT SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN + NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN + AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE + THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.

diff --git a/app/src/main/cpp/monerujo.cpp b/app/src/main/cpp/monerujo.cpp new file mode 100644 index 0000000..03ab0ab --- /dev/null +++ b/app/src/main/cpp/monerujo.cpp @@ -0,0 +1,1531 @@ +/** + * Copyright (c) 2017 m2049r + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "monerujo.h" +#include "wallet2_api.h" + +//TODO explicit casting jlong, jint, jboolean to avoid warnings + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include +#define LOG_TAG "WalletNDK" +#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG,__VA_ARGS__) +#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG , LOG_TAG,__VA_ARGS__) +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO , LOG_TAG,__VA_ARGS__) +#define LOGW(...) __android_log_print(ANDROID_LOG_WARN , LOG_TAG,__VA_ARGS__) +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR , LOG_TAG,__VA_ARGS__) + +static JavaVM *cachedJVM; +static jclass class_ArrayList; +static jclass class_WalletListener; +static jclass class_TransactionInfo; +static jclass class_Transfer; +static jclass class_Ledger; +static jclass class_WalletStatus; + +std::mutex _listenerMutex; + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) { + cachedJVM = jvm; + LOGI("JNI_OnLoad"); + JNIEnv *jenv; + if (jvm->GetEnv(reinterpret_cast(&jenv), JNI_VERSION_1_6) != JNI_OK) { + return -1; + } + //LOGI("JNI_OnLoad ok"); + + class_ArrayList = static_cast(jenv->NewGlobalRef( + jenv->FindClass("java/util/ArrayList"))); + class_TransactionInfo = static_cast(jenv->NewGlobalRef( + jenv->FindClass("com/m2049r/xmrwallet/model/TransactionInfo"))); + class_Transfer = static_cast(jenv->NewGlobalRef( + jenv->FindClass("com/m2049r/xmrwallet/model/Transfer"))); + class_WalletListener = static_cast(jenv->NewGlobalRef( + jenv->FindClass("com/m2049r/xmrwallet/model/WalletListener"))); + class_Ledger = static_cast(jenv->NewGlobalRef( + jenv->FindClass("com/m2049r/xmrwallet/ledger/Ledger"))); + class_WalletStatus = static_cast(jenv->NewGlobalRef( + jenv->FindClass("com/m2049r/xmrwallet/model/Wallet$Status"))); + return JNI_VERSION_1_6; +} +#ifdef __cplusplus +} +#endif + +int attachJVM(JNIEnv **jenv) { + int envStat = cachedJVM->GetEnv((void **) jenv, JNI_VERSION_1_6); + if (envStat == JNI_EDETACHED) { + if (cachedJVM->AttachCurrentThread(jenv, nullptr) != 0) { + LOGE("Failed to attach"); + return JNI_ERR; + } + } else if (envStat == JNI_EVERSION) { + LOGE("GetEnv: version not supported"); + return JNI_ERR; + } + //LOGI("envStat=%i", envStat); + return envStat; +} + +void detachJVM(JNIEnv *jenv, int envStat) { + //LOGI("envStat=%i", envStat); + if (jenv->ExceptionCheck()) { + jenv->ExceptionDescribe(); + } + + if (envStat == JNI_EDETACHED) { + cachedJVM->DetachCurrentThread(); + } +} + +struct MyWalletListener : Monero::WalletListener { + jobject jlistener; + + MyWalletListener(JNIEnv *env, jobject aListener) { + LOGD("Created MyListener"); + jlistener = env->NewGlobalRef(aListener);; + } + + ~MyWalletListener() { + LOGD("Destroyed MyListener"); + }; + + void deleteGlobalJavaRef(JNIEnv *env) { + std::lock_guard lock(_listenerMutex); + env->DeleteGlobalRef(jlistener); + jlistener = nullptr; + } + + /** + * @brief updated - generic callback, called when any event (sent/received/block reveived/etc) happened with the wallet; + */ + void updated() { + std::lock_guard lock(_listenerMutex); + if (jlistener == nullptr) return; + LOGD("updated"); + JNIEnv *jenv; + int envStat = attachJVM(&jenv); + if (envStat == JNI_ERR) return; + + jmethodID listenerClass_updated = jenv->GetMethodID(class_WalletListener, "updated", "()V"); + jenv->CallVoidMethod(jlistener, listenerClass_updated); + + detachJVM(jenv, envStat); + } + + + /** + * @brief moneySpent - called when money spent + * @param txId - transaction id + * @param amount - amount + */ + void moneySpent(const std::string &txId, uint64_t amount) { + std::lock_guard lock(_listenerMutex); + if (jlistener == nullptr) return; + LOGD("moneySpent %" + PRIu64, amount); + } + + /** + * @brief moneyReceived - called when money received + * @param txId - transaction id + * @param amount - amount + */ + void moneyReceived(const std::string &txId, uint64_t amount) { + std::lock_guard lock(_listenerMutex); + if (jlistener == nullptr) return; + LOGD("moneyReceived %" + PRIu64, amount); + } + + /** + * @brief unconfirmedMoneyReceived - called when payment arrived in tx pool + * @param txId - transaction id + * @param amount - amount + */ + void unconfirmedMoneyReceived(const std::string &txId, uint64_t amount) { + std::lock_guard lock(_listenerMutex); + if (jlistener == nullptr) return; + LOGD("unconfirmedMoneyReceived %" + PRIu64, amount); + } + + /** + * @brief newBlock - called when new block received + * @param height - block height + */ + void newBlock(uint64_t height) { + std::lock_guard lock(_listenerMutex); + if (jlistener == nullptr) return; + //LOGD("newBlock"); + JNIEnv *jenv; + int envStat = attachJVM(&jenv); + if (envStat == JNI_ERR) return; + + jlong h = static_cast(height); + jmethodID listenerClass_newBlock = jenv->GetMethodID(class_WalletListener, "newBlock", + "(J)V"); + jenv->CallVoidMethod(jlistener, listenerClass_newBlock, h); + + detachJVM(jenv, envStat); + } + +/** + * @brief refreshed - called when wallet refreshed by background thread or explicitly refreshed by calling "refresh" synchronously + */ + void refreshed() { + std::lock_guard lock(_listenerMutex); + if (jlistener == nullptr) return; + LOGD("refreshed"); + JNIEnv *jenv; + + int envStat = attachJVM(&jenv); + if (envStat == JNI_ERR) return; + + jmethodID listenerClass_refreshed = jenv->GetMethodID(class_WalletListener, "refreshed", + "()V"); + jenv->CallVoidMethod(jlistener, listenerClass_refreshed); + detachJVM(jenv, envStat); + } +}; + + +//// helper methods +std::vector java2cpp(JNIEnv *env, jobject arrayList) { + + jmethodID java_util_ArrayList_size = env->GetMethodID(class_ArrayList, "size", "()I"); + jmethodID java_util_ArrayList_get = env->GetMethodID(class_ArrayList, "get", + "(I)Ljava/lang/Object;"); + + jint len = env->CallIntMethod(arrayList, java_util_ArrayList_size); + std::vector result; + result.reserve(len); + for (jint i = 0; i < len; i++) { + jstring element = static_cast(env->CallObjectMethod(arrayList, + java_util_ArrayList_get, i)); + const char *pchars = env->GetStringUTFChars(element, nullptr); + result.emplace_back(pchars); + env->ReleaseStringUTFChars(element, pchars); + env->DeleteLocalRef(element); + } + return result; +} + +jobject cpp2java(JNIEnv *env, const std::vector &vector) { + + jmethodID java_util_ArrayList_ = env->GetMethodID(class_ArrayList, "", "(I)V"); + jmethodID java_util_ArrayList_add = env->GetMethodID(class_ArrayList, "add", + "(Ljava/lang/Object;)Z"); + + jobject result = env->NewObject(class_ArrayList, java_util_ArrayList_, + static_cast (vector.size())); + for (const std::string &s: vector) { + jstring element = env->NewStringUTF(s.c_str()); + env->CallBooleanMethod(result, java_util_ArrayList_add, element); + env->DeleteLocalRef(element); + } + return result; +} + +/// end helpers + +#ifdef __cplusplus +extern "C" +{ +#endif + + +/**********************************/ +/********** WalletManager *********/ +/**********************************/ +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_WalletManager_createWalletJ(JNIEnv *env, jobject instance, + jstring path, jstring password, + jstring language, + jint networkType) { + const char *_path = env->GetStringUTFChars(path, nullptr); + const char *_password = env->GetStringUTFChars(password, nullptr); + const char *_language = env->GetStringUTFChars(language, nullptr); + Monero::NetworkType _networkType = static_cast(networkType); + + Monero::Wallet *wallet = + Monero::WalletManagerFactory::getWalletManager()->createWallet( + std::string(_path), + std::string(_password), + std::string(_language), + _networkType); + + env->ReleaseStringUTFChars(path, _path); + env->ReleaseStringUTFChars(password, _password); + env->ReleaseStringUTFChars(language, _language); + return reinterpret_cast(wallet); +} + +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_WalletManager_openWalletJ(JNIEnv *env, jobject instance, + jstring path, jstring password, + jint networkType) { + const char *_path = env->GetStringUTFChars(path, nullptr); + const char *_password = env->GetStringUTFChars(password, nullptr); + Monero::NetworkType _networkType = static_cast(networkType); + + Monero::Wallet *wallet = + Monero::WalletManagerFactory::getWalletManager()->openWallet( + std::string(_path), + std::string(_password), + _networkType); + + env->ReleaseStringUTFChars(path, _path); + env->ReleaseStringUTFChars(password, _password); + return reinterpret_cast(wallet); +} + +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_WalletManager_recoveryWalletJ(JNIEnv *env, jobject instance, + jstring path, jstring password, + jstring mnemonic, jstring offset, + jint networkType, + jlong restoreHeight) { + const char *_path = env->GetStringUTFChars(path, nullptr); + const char *_password = env->GetStringUTFChars(password, nullptr); + const char *_mnemonic = env->GetStringUTFChars(mnemonic, nullptr); + const char *_offset = env->GetStringUTFChars(offset, nullptr); + Monero::NetworkType _networkType = static_cast(networkType); + + Monero::Wallet *wallet = + Monero::WalletManagerFactory::getWalletManager()->recoveryWallet( + std::string(_path), + std::string(_password), + std::string(_mnemonic), + _networkType, + (uint64_t) restoreHeight, + 1, // kdf_rounds + std::string(_offset)); + + env->ReleaseStringUTFChars(path, _path); + env->ReleaseStringUTFChars(password, _password); + env->ReleaseStringUTFChars(mnemonic, _mnemonic); + env->ReleaseStringUTFChars(offset, _offset); + return reinterpret_cast(wallet); +} + +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_WalletManager_createWalletFromKeysJ(JNIEnv *env, jobject instance, + jstring path, jstring password, + jstring language, + jint networkType, + jlong restoreHeight, + jstring addressString, + jstring viewKeyString, + jstring spendKeyString) { + const char *_path = env->GetStringUTFChars(path, nullptr); + const char *_password = env->GetStringUTFChars(password, nullptr); + const char *_language = env->GetStringUTFChars(language, nullptr); + Monero::NetworkType _networkType = static_cast(networkType); + const char *_addressString = env->GetStringUTFChars(addressString, nullptr); + const char *_viewKeyString = env->GetStringUTFChars(viewKeyString, nullptr); + const char *_spendKeyString = env->GetStringUTFChars(spendKeyString, nullptr); + + Monero::Wallet *wallet = + Monero::WalletManagerFactory::getWalletManager()->createWalletFromKeys( + std::string(_path), + std::string(_password), + std::string(_language), + _networkType, + (uint64_t) restoreHeight, + std::string(_addressString), + std::string(_viewKeyString), + std::string(_spendKeyString)); + + env->ReleaseStringUTFChars(path, _path); + env->ReleaseStringUTFChars(password, _password); + env->ReleaseStringUTFChars(language, _language); + env->ReleaseStringUTFChars(addressString, _addressString); + env->ReleaseStringUTFChars(viewKeyString, _viewKeyString); + env->ReleaseStringUTFChars(spendKeyString, _spendKeyString); + return reinterpret_cast(wallet); +} + + +// virtual void setSubaddressLookahead(uint32_t major, uint32_t minor) = 0; + +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_WalletManager_createWalletFromDeviceJ(JNIEnv *env, jobject instance, + jstring path, + jstring password, + jint networkType, + jstring deviceName, + jlong restoreHeight, + jstring subaddressLookahead) { + const char *_path = env->GetStringUTFChars(path, nullptr); + const char *_password = env->GetStringUTFChars(password, nullptr); + Monero::NetworkType _networkType = static_cast(networkType); + const char *_deviceName = env->GetStringUTFChars(deviceName, nullptr); + const char *_subaddressLookahead = env->GetStringUTFChars(subaddressLookahead, nullptr); + + Monero::Wallet *wallet = + Monero::WalletManagerFactory::getWalletManager()->createWalletFromDevice( + std::string(_path), + std::string(_password), + _networkType, + std::string(_deviceName), + (uint64_t) restoreHeight, + std::string(_subaddressLookahead)); + + env->ReleaseStringUTFChars(path, _path); + env->ReleaseStringUTFChars(password, _password); + env->ReleaseStringUTFChars(deviceName, _deviceName); + env->ReleaseStringUTFChars(subaddressLookahead, _subaddressLookahead); + return reinterpret_cast(wallet); +} + +JNIEXPORT jboolean JNICALL +Java_com_m2049r_xmrwallet_model_WalletManager_walletExists(JNIEnv *env, jobject instance, + jstring path) { + const char *_path = env->GetStringUTFChars(path, nullptr); + bool exists = + Monero::WalletManagerFactory::getWalletManager()->walletExists(std::string(_path)); + env->ReleaseStringUTFChars(path, _path); + return static_cast(exists); +} + +JNIEXPORT jboolean JNICALL +Java_com_m2049r_xmrwallet_model_WalletManager_verifyWalletPassword(JNIEnv *env, jobject instance, + jstring keys_file_name, + jstring password, + jboolean watch_only) { + const char *_keys_file_name = env->GetStringUTFChars(keys_file_name, nullptr); + const char *_password = env->GetStringUTFChars(password, nullptr); + bool passwordOk = + Monero::WalletManagerFactory::getWalletManager()->verifyWalletPassword( + std::string(_keys_file_name), std::string(_password), watch_only); + env->ReleaseStringUTFChars(keys_file_name, _keys_file_name); + env->ReleaseStringUTFChars(password, _password); + return static_cast(passwordOk); +} + +//virtual int queryWalletHardware(const std::string &keys_file_name, const std::string &password) const = 0; +JNIEXPORT jint JNICALL +Java_com_m2049r_xmrwallet_model_WalletManager_queryWalletDeviceJ(JNIEnv *env, jobject instance, + jstring keys_file_name, + jstring password) { + const char *_keys_file_name = env->GetStringUTFChars(keys_file_name, nullptr); + const char *_password = env->GetStringUTFChars(password, nullptr); + Monero::Wallet::Device device_type; + bool ok = Monero::WalletManagerFactory::getWalletManager()-> + queryWalletDevice(device_type, std::string(_keys_file_name), std::string(_password)); + env->ReleaseStringUTFChars(keys_file_name, _keys_file_name); + env->ReleaseStringUTFChars(password, _password); + if (ok) + return static_cast(device_type); + else + return -1; +} + +JNIEXPORT jobject JNICALL +Java_com_m2049r_xmrwallet_model_WalletManager_findWallets(JNIEnv *env, jobject instance, + jstring path) { + const char *_path = env->GetStringUTFChars(path, nullptr); + std::vector walletPaths = + Monero::WalletManagerFactory::getWalletManager()->findWallets(std::string(_path)); + env->ReleaseStringUTFChars(path, _path); + return cpp2java(env, walletPaths); +} + +//TODO virtual bool checkPayment(const std::string &address, const std::string &txid, const std::string &txkey, const std::string &daemon_address, uint64_t &received, uint64_t &height, std::string &error) const = 0; + +JNIEXPORT void JNICALL +Java_com_m2049r_xmrwallet_model_WalletManager_setDaemonAddressJ(JNIEnv *env, jobject instance, + jstring address) { + const char *_address = env->GetStringUTFChars(address, nullptr); + Monero::WalletManagerFactory::getWalletManager()->setDaemonAddress(std::string(_address)); + env->ReleaseStringUTFChars(address, _address); +} + +// returns whether the daemon can be reached, and its version number +JNIEXPORT jint JNICALL +Java_com_m2049r_xmrwallet_model_WalletManager_getDaemonVersion(JNIEnv *env, + jobject instance) { + uint32_t version; + bool isConnected = + Monero::WalletManagerFactory::getWalletManager()->connected(&version); + if (!isConnected) version = 0; + return version; +} + +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_WalletManager_getBlockchainHeight(JNIEnv *env, jobject instance) { + return Monero::WalletManagerFactory::getWalletManager()->blockchainHeight(); +} + +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_WalletManager_getBlockchainTargetHeight(JNIEnv *env, + jobject instance) { + return Monero::WalletManagerFactory::getWalletManager()->blockchainTargetHeight(); +} + +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_WalletManager_getNetworkDifficulty(JNIEnv *env, jobject instance) { + return Monero::WalletManagerFactory::getWalletManager()->networkDifficulty(); +} + +JNIEXPORT jdouble JNICALL +Java_com_m2049r_xmrwallet_model_WalletManager_getMiningHashRate(JNIEnv *env, jobject instance) { + return Monero::WalletManagerFactory::getWalletManager()->miningHashRate(); +} + +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_WalletManager_getBlockTarget(JNIEnv *env, jobject instance) { + return Monero::WalletManagerFactory::getWalletManager()->blockTarget(); +} + +JNIEXPORT jboolean JNICALL +Java_com_m2049r_xmrwallet_model_WalletManager_isMining(JNIEnv *env, jobject instance) { + return static_cast(Monero::WalletManagerFactory::getWalletManager()->isMining()); +} + +JNIEXPORT jboolean JNICALL +Java_com_m2049r_xmrwallet_model_WalletManager_startMining(JNIEnv *env, jobject instance, + jstring address, + jboolean background_mining, + jboolean ignore_battery) { + const char *_address = env->GetStringUTFChars(address, nullptr); + bool success = + Monero::WalletManagerFactory::getWalletManager()->startMining(std::string(_address), + background_mining, + ignore_battery); + env->ReleaseStringUTFChars(address, _address); + return static_cast(success); +} + +JNIEXPORT jboolean JNICALL +Java_com_m2049r_xmrwallet_model_WalletManager_stopMining(JNIEnv *env, jobject instance) { + return static_cast(Monero::WalletManagerFactory::getWalletManager()->stopMining()); +} + +JNIEXPORT jstring JNICALL +Java_com_m2049r_xmrwallet_model_WalletManager_resolveOpenAlias(JNIEnv *env, jobject instance, + jstring address, + jboolean dnssec_valid) { + const char *_address = env->GetStringUTFChars(address, nullptr); + bool _dnssec_valid = (bool) dnssec_valid; + std::string resolvedAlias = + Monero::WalletManagerFactory::getWalletManager()->resolveOpenAlias( + std::string(_address), + _dnssec_valid); + env->ReleaseStringUTFChars(address, _address); + return env->NewStringUTF(resolvedAlias.c_str()); +} + +JNIEXPORT jboolean JNICALL +Java_com_m2049r_xmrwallet_model_WalletManager_setProxy(JNIEnv *env, jobject instance, + jstring address) { + const char *_address = env->GetStringUTFChars(address, nullptr); + bool rc = + Monero::WalletManagerFactory::getWalletManager()->setProxy(std::string(_address)); + env->ReleaseStringUTFChars(address, _address); + return rc; +} + + +//TODO static std::tuple checkUpdates(const std::string &software, const std::string &subdir); + +JNIEXPORT jboolean JNICALL +Java_com_m2049r_xmrwallet_model_WalletManager_closeJ(JNIEnv *env, jobject instance, + jobject walletInstance) { + Monero::Wallet *wallet = getHandle(env, walletInstance); + bool closeSuccess = Monero::WalletManagerFactory::getWalletManager()->closeWallet(wallet, + false); + if (closeSuccess) { + MyWalletListener *walletListener = getHandle(env, walletInstance, + "listenerHandle"); + if (walletListener != nullptr) { + walletListener->deleteGlobalJavaRef(env); + delete walletListener; + } + } + LOGD("wallet closed"); + return static_cast(closeSuccess); +} + + + + +/**********************************/ +/************ Wallet **************/ +/**********************************/ + +JNIEXPORT jstring JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_getSeed(JNIEnv *env, jobject instance, jstring seedOffset) { + const char *_seedOffset = env->GetStringUTFChars(seedOffset, nullptr); + Monero::Wallet *wallet = getHandle(env, instance); + jstring seed = env->NewStringUTF(wallet->seed(std::string(_seedOffset)).c_str()); + env->ReleaseStringUTFChars(seedOffset, _seedOffset); + return seed; +} + +JNIEXPORT jstring JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_getSeedLanguage(JNIEnv *env, jobject instance) { + Monero::Wallet *wallet = getHandle(env, instance); + return env->NewStringUTF(wallet->getSeedLanguage().c_str()); +} + +JNIEXPORT void JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_setSeedLanguage(JNIEnv *env, jobject instance, + jstring language) { + const char *_language = env->GetStringUTFChars(language, nullptr); + Monero::Wallet *wallet = getHandle(env, instance); + wallet->setSeedLanguage(std::string(_language)); + env->ReleaseStringUTFChars(language, _language); +} + +JNIEXPORT jint JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_getStatusJ(JNIEnv *env, jobject instance) { + Monero::Wallet *wallet = getHandle(env, instance); + return wallet->status(); +} + +jobject newWalletStatusInstance(JNIEnv *env, int status, const std::string &errorString) { + jmethodID init = env->GetMethodID(class_WalletStatus, "", + "(ILjava/lang/String;)V"); + jstring _errorString = env->NewStringUTF(errorString.c_str()); + jobject instance = env->NewObject(class_WalletStatus, init, status, _errorString); + env->DeleteLocalRef(_errorString); + return instance; +} + + +JNIEXPORT jobject JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_statusWithErrorString(JNIEnv *env, jobject instance) { + Monero::Wallet *wallet = getHandle(env, instance); + + int status; + std::string errorString; + wallet->statusWithErrorString(status, errorString); + + return newWalletStatusInstance(env, status, errorString); +} + +JNIEXPORT jboolean JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_setPassword(JNIEnv *env, jobject instance, + jstring password) { + const char *_password = env->GetStringUTFChars(password, nullptr); + Monero::Wallet *wallet = getHandle(env, instance); + bool success = wallet->setPassword(std::string(_password)); + env->ReleaseStringUTFChars(password, _password); + return static_cast(success); +} + +JNIEXPORT jstring JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_getAddressJ(JNIEnv *env, jobject instance, + jint accountIndex, + jint addressIndex) { + Monero::Wallet *wallet = getHandle(env, instance); + return env->NewStringUTF( + wallet->address((uint32_t) accountIndex, (uint32_t) addressIndex).c_str()); +} + +JNIEXPORT jstring JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_getPath(JNIEnv *env, jobject instance) { + Monero::Wallet *wallet = getHandle(env, instance); + return env->NewStringUTF(wallet->path().c_str()); +} + +JNIEXPORT jint JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_nettype(JNIEnv *env, jobject instance) { + Monero::Wallet *wallet = getHandle(env, instance); + return wallet->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; + +JNIEXPORT jstring JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_getIntegratedAddress(JNIEnv *env, jobject instance, + jstring payment_id) { + const char *_payment_id = env->GetStringUTFChars(payment_id, nullptr); + Monero::Wallet *wallet = getHandle(env, instance); + std::string address = wallet->integratedAddress(_payment_id); + env->ReleaseStringUTFChars(payment_id, _payment_id); + return env->NewStringUTF(address.c_str()); +} + +JNIEXPORT jstring JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_getSecretViewKey(JNIEnv *env, jobject instance) { + Monero::Wallet *wallet = getHandle(env, instance); + return env->NewStringUTF(wallet->secretViewKey().c_str()); +} + +JNIEXPORT jstring JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_getSecretSpendKey(JNIEnv *env, jobject instance) { + Monero::Wallet *wallet = getHandle(env, instance); + return env->NewStringUTF(wallet->secretSpendKey().c_str()); +} + +JNIEXPORT jboolean JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_store(JNIEnv *env, jobject instance, + jstring path) { + const char *_path = env->GetStringUTFChars(path, nullptr); + Monero::Wallet *wallet = getHandle(env, instance); + bool success = wallet->store(std::string(_path)); + if (!success) { + LOGE("store() %s", wallet->errorString().c_str()); + } + env->ReleaseStringUTFChars(path, _path); + return static_cast(success); +} + +JNIEXPORT jstring JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_getFilename(JNIEnv *env, jobject instance) { + Monero::Wallet *wallet = getHandle(env, instance); + return env->NewStringUTF(wallet->filename().c_str()); +} + +// virtual std::string keysFilename() const = 0; + +JNIEXPORT jboolean JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_initJ(JNIEnv *env, jobject instance, + jstring daemon_address, + jlong upper_transaction_size_limit, + jstring daemon_username, jstring daemon_password) { + const char *_daemon_address = env->GetStringUTFChars(daemon_address, nullptr); + const char *_daemon_username = env->GetStringUTFChars(daemon_username, nullptr); + const char *_daemon_password = env->GetStringUTFChars(daemon_password, nullptr); + Monero::Wallet *wallet = getHandle(env, instance); + 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 static_cast(status); +} + +// virtual bool createWatchOnly(const std::string &path, const std::string &password, const std::string &language) const = 0; + +JNIEXPORT void JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_setRestoreHeight(JNIEnv *env, jobject instance, + jlong height) { + Monero::Wallet *wallet = getHandle(env, instance); + wallet->setRefreshFromBlockHeight((uint64_t) height); +} + +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_getRestoreHeight(JNIEnv *env, jobject instance) { + Monero::Wallet *wallet = getHandle(env, instance); + return wallet->getRefreshFromBlockHeight(); +} + +// virtual void setRecoveringFromSeed(bool recoveringFromSeed) = 0; +// virtual bool connectToDaemon() = 0; + +JNIEXPORT jint JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_getConnectionStatusJ(JNIEnv *env, jobject instance) { + Monero::Wallet *wallet = getHandle(env, instance); + return wallet->connected(); +} +//TODO virtual void setTrustedDaemon(bool arg) = 0; +//TODO virtual bool trustedDaemon() const = 0; + +JNIEXPORT jboolean JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_setProxy(JNIEnv *env, jobject instance, + jstring address) { + const char *_address = env->GetStringUTFChars(address, nullptr); + Monero::Wallet *wallet = getHandle(env, instance); + bool rc = wallet->setProxy(std::string(_address)); + env->ReleaseStringUTFChars(address, _address); + return rc; +} + +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_getBalance(JNIEnv *env, jobject instance, + jint accountIndex) { + Monero::Wallet *wallet = getHandle(env, instance); + return wallet->balance((uint32_t) accountIndex); +} + +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_getBalanceAll(JNIEnv *env, jobject instance) { + Monero::Wallet *wallet = getHandle(env, instance); + return wallet->balanceAll(); +} + +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_getUnlockedBalance(JNIEnv *env, jobject instance, + jint accountIndex) { + Monero::Wallet *wallet = getHandle(env, instance); + return wallet->unlockedBalance((uint32_t) accountIndex); +} + +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_getUnlockedBalanceAll(JNIEnv *env, jobject instance) { + Monero::Wallet *wallet = getHandle(env, instance); + return wallet->unlockedBalanceAll(); +} + +JNIEXPORT jboolean JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_isWatchOnly(JNIEnv *env, jobject instance) { + Monero::Wallet *wallet = getHandle(env, instance); + return static_cast(wallet->watchOnly()); +} + +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_getBlockChainHeight(JNIEnv *env, jobject instance) { + Monero::Wallet *wallet = getHandle(env, instance); + return wallet->blockChainHeight(); +} + +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_getApproximateBlockChainHeight(JNIEnv *env, + jobject instance) { + Monero::Wallet *wallet = getHandle(env, instance); + return wallet->approximateBlockChainHeight(); +} + +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_getDaemonBlockChainHeight(JNIEnv *env, jobject instance) { + Monero::Wallet *wallet = getHandle(env, instance); + return wallet->daemonBlockChainHeight(); +} + +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_getDaemonBlockChainTargetHeight(JNIEnv *env, + jobject instance) { + Monero::Wallet *wallet = getHandle(env, instance); + return wallet->daemonBlockChainTargetHeight(); +} + +JNIEXPORT jboolean JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_isSynchronizedJ(JNIEnv *env, jobject instance) { + Monero::Wallet *wallet = getHandle(env, instance); + return static_cast(wallet->synchronized()); +} + +JNIEXPORT jint JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_getDeviceTypeJ(JNIEnv *env, jobject instance) { + Monero::Wallet *wallet = getHandle(env, instance); + Monero::Wallet::Device device_type = wallet->getDeviceType(); + return static_cast(device_type); +} + +//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_slowHash(JNIEnv *env, jclass clazz, + jbyteArray data, jint brokenVariant) { + char hash[HASH_SIZE]; + jsize size = env->GetArrayLength(data); + if ((brokenVariant > 0) && (size < 200 /*sizeof(union hash_state)*/)) { + return nullptr; + } + + jbyte *buffer = env->GetByteArrayElements(data, nullptr); + switch (brokenVariant) { + case 1: + slow_hash_broken(buffer, hash, 1); + break; + case 2: + slow_hash_broken(buffer, hash, 0); + break; + default: // not broken + 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 +Java_com_m2049r_xmrwallet_model_Wallet_getDisplayAmount(JNIEnv *env, jclass clazz, + jlong amount) { + return env->NewStringUTF(Monero::Wallet::displayAmount(amount).c_str()); +} + +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_getAmountFromString(JNIEnv *env, jclass clazz, + jstring amount) { + const char *_amount = env->GetStringUTFChars(amount, nullptr); + uint64_t x = Monero::Wallet::amountFromString(_amount); + env->ReleaseStringUTFChars(amount, _amount); + return x; +} + +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_getAmountFromDouble(JNIEnv *env, jclass clazz, + jdouble amount) { + return Monero::Wallet::amountFromDouble(amount); +} + +JNIEXPORT jstring JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_generatePaymentId(JNIEnv *env, jclass clazz) { + return env->NewStringUTF(Monero::Wallet::genPaymentId().c_str()); +} + +JNIEXPORT jboolean JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_isPaymentIdValid(JNIEnv *env, jclass clazz, + jstring payment_id) { + const char *_payment_id = env->GetStringUTFChars(payment_id, nullptr); + bool isValid = Monero::Wallet::paymentIdValid(_payment_id); + env->ReleaseStringUTFChars(payment_id, _payment_id); + return static_cast(isValid); +} + +JNIEXPORT jboolean JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_isAddressValid(JNIEnv *env, jclass clazz, + jstring address, jint networkType) { + const char *_address = env->GetStringUTFChars(address, nullptr); + Monero::NetworkType _networkType = static_cast(networkType); + bool isValid = Monero::Wallet::addressValid(_address, _networkType); + env->ReleaseStringUTFChars(address, _address); + return static_cast(isValid); +} + +JNIEXPORT jstring JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_getPaymentIdFromAddress(JNIEnv *env, jclass clazz, + jstring address, + jint networkType) { + Monero::NetworkType _networkType = static_cast(networkType); + const char *_address = env->GetStringUTFChars(address, nullptr); + std::string payment_id = Monero::Wallet::paymentIdFromAddress(_address, _networkType); + env->ReleaseStringUTFChars(address, _address); + return env->NewStringUTF(payment_id.c_str()); +} + +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_getMaximumAllowedAmount(JNIEnv *env, jclass clazz) { + return Monero::Wallet::maximumAllowedAmount(); +} + +JNIEXPORT void JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_startRefresh(JNIEnv *env, jobject instance) { + Monero::Wallet *wallet = getHandle(env, instance); + wallet->startRefresh(); +} + +JNIEXPORT void JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_pauseRefresh(JNIEnv *env, jobject instance) { + Monero::Wallet *wallet = getHandle(env, instance); + wallet->pauseRefresh(); +} + +JNIEXPORT jboolean JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_refresh(JNIEnv *env, jobject instance) { + Monero::Wallet *wallet = getHandle(env, instance); + return static_cast(wallet->refresh()); +} + +JNIEXPORT void JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_refreshAsync(JNIEnv *env, jobject instance) { + Monero::Wallet *wallet = getHandle(env, instance); + wallet->refreshAsync(); +} + +//TODO virtual bool rescanBlockchain() = 0; + +//virtual void rescanBlockchainAsync() = 0; +JNIEXPORT void JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_rescanBlockchainAsyncJ(JNIEnv *env, jobject instance) { + Monero::Wallet *wallet = getHandle(env, instance); + wallet->rescanBlockchainAsync(); +} + + +//TODO virtual void setAutoRefreshInterval(int millis) = 0; +//TODO virtual int autoRefreshInterval() const = 0; + +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_createTransactionJ(JNIEnv *env, jobject instance, + jstring dst_addr, jstring payment_id, + jlong amount, jint mixin_count, + jint priority, + jint accountIndex) { + + const char *_dst_addr = env->GetStringUTFChars(dst_addr, nullptr); + const char *_payment_id = env->GetStringUTFChars(payment_id, nullptr); + Monero::PendingTransaction::Priority _priority = + static_cast(priority); + Monero::Wallet *wallet = getHandle(env, instance); + + Monero::PendingTransaction *tx = wallet->createTransaction(_dst_addr, _payment_id, + amount, (uint32_t) mixin_count, + _priority, + (uint32_t) accountIndex); + + env->ReleaseStringUTFChars(dst_addr, _dst_addr); + env->ReleaseStringUTFChars(payment_id, _payment_id); + return reinterpret_cast(tx); +} + +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_createSweepTransaction(JNIEnv *env, jobject instance, + jstring dst_addr, jstring payment_id, + jint mixin_count, + jint priority, + jint accountIndex) { + + const char *_dst_addr = env->GetStringUTFChars(dst_addr, nullptr); + const char *_payment_id = env->GetStringUTFChars(payment_id, nullptr); + Monero::PendingTransaction::Priority _priority = + static_cast(priority); + Monero::Wallet *wallet = getHandle(env, instance); + + Monero::optional empty; + + Monero::PendingTransaction *tx = wallet->createTransaction(_dst_addr, _payment_id, + empty, (uint32_t) mixin_count, + _priority, + (uint32_t) accountIndex); + + env->ReleaseStringUTFChars(dst_addr, _dst_addr); + env->ReleaseStringUTFChars(payment_id, _payment_id); + return reinterpret_cast(tx); +} + +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_createSweepUnmixableTransactionJ(JNIEnv *env, + jobject instance) { + Monero::Wallet *wallet = getHandle(env, instance); + Monero::PendingTransaction *tx = wallet->createSweepUnmixableTransaction(); + return reinterpret_cast(tx); +} + +//virtual UnsignedTransaction * loadUnsignedTx(const std::string &unsigned_filename) = 0; +//virtual bool submitTransaction(const std::string &fileName) = 0; + +JNIEXPORT void JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_disposeTransaction(JNIEnv *env, jobject instance, + jobject pendingTransaction) { + Monero::Wallet *wallet = getHandle(env, instance); + Monero::PendingTransaction *_pendingTransaction = + getHandle(env, pendingTransaction); + wallet->disposeTransaction(_pendingTransaction); +} + +//virtual bool exportKeyImages(const std::string &filename) = 0; +//virtual bool importKeyImages(const std::string &filename) = 0; + + +//virtual TransactionHistory * history() const = 0; +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_getHistoryJ(JNIEnv *env, jobject instance) { + Monero::Wallet *wallet = getHandle(env, instance); + return reinterpret_cast(wallet->history()); +} + +//virtual AddressBook * addressBook() const = 0; + +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_setListenerJ(JNIEnv *env, jobject instance, + jobject javaListener) { + Monero::Wallet *wallet = getHandle(env, instance); + wallet->setListener(nullptr); // clear old listener + // delete old listener + MyWalletListener *oldListener = getHandle(env, instance, + "listenerHandle"); + if (oldListener != nullptr) { + oldListener->deleteGlobalJavaRef(env); + delete oldListener; + } + if (javaListener == nullptr) { + LOGD("null listener"); + return 0; + } else { + MyWalletListener *listener = new MyWalletListener(env, javaListener); + wallet->setListener(listener); + return reinterpret_cast(listener); + } +} + +JNIEXPORT jint JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_getDefaultMixin(JNIEnv *env, jobject instance) { + Monero::Wallet *wallet = getHandle(env, instance); + return wallet->defaultMixin(); +} + +JNIEXPORT void JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_setDefaultMixin(JNIEnv *env, jobject instance, jint mixin) { + Monero::Wallet *wallet = getHandle(env, instance); + return wallet->setDefaultMixin(mixin); +} + +JNIEXPORT jboolean JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_setUserNote(JNIEnv *env, jobject instance, + jstring txid, jstring note) { + + const char *_txid = env->GetStringUTFChars(txid, nullptr); + const char *_note = env->GetStringUTFChars(note, nullptr); + + Monero::Wallet *wallet = getHandle(env, instance); + + bool success = wallet->setUserNote(_txid, _note); + + env->ReleaseStringUTFChars(txid, _txid); + env->ReleaseStringUTFChars(note, _note); + + return static_cast(success); +} + +JNIEXPORT jstring JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_getUserNote(JNIEnv *env, jobject instance, + jstring txid) { + + const char *_txid = env->GetStringUTFChars(txid, nullptr); + + Monero::Wallet *wallet = getHandle(env, instance); + + std::string note = wallet->getUserNote(_txid); + + env->ReleaseStringUTFChars(txid, _txid); + return env->NewStringUTF(note.c_str()); +} + +JNIEXPORT jstring JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_getTxKey(JNIEnv *env, jobject instance, + jstring txid) { + + const char *_txid = env->GetStringUTFChars(txid, nullptr); + + Monero::Wallet *wallet = getHandle(env, instance); + + std::string txKey = wallet->getTxKey(_txid); + + env->ReleaseStringUTFChars(txid, _txid); + return env->NewStringUTF(txKey.c_str()); +} + +//virtual void addSubaddressAccount(const std::string& label) = 0; +JNIEXPORT void JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_addAccount(JNIEnv *env, jobject instance, + jstring label) { + + const char *_label = env->GetStringUTFChars(label, nullptr); + + Monero::Wallet *wallet = getHandle(env, instance); + wallet->addSubaddressAccount(_label); + + env->ReleaseStringUTFChars(label, _label); +} + +//virtual std::string getSubaddressLabel(uint32_t accountIndex, uint32_t addressIndex) const = 0; +JNIEXPORT jstring JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_getSubaddressLabel(JNIEnv *env, jobject instance, + jint accountIndex, jint addressIndex) { + + Monero::Wallet *wallet = getHandle(env, instance); + + std::string label = wallet->getSubaddressLabel((uint32_t) accountIndex, + (uint32_t) addressIndex); + + return env->NewStringUTF(label.c_str()); +} + +//virtual void setSubaddressLabel(uint32_t accountIndex, uint32_t addressIndex, const std::string &label) = 0; +JNIEXPORT void JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_setSubaddressLabel(JNIEnv *env, jobject instance, + jint accountIndex, jint addressIndex, + jstring label) { + + const char *_label = env->GetStringUTFChars(label, nullptr); + + Monero::Wallet *wallet = getHandle(env, instance); + wallet->setSubaddressLabel(accountIndex, addressIndex, _label); + + env->ReleaseStringUTFChars(label, _label); +} + +// virtual size_t numSubaddressAccounts() const = 0; +JNIEXPORT jint JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_getNumAccounts(JNIEnv *env, jobject instance) { + Monero::Wallet *wallet = getHandle(env, instance); + return static_cast(wallet->numSubaddressAccounts()); +} + +//virtual size_t numSubaddresses(uint32_t accountIndex) const = 0; +JNIEXPORT jint JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_getNumSubaddresses(JNIEnv *env, jobject instance, + jint accountIndex) { + Monero::Wallet *wallet = getHandle(env, instance); + return static_cast(wallet->numSubaddresses(accountIndex)); +} + +//virtual void addSubaddress(uint32_t accountIndex, const std::string &label) = 0; +JNIEXPORT void JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_addSubaddress(JNIEnv *env, jobject instance, + jint accountIndex, + jstring label) { + + const char *_label = env->GetStringUTFChars(label, nullptr); + Monero::Wallet *wallet = getHandle(env, instance); + wallet->addSubaddress(accountIndex, _label); + env->ReleaseStringUTFChars(label, _label); +} + +/*JNIEXPORT jstring JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_getLastSubaddress(JNIEnv *env, jobject instance, + jint accountIndex) { + + Monero::Wallet *wallet = getHandle(env, instance); + size_t num = wallet->numSubaddresses(accountIndex); + //wallet->subaddress()->getAll()[num]->getAddress().c_str() + Monero::Subaddress *s = wallet->subaddress(); + s->refresh(accountIndex); + std::vector v = s->getAll(); + return env->NewStringUTF(v[num - 1]->getAddress().c_str()); +} +*/ +//virtual std::string signMessage(const std::string &message) = 0; +//virtual bool verifySignedMessage(const std::string &message, const std::string &addres, const std::string &signature) const = 0; + +//virtual bool parse_uri(const std::string &uri, std::string &address, std::string &payment_id, uint64_t &tvAmount, std::string &tx_description, std::string &recipient_name, std::vector &unknown_parameters, std::string &error) = 0; +//virtual bool rescanSpent() = 0; + + +// TransactionHistory +JNIEXPORT jint JNICALL +Java_com_m2049r_xmrwallet_model_TransactionHistory_getCount(JNIEnv *env, jobject instance) { + Monero::TransactionHistory *history = getHandle(env, + instance); + return history->count(); +} + +jobject newTransferInstance(JNIEnv *env, uint64_t amount, const std::string &address) { + jmethodID c = env->GetMethodID(class_Transfer, "", + "(JLjava/lang/String;)V"); + jstring _address = env->NewStringUTF(address.c_str()); + jobject transfer = env->NewObject(class_Transfer, c, static_cast (amount), _address); + env->DeleteLocalRef(_address); + return transfer; +} + +jobject newTransferList(JNIEnv *env, Monero::TransactionInfo *info) { + const std::vector &transfers = info->transfers(); + if (transfers.empty()) { // don't create empty Lists + return nullptr; + } + // make new ArrayList + jmethodID java_util_ArrayList_ = env->GetMethodID(class_ArrayList, "", "(I)V"); + jmethodID java_util_ArrayList_add = env->GetMethodID(class_ArrayList, "add", + "(Ljava/lang/Object;)Z"); + jobject result = env->NewObject(class_ArrayList, java_util_ArrayList_, + static_cast (transfers.size())); + // create Transfer objects and stick them in the List + for (const Monero::TransactionInfo::Transfer &s: transfers) { + jobject element = newTransferInstance(env, s.amount, s.address); + env->CallBooleanMethod(result, java_util_ArrayList_add, element); + env->DeleteLocalRef(element); + } + return result; +} + +jobject newTransactionInfo(JNIEnv *env, Monero::TransactionInfo *info) { + jmethodID c = env->GetMethodID(class_TransactionInfo, "", + "(IZZJJJLjava/lang/String;JLjava/lang/String;IIJLjava/lang/String;Ljava/util/List;)V"); + jobject transfers = newTransferList(env, info); + jstring _hash = env->NewStringUTF(info->hash().c_str()); + jstring _paymentId = env->NewStringUTF(info->paymentId().c_str()); + jstring _label = env->NewStringUTF(info->label().c_str()); + uint32_t subaddrIndex = 0; + if (info->direction() == Monero::TransactionInfo::Direction_In) + subaddrIndex = *(info->subaddrIndex().begin()); + jobject result = env->NewObject(class_TransactionInfo, c, + info->direction(), + info->isPending(), + info->isFailed(), + static_cast (info->amount()), + static_cast (info->fee()), + static_cast (info->blockHeight()), + _hash, + static_cast (info->timestamp()), + _paymentId, + static_cast (info->subaddrAccount()), + static_cast (subaddrIndex), + static_cast (info->confirmations()), + _label, + transfers); + env->DeleteLocalRef(transfers); + env->DeleteLocalRef(_hash); + env->DeleteLocalRef(_paymentId); + return result; +} + +#include +#include + +jobject cpp2java(JNIEnv *env, const std::vector &vector) { + + jmethodID java_util_ArrayList_ = env->GetMethodID(class_ArrayList, "", "(I)V"); + jmethodID java_util_ArrayList_add = env->GetMethodID(class_ArrayList, "add", + "(Ljava/lang/Object;)Z"); + + jobject arrayList = env->NewObject(class_ArrayList, java_util_ArrayList_, + static_cast (vector.size())); + for (Monero::TransactionInfo *s: vector) { + jobject info = newTransactionInfo(env, s); + env->CallBooleanMethod(arrayList, java_util_ArrayList_add, info); + env->DeleteLocalRef(info); + } + return arrayList; +} + +JNIEXPORT jobject JNICALL +Java_com_m2049r_xmrwallet_model_TransactionHistory_refreshJ(JNIEnv *env, jobject instance) { + Monero::TransactionHistory *history = getHandle(env, + instance); + history->refresh(); + return cpp2java(env, history->getAll()); +} + +// TransactionInfo is implemented in Java - no need here + +JNIEXPORT jint JNICALL +Java_com_m2049r_xmrwallet_model_PendingTransaction_getStatusJ(JNIEnv *env, jobject instance) { + Monero::PendingTransaction *tx = getHandle(env, instance); + return tx->status(); +} + +JNIEXPORT jstring JNICALL +Java_com_m2049r_xmrwallet_model_PendingTransaction_getErrorString(JNIEnv *env, jobject instance) { + Monero::PendingTransaction *tx = getHandle(env, instance); + return env->NewStringUTF(tx->errorString().c_str()); +} + +// commit transaction or save to file if filename is provided. +JNIEXPORT jboolean JNICALL +Java_com_m2049r_xmrwallet_model_PendingTransaction_commit(JNIEnv *env, jobject instance, + jstring filename, jboolean overwrite) { + + const char *_filename = env->GetStringUTFChars(filename, nullptr); + + Monero::PendingTransaction *tx = getHandle(env, instance); + bool success = tx->commit(_filename, overwrite); + + env->ReleaseStringUTFChars(filename, _filename); + return static_cast(success); +} + + +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_PendingTransaction_getAmount(JNIEnv *env, jobject instance) { + Monero::PendingTransaction *tx = getHandle(env, instance); + return tx->amount(); +} + +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_PendingTransaction_getDust(JNIEnv *env, jobject instance) { + Monero::PendingTransaction *tx = getHandle(env, instance); + return tx->dust(); +} + +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_PendingTransaction_getFee(JNIEnv *env, jobject instance) { + Monero::PendingTransaction *tx = getHandle(env, instance); + return tx->fee(); +} + +// 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_getFirstTxIdJ(JNIEnv *env, jobject instance) { + Monero::PendingTransaction *tx = getHandle(env, instance); + std::vector txids = tx->txid(); + if (!txids.empty()) + return env->NewStringUTF(txids.front().c_str()); + else + return nullptr; +} + +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_PendingTransaction_getTxCount(JNIEnv *env, jobject instance) { + Monero::PendingTransaction *tx = getHandle(env, instance); + return tx->txCount(); +} + + +// these are all in Monero::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, jclass clazz, + jstring argv0, + jstring default_log_base_name) { + + const char *_argv0 = env->GetStringUTFChars(argv0, nullptr); + const char *_default_log_base_name = env->GetStringUTFChars(default_log_base_name, nullptr); + + Monero::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, jclass clazz, + jstring category, jstring message) { + + const char *_category = env->GetStringUTFChars(category, nullptr); + const char *_message = env->GetStringUTFChars(message, nullptr); + + Monero::Wallet::debug(_category, _message); + + env->ReleaseStringUTFChars(category, _category); + env->ReleaseStringUTFChars(message, _message); +} + +JNIEXPORT void JNICALL +Java_com_m2049r_xmrwallet_model_WalletManager_logInfo(JNIEnv *env, jclass clazz, + jstring category, jstring message) { + + const char *_category = env->GetStringUTFChars(category, nullptr); + const char *_message = env->GetStringUTFChars(message, nullptr); + + Monero::Wallet::info(_category, _message); + + env->ReleaseStringUTFChars(category, _category); + env->ReleaseStringUTFChars(message, _message); +} + +JNIEXPORT void JNICALL +Java_com_m2049r_xmrwallet_model_WalletManager_logWarning(JNIEnv *env, jclass clazz, + jstring category, jstring message) { + + const char *_category = env->GetStringUTFChars(category, nullptr); + const char *_message = env->GetStringUTFChars(message, nullptr); + + Monero::Wallet::warning(_category, _message); + + env->ReleaseStringUTFChars(category, _category); + env->ReleaseStringUTFChars(message, _message); +} + +JNIEXPORT void JNICALL +Java_com_m2049r_xmrwallet_model_WalletManager_logError(JNIEnv *env, jclass clazz, + jstring category, jstring message) { + + const char *_category = env->GetStringUTFChars(category, nullptr); + const char *_message = env->GetStringUTFChars(message, nullptr); + + Monero::Wallet::error(_category, _message); + + env->ReleaseStringUTFChars(category, _category); + env->ReleaseStringUTFChars(message, _message); +} + +JNIEXPORT void JNICALL +Java_com_m2049r_xmrwallet_model_WalletManager_setLogLevel(JNIEnv *env, jclass clazz, + jint level) { + Monero::WalletManagerFactory::setLogLevel(level); +} + +JNIEXPORT jstring JNICALL +Java_com_m2049r_xmrwallet_model_WalletManager_moneroVersion(JNIEnv *env, jclass clazz) { + return env->NewStringUTF(MONERO_VERSION); +} + +// +// Ledger Stuff +// + +/** + * @brief LedgerExchange - exchange data with Ledger Device + * @param command - buffer for data to send + * @param cmd_len - length of send to send + * @param response - buffer for received data + * @param max_resp_len - size of receive buffer + * + * @return length of received data in response or -1 if error + */ +int LedgerExchange( + unsigned char *command, + unsigned int cmd_len, + unsigned char *response, + unsigned int max_resp_len) { + LOGD("LedgerExchange"); + JNIEnv *jenv; + int envStat = attachJVM(&jenv); + if (envStat == JNI_ERR) return -1; + + jmethodID exchangeMethod = jenv->GetStaticMethodID(class_Ledger, "Exchange", "([B)[B"); + + jsize sendLen = static_cast(cmd_len); + jbyteArray dataSend = jenv->NewByteArray(sendLen); + jenv->SetByteArrayRegion(dataSend, 0, sendLen, (jbyte *) command); + jbyteArray dataRecv = (jbyteArray) jenv->CallStaticObjectMethod(class_Ledger, exchangeMethod, + dataSend); + jenv->DeleteLocalRef(dataSend); + if (dataRecv == nullptr) { + detachJVM(jenv, envStat); + LOGD("LedgerExchange SCARD_E_NO_READERS_AVAILABLE"); + return -1; + } + jsize len = jenv->GetArrayLength(dataRecv); + LOGD("LedgerExchange SCARD_S_SUCCESS %u/%d", cmd_len, len); + if (len <= max_resp_len) { + jenv->GetByteArrayRegion(dataRecv, 0, len, (jbyte *) response); + jenv->DeleteLocalRef(dataRecv); + detachJVM(jenv, envStat); + return static_cast(len);; + } else { + jenv->DeleteLocalRef(dataRecv); + detachJVM(jenv, envStat); + LOGE("LedgerExchange SCARD_E_INSUFFICIENT_BUFFER"); + return -1; + } +} + +/** + * @brief LedgerFind - find Ledger Device and return it's name + * @param buffer - buffer for name of found device + * @param len - length of buffer + * @return 0 - success + * -1 - no device connected / found + * -2 - JVM not found + */ +int LedgerFind(char *buffer, size_t len) { + LOGD("LedgerName"); + JNIEnv *jenv; + int envStat = attachJVM(&jenv); + if (envStat == JNI_ERR) return -2; + + jmethodID nameMethod = jenv->GetStaticMethodID(class_Ledger, "Name", "()Ljava/lang/String;"); + jstring name = (jstring) jenv->CallStaticObjectMethod(class_Ledger, nameMethod); + + int ret; + if (name != nullptr) { + const char *_name = jenv->GetStringUTFChars(name, nullptr); + strncpy(buffer, _name, len); + jenv->ReleaseStringUTFChars(name, _name); + buffer[len - 1] = 0; // terminate in case _name is bigger + ret = 0; + LOGD("LedgerName is %s", buffer); + } else { + buffer[0] = 0; + ret = -1; + } + + detachJVM(jenv, envStat); + return ret; +} + +#ifdef __cplusplus +} +#endif diff --git a/app/src/main/cpp/monerujo.h b/app/src/main/cpp/monerujo.h new file mode 100644 index 0000000..0fb3444 --- /dev/null +++ b/app/src/main/cpp/monerujo.h @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2017 m2049r + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef XMRWALLET_WALLET_LIB_H +#define XMRWALLET_WALLET_LIB_H + +#include + +/* +#include + +#define LOG_TAG "[NDK]" +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__) +#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__) +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__) +*/ + +jfieldID getHandleField(JNIEnv *env, jobject obj, const char *fieldName = "handle") { + jclass c = env->GetObjectClass(obj); + return env->GetFieldID(c, fieldName, "J"); // of type long +} + +template +T *getHandle(JNIEnv *env, jobject obj, const char *fieldName = "handle") { + jlong handle = env->GetLongField(obj, getHandleField(env, obj, fieldName)); + return reinterpret_cast(handle); +} + +void setHandleFromLong(JNIEnv *env, jobject obj, jlong handle) { + env->SetLongField(obj, getHandleField(env, obj), handle); +} + +template +void setHandle(JNIEnv *env, jobject obj, T *t) { + jlong handle = reinterpret_cast(t); + setHandleFromLong(env, obj, handle); +} + +#ifdef __cplusplus +extern "C" +{ +#endif + +extern const char* const MONERO_VERSION; // the actual monero core version + +// from monero-core crypto/hash-ops.h - avoid #including monero code here +enum { + HASH_SIZE = 32, + HASH_DATA_AREA = 136 +}; + +void cn_slow_hash(const void *data, size_t length, char *hash, int variant, int prehashed, uint64_t height); + +inline void slow_hash(const void *data, const size_t length, char *hash) { + cn_slow_hash(data, length, hash, 0 /*variant*/, 0 /*prehashed*/, 0 /*height*/); +} + +inline void slow_hash_broken(const void *data, char *hash, int variant) { + cn_slow_hash(data, 200 /*sizeof(union hash_state)*/, hash, variant, 1 /*prehashed*/, 0 /*height*/); +} + +#ifdef __cplusplus +} +#endif + +#endif //XMRWALLET_WALLET_LIB_H diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png new file mode 100644 index 0000000000000000000000000000000000000000..8047f23e4d4937a505e05cf8fc2b71950b2e8cf4 GIT binary patch literal 251291 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�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

  • 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%0D?KF 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 8); + output.write(channel); + output.write(TAG_APDU); + output.write(sequenceIdx >> 8); + output.write(sequenceIdx); + sequenceIdx++; + output.write(command.length >> 8); + output.write(command.length); + int blockSize = (command.length > packetSize - 7 ? packetSize - 7 : command.length); + output.write(command, offset, blockSize); + offset += blockSize; + while (offset != command.length) { + output.write(channel >> 8); + output.write(channel); + output.write(TAG_APDU); + output.write(sequenceIdx >> 8); + output.write(sequenceIdx); + sequenceIdx++; + blockSize = (command.length - offset > packetSize - 5 ? packetSize - 5 : command.length - offset); + output.write(command, offset, blockSize); + offset += blockSize; + } + if ((output.size() % packetSize) != 0) { + byte[] padding = new byte[packetSize - (output.size() % packetSize)]; + output.write(padding, 0, padding.length); + } + return output.toByteArray(); + } + + public static byte[] unwrapResponseAPDU(int channel, byte[] data, int packetSize) { + ByteArrayOutputStream response = new ByteArrayOutputStream(); + int offset = 0; + int responseLength; + int sequenceIdx = 0; + if ((data == null) || (data.length < 7 + 5)) { + return null; + } + if (data[offset++] != (channel >> 8)) { + throw new IllegalArgumentException("Invalid channel"); + } + if (data[offset++] != (channel & 0xff)) { + throw new IllegalArgumentException("Invalid channel"); + } + if (data[offset++] != TAG_APDU) { + throw new IllegalArgumentException("Invalid tag"); + } + if (data[offset++] != 0x00) { + throw new IllegalArgumentException("Invalid sequence"); + } + if (data[offset++] != 0x00) { + throw new IllegalArgumentException("Invalid sequence"); + } + responseLength = ((data[offset++] & 0xff) << 8); + responseLength |= (data[offset++] & 0xff); + if (data.length < 7 + responseLength) { + return null; + } + int blockSize = (responseLength > packetSize - 7 ? packetSize - 7 : responseLength); + response.write(data, offset, blockSize); + offset += blockSize; + while (response.size() != responseLength) { + sequenceIdx++; + if (offset == data.length) { + return null; + } + if (data[offset++] != (channel >> 8)) { + throw new IllegalArgumentException("Invalid channel"); + } + if (data[offset++] != (channel & 0xff)) { + throw new IllegalArgumentException("Invalid channel"); + } + if (data[offset++] != TAG_APDU) { + throw new IllegalArgumentException("Invalid tag"); + } + if (data[offset++] != (sequenceIdx >> 8)) { + throw new IllegalArgumentException("Invalid sequence"); + } + if (data[offset++] != (sequenceIdx & 0xff)) { + throw new IllegalArgumentException("Invalid sequence"); + } + blockSize = (responseLength - response.size() > packetSize - 5 ? packetSize - 5 : responseLength - response.size()); + if (blockSize > data.length - offset) { + return null; + } + response.write(data, offset, blockSize); + offset += blockSize; + } + return response.toByteArray(); + } + +} diff --git a/app/src/main/java/com/btchip/comm/android/BTChipTransportAndroidHID.java b/app/src/main/java/com/btchip/comm/android/BTChipTransportAndroidHID.java new file mode 100644 index 0000000..37c4f49 --- /dev/null +++ b/app/src/main/java/com/btchip/comm/android/BTChipTransportAndroidHID.java @@ -0,0 +1,149 @@ +/* + ******************************************************************************* + * BTChip Bitcoin Hardware Wallet Java API + * (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn + * (c) 2018 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************** + */ + +package com.btchip.comm.android; + +import android.hardware.usb.UsbConstants; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbDeviceConnection; +import android.hardware.usb.UsbEndpoint; +import android.hardware.usb.UsbInterface; +import android.hardware.usb.UsbManager; +import android.hardware.usb.UsbRequest; + +import com.btchip.BTChipException; +import com.btchip.comm.BTChipTransport; +import com.btchip.comm.LedgerHelper; +import com.btchip.utils.Dump; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.HashMap; + +import timber.log.Timber; + +public class BTChipTransportAndroidHID implements BTChipTransport { + + public static UsbDevice getDevice(UsbManager manager) { + HashMap deviceList = manager.getDeviceList(); + for (UsbDevice device : deviceList.values()) { + Timber.d("%04X:%04X %s, %s", device.getVendorId(), device.getProductId(), device.getManufacturerName(), device.getProductName()); + if (device.getVendorId() == VID) { + final int deviceProductId = device.getProductId(); + for (int pid : PID_HIDS) { + if (deviceProductId == pid) + return device; + } + } + } + return null; + } + + public static BTChipTransport open(UsbManager manager, UsbDevice device) throws IOException { + UsbDeviceConnection connection = manager.openDevice(device); + if (connection == null) throw new IOException("Device not connected"); + // Must only be called once permission is granted (see http://developer.android.com/reference/android/hardware/usb/UsbManager.html) + // Important if enumerating, rather than being awaken by the intent notification + UsbInterface dongleInterface = device.getInterface(0); + UsbEndpoint in = null; + UsbEndpoint out = null; + for (int i = 0; i < dongleInterface.getEndpointCount(); i++) { + UsbEndpoint tmpEndpoint = dongleInterface.getEndpoint(i); + if (tmpEndpoint.getDirection() == UsbConstants.USB_DIR_IN) { + in = tmpEndpoint; + } else { + out = tmpEndpoint; + } + } + connection.claimInterface(dongleInterface, true); + return new BTChipTransportAndroidHID(connection, dongleInterface, in, out); + } + + private static final int VID = 0x2C97; + private static final int[] PID_HIDS = {0x0001, 0x0004, 0x0005}; + + private UsbDeviceConnection connection; + private UsbInterface dongleInterface; + private UsbEndpoint in; + private UsbEndpoint out; + private byte transferBuffer[]; + private boolean debug; + + public BTChipTransportAndroidHID(UsbDeviceConnection connection, UsbInterface dongleInterface, UsbEndpoint in, UsbEndpoint out) { + this.connection = connection; + this.dongleInterface = dongleInterface; + this.in = in; + this.out = out; + transferBuffer = new byte[HID_BUFFER_SIZE]; + } + + @Override + public byte[] exchange(byte[] command) { + ByteArrayOutputStream response = new ByteArrayOutputStream(); + byte[] responseData = null; + int offset = 0; + if (debug) { + Timber.d("=> %s", Dump.dump(command)); + } + command = LedgerHelper.wrapCommandAPDU(LEDGER_DEFAULT_CHANNEL, command, HID_BUFFER_SIZE); + UsbRequest requestOut = new UsbRequest(); + requestOut.initialize(connection, out); + while (offset != command.length) { + int blockSize = (command.length - offset > HID_BUFFER_SIZE ? HID_BUFFER_SIZE : command.length - offset); + System.arraycopy(command, offset, transferBuffer, 0, blockSize); + requestOut.queue(ByteBuffer.wrap(transferBuffer), HID_BUFFER_SIZE); + connection.requestWait(); + offset += blockSize; + } + requestOut.close(); + ByteBuffer responseBuffer = ByteBuffer.allocate(HID_BUFFER_SIZE); + UsbRequest requestIn = new UsbRequest(); + requestIn.initialize(connection, in); + while ((responseData = LedgerHelper.unwrapResponseAPDU(LEDGER_DEFAULT_CHANNEL, response.toByteArray(), HID_BUFFER_SIZE)) == null) { + responseBuffer.clear(); + requestIn.queue(responseBuffer, HID_BUFFER_SIZE); + connection.requestWait(); + responseBuffer.rewind(); + responseBuffer.get(transferBuffer, 0, HID_BUFFER_SIZE); + response.write(transferBuffer, 0, HID_BUFFER_SIZE); + } + requestIn.close(); + if (debug) { + Timber.d("<= %s", Dump.dump(responseData)); + } + return responseData; + } + + @Override + public void close() { + connection.releaseInterface(dongleInterface); + connection.close(); + } + + @Override + public void setDebug(boolean debugFlag) { + this.debug = debugFlag; + } + + private static final int HID_BUFFER_SIZE = 64; + private static final int LEDGER_DEFAULT_CHANNEL = 1; + private static final int SW1_DATA_AVAILABLE = 0x61; +} diff --git a/app/src/main/java/com/btchip/utils/Dump.java b/app/src/main/java/com/btchip/utils/Dump.java new file mode 100644 index 0000000..2d453fc --- /dev/null +++ b/app/src/main/java/com/btchip/utils/Dump.java @@ -0,0 +1,62 @@ +/* + ******************************************************************************* + * BTChip Bitcoin Hardware Wallet Java API + * (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************** + */ + +package com.btchip.utils; + +import java.io.ByteArrayOutputStream; + +public class Dump { + + public static String dump(byte[] buffer, int offset, int length) { + String result = ""; + for (int i = 0; i < length; i++) { + String temp = Integer.toHexString((buffer[offset + i]) & 0xff); + if (temp.length() < 2) { + temp = "0" + temp; + } + result += temp; + } + return result; + } + + public static String dump(byte[] buffer) { + return dump(buffer, 0, buffer.length); + } + + public static byte[] hexToBin(String src) { + ByteArrayOutputStream result = new ByteArrayOutputStream(); + int i = 0; + while (i < src.length()) { + char x = src.charAt(i); + if (!((x >= '0' && x <= '9') || (x >= 'A' && x <= 'F') || (x >= 'a' && x <= 'f'))) { + i++; + continue; + } + try { + result.write(Integer.valueOf("" + src.charAt(i) + src.charAt(i + 1), 16)); + i += 2; + } catch (Exception e) { + return null; + } + } + return result.toByteArray(); + } + + +} diff --git a/app/src/main/java/com/m2049r/levin/data/Bucket.java b/app/src/main/java/com/m2049r/levin/data/Bucket.java new file mode 100644 index 0000000..fc9b571 --- /dev/null +++ b/app/src/main/java/com/m2049r/levin/data/Bucket.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2018 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.levin.data; + +import com.m2049r.levin.util.HexHelper; +import com.m2049r.levin.util.LevinReader; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +public class Bucket { + + // constants copied from monero p2p & epee + + public final static int P2P_COMMANDS_POOL_BASE = 1000; + public final static int COMMAND_HANDSHAKE_ID = P2P_COMMANDS_POOL_BASE + 1; + public final static int COMMAND_TIMED_SYNC_ID = P2P_COMMANDS_POOL_BASE + 2; + public final static int COMMAND_PING_ID = P2P_COMMANDS_POOL_BASE + 3; + public final static int COMMAND_REQUEST_STAT_INFO_ID = P2P_COMMANDS_POOL_BASE + 4; + public final static int COMMAND_REQUEST_NETWORK_STATE_ID = P2P_COMMANDS_POOL_BASE + 5; + public final static int COMMAND_REQUEST_PEER_ID_ID = P2P_COMMANDS_POOL_BASE + 6; + public final static int COMMAND_REQUEST_SUPPORT_FLAGS_ID = P2P_COMMANDS_POOL_BASE + 7; + + public final static long LEVIN_SIGNATURE = 0x0101010101012101L; // Bender's nightmare + + public final static long LEVIN_DEFAULT_MAX_PACKET_SIZE = 100000000; // 100MB by default + + public final static int LEVIN_PACKET_REQUEST = 0x00000001; + public final static int LEVIN_PACKET_RESPONSE = 0x00000002; + + public final static int LEVIN_PROTOCOL_VER_0 = 0; + public final static int LEVIN_PROTOCOL_VER_1 = 1; + + public final static int LEVIN_OK = 0; + public final static int LEVIN_ERROR_CONNECTION = -1; + public final static int LEVIN_ERROR_CONNECTION_NOT_FOUND = -2; + public final static int LEVIN_ERROR_CONNECTION_DESTROYED = -3; + public final static int LEVIN_ERROR_CONNECTION_TIMEDOUT = -4; + public final static int LEVIN_ERROR_CONNECTION_NO_DUPLEX_PROTOCOL = -5; + public final static int LEVIN_ERROR_CONNECTION_HANDLER_NOT_DEFINED = -6; + public final static int LEVIN_ERROR_FORMAT = -7; + + public final static int P2P_SUPPORT_FLAG_FLUFFY_BLOCKS = 0x01; + public final static int P2P_SUPPORT_FLAGS = P2P_SUPPORT_FLAG_FLUFFY_BLOCKS; + + final private long signature; + final private long cb; + final public boolean haveToReturnData; + final public int command; + final public int returnCode; + final private int flags; + final private int protcolVersion; + final byte[] payload; + + final public Section payloadSection; + + // create a request + public Bucket(int command, byte[] payload) throws IOException { + this.signature = LEVIN_SIGNATURE; + this.cb = payload.length; + this.haveToReturnData = true; + this.command = command; + this.returnCode = 0; + this.flags = LEVIN_PACKET_REQUEST; + this.protcolVersion = LEVIN_PROTOCOL_VER_1; + this.payload = payload; + payloadSection = LevinReader.readPayload(payload); + } + + // create a response + public Bucket(int command, byte[] payload, int rc) throws IOException { + this.signature = LEVIN_SIGNATURE; + this.cb = payload.length; + this.haveToReturnData = false; + this.command = command; + this.returnCode = rc; + this.flags = LEVIN_PACKET_RESPONSE; + this.protcolVersion = LEVIN_PROTOCOL_VER_1; + this.payload = payload; + payloadSection = LevinReader.readPayload(payload); + } + + public Bucket(DataInput in) throws IOException { + signature = in.readLong(); + cb = in.readLong(); + haveToReturnData = in.readBoolean(); + command = in.readInt(); + returnCode = in.readInt(); + flags = in.readInt(); + protcolVersion = in.readInt(); + + if (signature == Bucket.LEVIN_SIGNATURE) { + if (cb > Integer.MAX_VALUE) + throw new IllegalArgumentException(); + payload = new byte[(int) cb]; + in.readFully(payload); + } else + throw new IllegalStateException(); + payloadSection = LevinReader.readPayload(payload); + } + + public Section getPayloadSection() { + return payloadSection; + } + + public void send(DataOutput out) throws IOException { + out.writeLong(signature); + out.writeLong(cb); + out.writeBoolean(haveToReturnData); + out.writeInt(command); + out.writeInt(returnCode); + out.writeInt(flags); + out.writeInt(protcolVersion); + out.write(payload); + } + + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("sig: ").append(signature).append("\n"); + sb.append("cb: ").append(cb).append("\n"); + sb.append("call: ").append(haveToReturnData).append("\n"); + sb.append("cmd: ").append(command).append("\n"); + sb.append("rc: ").append(returnCode).append("\n"); + sb.append("flags:").append(flags).append("\n"); + sb.append("proto:").append(protcolVersion).append("\n"); + sb.append(HexHelper.bytesToHex(payload)).append("\n"); + sb.append(payloadSection.toString()); + return sb.toString(); + } +} diff --git a/app/src/main/java/com/m2049r/levin/data/Section.java b/app/src/main/java/com/m2049r/levin/data/Section.java new file mode 100644 index 0000000..9be9732 --- /dev/null +++ b/app/src/main/java/com/m2049r/levin/data/Section.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2018 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.levin.data; + +import com.m2049r.levin.util.HexHelper; +import com.m2049r.levin.util.LevinReader; +import com.m2049r.levin.util.LevinWriter; +import com.m2049r.levin.util.LittleEndianDataOutputStream; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutput; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class Section { + + // constants copied from monero p2p & epee + + static final public int PORTABLE_STORAGE_SIGNATUREA = 0x01011101; + static final public int PORTABLE_STORAGE_SIGNATUREB = 0x01020101; + + static final public byte PORTABLE_STORAGE_FORMAT_VER = 1; + + static final public byte PORTABLE_RAW_SIZE_MARK_MASK = 0x03; + static final public byte PORTABLE_RAW_SIZE_MARK_BYTE = 0; + static final public byte PORTABLE_RAW_SIZE_MARK_WORD = 1; + static final public byte PORTABLE_RAW_SIZE_MARK_DWORD = 2; + static final public byte PORTABLE_RAW_SIZE_MARK_INT64 = 3; + + static final long MAX_STRING_LEN_POSSIBLE = 2000000000; // do not let string be so big + + // data types + static final public byte SERIALIZE_TYPE_INT64 = 1; + static final public byte SERIALIZE_TYPE_INT32 = 2; + static final public byte SERIALIZE_TYPE_INT16 = 3; + static final public byte SERIALIZE_TYPE_INT8 = 4; + static final public byte SERIALIZE_TYPE_UINT64 = 5; + static final public byte SERIALIZE_TYPE_UINT32 = 6; + static final public byte SERIALIZE_TYPE_UINT16 = 7; + static final public byte SERIALIZE_TYPE_UINT8 = 8; + static final public byte SERIALIZE_TYPE_DUOBLE = 9; + static final public byte SERIALIZE_TYPE_STRING = 10; + static final public byte SERIALIZE_TYPE_BOOL = 11; + static final public byte SERIALIZE_TYPE_OBJECT = 12; + static final public byte SERIALIZE_TYPE_ARRAY = 13; + + static final public byte SERIALIZE_FLAG_ARRAY = (byte) 0x80; + + private final Map entries = new HashMap(); + + public void add(String key, Object entry) { + entries.put(key, entry); + } + + public int size() { + return entries.size(); + } + + public Set> entrySet() { + return entries.entrySet(); + } + + public Object get(String key) { + return entries.get(key); + } + + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("\n"); + for (Map.Entry entry : entries.entrySet()) { + sb.append(entry.getKey()).append("="); + final Object value = entry.getValue(); + if (value instanceof List) { + @SuppressWarnings("unchecked") final List list = (List) value; + for (Object listEntry : list) { + sb.append(listEntry.toString()).append("\n"); + } + } else if (value instanceof String) { + sb.append("(").append(value).append(")\n"); + } else if (value instanceof byte[]) { + sb.append(HexHelper.bytesToHex((byte[]) value)).append("\n"); + } else { + sb.append(value.toString()).append("\n"); + } + } + return sb.toString(); + } + + static public Section fromByteArray(byte[] buffer) { + try { + return LevinReader.readPayload(buffer); + } catch (IOException ex) { + throw new IllegalStateException(); + } + } + + public byte[] asByteArray() { + try { + ByteArrayOutputStream bas = new ByteArrayOutputStream(); + DataOutput out = new LittleEndianDataOutputStream(bas); + LevinWriter writer = new LevinWriter(out); + writer.writePayload(this); + return bas.toByteArray(); + } catch (IOException ex) { + throw new IllegalStateException(); + } + } +} diff --git a/app/src/main/java/com/m2049r/levin/scanner/Dispatcher.java b/app/src/main/java/com/m2049r/levin/scanner/Dispatcher.java new file mode 100644 index 0000000..b9f8be5 --- /dev/null +++ b/app/src/main/java/com/m2049r/levin/scanner/Dispatcher.java @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2018 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.levin.scanner; + +import com.m2049r.xmrwallet.data.NodeInfo; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import timber.log.Timber; + +public class Dispatcher implements PeerRetriever.OnGetPeers { + static final public int NUM_THREADS = 50; + static final public int MAX_PEERS = 1000; + static final public long MAX_TIME = 30000000000L; //30 seconds + + private int peerCount = 0; + final private Set knownNodes = new HashSet<>(); // set of nodes to test + final private Set rpcNodes = new HashSet<>(); // set of RPC nodes we like + final private ExecutorService exeService = Executors.newFixedThreadPool(NUM_THREADS); + + public interface Listener { + void onGet(NodeInfo nodeInfo); + } + + private Listener listener; + + public Dispatcher(Listener listener) { + this.listener = listener; + } + + public Set getRpcNodes() { + return rpcNodes; + } + + public int getPeerCount() { + return peerCount; + } + + public boolean getMorePeers() { + return peerCount < MAX_PEERS; + } + + public void awaitTermination(int nodesToFind) { + try { + final long t = System.nanoTime(); + while (!jobs.isEmpty()) { + try { + Timber.d("Remaining jobs %d", jobs.size()); + final PeerRetriever retrievedPeer = jobs.poll().get(); + if (retrievedPeer.isGood() && getMorePeers()) + retrievePeers(retrievedPeer); + final NodeInfo nodeInfo = retrievedPeer.getNodeInfo(); + Timber.d("Retrieved %s", nodeInfo); + if ((nodeInfo.isValid() || nodeInfo.isFavourite())) { + nodeInfo.setDefaultName(); + rpcNodes.add(nodeInfo); + Timber.d("RPC: %s", nodeInfo); + // the following is not totally correct but it works (otherwise we need to + // load much more before filtering - but we don't have time + if (listener != null) listener.onGet(nodeInfo); + if (rpcNodes.size() >= nodesToFind) { + Timber.d("are we done here?"); + filterRpcNodes(); + if (rpcNodes.size() >= nodesToFind) { + Timber.d("we're done here"); + break; + } + } + } + if (System.nanoTime() - t > MAX_TIME) break; // watchdog + } catch (ExecutionException ex) { + Timber.d(ex); // tell us about it and continue + } + } + } catch (InterruptedException ex) { + Timber.d(ex); + } finally { + Timber.d("Shutting down!"); + exeService.shutdownNow(); + try { + exeService.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS); + } catch (InterruptedException ex) { + Timber.d(ex); + } + } + filterRpcNodes(); + } + + static final public int HEIGHT_WINDOW = 1; + + private boolean testHeight(long height, long consensus) { + return (height >= (consensus - HEIGHT_WINDOW)) + && (height <= (consensus + HEIGHT_WINDOW)); + } + + private long calcConsensusHeight() { + Timber.d("Calc Consensus height from %d nodes", rpcNodes.size()); + final Map nodeHeights = new TreeMap(); + for (NodeInfo info : rpcNodes) { + if (!info.isValid()) continue; + Integer h = nodeHeights.get(info.getHeight()); + if (h == null) + h = 0; + nodeHeights.put(info.getHeight(), h + 1); + } + long consensusHeight = 0; + long consensusCount = 0; + for (Map.Entry entry : nodeHeights.entrySet()) { + final long entryHeight = entry.getKey(); + int count = 0; + for (long i = entryHeight - HEIGHT_WINDOW; i <= entryHeight + HEIGHT_WINDOW; i++) { + Integer v = nodeHeights.get(i); + if (v == null) + v = 0; + count += v; + } + if (count >= consensusCount) { + consensusCount = count; + consensusHeight = entryHeight; + } + Timber.d("%d - %d/%d", entryHeight, count, entry.getValue()); + } + return consensusHeight; + } + + private void filterRpcNodes() { + long consensus = calcConsensusHeight(); + Timber.d("Consensus Height = %d for %d nodes", consensus, rpcNodes.size()); + for (Iterator iter = rpcNodes.iterator(); iter.hasNext(); ) { + NodeInfo info = iter.next(); + // don't remove favourites + if (!info.isFavourite()) { + if (!testHeight(info.getHeight(), consensus)) { + iter.remove(); + Timber.d("Removed %s", info); + } + } + } + } + + // TODO: does this NEED to be a ConcurrentLinkedDeque? + private ConcurrentLinkedDeque> jobs = new ConcurrentLinkedDeque<>(); + + private void retrievePeer(NodeInfo nodeInfo) { + if (knownNodes.add(nodeInfo)) { + Timber.d("\t%d:%s", knownNodes.size(), nodeInfo); + jobs.add(exeService.submit(new PeerRetriever(nodeInfo, this))); + peerCount++; // jobs.size() does not perform well + } + } + + private void retrievePeers(PeerRetriever peer) { + for (LevinPeer levinPeer : peer.getPeers()) { + if (getMorePeers()) + retrievePeer(new NodeInfo(levinPeer)); + else + break; + } + } + + public void seedPeers(Collection seedNodes) { + for (NodeInfo node : seedNodes) { + if (node.isFavourite()) { + rpcNodes.add(node); + if (listener != null) listener.onGet(node); + } + retrievePeer(node); + } + } +} diff --git a/app/src/main/java/com/m2049r/levin/scanner/LevinPeer.java b/app/src/main/java/com/m2049r/levin/scanner/LevinPeer.java new file mode 100644 index 0000000..aba2fa8 --- /dev/null +++ b/app/src/main/java/com/m2049r/levin/scanner/LevinPeer.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2018 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.levin.scanner; + +import java.net.InetAddress; +import java.net.InetSocketAddress; + +public class LevinPeer { + final public InetSocketAddress socketAddress; + final public int version; + final public long height; + final public String top; + + + public InetSocketAddress getSocketAddress() { + return socketAddress; + } + + LevinPeer(InetAddress address, int port, int version, long height, String top) { + this.socketAddress = new InetSocketAddress(address, port); + this.version = version; + this.height = height; + this.top = top; + } +} diff --git a/app/src/main/java/com/m2049r/levin/scanner/PeerRetriever.java b/app/src/main/java/com/m2049r/levin/scanner/PeerRetriever.java new file mode 100644 index 0000000..fcdc4a0 --- /dev/null +++ b/app/src/main/java/com/m2049r/levin/scanner/PeerRetriever.java @@ -0,0 +1,231 @@ +/* + * Copyright (c) 2018 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.levin.scanner; + +import com.m2049r.levin.data.Bucket; +import com.m2049r.levin.data.Section; +import com.m2049r.levin.util.HexHelper; +import com.m2049r.levin.util.LittleEndianDataInputStream; +import com.m2049r.levin.util.LittleEndianDataOutputStream; +import com.m2049r.xmrwallet.data.NodeInfo; +import com.m2049r.xmrwallet.util.Helper; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Random; +import java.util.concurrent.Callable; + +import timber.log.Timber; + +public class PeerRetriever implements Callable { + static final public int CONNECT_TIMEOUT = 500; //ms + static final public int SOCKET_TIMEOUT = 500; //ms + static final public long PEER_ID = new Random().nextLong(); + static final private byte[] HANDSHAKE = handshakeRequest().asByteArray(); + static final private byte[] FLAGS_RESP = flagsResponse().asByteArray(); + + final private List peers = new ArrayList<>(); + + private NodeInfo nodeInfo; + private OnGetPeers onGetPeersCallback; + + public interface OnGetPeers { + boolean getMorePeers(); + } + + public PeerRetriever(NodeInfo nodeInfo, OnGetPeers onGetPeers) { + this.nodeInfo = nodeInfo; + this.onGetPeersCallback = onGetPeers; + } + + public NodeInfo getNodeInfo() { + return nodeInfo; + } + + public boolean isGood() { + return !peers.isEmpty(); + } + + public List getPeers() { + return peers; + } + + public PeerRetriever call() { + if (isGood()) // we have already been called? + throw new IllegalStateException(); + // first check for an rpc service + nodeInfo.findRpcService(); + if (onGetPeersCallback.getMorePeers()) + try { + Timber.d("%s CONN", nodeInfo.getLevinSocketAddress()); + if (!connect()) + return this; + Bucket handshakeBucket = new Bucket(Bucket.COMMAND_HANDSHAKE_ID, HANDSHAKE); + handshakeBucket.send(getDataOutput()); + + while (true) {// wait for response (which may never come) + Bucket recv = new Bucket(getDataInput()); // times out after SOCKET_TIMEOUT + if ((recv.command == Bucket.COMMAND_HANDSHAKE_ID) + && (!recv.haveToReturnData)) { + readAddressList(recv.payloadSection); + return this; + } else if ((recv.command == Bucket.COMMAND_REQUEST_SUPPORT_FLAGS_ID) + && (recv.haveToReturnData)) { + Bucket flagsBucket = new Bucket(Bucket.COMMAND_REQUEST_SUPPORT_FLAGS_ID, FLAGS_RESP, 1); + flagsBucket.send(getDataOutput()); + } else {// and ignore others + Timber.d("Ignored LEVIN COMMAND %d", recv.command); + } + } + } catch (IOException ex) { + } finally { + disconnect(); // we have what we want - byebye + Timber.d("%s DISCONN", nodeInfo.getLevinSocketAddress()); + } + return this; + } + + private void readAddressList(Section section) { + Section data = (Section) section.get("payload_data"); + int topVersion = (Integer) data.get("top_version"); + long currentHeight = (Long) data.get("current_height"); + String topId = HexHelper.bytesToHex((byte[]) data.get("top_id")); + Timber.d("PAYLOAD_DATA %d/%d/%s", topVersion, currentHeight, topId); + + @SuppressWarnings("unchecked") + List
    peerList = (List
    ) section.get("local_peerlist_new"); + if (peerList != null) { + for (Section peer : peerList) { + Section adr = (Section) peer.get("adr"); + Integer type = (Integer) adr.get("type"); + if ((type == null) || (type != 1)) + continue; + Section addr = (Section) adr.get("addr"); + if (addr == null) + continue; + Integer ip = (Integer) addr.get("m_ip"); + if (ip == null) + continue; + Integer sport = (Integer) addr.get("m_port"); + if (sport == null) + continue; + int port = sport; + if (port < 0) // port is unsigned + port = port + 0x10000; + InetAddress inet = HexHelper.toInetAddress(ip); + // make sure this is an address we want to talk to (i.e. a remote address) + if (!inet.isSiteLocalAddress() && !inet.isAnyLocalAddress() + && !inet.isLoopbackAddress() + && !inet.isMulticastAddress() + && !inet.isLinkLocalAddress()) { + peers.add(new LevinPeer(inet, port, topVersion, currentHeight, topId)); + } + } + } + } + + private Socket socket = null; + + private boolean connect() { + if (socket != null) throw new IllegalStateException(); + try { + socket = new Socket(); + socket.connect(nodeInfo.getLevinSocketAddress(), CONNECT_TIMEOUT); + socket.setSoTimeout(SOCKET_TIMEOUT); + } catch (IOException ex) { + //Timber.d(ex); + return false; + } + return true; + } + + private boolean isConnected() { + return socket.isConnected(); + } + + private void disconnect() { + try { + dataInput = null; + dataOutput = null; + if ((socket != null) && (!socket.isClosed())) { + socket.close(); + } + } catch (IOException ex) { + Timber.d(ex); + } finally { + socket = null; + } + } + + private DataOutput dataOutput = null; + + private DataOutput getDataOutput() throws IOException { + if (dataOutput == null) + synchronized (this) { + if (dataOutput == null) + dataOutput = new LittleEndianDataOutputStream( + socket.getOutputStream()); + } + return dataOutput; + } + + private DataInput dataInput = null; + + private DataInput getDataInput() throws IOException { + if (dataInput == null) + synchronized (this) { + if (dataInput == null) + dataInput = new LittleEndianDataInputStream( + socket.getInputStream()); + } + return dataInput; + } + + static private Section handshakeRequest() { + Section section = new Section(); // root object + + Section nodeData = new Section(); + nodeData.add("local_time", (new Date()).getTime()); + nodeData.add("my_port", 0); + byte[] networkId = Helper.hexToBytes("1230f171610441611731008216a1a110"); // mainnet + nodeData.add("network_id", networkId); + nodeData.add("peer_id", PEER_ID); + section.add("node_data", nodeData); + + Section payloadData = new Section(); + payloadData.add("cumulative_difficulty", 1L); + payloadData.add("current_height", 1L); + byte[] genesisHash = + Helper.hexToBytes("418015bb9ae982a1975da7d79277c2705727a56894ba0fb246adaabb1f4632e3"); + payloadData.add("top_id", genesisHash); + payloadData.add("top_version", (byte) 1); + section.add("payload_data", payloadData); + return section; + } + + static private Section flagsResponse() { + Section section = new Section(); // root object + section.add("support_flags", Bucket.P2P_SUPPORT_FLAGS); + return section; + } +} diff --git a/app/src/main/java/com/m2049r/levin/util/HexHelper.java b/app/src/main/java/com/m2049r/levin/util/HexHelper.java new file mode 100644 index 0000000..3c26527 --- /dev/null +++ b/app/src/main/java/com/m2049r/levin/util/HexHelper.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2018 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.levin.util; + +import java.math.BigInteger; +import java.net.InetAddress; +import java.net.UnknownHostException; + +public class HexHelper { + + static public String bytesToHex(byte[] data) { + if ((data != null) && (data.length > 0)) + return String.format("%0" + (data.length * 2) + "X", + new BigInteger(1, data)); + else + return ""; + } + + static public InetAddress toInetAddress(int ip) { + try { + String ipAddress = String.format("%d.%d.%d.%d", (ip & 0xff), + (ip >> 8 & 0xff), (ip >> 16 & 0xff), (ip >> 24 & 0xff)); + return InetAddress.getByName(ipAddress); + } catch (UnknownHostException ex) { + throw new IllegalArgumentException(ex); + } + } +} diff --git a/app/src/main/java/com/m2049r/levin/util/LevinReader.java b/app/src/main/java/com/m2049r/levin/util/LevinReader.java new file mode 100644 index 0000000..fd67ca5 --- /dev/null +++ b/app/src/main/java/com/m2049r/levin/util/LevinReader.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2018 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.levin.util; + +import com.m2049r.levin.data.Section; + +import java.io.ByteArrayInputStream; +import java.io.DataInput; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +// Full Levin reader as seen on epee + +public class LevinReader { + private DataInput in; + + private LevinReader(byte[] buffer) { + ByteArrayInputStream bis = new ByteArrayInputStream(buffer); + in = new LittleEndianDataInputStream(bis); + } + + static public Section readPayload(byte[] payload) throws IOException { + LevinReader r = new LevinReader(payload); + return r.readPayload(); + } + + private Section readPayload() throws IOException { + if (in.readInt() != Section.PORTABLE_STORAGE_SIGNATUREA) + throw new IllegalStateException(); + if (in.readInt() != Section.PORTABLE_STORAGE_SIGNATUREB) + throw new IllegalStateException(); + if (in.readByte() != Section.PORTABLE_STORAGE_FORMAT_VER) + throw new IllegalStateException(); + return readSection(); + } + + private Section readSection() throws IOException { + Section section = new Section(); + long count = readVarint(); + while (count-- > 0) { + // read section name string + String sectionName = readSectionName(); + section.add(sectionName, loadStorageEntry()); + } + return section; + } + + private Object loadStorageArrayEntry(int type) throws IOException { + type &= ~Section.SERIALIZE_FLAG_ARRAY; + return readArrayEntry(type); + } + + private List readArrayEntry(int type) throws IOException { + List list = new ArrayList(); + long size = readVarint(); + while (size-- > 0) + list.add(read(type)); + return list; + } + + private Object read(int type) throws IOException { + switch (type) { + case Section.SERIALIZE_TYPE_UINT64: + case Section.SERIALIZE_TYPE_INT64: + return in.readLong(); + case Section.SERIALIZE_TYPE_UINT32: + case Section.SERIALIZE_TYPE_INT32: + return in.readInt(); + case Section.SERIALIZE_TYPE_UINT16: + return in.readUnsignedShort(); + case Section.SERIALIZE_TYPE_INT16: + return in.readShort(); + case Section.SERIALIZE_TYPE_UINT8: + return in.readUnsignedByte(); + case Section.SERIALIZE_TYPE_INT8: + return in.readByte(); + case Section.SERIALIZE_TYPE_OBJECT: + return readSection(); + case Section.SERIALIZE_TYPE_STRING: + return readByteArray(); + default: + throw new IllegalArgumentException("type " + type + + " not supported"); + } + } + + private Object loadStorageEntry() throws IOException { + int type = in.readUnsignedByte(); + if ((type & Section.SERIALIZE_FLAG_ARRAY) != 0) + return loadStorageArrayEntry(type); + if (type == Section.SERIALIZE_TYPE_ARRAY) + return readStorageEntryArrayEntry(); + else + return readStorageEntry(type); + } + + private Object readStorageEntry(int type) throws IOException { + return read(type); + } + + private Object readStorageEntryArrayEntry() throws IOException { + int type = in.readUnsignedByte(); + if ((type & Section.SERIALIZE_FLAG_ARRAY) != 0) + throw new IllegalStateException("wrong type sequences"); + return loadStorageArrayEntry(type); + } + + private String readSectionName() throws IOException { + int nameLen = in.readUnsignedByte(); + return readString(nameLen); + } + + private byte[] read(long count) throws IOException { + if (count > Integer.MAX_VALUE) + throw new IllegalArgumentException(); + int len = (int) count; + final byte buffer[] = new byte[len]; + in.readFully(buffer); + return buffer; + } + + private String readString(long count) throws IOException { + return new String(read(count), StandardCharsets.US_ASCII); + } + + private byte[] readByteArray(long count) throws IOException { + return read(count); + } + + private byte[] readByteArray() throws IOException { + long len = readVarint(); + return readByteArray(len); + } + + private long readVarint() throws IOException { + long v = 0; + int b = in.readUnsignedByte(); + int sizeMask = b & Section.PORTABLE_RAW_SIZE_MARK_MASK; + switch (sizeMask) { + case Section.PORTABLE_RAW_SIZE_MARK_BYTE: + v = b >>> 2; + break; + case Section.PORTABLE_RAW_SIZE_MARK_WORD: + v = readRest(b, 1) >>> 2; + break; + case Section.PORTABLE_RAW_SIZE_MARK_DWORD: + v = readRest(b, 3) >>> 2; + break; + case Section.PORTABLE_RAW_SIZE_MARK_INT64: + v = readRest(b, 7) >>> 2; + break; + default: + throw new IllegalStateException(); + } + return v; + } + + // this should be in LittleEndianDataInputStream because it has little + // endian logic + private long readRest(final int firstByte, final int bytes) throws IOException { + long result = firstByte; + for (int i = 1; i < bytes + 1; i++) { + result = result + (((long) in.readUnsignedByte()) << (8 * i)); + } + return result; + } + +} diff --git a/app/src/main/java/com/m2049r/levin/util/LevinWriter.java b/app/src/main/java/com/m2049r/levin/util/LevinWriter.java new file mode 100644 index 0000000..ad2fe32 --- /dev/null +++ b/app/src/main/java/com/m2049r/levin/util/LevinWriter.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2018 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.levin.util; + +import com.m2049r.levin.data.Section; + +import java.io.DataOutput; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +// a simplified Levin Writer WITHOUT support for arrays + +public class LevinWriter { + private DataOutput out; + + public LevinWriter(DataOutput out) { + this.out = out; + } + + public void writePayload(Section section) throws IOException { + out.writeInt(Section.PORTABLE_STORAGE_SIGNATUREA); + out.writeInt(Section.PORTABLE_STORAGE_SIGNATUREB); + out.writeByte(Section.PORTABLE_STORAGE_FORMAT_VER); + putSection(section); + } + + private void writeSection(Section section) throws IOException { + out.writeByte(Section.SERIALIZE_TYPE_OBJECT); + putSection(section); + } + + private void putSection(Section section) throws IOException { + writeVarint(section.size()); + for (Map.Entry kv : section.entrySet()) { + byte[] key = kv.getKey().getBytes(StandardCharsets.US_ASCII); + out.writeByte(key.length); + out.write(key); + write(kv.getValue()); + } + } + + private void writeVarint(long i) throws IOException { + if (i <= 63) { + out.writeByte(((int) i << 2) | Section.PORTABLE_RAW_SIZE_MARK_BYTE); + } else if (i <= 16383) { + out.writeShort(((int) i << 2) | Section.PORTABLE_RAW_SIZE_MARK_WORD); + } else if (i <= 1073741823) { + out.writeInt(((int) i << 2) | Section.PORTABLE_RAW_SIZE_MARK_DWORD); + } else { + if (i > 4611686018427387903L) + throw new IllegalArgumentException(); + out.writeLong((i << 2) | Section.PORTABLE_RAW_SIZE_MARK_INT64); + } + } + + private void write(Object object) throws IOException { + if (object instanceof byte[]) { + byte[] value = (byte[]) object; + out.writeByte(Section.SERIALIZE_TYPE_STRING); + writeVarint(value.length); + out.write(value); + } else if (object instanceof String) { + byte[] value = ((String) object) + .getBytes(StandardCharsets.US_ASCII); + out.writeByte(Section.SERIALIZE_TYPE_STRING); + writeVarint(value.length); + out.write(value); + } else if (object instanceof Integer) { + out.writeByte(Section.SERIALIZE_TYPE_UINT32); + out.writeInt((int) object); + } else if (object instanceof Long) { + out.writeByte(Section.SERIALIZE_TYPE_UINT64); + out.writeLong((long) object); + } else if (object instanceof Byte) { + out.writeByte(Section.SERIALIZE_TYPE_UINT8); + out.writeByte((byte) object); + } else if (object instanceof Section) { + writeSection((Section) object); + } else { + throw new IllegalArgumentException(); + } + } +} diff --git a/app/src/main/java/com/m2049r/levin/util/LittleEndianDataInputStream.java b/app/src/main/java/com/m2049r/levin/util/LittleEndianDataInputStream.java new file mode 100644 index 0000000..3924eeb --- /dev/null +++ b/app/src/main/java/com/m2049r/levin/util/LittleEndianDataInputStream.java @@ -0,0 +1,564 @@ +/* + * Copyright (c) 2018 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.levin.util; + +import java.io.DataInput; +import java.io.EOFException; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UTFDataFormatException; + +/** + * A little endian java.io.DataInputStream (without readLine()) + */ + +public class LittleEndianDataInputStream extends FilterInputStream implements + DataInput { + + /** + * Creates a DataInputStream that uses the specified underlying InputStream. + * + * @param in the specified input stream + */ + public LittleEndianDataInputStream(InputStream in) { + super(in); + } + + @Deprecated + public final String readLine() { + throw new UnsupportedOperationException(); + } + + /** + * Reads some number of bytes from the contained input stream and stores + * them into the buffer array b. The number of bytes actually + * read is returned as an integer. This method blocks until input data is + * available, end of file is detected, or an exception is thrown. + * + *

    + * If b is null, a NullPointerException is thrown. + * If the length of b is zero, then no bytes are read and + * 0 is returned; otherwise, there is an attempt to read at + * least one byte. If no byte is available because the stream is at end of + * file, the value -1 is returned; otherwise, at least one byte + * is read and stored into b. + * + *

    + * The first byte read is stored into element b[0], the next + * one into b[1], and so on. The number of bytes read is, at + * most, equal to the length of b. Let k be the + * number of bytes actually read; these bytes will be stored in elements + * b[0] through b[k-1], leaving elements + * b[k] through b[b.length-1] unaffected. + * + *

    + * The read(b) method has the same effect as:

    + * + *
    +     * read(b, 0, b.length)
    +     * 
    + * + *
    + * + * @param b the buffer into which the data is read. + * @return the total number of bytes read into the buffer, or + * -1 if there is no more data because the end of the + * stream has been reached. + * @throws IOException if the first byte cannot be read for any reason other than + * end of file, the stream has been closed and the underlying + * input stream does not support reading after close, or + * another I/O error occurs. + * @see FilterInputStream#in + * @see InputStream#read(byte[], int, int) + */ + public final int read(byte b[]) throws IOException { + return in.read(b, 0, b.length); + } + + /** + * Reads up to len bytes of data from the contained input + * stream into an array of bytes. An attempt is made to read as many as + * len bytes, but a smaller number may be read, possibly zero. + * The number of bytes actually read is returned as an integer. + * + *

    + * This method blocks until input data is available, end of file is + * detected, or an exception is thrown. + * + *

    + * If len is zero, then no bytes are read and 0 is + * returned; otherwise, there is an attempt to read at least one byte. If no + * byte is available because the stream is at end of file, the value + * -1 is returned; otherwise, at least one byte is read and + * stored into b. + * + *

    + * The first byte read is stored into element b[off], the next + * one into b[off+1], and so on. The number of bytes read is, + * at most, equal to len. Let k be the number of bytes + * actually read; these bytes will be stored in elements b[off] + * through b[off+k-1], leaving elements + * b[off+k] through + * b[off+len-1] unaffected. + * + *

    + * In every case, elements b[0] through b[off] and + * elements b[off+len] through b[b.length-1] are + * unaffected. + * + * @param b the buffer into which the data is read. + * @param off the start offset in the destination array b + * @param len the maximum number of bytes read. + * @return the total number of bytes read into the buffer, or + * -1 if there is no more data because the end of the + * stream has been reached. + * @throws NullPointerException If b is null. + * @throws IndexOutOfBoundsException If off is negative, len is + * negative, or len is greater than + * b.length - off + * @throws IOException if the first byte cannot be read for any reason other than + * end of file, the stream has been closed and the underlying + * input stream does not support reading after close, or + * another I/O error occurs. + * @see FilterInputStream#in + * @see InputStream#read(byte[], int, int) + */ + public final int read(byte b[], int off, int len) throws IOException { + return in.read(b, off, len); + } + + /** + * See the general contract of the readFully method of + * DataInput. + *

    + * Bytes for this operation are read from the contained input stream. + * + * @param b the buffer into which the data is read. + * @throws EOFException if this input stream reaches the end before reading all + * the bytes. + * @throws IOException the stream has been closed and the contained input stream + * does not support reading after close, or another I/O error + * occurs. + * @see FilterInputStream#in + */ + public final void readFully(byte b[]) throws IOException { + readFully(b, 0, b.length); + } + + /** + * See the general contract of the readFully method of + * DataInput. + *

    + * Bytes for this operation are read from the contained input stream. + * + * @param b the buffer into which the data is read. + * @param off the start offset of the data. + * @param len the number of bytes to read. + * @throws EOFException if this input stream reaches the end before reading all + * the bytes. + * @throws IOException the stream has been closed and the contained input stream + * does not support reading after close, or another I/O error + * occurs. + * @see FilterInputStream#in + */ + public final void readFully(byte b[], int off, int len) throws IOException { + if (len < 0) + throw new IndexOutOfBoundsException(); + int n = 0; + while (n < len) { + int count = in.read(b, off + n, len - n); + if (count < 0) + throw new EOFException(); + n += count; + } + } + + /** + * See the general contract of the skipBytes method of + * DataInput. + *

    + * Bytes for this operation are read from the contained input stream. + * + * @param n the number of bytes to be skipped. + * @return the actual number of bytes skipped. + * @throws IOException if the contained input stream does not support seek, or + * the stream has been closed and the contained input stream + * does not support reading after close, or another I/O error + * occurs. + */ + public final int skipBytes(int n) throws IOException { + int total = 0; + int cur = 0; + + while ((total < n) && ((cur = (int) in.skip(n - total)) > 0)) { + total += cur; + } + + return total; + } + + /** + * See the general contract of the readBoolean method of + * DataInput. + *

    + * Bytes for this operation are read from the contained input stream. + * + * @return the boolean value read. + * @throws EOFException if this input stream has reached the end. + * @throws IOException the stream has been closed and the contained input stream + * does not support reading after close, or another I/O error + * occurs. + * @see FilterInputStream#in + */ + public final boolean readBoolean() throws IOException { + int ch = in.read(); + if (ch < 0) + throw new EOFException(); + return (ch != 0); + } + + /** + * See the general contract of the readByte method of + * DataInput. + *

    + * Bytes for this operation are read from the contained input stream. + * + * @return the next byte of this input stream as a signed 8-bit + * byte. + * @throws EOFException if this input stream has reached the end. + * @throws IOException the stream has been closed and the contained input stream + * does not support reading after close, or another I/O error + * occurs. + * @see FilterInputStream#in + */ + public final byte readByte() throws IOException { + int ch = in.read(); + if (ch < 0) + throw new EOFException(); + return (byte) (ch); + } + + /** + * See the general contract of the readUnsignedByte method of + * DataInput. + *

    + * Bytes for this operation are read from the contained input stream. + * + * @return the next byte of this input stream, interpreted as an unsigned + * 8-bit number. + * @throws EOFException if this input stream has reached the end. + * @throws IOException the stream has been closed and the contained input stream + * does not support reading after close, or another I/O error + * occurs. + * @see FilterInputStream#in + */ + public final int readUnsignedByte() throws IOException { + int ch = in.read(); + if (ch < 0) + throw new EOFException(); + return ch; + } + + /** + * See the general contract of the readShort method of + * DataInput. + *

    + * Bytes for this operation are read from the contained input stream. + * + * @return the next two bytes of this input stream, interpreted as a signed + * 16-bit number. + * @throws EOFException if this input stream reaches the end before reading two + * bytes. + * @throws IOException the stream has been closed and the contained input stream + * does not support reading after close, or another I/O error + * occurs. + * @see FilterInputStream#in + */ + public final short readShort() throws IOException { + int ch1 = in.read(); + int ch2 = in.read(); + if ((ch1 | ch2) < 0) + throw new EOFException(); + return (short) ((ch1 << 0) + (ch2 << 8)); + } + + /** + * See the general contract of the readUnsignedShort method of + * DataInput. + *

    + * Bytes for this operation are read from the contained input stream. + * + * @return the next two bytes of this input stream, interpreted as an + * unsigned 16-bit integer. + * @throws EOFException if this input stream reaches the end before reading two + * bytes. + * @throws IOException the stream has been closed and the contained input stream + * does not support reading after close, or another I/O error + * occurs. + * @see FilterInputStream#in + */ + public final int readUnsignedShort() throws IOException { + int ch1 = in.read(); + int ch2 = in.read(); + if ((ch1 | ch2) < 0) + throw new EOFException(); + return (ch1 << 0) + (ch2 << 8); + } + + /** + * See the general contract of the readChar method of + * DataInput. + *

    + * Bytes for this operation are read from the contained input stream. + * + * @return the next two bytes of this input stream, interpreted as a + * char. + * @throws EOFException if this input stream reaches the end before reading two + * bytes. + * @throws IOException the stream has been closed and the contained input stream + * does not support reading after close, or another I/O error + * occurs. + * @see FilterInputStream#in + */ + public final char readChar() throws IOException { + int ch1 = in.read(); + int ch2 = in.read(); + if ((ch1 | ch2) < 0) + throw new EOFException(); + return (char) ((ch1 << 0) + (ch2 << 8)); + } + + /** + * See the general contract of the readInt method of + * DataInput. + *

    + * Bytes for this operation are read from the contained input stream. + * + * @return the next four bytes of this input stream, interpreted as an + * int. + * @throws EOFException if this input stream reaches the end before reading four + * bytes. + * @throws IOException the stream has been closed and the contained input stream + * does not support reading after close, or another I/O error + * occurs. + * @see FilterInputStream#in + */ + public final int readInt() throws IOException { + int ch1 = in.read(); + int ch2 = in.read(); + int ch3 = in.read(); + int ch4 = in.read(); + if ((ch1 | ch2 | ch3 | ch4) < 0) + throw new EOFException(); + return ((ch1 << 0) + (ch2 << 8) + (ch3 << 16) + (ch4 << 24)); + } + + private byte readBuffer[] = new byte[8]; + + /** + * See the general contract of the readLong method of + * DataInput. + *

    + * Bytes for this operation are read from the contained input stream. + * + * @return the next eight bytes of this input stream, interpreted as a + * long. + * @throws EOFException if this input stream reaches the end before reading eight + * bytes. + * @throws IOException the stream has been closed and the contained input stream + * does not support reading after close, or another I/O error + * occurs. + * @see FilterInputStream#in + */ + public final long readLong() throws IOException { + readFully(readBuffer, 0, 8); + return (((long) readBuffer[7] << 56) + + ((long) (readBuffer[6] & 255) << 48) + + ((long) (readBuffer[5] & 255) << 40) + + ((long) (readBuffer[4] & 255) << 32) + + ((long) (readBuffer[3] & 255) << 24) + + ((readBuffer[2] & 255) << 16) + ((readBuffer[1] & 255) << 8) + ((readBuffer[0] & 255) << 0)); + } + + /** + * See the general contract of the readFloat method of + * DataInput. + *

    + * Bytes for this operation are read from the contained input stream. + * + * @return the next four bytes of this input stream, interpreted as a + * float. + * @throws EOFException if this input stream reaches the end before reading four + * bytes. + * @throws IOException the stream has been closed and the contained input stream + * does not support reading after close, or another I/O error + * occurs. + * @see java.io.DataInputStream#readInt() + * @see Float#intBitsToFloat(int) + */ + public final float readFloat() throws IOException { + return Float.intBitsToFloat(readInt()); + } + + /** + * See the general contract of the readDouble method of + * DataInput. + *

    + * Bytes for this operation are read from the contained input stream. + * + * @return the next eight bytes of this input stream, interpreted as a + * double. + * @throws EOFException if this input stream reaches the end before reading eight + * bytes. + * @throws IOException the stream has been closed and the contained input stream + * does not support reading after close, or another I/O error + * occurs. + * @see java.io.DataInputStream#readLong() + * @see Double#longBitsToDouble(long) + */ + public final double readDouble() throws IOException { + return Double.longBitsToDouble(readLong()); + } + + /** + * See the general contract of the readUTF method of + * DataInput. + *

    + * Bytes for this operation are read from the contained input stream. + * + * @return a Unicode string. + * @throws EOFException if this input stream reaches the end before reading all + * the bytes. + * @throws IOException the stream has been closed and the contained input stream + * does not support reading after close, or another I/O error + * occurs. + * @throws UTFDataFormatException if the bytes do not represent a valid modified UTF-8 + * encoding of a string. + * @see java.io.DataInputStream#readUTF(DataInput) + */ + public final String readUTF() throws IOException { + return readUTF(this); + } + + /** + * working arrays initialized on demand by readUTF + */ + private byte bytearr[] = new byte[80]; + private char chararr[] = new char[80]; + + /** + * Reads from the stream in a representation of a Unicode + * character string encoded in modified UTF-8 format; this + * string of characters is then returned as a String. The + * details of the modified UTF-8 representation are exactly the same as for + * the readUTF method of DataInput. + * + * @param in a data input stream. + * @return a Unicode string. + * @throws EOFException if the input stream reaches the end before all the bytes. + * @throws IOException the stream has been closed and the contained input stream + * does not support reading after close, or another I/O error + * occurs. + * @throws UTFDataFormatException if the bytes do not represent a valid modified UTF-8 + * encoding of a Unicode string. + * @see java.io.DataInputStream#readUnsignedShort() + */ + public final static String readUTF(DataInput in) throws IOException { + int utflen = in.readUnsignedShort(); + byte[] bytearr = null; + char[] chararr = null; + if (in instanceof LittleEndianDataInputStream) { + LittleEndianDataInputStream dis = (LittleEndianDataInputStream) in; + if (dis.bytearr.length < utflen) { + dis.bytearr = new byte[utflen * 2]; + dis.chararr = new char[utflen * 2]; + } + chararr = dis.chararr; + bytearr = dis.bytearr; + } else { + bytearr = new byte[utflen]; + chararr = new char[utflen]; + } + + int c, char2, char3; + int count = 0; + int chararr_count = 0; + + in.readFully(bytearr, 0, utflen); + + while (count < utflen) { + c = (int) bytearr[count] & 0xff; + if (c > 127) + break; + count++; + chararr[chararr_count++] = (char) c; + } + + while (count < utflen) { + c = (int) bytearr[count] & 0xff; + switch (c >> 4) { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + /* 0xxxxxxx */ + count++; + chararr[chararr_count++] = (char) c; + break; + case 12: + case 13: + /* 110x xxxx 10xx xxxx */ + count += 2; + if (count > utflen) + throw new UTFDataFormatException( + "malformed input: partial character at end"); + char2 = (int) bytearr[count - 1]; + if ((char2 & 0xC0) != 0x80) + throw new UTFDataFormatException( + "malformed input around byte " + count); + chararr[chararr_count++] = (char) (((c & 0x1F) << 6) | (char2 & 0x3F)); + break; + case 14: + /* 1110 xxxx 10xx xxxx 10xx xxxx */ + count += 3; + if (count > utflen) + throw new UTFDataFormatException( + "malformed input: partial character at end"); + char2 = (int) bytearr[count - 2]; + char3 = (int) bytearr[count - 1]; + if (((char2 & 0xC0) != 0x80) || ((char3 & 0xC0) != 0x80)) + throw new UTFDataFormatException( + "malformed input around byte " + (count - 1)); + chararr[chararr_count++] = (char) (((c & 0x0F) << 12) + | ((char2 & 0x3F) << 6) | ((char3 & 0x3F) << 0)); + break; + default: + /* 10xx xxxx, 1111 xxxx */ + throw new UTFDataFormatException("malformed input around byte " + + count); + } + } + // The number of chars produced may be less than utflen + return new String(chararr, 0, chararr_count); + } +} diff --git a/app/src/main/java/com/m2049r/levin/util/LittleEndianDataOutputStream.java b/app/src/main/java/com/m2049r/levin/util/LittleEndianDataOutputStream.java new file mode 100644 index 0000000..fbf7e0c --- /dev/null +++ b/app/src/main/java/com/m2049r/levin/util/LittleEndianDataOutputStream.java @@ -0,0 +1,403 @@ +/* + * Copyright (c) 2018 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.levin.util; + +import java.io.DataOutput; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UTFDataFormatException; + +/** + * A little endian java.io.DataOutputStream + */ + +public class LittleEndianDataOutputStream extends FilterOutputStream implements + DataOutput { + + /** + * The number of bytes written to the data output stream so far. If this + * counter overflows, it will be wrapped to Integer.MAX_VALUE. + */ + protected int written; + + /** + * Creates a new data output stream to write data to the specified + * underlying output stream. The counter written is set to + * zero. + * + * @param out the underlying output stream, to be saved for later use. + * @see FilterOutputStream#out + */ + public LittleEndianDataOutputStream(OutputStream out) { + super(out); + } + + /** + * Increases the written counter by the specified value until it reaches + * Integer.MAX_VALUE. + */ + private void incCount(int value) { + int temp = written + value; + if (temp < 0) { + temp = Integer.MAX_VALUE; + } + written = temp; + } + + /** + * Writes the specified byte (the low eight bits of the argument + * b) to the underlying output stream. If no exception is + * thrown, the counter written is incremented by 1 + * . + *

    + * Implements the write method of OutputStream. + * + * @param b the byte to be written. + * @throws IOException if an I/O error occurs. + * @see FilterOutputStream#out + */ + public synchronized void write(int b) throws IOException { + out.write(b); + incCount(1); + } + + /** + * Writes len bytes from the specified byte array starting at + * offset off to the underlying output stream. If no exception + * is thrown, the counter written is incremented by + * len. + * + * @param b the data. + * @param off the start offset in the data. + * @param len the number of bytes to write. + * @throws IOException if an I/O error occurs. + * @see FilterOutputStream#out + */ + public synchronized void write(byte b[], int off, int len) + throws IOException { + out.write(b, off, len); + incCount(len); + } + + /** + * Flushes this data output stream. This forces any buffered output bytes to + * be written out to the stream. + *

    + * The flush method of DataOutputStream calls the + * flush method of its underlying output stream. + * + * @throws IOException if an I/O error occurs. + * @see FilterOutputStream#out + * @see OutputStream#flush() + */ + public void flush() throws IOException { + out.flush(); + } + + /** + * Writes a boolean to the underlying output stream as a 1-byte + * value. The value true is written out as the value + * (byte)1; the value false is written out as the + * value (byte)0. If no exception is thrown, the counter + * written is incremented by 1. + * + * @param v a boolean value to be written. + * @throws IOException if an I/O error occurs. + * @see FilterOutputStream#out + */ + public final void writeBoolean(boolean v) throws IOException { + out.write(v ? 1 : 0); + incCount(1); + } + + /** + * Writes out a byte to the underlying output stream as a + * 1-byte value. If no exception is thrown, the counter written + * is incremented by 1. + * + * @param v a byte value to be written. + * @throws IOException if an I/O error occurs. + * @see FilterOutputStream#out + */ + public final void writeByte(int v) throws IOException { + out.write(v); + incCount(1); + } + + /** + * Writes a short to the underlying output stream as two bytes, + * low byte first. If no exception is thrown, the counter + * written is incremented by 2. + * + * @param v a short to be written. + * @throws IOException if an I/O error occurs. + * @see FilterOutputStream#out + */ + public final void writeShort(int v) throws IOException { + out.write((v >>> 0) & 0xFF); + out.write((v >>> 8) & 0xFF); + incCount(2); + } + + /** + * Writes a char to the underlying output stream as a 2-byte + * value, low byte first. If no exception is thrown, the counter + * written is incremented by 2. + * + * @param v a char value to be written. + * @throws IOException if an I/O error occurs. + * @see FilterOutputStream#out + */ + public final void writeChar(int v) throws IOException { + out.write((v >>> 0) & 0xFF); + out.write((v >>> 8) & 0xFF); + incCount(2); + } + + /** + * Writes an int to the underlying output stream as four bytes, + * low byte first. If no exception is thrown, the counter + * written is incremented by 4. + * + * @param v an int to be written. + * @throws IOException if an I/O error occurs. + * @see FilterOutputStream#out + */ + public final void writeInt(int v) throws IOException { + out.write((v >>> 0) & 0xFF); + out.write((v >>> 8) & 0xFF); + out.write((v >>> 16) & 0xFF); + out.write((v >>> 24) & 0xFF); + incCount(4); + } + + private byte writeBuffer[] = new byte[8]; + + /** + * Writes a long to the underlying output stream as eight + * bytes, low byte first. In no exception is thrown, the counter + * written is incremented by 8. + * + * @param v a long to be written. + * @throws IOException if an I/O error occurs. + * @see FilterOutputStream#out + */ + public final void writeLong(long v) throws IOException { + writeBuffer[7] = (byte) (v >>> 56); + writeBuffer[6] = (byte) (v >>> 48); + writeBuffer[5] = (byte) (v >>> 40); + writeBuffer[4] = (byte) (v >>> 32); + writeBuffer[3] = (byte) (v >>> 24); + writeBuffer[2] = (byte) (v >>> 16); + writeBuffer[1] = (byte) (v >>> 8); + writeBuffer[0] = (byte) (v >>> 0); + out.write(writeBuffer, 0, 8); + incCount(8); + } + + /** + * Converts the float argument to an int using the + * floatToIntBits method in class Float, and then + * writes that int value to the underlying output stream as a + * 4-byte quantity, low byte first. If no exception is thrown, the counter + * written is incremented by 4. + * + * @param v a float value to be written. + * @throws IOException if an I/O error occurs. + * @see FilterOutputStream#out + * @see Float#floatToIntBits(float) + */ + public final void writeFloat(float v) throws IOException { + writeInt(Float.floatToIntBits(v)); + } + + /** + * Converts the double argument to a long using the + * doubleToLongBits method in class Double, and + * then writes that long value to the underlying output stream + * as an 8-byte quantity, low byte first. If no exception is thrown, the + * counter written is incremented by 8. + * + * @param v a double value to be written. + * @throws IOException if an I/O error occurs. + * @see FilterOutputStream#out + * @see Double#doubleToLongBits(double) + */ + public final void writeDouble(double v) throws IOException { + writeLong(Double.doubleToLongBits(v)); + } + + /** + * Writes out the string to the underlying output stream as a sequence of + * bytes. Each character in the string is written out, in sequence, by + * discarding its high eight bits. If no exception is thrown, the counter + * written is incremented by the length of s. + * + * @param s a string of bytes to be written. + * @throws IOException if an I/O error occurs. + * @see FilterOutputStream#out + */ + public final void writeBytes(String s) throws IOException { + int len = s.length(); + for (int i = 0; i < len; i++) { + out.write((byte) s.charAt(i)); + } + incCount(len); + } + + /** + * Writes a string to the underlying output stream as a sequence of + * characters. Each character is written to the data output stream as if by + * the writeChar method. If no exception is thrown, the counter + * written is incremented by twice the length of s + * . + * + * @param s a String value to be written. + * @throws IOException if an I/O error occurs. + * @see java.io.DataOutputStream#writeChar(int) + * @see FilterOutputStream#out + */ + public final void writeChars(String s) throws IOException { + int len = s.length(); + for (int i = 0; i < len; i++) { + int v = s.charAt(i); + out.write((v >>> 0) & 0xFF); + out.write((v >>> 8) & 0xFF); + } + incCount(len * 2); + } + + /** + * Writes a string to the underlying output stream using modified UTF-8 encoding in a + * machine-independent manner. + *

    + * First, two bytes are written to the output stream as if by the + * writeShort method giving the number of bytes to follow. This + * value is the number of bytes actually written out, not the length of the + * string. Following the length, each character of the string is output, in + * sequence, using the modified UTF-8 encoding for the character. If no + * exception is thrown, the counter written is incremented by + * the total number of bytes written to the output stream. This will be at + * least two plus the length of str, and at most two plus + * thrice the length of str. + * + * @param str a string to be written. + * @throws IOException if an I/O error occurs. + */ + public final void writeUTF(String str) throws IOException { + writeUTF(str, this); + } + + /** + * bytearr is initialized on demand by writeUTF + */ + private byte[] bytearr = null; + + /** + * Writes a string to the specified DataOutput using modified UTF-8 encoding in a + * machine-independent manner. + *

    + * First, two bytes are written to out as if by the writeShort + * method giving the number of bytes to follow. This value is the number of + * bytes actually written out, not the length of the string. Following the + * length, each character of the string is output, in sequence, using the + * modified UTF-8 encoding for the character. If no exception is thrown, the + * counter written is incremented by the total number of bytes + * written to the output stream. This will be at least two plus the length + * of str, and at most two plus thrice the length of + * str. + * + * @param str a string to be written. + * @param out destination to write to + * @return The number of bytes written out. + * @throws IOException if an I/O error occurs. + */ + static int writeUTF(String str, DataOutput out) throws IOException { + int strlen = str.length(); + int utflen = 0; + int c, count = 0; + + /* use charAt instead of copying String to char array */ + for (int i = 0; i < strlen; i++) { + c = str.charAt(i); + if ((c >= 0x0001) && (c <= 0x007F)) { + utflen++; + } else if (c > 0x07FF) { + utflen += 3; + } else { + utflen += 2; + } + } + + if (utflen > 65535) + throw new UTFDataFormatException("encoded string too long: " + + utflen + " bytes"); + + byte[] bytearr = null; + if (out instanceof LittleEndianDataOutputStream) { + LittleEndianDataOutputStream dos = (LittleEndianDataOutputStream) out; + if (dos.bytearr == null || (dos.bytearr.length < (utflen + 2))) + dos.bytearr = new byte[(utflen * 2) + 2]; + bytearr = dos.bytearr; + } else { + bytearr = new byte[utflen + 2]; + } + + bytearr[count++] = (byte) ((utflen >>> 8) & 0xFF); + bytearr[count++] = (byte) ((utflen >>> 0) & 0xFF); + + int i = 0; + for (i = 0; i < strlen; i++) { + c = str.charAt(i); + if (!((c >= 0x0001) && (c <= 0x007F))) + break; + bytearr[count++] = (byte) c; + } + + for (; i < strlen; i++) { + c = str.charAt(i); + if ((c >= 0x0001) && (c <= 0x007F)) { + bytearr[count++] = (byte) c; + + } else if (c > 0x07FF) { + bytearr[count++] = (byte) (0xE0 | ((c >> 12) & 0x0F)); + bytearr[count++] = (byte) (0x80 | ((c >> 6) & 0x3F)); + bytearr[count++] = (byte) (0x80 | ((c >> 0) & 0x3F)); + } else { + bytearr[count++] = (byte) (0xC0 | ((c >> 6) & 0x1F)); + bytearr[count++] = (byte) (0x80 | ((c >> 0) & 0x3F)); + } + } + out.write(bytearr, 0, utflen + 2); + return utflen + 2; + } + + /** + * Returns the current value of the counter written, the number + * of bytes written to this data output stream so far. If the counter + * overflows, it will be wrapped to Integer.MAX_VALUE. + * + * @return the value of the written field. + * @see java.io.DataOutputStream#written + */ + public final int size() { + return written; + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/BaseActivity.java b/app/src/main/java/com/m2049r/xmrwallet/BaseActivity.java new file mode 100644 index 0000000..1f1f9b1 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/BaseActivity.java @@ -0,0 +1,313 @@ +/* + * Copyright (c) 2017-2020 m2049r et al. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.nfc.FormatException; +import android.nfc.NdefMessage; +import android.nfc.NdefRecord; +import android.nfc.NfcAdapter; +import android.nfc.Tag; +import android.nfc.tech.Ndef; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.PowerManager; +import android.widget.Toast; + +import androidx.annotation.CallSuper; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import com.m2049r.xmrwallet.data.BarcodeData; +import com.m2049r.xmrwallet.dialog.ProgressDialog; +import com.m2049r.xmrwallet.fragment.send.SendFragment; +import com.m2049r.xmrwallet.ledger.Ledger; +import com.m2049r.xmrwallet.ledger.LedgerProgressDialog; + +import java.io.IOException; + +import timber.log.Timber; + +public class BaseActivity extends SecureActivity + implements GenerateReviewFragment.ProgressListener, SubaddressFragment.ProgressListener { + + ProgressDialog progressDialog = null; + + private class SimpleProgressDialog extends ProgressDialog { + + SimpleProgressDialog(Context context, int msgId) { + super(context); + setCancelable(false); + setMessage(context.getString(msgId)); + } + + @Override + public void onBackPressed() { + // prevent back button + } + } + + @Override + public void showProgressDialog(int msgId) { + showProgressDialog(msgId, 250); // don't show dialog for fast operations + } + + public void showProgressDialog(int msgId, long delayMillis) { + dismissProgressDialog(); // just in case + progressDialog = new SimpleProgressDialog(BaseActivity.this, msgId); + if (delayMillis > 0) { + Handler handler = new Handler(); + handler.postDelayed(new Runnable() { + public void run() { + if (progressDialog != null) progressDialog.show(); + } + }, delayMillis); + } else { + progressDialog.show(); + } + } + + @Override + public void showLedgerProgressDialog(int mode) { + dismissProgressDialog(); // just in case + progressDialog = new LedgerProgressDialog(BaseActivity.this, mode); + Ledger.setListener((Ledger.Listener) progressDialog); + progressDialog.show(); + } + + @Override + public void dismissProgressDialog() { + if (progressDialog == null) return; // nothing to do + if (progressDialog instanceof Ledger.Listener) { + Ledger.unsetListener((Ledger.Listener) progressDialog); + } + if (progressDialog.isShowing()) { + progressDialog.dismiss(); + } + progressDialog = null; + } + + static final int RELEASE_WAKE_LOCK_DELAY = 5000; // millisconds + + private PowerManager.WakeLock wl = null; + + void acquireWakeLock() { + if ((wl != null) && wl.isHeld()) return; + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + this.wl = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, getString(R.string.app_name)); + try { + wl.acquire(); + Timber.d("WakeLock acquired"); + } catch (SecurityException ex) { + Timber.w("WakeLock NOT acquired: %s", ex.getLocalizedMessage()); + wl = null; + } + } + + void releaseWakeLock(int delayMillis) { + Handler handler = new Handler(Looper.getMainLooper()); + handler.postDelayed(new Runnable() { + @Override + public void run() { + releaseWakeLock(); + } + }, delayMillis); + } + + void releaseWakeLock() { + if ((wl == null) || !wl.isHeld()) return; + wl.release(); + wl = null; + Timber.d("WakeLock released"); + } + + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + initNfc(); + } + + @Override + protected void onPostResume() { + super.onPostResume(); + if (nfcAdapter != null) { + nfcAdapter.enableForegroundDispatch(this, nfcPendingIntent, null, null); + // intercept all techs so we can tell the user their tag is no good + } + } + + @Override + protected void onPause() { + Timber.d("onPause()"); + if (nfcAdapter != null) + nfcAdapter.disableForegroundDispatch(this); + super.onPause(); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + processNfcIntent(intent); + } + + // NFC stuff + private NfcAdapter nfcAdapter; + private PendingIntent nfcPendingIntent; + + public void initNfc() { + nfcAdapter = NfcAdapter.getDefaultAdapter(this); + if (nfcAdapter == null) // no NFC support + return; + nfcPendingIntent = PendingIntent.getActivity(this, 0, + new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_IMMUTABLE : 0); + } + + private void processNfcIntent(Intent intent) { + String action = intent.getAction(); + Timber.d("ACTION=%s", action); + if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action) + || NfcAdapter.ACTION_TAG_DISCOVERED.equals(action) + || NfcAdapter.ACTION_TECH_DISCOVERED.equals(action)) { + Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); + Ndef ndef = Ndef.get(tag); + if (ndef == null) { + Toast.makeText(this, getString(R.string.nfc_tag_unsupported), Toast.LENGTH_LONG).show(); + return; + } + + Fragment f = getSupportFragmentManager().findFragmentById(R.id.fragment_container); + if (f instanceof ReceiveFragment) { + // We want to write a Tag from the ReceiveFragment + BarcodeData bc = ((ReceiveFragment) f).getBarcodeData(); + if (bc != null) { + new AsyncWriteTag(ndef, bc.getUri()).execute(); + } // else wallet is not loaded yet or receive is otherwise not ready - ignore + } else if (f instanceof SendFragment) { + // We want to read a Tag for the SendFragment + NdefMessage ndefMessage = ndef.getCachedNdefMessage(); + if (ndefMessage == null) { + Toast.makeText(this, getString(R.string.nfc_tag_read_undef), Toast.LENGTH_LONG).show(); + return; + } + NdefRecord firstRecord = ndefMessage.getRecords()[0]; + Uri uri = firstRecord.toUri(); // we insist on the first record + if (uri == null) { + Toast.makeText(this, getString(R.string.nfc_tag_read_undef), Toast.LENGTH_LONG).show(); + } else { + BarcodeData bc = BarcodeData.fromString(uri.toString()); + if (bc == null) + Toast.makeText(this, getString(R.string.nfc_tag_read_undef), Toast.LENGTH_LONG).show(); + else + onUriScanned(bc); + } + } + } + } + + // this gets called only if we get data + @CallSuper + void onUriScanned(BarcodeData barcodeData) { + // do nothing by default yet + } + + private BarcodeData barcodeData = null; + + private BarcodeData popBarcodeData() { + BarcodeData popped = barcodeData; + barcodeData = null; + return popped; + } + + private class AsyncWriteTag extends AsyncTask { + + Ndef ndef; + Uri uri; + String errorMessage = null; + + AsyncWriteTag(Ndef ndef, Uri uri) { + this.ndef = ndef; + this.uri = uri; + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + showProgressDialog(R.string.progress_nfc_write); + } + + @Override + protected Boolean doInBackground(Void... params) { + if (params.length != 0) return false; + try { + writeNdef(ndef, uri); + return true; + } catch (IOException | FormatException ex) { + Timber.e(ex); + } catch (IllegalArgumentException ex) { + errorMessage = ex.getMessage(); + Timber.d(errorMessage); + } finally { + try { + ndef.close(); + } catch (IOException ex) { + Timber.e(ex); + } + } + return false; + } + + @Override + protected void onPostExecute(Boolean result) { + super.onPostExecute(result); + if (isDestroyed()) { + return; + } + dismissProgressDialog(); + if (!result) { + if (errorMessage != null) + Toast.makeText(getApplicationContext(), errorMessage, Toast.LENGTH_LONG).show(); + else + Toast.makeText(getApplicationContext(), getString(R.string.nfc_write_failed), Toast.LENGTH_LONG).show(); + } else { + Toast.makeText(getApplicationContext(), getString(R.string.nfc_write_successful), Toast.LENGTH_SHORT).show(); + } + } + } + + void writeNdef(Ndef ndef, Uri uri) throws IOException, FormatException { + NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this); + if (nfcAdapter == null) return; // no NFC support here + + NdefRecord recordNFC = NdefRecord.createUri(uri); + NdefMessage message = new NdefMessage(recordNFC); + ndef.connect(); + int tagSize = ndef.getMaxSize(); + int msgSize = message.getByteArrayLength(); + Timber.d("tagSize=%d, msgSIze=%d, uriSize=%d", tagSize, msgSize, uri.toString().length()); + if (tagSize < msgSize) + throw new IllegalArgumentException(getString(R.string.nfc_tag_size, tagSize, msgSize)); + ndef.writeNdefMessage(message); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java b/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java new file mode 100644 index 0000000..6370e99 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java @@ -0,0 +1,615 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet; + +import androidx.annotation.NonNull; + +import android.app.Activity; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.text.Editable; +import android.text.Html; +import android.text.InputType; +import android.text.TextWatcher; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.inputmethod.EditorInfo; +import android.widget.Button; +import android.widget.LinearLayout; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.switchmaterial.SwitchMaterial; +import com.google.android.material.textfield.TextInputLayout; +import com.m2049r.xmrwallet.model.Wallet; +import com.m2049r.xmrwallet.model.WalletManager; +import com.m2049r.xmrwallet.util.FingerprintHelper; +import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.KeyStoreHelper; +import com.m2049r.xmrwallet.util.RestoreHeight; +import com.m2049r.xmrwallet.util.ledger.Monero; +import com.m2049r.xmrwallet.widget.PasswordEntryView; +import com.m2049r.xmrwallet.widget.Toolbar; + +import java.io.File; +import java.text.ParseException; +import java.text.SimpleDateFormat; + +import timber.log.Timber; + +public class GenerateFragment extends Fragment { + + static final String TYPE = "type"; + static final String TYPE_NEW = "new"; + static final String TYPE_KEY = "key"; + static final String TYPE_SEED = "seed"; + static final String TYPE_LEDGER = "ledger"; + static final String TYPE_VIEWONLY = "view"; + + private TextInputLayout etWalletName; + private PasswordEntryView etWalletPassword; + private LinearLayout llFingerprintAuth; + private TextInputLayout etWalletAddress; + private TextInputLayout etWalletMnemonic; + private TextInputLayout etWalletViewKey; + private TextInputLayout etWalletSpendKey; + private TextInputLayout etWalletRestoreHeight; + private Button bGenerate; + + private Button bSeedOffset; + private TextInputLayout etSeedOffset; + + private String type = null; + + private void clearErrorOnTextEntry(final TextInputLayout textInputLayout) { + textInputLayout.getEditText().addTextChangedListener(new TextWatcher() { + @Override + public void afterTextChanged(Editable editable) { + textInputLayout.setError(null); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + }); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + Bundle args = getArguments(); + this.type = args.getString(TYPE); + + View view = inflater.inflate(R.layout.fragment_generate, container, false); + + etWalletName = view.findViewById(R.id.etWalletName); + etWalletPassword = view.findViewById(R.id.etWalletPassword); + llFingerprintAuth = view.findViewById(R.id.llFingerprintAuth); + etWalletMnemonic = view.findViewById(R.id.etWalletMnemonic); + etWalletAddress = view.findViewById(R.id.etWalletAddress); + etWalletViewKey = view.findViewById(R.id.etWalletViewKey); + etWalletSpendKey = view.findViewById(R.id.etWalletSpendKey); + etWalletRestoreHeight = view.findViewById(R.id.etWalletRestoreHeight); + bGenerate = view.findViewById(R.id.bGenerate); + bSeedOffset = view.findViewById(R.id.bSeedOffset); + etSeedOffset = view.findViewById(R.id.etSeedOffset); + + etWalletAddress.getEditText().setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + etWalletViewKey.getEditText().setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + etWalletSpendKey.getEditText().setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + + etWalletName.getEditText().setOnFocusChangeListener((v, hasFocus) -> { + if (!hasFocus) { + checkName(); + } + }); + clearErrorOnTextEntry(etWalletName); + + etWalletMnemonic.getEditText().setOnFocusChangeListener((v, hasFocus) -> { + if (!hasFocus) { + checkMnemonic(); + } + }); + clearErrorOnTextEntry(etWalletMnemonic); + + etWalletAddress.getEditText().setOnFocusChangeListener((v, hasFocus) -> { + if (!hasFocus) { + checkAddress(); + } + }); + clearErrorOnTextEntry(etWalletAddress); + + etWalletViewKey.getEditText().setOnFocusChangeListener((v, hasFocus) -> { + if (!hasFocus) { + checkViewKey(); + } + }); + clearErrorOnTextEntry(etWalletViewKey); + + etWalletSpendKey.getEditText().setOnFocusChangeListener((v, hasFocus) -> { + if (!hasFocus) { + checkSpendKey(); + } + }); + clearErrorOnTextEntry(etWalletSpendKey); + + Helper.showKeyboard(requireActivity()); + + etWalletName.getEditText().setOnEditorActionListener((v, actionId, event) -> { + if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN)) + || (actionId == EditorInfo.IME_ACTION_NEXT)) { + if (checkName()) { + etWalletPassword.requestFocus(); + } // otherwise ignore + return true; + } + return false; + }); + + if (FingerprintHelper.isDeviceSupported(getContext())) { + llFingerprintAuth.setVisibility(View.VISIBLE); + + final SwitchMaterial swFingerprintAllowed = (SwitchMaterial) llFingerprintAuth.getChildAt(0); + swFingerprintAllowed.setOnClickListener(view1 -> { + if (!swFingerprintAllowed.isChecked()) return; + + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity()); + builder.setMessage(Html.fromHtml(getString(R.string.generate_fingerprint_warn))) + .setCancelable(false) + .setPositiveButton(getString(R.string.label_ok), null) + .setNegativeButton(getString(R.string.label_cancel), (dialogInterface, i) -> swFingerprintAllowed.setChecked(false)) + .show(); + }); + } + + switch (type) { + case TYPE_NEW: + etWalletPassword.getEditText().setImeOptions(EditorInfo.IME_ACTION_UNSPECIFIED); + break; + case TYPE_LEDGER: + etWalletPassword.getEditText().setImeOptions(EditorInfo.IME_ACTION_DONE); + etWalletPassword.getEditText().setOnEditorActionListener((v, actionId, event) -> { + if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN)) + || (actionId == EditorInfo.IME_ACTION_DONE)) { + etWalletRestoreHeight.requestFocus(); + return true; + } + return false; + }); + break; + case TYPE_SEED: + etWalletPassword.getEditText().setOnEditorActionListener((v, actionId, event) -> { + if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN)) + || (actionId == EditorInfo.IME_ACTION_NEXT)) { + etWalletMnemonic.requestFocus(); + return true; + } + return false; + }); + etWalletMnemonic.setVisibility(View.VISIBLE); + etWalletMnemonic.getEditText().setOnEditorActionListener((v, actionId, event) -> { + if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN)) + || (actionId == EditorInfo.IME_ACTION_NEXT)) { + if (checkMnemonic()) { + etWalletRestoreHeight.requestFocus(); + } + return true; + } + return false; + }); + bSeedOffset.setVisibility(View.VISIBLE); + bSeedOffset.setOnClickListener(v -> toggleSeedOffset()); + break; + case TYPE_KEY: + case TYPE_VIEWONLY: + etWalletPassword.getEditText().setOnEditorActionListener((v, actionId, event) -> { + if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN)) + || (actionId == EditorInfo.IME_ACTION_NEXT)) { + etWalletAddress.requestFocus(); + return true; + } + return false; + }); + etWalletAddress.setVisibility(View.VISIBLE); + etWalletAddress.getEditText().setOnEditorActionListener((v, actionId, event) -> { + if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN)) + || (actionId == EditorInfo.IME_ACTION_NEXT)) { + if (checkAddress()) { + etWalletViewKey.requestFocus(); + } + return true; + } + return false; + }); + etWalletViewKey.setVisibility(View.VISIBLE); + etWalletViewKey.getEditText().setOnEditorActionListener((v, actionId, event) -> { + if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN)) + || (actionId == EditorInfo.IME_ACTION_NEXT)) { + if (checkViewKey()) { + if (type.equals(TYPE_KEY)) { + etWalletSpendKey.requestFocus(); + } else { + etWalletRestoreHeight.requestFocus(); + } + } + return true; + } + return false; + }); + break; + } + if (type.equals(TYPE_KEY)) { + etWalletSpendKey.setVisibility(View.VISIBLE); + etWalletSpendKey.getEditText().setOnEditorActionListener((v, actionId, event) -> { + if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN)) + || (actionId == EditorInfo.IME_ACTION_NEXT)) { + if (checkSpendKey()) { + etWalletRestoreHeight.requestFocus(); + } + return true; + } + return false; + }); + } + if (!type.equals(TYPE_NEW)) { + etWalletRestoreHeight.setVisibility(View.VISIBLE); + etWalletRestoreHeight.getEditText().setImeOptions(EditorInfo.IME_ACTION_UNSPECIFIED); + } + bGenerate.setOnClickListener(v -> { + Helper.hideKeyboard(getActivity()); + generateWallet(); + }); + + etWalletName.requestFocus(); + + return view; + } + + void toggleSeedOffset() { + if (etSeedOffset.getVisibility() == View.VISIBLE) { + etSeedOffset.getEditText().getText().clear(); + etSeedOffset.setVisibility(View.GONE); + bSeedOffset.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_keyboard_arrow_down, 0, 0, 0); + } else { + etSeedOffset.setVisibility(View.VISIBLE); + bSeedOffset.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_keyboard_arrow_up, 0, 0, 0); + etSeedOffset.requestFocusFromTouch(); + } + } + + private boolean checkName() { + String name = etWalletName.getEditText().getText().toString(); + boolean ok = true; + if (name.length() == 0) { + etWalletName.setError(getString(R.string.generate_wallet_name)); + ok = false; + } else if (name.charAt(0) == '.') { + etWalletName.setError(getString(R.string.generate_wallet_dot)); + ok = false; + } else { + File walletFile = Helper.getWalletFile(getActivity(), name); + if (WalletManager.getInstance().walletExists(walletFile)) { + etWalletName.setError(getString(R.string.generate_wallet_exists)); + ok = false; + } + } + if (ok) { + etWalletName.setError(null); + } + return ok; + } + + private boolean checkHeight() { + long height = !type.equals(TYPE_NEW) ? getHeight() : 0; + boolean ok = true; + if (height < 0) { + etWalletRestoreHeight.setError(getString(R.string.generate_restoreheight_error)); + ok = false; + } + if (ok) { + etWalletRestoreHeight.setError(null); + } + return ok; + } + + private long getHeight() { + long height = -1; + + String restoreHeight = etWalletRestoreHeight.getEditText().getText().toString().trim(); + if (restoreHeight.isEmpty()) return -1; + try { + // is it a date? + SimpleDateFormat parser = new SimpleDateFormat("yyyy-MM-dd"); + parser.setLenient(false); + height = RestoreHeight.getInstance().getHeight(parser.parse(restoreHeight)); + } catch (ParseException ignored) { + } + if ((height < 0) && (restoreHeight.length() == 8)) + try { + // is it a date without dashes? + SimpleDateFormat parser = new SimpleDateFormat("yyyyMMdd"); + parser.setLenient(false); + height = RestoreHeight.getInstance().getHeight(parser.parse(restoreHeight)); + } catch (ParseException ignored) { + } + if (height < 0) + try { + // or is it a height? + height = Long.parseLong(restoreHeight); + } catch (NumberFormatException ex) { + return -1; + } + Timber.d("Using Restore Height = %d", height); + return height; + } + + private boolean checkMnemonic() { + String seed = etWalletMnemonic.getEditText().getText().toString(); + boolean ok = (seed.split("\\s").length == 25); // 25 words + if (!ok) { + etWalletMnemonic.setError(getString(R.string.generate_check_mnemonic)); + } else { + etWalletMnemonic.setError(null); + } + return ok; + } + + private boolean checkAddress() { + String address = etWalletAddress.getEditText().getText().toString(); + boolean ok = Wallet.isAddressValid(address); + if (!ok) { + etWalletAddress.setError(getString(R.string.generate_check_address)); + } else { + etWalletAddress.setError(null); + } + return ok; + } + + private boolean checkViewKey() { + String viewKey = etWalletViewKey.getEditText().getText().toString(); + boolean ok = (viewKey.length() == 64) && (viewKey.matches("^[0-9a-fA-F]+$")); + if (!ok) { + etWalletViewKey.setError(getString(R.string.generate_check_key)); + } else { + etWalletViewKey.setError(null); + } + return ok; + } + + private boolean checkSpendKey() { + String spendKey = etWalletSpendKey.getEditText().getText().toString(); + boolean ok = ((spendKey.length() == 0) || ((spendKey.length() == 64) && (spendKey.matches("^[0-9a-fA-F]+$")))); + if (!ok) { + etWalletSpendKey.setError(getString(R.string.generate_check_key)); + } else { + etWalletSpendKey.setError(null); + } + return ok; + } + + private void generateWallet() { + if (!checkName()) return; + if (!checkHeight()) return; + + String name = etWalletName.getEditText().getText().toString(); + String password = etWalletPassword.getEditText().getText().toString(); + boolean fingerprintAuthAllowed = ((SwitchMaterial) llFingerprintAuth.getChildAt(0)).isChecked(); + + // create the real wallet password + String crazyPass = KeyStoreHelper.getCrazyPass(getActivity(), password); + + long height = getHeight(); + if (height < 0) height = 0; + + switch (type) { + case TYPE_NEW: + bGenerate.setEnabled(false); + if (fingerprintAuthAllowed) { + KeyStoreHelper.saveWalletUserPass(requireActivity(), name, password); + } + activityCallback.onGenerate(name, crazyPass); + break; + case TYPE_SEED: + if (!checkMnemonic()) return; + final String seed = etWalletMnemonic.getEditText().getText().toString(); + bGenerate.setEnabled(false); + if (fingerprintAuthAllowed) { + KeyStoreHelper.saveWalletUserPass(requireActivity(), name, password); + } + final String offset = etSeedOffset.getEditText().getText().toString(); + activityCallback.onGenerate(name, crazyPass, seed, offset, height); + break; + case TYPE_LEDGER: + bGenerate.setEnabled(false); + if (fingerprintAuthAllowed) { + KeyStoreHelper.saveWalletUserPass(requireActivity(), name, password); + } + activityCallback.onGenerateLedger(name, crazyPass, height); + break; + case TYPE_KEY: + case TYPE_VIEWONLY: + if (checkAddress() && checkViewKey() && checkSpendKey()) { + bGenerate.setEnabled(false); + String address = etWalletAddress.getEditText().getText().toString(); + String viewKey = etWalletViewKey.getEditText().getText().toString(); + String spendKey = ""; + if (type.equals(TYPE_KEY)) { + spendKey = etWalletSpendKey.getEditText().getText().toString(); + } + if (fingerprintAuthAllowed) { + KeyStoreHelper.saveWalletUserPass(requireActivity(), name, password); + } + activityCallback.onGenerate(name, crazyPass, address, viewKey, spendKey, height); + } + break; + } + } + + public void walletGenerateError() { + bGenerate.setEnabled(true); + } + + @Override + public void onResume() { + super.onResume(); + Timber.d("onResume()"); + activityCallback.setTitle(getString(R.string.generate_title) + " - " + getType()); + activityCallback.setToolbarButton(Toolbar.BUTTON_BACK); + + } + + String getType() { + switch (type) { + case TYPE_KEY: + return getString(R.string.generate_wallet_type_key); + case TYPE_NEW: + return getString(R.string.generate_wallet_type_new); + case TYPE_SEED: + return getString(R.string.generate_wallet_type_seed); + case TYPE_LEDGER: + return getString(R.string.generate_wallet_type_ledger); + case TYPE_VIEWONLY: + return getString(R.string.generate_wallet_type_view); + default: + Timber.e("unknown type %s", type); + return "?"; + } + } + + GenerateFragment.Listener activityCallback; + + public interface Listener { + void onGenerate(String name, String password); + + void onGenerate(String name, String password, String seed, String offset, long height); + + void onGenerate(String name, String password, String address, String viewKey, String spendKey, long height); + + void onGenerateLedger(String name, String password, long height); + + void setTitle(String title); + + void setToolbarButton(int type); + + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + if (context instanceof GenerateFragment.Listener) { + this.activityCallback = (GenerateFragment.Listener) context; + } else { + throw new ClassCastException(context.toString() + + " must implement Listener"); + } + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + switch (type) { + case TYPE_KEY: + inflater.inflate(R.menu.create_wallet_keys, menu); + break; + case TYPE_NEW: + inflater.inflate(R.menu.create_wallet_new, menu); + break; + case TYPE_SEED: + inflater.inflate(R.menu.create_wallet_seed, menu); + break; + case TYPE_LEDGER: + inflater.inflate(R.menu.create_wallet_ledger, menu); + break; + case TYPE_VIEWONLY: + inflater.inflate(R.menu.create_wallet_view, menu); + break; + default: + } + super.onCreateOptionsMenu(menu, inflater); + } + + AlertDialog ledgerDialog = null; + + public void convertLedgerSeed() { + if (ledgerDialog != null) return; + final Activity activity = requireActivity(); + View promptsView = getLayoutInflater().inflate(R.layout.prompt_ledger_seed, null); + MaterialAlertDialogBuilder alertDialogBuilder = new MaterialAlertDialogBuilder(activity); + alertDialogBuilder.setView(promptsView); + + final TextInputLayout etSeed = promptsView.findViewById(R.id.etSeed); + final TextInputLayout etPassphrase = promptsView.findViewById(R.id.etPassphrase); + + clearErrorOnTextEntry(etSeed); + + alertDialogBuilder + .setCancelable(false) + .setPositiveButton(getString(R.string.label_ok), null) + .setNegativeButton(getString(R.string.label_cancel), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + Helper.hideKeyboardAlways(activity); + etWalletMnemonic.getEditText().getText().clear(); + dialog.cancel(); + ledgerDialog = null; + } + }); + + ledgerDialog = alertDialogBuilder.create(); + + ledgerDialog.setOnShowListener(dialog -> { + Button button = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE); + button.setOnClickListener(view -> { + String ledgerSeed = etSeed.getEditText().getText().toString(); + String ledgerPassphrase = etPassphrase.getEditText().getText().toString(); + String moneroSeed = Monero.convert(ledgerSeed, ledgerPassphrase); + if (moneroSeed != null) { + etWalletMnemonic.getEditText().setText(moneroSeed); + ledgerDialog.dismiss(); + ledgerDialog = null; + } else { + etSeed.setError(getString(R.string.bad_ledger_seed)); + } + }); + }); + + if (Helper.preventScreenshot()) { + ledgerDialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE); + } + + ledgerDialog.show(); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java b/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java new file mode 100644 index 0000000..cb40b84 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java @@ -0,0 +1,711 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet; + +import android.content.Context; +import android.content.DialogInterface; +import android.os.AsyncTask; +import android.os.Bundle; +import android.text.Editable; +import android.text.Html; +import android.text.TextWatcher; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.inputmethod.EditorInfo; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.ScrollView; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.switchmaterial.SwitchMaterial; +import com.google.android.material.textfield.TextInputLayout; +import com.m2049r.xmrwallet.ledger.Ledger; +import com.m2049r.xmrwallet.ledger.LedgerProgressDialog; +import com.m2049r.xmrwallet.model.NetworkType; +import com.m2049r.xmrwallet.model.Wallet; +import com.m2049r.xmrwallet.model.WalletManager; +import com.m2049r.xmrwallet.util.FingerprintHelper; +import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.KeyStoreHelper; +import com.m2049r.xmrwallet.util.MoneroThreadPoolExecutor; +import com.m2049r.xmrwallet.widget.PasswordEntryView; +import com.m2049r.xmrwallet.widget.Toolbar; + +import java.text.NumberFormat; + +import timber.log.Timber; + +public class GenerateReviewFragment extends Fragment { + static final public String VIEW_TYPE_DETAILS = "details"; + static final public String VIEW_TYPE_ACCEPT = "accept"; + static final public String VIEW_TYPE_WALLET = "wallet"; + + public static final String REQUEST_TYPE = "type"; + public static final String REQUEST_PATH = "path"; + public static final String REQUEST_PASSWORD = "password"; + + private ScrollView scrollview; + + private ProgressBar pbProgress; + private TextView tvWalletPassword; + private TextView tvWalletAddress; + private FrameLayout flWalletMnemonic; + private TextView tvWalletMnemonic; + private TextView tvWalletHeight; + private TextView tvWalletViewKey; + private TextView tvWalletSpendKey; + private ImageButton bCopyAddress; + private LinearLayout llAdvancedInfo; + private LinearLayout llPassword; + private LinearLayout llMnemonic; + private LinearLayout llSpendKey; + private LinearLayout llViewKey; + private Button bAdvancedInfo; + private Button bAccept; + + private Button bSeedOffset; + private TextInputLayout etSeedOffset; + + private String walletPath; + private String walletName; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + View view = inflater.inflate(R.layout.fragment_review, container, false); + + scrollview = view.findViewById(R.id.scrollview); + pbProgress = view.findViewById(R.id.pbProgress); + tvWalletPassword = view.findViewById(R.id.tvWalletPassword); + tvWalletAddress = view.findViewById(R.id.tvWalletAddress); + tvWalletViewKey = view.findViewById(R.id.tvWalletViewKey); + tvWalletSpendKey = view.findViewById(R.id.tvWalletSpendKey); + tvWalletMnemonic = view.findViewById(R.id.tvWalletMnemonic); + flWalletMnemonic = view.findViewById(R.id.flWalletMnemonic); + tvWalletHeight = view.findViewById(R.id.tvWalletHeight); + bCopyAddress = view.findViewById(R.id.bCopyAddress); + bAdvancedInfo = view.findViewById(R.id.bAdvancedInfo); + llAdvancedInfo = view.findViewById(R.id.llAdvancedInfo); + llPassword = view.findViewById(R.id.llPassword); + llMnemonic = view.findViewById(R.id.llMnemonic); + llSpendKey = view.findViewById(R.id.llSpendKey); + llViewKey = view.findViewById(R.id.llViewKey); + + etSeedOffset = view.findViewById(R.id.etSeedOffset); + bSeedOffset = view.findViewById(R.id.bSeedOffset); + + bAccept = view.findViewById(R.id.bAccept); + + boolean allowCopy = WalletManager.getInstance().getNetworkType() != NetworkType.NetworkType_Mainnet; + tvWalletMnemonic.setTextIsSelectable(allowCopy); + tvWalletSpendKey.setTextIsSelectable(allowCopy); + tvWalletPassword.setTextIsSelectable(allowCopy); + + bAccept.setOnClickListener(v -> acceptWallet()); + view.findViewById(R.id.bCopyViewKey).setOnClickListener(v -> copyViewKey()); + bCopyAddress.setEnabled(false); + bCopyAddress.setOnClickListener(v -> copyAddress()); + bAdvancedInfo.setOnClickListener(v -> toggleAdvancedInfo()); + + bSeedOffset.setOnClickListener(v -> toggleSeedOffset()); + etSeedOffset.getEditText().addTextChangedListener(new TextWatcher() { + @Override + public void afterTextChanged(Editable s) { + showSeed(); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, + int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, + int before, int count) { + } + }); + + Bundle args = getArguments(); + type = args.getString(REQUEST_TYPE); + walletPath = args.getString(REQUEST_PATH); + localPassword = args.getString(REQUEST_PASSWORD); + showDetails(); + return view; + } + + String getSeedOffset() { + return etSeedOffset.getEditText().getText().toString(); + } + + boolean seedOffsetInProgress = false; + + void showSeed() { + synchronized (this) { + if (seedOffsetInProgress) return; + seedOffsetInProgress = true; + } + new AsyncShowSeed().executeOnExecutor(MoneroThreadPoolExecutor.MONERO_THREAD_POOL_EXECUTOR, walletPath); + } + + void showDetails() { + tvWalletPassword.setText(null); + new AsyncShow().executeOnExecutor(MoneroThreadPoolExecutor.MONERO_THREAD_POOL_EXECUTOR, walletPath); + } + + void copyViewKey() { + Helper.clipBoardCopy(requireActivity(), getString(R.string.label_copy_viewkey), tvWalletViewKey.getText().toString()); + Toast.makeText(getActivity(), getString(R.string.message_copy_viewkey), Toast.LENGTH_SHORT).show(); + } + + void copyAddress() { + Helper.clipBoardCopy(requireActivity(), getString(R.string.label_copy_address), tvWalletAddress.getText().toString()); + Toast.makeText(getActivity(), getString(R.string.message_copy_address), Toast.LENGTH_SHORT).show(); + } + + void nocopy() { + Toast.makeText(getActivity(), getString(R.string.message_nocopy), Toast.LENGTH_SHORT).show(); + } + + void toggleAdvancedInfo() { + if (llAdvancedInfo.getVisibility() == View.VISIBLE) { + llAdvancedInfo.setVisibility(View.GONE); + bAdvancedInfo.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_keyboard_arrow_down, 0, 0, 0); + } else { + llAdvancedInfo.setVisibility(View.VISIBLE); + bAdvancedInfo.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_keyboard_arrow_up, 0, 0, 0); + scrollview.post(() -> scrollview.fullScroll(ScrollView.FOCUS_DOWN)); + } + } + + void toggleSeedOffset() { + if (etSeedOffset.getVisibility() == View.VISIBLE) { + etSeedOffset.getEditText().getText().clear(); + etSeedOffset.setVisibility(View.GONE); + bSeedOffset.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_keyboard_arrow_down, 0, 0, 0); + } else { + etSeedOffset.setVisibility(View.VISIBLE); + bSeedOffset.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_keyboard_arrow_up, 0, 0, 0); + etSeedOffset.requestFocusFromTouch(); + } + } + + String type; + + private void acceptWallet() { + bAccept.setEnabled(false); + acceptCallback.onAccept(walletName, getPassword()); + } + + private class AsyncShow extends AsyncTask { + String name; + String address; + long height; + String seed; + String viewKey; + String spendKey; + Wallet.Status walletStatus; + + boolean dialogOpened = false; + + @Override + protected void onPreExecute() { + super.onPreExecute(); + showProgress(); + if ((walletPath != null) + && (WalletManager.getInstance().queryWalletDevice(walletPath + ".keys", getPassword()) + == Wallet.Device.Device_Ledger) + && (progressCallback != null)) { + progressCallback.showLedgerProgressDialog(LedgerProgressDialog.TYPE_RESTORE); + dialogOpened = true; + } + } + + @Override + protected Boolean doInBackground(String... params) { + if (params.length != 1) return false; + String walletPath = params[0]; + + Wallet wallet; + boolean closeWallet; + if (type.equals(GenerateReviewFragment.VIEW_TYPE_WALLET)) { + wallet = GenerateReviewFragment.this.walletCallback.getWallet(); + closeWallet = false; + } else { + wallet = WalletManager.getInstance().openWallet(walletPath, getPassword()); + closeWallet = true; + } + name = wallet.getName(); + walletStatus = wallet.getStatus(); + if (!walletStatus.isOk()) { + if (closeWallet) wallet.close(); + return false; + } + + address = wallet.getAddress(); + height = wallet.getRestoreHeight(); + seed = wallet.getSeed(getSeedOffset()); + switch (wallet.getDeviceType()) { + case Device_Ledger: + viewKey = Ledger.Key(); + break; + case Device_Software: + viewKey = wallet.getSecretViewKey(); + break; + default: + throw new IllegalStateException("Hardware backing not supported. At all!"); + } + spendKey = wallet.isWatchOnly() ? getActivity().getString(R.string.label_watchonly) : wallet.getSecretSpendKey(); + if (closeWallet) wallet.close(); + return true; + } + + @Override + protected void onPostExecute(Boolean result) { + super.onPostExecute(result); + if (dialogOpened) + progressCallback.dismissProgressDialog(); + if (!isAdded()) return; // never mind + walletName = name; + if (result) { + if (type.equals(GenerateReviewFragment.VIEW_TYPE_ACCEPT)) { + bAccept.setVisibility(View.VISIBLE); + bAccept.setEnabled(true); + } + llPassword.setVisibility(View.VISIBLE); + tvWalletPassword.setText(getPassword()); + tvWalletAddress.setText(address); + tvWalletHeight.setText(NumberFormat.getInstance().format(height)); + if (!seed.isEmpty()) { + llMnemonic.setVisibility(View.VISIBLE); + tvWalletMnemonic.setText(seed); + } + boolean showAdvanced = false; + if (isKeyValid(viewKey)) { + llViewKey.setVisibility(View.VISIBLE); + tvWalletViewKey.setText(viewKey); + showAdvanced = true; + } + if (isKeyValid(spendKey)) { + llSpendKey.setVisibility(View.VISIBLE); + tvWalletSpendKey.setText(spendKey); + showAdvanced = true; + } + if (showAdvanced) bAdvancedInfo.setVisibility(View.VISIBLE); + bCopyAddress.setEnabled(true); + activityCallback.setTitle(name, getString(R.string.details_title)); + activityCallback.setToolbarButton( + GenerateReviewFragment.VIEW_TYPE_ACCEPT.equals(type) ? Toolbar.BUTTON_NONE : Toolbar.BUTTON_BACK); + } else { + // TODO show proper error message and/or end the fragment? + tvWalletAddress.setText(walletStatus.toString()); + tvWalletHeight.setText(walletStatus.toString()); + tvWalletMnemonic.setText(walletStatus.toString()); + tvWalletViewKey.setText(walletStatus.toString()); + tvWalletSpendKey.setText(walletStatus.toString()); + } + hideProgress(); + } + } + + Listener activityCallback = null; + ProgressListener progressCallback = null; + AcceptListener acceptCallback = null; + ListenerWithWallet walletCallback = null; + PasswordChangedListener passwordCallback = null; + + public interface Listener { + void setTitle(String title, String subtitle); + + void setToolbarButton(int type); + } + + public interface ProgressListener { + void showProgressDialog(int msgId); + + void showLedgerProgressDialog(int mode); + + void dismissProgressDialog(); + } + + public interface AcceptListener { + void onAccept(String name, String password); + } + + public interface ListenerWithWallet { + Wallet getWallet(); + } + + public interface PasswordChangedListener { + void onPasswordChanged(String newPassword); + + String getPassword(); + } + + private String localPassword = null; + + private String getPassword() { + if (passwordCallback != null) return passwordCallback.getPassword(); + return localPassword; + } + + private void setPassword(String password) { + if (passwordCallback != null) passwordCallback.onPasswordChanged(password); + else localPassword = password; + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + if (context instanceof Listener) { + this.activityCallback = (Listener) context; + } + if (context instanceof ProgressListener) { + this.progressCallback = (ProgressListener) context; + } + if (context instanceof AcceptListener) { + this.acceptCallback = (AcceptListener) context; + } + if (context instanceof ListenerWithWallet) { + this.walletCallback = (ListenerWithWallet) context; + } + if (context instanceof PasswordChangedListener) { + this.passwordCallback = (PasswordChangedListener) context; + } + } + + @Override + public void onResume() { + super.onResume(); + Timber.d("onResume()"); + activityCallback.setTitle(walletName, getString(R.string.details_title)); + activityCallback.setToolbarButton( + GenerateReviewFragment.VIEW_TYPE_ACCEPT.equals(type) ? Toolbar.BUTTON_NONE : Toolbar.BUTTON_BACK); + } + + public void showProgress() { + pbProgress.setVisibility(View.VISIBLE); + } + + public void hideProgress() { + pbProgress.setVisibility(View.INVISIBLE); + } + + boolean backOk() { + return !type.equals(GenerateReviewFragment.VIEW_TYPE_ACCEPT); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + String type = getArguments().getString(REQUEST_TYPE); // intance variable not set yet + if (GenerateReviewFragment.VIEW_TYPE_ACCEPT.equals(type)) { + inflater.inflate(R.menu.wallet_details_help_menu, menu); + super.onCreateOptionsMenu(menu, inflater); + } else { + inflater.inflate(R.menu.wallet_details_menu, menu); + super.onCreateOptionsMenu(menu, inflater); + } + } + + boolean changeWalletPassword(String newPassword) { + Wallet wallet; + boolean closeWallet; + if (type.equals(GenerateReviewFragment.VIEW_TYPE_WALLET)) { + wallet = GenerateReviewFragment.this.walletCallback.getWallet(); + closeWallet = false; + } else { + wallet = WalletManager.getInstance().openWallet(walletPath, getPassword()); + closeWallet = true; + } + + boolean ok = false; + Wallet.Status walletStatus = wallet.getStatus(); + if (walletStatus.isOk()) { + wallet.setPassword(newPassword); + ok = true; + } + if (closeWallet) wallet.close(); + return ok; + } + + private class AsyncChangePassword extends AsyncTask { + String newPassword; + + @Override + protected void onPreExecute() { + super.onPreExecute(); + if (progressCallback != null) + progressCallback.showProgressDialog(R.string.changepw_progress); + } + + @Override + protected Boolean doInBackground(String... params) { + if (params.length != 2) return false; + final String userPassword = params[0]; + final boolean fingerPassValid = Boolean.parseBoolean(params[1]); + newPassword = KeyStoreHelper.getCrazyPass(getActivity(), userPassword); + final boolean success = changeWalletPassword(newPassword); + if (success) { + Context ctx = getActivity(); + if (ctx != null) + if (fingerPassValid) { + KeyStoreHelper.saveWalletUserPass(ctx, walletName, userPassword); + } else { + KeyStoreHelper.removeWalletUserPass(ctx, walletName); + } + } + return success; + } + + @Override + protected void onPostExecute(Boolean result) { + super.onPostExecute(result); + if ((getActivity() == null) || getActivity().isDestroyed()) { + return; + } + if (progressCallback != null) + progressCallback.dismissProgressDialog(); + if (result) { + Toast.makeText(getActivity(), getString(R.string.changepw_success), Toast.LENGTH_SHORT).show(); + setPassword(newPassword); + showDetails(); + } else { + Toast.makeText(getActivity(), getString(R.string.changepw_failed), Toast.LENGTH_LONG).show(); + } + } + } + + AlertDialog openDialog = null; // for preventing opening of multiple dialogs + + public AlertDialog createChangePasswordDialog() { + if (openDialog != null) return null; // we are already open + LayoutInflater li = LayoutInflater.from(getActivity()); + View promptsView = li.inflate(R.layout.prompt_changepw, null); + + AlertDialog.Builder alertDialogBuilder = new MaterialAlertDialogBuilder(requireActivity()); + alertDialogBuilder.setView(promptsView); + + final PasswordEntryView etPasswordA = promptsView.findViewById(R.id.etWalletPasswordA); + etPasswordA.setHint(getString(R.string.prompt_changepw, walletName)); + + final TextInputLayout etPasswordB = promptsView.findViewById(R.id.etWalletPasswordB); + etPasswordB.setHint(getString(R.string.prompt_changepwB, walletName)); + + LinearLayout llFingerprintAuth = promptsView.findViewById(R.id.llFingerprintAuth); + final SwitchMaterial swFingerprintAllowed = (SwitchMaterial) llFingerprintAuth.getChildAt(0); + if (FingerprintHelper.isDeviceSupported(getActivity())) { + llFingerprintAuth.setVisibility(View.VISIBLE); + + swFingerprintAllowed.setOnClickListener(view -> { + if (!swFingerprintAllowed.isChecked()) return; + + AlertDialog.Builder builder = new MaterialAlertDialogBuilder(requireActivity()); + builder.setMessage(Html.fromHtml(getString(R.string.generate_fingerprint_warn))) + .setCancelable(false) + .setPositiveButton(getString(R.string.label_ok), null) + .setNegativeButton(getString(R.string.label_cancel), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + swFingerprintAllowed.setChecked(false); + } + }) + .show(); + }); + + swFingerprintAllowed.setChecked(FingerprintHelper.isFingerPassValid(getActivity(), walletName)); + } + + etPasswordA.getEditText().addTextChangedListener(new TextWatcher() { + @Override + public void afterTextChanged(Editable s) { + if (etPasswordB.getError() != null) { + etPasswordB.setError(null); + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, + int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, + int before, int count) { + } + }); + + etPasswordB.getEditText().addTextChangedListener(new TextWatcher() { + @Override + public void afterTextChanged(Editable s) { + if (etPasswordB.getError() != null) { + etPasswordB.setError(null); + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, + int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, + int before, int count) { + } + }); + + // set dialog message + alertDialogBuilder + .setCancelable(false) + .setPositiveButton(getString(R.string.label_ok), null) + .setNegativeButton(getString(R.string.label_cancel), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + Helper.hideKeyboardAlways(requireActivity()); + dialog.cancel(); + openDialog = null; + } + }); + + openDialog = alertDialogBuilder.create(); + openDialog.setOnShowListener(dialog -> { + Button button = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE); + button.setOnClickListener(view -> { + String newPasswordA = etPasswordA.getEditText().getText().toString(); + String newPasswordB = etPasswordB.getEditText().getText().toString(); + if (!newPasswordA.equals(newPasswordB)) { + etPasswordB.setError(getString(R.string.generate_bad_passwordB)); + } else { + new AsyncChangePassword().execute(newPasswordA, Boolean.toString(swFingerprintAllowed.isChecked())); + Helper.hideKeyboardAlways(requireActivity()); + openDialog.dismiss(); + openDialog = null; + } + }); + }); + + // accept keyboard "ok" + etPasswordB.getEditText().setOnEditorActionListener((v, actionId, event) -> { + if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN)) + || (actionId == EditorInfo.IME_ACTION_DONE)) { + String newPasswordA = etPasswordA.getEditText().getText().toString(); + String newPasswordB = etPasswordB.getEditText().getText().toString(); + // disallow empty passwords + if (!newPasswordA.equals(newPasswordB)) { + etPasswordB.setError(getString(R.string.generate_bad_passwordB)); + } else { + new AsyncChangePassword().execute(newPasswordA, Boolean.toString(swFingerprintAllowed.isChecked())); + Helper.hideKeyboardAlways(requireActivity()); + openDialog.dismiss(); + openDialog = null; + } + return true; + } + return false; + }); + + if (Helper.preventScreenshot()) { + openDialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE); + } + + return openDialog; + } + + private boolean isKeyValid(String key) { + return (key != null) && (key.length() == 64) + && !key.equals("0000000000000000000000000000000000000000000000000000000000000000") + && !key.toLowerCase().equals("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + // ledger implmenetation returns the spend key as f's + } + + private class AsyncShowSeed extends AsyncTask { + String seed; + String offset; + Wallet.Status walletStatus; + + @Override + protected void onPreExecute() { + super.onPreExecute(); + offset = getSeedOffset(); + flWalletMnemonic.setAlpha(0.1f); + } + + @Override + protected Boolean doInBackground(String... params) { + if (params.length != 1) return false; + String walletPath = params[0]; + + Wallet wallet; + boolean closeWallet; + if (type.equals(GenerateReviewFragment.VIEW_TYPE_WALLET)) { + wallet = GenerateReviewFragment.this.walletCallback.getWallet(); + closeWallet = false; + } else { + wallet = WalletManager.getInstance().openWallet(walletPath, getPassword()); + closeWallet = true; + } + walletStatus = wallet.getStatus(); + if (!walletStatus.isOk()) { + if (closeWallet) wallet.close(); + return false; + } + + seed = wallet.getSeed(offset); + if (closeWallet) wallet.close(); + return true; + } + + @Override + protected void onPostExecute(Boolean result) { + super.onPostExecute(result); + if (!isAdded()) return; // never mind + if (result) { + if (!seed.isEmpty()) { + llMnemonic.setVisibility(View.VISIBLE); + tvWalletMnemonic.setText(seed); + } + } else { + tvWalletMnemonic.setText(walletStatus.toString()); + } + seedOffsetInProgress = false; + if (!getSeedOffset().equals(offset)) { // make sure we have encrypted with the correct offset + showSeed(); // seed has changed in the meantime - recalc + } else + flWalletMnemonic.setAlpha(1); + } + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java b/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java new file mode 100644 index 0000000..0cb2991 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java @@ -0,0 +1,1469 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet; + +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbManager; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; + +import com.google.android.material.checkbox.MaterialCheckBox; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.m2049r.xmrwallet.data.DefaultNodes; +import com.m2049r.xmrwallet.data.Node; +import com.m2049r.xmrwallet.data.NodeInfo; +import com.m2049r.xmrwallet.dialog.CreditsFragment; +import com.m2049r.xmrwallet.dialog.HelpFragment; +import com.m2049r.xmrwallet.ledger.Ledger; +import com.m2049r.xmrwallet.ledger.LedgerProgressDialog; +import com.m2049r.xmrwallet.model.NetworkType; +import com.m2049r.xmrwallet.model.Wallet; +import com.m2049r.xmrwallet.model.WalletManager; +import com.m2049r.xmrwallet.service.WalletService; +import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.KeyStoreHelper; +import com.m2049r.xmrwallet.util.LegacyStorageHelper; +import com.m2049r.xmrwallet.util.MoneroThreadPoolExecutor; +import com.m2049r.xmrwallet.util.NetCipherHelper; +import com.m2049r.xmrwallet.util.ThemeHelper; +import com.m2049r.xmrwallet.util.ZipBackup; +import com.m2049r.xmrwallet.util.ZipRestore; +import com.m2049r.xmrwallet.widget.Toolbar; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import timber.log.Timber; + +public class LoginActivity extends BaseActivity + implements LoginFragment.Listener, GenerateFragment.Listener, + GenerateReviewFragment.Listener, GenerateReviewFragment.AcceptListener, + NodeFragment.Listener, SettingsFragment.Listener { + private static final String GENERATE_STACK = "gen"; + + private static final String NODES_PREFS_NAME = "nodes"; + private static final String SELECTED_NODE_PREFS_NAME = "selected_node"; + private static final String PREF_DAEMON_TESTNET = "daemon_testnet"; + private static final String PREF_DAEMON_STAGENET = "daemon_stagenet"; + private static final String PREF_DAEMON_MAINNET = "daemon_mainnet"; + + private NodeInfo node = null; + + Set favouriteNodes = new HashSet<>(); + + @Override + public NodeInfo getNode() { + return node; + } + + @Override + public void setNode(NodeInfo node) { + setNode(node, true); + } + + private void setNode(NodeInfo node, boolean save) { + if (node != this.node) { + if ((node != null) && (node.getNetworkType() != WalletManager.getInstance().getNetworkType())) + throw new IllegalArgumentException("network type does not match"); + this.node = node; + for (NodeInfo nodeInfo : favouriteNodes) { + nodeInfo.setSelected(nodeInfo == node); + } + WalletManager.getInstance().setDaemon(node); + if (save) + saveSelectedNode(); + } + } + + @Override + public Set getFavouriteNodes() { + return favouriteNodes; + } + + @Override + public Set getOrPopulateFavourites() { + if (favouriteNodes.isEmpty()) { + for (DefaultNodes node : DefaultNodes.values()) { + NodeInfo nodeInfo = NodeInfo.fromString(node.getUri()); + if (nodeInfo != null) { + nodeInfo.setFavourite(true); + favouriteNodes.add(nodeInfo); + } + } + saveFavourites(); + } + return favouriteNodes; + } + + @Override + public void setFavouriteNodes(Collection nodes) { + Timber.d("adding %d nodes", nodes.size()); + favouriteNodes.clear(); + for (NodeInfo node : nodes) { + Timber.d("adding %s %b", node, node.isFavourite()); + if (node.isFavourite()) + favouriteNodes.add(node); + } + saveFavourites(); + } + + private void loadFavouritesWithNetwork() { + Helper.runWithNetwork(() -> { + loadFavourites(); + return true; + }); + } + + private void loadFavourites() { + Timber.d("loadFavourites"); + favouriteNodes.clear(); + final String selectedNodeId = getSelectedNodeId(); + Map storedNodes = getSharedPreferences(NODES_PREFS_NAME, Context.MODE_PRIVATE).getAll(); + for (Map.Entry nodeEntry : storedNodes.entrySet()) { + if (nodeEntry != null) { // just in case, ignore possible future errors + final String nodeId = (String) nodeEntry.getValue(); + final NodeInfo addedNode = addFavourite(nodeId); + if (addedNode != null) { + if (nodeId.equals(selectedNodeId)) { + addedNode.setSelected(true); + } + } + } + } + if (storedNodes.isEmpty()) { // try to load legacy list & remove it (i.e. migrate the data once) + SharedPreferences sharedPref = getPreferences(Context.MODE_PRIVATE); + switch (WalletManager.getInstance().getNetworkType()) { + case NetworkType_Mainnet: + loadLegacyList(sharedPref.getString(PREF_DAEMON_MAINNET, null)); + sharedPref.edit().remove(PREF_DAEMON_MAINNET).apply(); + break; + case NetworkType_Stagenet: + loadLegacyList(sharedPref.getString(PREF_DAEMON_STAGENET, null)); + sharedPref.edit().remove(PREF_DAEMON_STAGENET).apply(); + break; + case NetworkType_Testnet: + loadLegacyList(sharedPref.getString(PREF_DAEMON_TESTNET, null)); + sharedPref.edit().remove(PREF_DAEMON_TESTNET).apply(); + break; + default: + throw new IllegalStateException("unsupported net " + WalletManager.getInstance().getNetworkType()); + } + } + } + + private void saveFavourites() { + Timber.d("SAVE"); + SharedPreferences.Editor editor = getSharedPreferences(NODES_PREFS_NAME, Context.MODE_PRIVATE).edit(); + editor.clear(); + int i = 1; + for (Node info : favouriteNodes) { + final String nodeString = info.toNodeString(); + editor.putString(Integer.toString(i), nodeString); + Timber.d("saved %d:%s", i, nodeString); + i++; + } + editor.apply(); + } + + private NodeInfo addFavourite(String nodeString) { + final NodeInfo nodeInfo = NodeInfo.fromString(nodeString); + if (nodeInfo != null) { + nodeInfo.setFavourite(true); + favouriteNodes.add(nodeInfo); + } else + Timber.w("nodeString invalid: %s", nodeString); + return nodeInfo; + } + + private void loadLegacyList(final String legacyListString) { + if (legacyListString == null) return; + final String[] nodeStrings = legacyListString.split(";"); + for (final String nodeString : nodeStrings) { + addFavourite(nodeString); + } + } + + private void saveSelectedNode() { // save only if changed + final NodeInfo nodeInfo = getNode(); + final String selectedNodeId = getSelectedNodeId(); + if (nodeInfo != null) { + if (!nodeInfo.toNodeString().equals(selectedNodeId)) + saveSelectedNode(nodeInfo); + } else { + if (selectedNodeId != null) + saveSelectedNode(null); + } + } + + private void saveSelectedNode(NodeInfo nodeInfo) { + SharedPreferences.Editor editor = getSharedPreferences(SELECTED_NODE_PREFS_NAME, Context.MODE_PRIVATE).edit(); + if (nodeInfo == null) { + editor.clear(); + } else { + editor.putString("0", getNode().toNodeString()); + } + editor.apply(); + } + + private String getSelectedNodeId() { + return getSharedPreferences(SELECTED_NODE_PREFS_NAME, Context.MODE_PRIVATE) + .getString("0", null); + } + + + 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 + public boolean hasLedger() { + return Ledger.isConnected(); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + Timber.d("onCreate()"); + ThemeHelper.setPreferred(this); + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_login); + toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + getSupportActionBar().setDisplayShowTitleEnabled(false); + + toolbar.setOnButtonListener(type -> { + switch (type) { + case Toolbar.BUTTON_BACK: + onBackPressed(); + break; + case Toolbar.BUTTON_CLOSE: + finish(); + break; + case Toolbar.BUTTON_SETTINGS: + startSettingsFragment(); + break; + case Toolbar.BUTTON_NONE: + break; + default: + Timber.e("Button " + type + "pressed - how can this be?"); + } + }); + + loadFavouritesWithNetwork(); + + LegacyStorageHelper.migrateWallets(this); + + if (savedInstanceState == null) startLoginFragment(); + + // try intents + Intent intent = getIntent(); + if (!processUsbIntent(intent)) + processUriIntent(intent); + } + + 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, boolean streetmode) { + if (node == null) { + Toast.makeText(this, getString(R.string.prompt_daemon_missing), Toast.LENGTH_SHORT).show(); + return false; + } + if (checkServiceRunning()) return false; + try { + new AsyncOpenWallet(walletName, node, streetmode).execute(); + } 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 = (dialog, 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 act(String walletName1, String password, boolean fingerprintUsed) { + if (checkDevice(walletName1, password)) + startDetails(walletFile, password, GenerateReviewFragment.VIEW_TYPE_DETAILS); + } + + @Override + public void fail(String walletName) { + } + }); + } 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 MaterialAlertDialogBuilder(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(); + } + + private void renameWallet(String oldName, String newName) { + File walletFile = Helper.getWalletFile(this, oldName); + boolean success = renameWallet(walletFile, newName); + if (success) { + reloadWalletList(); + } else { + Toast.makeText(LoginActivity.this, getString(R.string.rename_failed), Toast.LENGTH_LONG).show(); + } + } + + // copy + delete seems safer than rename because we can rollback easily + boolean renameWallet(File walletFile, String newName) { + if (copyWallet(walletFile, new File(walletFile.getParentFile(), newName), false, true)) { + try { + KeyStoreHelper.copyWalletUserPass(this, walletFile.getName(), newName); + } catch (KeyStoreHelper.BrokenPasswordStoreException ex) { + Timber.w(ex); + } + deleteWallet(walletFile); // also deletes the keystore entry + return true; + } else { + return false; + } + } + + @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 MaterialAlertDialogBuilder(this); + alertDialogBuilder.setView(promptsView); + + final EditText etRename = promptsView.findViewById(R.id.etRename); + final TextView tvRenameLabel = 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), + (dialog, id) -> { + Helper.hideKeyboardAlways(LoginActivity.this); + String newName = etRename.getText().toString(); + renameWallet(walletName, newName); + }) + .setNegativeButton(getString(R.string.label_cancel), + (dialog, id) -> { + Helper.hideKeyboardAlways(LoginActivity.this); + dialog.cancel(); + }); + + final AlertDialog dialog = alertDialogBuilder.create(); + Helper.showKeyboard(dialog); + + // accept keyboard "ok" + etRename.setOnEditorActionListener((v, actionId, event) -> { + if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN)) + || (actionId == EditorInfo.IME_ACTION_DONE)) { + Helper.hideKeyboardAlways(LoginActivity.this); + String newName = etRename.getText().toString(); + dialog.cancel(); + renameWallet(walletName, newName); + return false; + } + return false; + }); + + dialog.show(); + } + + private static final int CREATE_BACKUP_INTENT = 4711; + private static final int RESTORE_BACKUP_INTENT = 4712; + private ZipBackup zipBackup; + + @Override + public void onWalletBackup(String walletName) { + Timber.d("backup for wallet ." + walletName + "."); + // overwrite any pending backup request + zipBackup = new ZipBackup(this, walletName); + + Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("application/zip"); + intent.putExtra(Intent.EXTRA_TITLE, zipBackup.getBackupName()); + startActivityForResult(intent, CREATE_BACKUP_INTENT); + } + + @Override + public void onWalletRestore() { + Timber.d("restore wallet"); + + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("application/zip"); + startActivityForResult(intent, RESTORE_BACKUP_INTENT); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == CREATE_BACKUP_INTENT) { + if (data == null) { + // nothing selected + Toast.makeText(this, getString(R.string.backup_failed), Toast.LENGTH_LONG).show(); + zipBackup = null; + return; + } + try { + if (zipBackup == null) return; // ignore unsolicited request + zipBackup.writeTo(data.getData()); + Toast.makeText(this, getString(R.string.backup_success), Toast.LENGTH_SHORT).show(); + } catch (IOException ex) { + Timber.e(ex); + Toast.makeText(this, getString(R.string.backup_failed), Toast.LENGTH_LONG).show(); + } finally { + zipBackup = null; + } + } else if (requestCode == RESTORE_BACKUP_INTENT) { + if (data == null) { + // nothing selected + Toast.makeText(this, getString(R.string.restore_failed), Toast.LENGTH_LONG).show(); + return; + } + try { + ZipRestore zipRestore = new ZipRestore(this, data.getData()); + Toast.makeText(this, getString(R.string.menu_restore), Toast.LENGTH_SHORT).show(); + if (zipRestore.restore()) { + reloadWalletList(); + } else { + Toast.makeText(this, getString(R.string.restore_failed), Toast.LENGTH_LONG).show(); + } + } catch (IOException ex) { + Timber.e(ex); + Toast.makeText(this, getString(R.string.restore_failed), Toast.LENGTH_LONG).show(); + } + } + } + + @Override + public void onWalletDelete(final String walletName) { + Timber.d("delete for wallet ." + walletName + "."); + if (checkServiceRunning()) return; + DialogInterface.OnClickListener dialogClickListener = (dialog, which) -> { + switch (which) { + case DialogInterface.BUTTON_POSITIVE: + if (deleteWallet(Helper.getWalletFile(LoginActivity.this, walletName))) { + reloadWalletList(); + } else { + Toast.makeText(LoginActivity.this, getString(R.string.delete_failed), Toast.LENGTH_LONG).show(); + } + break; + case DialogInterface.BUTTON_NEGATIVE: + // do nothing + break; + } + }; + + final AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this); + final AlertDialog confirm = builder.setMessage(getString(R.string.delete_alert_message)) + .setTitle(walletName) + .setPositiveButton(getString(R.string.delete_alert_yes), dialogClickListener) + .setNegativeButton(getString(R.string.delete_alert_no), dialogClickListener) + .setView(View.inflate(builder.getContext(), R.layout.checkbox_confirm, null)) + .show(); + confirm.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(false); + final MaterialCheckBox checkBox = confirm.findViewById(R.id.checkbox); + checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> { + confirm.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(isChecked); + }); + } + + @Override + public void onWalletDeleteCache(final String walletName) { + Timber.d("delete cache for wallet ." + walletName + "."); + if (checkServiceRunning()) return; + DialogInterface.OnClickListener dialogClickListener = (dialog, which) -> { + switch (which) { + case DialogInterface.BUTTON_POSITIVE: + if (!deleteWalletCache(Helper.getWalletFile(LoginActivity.this, walletName))) { + Toast.makeText(LoginActivity.this, getString(R.string.delete_failed), Toast.LENGTH_LONG).show(); + } + break; + case DialogInterface.BUTTON_NEGATIVE: + // do nothing + break; + } + }; + + final AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this); + final AlertDialog confirm = builder.setMessage(getString(R.string.deletecache_alert_message)) + .setTitle(walletName) + .setPositiveButton(getString(R.string.delete_alert_yes), dialogClickListener) + .setNegativeButton(getString(R.string.delete_alert_no), dialogClickListener) + .setView(View.inflate(builder.getContext(), R.layout.checkbox_confirm, null)) + .show(); + confirm.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(false); + final MaterialCheckBox checkBox = confirm.findViewById(R.id.checkbox); + checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> { + confirm.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(isChecked); + }); + } + + void reloadWalletList() { + Timber.d("reloadWalletList()"); + try { + LoginFragment loginFragment = (LoginFragment) + getSupportFragmentManager().findFragmentById(R.id.fragment_container); + if (loginFragment != null) { + loginFragment.loadList(); + } + } catch (ClassCastException ex) { + Timber.w(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); + } + + @Override + public void onNodePrefs() { + Timber.d("node prefs"); + if (checkServiceRunning()) return; + startNodeFragment(); + } + + //////////////////////////////////////// + // LoginFragment.Listener + //////////////////////////////////////// + + @Override + public File getStorageRoot() { + return Helper.getWalletRoot(getApplicationContext()); + } + + //////////////////////////////////////// + //////////////////////////////////////// + + @Override + public void showNet() { + showNet(WalletManager.getInstance().getNetworkType()); + } + + private void showNet(NetworkType net) { + switch (net) { + case NetworkType_Mainnet: + toolbar.setSubtitle(null); + toolbar.setBackgroundResource(R.drawable.backgound_toolbar_mainnet); + break; + case NetworkType_Testnet: + toolbar.setSubtitle(getString(R.string.connect_testnet)); + toolbar.setBackgroundResource(ThemeHelper.getThemedResourceId(this, R.attr.colorPrimaryDark)); + break; + case NetworkType_Stagenet: + toolbar.setSubtitle(getString(R.string.connect_stagenet)); + toolbar.setBackgroundResource(ThemeHelper.getThemedResourceId(this, R.attr.colorPrimaryDark)); + break; + default: + throw new IllegalStateException("NetworkType unknown: " + net); + } + } + + @Override + protected void onPause() { + Timber.d("onPause()"); + super.onPause(); + } + + @Override + protected void onDestroy() { + Timber.d("onDestroy"); + dismissProgressDialog(); + unregisterDetachReceiver(); + Ledger.disconnect(); + 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(); + } + if (!Ledger.isConnected()) attachLedger(); + registerTor(); + } + + 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; + } + + @Override + protected void onPostExecute(Void result) { + super.onPostExecute(result); + if (isDestroyed()) { + return; + } + dismissProgressDialog(); + } + } + + void startWallet(String walletName, String walletPassword, + boolean fingerprintUsed, boolean streetmode) { + 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); + intent.putExtra(WalletActivity.REQUEST_STREETMODE, streetmode); + if (uri != null) { + intent.putExtra(WalletActivity.REQUEST_URI, uri); + uri = null; // use only once + } + 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 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 startNodeFragment() { + replaceFragment(new NodeFragment(), null, null); + Timber.d("NodeFragment placed"); + } + + void startSettingsFragment() { + replaceFragment(new SettingsFragment(), null, null); + Timber.d("SettingsFragment 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(); + acquireWakeLock(); + if (walletCreator.isLedger()) { + showLedgerProgressDialog(LedgerProgressDialog.TYPE_RESTORE); + } else { + showProgressDialog(R.string.generate_wallet_creating); + } + } + + @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; + } + } + + @Override + protected void onPostExecute(Boolean result) { + super.onPostExecute(result); + releaseWakeLock(RELEASE_WAKE_LOCK_DELAY); + if (isDestroyed()) { + return; + } + dismissProgressDialog(); + if (result) { + startDetails(newWalletFile, walletPassword, GenerateReviewFragment.VIEW_TYPE_ACCEPT); + } else { + walletGenerateError(); + } + } + } + + 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); + + boolean isLedger(); + + } + + boolean checkAndCloseWallet(Wallet aWallet) { + Wallet.Status walletStatus = aWallet.getStatus(); + if (!walletStatus.isOk()) { + Timber.e(walletStatus.getErrorString()); + toast(walletStatus.getErrorString()); + } + aWallet.close(); + return walletStatus.isOk(); + } + + @Override + public void onGenerate(final String name, final String password) { + createWallet(name, password, + new WalletCreator() { + @Override + public boolean isLedger() { + return false; + } + + @Override + public boolean createWallet(File aFile, String password) { + NodeInfo currentNode = getNode(); + // get it from the connected node if we have one + final long restoreHeight = + (currentNode != null) ? currentNode.getHeight() : -1; + Wallet newWallet = WalletManager.getInstance() + .createWallet(aFile, password, MNEMONIC_LANGUAGE, restoreHeight); + return checkAndCloseWallet(newWallet); + } + }); + } + + @Override + public void onGenerate(final String name, final String password, + final String seed, final String offset, + final long restoreHeight) { + createWallet(name, password, + new WalletCreator() { + @Override + public boolean isLedger() { + return false; + } + + @Override + public boolean createWallet(File aFile, String password) { + Wallet newWallet = WalletManager.getInstance() + .recoveryWallet(aFile, password, seed, offset, restoreHeight); + return checkAndCloseWallet(newWallet); + } + }); + } + + @Override + public void onGenerateLedger(final String name, final String password, + final long restoreHeight) { + createWallet(name, password, + new WalletCreator() { + @Override + public boolean isLedger() { + return true; + } + + @Override + public boolean createWallet(File aFile, String password) { + Wallet newWallet = WalletManager.getInstance() + .createWalletFromDevice(aFile, password, + restoreHeight, "Ledger"); + return checkAndCloseWallet(newWallet); + } + }); + } + + @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() { + @Override + public boolean isLedger() { + return false; + } + + @Override + public boolean createWallet(File aFile, String password) { + Wallet newWallet = WalletManager.getInstance() + .createWalletWithKeys(aFile, password, MNEMONIC_LANGUAGE, restoreHeight, + address, viewKey, spendKey); + return checkAndCloseWallet(newWallet); + } + }); + } + + private void toast(final String msg) { + runOnUiThread(() -> Toast.makeText(LoginActivity.this, msg, Toast.LENGTH_LONG).show()); + } + + private void toast(final int msgId) { + runOnUiThread(() -> Toast.makeText(LoginActivity.this, getString(msgId), Toast.LENGTH_LONG).show()); + } + + @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 - so remove it + + popFragmentStack(GENERATE_STACK); + Toast.makeText(LoginActivity.this, + getString(R.string.generate_wallet_created), Toast.LENGTH_SHORT).show(); + } + + 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 { + copyFile(new File(srcDir, srcName + ".keys"), new File(dstDir, dstName + ".keys")); + try { // cache & address.txt are optional files + copyFile(new File(srcDir, srcName), new File(dstDir, dstName)); + copyFile(new File(srcDir, srcName + ".address.txt"), new File(dstDir, dstName + ".address.txt")); + } catch (IOException ex) { + Timber.d("CACHE %s", ignoreCacheError); + if (!ignoreCacheError) { // ignore cache backup error if backing up (can be resynced) + throw ex; + } + } + 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); + KeyStoreHelper.removeWalletUserPass(this, walletFile.getName()); + return success; + } + + boolean deleteWalletCache(File walletFile) { + Timber.d("deleteWalletCache %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(); + } + return success; + } + + void copyFile(File src, File dst) throws IOException { + try (FileChannel inChannel = new FileInputStream(src).getChannel(); + FileChannel outChannel = new FileOutputStream(dst).getChannel()) { + inChannel.transferTo(0, inChannel.size(), outChannel); + } + } + + @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 NodeFragment) { + if (!((NodeFragment) f).isRefreshing()) { + super.onBackPressed(); + } else { + Toast.makeText(LoginActivity.this, getString(R.string.node_refresh_wait), Toast.LENGTH_LONG).show(); + } + } else if (f instanceof LoginFragment) { + if (((LoginFragment) f).isFabOpen()) { + ((LoginFragment) f).animateFAB(); + } else { + super.onBackPressed(); + } + } else { + super.onBackPressed(); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + final int id = item.getItemId(); + if (id == R.id.action_create_help_new) { + HelpFragment.display(getSupportFragmentManager(), R.string.help_create_new); + return true; + } else if (id == R.id.action_create_help_keys) { + HelpFragment.display(getSupportFragmentManager(), R.string.help_create_keys); + return true; + } else if (id == R.id.action_create_help_view) { + HelpFragment.display(getSupportFragmentManager(), R.string.help_create_view); + return true; + } else if (id == R.id.action_create_help_seed) { + HelpFragment.display(getSupportFragmentManager(), R.string.help_create_seed); + return true; + } else if (id == R.id.action_create_help_ledger) { + HelpFragment.display(getSupportFragmentManager(), R.string.help_create_ledger); + return true; + } else if (id == R.id.action_details_help) { + HelpFragment.display(getSupportFragmentManager(), R.string.help_details); + return true; + } else if (id == R.id.action_details_changepw) { + onWalletChangePassword(); + return true; + } else if (id == R.id.action_help_list) { + HelpFragment.display(getSupportFragmentManager(), R.string.help_list); + return true; + } else if (id == R.id.action_help_node) { + HelpFragment.display(getSupportFragmentManager(), R.string.help_node); + return true; + } else if (id == R.id.action_default_nodes) { + Fragment f = getSupportFragmentManager().findFragmentById(R.id.fragment_container); + if ((WalletManager.getInstance().getNetworkType() == NetworkType.NetworkType_Mainnet) && + (f instanceof NodeFragment)) { + ((NodeFragment) f).restoreDefaultNodes(); + } + return true; + } else if (id == R.id.action_ledger_seed) { + Fragment f = getSupportFragmentManager().findFragmentById(R.id.fragment_container); + if (f instanceof GenerateFragment) { + ((GenerateFragment) f).convertLedgerSeed(); + } + return true; + } else { + return super.onOptionsItemSelected(item); + } + } + + // an AsyncTask which tests the node before trying to open the wallet + 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; + + private final String walletName; + private final NodeInfo node; + private final boolean streetmode; + + AsyncOpenWallet(String walletName, NodeInfo node, boolean streetmode) { + this.walletName = walletName; + this.node = node; + this.streetmode = streetmode; + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + } + + @Override + protected Boolean doInBackground(Void... params) { + Timber.d("checking %s", node.getAddress()); + return node.testRpcService(); + } + + @Override + protected void onPostExecute(Boolean result) { + super.onPostExecute(result); + if (isDestroyed()) { + return; + } + if (result) { + Timber.d("selected wallet is .%s.", node.getName()); + // now it's getting real, onValidateFields if wallet exists + promptAndStart(walletName, streetmode); + } else { + if (node.getResponseCode() == 0) { // IOException + Toast.makeText(LoginActivity.this, getString(R.string.status_wallet_node_invalid), Toast.LENGTH_LONG).show(); + } else { // connected but broken + Toast.makeText(LoginActivity.this, getString(R.string.status_wallet_connect_ioex), Toast.LENGTH_LONG).show(); + } + } + } + + } + + boolean checkDevice(String walletName, String password) { + String keyPath = new File(Helper.getWalletRoot(LoginActivity.this), + walletName + ".keys").getAbsolutePath(); + // check if we need connected hardware + Wallet.Device device = WalletManager.getInstance().queryWalletDevice(keyPath, password); + if (device == Wallet.Device.Device_Ledger) { + if (!hasLedger()) { + toast(R.string.open_wallet_ledger_missing); + } else { + return true; + } + } else {// device could be undefined meaning the password is wrong + // this gets dealt with later + return true; + } + return false; + } + + void promptAndStart(String walletName, final boolean streetmode) { + File walletFile = Helper.getWalletFile(this, walletName); + if (WalletManager.getInstance().walletExists(walletFile)) { + Helper.promptPassword(LoginActivity.this, walletName, false, + new Helper.PasswordAction() { + @Override + public void act(String walletName, String password, boolean fingerprintUsed) { + if (checkDevice(walletName, password)) + startWallet(walletName, password, fingerprintUsed, streetmode); + } + + @Override + public void fail(String walletName) { + } + + }); + } else { // this cannot really happen as we prefilter choices + Toast.makeText(this, getString(R.string.bad_wallet), Toast.LENGTH_SHORT).show(); + } + } + + // USB Stuff - (Ledger) + + private static final String ACTION_USB_PERMISSION = "com.m2049r.xmrwallet.USB_PERMISSION"; + + void attachLedger() { + final UsbManager usbManager = getUsbManager(); + UsbDevice device = Ledger.findDevice(usbManager); + if (device != null) { + if (usbManager.hasPermission(device)) { + connectLedger(usbManager, device); + } else { + registerReceiver(usbPermissionReceiver, new IntentFilter(ACTION_USB_PERMISSION)); + usbManager.requestPermission(device, + PendingIntent.getBroadcast(this, 0, + new Intent(ACTION_USB_PERMISSION), + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_IMMUTABLE : 0)); + } + } else { + Timber.d("no ledger device found"); + } + } + + private final BroadcastReceiver usbPermissionReceiver = new BroadcastReceiver() { + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (ACTION_USB_PERMISSION.equals(action)) { + unregisterReceiver(usbPermissionReceiver); + synchronized (this) { + UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); + if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { + if (device != null) { + connectLedger(getUsbManager(), device); + } + } else { + Timber.w("User denied permission for device %s", device.getProductName()); + } + } + } + } + }; + + private void connectLedger(UsbManager usbManager, final UsbDevice usbDevice) { + if (Ledger.ENABLED) + try { + Ledger.connect(usbManager, usbDevice); + if (!Ledger.check()) { + Ledger.disconnect(); + runOnUiThread(() -> Toast.makeText(LoginActivity.this, + getString(R.string.toast_ledger_start_app, usbDevice.getProductName()), + Toast.LENGTH_SHORT) + .show()); + } else { + registerDetachReceiver(); + onLedgerAction(); + runOnUiThread(() -> Toast.makeText(LoginActivity.this, + getString(R.string.toast_ledger_attached, usbDevice.getProductName()), + Toast.LENGTH_SHORT) + .show()); + } + } catch (IOException ex) { + runOnUiThread(() -> Toast.makeText(LoginActivity.this, + getString(R.string.open_wallet_ledger_missing), + Toast.LENGTH_SHORT) + .show()); + } + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + processUsbIntent(intent); + } + + private boolean processUsbIntent(Intent intent) { + String action = intent.getAction(); + if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) { + synchronized (this) { + final UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); + if (device != null) { + final UsbManager usbManager = getUsbManager(); + if (usbManager.hasPermission(device)) { + Timber.d("Ledger attached by intent"); + connectLedger(usbManager, device); + } + } + } + return true; + } + return false; + } + + private String uri = null; + + private void processUriIntent(Intent intent) { + String action = intent.getAction(); + if (Intent.ACTION_VIEW.equals(action)) { + synchronized (this) { + uri = intent.getDataString(); + Timber.d("URI Intent %s", uri); + HelpFragment.display(getSupportFragmentManager(), R.string.help_uri); + } + } + } + + BroadcastReceiver detachReceiver; + + private void unregisterDetachReceiver() { + if (detachReceiver != null) { + unregisterReceiver(detachReceiver); + detachReceiver = null; + } + } + + private void registerDetachReceiver() { + detachReceiver = new BroadcastReceiver() { + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) { + unregisterDetachReceiver(); + final UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); + Timber.i("Ledger detached!"); + if (device != null) + runOnUiThread(() -> Toast.makeText(LoginActivity.this, + getString(R.string.toast_ledger_detached, device.getProductName()), + Toast.LENGTH_SHORT) + .show()); + Ledger.disconnect(); + onLedgerAction(); + } + } + }; + + registerReceiver(detachReceiver, new IntentFilter(UsbManager.ACTION_USB_DEVICE_DETACHED)); + } + + public void onLedgerAction() { + Fragment f = getSupportFragmentManager().findFragmentById(R.id.fragment_container); + if (f instanceof GenerateFragment) { + onBackPressed(); + } else if (f instanceof LoginFragment) { + if (((LoginFragment) f).isFabOpen()) { + ((LoginFragment) f).animateFAB(); + } + } + } + + // get UsbManager or die trying + @NonNull + private UsbManager getUsbManager() { + final UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE); + if (usbManager == null) { + throw new IllegalStateException("no USB_SERVICE"); + } + return usbManager; + } + + // + // Tor (Orbot) stuff + // + + void torNotify() { + final Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_container); + if (fragment == null) return; + + if (fragment instanceof LoginFragment) { + runOnUiThread(((LoginFragment) fragment)::showNetwork); + } + } + + private void deregisterTor() { + NetCipherHelper.deregister(); + } + + private void registerTor() { + NetCipherHelper.register(new NetCipherHelper.OnStatusChangedListener() { + @Override + public void connected() { + Timber.d("CONNECTED"); + WalletManager.getInstance().setProxy(NetCipherHelper.getProxy()); + torNotify(); + if (waitingUiTask != null) { + Timber.d("RUN"); + runOnUiThread(waitingUiTask); + waitingUiTask = null; + } + } + + @Override + public void disconnected() { + Timber.d("DISCONNECTED"); + WalletManager.getInstance().setProxy(""); + torNotify(); + } + + @Override + public void notInstalled() { + Timber.d("NOT INSTALLED"); + WalletManager.getInstance().setProxy(""); + torNotify(); + } + + @Override + public void notEnabled() { + Timber.d("NOT ENABLED"); + notInstalled(); + } + }); + } + + private Runnable waitingUiTask; + + @Override + public void runOnNetCipher(Runnable uiTask) { + if (waitingUiTask != null) throw new IllegalStateException("only one tor task at a time"); + if (NetCipherHelper.hasClient()) { + runOnUiThread(uiTask); + } else { + waitingUiTask = uiTask; + } + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java b/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java new file mode 100644 index 0000000..21b85be --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java @@ -0,0 +1,563 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet; + +import android.content.Context; +import android.os.AsyncTask; +import android.os.Bundle; +import android.text.Html; +import android.text.Spanned; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.FrameLayout; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.google.android.material.progressindicator.CircularProgressIndicator; +import com.m2049r.xmrwallet.data.NodeInfo; +import com.m2049r.xmrwallet.dialog.HelpFragment; +import com.m2049r.xmrwallet.layout.WalletInfoAdapter; +import com.m2049r.xmrwallet.model.WalletManager; +import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.KeyStoreHelper; +import com.m2049r.xmrwallet.util.NetCipherHelper; +import com.m2049r.xmrwallet.util.NodePinger; +import com.m2049r.xmrwallet.util.Notice; +import com.m2049r.xmrwallet.util.ThemeHelper; +import com.m2049r.xmrwallet.widget.Toolbar; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import timber.log.Timber; + +public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInteractionListener, + View.OnClickListener { + + private WalletInfoAdapter adapter; + + private final List walletList = new ArrayList<>(); + + private View tvGuntherSays; + private ImageView ivGunther; + private TextView tvNodeName; + private TextView tvNodeInfo; + private ImageButton ibNetwork; + private CircularProgressIndicator pbNetwork; + + private Listener activityCallback; + + // Container Activity must implement this interface + public interface Listener { + File getStorageRoot(); + + boolean onWalletSelected(String wallet, boolean streetmode); + + void onWalletDetails(String wallet); + + void onWalletRename(String name); + + void onWalletBackup(String name); + + void onWalletRestore(); + + void onWalletDelete(String walletName); + + void onWalletDeleteCache(String walletName); + + void onAddWallet(String type); + + void onNodePrefs(); + + void showNet(); + + void setToolbarButton(int type); + + void setTitle(String title); + + void setNode(NodeInfo node); + + NodeInfo getNode(); + + Set getFavouriteNodes(); + + Set getOrPopulateFavourites(); + + boolean hasLedger(); + + void runOnNetCipher(Runnable runnable); + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + if (context instanceof Listener) { + this.activityCallback = (Listener) context; + } else { + throw new ClassCastException(context.toString() + + " must implement Listener"); + } + } + + @Override + public void onPause() { + Timber.d("onPause()"); + torStatus = null; + super.onPause(); + } + + @Override + public void onResume() { + super.onResume(); + Timber.d("onResume() %s", activityCallback.getFavouriteNodes().size()); + activityCallback.setTitle(null); + activityCallback.setToolbarButton(Toolbar.BUTTON_SETTINGS); + activityCallback.showNet(); + showNetwork(); + //activityCallback.runOnNetCipher(this::pingSelectedNode); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + Timber.d("onCreateView"); + View view = inflater.inflate(R.layout.fragment_login, container, false); + + tvGuntherSays = view.findViewById(R.id.tvGuntherSays); + ivGunther = view.findViewById(R.id.ivGunther); + fabScreen = view.findViewById(R.id.fabScreen); + fab = view.findViewById(R.id.fab); + fabNew = view.findViewById(R.id.fabNew); + fabView = view.findViewById(R.id.fabView); + fabKey = view.findViewById(R.id.fabKey); + fabSeed = view.findViewById(R.id.fabSeed); + fabImport = view.findViewById(R.id.fabImport); + fabLedger = view.findViewById(R.id.fabLedger); + + fabNewL = view.findViewById(R.id.fabNewL); + fabViewL = view.findViewById(R.id.fabViewL); + fabKeyL = view.findViewById(R.id.fabKeyL); + fabSeedL = view.findViewById(R.id.fabSeedL); + fabImportL = view.findViewById(R.id.fabImportL); + fabLedgerL = view.findViewById(R.id.fabLedgerL); + + fab_pulse = AnimationUtils.loadAnimation(getContext(), R.anim.fab_pulse); + fab_open_screen = AnimationUtils.loadAnimation(getContext(), R.anim.fab_open_screen); + fab_close_screen = AnimationUtils.loadAnimation(getContext(), R.anim.fab_close_screen); + fab_open = AnimationUtils.loadAnimation(getContext(), R.anim.fab_open); + fab_close = AnimationUtils.loadAnimation(getContext(), R.anim.fab_close); + rotate_forward = AnimationUtils.loadAnimation(getContext(), R.anim.rotate_forward); + rotate_backward = AnimationUtils.loadAnimation(getContext(), R.anim.rotate_backward); + fab.setOnClickListener(this); + fabNew.setOnClickListener(this); + fabView.setOnClickListener(this); + fabKey.setOnClickListener(this); + fabSeed.setOnClickListener(this); + fabImport.setOnClickListener(this); + fabLedger.setOnClickListener(this); + fabScreen.setOnClickListener(this); + + RecyclerView recyclerView = view.findViewById(R.id.list); + registerForContextMenu(recyclerView); + this.adapter = new WalletInfoAdapter(getActivity(), this); + recyclerView.setAdapter(adapter); + + ViewGroup llNotice = view.findViewById(R.id.llNotice); + Notice.showAll(llNotice, ".*_login"); + + view.findViewById(R.id.llNode).setOnClickListener(v -> startNodePrefs()); + tvNodeName = view.findViewById(R.id.tvNodeName); + tvNodeInfo = view.findViewById(R.id.tvInfo); + view.findViewById(R.id.ibRenew).setOnClickListener(v -> findBestNode()); + ibNetwork = view.findViewById(R.id.ibNetwork); + ibNetwork.setOnClickListener(v -> changeNetwork()); + ibNetwork.setEnabled(false); + pbNetwork = view.findViewById(R.id.pbNetwork); + + Helper.hideKeyboard(getActivity()); + + loadList(); + + return view; + } + + // Callbacks from WalletInfoAdapter + + // Wallet touched + @Override + public void onInteraction(final View view, final WalletManager.WalletInfo infoItem) { + openWallet(infoItem.getName(), false); + } + + private void openWallet(String name, boolean streetmode) { + activityCallback.onWalletSelected(name, streetmode); + } + + @Override + public boolean onContextInteraction(MenuItem item, WalletManager.WalletInfo listItem) { + final int id = item.getItemId(); + if (id == R.id.action_streetmode) { + openWallet(listItem.getName(), true); + } else if (id == R.id.action_info) { + showInfo(listItem.getName()); + } else if (id == R.id.action_rename) { + activityCallback.onWalletRename(listItem.getName()); + } else if (id == R.id.action_backup) { + activityCallback.onWalletBackup(listItem.getName()); + } else if (id == R.id.action_archive) { + activityCallback.onWalletDelete(listItem.getName()); + } else if (id == R.id.action_deletecache) { + activityCallback.onWalletDeleteCache(listItem.getName()); + } else { + return super.onContextItemSelected(item); + } + return true; + } + + public void loadList() { + Timber.d("loadList()"); + WalletManager mgr = WalletManager.getInstance(); + walletList.clear(); + walletList.addAll(mgr.findWallets(activityCallback.getStorageRoot())); + adapter.setInfos(walletList); + + // deal with Gunther & FAB animation + if (walletList.isEmpty()) { + fab.startAnimation(fab_pulse); + if (ivGunther.getDrawable() == null) { + ivGunther.setImageResource(R.drawable.ic_emptygunther); + tvGuntherSays.setVisibility(View.VISIBLE); + } + } else { + fab.clearAnimation(); + if (ivGunther.getDrawable() != null) { + ivGunther.setImageDrawable(null); + } + tvGuntherSays.setVisibility(View.GONE); + } + + // remove information of non-existent wallet + Set removedWallets = getActivity() + .getSharedPreferences(KeyStoreHelper.SecurityConstants.WALLET_PASS_PREFS_NAME, Context.MODE_PRIVATE) + .getAll().keySet(); + for (WalletManager.WalletInfo s : walletList) { + removedWallets.remove(s.getName()); + } + for (String name : removedWallets) { + KeyStoreHelper.removeWalletUserPass(getActivity(), name); + } + } + + private void showInfo(@NonNull String name) { + activityCallback.onWalletDetails(name); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.list_menu, menu); + super.onCreateOptionsMenu(menu, inflater); + } + + private boolean isFabOpen = false; + private FloatingActionButton fab, fabNew, fabView, fabKey, fabSeed, fabImport, fabLedger; + private RelativeLayout fabScreen; + private RelativeLayout fabNewL, fabViewL, fabKeyL, fabSeedL, fabImportL, fabLedgerL; + private Animation fab_open, fab_close, rotate_forward, rotate_backward, fab_open_screen, fab_close_screen; + private Animation fab_pulse; + + public boolean isFabOpen() { + return isFabOpen; + } + + public void animateFAB() { + if (isFabOpen) { // close the fab + fabScreen.setClickable(false); + fabScreen.startAnimation(fab_close_screen); + fab.startAnimation(rotate_backward); + if (fabLedgerL.getVisibility() == View.VISIBLE) { + fabLedgerL.startAnimation(fab_close); + fabLedger.setClickable(false); + } else { + fabNewL.startAnimation(fab_close); + fabNew.setClickable(false); + fabViewL.startAnimation(fab_close); + fabView.setClickable(false); + fabKeyL.startAnimation(fab_close); + fabKey.setClickable(false); + fabSeedL.startAnimation(fab_close); + fabSeed.setClickable(false); + fabImportL.startAnimation(fab_close); + fabImport.setClickable(false); + } + isFabOpen = false; + } else { // open the fab + fabScreen.setClickable(true); + fabScreen.startAnimation(fab_open_screen); + fab.startAnimation(rotate_forward); + if (activityCallback.hasLedger()) { + fabLedgerL.setVisibility(View.VISIBLE); + fabNewL.setVisibility(View.GONE); + fabViewL.setVisibility(View.GONE); + fabKeyL.setVisibility(View.GONE); + fabSeedL.setVisibility(View.GONE); + fabImportL.setVisibility(View.GONE); + + fabLedgerL.startAnimation(fab_open); + fabLedger.setClickable(true); + } else { + fabLedgerL.setVisibility(View.GONE); + fabNewL.setVisibility(View.VISIBLE); + fabViewL.setVisibility(View.VISIBLE); + fabKeyL.setVisibility(View.VISIBLE); + fabSeedL.setVisibility(View.VISIBLE); + fabImportL.setVisibility(View.VISIBLE); + + fabNewL.startAnimation(fab_open); + fabNew.setClickable(true); + fabViewL.startAnimation(fab_open); + fabView.setClickable(true); + fabKeyL.startAnimation(fab_open); + fabKey.setClickable(true); + fabSeedL.startAnimation(fab_open); + fabSeed.setClickable(true); + fabImportL.startAnimation(fab_open); + fabImport.setClickable(true); + } + isFabOpen = true; + } + } + + @Override + public void onClick(View v) { + final int id = v.getId(); + Timber.d("onClick %d/%d", id, R.id.fabLedger); + if (id == R.id.fab) { + animateFAB(); + } else if (id == R.id.fabNew) { + fabScreen.setVisibility(View.INVISIBLE); + isFabOpen = false; + activityCallback.onAddWallet(GenerateFragment.TYPE_NEW); + } else if (id == R.id.fabView) { + animateFAB(); + activityCallback.onAddWallet(GenerateFragment.TYPE_VIEWONLY); + } else if (id == R.id.fabKey) { + animateFAB(); + activityCallback.onAddWallet(GenerateFragment.TYPE_KEY); + } else if (id == R.id.fabSeed) { + animateFAB(); + activityCallback.onAddWallet(GenerateFragment.TYPE_SEED); + } else if (id == R.id.fabImport) { + animateFAB(); + activityCallback.onWalletRestore(); + } else if (id == R.id.fabLedger) { + Timber.d("FAB_LEDGER"); + animateFAB(); + activityCallback.onAddWallet(GenerateFragment.TYPE_LEDGER); + } else if (id == R.id.fabScreen) { + animateFAB(); + } + } + + public void findBestNode() { + new AsyncFindBestNode().execute(AsyncFindBestNode.FIND_BEST); + } + + public void pingSelectedNode() { + new AsyncFindBestNode().execute(AsyncFindBestNode.PING_SELECTED); + } + + private NodeInfo autoselect(Set nodes) { + if (nodes.isEmpty()) return null; + NodePinger.execute(nodes, null); + List nodeList = new ArrayList<>(nodes); + Collections.sort(nodeList, NodeInfo.BestNodeComparator); + return nodeList.get(0); + } + + private void setSubtext(String status) { + final Context ctx = getContext(); + final Spanned text = Html.fromHtml(ctx.getString(R.string.status, + Integer.toHexString(ThemeHelper.getThemedColor(ctx, R.attr.positiveColor) & 0xFFFFFF), + Integer.toHexString(ThemeHelper.getThemedColor(ctx, android.R.attr.colorBackground) & 0xFFFFFF), + status, "")); + tvNodeInfo.setText(text); + } + + private class AsyncFindBestNode extends AsyncTask { + final static int PING_SELECTED = 0; + final static int FIND_BEST = 1; + + private boolean netState; + + @Override + protected void onPreExecute() { + super.onPreExecute(); + tvNodeName.setVisibility(View.GONE); + pbNetwork.setVisibility(View.VISIBLE); + netState = ibNetwork.isClickable(); + ibNetwork.setClickable(false); + setSubtext(getString(R.string.node_waiting)); + } + + @Override + protected NodeInfo doInBackground(Integer... params) { + Set favourites = activityCallback.getOrPopulateFavourites(); + NodeInfo selectedNode; + if (params[0] == FIND_BEST) { + selectedNode = autoselect(favourites); + } else if (params[0] == PING_SELECTED) { + selectedNode = activityCallback.getNode(); + if (!activityCallback.getFavouriteNodes().contains(selectedNode)) + selectedNode = null; // it's not in the favourites (any longer) + if (selectedNode == null) + for (NodeInfo node : favourites) { + if (node.isSelected()) { + selectedNode = node; + break; + } + } + if (selectedNode == null) { // autoselect + selectedNode = autoselect(favourites); + } else { + selectedNode.testRpcService(); + } + } else throw new IllegalStateException(); + if ((selectedNode != null) && selectedNode.isValid()) { + activityCallback.setNode(selectedNode); + return selectedNode; + } else { + activityCallback.setNode(null); + return null; + } + } + + @Override + protected void onPostExecute(NodeInfo result) { + if (!isAdded()) return; + tvNodeName.setVisibility(View.VISIBLE); + pbNetwork.setVisibility(View.INVISIBLE); + ibNetwork.setClickable(netState); + if (result != null) { + Timber.d("found a good node %s", result.toString()); + showNode(result); + } else { + tvNodeName.setText(getResources().getText(R.string.node_create_hint)); + tvNodeName.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); + tvNodeInfo.setText(null); + tvNodeInfo.setVisibility(View.GONE); + } + } + + @Override + protected void onCancelled(NodeInfo result) { //TODO: cancel this on exit from fragment + Timber.d("cancelled with %s", result); + } + } + + private void showNode(NodeInfo nodeInfo) { + tvNodeName.setText(nodeInfo.getName()); + nodeInfo.showInfo(tvNodeInfo); + tvNodeInfo.setVisibility(View.VISIBLE); + } + + private void startNodePrefs() { + activityCallback.onNodePrefs(); + } + + // Network (Tor) stuff + + private void changeNetwork() { + Timber.d("S: %s", NetCipherHelper.getStatus()); + final NetCipherHelper.Status status = NetCipherHelper.getStatus(); + if (status == NetCipherHelper.Status.NOT_INSTALLED) { + HelpFragment.display(requireActivity().getSupportFragmentManager(), R.string.help_tor); + } else if (status == NetCipherHelper.Status.NOT_ENABLED) { + Toast.makeText(getActivity(), getString(R.string.tor_enable_background), Toast.LENGTH_LONG).show(); + } else { + pbNetwork.setVisibility(View.VISIBLE); + ibNetwork.setEnabled(false); + NetCipherHelper.getInstance().toggle(); + } + } + + private NetCipherHelper.Status torStatus = null; + + void showNetwork() { + final NetCipherHelper.Status status = NetCipherHelper.getStatus(); + Timber.d("SHOW %s", status); + if (status == torStatus) return; + torStatus = status; + switch (status) { + case ENABLED: + ibNetwork.setImageResource(R.drawable.ic_network_tor_on); + ibNetwork.setEnabled(true); + ibNetwork.setClickable(true); + pbNetwork.setVisibility(View.INVISIBLE); + break; + case NOT_ENABLED: + case DISABLED: + ibNetwork.setImageResource(R.drawable.ic_network_clearnet); + ibNetwork.setEnabled(true); + ibNetwork.setClickable(true); + pbNetwork.setVisibility(View.INVISIBLE); + break; + case STARTING: + ibNetwork.setImageResource(R.drawable.ic_network_clearnet); + ibNetwork.setEnabled(false); + pbNetwork.setVisibility(View.VISIBLE); + break; + case STOPPING: + ibNetwork.setImageResource(R.drawable.ic_network_clearnet); + ibNetwork.setEnabled(false); + pbNetwork.setVisibility(View.VISIBLE); + break; + case NOT_INSTALLED: + ibNetwork.setEnabled(true); + ibNetwork.setClickable(true); + pbNetwork.setVisibility(View.INVISIBLE); + ibNetwork.setImageResource(R.drawable.ic_network_clearnet); + break; + default: + return; + } + activityCallback.runOnNetCipher(this::pingSelectedNode); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/MainActivity.java b/app/src/main/java/com/m2049r/xmrwallet/MainActivity.java new file mode 100644 index 0000000..5c7cc75 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/MainActivity.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2018-2020 EarlOfEgo, m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet; + +import android.content.Intent; +import android.os.Bundle; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; + +import com.m2049r.xmrwallet.onboarding.OnBoardingActivity; +import com.m2049r.xmrwallet.onboarding.OnBoardingManager; + +public class MainActivity extends BaseActivity { + @Override + protected void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (OnBoardingManager.shouldShowOnBoarding(getApplicationContext())) { + startActivity(new Intent(this, OnBoardingActivity.class)); + } else { + startActivity(new Intent(this, LoginActivity.class)); + } + finish(); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/NodeFragment.java b/app/src/main/java/com/m2049r/xmrwallet/NodeFragment.java new file mode 100644 index 0000000..0bbc5a4 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/NodeFragment.java @@ -0,0 +1,589 @@ +/* + * Copyright (c) 2018 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet; + +import android.content.Context; +import android.content.DialogInterface; +import android.os.AsyncTask; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.inputmethod.EditorInfo; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.textfield.TextInputLayout; +import com.m2049r.levin.scanner.Dispatcher; +import com.m2049r.xmrwallet.data.DefaultNodes; +import com.m2049r.xmrwallet.data.Node; +import com.m2049r.xmrwallet.data.NodeInfo; +import com.m2049r.xmrwallet.layout.NodeInfoAdapter; +import com.m2049r.xmrwallet.model.NetworkType; +import com.m2049r.xmrwallet.model.WalletManager; +import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.NodePinger; +import com.m2049r.xmrwallet.util.Notice; +import com.m2049r.xmrwallet.widget.Toolbar; + +import java.io.File; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.text.NumberFormat; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import timber.log.Timber; + +public class NodeFragment extends Fragment + implements NodeInfoAdapter.OnInteractionListener, View.OnClickListener { + + static private int NODES_TO_FIND = 10; + + static private NumberFormat FORMATTER = NumberFormat.getInstance(); + + private SwipeRefreshLayout pullToRefresh; + private TextView tvPull; + private View fab; + + private Set nodeList = new HashSet<>(); + + private NodeInfoAdapter nodesAdapter; + + private Listener activityCallback; + + public interface Listener { + File getStorageRoot(); + + void setToolbarButton(int type); + + void setSubtitle(String title); + + Set getFavouriteNodes(); + + Set getOrPopulateFavourites(); + + void setFavouriteNodes(Collection favouriteNodes); + + void setNode(NodeInfo node); + } + + void filterFavourites() { + for (Iterator iter = nodeList.iterator(); iter.hasNext(); ) { + Node node = iter.next(); + if (!node.isFavourite()) iter.remove(); + } + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (context instanceof Listener) { + this.activityCallback = (Listener) context; + } else { + throw new ClassCastException(context.toString() + + " must implement Listener"); + } + } + + @Override + public void onPause() { + Timber.d("onPause() %d", nodeList.size()); + if (asyncFindNodes != null) + asyncFindNodes.cancel(true); + if (activityCallback != null) + activityCallback.setFavouriteNodes(nodeList); + super.onPause(); + } + + @Override + public void onResume() { + super.onResume(); + Timber.d("onResume()"); + activityCallback.setSubtitle(getString(R.string.label_nodes)); + updateRefreshElements(); + } + + boolean isRefreshing() { + return asyncFindNodes != null; + } + + void updateRefreshElements() { + if (isRefreshing()) { + activityCallback.setToolbarButton(Toolbar.BUTTON_NONE); + fab.setVisibility(View.GONE); + } else { + activityCallback.setToolbarButton(Toolbar.BUTTON_BACK); + fab.setVisibility(View.VISIBLE); + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + Timber.d("onCreateView"); + View view = inflater.inflate(R.layout.fragment_node, container, false); + + fab = view.findViewById(R.id.fab); + fab.setOnClickListener(this); + + RecyclerView recyclerView = view.findViewById(R.id.list); + nodesAdapter = new NodeInfoAdapter(getActivity(), this); + recyclerView.setAdapter(nodesAdapter); + + tvPull = view.findViewById(R.id.tvPull); + + pullToRefresh = view.findViewById(R.id.pullToRefresh); + pullToRefresh.setOnRefreshListener(() -> { + if (WalletManager.getInstance().getNetworkType() == NetworkType.NetworkType_Mainnet) { + refresh(AsyncFindNodes.SCAN); + } else { + Toast.makeText(getActivity(), getString(R.string.node_wrong_net), Toast.LENGTH_LONG).show(); + pullToRefresh.setRefreshing(false); + } + }); + + Helper.hideKeyboard(getActivity()); + + nodeList = new HashSet<>(activityCallback.getFavouriteNodes()); + nodesAdapter.setNodes(nodeList); + + ViewGroup llNotice = view.findViewById(R.id.llNotice); + Notice.showAll(llNotice, ".*_nodes"); + + refresh(AsyncFindNodes.PING); // start connection tests + + return view; + } + + private AsyncFindNodes asyncFindNodes = null; + + private boolean refresh(int type) { + if (asyncFindNodes != null) return false; // ignore refresh request as one is ongoing + asyncFindNodes = new AsyncFindNodes(); + updateRefreshElements(); + asyncFindNodes.execute(type); + return true; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.node_menu, menu); + super.onCreateOptionsMenu(menu, inflater); + } + + // Callbacks from NodeInfoAdapter + @Override + public void onInteraction(final View view, final NodeInfo nodeItem) { + Timber.d("onInteraction"); + if (!nodeItem.isFavourite()) { + nodeItem.setFavourite(true); + activityCallback.setFavouriteNodes(nodeList); + } + AsyncTask.execute(() -> { + activityCallback.setNode(nodeItem); // this marks it as selected & saves it as well + nodeItem.setSelecting(false); + try { + requireActivity().runOnUiThread(() -> nodesAdapter.allowClick(true)); + } catch (IllegalStateException ex) { + // it's ok + } + }); + } + + // open up edit dialog + @Override + public boolean onLongInteraction(final View view, final NodeInfo nodeItem) { + Timber.d("onLongInteraction"); + EditDialog diag = createEditDialog(nodeItem); + if (diag != null) { + diag.show(); + } + return true; + } + + @Override + public void onClick(View v) { + int id = v.getId(); + if (id == R.id.fab) { + EditDialog diag = createEditDialog(null); + if (diag != null) { + diag.show(); + } + } + } + + private class AsyncFindNodes extends AsyncTask + implements NodePinger.Listener { + final static int SCAN = 0; + final static int RESTORE_DEFAULTS = 1; + final static int PING = 2; + + @Override + protected void onPreExecute() { + super.onPreExecute(); + filterFavourites(); + nodesAdapter.setNodes(null); + nodesAdapter.allowClick(false); + tvPull.setText(getString(R.string.node_scanning)); + } + + @Override + protected Boolean doInBackground(Integer... params) { + if (params[0] == RESTORE_DEFAULTS) { // true = restore defaults + for (DefaultNodes node : DefaultNodes.values()) { + NodeInfo nodeInfo = NodeInfo.fromString(node.getUri()); + if (nodeInfo != null) { + nodeInfo.setFavourite(true); + nodeList.add(nodeInfo); + } + } + NodePinger.execute(nodeList, this); + return true; + } else if (params[0] == PING) { + NodePinger.execute(nodeList, this); + return true; + } else if (params[0] == SCAN) { + // otherwise scan the network + Timber.d("scanning"); + Set seedList = new HashSet<>(); + seedList.addAll(nodeList); + nodeList.clear(); + Timber.d("seed %d", seedList.size()); + Dispatcher d = new Dispatcher(info -> publishProgress(info)); + d.seedPeers(seedList); + d.awaitTermination(NODES_TO_FIND); + + // we didn't find enough because we didn't ask around enough? ask more! + if ((d.getRpcNodes().size() < NODES_TO_FIND) && + (d.getPeerCount() < NODES_TO_FIND + seedList.size())) { + // try again + publishProgress((NodeInfo[]) null); + d = new Dispatcher(new Dispatcher.Listener() { + @Override + public void onGet(NodeInfo info) { + publishProgress(info); + } + }); + // also seed with monero seed nodes (see p2p/net_node.inl:410 in monero src) + seedList.add(new NodeInfo(new InetSocketAddress("107.152.130.98", 18080))); + seedList.add(new NodeInfo(new InetSocketAddress("212.83.175.67", 18080))); + seedList.add(new NodeInfo(new InetSocketAddress("5.9.100.248", 18080))); + seedList.add(new NodeInfo(new InetSocketAddress("163.172.182.165", 18080))); + seedList.add(new NodeInfo(new InetSocketAddress("161.67.132.39", 18080))); + seedList.add(new NodeInfo(new InetSocketAddress("198.74.231.92", 18080))); + seedList.add(new NodeInfo(new InetSocketAddress("195.154.123.123", 18080))); + seedList.add(new NodeInfo(new InetSocketAddress("212.83.172.165", 18080))); + seedList.add(new NodeInfo(new InetSocketAddress("192.110.160.146", 18080))); + d.seedPeers(seedList); + d.awaitTermination(NODES_TO_FIND); + } + // final (filtered) result + nodeList.addAll(d.getRpcNodes()); + return true; + } + return false; + } + + @Override + protected void onProgressUpdate(NodeInfo... values) { + Timber.d("onProgressUpdate"); + if (!isCancelled()) + if (values != null) + nodesAdapter.addNode(values[0]); + else + nodesAdapter.setNodes(null); + } + + @Override + protected void onPostExecute(Boolean result) { + Timber.d("done scanning"); + complete(); + } + + @Override + protected void onCancelled(Boolean result) { + Timber.d("cancelled scanning"); + complete(); + } + + private void complete() { + asyncFindNodes = null; + if (!isAdded()) return; + //if (isCancelled()) return; + tvPull.setText(getString(R.string.node_pull_hint)); + pullToRefresh.setRefreshing(false); + nodesAdapter.setNodes(nodeList); + nodesAdapter.allowClick(true); + updateRefreshElements(); + } + + public void publish(NodeInfo nodeInfo) { + publishProgress(nodeInfo); + } + } + + @Override + public void onDetach() { + Timber.d("detached"); + super.onDetach(); + } + + private EditDialog editDialog = null; // for preventing opening of multiple dialogs + + private EditDialog createEditDialog(final NodeInfo nodeInfo) { + if (editDialog != null) return null; // we are already open + editDialog = new EditDialog(nodeInfo); + return editDialog; + } + + class EditDialog { + final NodeInfo nodeInfo; + final NodeInfo nodeBackup; + + private boolean applyChanges() { + nodeInfo.clear(); + showTestResult(); + + final String portString = etNodePort.getEditText().getText().toString().trim(); + int port; + if (portString.isEmpty()) { + port = Node.getDefaultRpcPort(); + } else { + try { + port = Integer.parseInt(portString); + } catch (NumberFormatException ex) { + etNodePort.setError(getString(R.string.node_port_numeric)); + return false; + } + } + etNodePort.setError(null); + if ((port <= 0) || (port > 65535)) { + etNodePort.setError(getString(R.string.node_port_range)); + return false; + } + + final String host = etNodeHost.getEditText().getText().toString().trim(); + if (host.isEmpty()) { + etNodeHost.setError(getString(R.string.node_host_empty)); + return false; + } + final boolean setHostSuccess = Helper.runWithNetwork(() -> { + try { + nodeInfo.setHost(host); + return true; + } catch (UnknownHostException ex) { + return false; + } + }); + if (!setHostSuccess) { + etNodeHost.setError(getString(R.string.node_host_unresolved)); + return false; + } + etNodeHost.setError(null); + nodeInfo.setRpcPort(port); + // setName() may trigger reverse DNS + Helper.runWithNetwork(new Helper.Action() { + @Override + public boolean run() { + nodeInfo.setName(etNodeName.getEditText().getText().toString().trim()); + return true; + } + }); + nodeInfo.setUsername(etNodeUser.getEditText().getText().toString().trim()); + nodeInfo.setPassword(etNodePass.getEditText().getText().toString()); // no trim for pw + return true; + } + + private boolean shutdown = false; + + private void apply() { + if (applyChanges()) { + closeDialog(); + if (nodeBackup == null) { // this is a (FAB) new node + nodeInfo.setFavourite(true); + nodeList.add(nodeInfo); + } + shutdown = true; + new AsyncTestNode().execute(); + } + } + + private void closeDialog() { + if (editDialog == null) throw new IllegalStateException(); + Helper.hideKeyboardAlways(getActivity()); + editDialog.dismiss(); + editDialog = null; + NodeFragment.this.editDialog = null; + } + + private void show() { + editDialog.show(); + } + + private void test() { + if (applyChanges()) + new AsyncTestNode().execute(); + } + + private void showKeyboard() { + Helper.showKeyboard(editDialog); + } + + AlertDialog editDialog = null; + + TextInputLayout etNodeName; + TextInputLayout etNodeHost; + TextInputLayout etNodePort; + TextInputLayout etNodeUser; + TextInputLayout etNodePass; + TextView tvResult; + + void showTestResult() { + if (nodeInfo.isSuccessful()) { + tvResult.setText(getString(R.string.node_result, + FORMATTER.format(nodeInfo.getHeight()), nodeInfo.getMajorVersion(), + nodeInfo.getResponseTime(), nodeInfo.getHostAddress())); + } else { + tvResult.setText(NodeInfoAdapter.getResponseErrorText(getActivity(), nodeInfo.getResponseCode())); + } + } + + EditDialog(final NodeInfo nodeInfo) { + AlertDialog.Builder alertDialogBuilder = new MaterialAlertDialogBuilder(getActivity()); + LayoutInflater li = LayoutInflater.from(alertDialogBuilder.getContext()); + View promptsView = li.inflate(R.layout.prompt_editnode, null); + alertDialogBuilder.setView(promptsView); + + etNodeName = promptsView.findViewById(R.id.etNodeName); + etNodeHost = promptsView.findViewById(R.id.etNodeHost); + etNodePort = promptsView.findViewById(R.id.etNodePort); + etNodeUser = promptsView.findViewById(R.id.etNodeUser); + etNodePass = promptsView.findViewById(R.id.etNodePass); + tvResult = promptsView.findViewById(R.id.tvResult); + + if (nodeInfo != null) { + this.nodeInfo = nodeInfo; + nodeBackup = new NodeInfo(nodeInfo); + etNodeName.getEditText().setText(nodeInfo.getName()); + etNodeHost.getEditText().setText(nodeInfo.getHost()); + etNodePort.getEditText().setText(Integer.toString(nodeInfo.getRpcPort())); + etNodeUser.getEditText().setText(nodeInfo.getUsername()); + etNodePass.getEditText().setText(nodeInfo.getPassword()); + showTestResult(); + } else { + this.nodeInfo = new NodeInfo(); + nodeBackup = null; + } + + // set dialog message + alertDialogBuilder + .setCancelable(false) + .setPositiveButton(getString(R.string.label_ok), null) + .setNeutralButton(getString(R.string.label_test), null) + .setNegativeButton(getString(R.string.label_cancel), + (dialog, id) -> { + closeDialog(); + nodesAdapter.setNodes(); // to refresh test results + }); + + editDialog = alertDialogBuilder.create(); + // these need to be here, since we don't always close the dialog + editDialog.setOnShowListener(new DialogInterface.OnShowListener() { + @Override + public void onShow(final DialogInterface dialog) { + Button testButton = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_NEUTRAL); + testButton.setOnClickListener(view -> test()); + + Button button = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE); + button.setOnClickListener(view -> apply()); + } + }); + + if (Helper.preventScreenshot()) { + editDialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE); + } + + etNodePass.getEditText().setOnEditorActionListener((v, actionId, event) -> { + if (actionId == EditorInfo.IME_ACTION_DONE) { + editDialog.getButton(DialogInterface.BUTTON_NEUTRAL).requestFocus(); + test(); + return true; + } + return false; + }); + } + + private class AsyncTestNode extends AsyncTask { + @Override + protected void onPreExecute() { + super.onPreExecute(); + tvResult.setText(getString(R.string.node_testing, nodeInfo.getHostAddress())); + } + + @Override + protected Boolean doInBackground(Void... params) { + nodeInfo.testRpcService(); + return true; + } + + @Override + protected void onPostExecute(Boolean result) { + if (editDialog != null) { + showTestResult(); + } + if (shutdown) { + if (nodeBackup == null) { + nodesAdapter.addNode(nodeInfo); + } else { + nodesAdapter.setNodes(); + } + nodesAdapter.notifyItemChanged(nodeInfo); + } + } + } + } + + void restoreDefaultNodes() { + if (WalletManager.getInstance().getNetworkType() == NetworkType.NetworkType_Mainnet) { + if (!refresh(AsyncFindNodes.RESTORE_DEFAULTS)) { + Toast.makeText(getActivity(), getString(R.string.toast_default_nodes), Toast.LENGTH_SHORT).show(); + } + } else { + Toast.makeText(getActivity(), getString(R.string.node_wrong_net), Toast.LENGTH_LONG).show(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/OnBackPressedListener.java b/app/src/main/java/com/m2049r/xmrwallet/OnBackPressedListener.java new file mode 100644 index 0000000..eb09125 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/OnBackPressedListener.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet; + +public interface OnBackPressedListener { + boolean onBackPressed(); +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/OnBlockUpdateListener.java b/app/src/main/java/com/m2049r/xmrwallet/OnBlockUpdateListener.java new file mode 100644 index 0000000..242bea0 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/OnBlockUpdateListener.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2021 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet; + +import com.m2049r.xmrwallet.model.Wallet; + +public interface OnBlockUpdateListener { + void onBlockUpdate(final Wallet wallet); +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/OnUriScannedListener.java b/app/src/main/java/com/m2049r/xmrwallet/OnUriScannedListener.java new file mode 100644 index 0000000..34fa1c5 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/OnUriScannedListener.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet; + +import com.m2049r.xmrwallet.data.BarcodeData; + +public interface OnUriScannedListener { + boolean onUriScanned(BarcodeData barcodeData); +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java b/app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java new file mode 100644 index 0000000..06621ff --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java @@ -0,0 +1,469 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet; + +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.BitmapDrawable; +import android.net.Uri; +import android.nfc.NfcManager; +import android.os.Bundle; +import android.text.Editable; +import android.text.Html; +import android.text.InputType; +import android.text.Spanned; +import android.text.TextWatcher; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.FileProvider; +import androidx.fragment.app.Fragment; + +import com.google.android.material.textfield.TextInputLayout; +import com.google.android.material.transition.MaterialContainerTransform; +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.qrcode.QRCodeWriter; +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; +import com.m2049r.xmrwallet.data.BarcodeData; +import com.m2049r.xmrwallet.data.Crypto; +import com.m2049r.xmrwallet.data.Subaddress; +import com.m2049r.xmrwallet.model.Wallet; +import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.ThemeHelper; +import com.m2049r.xmrwallet.widget.ExchangeView; +import com.m2049r.xmrwallet.widget.Toolbar; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import timber.log.Timber; + +public class ReceiveFragment extends Fragment { + + private ProgressBar pbProgress; + private TextView tvAddress; + private TextInputLayout etNotes; + private ExchangeView evAmount; + private TextView tvQrCode; + private ImageView ivQrCode; + private ImageView ivQrCodeFull; + private EditText etDummy; + private ImageButton bCopyAddress; + private MenuItem shareItem; + + private Wallet wallet = null; + private boolean isMyWallet = false; + + public interface Listener { + void setToolbarButton(int type); + + void setTitle(String title); + + void setSubtitle(String subtitle); + + void showSubaddresses(boolean managerMode); + + Subaddress getSelectedSubaddress(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + View view = inflater.inflate(R.layout.fragment_receive, container, false); + + pbProgress = view.findViewById(R.id.pbProgress); + tvAddress = view.findViewById(R.id.tvAddress); + etNotes = view.findViewById(R.id.etNotes); + evAmount = view.findViewById(R.id.evAmount); + ivQrCode = view.findViewById(R.id.qrCode); + tvQrCode = view.findViewById(R.id.tvQrCode); + ivQrCodeFull = view.findViewById(R.id.qrCodeFull); + etDummy = view.findViewById(R.id.etDummy); + bCopyAddress = view.findViewById(R.id.bCopyAddress); + + etDummy.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + + bCopyAddress.setOnClickListener(v -> copyAddress()); + + evAmount.setOnNewAmountListener(xmr -> { + Timber.d("new amount = %s", xmr); + generateQr(); + if (shareRequested && (xmr != null)) share(); + }); + + evAmount.setOnFailedExchangeListener(() -> { + if (isAdded()) { + clearQR(); + Toast.makeText(getActivity(), getString(R.string.message_exchange_failed), Toast.LENGTH_LONG).show(); + } + }); + + final EditText notesEdit = etNotes.getEditText(); + notesEdit.setRawInputType(InputType.TYPE_CLASS_TEXT); + notesEdit.setOnEditorActionListener((v, actionId, event) -> { + if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN)) + || (actionId == EditorInfo.IME_ACTION_DONE)) { + generateQr(); + return true; + } + return false; + }); + notesEdit.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + clearQR(); + } + + @Override + public void afterTextChanged(Editable s) { + } + }); + + tvAddress.setOnClickListener(v -> { + listenerCallback.showSubaddresses(false); + }); + + view.findViewById(R.id.cvQrCode).setOnClickListener(v -> { + Helper.hideKeyboard(getActivity()); + etDummy.requestFocus(); + if (qrValid) { + ivQrCodeFull.setImageBitmap(((BitmapDrawable) ivQrCode.getDrawable()).getBitmap()); + ivQrCodeFull.setVisibility(View.VISIBLE); + } else { + evAmount.doExchange(); + } + }); + + ivQrCodeFull.setOnClickListener(v -> { + ivQrCodeFull.setImageBitmap(null); + ivQrCodeFull.setVisibility(View.GONE); + }); + + showProgress(); + clearQR(); + + if (getActivity() instanceof GenerateReviewFragment.ListenerWithWallet) { + wallet = ((GenerateReviewFragment.ListenerWithWallet) getActivity()).getWallet(); + show(); + } else { + throw new IllegalStateException("no wallet info"); + } + + View tvNfc = view.findViewById(R.id.tvNfc); + NfcManager manager = (NfcManager) getContext().getSystemService(Context.NFC_SERVICE); + if ((manager != null) && (manager.getDefaultAdapter() != null)) + tvNfc.setVisibility(View.VISIBLE); + + return view; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + final MaterialContainerTransform transform = new MaterialContainerTransform(); + transform.setDrawingViewId(R.id.fragment_container); + transform.setDuration(getResources().getInteger(R.integer.tx_item_transition_duration)); + transform.setAllContainerColors(ThemeHelper.getThemedColor(getContext(), android.R.attr.colorBackground)); + setSharedElementEnterTransition(transform); + } + + private boolean shareRequested = false; + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, final MenuInflater inflater) { + inflater.inflate(R.menu.receive_menu, menu); + super.onCreateOptionsMenu(menu, inflater); + + shareItem = menu.findItem(R.id.menu_item_share); + shareItem.setOnMenuItemClickListener(item -> { + if (shareRequested) return true; + shareRequested = true; + if (!qrValid) { + evAmount.doExchange(); + } else { + share(); + } + return true; + }); + } + + private void share() { + shareRequested = false; + if (saveQrCode()) { + final Intent sendIntent = getSendIntent(); + if (sendIntent != null) + startActivity(Intent.createChooser(sendIntent, null)); + } else { + Toast.makeText(getActivity(), getString(R.string.message_qr_failed), Toast.LENGTH_SHORT).show(); + } + } + + private boolean saveQrCode() { + if (!qrValid) throw new IllegalStateException("trying to save null qr code!"); + + File cachePath = new File(getActivity().getCacheDir(), "images"); + if (!cachePath.exists()) + if (!cachePath.mkdirs()) throw new IllegalStateException("cannot create images folder"); + File png = new File(cachePath, "QR.png"); + try { + FileOutputStream stream = new FileOutputStream(png); + Bitmap qrBitmap = ((BitmapDrawable) ivQrCode.getDrawable()).getBitmap(); + qrBitmap.compress(Bitmap.CompressFormat.PNG, 100, stream); + stream.close(); + return true; + } catch (IOException ex) { + Timber.e(ex); + // make sure we don't share an old qr code + if (!png.delete()) throw new IllegalStateException("cannot delete old qr code"); + // if we manage to delete it, the URI points to nothing and the user gets a toast with the error + } + return false; + } + + private Intent getSendIntent() { + File imagePath = new File(requireActivity().getCacheDir(), "images"); + File png = new File(imagePath, "QR.png"); + Uri contentUri = FileProvider.getUriForFile(requireActivity(), BuildConfig.APPLICATION_ID + ".fileprovider", png); + if (contentUri != null) { + Intent shareIntent = new Intent(); + shareIntent.setAction(Intent.ACTION_SEND); + shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // temp permission for receiving app to read this file + shareIntent.setTypeAndNormalize("image/png"); + shareIntent.putExtra(Intent.EXTRA_STREAM, contentUri); + if (bcData != null) + shareIntent.putExtra(Intent.EXTRA_TEXT, bcData.getUriString()); + return shareIntent; + } + return null; + } + + void copyAddress() { + Helper.clipBoardCopy(requireActivity(), getString(R.string.label_copy_address), subaddress.getAddress()); + Toast.makeText(getActivity(), getString(R.string.message_copy_address), Toast.LENGTH_SHORT).show(); + } + + private boolean qrValid = false; + + void clearQR() { + if (qrValid) { + ivQrCode.setImageBitmap(null); + qrValid = false; + if (isLoaded) + tvQrCode.setVisibility(View.VISIBLE); + } + } + + void setQR(Bitmap qr) { + ivQrCode.setImageBitmap(qr); + qrValid = true; + tvQrCode.setVisibility(View.GONE); + } + + @Override + public void onResume() { + super.onResume(); + Timber.d("onResume()"); + listenerCallback.setToolbarButton(Toolbar.BUTTON_BACK); + if (wallet != null) { + listenerCallback.setTitle(wallet.getName()); + listenerCallback.setSubtitle(wallet.getAccountLabel()); + setNewSubaddress(); + } else { + listenerCallback.setSubtitle(getString(R.string.status_wallet_loading)); + clearQR(); + } + } + + private boolean isLoaded = false; + + private void show() { + Timber.d("name=%s", wallet.getName()); + isLoaded = true; + hideProgress(); + } + + public BarcodeData getBarcodeData() { + if (qrValid) + return bcData; + else + return null; + } + + private BarcodeData bcData = null; + + private void generateQr() { + Timber.d("GENQR"); + String address = subaddress.getAddress(); + String notes = etNotes.getEditText().getText().toString(); + String xmrAmount = evAmount.getAmount(); + Timber.d("%s/%s/%s", xmrAmount, notes, address); + if ((xmrAmount == null) || !Wallet.isAddressValid(address)) { + clearQR(); + Timber.d("CLEARQR"); + return; + } + bcData = new BarcodeData(Crypto.XMR, address, notes, xmrAmount); + int size = Math.max(ivQrCode.getWidth(), ivQrCode.getHeight()); + Bitmap qr = generate(bcData.getUriString(), size, size); + if (qr != null) { + setQR(qr); + Timber.d("SETQR"); + etDummy.requestFocus(); + Helper.hideKeyboard(getActivity()); + } + } + + public Bitmap generate(String text, int width, int height) { + if ((width <= 0) || (height <= 0)) return null; + Map hints = new HashMap<>(); + hints.put(EncodeHintType.CHARACTER_SET, "utf-8"); + hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M); + try { + BitMatrix bitMatrix = new QRCodeWriter().encode(text, BarcodeFormat.QR_CODE, width, height, hints); + int[] pixels = new int[width * height]; + for (int i = 0; i < height; i++) { + for (int j = 0; j < width; j++) { + if (bitMatrix.get(j, i)) { + pixels[i * width + j] = 0x00000000; + } else { + pixels[i * height + j] = 0xffffffff; + } + } + } + Bitmap bitmap = Bitmap.createBitmap(pixels, 0, width, width, height, Bitmap.Config.RGB_565); + bitmap = addLogo(bitmap); + return bitmap; + } catch (WriterException ex) { + Timber.e(ex); + } + return null; + } + + private Bitmap addLogo(Bitmap qrBitmap) { + // addume logo & qrcode are both square + Bitmap logo = getMoneroLogo(); + final int qrSize = qrBitmap.getWidth(); + final int logoSize = logo.getWidth(); + + Bitmap logoBitmap = Bitmap.createBitmap(qrSize, qrSize, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(logoBitmap); + canvas.drawBitmap(qrBitmap, 0, 0, null); + canvas.save(); + final float sx = 0.2f * qrSize / logoSize; + canvas.scale(sx, sx, qrSize / 2f, qrSize / 2f); + canvas.drawBitmap(logo, (qrSize - logoSize) / 2f, (qrSize - logoSize) / 2f, null); + canvas.restore(); + return logoBitmap; + } + + private Bitmap logo = null; + + private Bitmap getMoneroLogo() { + if (logo == null) { + logo = Helper.getBitmap(getContext(), R.drawable.ic_monero_logo_b); + } + return logo; + } + + public void showProgress() { + pbProgress.setVisibility(View.VISIBLE); + } + + public void hideProgress() { + pbProgress.setVisibility(View.GONE); + } + + Listener listenerCallback = null; + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + if (context instanceof Listener) { + this.listenerCallback = (Listener) context; + } else { + throw new ClassCastException(context.toString() + + " must implement Listener"); + } + } + + @Override + public void onPause() { + Timber.d("onPause()"); + Helper.hideKeyboard(getActivity()); + super.onPause(); + } + + @Override + public void onDetach() { + Timber.d("onDetach()"); + if ((wallet != null) && (isMyWallet)) { + wallet.close(); + wallet = null; + isMyWallet = false; + } + super.onDetach(); + } + + private Subaddress subaddress = null; + + void setNewSubaddress() { + final Subaddress newSubaddress = listenerCallback.getSelectedSubaddress(); + if (!Objects.equals(subaddress, newSubaddress)) { + final Runnable resetSize = () -> tvAddress.animate().setDuration(125).scaleX(1).scaleY(1).start(); + tvAddress.animate().alpha(1).setDuration(125) + .scaleX(1.2f).scaleY(1.2f) + .withEndAction(resetSize).start(); + } + subaddress = newSubaddress; + final Context context = getContext(); + Spanned label = Html.fromHtml(context.getString(R.string.receive_subaddress, + Integer.toHexString(ThemeHelper.getThemedColor(context, R.attr.positiveColor) & 0xFFFFFF), + Integer.toHexString(ThemeHelper.getThemedColor(context, android.R.attr.colorBackground) & 0xFFFFFF), + subaddress.getDisplayLabel(), subaddress.getAddress())); + tvAddress.setText(label); + generateQr(); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/ScannerFragment.java b/app/src/main/java/com/m2049r/xmrwallet/ScannerFragment.java new file mode 100644 index 0000000..32512fd --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/ScannerFragment.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2017 dm77, m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet; + +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import androidx.fragment.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.Result; + +import me.dm7.barcodescanner.zxing.ZXingScannerView; +import timber.log.Timber; + +public class ScannerFragment extends Fragment implements ZXingScannerView.ResultHandler { + + private OnScannedListener onScannedListener; + + public interface OnScannedListener { + boolean onScanned(String qrCode); + } + + private ZXingScannerView mScannerView; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + Timber.d("onCreateView"); + mScannerView = new ZXingScannerView(getActivity()); + return mScannerView; + } + + @Override + public void onResume() { + super.onResume(); + Timber.d("onResume"); + mScannerView.setResultHandler(this); + mScannerView.startCamera(); + } + + @Override + public void handleResult(Result rawResult) { + if ((rawResult.getBarcodeFormat() == BarcodeFormat.QR_CODE)) { + if (onScannedListener.onScanned(rawResult.getText())) { + return; + } else { + Toast.makeText(getActivity(), getString(R.string.send_qr_address_invalid), Toast.LENGTH_SHORT).show(); + } + } else { + Toast.makeText(getActivity(), getString(R.string.send_qr_invalid), Toast.LENGTH_SHORT).show(); + } + + // Note from dm77: + // * Wait 2 seconds to resume the preview. + // * On older devices continuously stopping and resuming camera preview can result in freezing the app. + // * I don't know why this is the case but I don't have the time to figure out. + Handler handler = new Handler(); + handler.postDelayed(() -> mScannerView.resumeCameraPreview(ScannerFragment.this), 2000); + } + + @Override + public void onPause() { + Timber.d("onPause"); + mScannerView.stopCamera(); + super.onPause(); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (context instanceof OnScannedListener) { + this.onScannedListener = (OnScannedListener) context; + } else { + throw new ClassCastException(context.toString() + + " must implement Listener"); + } + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/SecureActivity.java b/app/src/main/java/com/m2049r/xmrwallet/SecureActivity.java new file mode 100644 index 0000000..238aeea --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/SecureActivity.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet; + +import android.content.Context; +import android.content.res.Configuration; +import android.os.Build; +import android.os.Bundle; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; + +import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.LocaleHelper; + +import java.util.Locale; + +import static android.view.WindowManager.LayoutParams; + +public abstract class SecureActivity extends AppCompatActivity { + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (Helper.preventScreenshot()) { + getWindow().setFlags(LayoutParams.FLAG_SECURE, LayoutParams.FLAG_SECURE); + } + } + + @Override + protected void attachBaseContext(Context newBase) { + super.attachBaseContext(newBase); + applyOverrideConfiguration(new Configuration()); + } + + @Override + public void applyOverrideConfiguration(Configuration newConfig) { + super.applyOverrideConfiguration(updateConfigurationIfSupported(newConfig)); + } + + private Configuration updateConfigurationIfSupported(Configuration config) { + // Configuration.getLocales is added after 24 and Configuration.locale is deprecated in 24 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (!config.getLocales().isEmpty()) { + return config; + } + } else { + if (config.locale != null) { + return config; + } + } + + Locale locale = LocaleHelper.getPreferredLocale(this); + if (locale != null) { + config.setLocale(locale); + } + return config; + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/SettingsFragment.java b/app/src/main/java/com/m2049r/xmrwallet/SettingsFragment.java new file mode 100644 index 0000000..6a4bb5a --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/SettingsFragment.java @@ -0,0 +1,125 @@ +package com.m2049r.xmrwallet; + +import android.app.Activity; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; + +import androidx.annotation.StyleRes; +import androidx.preference.ListPreference; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceManager; + +import com.m2049r.xmrwallet.dialog.AboutFragment; +import com.m2049r.xmrwallet.dialog.CreditsFragment; +import com.m2049r.xmrwallet.dialog.PrivacyFragment; +import com.m2049r.xmrwallet.util.DayNightMode; +import com.m2049r.xmrwallet.util.LocaleHelper; +import com.m2049r.xmrwallet.util.NightmodeHelper; +import com.m2049r.xmrwallet.util.ThemeHelper; +import com.m2049r.xmrwallet.widget.Toolbar; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Locale; + +import timber.log.Timber; + +public class SettingsFragment extends PreferenceFragmentCompat + implements SharedPreferences.OnSharedPreferenceChangeListener { + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + setPreferencesFromResource(R.xml.root_preferences, rootKey); + + findPreference(getString(R.string.about_info)).setOnPreferenceClickListener(preference -> { + AboutFragment.display(getParentFragmentManager()); + return true; + }); + findPreference(getString(R.string.privacy_info)).setOnPreferenceClickListener(preference -> { + PrivacyFragment.display(getParentFragmentManager()); + return true; + }); + findPreference(getString(R.string.credits_info)).setOnPreferenceClickListener(preference -> { + CreditsFragment.display(getParentFragmentManager()); + return true; + }); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (key.equals(getString(R.string.preferred_locale))) { + activity.recreate(); + } else if (key.equals(getString(R.string.preferred_nightmode))) { + NightmodeHelper.setNightMode(DayNightMode.valueOf(sharedPreferences.getString(key, "AUTO"))); + } else if (key.equals(getString(R.string.preferred_theme))) { + ThemeHelper.setTheme((Activity) activity, sharedPreferences.getString(key, "Classic")); + activity.recreate(); + } + } + + private SettingsFragment.Listener activity; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (context instanceof SettingsFragment.Listener) { + activity = (SettingsFragment.Listener) context; + } else { + throw new ClassCastException(context + " must implement Listener"); + } + } + + @Override + public void onResume() { + super.onResume(); + Timber.d("onResume()"); + activity.setSubtitle(getString(R.string.menu_settings)); + activity.setToolbarButton(Toolbar.BUTTON_BACK); + populateLanguages(); + PreferenceManager.getDefaultSharedPreferences(requireContext()) + .registerOnSharedPreferenceChangeListener(this); + } + + @Override + public void onPause() { + super.onPause(); + PreferenceManager.getDefaultSharedPreferences(requireContext()) + .unregisterOnSharedPreferenceChangeListener(this); + } + + public interface Listener { + void setToolbarButton(int type); + + void setSubtitle(String title); + + void recreate(); + + void setTheme(@StyleRes final int resId); + } + + public void populateLanguages() { + ListPreference language = findPreference(getString(R.string.preferred_locale)); + assert language != null; + + final ArrayList availableLocales = LocaleHelper.getAvailableLocales(requireContext()); + Collections.sort(availableLocales, (locale1, locale2) -> { + String localeString1 = LocaleHelper.getDisplayName(locale1, true); + String localeString2 = LocaleHelper.getDisplayName(locale2, true); + return localeString1.compareTo(localeString2); + }); + + String[] localeDisplayNames = new String[1 + availableLocales.size()]; + localeDisplayNames[0] = getString(R.string.language_system_default); + for (int i = 1; i < localeDisplayNames.length; i++) { + localeDisplayNames[i] = LocaleHelper.getDisplayName(availableLocales.get(i - 1), true); + } + language.setEntries(localeDisplayNames); + + String[] languageTags = new String[1 + availableLocales.size()]; + languageTags[0] = ""; + for (int i = 1; i < languageTags.length; i++) { + languageTags[i] = availableLocales.get(i - 1).toLanguageTag(); + } + language.setEntryValues(languageTags); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/SubaddressFragment.java b/app/src/main/java/com/m2049r/xmrwallet/SubaddressFragment.java new file mode 100644 index 0000000..c42ae7a --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/SubaddressFragment.java @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.AsyncTask; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.RecyclerView; + +import com.m2049r.xmrwallet.data.Subaddress; +import com.m2049r.xmrwallet.layout.SubaddressInfoAdapter; +import com.m2049r.xmrwallet.ledger.LedgerProgressDialog; +import com.m2049r.xmrwallet.model.TransactionInfo; +import com.m2049r.xmrwallet.model.Wallet; +import com.m2049r.xmrwallet.model.WalletManager; +import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.MoneroThreadPoolExecutor; +import com.m2049r.xmrwallet.widget.Toolbar; + +import java.util.ArrayList; +import java.util.List; + +import lombok.RequiredArgsConstructor; +import timber.log.Timber; + +public class SubaddressFragment extends Fragment implements SubaddressInfoAdapter.OnInteractionListener, + View.OnClickListener, OnBlockUpdateListener { + static public final String KEY_MODE = "mode"; + static public final String MODE_MANAGER = "manager"; + + private SubaddressInfoAdapter adapter; + + private Listener activityCallback; + + private Wallet wallet; + + // Container Activity must implement this interface + public interface Listener { + void onSubaddressSelected(Subaddress subaddress); + + void setSubtitle(String title); + + void setToolbarButton(int type); + + void showSubaddress(View view, final int subaddressIndex); + + void saveWallet(); + } + + public interface ProgressListener { + void showProgressDialog(int msgId); + + void showLedgerProgressDialog(int mode); + + void dismissProgressDialog(); + } + + private ProgressListener progressCallback = null; + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + if (context instanceof ProgressListener) { + progressCallback = (ProgressListener) context; + } + if (context instanceof Listener) { + activityCallback = (Listener) context; + } else { + throw new ClassCastException(context.toString() + + " must implement Listener"); + } + } + + @Override + public void onPause() { + Timber.d("onPause()"); + super.onPause(); + } + + @Override + public void onResume() { + super.onResume(); + activityCallback.setSubtitle(getString(R.string.subbaddress_title)); + activityCallback.setToolbarButton(Toolbar.BUTTON_BACK); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + Timber.d("onCreateView"); + + final Bundle b = getArguments(); + managerMode = ((b != null) && (MODE_MANAGER.equals(b.getString(KEY_MODE)))); + + View view = inflater.inflate(R.layout.fragment_subaddress, container, false); + view.findViewById(R.id.fab).setOnClickListener(this); + + if (managerMode) { + view.findViewById(R.id.tvInstruction).setVisibility(View.GONE); + view.findViewById(R.id.tvHint).setVisibility(View.GONE); + } + + final RecyclerView list = view.findViewById(R.id.list); + adapter = new SubaddressInfoAdapter(getActivity(), this); + list.setAdapter(adapter); + adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + list.scrollToPosition(positionStart); + } + }); + + Helper.hideKeyboard(getActivity()); + + wallet = WalletManager.getInstance().getWallet(); + + loadList(); + + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + } + + public void loadList() { + Timber.d("loadList()"); + final int numSubaddresses = wallet.getNumSubaddresses(); + final List list = new ArrayList<>(); + for (int i = 0; i < numSubaddresses; i++) { + list.add(wallet.getSubaddressObject(i)); + } + adapter.setInfos(list); + } + + @Override + public void onBlockUpdate(Wallet wallet) { + loadList(); + } + + @Override + public void onClick(View v) { + int id = v.getId(); + if (id == R.id.fab) { + getNewSubaddress(); + } + } + + private int lastUsedSubaddress() { + int lastUsedSubaddress = 0; + for (TransactionInfo info : wallet.getHistory().getAll()) { + if (info.addressIndex > lastUsedSubaddress) + lastUsedSubaddress = info.addressIndex; + } + return lastUsedSubaddress; + } + + private void getNewSubaddress() { + final int maxSubaddresses = lastUsedSubaddress() + wallet.getDeviceType().getSubaddressLookahead(); + if (wallet.getNumSubaddresses() < maxSubaddresses) + new AsyncSubaddress().executeOnExecutor(MoneroThreadPoolExecutor.MONERO_THREAD_POOL_EXECUTOR); + else + Toast.makeText(getActivity(), getString(R.string.max_subaddress_warning), Toast.LENGTH_LONG).show(); + } + + @SuppressLint("StaticFieldLeak") + @RequiredArgsConstructor + private class AsyncSubaddress extends AsyncTask { + boolean dialogOpened = false; + + @Override + protected void onPreExecute() { + super.onPreExecute(); + if ((wallet.getDeviceType() == Wallet.Device.Device_Ledger) && (progressCallback != null)) { + progressCallback.showLedgerProgressDialog(LedgerProgressDialog.TYPE_SUBADDRESS); + dialogOpened = true; + } + } + + @Override + protected Boolean doInBackground(Void... params) { + if (params.length != 0) return false; + wallet.getNewSubaddress(); + if (activityCallback != null) { + activityCallback.saveWallet(); + } + return true; + } + + @Override + protected void onPostExecute(Boolean result) { + super.onPostExecute(result); + if (dialogOpened) + progressCallback.dismissProgressDialog(); + if (!isAdded()) // never mind then + return; + loadList(); + } + } + + boolean managerMode = false; + + // Callbacks from SubaddressInfoAdapter + @Override + public void onInteraction(final View view, final Subaddress subaddress) { + if (managerMode) + activityCallback.showSubaddress(view, subaddress.getAddressIndex()); + else + activityCallback.onSubaddressSelected(subaddress); // also closes the fragment with onBackpressed() + } + + @Override + public boolean onLongInteraction(View view, Subaddress subaddress) { + activityCallback.showSubaddress(view, subaddress.getAddressIndex()); + return false; + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/SubaddressInfoFragment.java b/app/src/main/java/com/m2049r/xmrwallet/SubaddressInfoFragment.java new file mode 100644 index 0000000..3b0941a --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/SubaddressInfoFragment.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet; + +import android.content.Context; +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.RecyclerView; +import androidx.transition.Transition; +import androidx.transition.TransitionInflater; + +import com.google.android.material.textfield.TextInputLayout; +import com.m2049r.xmrwallet.data.Subaddress; +import com.m2049r.xmrwallet.layout.TransactionInfoAdapter; +import com.m2049r.xmrwallet.model.TransactionInfo; +import com.m2049r.xmrwallet.model.Wallet; +import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.widget.Toolbar; + +import java.util.ArrayList; +import java.util.List; + +import timber.log.Timber; + +public class SubaddressInfoFragment extends Fragment + implements TransactionInfoAdapter.OnInteractionListener, OnBlockUpdateListener { + private TransactionInfoAdapter adapter; + + private Subaddress subaddress; + + private TextInputLayout etName; + private TextView tvAddress; + private TextView tvTxLabel; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_subaddressinfo, container, false); + + etName = view.findViewById(R.id.etName); + tvAddress = view.findViewById(R.id.tvAddress); + tvTxLabel = view.findViewById(R.id.tvTxLabel); + + final RecyclerView list = view.findViewById(R.id.list); + adapter = new TransactionInfoAdapter(getActivity(), this); + list.setAdapter(adapter); + + final Wallet wallet = activityCallback.getWallet(); + + Bundle b = getArguments(); + final int subaddressIndex = b.getInt("subaddressIndex"); + subaddress = wallet.getSubaddressObject(subaddressIndex); + + etName.getEditText().setText(subaddress.getDisplayLabel()); + tvAddress.setText(getContext().getString(R.string.subbaddress_info_subtitle, + subaddress.getAddressIndex(), subaddress.getSquashedAddress())); + + etName.getEditText().setOnFocusChangeListener((v, hasFocus) -> { + if (!hasFocus) { + wallet.setSubaddressLabel(subaddressIndex, etName.getEditText().getText().toString()); + } + }); + etName.getEditText().setOnEditorActionListener((v, actionId, event) -> { + if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN)) + || (actionId == EditorInfo.IME_ACTION_DONE)) { + Helper.hideKeyboard(getActivity()); + wallet.setSubaddressLabel(subaddressIndex, etName.getEditText().getText().toString()); + onRefreshed(wallet); + return true; + } + return false; + }); + + onRefreshed(wallet); + + return view; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Transition transform = TransitionInflater.from(requireContext()) + .inflateTransition(R.transition.details); + setSharedElementEnterTransition(transform); + } + + public void onRefreshed(final Wallet wallet) { + Timber.d("onRefreshed"); + List list = new ArrayList<>(); + for (TransactionInfo info : wallet.getHistory().getAll()) { + if (info.addressIndex == subaddress.getAddressIndex()) + list.add(info); + } + adapter.setInfos(list); + if (list.isEmpty()) + tvTxLabel.setText(R.string.subaddress_notx_label); + else + tvTxLabel.setText(R.string.subaddress_tx_label); + } + + @Override + public void onBlockUpdate(Wallet wallet) { + onRefreshed(wallet); + } + + // Callbacks from TransactionInfoAdapter + @Override + public void onInteraction(final View view, final TransactionInfo infoItem) { + activityCallback.onTxDetailsRequest(view, infoItem); + } + + Listener activityCallback; + + // Container Activity must implement this interface + public interface Listener { + void onTxDetailsRequest(View view, TransactionInfo info); + + Wallet getWallet(); + + void setToolbarButton(int type); + + void setTitle(String title, String subtitle); + + void setSubtitle(String subtitle); + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + if (context instanceof Listener) { + this.activityCallback = (Listener) context; + } else { + throw new ClassCastException(context.toString() + + " must implement Listener"); + } + } + + @Override + public void onResume() { + super.onResume(); + Timber.d("onResume()"); + activityCallback.setSubtitle(getString(R.string.subbaddress_title)); + activityCallback.setToolbarButton(Toolbar.BUTTON_BACK); + } + + @Override + public void onPause() { + super.onPause(); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/TxFragment.java b/app/src/main/java/com/m2049r/xmrwallet/TxFragment.java new file mode 100644 index 0000000..82be82b --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/TxFragment.java @@ -0,0 +1,406 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.Intent; +import android.graphics.Paint; +import android.net.Uri; +import android.os.Bundle; +import android.text.Html; +import android.text.InputType; +import android.text.Spanned; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.transition.Transition; +import androidx.transition.TransitionInflater; + +import com.m2049r.xmrwallet.data.Subaddress; +import com.m2049r.xmrwallet.data.UserNotes; +import com.m2049r.xmrwallet.model.TransactionInfo; +import com.m2049r.xmrwallet.model.Transfer; +import com.m2049r.xmrwallet.model.Wallet; +import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.ThemeHelper; +import com.m2049r.xmrwallet.widget.Toolbar; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.HashSet; +import java.util.Set; +import java.util.TimeZone; + +import timber.log.Timber; + +public class TxFragment extends Fragment { + + static public final String ARG_INFO = "info"; + + private final SimpleDateFormat TS_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z"); + + public TxFragment() { + super(); + Calendar cal = Calendar.getInstance(); + TimeZone tz = cal.getTimeZone(); //get the local time zone. + TS_FORMATTER.setTimeZone(tz); + } + + private TextView tvAccount; + private TextView tvAddress; + private TextView tvTxTimestamp; + private TextView tvTxId; + private TextView tvTxKey; + private TextView tvDestination; + private TextView tvTxPaymentId; + private TextView tvTxBlockheight; + private TextView tvTxAmount; + private TextView tvTxFee; + private TextView tvTxTransfers; + private TextView etTxNotes; + + // XMRTO stuff + private View cvXmrTo; + private TextView tvTxXmrToKey; + private TextView tvDestinationBtc; + private TextView tvTxAmountBtc; + private TextView tvXmrToSupport; + private TextView tvXmrToKeyLabel; + private ImageView tvXmrToLogo; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_tx_info, container, false); + + cvXmrTo = view.findViewById(R.id.cvXmrTo); + tvTxXmrToKey = view.findViewById(R.id.tvTxXmrToKey); + tvDestinationBtc = view.findViewById(R.id.tvDestinationBtc); + tvTxAmountBtc = view.findViewById(R.id.tvTxAmountBtc); + tvXmrToSupport = view.findViewById(R.id.tvXmrToSupport); + tvXmrToKeyLabel = view.findViewById(R.id.tvXmrToKeyLabel); + tvXmrToLogo = view.findViewById(R.id.tvXmrToLogo); + + tvAccount = view.findViewById(R.id.tvAccount); + tvAddress = view.findViewById(R.id.tvAddress); + tvTxTimestamp = view.findViewById(R.id.tvTxTimestamp); + tvTxId = view.findViewById(R.id.tvTxId); + tvTxKey = view.findViewById(R.id.tvTxKey); + tvDestination = view.findViewById(R.id.tvDestination); + tvTxPaymentId = view.findViewById(R.id.tvTxPaymentId); + tvTxBlockheight = view.findViewById(R.id.tvTxBlockheight); + tvTxAmount = view.findViewById(R.id.tvTxAmount); + tvTxFee = view.findViewById(R.id.tvTxFee); + tvTxTransfers = view.findViewById(R.id.tvTxTransfers); + etTxNotes = view.findViewById(R.id.etTxNotes); + + etTxNotes.setRawInputType(InputType.TYPE_CLASS_TEXT); + + tvTxXmrToKey.setOnClickListener(v -> { + Helper.clipBoardCopy(getActivity(), getString(R.string.label_copy_xmrtokey), tvTxXmrToKey.getText().toString()); + Toast.makeText(getActivity(), getString(R.string.message_copy_xmrtokey), Toast.LENGTH_SHORT).show(); + }); + + info = getArguments().getParcelable(ARG_INFO); + show(); + return view; + } + + void shareTxInfo() { + if (this.info == null) return; + StringBuffer sb = new StringBuffer(); + + sb.append(getString(R.string.tx_timestamp)).append(":\n"); + sb.append(TS_FORMATTER.format(new Date(info.timestamp * 1000))).append("\n\n"); + + sb.append(getString(R.string.tx_amount)).append(":\n"); + sb.append((info.direction == TransactionInfo.Direction.Direction_In ? "+" : "-")); + sb.append(Wallet.getDisplayAmount(info.amount)).append("\n"); + sb.append(getString(R.string.tx_fee)).append(":\n"); + sb.append(Wallet.getDisplayAmount(info.fee)).append("\n\n"); + + sb.append(getString(R.string.tx_notes)).append(":\n"); + String oneLineNotes = info.notes.replace("\n", " ; "); + sb.append(oneLineNotes.isEmpty() ? "-" : oneLineNotes).append("\n\n"); + + sb.append(getString(R.string.tx_destination)).append(":\n"); + sb.append(tvDestination.getText()).append("\n\n"); + + sb.append(getString(R.string.tx_paymentId)).append(":\n"); + sb.append(info.paymentId).append("\n\n"); + + sb.append(getString(R.string.tx_id)).append(":\n"); + sb.append(info.hash).append("\n"); + sb.append(getString(R.string.tx_key)).append(":\n"); + sb.append(info.txKey.isEmpty() ? "-" : info.txKey).append("\n\n"); + + sb.append(getString(R.string.tx_blockheight)).append(":\n"); + if (info.isFailed) { + sb.append(getString(R.string.tx_failed)).append("\n"); + } else if (info.isPending) { + sb.append(getString(R.string.tx_pending)).append("\n"); + } else { + sb.append(info.blockheight).append("\n"); + } + sb.append("\n"); + + sb.append(getString(R.string.tx_transfers)).append(":\n"); + if (info.transfers != null) { + boolean comma = false; + for (Transfer transfer : info.transfers) { + if (comma) { + sb.append(", "); + } else { + comma = true; + } + sb.append(transfer.address).append(": "); + sb.append(Wallet.getDisplayAmount(transfer.amount)); + } + } else { + sb.append("-"); + } + sb.append("\n\n"); + + Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_TEXT, sb.toString()); + sendIntent.setType("text/plain"); + startActivity(Intent.createChooser(sendIntent, null)); + } + + TransactionInfo info = null; + UserNotes userNotes = null; + + void loadNotes() { + if ((userNotes == null) || (info.notes == null)) { + info.notes = activityCallback.getTxNotes(info.hash); + } + userNotes = new UserNotes(info.notes); + etTxNotes.setText(userNotes.note); + } + + private void setTxColour(int clr) { + tvTxAmount.setTextColor(clr); + tvTxFee.setTextColor(clr); + } + + private void showSubaddressLabel() { + final Subaddress subaddress = activityCallback.getWalletSubaddress(info.accountIndex, info.addressIndex); + final Context ctx = getContext(); + Spanned label = Html.fromHtml(ctx.getString(R.string.tx_account_formatted, + info.accountIndex, info.addressIndex, + Integer.toHexString(ThemeHelper.getThemedColor(ctx, R.attr.positiveColor) & 0xFFFFFF), + Integer.toHexString(ThemeHelper.getThemedColor(ctx, android.R.attr.colorBackground) & 0xFFFFFF), + subaddress.getDisplayLabel())); + tvAccount.setText(label); + tvAccount.setOnClickListener(v -> activityCallback.showSubaddress(v, info.addressIndex)); + } + + private void show() { + if (info.txKey == null) { + info.txKey = activityCallback.getTxKey(info.hash); + } + if (info.address == null) { + info.address = activityCallback.getTxAddress(info.accountIndex, info.addressIndex); + } + loadNotes(); + + showSubaddressLabel(); + tvAddress.setText(info.address); + + tvTxTimestamp.setText(TS_FORMATTER.format(new Date(info.timestamp * 1000))); + tvTxId.setText(info.hash); + tvTxKey.setText(info.txKey.isEmpty() ? "-" : info.txKey); + tvTxPaymentId.setText(info.paymentId); + if (info.isFailed) { + tvTxBlockheight.setText(getString(R.string.tx_failed)); + } else if (info.isPending) { + tvTxBlockheight.setText(getString(R.string.tx_pending)); + } else { + tvTxBlockheight.setText("" + info.blockheight); + } + String sign = (info.direction == TransactionInfo.Direction.Direction_In ? "+" : "-"); + + long realAmount = info.amount; + tvTxAmount.setText(sign + Wallet.getDisplayAmount(realAmount)); + + if ((info.fee > 0)) { + String fee = Wallet.getDisplayAmount(info.fee); + tvTxFee.setText(getString(R.string.tx_list_fee, fee)); + } else { + tvTxFee.setText(null); + tvTxFee.setVisibility(View.GONE); + } + + if (info.isFailed) { + tvTxAmount.setText(getString(R.string.tx_list_amount_failed, Wallet.getDisplayAmount(info.amount))); + tvTxFee.setText(getString(R.string.tx_list_failed_text)); + setTxColour(ThemeHelper.getThemedColor(getContext(), R.attr.neutralColor)); + } else if (info.isPending) { + setTxColour(ThemeHelper.getThemedColor(getContext(), R.attr.neutralColor)); + } else if (info.direction == TransactionInfo.Direction.Direction_In) { + setTxColour(ThemeHelper.getThemedColor(getContext(), R.attr.positiveColor)); + } else { + setTxColour(ThemeHelper.getThemedColor(getContext(), R.attr.negativeColor)); + } + Set destinations = new HashSet<>(); + StringBuilder sb = new StringBuilder(); + StringBuilder dstSb = new StringBuilder(); + if (info.transfers != null) { + boolean newline = false; + for (Transfer transfer : info.transfers) { + destinations.add(transfer.address); + if (newline) { + sb.append("\n"); + } else { + newline = true; + } + sb.append("[").append(transfer.address.substring(0, 6)).append("] "); + sb.append(Wallet.getDisplayAmount(transfer.amount)); + } + newline = false; + for (String dst : destinations) { + if (newline) { + dstSb.append("\n"); + } else { + newline = true; + } + dstSb.append(dst); + } + } else { + sb.append("-"); + dstSb.append(info.direction == TransactionInfo.Direction.Direction_In ? + activityCallback.getWalletSubaddress(info.accountIndex, info.addressIndex).getAddress() : + "-"); + } + tvTxTransfers.setText(sb.toString()); + tvDestination.setText(dstSb.toString()); + showBtcInfo(); + } + + @SuppressLint("SetTextI18n") + void showBtcInfo() { + if (userNotes.xmrtoKey != null) { + cvXmrTo.setVisibility(View.VISIBLE); + String key = userNotes.xmrtoKey; + if ("xmrto".equals(userNotes.xmrtoTag)) { // legacy xmr.to service :( + key = "xmrto-" + key; + } + tvTxXmrToKey.setText(key); + tvDestinationBtc.setText(userNotes.xmrtoDestination); + tvTxAmountBtc.setText(userNotes.xmrtoAmount + " " + userNotes.xmrtoCurrency); + switch (userNotes.xmrtoTag) { + case "xmrto": + tvXmrToSupport.setVisibility(View.GONE); + tvXmrToKeyLabel.setVisibility(View.INVISIBLE); + tvXmrToLogo.setImageResource(R.drawable.ic_xmrto_logo); + break; + case "side": // defaults in layout - just add underline + tvXmrToSupport.setPaintFlags(tvXmrToSupport.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG); + tvXmrToSupport.setOnClickListener(v -> { + Uri uri = Uri.parse("https://sideshift.ai/orders/" + userNotes.xmrtoKey); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + startActivity(intent); + }); + break; + default: + tvXmrToSupport.setVisibility(View.GONE); + tvXmrToKeyLabel.setVisibility(View.INVISIBLE); + tvXmrToLogo.setVisibility(View.GONE); + } + } else { + cvXmrTo.setVisibility(View.GONE); + } + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + Transition transform = TransitionInflater.from(requireContext()) + .inflateTransition(R.transition.details); + setSharedElementEnterTransition(transform); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.tx_info_menu, menu); + super.onCreateOptionsMenu(menu, inflater); + } + + Listener activityCallback; + + public interface Listener { + Subaddress getWalletSubaddress(int accountIndex, int subaddressIndex); + + String getTxKey(String hash); + + String getTxNotes(String hash); + + boolean setTxNotes(String txId, String txNotes); + + String getTxAddress(int major, int minor); + + void setToolbarButton(int type); + + void setSubtitle(String subtitle); + + void showSubaddress(View view, final int subaddressIndex); + + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (context instanceof TxFragment.Listener) { + this.activityCallback = (TxFragment.Listener) context; + } else { + throw new ClassCastException(context.toString() + + " must implement Listener"); + } + } + + @Override + public void onPause() { + if (!etTxNotes.getText().toString().equals(userNotes.note)) { // notes have changed + // save them + userNotes.setNote(etTxNotes.getText().toString()); + info.notes = userNotes.txNotes; + activityCallback.setTxNotes(info.hash, info.notes); + } + Helper.hideKeyboard(getActivity()); + super.onPause(); + } + + @Override + public void onResume() { + super.onResume(); + Timber.d("onResume()"); + activityCallback.setSubtitle(getString(R.string.tx_title)); + activityCallback.setToolbarButton(Toolbar.BUTTON_BACK); + showSubaddressLabel(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java b/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java new file mode 100644 index 0000000..5ab6ad7 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java @@ -0,0 +1,1220 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet; + +import android.annotation.SuppressLint; +import android.content.ComponentName; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.IBinder; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBarDrawerToggle; +import androidx.appcompat.app.AlertDialog; +import androidx.core.view.GravityCompat; +import androidx.drawerlayout.widget.DrawerLayout; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.navigation.NavigationView; +import com.m2049r.xmrwallet.data.BarcodeData; +import com.m2049r.xmrwallet.data.Subaddress; +import com.m2049r.xmrwallet.data.TxData; +import com.m2049r.xmrwallet.data.UserNotes; +import com.m2049r.xmrwallet.dialog.CreditsFragment; +import com.m2049r.xmrwallet.dialog.HelpFragment; +import com.m2049r.xmrwallet.fragment.send.SendAddressWizardFragment; +import com.m2049r.xmrwallet.fragment.send.SendFragment; +import com.m2049r.xmrwallet.ledger.LedgerProgressDialog; +import com.m2049r.xmrwallet.model.PendingTransaction; +import com.m2049r.xmrwallet.model.TransactionInfo; +import com.m2049r.xmrwallet.model.Wallet; +import com.m2049r.xmrwallet.model.WalletManager; +import com.m2049r.xmrwallet.service.WalletService; +import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.MoneroThreadPoolExecutor; +import com.m2049r.xmrwallet.util.ThemeHelper; +import com.m2049r.xmrwallet.widget.Toolbar; + +import java.util.ArrayList; +import java.util.List; + +import timber.log.Timber; + +public class WalletActivity extends BaseActivity implements WalletFragment.Listener, + WalletService.Observer, SendFragment.Listener, TxFragment.Listener, + GenerateReviewFragment.ListenerWithWallet, + GenerateReviewFragment.Listener, + GenerateReviewFragment.PasswordChangedListener, + ScannerFragment.OnScannedListener, ReceiveFragment.Listener, + SendAddressWizardFragment.OnScanListener, + WalletFragment.DrawerLocker, + NavigationView.OnNavigationItemSelectedListener, + SubaddressFragment.Listener, + SubaddressInfoFragment.Listener { + + public static final String REQUEST_ID = "id"; + public static final String REQUEST_PW = "pw"; + public static final String REQUEST_FINGERPRINT_USED = "fingerprint"; + public static final String REQUEST_STREETMODE = "streetmode"; + public static final String REQUEST_URI = "uri"; + + private NavigationView accountsView; + private DrawerLayout drawer; + private ActionBarDrawerToggle drawerToggle; + + private Toolbar toolbar; + private boolean requestStreetMode = false; + + private String password; + + private String uri = null; + + private long streetMode = 0; + + @Override + public void onPasswordChanged(String newPassword) { + password = newPassword; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public void setToolbarButton(int type) { + toolbar.setButton(type); + } + + @Override + public void setTitle(String title, String subtitle) { + toolbar.setTitle(title, subtitle); + } + + @Override + public void setTitle(String title) { + Timber.d("setTitle:%s.", title); + toolbar.setTitle(title); + } + + @Override + public void setSubtitle(String subtitle) { + toolbar.setSubtitle(subtitle); + } + + private boolean synced = false; + + @Override + public boolean isSynced() { + return synced; + } + + private WalletFragment getWalletFragment() { + return (WalletFragment) getSupportFragmentManager().findFragmentByTag(WalletFragment.class.getName()); + } + + private Fragment getCurrentFragment() { + return getSupportFragmentManager().findFragmentById(R.id.fragment_container); + } + + @Override + public boolean isStreetMode() { + return streetMode > 0; + } + + private void enableStreetMode(boolean enable) { + if (enable) { + streetMode = getWallet().getDaemonBlockChainHeight(); + } else { + streetMode = 0; + } + final WalletFragment walletFragment = getWalletFragment(); + if (walletFragment != null) walletFragment.resetDismissedTransactions(); + forceUpdate(); + runOnUiThread(() -> { + if (getWallet() != null) + updateAccountsBalance(); + }); + } + + @Override + public long getStreetModeHeight() { + return streetMode; + } + + @Override + public boolean isWatchOnly() { + return getWallet().isWatchOnly(); + } + + @Override + public String getTxKey(String txId) { + return getWallet().getTxKey(txId); + } + + @Override + public String getTxNotes(String txId) { + return getWallet().getUserNote(txId); + } + + @Override + public boolean setTxNotes(String txId, String txNotes) { + return getWallet().setUserNote(txId, txNotes); + } + + @Override + public String getTxAddress(int major, int minor) { + return getWallet().getSubaddress(major, minor); + } + + @Override + protected void onStart() { + super.onStart(); + Timber.d("onStart()"); + } + + private void startWalletService() { + Bundle extras = getIntent().getExtras(); + if (extras != null) { + acquireWakeLock(); + String walletId = extras.getString(REQUEST_ID); + // we can set the streetmode height AFTER opening the wallet + requestStreetMode = extras.getBoolean(REQUEST_STREETMODE); + password = extras.getString(REQUEST_PW); + uri = extras.getString(REQUEST_URI); + connectWalletService(walletId, password); + } else { + finish(); + } + } + + private void stopWalletService() { + disconnectWalletService(); + releaseWakeLock(); + } + + private void onWalletRescan() { + try { + final WalletFragment walletFragment = getWalletFragment(); + getWallet().rescanBlockchainAsync(); + synced = false; + walletFragment.unsync(); + invalidateOptionsMenu(); + } catch (ClassCastException ex) { + Timber.d(ex.getLocalizedMessage()); + // keep calm and carry on + } + } + + @Override + protected void onStop() { + Timber.d("onStop()"); + super.onStop(); + } + + @Override + protected void onDestroy() { + Timber.d("onDestroy()"); + if ((mBoundService != null) && (getWallet() != null)) { + saveWallet(); + } + stopWalletService(); + if (drawer != null) drawer.removeDrawerListener(drawerToggle); + super.onDestroy(); + } + + @Override + public boolean hasWallet() { + return haveWallet; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + MenuItem renameItem = menu.findItem(R.id.action_rename); + if (renameItem != null) + renameItem.setEnabled(hasWallet() && getWallet().isSynchronized()); + MenuItem streetmodeItem = menu.findItem(R.id.action_streetmode); + if (streetmodeItem != null) + if (isStreetMode()) { + streetmodeItem.setIcon(R.drawable.gunther_csi_24dp); + } else { + streetmodeItem.setIcon(R.drawable.gunther_24dp); + } + final MenuItem rescanItem = menu.findItem(R.id.action_rescan); + if (rescanItem != null) + rescanItem.setEnabled(isSynced()); + return super.onPrepareOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + final int itemId = item.getItemId(); + if (itemId == R.id.action_rescan) { + onWalletRescan(); + } else if (itemId == R.id.action_info) { + onWalletDetails(); + } else if (itemId == R.id.action_credits) { + CreditsFragment.display(getSupportFragmentManager()); + } else if (itemId == R.id.action_share) { + onShareTxInfo(); + } else if (itemId == R.id.action_help_tx_info) { + HelpFragment.display(getSupportFragmentManager(), R.string.help_tx_details); + } else if (itemId == R.id.action_help_wallet) { + HelpFragment.display(getSupportFragmentManager(), R.string.help_wallet); + } else if (itemId == R.id.action_details_help) { + HelpFragment.display(getSupportFragmentManager(), R.string.help_details); + } else if (itemId == R.id.action_details_changepw) { + onWalletChangePassword(); + } else if (itemId == R.id.action_help_send) { + HelpFragment.display(getSupportFragmentManager(), R.string.help_send); + } else if (itemId == R.id.action_rename) { + onAccountRename(); + } else if (itemId == R.id.action_subaddresses) { + showSubaddresses(true); + } else if (itemId == R.id.action_streetmode) { + if (isStreetMode()) { // disable streetmode + onDisableStreetMode(); + } else { + onEnableStreetMode(); + } + } else + return super.onOptionsItemSelected(item); + return true; + } + + private void updateStreetMode() { + invalidateOptionsMenu(); + } + + private void onEnableStreetMode() { + enableStreetMode(true); + updateStreetMode(); + } + + private void onDisableStreetMode() { + Helper.promptPassword(WalletActivity.this, getWallet().getName(), false, new Helper.PasswordAction() { + @Override + public void act(String walletName, String password, boolean fingerprintUsed) { + runOnUiThread(() -> { + enableStreetMode(false); + updateStreetMode(); + }); + } + + @Override + public void fail(String walletName) { + } + }); + } + + + public void onWalletChangePassword() { + try { + GenerateReviewFragment detailsFragment = (GenerateReviewFragment) getCurrentFragment(); + 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()"); + ThemeHelper.setPreferred(this); + super.onCreate(savedInstanceState); + if (savedInstanceState != null) { + // activity restarted + // we don't want that - finish it and fall back to previous activity + finish(); + return; + } + + setContentView(R.layout.activity_wallet); + toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + getSupportActionBar().setDisplayShowTitleEnabled(false); + + toolbar.setOnButtonListener(new Toolbar.OnButtonListener() { + @Override + public void onButton(int type) { + switch (type) { + case Toolbar.BUTTON_BACK: + onDisposeRequest(); + onBackPressed(); + break; + case Toolbar.BUTTON_CANCEL: + onDisposeRequest(); + Helper.hideKeyboard(WalletActivity.this); + WalletActivity.super.onBackPressed(); + break; + case Toolbar.BUTTON_CLOSE: + finish(); + break; + case Toolbar.BUTTON_SETTINGS: + 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?"); + } + } + }); + + drawer = findViewById(R.id.drawer_layout); + drawerToggle = new ActionBarDrawerToggle(this, drawer, toolbar, 0, 0); + drawer.addDrawerListener(drawerToggle); + drawerToggle.syncState(); + setDrawerEnabled(false); // disable until synced + + accountsView = findViewById(R.id.accounts_nav); + accountsView.setNavigationItemSelectedListener(this); + + showNet(); + + Fragment walletFragment = new WalletFragment(); + getSupportFragmentManager().beginTransaction() + .add(R.id.fragment_container, walletFragment, WalletFragment.class.getName()).commit(); + Timber.d("fragment added"); + + startWalletService(); + Timber.d("onCreate() done."); + } + + public void showNet() { + switch (WalletManager.getInstance().getNetworkType()) { + case NetworkType_Mainnet: + toolbar.setBackgroundResource(R.drawable.backgound_toolbar_mainnet); + break; + case NetworkType_Stagenet: + case NetworkType_Testnet: + toolbar.setBackgroundResource(ThemeHelper.getThemedResourceId(this, R.attr.colorPrimaryDark)); + break; + default: + throw new IllegalStateException("Unsupported Network: " + WalletManager.getInstance().getNetworkType()); + } + } + + @Override + public Wallet getWallet() { + if (mBoundService == null) throw new IllegalStateException("WalletService not bound."); + return mBoundService.getWallet(); + } + + private WalletService mBoundService = null; + private boolean mIsBound = false; + + private final ServiceConnection mConnection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder service) { + // This is called when the connection with the service has been + // established, giving us the service object we can use to + // interact with the service. Because we have bound to a explicit + // service that we know is running in our own process, we can + // cast its IBinder to a concrete class and directly access it. + mBoundService = ((WalletService.WalletServiceBinder) service).getService(); + mBoundService.setObserver(WalletActivity.this); + Bundle extras = getIntent().getExtras(); + if (extras != null) { + String walletId = extras.getString(REQUEST_ID); + if (walletId != null) { + setTitle(walletId, getString(R.string.status_wallet_connecting)); + } + } + updateProgress(); + Timber.d("CONNECTED"); + } + + public void onServiceDisconnected(ComponentName className) { + // This is called when the connection with the service has been + // unexpectedly disconnected -- that is, its process crashed. + // Because it is running in our same process, we should never + // see this happen. + mBoundService = null; + setTitle(getString(R.string.wallet_activity_name), getString(R.string.status_wallet_disconnected)); + Timber.d("DISCONNECTED"); + } + }; + + void connectWalletService(String walletName, String walletPassword) { + // Establish a connection with the service. We use an explicit + // class name because we want a specific service implementation that + // we know will be running in our own process (and thus won't be + // supporting component replacement by other applications). + Intent intent = new Intent(getApplicationContext(), WalletService.class); + intent.putExtra(WalletService.REQUEST_WALLET, walletName); + intent.putExtra(WalletService.REQUEST, WalletService.REQUEST_CMD_LOAD); + intent.putExtra(WalletService.REQUEST_CMD_LOAD_PW, walletPassword); + startService(intent); + bindService(intent, mConnection, Context.BIND_AUTO_CREATE); + mIsBound = true; + Timber.d("BOUND"); + } + + void disconnectWalletService() { + if (mIsBound) { + // Detach our existing connection. + mBoundService.setObserver(null); + unbindService(mConnection); + mIsBound = false; + Timber.d("UNBOUND"); + } + } + + @Override + protected void onPause() { + Timber.d("onPause()"); + super.onPause(); + } + + @Override + protected void onResume() { + super.onResume(); + Timber.d("onResume()"); + } + + @Override + public void saveWallet() { + if (mIsBound) { // no point in talking to unbound service + Intent intent = new Intent(getApplicationContext(), WalletService.class); + intent.putExtra(WalletService.REQUEST, WalletService.REQUEST_CMD_STORE); + startService(intent); + Timber.d("STORE request sent"); + } else { + Timber.e("Service not bound"); + } + } + +////////////////////////////////////////// +// WalletFragment.Listener +////////////////////////////////////////// + + @Override + public boolean hasBoundService() { + return mBoundService != null; + } + + @Override + public Wallet.ConnectionStatus getConnectionStatus() { + return mBoundService.getConnectionStatus(); + } + + @Override + public long getDaemonHeight() { + return mBoundService.getDaemonHeight(); + } + + @Override + public void onSendRequest(View view) { + replaceFragment(SendFragment.newInstance(uri), null, null); + uri = null; // only use uri once + } + + @Override + public void onTxDetailsRequest(View view, TransactionInfo info) { + Bundle args = new Bundle(); + args.putParcelable(TxFragment.ARG_INFO, info); + replaceFragmentWithTransition(view, new TxFragment(), null, args); + } + + @Override + public void forceUpdate() { + try { + onRefreshed(getWallet(), true); + } catch (IllegalStateException ex) { + Timber.e(ex.getLocalizedMessage()); + } + } + +/////////////////////////// +// WalletService.Observer +/////////////////////////// + + private int numAccounts = -1; + + // refresh and return true if successful + @Override + public boolean onRefreshed(final Wallet wallet, final boolean full) { + Timber.d("onRefreshed()"); + runOnUiThread(() -> { + if (getWallet() != null) + updateAccountsBalance(); + }); + if (numAccounts != wallet.getNumAccounts()) { + numAccounts = wallet.getNumAccounts(); + runOnUiThread(this::updateAccountsList); + } + try { + final WalletFragment walletFragment = getWalletFragment(); + if (wallet.isSynchronized()) { + releaseWakeLock(RELEASE_WAKE_LOCK_DELAY); // the idea is to stay awake until synced + if (!synced) { // first sync + onProgress(-1); + saveWallet(); // save on first sync + synced = true; + runOnUiThread(walletFragment::onSynced); + } + } + runOnUiThread(() -> { + walletFragment.onRefreshed(wallet, full); + updateCurrentFragment(wallet); + }); + return true; + } catch (ClassCastException ex) { + // not in wallet fragment (probably send monero) + Timber.d(ex.getLocalizedMessage()); + // keep calm and carry on + } + return false; + } + + private void updateCurrentFragment(final Wallet wallet) { + final Fragment fragment = getCurrentFragment(); + if (fragment instanceof OnBlockUpdateListener) { + ((OnBlockUpdateListener) fragment).onBlockUpdate(wallet); + } + } + + @Override + public void onWalletStored(final boolean success) { + runOnUiThread(() -> { + if (!success) { + Toast.makeText(WalletActivity.this, getString(R.string.status_wallet_unload_failed), Toast.LENGTH_LONG).show(); + } + }); + } + + boolean haveWallet = false; + + @Override + public void onWalletOpen(final Wallet.Device device) { + if (device == Wallet.Device.Device_Ledger) { + runOnUiThread(() -> showLedgerProgressDialog(LedgerProgressDialog.TYPE_RESTORE)); + } + } + + @Override + public void onWalletStarted(final Wallet.Status walletStatus) { + runOnUiThread(() -> { + dismissProgressDialog(); + if (walletStatus == null) { + // guess what went wrong + Toast.makeText(WalletActivity.this, getString(R.string.status_wallet_connect_failed), Toast.LENGTH_LONG).show(); + } else { + if (Wallet.ConnectionStatus.ConnectionStatus_WrongVersion == walletStatus.getConnectionStatus()) + Toast.makeText(WalletActivity.this, getString(R.string.status_wallet_connect_wrongversion), Toast.LENGTH_LONG).show(); + else if (!walletStatus.isOk()) + Toast.makeText(WalletActivity.this, walletStatus.getErrorString(), Toast.LENGTH_LONG).show(); + } + }); + if ((walletStatus == null) || (Wallet.ConnectionStatus.ConnectionStatus_Connected != walletStatus.getConnectionStatus())) { + finish(); + } else { + haveWallet = true; + invalidateOptionsMenu(); + + if (requestStreetMode) onEnableStreetMode(); + + final WalletFragment walletFragment = getWalletFragment(); + runOnUiThread(() -> { + updateAccountsHeader(); + if (walletFragment != null) { + walletFragment.onLoaded(); + } + }); + } + } + + @Override + public void onTransactionCreated(final String txTag, final PendingTransaction pendingTransaction) { + try { + final SendFragment sendFragment = (SendFragment) getCurrentFragment(); + runOnUiThread(() -> { + dismissProgressDialog(); + PendingTransaction.Status status = pendingTransaction.getStatus(); + if (status != PendingTransaction.Status.Status_Ok) { + String errorText = pendingTransaction.getErrorString(); + getWallet().disposePendingTransaction(); + sendFragment.onCreateTransactionFailed(errorText); + } else { + sendFragment.onTransactionCreated(txTag, pendingTransaction); + } + }); + } catch (ClassCastException ex) { + // not in spend fragment + Timber.d(ex.getLocalizedMessage()); + // don't need the transaction any more + getWallet().disposePendingTransaction(); + } + } + + @Override + public void onSendTransactionFailed(final String error) { + try { + final SendFragment sendFragment = (SendFragment) getCurrentFragment(); + runOnUiThread(() -> sendFragment.onSendTransactionFailed(error)); + } catch (ClassCastException ex) { + // not in spend fragment + Timber.d(ex.getLocalizedMessage()); + } + } + + @Override + public void onTransactionSent(final String txId) { + try { + final SendFragment sendFragment = (SendFragment) getCurrentFragment(); + runOnUiThread(() -> sendFragment.onTransactionSent(txId)); + } catch (ClassCastException ex) { + // not in spend fragment + Timber.d(ex.getLocalizedMessage()); + } + } + + @Override + public void onProgress(final String text) { + try { + final WalletFragment walletFragment = getWalletFragment(); + runOnUiThread(new Runnable() { + public void run() { + walletFragment.setProgress(text); + } + }); + } catch (ClassCastException ex) { + // not in wallet fragment (probably send monero) + Timber.d(ex.getLocalizedMessage()); + // keep calm and carry on + } + } + + @Override + public void onProgress(final int n) { + runOnUiThread(() -> { + try { + WalletFragment walletFragment = getWalletFragment(); + if (walletFragment != null) + walletFragment.setProgress(n); + } catch (ClassCastException ex) { + // not in wallet fragment (probably send monero) + Timber.d(ex.getLocalizedMessage()); + // keep calm and carry on + } + }); + } + + private void updateProgress() { + // TODO maybe show real state of WalletService (like "still closing previous wallet") + if (hasBoundService()) { + onProgress(mBoundService.getProgressText()); + onProgress(mBoundService.getProgressValue()); + } + } + +/////////////////////////// +// SendFragment.Listener +/////////////////////////// + + @Override + public void onSend(UserNotes notes) { + if (mIsBound) { // no point in talking to unbound service + Intent intent = new Intent(getApplicationContext(), WalletService.class); + intent.putExtra(WalletService.REQUEST, WalletService.REQUEST_CMD_SEND); + intent.putExtra(WalletService.REQUEST_CMD_SEND_NOTES, notes.txNotes); + startService(intent); + Timber.d("SEND TX request sent"); + } else { + Timber.e("Service not bound"); + } + + } + + @Override + public void onPrepareSend(final String tag, final TxData txData) { + if (mIsBound) { // no point in talking to unbound service + Intent intent = new Intent(getApplicationContext(), WalletService.class); + intent.putExtra(WalletService.REQUEST, WalletService.REQUEST_CMD_TX); + intent.putExtra(WalletService.REQUEST_CMD_TX_DATA, txData); + intent.putExtra(WalletService.REQUEST_CMD_TX_TAG, tag); + startService(intent); + Timber.d("CREATE TX request sent"); + if (getWallet().getDeviceType() == Wallet.Device.Device_Ledger) + showLedgerProgressDialog(LedgerProgressDialog.TYPE_SEND); + } else { + Timber.e("Service not bound"); + } + } + + @Override + public Subaddress getWalletSubaddress(int accountIndex, int subaddressIndex) { + return getWallet().getSubaddressObject(accountIndex, subaddressIndex); + } + + public String getWalletName() { + return getWallet().getName(); + } + + void popFragmentStack(String name) { + if (name == null) { + getSupportFragmentManager().popBackStack(); + } else { + getSupportFragmentManager().popBackStack(name, FragmentManager.POP_BACK_STACK_INCLUSIVE); + } + } + + void replaceFragmentWithTransition(View view, Fragment newFragment, String stackName, Bundle extras) { + if (extras != null) { + newFragment.setArguments(extras); + } + int transition; + if (newFragment instanceof TxFragment) + transition = R.string.tx_details_transition_name; + else if (newFragment instanceof SubaddressInfoFragment) + transition = R.string.subaddress_info_transition_name; + else + throw new IllegalStateException("expecting known transition"); + + getSupportFragmentManager().beginTransaction() + .addSharedElement(view, getString(transition)) + .replace(R.id.fragment_container, newFragment) + .addToBackStack(stackName) + .commit(); + } + + void replaceFragment(Fragment newFragment, String stackName, Bundle extras) { + if (extras != null) { + newFragment.setArguments(extras); + } + getSupportFragmentManager() + .beginTransaction() + .replace(R.id.fragment_container, newFragment) + .addToBackStack(stackName) + .commit(); + } + + private void onWalletDetails() { + DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case DialogInterface.BUTTON_POSITIVE: + final Bundle extras = new Bundle(); + extras.putString(GenerateReviewFragment.REQUEST_TYPE, GenerateReviewFragment.VIEW_TYPE_WALLET); + + Helper.promptPassword(WalletActivity.this, getWallet().getName(), true, new Helper.PasswordAction() { + @Override + public void act(String walletName, String password, boolean fingerprintUsed) { + replaceFragment(new GenerateReviewFragment(), null, extras); + } + + @Override + public void fail(String walletName) { + } + }); + + break; + case DialogInterface.BUTTON_NEGATIVE: + // do nothing + break; + } + } + }; + + AlertDialog.Builder builder = new MaterialAlertDialogBuilder(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(); + } + + void onShareTxInfo() { + try { + TxFragment fragment = (TxFragment) getCurrentFragment(); + fragment.shareTxInfo(); + } catch (ClassCastException ex) { + // not in wallet fragment + Timber.e(ex.getLocalizedMessage()); + // keep calm and carry on + } + } + + @Override + public void onDisposeRequest() { + //TODO consider doing this through the WalletService to avoid concurrency issues + getWallet().disposePendingTransaction(); + } + + private boolean startScanFragment = false; + + @Override + protected void onResumeFragments() { + super.onResumeFragments(); + if (startScanFragment) { + startScanFragment(); + startScanFragment = false; + } + } + + private void startScanFragment() { + Bundle extras = new Bundle(); + replaceFragment(new ScannerFragment(), null, extras); + } + + /// QR scanner callbacks + @Override + public void onScan() { + if (Helper.getCameraPermission(this)) { + startScanFragment(); + } else { + Timber.i("Waiting for permissions"); + } + } + + @Override + public boolean onScanned(String qrCode) { + // #gurke + BarcodeData bcData = BarcodeData.fromString(qrCode); + if (bcData != null) { + popFragmentStack(null); + Timber.d("AAA"); + onUriScanned(bcData); + return true; + } else { + return false; + } + } + + OnUriScannedListener onUriScannedListener = null; + + @Override + public void setOnUriScannedListener(OnUriScannedListener onUriScannedListener) { + this.onUriScannedListener = onUriScannedListener; + } + + @Override + void onUriScanned(BarcodeData barcodeData) { + super.onUriScanned(barcodeData); + boolean processed = false; + if (onUriScannedListener != null) { + processed = onUriScannedListener.onUriScanned(barcodeData); + } + if (!processed || (onUriScannedListener == null)) { + Toast.makeText(this, getString(R.string.nfc_tag_read_what), Toast.LENGTH_LONG).show(); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + Timber.d("onRequestPermissionsResult()"); + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == Helper.PERMISSIONS_REQUEST_CAMERA) { // If request is cancelled, the result arrays are empty. + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + startScanFragment = true; + } else { + String msg = getString(R.string.message_camera_not_permitted); + Toast.makeText(this, msg, Toast.LENGTH_LONG).show(); + } + } + } + + @Override + public void onWalletReceive(View view) { + final String address = getWallet().getAddress(); + Timber.d("startReceive()"); + Bundle b = new Bundle(); + b.putString("address", address); + b.putString("name", getWalletName()); + replaceFragment(new ReceiveFragment(), null, b); + Timber.d("ReceiveFragment placed"); + } + + @Override + public long getTotalFunds() { + return getWallet().getUnlockedBalance(); + } + + @Override + public void onBackPressed() { + if (drawer.isDrawerOpen(GravityCompat.START)) { + drawer.closeDrawer(GravityCompat.START); + return; + } + final Fragment fragment = getCurrentFragment(); + if (fragment instanceof OnBackPressedListener) { + if (!((OnBackPressedListener) fragment).onBackPressed()) { + super.onBackPressed(); + } + } else { + super.onBackPressed(); + } + Helper.hideKeyboard(this); + } + + @Override + public void onFragmentDone() { + popFragmentStack(null); + } + + @Override + public SharedPreferences getPrefs() { + return getPreferences(Context.MODE_PRIVATE); + } + + private final List accountIds = new ArrayList<>(); + + // generate and cache unique ids for use in accounts list + private int getAccountId(int accountIndex) { + final int n = accountIds.size(); + for (int i = n; i <= accountIndex; i++) { + accountIds.add(View.generateViewId()); + } + return accountIds.get(accountIndex); + } + + // drawer stuff + + void updateAccountsBalance() { + final TextView tvBalance = accountsView.getHeaderView(0).findViewById(R.id.tvBalance); + if (!isStreetMode()) { + tvBalance.setText(getString(R.string.accounts_balance, + Helper.getDisplayAmount(getWallet().getBalanceAll(), 5))); + } else { + tvBalance.setText(null); + } + updateAccountsList(); + } + + void updateAccountsHeader() { + final Wallet wallet = getWallet(); + final TextView tvName = accountsView.getHeaderView(0).findViewById(R.id.tvName); + tvName.setText(wallet.getName()); + } + + void updateAccountsList() { + Menu menu = accountsView.getMenu(); + menu.removeGroup(R.id.accounts_list); + final Wallet wallet = getWallet(); + if (wallet != null) { + final int n = wallet.getNumAccounts(); + final boolean showBalances = (n > 1) && !isStreetMode(); + for (int i = 0; i < n; i++) { + final String label = (showBalances ? + getString(R.string.label_account, wallet.getAccountLabel(i), Helper.getDisplayAmount(wallet.getBalance(i), 2)) + : wallet.getAccountLabel(i)); + final MenuItem item = menu.add(R.id.accounts_list, getAccountId(i), 2 * i, label); + item.setIcon(R.drawable.ic_account_balance_wallet_black_24dp); + if (i == wallet.getAccountIndex()) + item.setChecked(true); + } + menu.setGroupCheckable(R.id.accounts_list, true, true); + } + } + + @Override + public void setDrawerEnabled(boolean enabled) { + Timber.d("setDrawerEnabled %b", enabled); + final int lockMode = enabled ? DrawerLayout.LOCK_MODE_UNLOCKED : + DrawerLayout.LOCK_MODE_LOCKED_CLOSED; + drawer.setDrawerLockMode(lockMode); + drawerToggle.setDrawerIndicatorEnabled(enabled); + invalidateOptionsMenu(); // menu may need to be changed + } + + void updateAccountName() { + setSubtitle(getWallet().getAccountLabel()); + updateAccountsList(); + } + + public void onAccountRename() { + final LayoutInflater li = LayoutInflater.from(this); + final View promptsView = li.inflate(R.layout.prompt_rename, null); + + final AlertDialog.Builder alertDialogBuilder = new MaterialAlertDialogBuilder(this); + alertDialogBuilder.setView(promptsView); + + final EditText etRename = promptsView.findViewById(R.id.etRename); + final TextView tvRenameLabel = promptsView.findViewById(R.id.tvRenameLabel); + final Wallet wallet = getWallet(); + tvRenameLabel.setText(getString(R.string.prompt_rename, wallet.getAccountLabel())); + + // set dialog message + alertDialogBuilder + .setCancelable(false) + .setPositiveButton(getString(R.string.label_ok), + (dialog, id) -> { + Helper.hideKeyboardAlways(WalletActivity.this); + String newName = etRename.getText().toString(); + wallet.setAccountLabel(newName); + updateAccountName(); + }) + .setNegativeButton(getString(R.string.label_cancel), + (dialog, id) -> { + Helper.hideKeyboardAlways(WalletActivity.this); + dialog.cancel(); + }); + + final AlertDialog dialog = alertDialogBuilder.create(); + Helper.showKeyboard(dialog); + + // accept keyboard "ok" + etRename.setOnEditorActionListener((v, actionId, event) -> { + if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN)) + || (actionId == EditorInfo.IME_ACTION_DONE)) { + Helper.hideKeyboardAlways(WalletActivity.this); + String newName = etRename.getText().toString(); + dialog.cancel(); + wallet.setAccountLabel(newName); + updateAccountName(); + return false; + } + return false; + }); + + dialog.show(); + } + + public void setAccountIndex(int accountIndex) { + getWallet().setAccountIndex(accountIndex); + selectedSubaddressIndex = 0; + } + + @Override + public boolean onNavigationItemSelected(MenuItem item) { + final int id = item.getItemId(); + if (id == R.id.account_new) { + addAccount(); + } else { + Timber.d("NavigationDrawer ID=%d", id); + int accountIdx = accountIds.indexOf(id); + if (accountIdx >= 0) { + Timber.d("found @%d", accountIdx); + setAccountIndex(accountIdx); + } + forceUpdate(); + drawer.closeDrawer(GravityCompat.START); + } + return true; + } + + private int lastUsedAccount() { + int lastUsedAccount = 0; + for (TransactionInfo info : getWallet().getHistory().getAll()) { + if (info.accountIndex > lastUsedAccount) + lastUsedAccount = info.accountIndex; + } + return lastUsedAccount; + } + + private void addAccount() { + final Wallet wallet = getWallet(); + final int maxAccounts = lastUsedAccount() + wallet.getDeviceType().getAccountLookahead(); + if (wallet.getNumAccounts() < maxAccounts) + new AsyncAddAccount().executeOnExecutor(MoneroThreadPoolExecutor.MONERO_THREAD_POOL_EXECUTOR); + else + Toast.makeText(this, getString(R.string.max_account_warning), Toast.LENGTH_LONG).show(); + } + + @SuppressLint("StaticFieldLeak") + private class AsyncAddAccount extends AsyncTask { + boolean dialogOpened = false; + + @Override + protected void onPreExecute() { + super.onPreExecute(); + switch (getWallet().getDeviceType()) { + case Device_Ledger: + showLedgerProgressDialog(LedgerProgressDialog.TYPE_ACCOUNT); + dialogOpened = true; + break; + case Device_Software: + showProgressDialog(R.string.accounts_progress_new); + dialogOpened = true; + break; + default: + throw new IllegalStateException("Hardware backing not supported. At all!"); + } + } + + @Override + protected Boolean doInBackground(Void... params) { + if (params.length != 0) return false; + getWallet().addAccount(); + setAccountIndex(getWallet().getNumAccounts() - 1); + return true; + } + + @Override + protected void onPostExecute(Boolean result) { + super.onPostExecute(result); + forceUpdate(); + drawer.closeDrawer(GravityCompat.START); + if (dialogOpened) + dismissProgressDialog(); + Toast.makeText(WalletActivity.this, + getString(R.string.accounts_new, getWallet().getNumAccounts() - 1), + Toast.LENGTH_SHORT).show(); + } + } + + // we store the index only and always retrieve a new Subaddress object + // to ensure we get the current label + private int selectedSubaddressIndex = 0; + + @Override + public Subaddress getSelectedSubaddress() { + return getWallet().getSubaddressObject(selectedSubaddressIndex); + } + + @Override + public void onSubaddressSelected(@Nullable final Subaddress subaddress) { + selectedSubaddressIndex = subaddress.getAddressIndex(); + onBackPressed(); + } + + @Override + public void showSubaddresses(boolean managerMode) { + final Bundle b = new Bundle(); + if (managerMode) + b.putString(SubaddressFragment.KEY_MODE, SubaddressFragment.MODE_MANAGER); + replaceFragment(new SubaddressFragment(), null, b); + } + + @Override + public void showSubaddress(View view, final int subaddressIndex) { + final Bundle b = new Bundle(); + b.putInt("subaddressIndex", subaddressIndex); + replaceFragmentWithTransition(view, new SubaddressInfoFragment(), null, b); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/WalletFragment.java b/app/src/main/java/com/m2049r/xmrwallet/WalletFragment.java new file mode 100644 index 0000000..515d2d8 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/WalletFragment.java @@ -0,0 +1,559 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.Spinner; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.RecyclerView; + +import com.github.brnunes.swipeablerecyclerview.SwipeableRecyclerViewTouchListener; +import com.m2049r.xmrwallet.layout.TransactionInfoAdapter; +import com.m2049r.xmrwallet.model.TransactionInfo; +import com.m2049r.xmrwallet.model.Wallet; +import com.m2049r.xmrwallet.service.exchange.api.ExchangeApi; +import com.m2049r.xmrwallet.service.exchange.api.ExchangeCallback; +import com.m2049r.xmrwallet.service.exchange.api.ExchangeRate; +import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.ServiceHelper; +import com.m2049r.xmrwallet.util.ThemeHelper; +import com.m2049r.xmrwallet.widget.Toolbar; + +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import timber.log.Timber; + +public class WalletFragment extends Fragment + implements TransactionInfoAdapter.OnInteractionListener { + private TransactionInfoAdapter adapter; + private final NumberFormat formatter = NumberFormat.getInstance(); + + private TextView tvStreetView; + private LinearLayout llBalance; + private FrameLayout flExchange; + private TextView tvBalance; + private TextView tvUnconfirmedAmount; + private TextView tvProgress; + private ImageView ivSynced; + private ProgressBar pbProgress; + private Button bReceive; + private Button bSend; + private ImageView ivStreetGunther; + private Drawable streetGunther = null; + RecyclerView txlist; + + private Spinner sCurrency; + + private final List dismissedTransactions = new ArrayList<>(); + + public void resetDismissedTransactions() { + dismissedTransactions.clear(); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + if (activityCallback.hasWallet()) + inflater.inflate(R.menu.wallet_menu, menu); + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_wallet, container, false); + + ivStreetGunther = view.findViewById(R.id.ivStreetGunther); + tvStreetView = view.findViewById(R.id.tvStreetView); + llBalance = view.findViewById(R.id.llBalance); + flExchange = view.findViewById(R.id.flExchange); + ((ProgressBar) view.findViewById(R.id.pbExchange)).getIndeterminateDrawable(). + setColorFilter( + ThemeHelper.getThemedColor(getContext(), R.attr.colorPrimaryVariant), + android.graphics.PorterDuff.Mode.MULTIPLY); + + tvProgress = view.findViewById(R.id.tvProgress); + pbProgress = view.findViewById(R.id.pbProgress); + tvBalance = view.findViewById(R.id.tvBalance); + showBalance(Helper.getDisplayAmount(0)); + tvUnconfirmedAmount = view.findViewById(R.id.tvUnconfirmedAmount); + showUnconfirmed(0); + ivSynced = view.findViewById(R.id.ivSynced); + + sCurrency = view.findViewById(R.id.sCurrency); + List currencies = new ArrayList<>(); + currencies.add(Helper.BASE_CRYPTO); + if (Helper.SHOW_EXCHANGERATES) + currencies.addAll(Arrays.asList(getResources().getStringArray(R.array.currency))); + ArrayAdapter spinnerAdapter = new ArrayAdapter<>(requireContext(), R.layout.item_spinner_balance, currencies); + spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + sCurrency.setAdapter(spinnerAdapter); + + bSend = view.findViewById(R.id.bSend); + bReceive = view.findViewById(R.id.bReceive); + + txlist = view.findViewById(R.id.list); + adapter = new TransactionInfoAdapter(getActivity(), this); + txlist.setAdapter(adapter); + adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + if ((positionStart == 0) && (txlist.computeVerticalScrollOffset() == 0)) + txlist.scrollToPosition(positionStart); + } + }); + + txlist.addOnItemTouchListener( + new SwipeableRecyclerViewTouchListener(txlist, + new SwipeableRecyclerViewTouchListener.SwipeListener() { + @Override + public boolean canSwipeLeft(int position) { + return activityCallback.isStreetMode(); + } + + @Override + public boolean canSwipeRight(int position) { + return activityCallback.isStreetMode(); + } + + @Override + public void onDismissedBySwipeLeft(RecyclerView recyclerView, int[] reverseSortedPositions) { + for (int position : reverseSortedPositions) { + dismissedTransactions.add(adapter.getItem(position).hash); + adapter.removeItem(position); + } + } + + @Override + public void onDismissedBySwipeRight(RecyclerView recyclerView, int[] reverseSortedPositions) { + for (int position : reverseSortedPositions) { + dismissedTransactions.add(adapter.getItem(position).hash); + adapter.removeItem(position); + } + } + })); + + bSend.setOnClickListener(v -> activityCallback.onSendRequest(v)); + bReceive.setOnClickListener(v -> activityCallback.onWalletReceive(v)); + + sCurrency.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parentView, View selectedItemView, int position, long id) { + refreshBalance(); + } + + @Override + public void onNothingSelected(AdapterView parentView) { + // nothing (yet?) + } + }); + + if (activityCallback.isSynced()) { + onSynced(); + } + + activityCallback.forceUpdate(); + + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + } + + void showBalance(String balance) { + tvBalance.setText(balance); + final boolean streetMode = activityCallback.isStreetMode(); + if (!streetMode) { + llBalance.setVisibility(View.VISIBLE); + tvStreetView.setVisibility(View.INVISIBLE); + } else { + llBalance.setVisibility(View.INVISIBLE); + tvStreetView.setVisibility(View.VISIBLE); + } + setStreetModeBackground(streetMode); + } + + void showUnconfirmed(double unconfirmedAmount) { + if (activityCallback.isStreetMode() || unconfirmedAmount == 0) { + tvUnconfirmedAmount.setText(null); + tvUnconfirmedAmount.setVisibility(View.GONE); + } else { + String unconfirmed = Helper.getFormattedAmount(unconfirmedAmount, true); + tvUnconfirmedAmount.setText(getResources().getString(R.string.xmr_unconfirmed_amount, unconfirmed)); + tvUnconfirmedAmount.setVisibility(View.VISIBLE); + } + } + + void updateBalance() { + if (isExchanging) return; // wait for exchange to finish - it will fire this itself then. + // at this point selection is XMR in case of error + String displayB; + double amountA = Helper.getDecimalAmount(unlockedBalance).doubleValue(); + if (!Helper.BASE_CRYPTO.equals(balanceCurrency)) { // not XMR + double amountB = amountA * balanceRate; + displayB = Helper.getFormattedAmount(amountB, false); + } else { // XMR + displayB = Helper.getFormattedAmount(amountA, true); + } + showBalance(displayB); + } + + String balanceCurrency = Helper.BASE_CRYPTO; + double balanceRate = 1.0; + + private final ExchangeApi exchangeApi = ServiceHelper.getExchangeApi(); + + void refreshBalance() { + double unconfirmedXmr = Helper.getDecimalAmount(balance - unlockedBalance).doubleValue(); + showUnconfirmed(unconfirmedXmr); + if (sCurrency.getSelectedItemPosition() == 0) { // XMR + double amountXmr = Helper.getDecimalAmount(unlockedBalance).doubleValue(); + showBalance(Helper.getFormattedAmount(amountXmr, true)); + } else { // not XMR + String currency = (String) sCurrency.getSelectedItem(); + Timber.d(currency); + if (!currency.equals(balanceCurrency) || (balanceRate <= 0)) { + showExchanging(); + exchangeApi.queryExchangeRate(Helper.BASE_CRYPTO, currency, + new ExchangeCallback() { + @Override + public void onSuccess(final ExchangeRate exchangeRate) { + if (isAdded()) + new Handler(Looper.getMainLooper()).post(() -> exchange(exchangeRate)); + } + + @Override + public void onError(final Exception e) { + Timber.e(e.getLocalizedMessage()); + if (isAdded()) + new Handler(Looper.getMainLooper()).post(() -> exchangeFailed()); + } + }); + } else { + updateBalance(); + } + } + } + + boolean isExchanging = false; + + void showExchanging() { + isExchanging = true; + tvBalance.setVisibility(View.GONE); + flExchange.setVisibility(View.VISIBLE); + sCurrency.setEnabled(false); + } + + void hideExchanging() { + isExchanging = false; + tvBalance.setVisibility(View.VISIBLE); + flExchange.setVisibility(View.GONE); + sCurrency.setEnabled(true); + } + + public void exchangeFailed() { + sCurrency.setSelection(0, true); // default to XMR + double amountXmr = Helper.getDecimalAmount(unlockedBalance).doubleValue(); + showBalance(Helper.getFormattedAmount(amountXmr, true)); + hideExchanging(); + } + + public void exchange(final ExchangeRate exchangeRate) { + hideExchanging(); + if (!Helper.BASE_CRYPTO.equals(exchangeRate.getBaseCurrency())) { + Timber.e("Not XMR"); + sCurrency.setSelection(0, true); + balanceCurrency = Helper.BASE_CRYPTO; + balanceRate = 1.0; + } else { + int spinnerPosition = ((ArrayAdapter) sCurrency.getAdapter()).getPosition(exchangeRate.getQuoteCurrency()); + if (spinnerPosition < 0) { // requested currency not in list + Timber.e("Requested currency not in list %s", exchangeRate.getQuoteCurrency()); + sCurrency.setSelection(0, true); + } else { + sCurrency.setSelection(spinnerPosition, true); + } + balanceCurrency = exchangeRate.getQuoteCurrency(); + balanceRate = exchangeRate.getRate(); + } + updateBalance(); + } + + // Callbacks from TransactionInfoAdapter + @Override + public void onInteraction(final View view, final TransactionInfo infoItem) { + activityCallback.onTxDetailsRequest(view, infoItem); + } + + // if account index has changed scroll to top? + private int accountIndex = 0; + + public void onRefreshed(final Wallet wallet, boolean full) { + Timber.d("onRefreshed(%b)", full); + + if (adapter.needsTransactionUpdateOnNewBlock()) { + wallet.refreshHistory(); + full = true; + } + if (full) { + List list = new ArrayList<>(); + final long streetHeight = activityCallback.getStreetModeHeight(); + Timber.d("StreetHeight=%d", streetHeight); + wallet.refreshHistory(); + for (TransactionInfo info : wallet.getHistory().getAll()) { + Timber.d("TxHeight=%d, Label=%s", info.blockheight, info.subaddressLabel); + if ((info.isPending || (info.blockheight >= streetHeight)) + && !dismissedTransactions.contains(info.hash)) + list.add(info); + } + adapter.setInfos(list); + if (accountIndex != wallet.getAccountIndex()) { + accountIndex = wallet.getAccountIndex(); + txlist.scrollToPosition(0); + } + } + updateStatus(wallet); + } + + public void onSynced() { + if (!activityCallback.isWatchOnly()) { + bSend.setVisibility(View.VISIBLE); + bSend.setEnabled(true); + } + if (isVisible()) enableAccountsList(true); //otherwise it is enabled in onResume() + } + + public void unsync() { + if (!activityCallback.isWatchOnly()) { + bSend.setVisibility(View.INVISIBLE); + bSend.setEnabled(false); + } + if (isVisible()) enableAccountsList(false); //otherwise it is enabled in onResume() + firstBlock = 0; + } + + boolean walletLoaded = false; + + public void onLoaded() { + walletLoaded = true; + showReceive(); + } + + private void showReceive() { + if (walletLoaded) { + bReceive.setVisibility(View.VISIBLE); + bReceive.setEnabled(true); + } + } + + private String syncText = null; + + public void setProgress(final String text) { + syncText = text; + tvProgress.setText(text); + } + + private int syncProgress = -1; + + public void setProgress(final int n) { + syncProgress = n; + if (n > 100) { + pbProgress.setIndeterminate(true); + pbProgress.setVisibility(View.VISIBLE); + } else if (n >= 0) { + pbProgress.setIndeterminate(false); + pbProgress.setProgress(n); + pbProgress.setVisibility(View.VISIBLE); + } else { // <0 + pbProgress.setVisibility(View.INVISIBLE); + } + } + + void setActivityTitle(Wallet wallet) { + if (wallet == null) return; + walletTitle = wallet.getName(); + walletSubtitle = wallet.getAccountLabel(); + activityCallback.setTitle(walletTitle, walletSubtitle); + Timber.d("wallet title is %s", walletTitle); + } + + private long firstBlock = 0; + private String walletTitle = null; + private String walletSubtitle = null; + private long unlockedBalance = 0; + private long balance = 0; + + private int accountIdx = -1; + + private void updateStatus(Wallet wallet) { + if (!isAdded()) return; + Timber.d("updateStatus()"); + if ((walletTitle == null) || (accountIdx != wallet.getAccountIndex())) { + accountIdx = wallet.getAccountIndex(); + setActivityTitle(wallet); + } + balance = wallet.getBalance(); + unlockedBalance = wallet.getUnlockedBalance(); + refreshBalance(); + String sync; + if (!activityCallback.hasBoundService()) + throw new IllegalStateException("WalletService not bound."); + Wallet.ConnectionStatus daemonConnected = activityCallback.getConnectionStatus(); + if (daemonConnected == Wallet.ConnectionStatus.ConnectionStatus_Connected) { + if (!wallet.isSynchronized()) { + long daemonHeight = activityCallback.getDaemonHeight(); + long walletHeight = wallet.getBlockChainHeight(); + long n = daemonHeight - walletHeight; + sync = getString(R.string.status_syncing) + " " + formatter.format(n) + " " + getString(R.string.status_remaining); + if (firstBlock == 0) { + firstBlock = walletHeight; + } + int x = 100 - Math.round(100f * n / (1f * daemonHeight - firstBlock)); + if (x == 0) x = 101; // indeterminate + setProgress(x); + ivSynced.setVisibility(View.GONE); + } else { + sync = getString(R.string.status_synced) + " " + formatter.format(wallet.getBlockChainHeight()); + ivSynced.setVisibility(View.VISIBLE); + } + } else { + sync = getString(R.string.status_wallet_connecting); + setProgress(101); + } + setProgress(sync); + // TODO show connected status somewhere + } + + Listener activityCallback; + + // Container Activity must implement this interface + public interface Listener { + boolean hasBoundService(); + + void forceUpdate(); + + Wallet.ConnectionStatus getConnectionStatus(); + + long getDaemonHeight(); //mBoundService.getDaemonHeight(); + + void onSendRequest(View view); + + void onTxDetailsRequest(View view, TransactionInfo info); + + boolean isSynced(); + + boolean isStreetMode(); + + long getStreetModeHeight(); + + boolean isWatchOnly(); + + String getTxKey(String txId); + + void onWalletReceive(View view); + + boolean hasWallet(); + + Wallet getWallet(); + + void setToolbarButton(int type); + + void setTitle(String title, String subtitle); + + void setSubtitle(String subtitle); + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + if (context instanceof Listener) { + this.activityCallback = (Listener) context; + } else { + throw new ClassCastException(context.toString() + + " must implement Listener"); + } + } + + @Override + public void onResume() { + super.onResume(); + Timber.d("onResume()"); + activityCallback.setTitle(walletTitle, walletSubtitle); + activityCallback.setToolbarButton(Toolbar.BUTTON_NONE); + setProgress(syncProgress); + setProgress(syncText); + showReceive(); + if (activityCallback.isSynced()) enableAccountsList(true); + } + + @Override + public void onPause() { + enableAccountsList(false); + super.onPause(); + } + + public interface DrawerLocker { + void setDrawerEnabled(boolean enabled); + } + + private void enableAccountsList(boolean enable) { + if (activityCallback instanceof DrawerLocker) { + ((DrawerLocker) activityCallback).setDrawerEnabled(enable); + } + } + + public void setStreetModeBackground(boolean enable) { + //TODO figure out why gunther disappears on return from send although he is still set + if (enable) { + if (streetGunther == null) + streetGunther = ContextCompat.getDrawable(requireContext(), R.drawable.ic_gunther_streetmode); + ivStreetGunther.setImageDrawable(streetGunther); + } else + ivStreetGunther.setImageDrawable(null); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/XmrWalletApplication.java b/app/src/main/java/com/m2049r/xmrwallet/XmrWalletApplication.java new file mode 100644 index 0000000..3762fb1 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/XmrWalletApplication.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2017 m2049r et al. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet; + +import android.app.Application; +import android.content.Context; +import android.content.res.Configuration; +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.fragment.app.FragmentManager; + +import com.m2049r.xmrwallet.model.NetworkType; +import com.m2049r.xmrwallet.util.LocaleHelper; +import com.m2049r.xmrwallet.util.NetCipherHelper; +import com.m2049r.xmrwallet.util.NightmodeHelper; + +import timber.log.Timber; + +public class XmrWalletApplication extends Application { + + @Override + public void onCreate() { + super.onCreate(); + FragmentManager.enableNewStateManager(false); + if (BuildConfig.DEBUG) { + Timber.plant(new Timber.DebugTree()); + } + + NightmodeHelper.setPreferredNightmode(this); + + NetCipherHelper.createInstance(this); + } + + @Override + protected void attachBaseContext(Context context) { + super.attachBaseContext(LocaleHelper.setPreferredLocale(context)); + } + + @Override + public void onConfigurationChanged(@NonNull Configuration configuration) { + super.onConfigurationChanged(configuration); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + LocaleHelper.updateSystemDefaultLocale(configuration.getLocales().get(0)); + } else { + LocaleHelper.updateSystemDefaultLocale(configuration.locale); + } + LocaleHelper.setPreferredLocale(this); + } + + static public NetworkType getNetworkType() { + switch (BuildConfig.FLAVOR_net) { + case "mainnet": + return NetworkType.NetworkType_Mainnet; + case "stagenet": + return NetworkType.NetworkType_Stagenet; + case "devnet": // flavors cannot start with "test" + return NetworkType.NetworkType_Testnet; + default: + throw new IllegalStateException("unknown net flavor " + BuildConfig.FLAVOR_net); + } + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/data/BarcodeData.java b/app/src/main/java/com/m2049r/xmrwallet/data/BarcodeData.java new file mode 100644 index 0000000..3a5cecb --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/data/BarcodeData.java @@ -0,0 +1,232 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.data; + +import android.net.Uri; + +import com.m2049r.xmrwallet.util.OpenAliasHelper; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import lombok.ToString; +import timber.log.Timber; + +@ToString +public class BarcodeData { + public enum Security { + NORMAL, + OA_NO_DNSSEC, + OA_DNSSEC + } + + final public Crypto asset; + final public List ambiguousAssets; + final public String address; + final public String addressName; + final public String amount; + final public String description; + final public Security security; + + public BarcodeData(List assets, String address) { + if (assets.isEmpty()) + throw new IllegalArgumentException("no assets specified"); + this.addressName = null; + this.description = null; + this.amount = null; + this.security = Security.NORMAL; + this.address = address; + if (assets.size() == 1) { + this.asset = assets.get(0); + this.ambiguousAssets = null; + } else { + this.asset = null; + this.ambiguousAssets = assets; + } + } + + public BarcodeData(Crypto asset, String address, String description, String amount) { + this(asset, address, null, description, amount, Security.NORMAL); + } + + public BarcodeData(Crypto asset, String address, String addressName, String description, String amount, Security security) { + this.ambiguousAssets = null; + this.asset = asset; + this.address = address; + this.addressName = addressName; + this.description = description; + this.amount = amount; + this.security = security; + } + + public Uri getUri() { + return Uri.parse(getUriString()); + } + + public String getUriString() { + if (asset != Crypto.XMR) throw new IllegalStateException("We can only do XMR stuff!"); + StringBuilder sb = new StringBuilder(); + sb.append(Crypto.XMR.getUriScheme()) + .append(':') + .append(address); + boolean first = true; + if ((description != null) && !description.isEmpty()) { + sb.append(first ? "?" : "&"); + first = false; + sb.append(Crypto.XMR.getUriMessage()).append('=').append(Uri.encode(description)); + } + if ((amount != null) && !amount.isEmpty()) { + sb.append(first ? "?" : "&"); + sb.append(Crypto.XMR.getUriAmount()).append('=').append(amount); + } + return sb.toString(); + } + + static private BarcodeData parseNaked(String address) { + List possibleAssets = new ArrayList<>(); + for (Crypto crypto : Crypto.values()) { + if (crypto.validate(address)) { + possibleAssets.add(crypto); + } + } + if (possibleAssets.isEmpty()) + return null; + return new BarcodeData(possibleAssets, address); + } + + static public BarcodeData parseUri(String uriString) { + Timber.d("parseBitUri=%s", uriString); + + URI uri; + try { + uri = new URI(uriString); + } catch (URISyntaxException ex) { + return null; + } + if (!uri.isOpaque()) return null; + final String scheme = uri.getScheme(); + Crypto crypto = Crypto.withScheme(scheme); + if (crypto == null) return null; + + String[] parts = uri.getRawSchemeSpecificPart().split("[?]"); + if ((parts.length <= 0) || (parts.length > 2)) { + Timber.d("invalid number of parts %d", parts.length); + return null; + } + + Map parms = new HashMap<>(); + if (parts.length == 2) { + String[] args = parts[1].split("&"); + for (String arg : args) { + String[] namevalue = arg.split("="); + if (namevalue.length == 0) { + continue; + } + parms.put(Uri.decode(namevalue[0]).toLowerCase(), + namevalue.length > 1 ? Uri.decode(namevalue[1]) : ""); + } + } + + String addressName = parms.get(crypto.getUriLabel()); + String description = parms.get(crypto.getUriMessage()); + String address = parts[0]; // no need to decode as there can be no special characters + if (address.isEmpty()) { + Timber.d("no address"); + return null; + } + if (!crypto.validate(address)) { + Timber.d("%s address (%s) invalid", crypto, address); + return null; + } + String amount = parms.get(crypto.getUriAmount()); + if ((amount != null) && (!amount.isEmpty())) { + try { + Double.parseDouble(amount); + } catch (NumberFormatException ex) { + Timber.d(ex.getLocalizedMessage()); + return null; // we have an amount but its not a number! + } + } + return new BarcodeData(crypto, address, addressName, description, amount, Security.NORMAL); + } + + + static public BarcodeData fromString(String qrCode) { + BarcodeData bcData = parseUri(qrCode); + if (bcData == null) { + // maybe it's naked? + bcData = parseNaked(qrCode); + } + if (bcData == null) { + // check for OpenAlias + bcData = parseOpenAlias(qrCode, false); + } + return bcData; + } + + static public BarcodeData parseOpenAlias(String oaString, boolean dnssec) { + Timber.d("parseOpenAlias=%s", oaString); + if (oaString == null) return null; + + Map oaAttrs = OpenAliasHelper.parse(oaString); + if (oaAttrs == null) return null; + + String oaAsset = oaAttrs.get(OpenAliasHelper.OA1_ASSET); + if (oaAsset == null) return null; + + String address = oaAttrs.get(OpenAliasHelper.OA1_ADDRESS); + if (address == null) return null; + + Crypto crypto = Crypto.withSymbol(oaAsset); + if (crypto == null) { + Timber.i("Unsupported OpenAlias asset %s", oaAsset); + return null; + } + if (!crypto.validate(address)) { + Timber.d("%s address invalid", crypto); + return null; + } + + String description = oaAttrs.get(OpenAliasHelper.OA1_DESCRIPTION); + if (description == null) { + description = oaAttrs.get(OpenAliasHelper.OA1_NAME); + } + String amount = oaAttrs.get(OpenAliasHelper.OA1_AMOUNT); + String addressName = oaAttrs.get(OpenAliasHelper.OA1_NAME); + + if (amount != null) { + try { + Double.parseDouble(amount); + } catch (NumberFormatException ex) { + Timber.d(ex.getLocalizedMessage()); + return null; // we have an amount but its not a number! + } + } + + Security sec = dnssec ? BarcodeData.Security.OA_DNSSEC : BarcodeData.Security.OA_NO_DNSSEC; + + return new BarcodeData(crypto, address, addressName, description, amount, sec); + } + + public boolean isAmbiguous() { + return ambiguousAssets != null; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/data/Crypto.java b/app/src/main/java/com/m2049r/xmrwallet/data/Crypto.java new file mode 100644 index 0000000..e9e66f1 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/data/Crypto.java @@ -0,0 +1,89 @@ +package com.m2049r.xmrwallet.data; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.m2049r.xmrwallet.R; +import com.m2049r.xmrwallet.model.Wallet; +import com.m2049r.xmrwallet.util.validator.BitcoinAddressType; +import com.m2049r.xmrwallet.util.validator.BitcoinAddressValidator; +import com.m2049r.xmrwallet.util.validator.EthAddressValidator; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum Crypto { + XMR("XMR", true, "monero:tx_amount:recipient_name:tx_description", R.id.ibXMR, R.drawable.ic_monero, R.drawable.ic_monero_bw, Wallet::isAddressValid), + BTC("BTC", true, "bitcoin:amount:label:message", R.id.ibBTC, R.drawable.ic_xmrto_btc, R.drawable.ic_xmrto_btc_off, address -> { + return BitcoinAddressValidator.validate(address, BitcoinAddressType.BTC); + }), + DASH("DASH", true, "dash:amount:label:message", R.id.ibDASH, R.drawable.ic_xmrto_dash, R.drawable.ic_xmrto_dash_off, address -> { + return BitcoinAddressValidator.validate(address, BitcoinAddressType.DASH); + }), + DOGE("DOGE", true, "dogecoin:amount:label:message", R.id.ibDOGE, R.drawable.ic_xmrto_doge, R.drawable.ic_xmrto_doge_off, address -> { + return BitcoinAddressValidator.validate(address, BitcoinAddressType.DOGE); + }), + ETH("ETH", false, "ethereum:amount:label:message", R.id.ibETH, R.drawable.ic_xmrto_eth, R.drawable.ic_xmrto_eth_off, EthAddressValidator::validate), + LTC("LTC", true, "litecoin:amount:label:message", R.id.ibLTC, R.drawable.ic_xmrto_ltc, R.drawable.ic_xmrto_ltc_off, address -> { + return BitcoinAddressValidator.validate(address, BitcoinAddressType.LTC); + }); + + @Getter + @NonNull + private final String symbol; + @Getter + private final boolean casefull; + @NonNull + private final String uriSpec; + @Getter + private final int buttonId; + @Getter + private final int iconEnabledId; + @Getter + private final int iconDisabledId; + @NonNull + private final Validator validator; + + @Nullable + public static Crypto withScheme(@NonNull String scheme) { + for (Crypto crypto : values()) { + if (crypto.getUriScheme().equals(scheme)) return crypto; + } + return null; + } + + @Nullable + public static Crypto withSymbol(@NonNull String symbol) { + final String upperSymbol = symbol.toUpperCase(); + for (Crypto crypto : values()) { + if (crypto.symbol.equals(upperSymbol)) return crypto; + } + return null; + } + + interface Validator { + boolean validate(String address); + } + + // TODO maybe cache these segments + String getUriScheme() { + return uriSpec.split(":")[0]; + } + + String getUriAmount() { + return uriSpec.split(":")[1]; + } + + String getUriLabel() { + return uriSpec.split(":")[2]; + } + + String getUriMessage() { + return uriSpec.split(":")[3]; + } + + boolean validate(String address) { + return validator.validate(address); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/data/DefaultNodes.java b/app/src/main/java/com/m2049r/xmrwallet/data/DefaultNodes.java new file mode 100644 index 0000000..49aab3e --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/data/DefaultNodes.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.data; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +// Nodes stolen from https://moneroworld.com/#nodes + +@AllArgsConstructor +public enum DefaultNodes { + MONERUJO("nodex.monerujo.io:18081"), + XMRTO("node.xmr.to:18081"), + SUPPORTXMR("node.supportxmr.com:18081"), + HASHVAULT("nodes.hashvault.pro:18081"), + MONEROWORLD("node.moneroworld.com:18089"), + XMRTW("opennode.xmr-tw.org:18089"), + MONERUJO_ONION("monerujods7mbghwe6cobdr6ujih6c22zu5rl7zshmizz2udf7v7fsad.onion:18081/mainnet/monerujo.onion"), + Criminales78("56wl7y2ebhamkkiza4b7il4mrzwtyvpdym7bm2bkg3jrei2je646k3qd.onion:18089/mainnet/Criminales78.onion"), + xmrfail("mxcd4577fldb3ppzy7obmmhnu3tf57gbcbd4qhwr2kxyjj2qi3dnbfqd.onion:18081/mainnet/xmrfail.onion"), + boldsuck("6dsdenp6vjkvqzy4wzsnzn6wixkdzihx3khiumyzieauxuxslmcaeiad.onion:18081/mainnet/boldsuck.onion"); + + @Getter + private final String uri; +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/data/Node.java b/app/src/main/java/com/m2049r/xmrwallet/data/Node.java new file mode 100644 index 0000000..0f01153 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/data/Node.java @@ -0,0 +1,371 @@ +/* + * Copyright (c) 2018 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.data; + +import com.m2049r.xmrwallet.model.NetworkType; +import com.m2049r.xmrwallet.model.WalletManager; +import com.m2049r.xmrwallet.util.OnionHelper; + +import java.io.UnsupportedEncodingException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.net.UnknownHostException; + +import lombok.Getter; +import lombok.Setter; +import timber.log.Timber; + +public class Node { + static public final String MAINNET = "mainnet"; + static public final String STAGENET = "stagenet"; + static public final String TESTNET = "testnet"; + + static class Address { + final private InetAddress inet; + final private String onion; + + public boolean isOnion() { + return onion != null; + } + + public String getHostName() { + if (inet != null) { + return inet.getHostName(); + } else { + return onion; + } + } + + public String getHostAddress() { + if (inet != null) { + return inet.getHostAddress(); + } else { + return onion; + } + } + + private Address(InetAddress address, String onion) { + this.inet = address; + this.onion = onion; + } + + static Address of(InetAddress address) { + return new Address(address, null); + } + + static Address of(String host) throws UnknownHostException { + if (OnionHelper.isOnionHost(host)) { + return new Address(null, host); + } else { + return new Address(InetAddress.getByName(host), null); + } + } + + @Override + public int hashCode() { + return getHostAddress().hashCode(); + } + + @Override + public boolean equals(Object other) { + return (other instanceof Address) && (getHostAddress().equals(((Address) other).getHostAddress())); + } + } + + @Getter + private String name = null; + @Getter + final private NetworkType networkType; + Address hostAddress; + @Getter + private String host; + @Getter + @Setter + int rpcPort = 0; + private int levinPort = 0; + @Getter + @Setter + private String username = ""; + @Getter + @Setter + private String password = ""; + @Getter + @Setter + private boolean favourite = false; + @Getter + @Setter + private boolean selected = false; + + @Override + public int hashCode() { + return hostAddress.hashCode(); + } + + // Nodes are equal if they are the same host address:port & are on the same network + @Override + public boolean equals(Object other) { + if (!(other instanceof Node)) return false; + final Node anotherNode = (Node) other; + return (hostAddress.equals(anotherNode.hostAddress) + && (rpcPort == anotherNode.rpcPort) + && (networkType == anotherNode.networkType)); + } + + public boolean isOnion() { + return hostAddress.isOnion(); + } + + static public Node fromString(String nodeString) { + try { + return new Node(nodeString); + } catch (IllegalArgumentException ex) { + Timber.w(ex); + return null; + } + } + + Node(String nodeString) { + if ((nodeString == null) || nodeString.isEmpty()) + throw new IllegalArgumentException("daemon is empty"); + String daemonAddress; + String a[] = nodeString.split("@"); + if (a.length == 1) { // no credentials + daemonAddress = a[0]; + username = ""; + password = ""; + } else if (a.length == 2) { // credentials + String userPassword[] = a[0].split(":"); + if (userPassword.length != 2) + throw new IllegalArgumentException("User:Password invalid"); + username = userPassword[0]; + if (!username.isEmpty()) { + password = userPassword[1]; + } else { + password = ""; + } + daemonAddress = a[1]; + } else { + throw new IllegalArgumentException("Too many @"); + } + + String daParts[] = daemonAddress.split("/"); + if ((daParts.length > 3) || (daParts.length < 1)) + throw new IllegalArgumentException("Too many '/' or too few"); + + daemonAddress = daParts[0]; + String da[] = daemonAddress.split(":"); + if ((da.length > 2) || (da.length < 1)) + throw new IllegalArgumentException("Too many ':' or too few"); + String host = da[0]; + + if (daParts.length == 1) { + networkType = NetworkType.NetworkType_Mainnet; + } else { + switch (daParts[1]) { + case MAINNET: + networkType = NetworkType.NetworkType_Mainnet; + break; + case STAGENET: + networkType = NetworkType.NetworkType_Stagenet; + break; + case TESTNET: + networkType = NetworkType.NetworkType_Testnet; + break; + default: + throw new IllegalArgumentException("invalid net: " + daParts[1]); + } + } + if (networkType != WalletManager.getInstance().getNetworkType()) + throw new IllegalArgumentException("wrong net: " + networkType); + + String name = host; + if (daParts.length == 3) { + try { + name = URLDecoder.decode(daParts[2], "UTF-8"); + } catch (UnsupportedEncodingException ex) { + Timber.w(ex); // if we can't encode it, we don't use it + } + } + this.name = name; + + int port; + if (da.length == 2) { + try { + port = Integer.parseInt(da[1]); + } catch (NumberFormatException ex) { + throw new IllegalArgumentException("Port not numeric"); + } + } else { + port = getDefaultRpcPort(); + } + try { + setHost(host); + } catch (UnknownHostException ex) { + throw new IllegalArgumentException("cannot resolve host " + host); + } + this.rpcPort = port; + this.levinPort = getDefaultLevinPort(); + } + + public String toNodeString() { + return toString(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + if (!username.isEmpty() && !password.isEmpty()) { + sb.append(username).append(":").append(password).append("@"); + } + sb.append(host).append(":").append(rpcPort); + sb.append("/"); + switch (networkType) { + case NetworkType_Mainnet: + sb.append(MAINNET); + break; + case NetworkType_Stagenet: + sb.append(STAGENET); + break; + case NetworkType_Testnet: + sb.append(TESTNET); + break; + } + if (name != null) + try { + sb.append("/").append(URLEncoder.encode(name, "UTF-8")); + } catch (UnsupportedEncodingException ex) { + Timber.w(ex); // if we can't encode it, we don't store it + } + return sb.toString(); + } + + public Node() { + this.networkType = WalletManager.getInstance().getNetworkType(); + } + + // constructor used for created nodes from retrieved peer lists + public Node(InetSocketAddress socketAddress) { + this(); + this.hostAddress = Address.of(socketAddress.getAddress()); + this.host = socketAddress.getHostString(); + this.rpcPort = 0; // unknown + this.levinPort = socketAddress.getPort(); + this.username = ""; + this.password = ""; + } + + public String getAddress() { + return getHostAddress() + ":" + rpcPort; + } + + public String getHostAddress() { + return hostAddress.getHostAddress(); + } + + public void setHost(String host) throws UnknownHostException { + if ((host == null) || (host.isEmpty())) + throw new UnknownHostException("loopback not supported (yet?)"); + this.host = host; + this.hostAddress = Address.of(host); + } + + public void setDefaultName() { + if (name != null) return; + String nodeName = hostAddress.getHostName(); + if (hostAddress.isOnion()) { + nodeName = nodeName.substring(0, nodeName.length() - ".onion".length()); + if (nodeName.length() > 16) { + nodeName = nodeName.substring(0, 8) + "…" + nodeName.substring(nodeName.length() - 6); + } + nodeName = nodeName + ".onion"; + } + this.name = nodeName; + } + + public void setName(String name) { + if ((name == null) || (name.isEmpty())) + setDefaultName(); + else + this.name = name; + } + + public void toggleFavourite() { + favourite = !favourite; + } + + public Node(Node anotherNode) { + networkType = anotherNode.networkType; + overwriteWith(anotherNode); + } + + public void overwriteWith(Node anotherNode) { + if (networkType != anotherNode.networkType) + throw new IllegalStateException("network types do not match"); + name = anotherNode.name; + hostAddress = anotherNode.hostAddress; + host = anotherNode.host; + rpcPort = anotherNode.rpcPort; + levinPort = anotherNode.levinPort; + username = anotherNode.username; + password = anotherNode.password; + favourite = anotherNode.favourite; + } + + static private int DEFAULT_LEVIN_PORT = 0; + + // every node knows its network, but they are all the same + static public int getDefaultLevinPort() { + if (DEFAULT_LEVIN_PORT > 0) return DEFAULT_LEVIN_PORT; + switch (WalletManager.getInstance().getNetworkType()) { + case NetworkType_Mainnet: + DEFAULT_LEVIN_PORT = 18080; + break; + case NetworkType_Testnet: + DEFAULT_LEVIN_PORT = 28080; + break; + case NetworkType_Stagenet: + DEFAULT_LEVIN_PORT = 38080; + break; + default: + throw new IllegalStateException("unsupported net " + WalletManager.getInstance().getNetworkType()); + } + return DEFAULT_LEVIN_PORT; + } + + static private int DEFAULT_RPC_PORT = 0; + + // every node knows its network, but they are all the same + static public int getDefaultRpcPort() { + if (DEFAULT_RPC_PORT > 0) return DEFAULT_RPC_PORT; + switch (WalletManager.getInstance().getNetworkType()) { + case NetworkType_Mainnet: + DEFAULT_RPC_PORT = 18081; + break; + case NetworkType_Testnet: + DEFAULT_RPC_PORT = 28081; + break; + case NetworkType_Stagenet: + DEFAULT_RPC_PORT = 38081; + break; + default: + throw new IllegalStateException("unsupported net " + WalletManager.getInstance().getNetworkType()); + } + return DEFAULT_RPC_PORT; + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/data/NodeInfo.java b/app/src/main/java/com/m2049r/xmrwallet/data/NodeInfo.java new file mode 100644 index 0000000..351e794 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/data/NodeInfo.java @@ -0,0 +1,303 @@ +/* + * Copyright (c) 2018 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.data; + +import android.content.Context; +import android.text.Html; +import android.text.Spanned; +import android.widget.TextView; + +import androidx.core.content.ContextCompat; + +import com.m2049r.levin.scanner.LevinPeer; +import com.m2049r.xmrwallet.R; +import com.m2049r.xmrwallet.util.NetCipherHelper; +import com.m2049r.xmrwallet.util.NetCipherHelper.Request; +import com.m2049r.xmrwallet.util.NodePinger; +import com.m2049r.xmrwallet.util.ThemeHelper; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.Calendar; +import java.util.Comparator; + +import lombok.Getter; +import lombok.Setter; +import okhttp3.HttpUrl; +import okhttp3.Response; +import okhttp3.ResponseBody; +import timber.log.Timber; + +public class NodeInfo extends Node { + final static public int MIN_MAJOR_VERSION = 14; + final static public String RPC_VERSION = "2.0"; + + @Getter + private long height = 0; + @Getter + private long timestamp = 0; + @Getter + private int majorVersion = 0; + @Getter + private double responseTime = Double.MAX_VALUE; + @Getter + private int responseCode = 0; + @Getter + private boolean tested = false; + @Getter + @Setter + private boolean selecting = false; + + public void clear() { + height = 0; + majorVersion = 0; + responseTime = Double.MAX_VALUE; + responseCode = 0; + timestamp = 0; + tested = false; + } + + static public NodeInfo fromString(String nodeString) { + try { + return new NodeInfo(nodeString); + } catch (IllegalArgumentException ex) { + return null; + } + } + + public NodeInfo(NodeInfo anotherNode) { + super(anotherNode); + overwriteWith(anotherNode); + } + + private SocketAddress levinSocketAddress = null; + + synchronized public SocketAddress getLevinSocketAddress() { + if (levinSocketAddress == null) { + // use default peer port if not set - very few peers use nonstandard port + levinSocketAddress = new InetSocketAddress(hostAddress.getHostAddress(), getDefaultLevinPort()); + } + return levinSocketAddress; + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public boolean equals(Object other) { + return super.equals(other); + } + + public NodeInfo(String nodeString) { + super(nodeString); + } + + public NodeInfo(LevinPeer levinPeer) { + super(levinPeer.getSocketAddress()); + } + + public NodeInfo(InetSocketAddress address) { + super(address); + } + + public NodeInfo() { + super(); + } + + public boolean isSuccessful() { + return (responseCode >= 200) && (responseCode < 300); + } + + public boolean isUnauthorized() { + return responseCode == HttpURLConnection.HTTP_UNAUTHORIZED; + } + + public boolean isValid() { + return isSuccessful() && (majorVersion >= MIN_MAJOR_VERSION) && (responseTime < Double.MAX_VALUE); + } + + static public Comparator BestNodeComparator = (o1, o2) -> { + if (o1.isValid()) { + if (o2.isValid()) { // both are valid + // higher node wins + int heightDiff = (int) (o2.height - o1.height); + if (heightDiff != 0) + return heightDiff; + // if they are equal, faster node wins + return (int) Math.signum(o1.responseTime - o2.responseTime); + } else { + return -1; + } + } else { + return 1; + } + }; + + public void overwriteWith(NodeInfo anotherNode) { + super.overwriteWith(anotherNode); + height = anotherNode.height; + timestamp = anotherNode.timestamp; + majorVersion = anotherNode.majorVersion; + responseTime = anotherNode.responseTime; + responseCode = anotherNode.responseCode; + } + + public String toNodeString() { + return super.toString(); + } + + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(super.toString()); + sb.append("?rc=").append(responseCode); + sb.append("?v=").append(majorVersion); + sb.append("&h=").append(height); + sb.append("&ts=").append(timestamp); + if (responseTime < Double.MAX_VALUE) { + sb.append("&t=").append(responseTime).append("ms"); + } + return sb.toString(); + } + + private static final int HTTP_TIMEOUT = 1000; //ms + public static final double PING_GOOD = HTTP_TIMEOUT / 3.0; //ms + public static final double PING_MEDIUM = 2 * PING_GOOD; //ms + public static final double PING_BAD = HTTP_TIMEOUT; + + public boolean testRpcService() { + return testRpcService(rpcPort); + } + + public boolean testRpcService(NodePinger.Listener listener) { + boolean result = testRpcService(rpcPort); + if (listener != null) + listener.publish(this); + return result; + } + + private Request rpcServiceRequest(int port) { + final HttpUrl url = new HttpUrl.Builder() + .scheme("http") + .host(getHost()) + .port(port) + .addPathSegment("json_rpc") + .build(); + final String json = "{\"jsonrpc\":\"2.0\",\"id\":\"0\",\"method\":\"getlastblockheader\"}"; + return new Request(url, json, getUsername(), getPassword()); + } + + private boolean testRpcService(int port) { + Timber.d("Testing %s", toNodeString()); + clear(); + if (hostAddress.isOnion() && !NetCipherHelper.isTor()) { + tested = true; // sortof + responseCode = 418; // I'm a teapot - or I need an Onion - who knows + return false; // autofail + } + try { + long ta = System.nanoTime(); + try (Response response = rpcServiceRequest(port).execute()) { + Timber.d("%s: %s", response.code(), response.request().url()); + responseTime = (System.nanoTime() - ta) / 1000000.0; + responseCode = response.code(); + if (response.isSuccessful()) { + ResponseBody respBody = response.body(); // closed through Response object + if ((respBody != null) && (respBody.contentLength() < 2000)) { // sanity check + final JSONObject json = new JSONObject(respBody.string()); + String rpcVersion = json.getString("jsonrpc"); + if (!RPC_VERSION.equals(rpcVersion)) + return false; + final JSONObject result = json.getJSONObject("result"); + if (!result.has("credits")) // introduced in monero v0.15.0 + return false; + final JSONObject header = result.getJSONObject("block_header"); + height = header.getLong("height"); + timestamp = header.getLong("timestamp"); + majorVersion = header.getInt("major_version"); + return true; // success + } + } + } + } catch (IOException | JSONException ex) { + Timber.d("EX: %s", ex.getMessage()); //TODO: do something here (show error?) + } finally { + tested = true; + } + return false; + } + + static final private int[] TEST_PORTS = {18089}; // check only opt-in port + + public boolean findRpcService() { + // if already have an rpcPort, use that + if (rpcPort > 0) return testRpcService(rpcPort); + // otherwise try to find one + for (int port : TEST_PORTS) { + if (testRpcService(port)) { // found a service + this.rpcPort = port; + return true; + } + } + return false; + } + + static public final int STALE_NODE_HOURS = 2; + + public void showInfo(TextView view, String info, boolean isError) { + final Context ctx = view.getContext(); + final Spanned text = Html.fromHtml(ctx.getString(R.string.status, + Integer.toHexString(ThemeHelper.getThemedColor(ctx, R.attr.positiveColor) & 0xFFFFFF), + Integer.toHexString(ThemeHelper.getThemedColor(ctx, android.R.attr.colorBackground) & 0xFFFFFF), + (hostAddress.isOnion() ? " .onion  " : ""), " " + info)); + view.setText(text); + if (isError) + view.setTextColor(ThemeHelper.getThemedColor(ctx, R.attr.colorError)); + else + view.setTextColor(ThemeHelper.getThemedColor(ctx, android.R.attr.textColorSecondary)); + } + + public void showInfo(TextView view) { + if (!isTested()) { + showInfo(view, "", false); + return; + } + final Context ctx = view.getContext(); + final long now = Calendar.getInstance().getTimeInMillis() / 1000; + final long secs = (now - timestamp); + final long mins = secs / 60; + final long hours = mins / 60; + final long days = hours / 24; + String info; + if (mins < 2) { + info = ctx.getString(R.string.node_updated_now, secs); + } else if (hours < 2) { + info = ctx.getString(R.string.node_updated_mins, mins); + } else if (days < 2) { + info = ctx.getString(R.string.node_updated_hours, hours); + } else { + info = ctx.getString(R.string.node_updated_days, days); + } + showInfo(view, info, hours >= STALE_NODE_HOURS); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/data/PendingTx.java b/app/src/main/java/com/m2049r/xmrwallet/data/PendingTx.java new file mode 100644 index 0000000..7f99ad9 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/data/PendingTx.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.data; + +import com.m2049r.xmrwallet.model.PendingTransaction; + +public class PendingTx { + final public PendingTransaction.Status status; + final public String error; + final public long amount; + final public long dust; + final public long fee; + final public String txId; + final public long txCount; + + public PendingTx(PendingTransaction pendingTransaction) { + status = pendingTransaction.getStatus(); + error = pendingTransaction.getErrorString(); + amount = pendingTransaction.getAmount(); + dust = pendingTransaction.getDust(); + fee = pendingTransaction.getFee(); + txId = pendingTransaction.getFirstTxId(); + txCount = pendingTransaction.getTxCount(); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/data/Subaddress.java b/app/src/main/java/com/m2049r/xmrwallet/data/Subaddress.java new file mode 100644 index 0000000..582ce87 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/data/Subaddress.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2018 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.data; + +import java.util.regex.Pattern; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@RequiredArgsConstructor +@ToString +@EqualsAndHashCode +public class Subaddress implements Comparable { + @Getter + final private int accountIndex; + @Getter + final private int addressIndex; + @Getter + final private String address; + @Getter + private final String label; + @Getter + @Setter + private long amount; + + @Override + public int compareTo(Subaddress another) { // newer is < + final int compareAccountIndex = another.accountIndex - accountIndex; + if (compareAccountIndex == 0) + return another.addressIndex - addressIndex; + return compareAccountIndex; + } + + public String getSquashedAddress() { + return address.substring(0, 8) + "…" + address.substring(address.length() - 8); + } + + public static final Pattern DEFAULT_LABEL_FORMATTER = Pattern.compile("^[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}:[0-9]{2}:[0-9]{2}$"); + + public String getDisplayLabel() { + if (label.isEmpty() || (DEFAULT_LABEL_FORMATTER.matcher(label).matches())) + return ("#" + addressIndex); + else + return label; + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/data/TxData.java b/app/src/main/java/com/m2049r/xmrwallet/data/TxData.java new file mode 100644 index 0000000..144948c --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/data/TxData.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.data; + +import android.os.Parcel; +import android.os.Parcelable; + +import com.m2049r.xmrwallet.model.PendingTransaction; +import com.m2049r.xmrwallet.model.Wallet; +import com.m2049r.xmrwallet.util.Helper; + +// https://stackoverflow.com/questions/2139134/how-to-send-an-object-from-one-android-activity-to-another-using-intents +public class TxData implements Parcelable { + + public TxData() { + } + + public TxData(TxData txData) { + this.dstAddr = txData.dstAddr; + this.amount = txData.amount; + this.mixin = txData.mixin; + this.priority = txData.priority; + } + + public TxData(String dstAddr, + long amount, + int mixin, + PendingTransaction.Priority priority) { + this.dstAddr = dstAddr; + this.amount = amount; + this.mixin = mixin; + this.priority = priority; + } + + public String getDestinationAddress() { + return dstAddr; + } + + public long getAmount() { + return amount; + } + + public double getAmountAsDouble() { + return 1.0 * amount / Helper.ONE_XMR; + } + + public int getMixin() { + return mixin; + } + + public PendingTransaction.Priority getPriority() { + return priority; + } + + public void setDestinationAddress(String dstAddr) { + this.dstAddr = dstAddr; + } + + public void setAmount(long amount) { + this.amount = amount; + } + + public void setAmount(double amount) { + this.amount = Wallet.getAmountFromDouble(amount); + } + + public void setMixin(int mixin) { + this.mixin = mixin; + } + + public void setPriority(PendingTransaction.Priority priority) { + this.priority = priority; + } + + public UserNotes getUserNotes() { + return userNotes; + } + + public void setUserNotes(UserNotes userNotes) { + this.userNotes = userNotes; + } + + private String dstAddr; + private long amount; + private int mixin; + private PendingTransaction.Priority priority; + + private UserNotes userNotes; + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeString(dstAddr); + out.writeLong(amount); + out.writeInt(mixin); + out.writeInt(priority.getValue()); + } + + // this is used to regenerate your object. All Parcelables must have a CREATOR that implements these two methods + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public TxData createFromParcel(Parcel in) { + return new TxData(in); + } + + public TxData[] newArray(int size) { + return new TxData[size]; + } + }; + + protected TxData(Parcel in) { + dstAddr = in.readString(); + amount = in.readLong(); + mixin = in.readInt(); + priority = PendingTransaction.Priority.fromInteger(in.readInt()); + + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append("dstAddr:"); + sb.append(dstAddr); + sb.append(",amount:"); + sb.append(amount); + sb.append(",mixin:"); + sb.append(mixin); + sb.append(",priority:"); + sb.append(priority); + return sb.toString(); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/data/TxDataBtc.java b/app/src/main/java/com/m2049r/xmrwallet/data/TxDataBtc.java new file mode 100644 index 0000000..55ec71a --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/data/TxDataBtc.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.data; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import lombok.Getter; +import lombok.Setter; + +public class TxDataBtc extends TxData { + @Getter + @Setter + private String btcSymbol; // the actual non-XMR thing we're sending + @Getter + @Setter + private String xmrtoOrderId; // shown in success screen + @Getter + @Setter + private String btcAddress; + @Getter + @Setter + private double btcAmount; + + public TxDataBtc() { + super(); + } + + public TxDataBtc(TxDataBtc txDataBtc) { + super(txDataBtc); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeString(btcSymbol); + out.writeString(xmrtoOrderId); + out.writeString(btcAddress); + out.writeDouble(btcAmount); + } + + // this is used to regenerate your object. All Parcelables must have a CREATOR that implements these two methods + public static final Creator CREATOR = new Creator() { + public TxDataBtc createFromParcel(Parcel in) { + return new TxDataBtc(in); + } + + public TxDataBtc[] newArray(int size) { + return new TxDataBtc[size]; + } + }; + + protected TxDataBtc(Parcel in) { + super(in); + btcSymbol = in.readString(); + xmrtoOrderId = in.readString(); + btcAddress = in.readString(); + btcAmount = in.readDouble(); + } + + @NonNull + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("xmrtoOrderId:"); + sb.append(xmrtoOrderId); + sb.append(",btcSymbol:"); + sb.append(btcSymbol); + sb.append(",btcAddress:"); + sb.append(btcAddress); + sb.append(",btcAmount:"); + sb.append(btcAmount); + return sb.toString(); + } + + public boolean validateAddress(@NonNull String address) { + if ((btcSymbol == null) || (btcAddress == null)) return false; + final Crypto crypto = Crypto.withSymbol(btcSymbol); + if (crypto == null) return false; + if (crypto.isCasefull()) { // compare as-is + return address.equals(btcAddress); + } else { // normalize & compare (e.g. ETH with and without checksum capitals + return address.toLowerCase().equals(btcAddress.toLowerCase()); + } + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/data/UserNotes.java b/app/src/main/java/com/m2049r/xmrwallet/data/UserNotes.java new file mode 100644 index 0000000..f5eb14b --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/data/UserNotes.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.data; + +import com.m2049r.xmrwallet.service.shift.sideshift.api.CreateOrder; +import com.m2049r.xmrwallet.util.Helper; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class UserNotes { + public String txNotes = ""; + public String note = ""; + public String xmrtoTag = null; + public String xmrtoKey = null; + public String xmrtoAmount = null; // could be a double - but we are not doing any calculations + public String xmrtoCurrency = null; + public String xmrtoDestination = null; + + public UserNotes(final String txNotes) { + if (txNotes == null) { + return; + } + this.txNotes = txNotes; + Pattern p = Pattern.compile("^\\{([a-z]+)-(\\w{6,}),([0-9.]*)([A-Z]+),(\\w*)\\} ?(.*)"); + Matcher m = p.matcher(txNotes); + if (m.find()) { + xmrtoTag = m.group(1); + xmrtoKey = m.group(2); + xmrtoAmount = m.group(3); + xmrtoCurrency = m.group(4); + xmrtoDestination = m.group(5); + note = m.group(6); + } else { + note = txNotes; + } + } + + public void setNote(String newNote) { + if (newNote != null) { + note = newNote; + } else { + note = ""; + } + txNotes = buildTxNote(); + } + + public void setXmrtoOrder(CreateOrder order) { + if (order != null) { + xmrtoTag = order.TAG; + xmrtoKey = order.getOrderId(); + xmrtoAmount = Helper.getDisplayAmount(order.getBtcAmount()); + xmrtoCurrency = order.getBtcCurrency(); + xmrtoDestination = order.getBtcAddress(); + } else { + xmrtoTag = null; + xmrtoKey = null; + xmrtoAmount = null; + xmrtoDestination = null; + } + txNotes = buildTxNote(); + } + + private String buildTxNote() { + StringBuilder sb = new StringBuilder(); + if (xmrtoKey != null) { + if ((xmrtoAmount == null) || (xmrtoDestination == null)) + throw new IllegalArgumentException("Broken notes"); + sb.append("{"); + sb.append(xmrtoTag); + sb.append("-"); + sb.append(xmrtoKey); + sb.append(","); + sb.append(xmrtoAmount); + sb.append(xmrtoCurrency); + sb.append(","); + sb.append(xmrtoDestination); + sb.append("}"); + if ((note != null) && (!note.isEmpty())) + sb.append(" "); + } + sb.append(note); + return sb.toString(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/dialog/AboutFragment.java b/app/src/main/java/com/m2049r/xmrwallet/dialog/AboutFragment.java new file mode 100644 index 0000000..dc1047e --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/dialog/AboutFragment.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.dialog; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; +import android.text.Html; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.m2049r.xmrwallet.BuildConfig; +import com.m2049r.xmrwallet.R; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; + +import timber.log.Timber; + +public class AboutFragment extends DialogFragment { + static final String TAG = "AboutFragment"; + + public static AboutFragment newInstance() { + return new AboutFragment(); + } + + public static void display(FragmentManager fm) { + FragmentTransaction ft = fm.beginTransaction(); + Fragment prev = fm.findFragmentByTag(TAG); + if (prev != null) { + ft.remove(prev); + } + + AboutFragment.newInstance().show(ft, TAG); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_about, null); + ((TextView) view.findViewById(R.id.tvHelp)).setText(Html.fromHtml(getLicencesHtml())); + ((TextView) view.findViewById(R.id.tvVersion)).setText(getString(R.string.about_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)); + + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity()) + .setView(view) + .setNegativeButton(R.string.about_close, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + dialog.dismiss(); + } + }); + return builder.create(); + } + + private String getLicencesHtml() { + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(getContext().getAssets().open("licenses.html"), StandardCharsets.UTF_8))) { + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) + sb.append(line); + return sb.toString(); + } catch (IOException ex) { + Timber.e(ex); + return ex.getLocalizedMessage(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/dialog/CreditsFragment.java b/app/src/main/java/com/m2049r/xmrwallet/dialog/CreditsFragment.java new file mode 100644 index 0000000..d33921e --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/dialog/CreditsFragment.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.dialog; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.text.Html; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; + +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.m2049r.xmrwallet.R; + +public class CreditsFragment extends DialogFragment { + static final String TAG = "DonationFragment"; + + public static CreditsFragment newInstance() { + return new CreditsFragment(); + } + + public static void display(FragmentManager fm) { + FragmentTransaction ft = fm.beginTransaction(); + Fragment prev = fm.findFragmentByTag(TAG); + if (prev != null) { + ft.remove(prev); + } + + CreditsFragment.newInstance().show(ft, TAG); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_credits, null); + + ((TextView) view.findViewById(R.id.tvCredits)).setText(Html.fromHtml(getString(R.string.credits_text))); + + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity()) + .setView(view) + .setNegativeButton(R.string.about_close, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + dialog.dismiss(); + } + }); + return builder.create(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/dialog/HelpFragment.java b/app/src/main/java/com/m2049r/xmrwallet/dialog/HelpFragment.java new file mode 100644 index 0000000..6928937 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/dialog/HelpFragment.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.dialog; + +import android.app.Dialog; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; +import android.text.Html; +import android.text.Spanned; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.m2049r.xmrwallet.R; +import com.m2049r.xmrwallet.util.NetCipherHelper; + +public class HelpFragment extends DialogFragment { + static final String TAG = "HelpFragment"; + private static final String HELP_ID = "HELP_ID"; + private static final String TOR_BUTTON = "TOR"; + + public static HelpFragment newInstance(int helpResourceId) { + HelpFragment fragment = new HelpFragment(); + Bundle bundle = new Bundle(); + bundle.putInt(HELP_ID, helpResourceId); + // a hack for the tor button + if (helpResourceId == R.string.help_tor) + bundle.putInt(TOR_BUTTON, 7); + fragment.setArguments(bundle); + return fragment; + } + + public static void display(FragmentManager fm, int helpResourceId) { + FragmentTransaction ft = fm.beginTransaction(); + Fragment prev = fm.findFragmentByTag(TAG); + if (prev != null) { + ft.remove(prev); + } + + HelpFragment.newInstance(helpResourceId).show(ft, TAG); + } + + private Spanned getHtml(String html, double textSize) { + final Html.ImageGetter imageGetter = source -> { + final int imageId = getResources().getIdentifier(source.replace("/", ""), "drawable", requireActivity().getPackageName()); + // Don't die if we don't find the image - use a heart instead + final Drawable drawable = ContextCompat.getDrawable(requireActivity(), imageId > 0 ? imageId : R.drawable.ic_favorite_24dp); + final double f = textSize / drawable.getIntrinsicHeight(); + drawable.setBounds(0, 0, (int) (f * drawable.getIntrinsicWidth()), (int) textSize); + return drawable; + }; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY, imageGetter, null); + } else { + return Html.fromHtml(html, imageGetter, null); + } + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_help, null); + + int helpId = 0; + boolean torButton = false; + Bundle arguments = getArguments(); + if (arguments != null) { + helpId = arguments.getInt(HELP_ID); + torButton = arguments.getInt(TOR_BUTTON) > 0; + } + final TextView helpTv = view.findViewById(R.id.tvHelp); + if (helpId > 0) + helpTv.setText(getHtml(getString(helpId), helpTv.getTextSize())); + + MaterialAlertDialogBuilder builder = + new MaterialAlertDialogBuilder(requireActivity()) + .setView(view); + if (torButton) { + builder.setNegativeButton(R.string.help_nok, + (dialog, id) -> dialog.dismiss()) + .setPositiveButton(R.string.help_getorbot, + (dialog, id) -> { + dialog.dismiss(); + NetCipherHelper.getInstance().installOrbot(requireActivity()); + }); + } else { + builder.setNegativeButton(R.string.help_ok, + (dialog, id) -> dialog.dismiss()); + } + return builder.create(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/dialog/PrivacyFragment.java b/app/src/main/java/com/m2049r/xmrwallet/dialog/PrivacyFragment.java new file mode 100644 index 0000000..6ddb2e4 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/dialog/PrivacyFragment.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.dialog; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.text.Html; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; + +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.m2049r.xmrwallet.R; + +public class PrivacyFragment extends DialogFragment { + static final String TAG = "PrivacyFragment"; + + public static PrivacyFragment newInstance() { + return new PrivacyFragment(); + } + + public static void display(FragmentManager fm) { + FragmentTransaction ft = fm.beginTransaction(); + Fragment prev = fm.findFragmentByTag(TAG); + if (prev != null) { + ft.remove(prev); + } + + PrivacyFragment.newInstance().show(ft, TAG); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_privacy_policy, null); + + ((TextView) view.findViewById(R.id.tvCredits)).setText(Html.fromHtml(getString(R.string.privacy_policy))); + + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity()) + .setView(view) + .setNegativeButton(R.string.about_close, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + dialog.dismiss(); + } + }); + return builder.create(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/dialog/ProgressDialog.java b/app/src/main/java/com/m2049r/xmrwallet/dialog/ProgressDialog.java new file mode 100644 index 0000000..a8bb780 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/dialog/ProgressDialog.java @@ -0,0 +1,132 @@ +package com.m2049r.xmrwallet.dialog; + +/* + * Copyright (C) 2007 The Android Open Source Project + * Copyright (C) 2018 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.WindowManager; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.appcompat.app.AlertDialog; + +import com.m2049r.xmrwallet.R; +import com.m2049r.xmrwallet.util.Helper; + +import java.util.Locale; + +import timber.log.Timber; + +public class ProgressDialog extends AlertDialog { + + private ProgressBar pbBar; + + private TextView tvMessage; + + private TextView tvProgress; + + private View rlProgressBar, pbCircle; + + static private final String PROGRESS_FORMAT = "%1d/%2d"; + + private CharSequence message; + private int maxValue, progressValue; + private boolean indeterminate = true; + + public ProgressDialog(Context context) { + super(context); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + final View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_ledger_progress, null); + pbCircle = view.findViewById(R.id.pbCircle); + tvMessage = view.findViewById(R.id.tvMessage); + rlProgressBar = view.findViewById(R.id.rlProgressBar); + pbBar = view.findViewById(R.id.pbBar); + tvProgress = view.findViewById(R.id.tvProgress); + setView(view); + setIndeterminate(indeterminate); + if (maxValue > 0) { + setMax(maxValue); + } + if (progressValue > 0) { + setProgress(progressValue); + } + if (message != null) { + Timber.d("msg=%s", message); + setMessage(message); + } + + super.onCreate(savedInstanceState); + + if (Helper.preventScreenshot()) { + getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE); + } + } + + public void setProgress(int value, int max) { + progressValue = value; + maxValue = max; + if (pbBar != null) { + pbBar.setProgress(value); + pbBar.setMax(max); + tvProgress.setText(String.format(Locale.getDefault(), PROGRESS_FORMAT, value, maxValue)); + } + } + + public void setProgress(int value) { + progressValue = value; + if (pbBar != null) { + pbBar.setProgress(value); + tvProgress.setText(String.format(Locale.getDefault(), PROGRESS_FORMAT, value, maxValue)); + } + } + + public void setMax(int max) { + maxValue = max; + if (pbBar != null) { + pbBar.setMax(max); + } + } + + public void setIndeterminate(boolean indeterminate) { + if (this.indeterminate != indeterminate) { + if (rlProgressBar != null) { + if (indeterminate) { + pbCircle.setVisibility(View.VISIBLE); + rlProgressBar.setVisibility(View.GONE); + } else { + pbCircle.setVisibility(View.GONE); + rlProgressBar.setVisibility(View.VISIBLE); + } + } + this.indeterminate = indeterminate; + } + } + + @Override + public void setMessage(CharSequence message) { + this.message = message; + if (tvMessage != null) { + tvMessage.setText(message); + } + } +} 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 new file mode 100644 index 0000000..f25ad19 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAddressWizardFragment.java @@ -0,0 +1,525 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.fragment.send; + +import android.content.Context; +import android.nfc.NfcManager; +import android.os.Bundle; +import android.text.Editable; +import android.text.Html; +import android.text.InputType; +import android.text.Spanned; +import android.text.TextWatcher; +import android.util.Patterns; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; + +import com.google.android.material.textfield.TextInputLayout; +import com.m2049r.xmrwallet.R; +import com.m2049r.xmrwallet.data.BarcodeData; +import com.m2049r.xmrwallet.data.Crypto; +import com.m2049r.xmrwallet.data.TxData; +import com.m2049r.xmrwallet.data.TxDataBtc; +import com.m2049r.xmrwallet.data.UserNotes; +import com.m2049r.xmrwallet.model.PendingTransaction; +import com.m2049r.xmrwallet.model.Wallet; +import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.OpenAliasHelper; +import com.m2049r.xmrwallet.util.ServiceHelper; +import com.m2049r.xmrwallet.util.validator.BitcoinAddressType; +import com.m2049r.xmrwallet.util.validator.BitcoinAddressValidator; +import com.m2049r.xmrwallet.util.validator.EthAddressValidator; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import timber.log.Timber; + +public class SendAddressWizardFragment extends SendWizardFragment { + + static final int INTEGRATED_ADDRESS_LENGTH = 106; + + public static SendAddressWizardFragment newInstance(Listener listener) { + SendAddressWizardFragment instance = new SendAddressWizardFragment(); + instance.setSendListener(listener); + return instance; + } + + Listener sendListener; + + public void setSendListener(Listener listener) { + this.sendListener = listener; + } + + public interface Listener { + void setBarcodeData(BarcodeData data); + + BarcodeData getBarcodeData(); + + BarcodeData popBarcodeData(); + + void setMode(SendFragment.Mode mode); + + TxData getTxData(); + } + + private EditText etDummy; + private TextInputLayout etAddress; + private TextInputLayout etNotes; + private TextView tvXmrTo; + private TextView tvTor; + private Map ibCrypto; + final private Set possibleCryptos = new HashSet<>(); + private Crypto selectedCrypto = null; + + private boolean resolvingOA = false; + + OnScanListener onScanListener; + + public interface OnScanListener { + void onScan(); + } + + private Crypto getCryptoForButton(ImageButton button) { + for (Map.Entry entry : ibCrypto.entrySet()) { + if (entry.getValue() == button) return entry.getKey(); + } + return null; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + Timber.d("onCreateView() %s", (String.valueOf(savedInstanceState))); + + View view = inflater.inflate(R.layout.fragment_send_address, container, false); + + tvTor = view.findViewById(R.id.tvTor); + tvXmrTo = view.findViewById(R.id.tvXmrTo); + ibCrypto = new HashMap<>(); + for (Crypto crypto : Crypto.values()) { + final ImageButton button = view.findViewById(crypto.getButtonId()); + if (Helper.ALLOW_SHIFT || (crypto == Crypto.XMR)) { + ibCrypto.put(crypto, button); + button.setOnClickListener(v -> { + if (possibleCryptos.contains(crypto)) { + selectedCrypto = crypto; + updateCryptoButtons(false); + } else { + // show help what to do: + if (button.getId() != R.id.ibXMR) { + final String name = getResources().getStringArray(R.array.cryptos)[crypto.ordinal()]; + final String symbol = getCryptoForButton(button).getSymbol(); + tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto_help, name, symbol))); + tvXmrTo.setVisibility(View.VISIBLE); + } else { + tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto_help_xmr))); + tvXmrTo.setVisibility(View.VISIBLE); + tvTor.setVisibility(View.INVISIBLE); + } + } + }); + } else { + button.setImageResource(crypto.getIconDisabledId()); + button.setImageAlpha(128); + button.setEnabled(false); + } + } + if (!Helper.ALLOW_SHIFT) { + tvTor.setVisibility(View.VISIBLE); + } + updateCryptoButtons(true); + + etAddress = view.findViewById(R.id.etAddress); + etAddress.getEditText().setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + etAddress.getEditText().setOnEditorActionListener(new TextView.OnEditorActionListener() { + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + // ignore ENTER + return ((event != null) && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)); + } + }); + etAddress.getEditText().setOnFocusChangeListener((v, hasFocus) -> { + if (!hasFocus) { + String enteredAddress = etAddress.getEditText().getText().toString().trim(); + String dnsOA = dnsFromOpenAlias(enteredAddress); + Timber.d("OpenAlias is %s", dnsOA); + if (dnsOA != null) { + processOpenAlias(dnsOA); + } + } + }); + etAddress.getEditText().addTextChangedListener(new TextWatcher() { + @Override + public void afterTextChanged(Editable editable) { + Timber.d("AFTER: %s", editable.toString()); + etAddress.setError(null); + possibleCryptos.clear(); + selectedCrypto = null; + final String address = etAddress.getEditText().getText().toString(); + if (isIntegratedAddress(address)) { + Timber.d("isIntegratedAddress"); + possibleCryptos.add(Crypto.XMR); + selectedCrypto = Crypto.XMR; + etAddress.setError(getString(R.string.info_paymentid_integrated)); + sendListener.setMode(SendFragment.Mode.XMR); + } else if (isStandardAddress(address)) { + Timber.d("isStandardAddress"); + possibleCryptos.add(Crypto.XMR); + selectedCrypto = Crypto.XMR; + sendListener.setMode(SendFragment.Mode.XMR); + } + if (!Helper.ALLOW_SHIFT) return; + if ((selectedCrypto == null) && isEthAddress(address)) { + Timber.d("isEthAddress"); + possibleCryptos.add(Crypto.ETH); + selectedCrypto = Crypto.ETH; + tvXmrTo.setVisibility(View.VISIBLE); + sendListener.setMode(SendFragment.Mode.BTC); + } + if (possibleCryptos.isEmpty()) { + Timber.d("isBitcoinAddress"); + for (BitcoinAddressType type : BitcoinAddressType.values()) { + if (BitcoinAddressValidator.validate(address, type)) { + possibleCryptos.add(Crypto.valueOf(type.name())); + } + } + if (!possibleCryptos.isEmpty()) // found something in need of shifting! + sendListener.setMode(SendFragment.Mode.BTC); + if (possibleCryptos.size() == 1) { + selectedCrypto = (Crypto) possibleCryptos.toArray()[0]; + } + } + if (possibleCryptos.isEmpty()) { + Timber.d("other"); + tvXmrTo.setVisibility(View.INVISIBLE); + sendListener.setMode(SendFragment.Mode.XMR); + } + updateCryptoButtons(address.isEmpty()); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + }); + + final ImageButton bPasteAddress = view.findViewById(R.id.bPasteAddress); + bPasteAddress.setOnClickListener(v -> { + final String clip = Helper.getClipBoardText(getActivity()); + if (clip == null) return; + // clean it up + final String address = clip.replaceAll("( +)|(\\r?\\n?)", ""); + BarcodeData bc = BarcodeData.fromString(address); + if (bc != null) { + processScannedData(bc); + final EditText et = etAddress.getEditText(); + et.setSelection(et.getText().length()); + etAddress.requestFocus(); + } else { + Toast.makeText(getActivity(), getString(R.string.send_address_invalid), Toast.LENGTH_SHORT).show(); + } + }); + + etNotes = view.findViewById(R.id.etNotes); + etNotes.getEditText().setRawInputType(InputType.TYPE_CLASS_TEXT); + etNotes.getEditText(). + + setOnEditorActionListener((v, actionId, event) -> { + if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN)) + || (actionId == EditorInfo.IME_ACTION_DONE)) { + etDummy.requestFocus(); + return true; + } + return false; + }); + + final View cvScan = view.findViewById(R.id.bScan); + cvScan.setOnClickListener(v -> onScanListener.onScan()); + + etDummy = view.findViewById(R.id.etDummy); + etDummy.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + etDummy.requestFocus(); + + View tvNfc = view.findViewById(R.id.tvNfc); + NfcManager manager = (NfcManager) getContext().getSystemService(Context.NFC_SERVICE); + if ((manager != null) && (manager.getDefaultAdapter() != null)) + tvNfc.setVisibility(View.VISIBLE); + + return view; + } + + private void selectedCrypto(Crypto crypto) { + final ImageButton button = ibCrypto.get(crypto); + button.setImageResource(crypto.getIconEnabledId()); + button.setImageAlpha(255); + button.setEnabled(true); + } + + private void possibleCrypto(Crypto crypto) { + final ImageButton button = ibCrypto.get(crypto); + button.setImageResource(crypto.getIconDisabledId()); + button.setImageAlpha(255); + button.setEnabled(true); + } + + private void impossibleCrypto(Crypto crypto) { + final ImageButton button = ibCrypto.get(crypto); + button.setImageResource(crypto.getIconDisabledId()); + button.setImageAlpha(128); + button.setEnabled(true); + } + + private void updateCryptoButtons(boolean noAddress) { + if (!Helper.ALLOW_SHIFT) return; + for (Crypto crypto : Crypto.values()) { + if (crypto == selectedCrypto) { + selectedCrypto(crypto); + } else if (possibleCryptos.contains(crypto)) { + possibleCrypto(crypto); + } else { + impossibleCrypto(crypto); + } + } + if ((selectedCrypto != null) && (selectedCrypto != Crypto.XMR)) { + tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto, selectedCrypto.getSymbol()))); + tvXmrTo.setVisibility(View.VISIBLE); + } else if ((selectedCrypto == null) && (possibleCryptos.size() > 1)) { + tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto_ambiguous))); + tvXmrTo.setVisibility(View.VISIBLE); + } else { + tvXmrTo.setVisibility(View.INVISIBLE); + } + if (noAddress) { + selectedCrypto(Crypto.XMR); + } + } + + private void processOpenAlias(String dnsOA) { + if (resolvingOA) return; // already resolving - just wait + sendListener.popBarcodeData(); + if (dnsOA != null) { + resolvingOA = true; + etAddress.setError(getString(R.string.send_address_resolve_openalias)); + OpenAliasHelper.resolve(dnsOA, new OpenAliasHelper.OnResolvedListener() { + @Override + public void onResolved(Map dataMap) { + resolvingOA = false; + BarcodeData barcodeData = dataMap.get(Crypto.XMR); + if (barcodeData == null) barcodeData = dataMap.get(Crypto.BTC); + if (barcodeData != null) { + Timber.d("Security=%s, %s", barcodeData.security.toString(), barcodeData.address); + processScannedData(barcodeData); + } else { + etAddress.setError(getString(R.string.send_address_not_openalias)); + Timber.d("NO XMR OPENALIAS TXT FOUND"); + } + } + + @Override + public void onFailure() { + resolvingOA = false; + etAddress.setError(getString(R.string.send_address_not_openalias)); + Timber.e("OA FAILED"); + } + }); + } // else ignore + } + + private boolean checkAddressNoError() { + return selectedCrypto != null; + } + + private boolean checkAddress() { + boolean ok = checkAddressNoError(); + if (possibleCryptos.isEmpty()) { + etAddress.setError(getString(R.string.send_address_invalid)); + } else { + etAddress.setError(null); + } + return ok; + } + + private boolean isStandardAddress(String address) { + return Wallet.isAddressValid(address); + } + + private boolean isIntegratedAddress(String address) { + return (address.length() == INTEGRATED_ADDRESS_LENGTH) + && Wallet.isAddressValid(address); + } + + private boolean isBitcoinishAddress(String address) { + return BitcoinAddressValidator.validate(address, BitcoinAddressType.BTC) + || + BitcoinAddressValidator.validate(address, BitcoinAddressType.LTC) + || + BitcoinAddressValidator.validate(address, BitcoinAddressType.DASH); + } + + private boolean isEthAddress(String address) { + return EthAddressValidator.validate(address); + } + + private void shakeAddress() { + if (possibleCryptos.size() > 1) { // address ambiguous + for (Crypto crypto : Crypto.values()) { + if (possibleCryptos.contains(crypto)) { + ibCrypto.get(crypto).startAnimation(Helper.getShakeAnimation(getContext())); + } + } + } else { + etAddress.startAnimation(Helper.getShakeAnimation(getContext())); + } + } + + @Override + public boolean onValidateFields() { + if (!checkAddressNoError()) { + shakeAddress(); + String enteredAddress = etAddress.getEditText().getText().toString().trim(); + String dnsOA = dnsFromOpenAlias(enteredAddress); + Timber.d("OpenAlias is %s", dnsOA); + if (dnsOA != null) { + processOpenAlias(dnsOA); + } + return false; + } + + if (sendListener != null) { + TxData txData = sendListener.getTxData(); + if (txData instanceof TxDataBtc) { + ((TxDataBtc) txData).setBtcAddress(etAddress.getEditText().getText().toString()); + ((TxDataBtc) txData).setBtcSymbol(selectedCrypto.getSymbol()); + txData.setDestinationAddress(null); + ServiceHelper.ASSET = selectedCrypto.getSymbol().toLowerCase(); + } else { + txData.setDestinationAddress(etAddress.getEditText().getText().toString()); + ServiceHelper.ASSET = null; + } + txData.setUserNotes(new UserNotes(etNotes.getEditText().getText().toString())); + txData.setPriority(PendingTransaction.Priority.Priority_Default); + txData.setMixin(SendFragment.MIXIN); + } + return true; + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + if (context instanceof OnScanListener) { + onScanListener = (OnScanListener) context; + } else { + throw new ClassCastException(context.toString() + + " must implement ScanListener"); + } + } + + // QR Scan Stuff + + @Override + public void onResume() { + super.onResume(); + Timber.d("onResume"); + processScannedData(); + } + + public void processScannedData(BarcodeData barcodeData) { + sendListener.setBarcodeData(barcodeData); + if (isResumed()) + processScannedData(); + } + + public void processScannedData() { + BarcodeData barcodeData = sendListener.getBarcodeData(); + if (barcodeData != null) { + Timber.d("GOT DATA"); + if (!Helper.ALLOW_SHIFT && (barcodeData.asset != Crypto.XMR)) { + Timber.d("BUT ONLY XMR SUPPORTED"); + barcodeData = null; + sendListener.setBarcodeData(barcodeData); + return; + } + if (barcodeData.address != null) { + etAddress.getEditText().setText(barcodeData.address); + possibleCryptos.clear(); + selectedCrypto = null; + if (barcodeData.isAmbiguous()) { + possibleCryptos.addAll(barcodeData.ambiguousAssets); + } else { + possibleCryptos.add(barcodeData.asset); + selectedCrypto = barcodeData.asset; + } + if (Helper.ALLOW_SHIFT) + updateCryptoButtons(false); + if (checkAddress()) { + if (barcodeData.security == BarcodeData.Security.OA_NO_DNSSEC) + etAddress.setError(getString(R.string.send_address_no_dnssec)); + else if (barcodeData.security == BarcodeData.Security.OA_DNSSEC) + etAddress.setError(getString(R.string.send_address_openalias)); + } + } else { + etAddress.getEditText().getText().clear(); + etAddress.setError(null); + } + + String scannedNotes = barcodeData.addressName; + if (scannedNotes == null) { + scannedNotes = barcodeData.description; + } else if (barcodeData.description != null) { + scannedNotes = scannedNotes + ": " + barcodeData.description; + } + if (scannedNotes != null) { + etNotes.getEditText().setText(scannedNotes); + } else { + etNotes.getEditText().getText().clear(); + etNotes.setError(null); + } + } else + Timber.d("barcodeData=null"); + } + + @Override + public void onResumeFragment() { + super.onResumeFragment(); + Timber.d("onResumeFragment()"); + etDummy.requestFocus(); + } + + String dnsFromOpenAlias(String openalias) { + Timber.d("checking openalias candidate %s", openalias); + if (Patterns.DOMAIN_NAME.matcher(openalias).matches()) return openalias; + if (Patterns.EMAIL_ADDRESS.matcher(openalias).matches()) { + openalias = openalias.replaceFirst("@", "."); + if (Patterns.DOMAIN_NAME.matcher(openalias).matches()) return openalias; + } + return null; // not an openalias + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAmountWizardFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAmountWizardFragment.java new file mode 100644 index 0000000..12edaf6 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAmountWizardFragment.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.fragment.send; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.TextView; + +import com.m2049r.xmrwallet.R; +import com.m2049r.xmrwallet.data.BarcodeData; +import com.m2049r.xmrwallet.data.TxData; +import com.m2049r.xmrwallet.model.Wallet; +import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.widget.ExchangeEditText; + +import timber.log.Timber; + +public class SendAmountWizardFragment extends SendWizardFragment { + + public static SendAmountWizardFragment newInstance(Listener listener) { + SendAmountWizardFragment instance = new SendAmountWizardFragment(); + instance.setSendListener(listener); + return instance; + } + + Listener sendListener; + + public void setSendListener(Listener listener) { + this.sendListener = listener; + } + + interface Listener { + SendFragment.Listener getActivityCallback(); + + TxData getTxData(); + + BarcodeData popBarcodeData(); + } + + private TextView tvFunds; + private ExchangeEditText etAmount; + private View rlSweep; + private ImageButton ibSweep; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + Timber.d("onCreateView() %s", (String.valueOf(savedInstanceState))); + + sendListener = (Listener) getParentFragment(); + + View view = inflater.inflate(R.layout.fragment_send_amount, container, false); + + tvFunds = view.findViewById(R.id.tvFunds); + etAmount = view.findViewById(R.id.etAmount); + rlSweep = view.findViewById(R.id.rlSweep); + + view.findViewById(R.id.ivSweep).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + sweepAll(false); + } + }); + + ibSweep = view.findViewById(R.id.ibSweep); + + ibSweep.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + sweepAll(true); + } + }); + + etAmount.requestFocus(); + return view; + } + + private boolean spendAllMode = false; + + private void sweepAll(boolean spendAllMode) { + if (spendAllMode) { + ibSweep.setVisibility(View.INVISIBLE); + etAmount.setVisibility(View.GONE); + rlSweep.setVisibility(View.VISIBLE); + } else { + ibSweep.setVisibility(View.VISIBLE); + etAmount.setVisibility(View.VISIBLE); + rlSweep.setVisibility(View.GONE); + } + this.spendAllMode = spendAllMode; + } + + @Override + public boolean onValidateFields() { + if (spendAllMode) { + if (sendListener != null) { + sendListener.getTxData().setAmount(Wallet.SWEEP_ALL); + } + } else { + if (!etAmount.validate(maxFunds, 0)) { + return false; + } + + if (sendListener != null) { + String xmr = etAmount.getNativeAmount(); + if (xmr != null) { + sendListener.getTxData().setAmount(Wallet.getAmountFromString(xmr)); + } else { + sendListener.getTxData().setAmount(0L); + } + } + } + return true; + } + + double maxFunds = 0; + + @Override + public void onResumeFragment() { + super.onResumeFragment(); + Timber.d("onResumeFragment()"); + Helper.showKeyboard(getActivity()); + final long funds = getTotalFunds(); + maxFunds = 1.0 * funds / Helper.ONE_XMR; + if (!sendListener.getActivityCallback().isStreetMode()) { + tvFunds.setText(getString(R.string.send_available, + Wallet.getDisplayAmount(funds))); + } else { + tvFunds.setText(getString(R.string.send_available, + getString(R.string.unknown_amount))); + } + final BarcodeData data = sendListener.popBarcodeData(); + if ((data != null) && (data.amount != null)) { + etAmount.setAmount(data.amount); + } + } + + long getTotalFunds() { + return sendListener.getActivityCallback().getTotalFunds(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcAmountWizardFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcAmountWizardFragment.java new file mode 100644 index 0000000..72b2e98 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcAmountWizardFragment.java @@ -0,0 +1,263 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.fragment.send; + +import android.os.Bundle; +import android.text.Html; +import android.text.Spanned; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.m2049r.xmrwallet.R; +import com.m2049r.xmrwallet.data.BarcodeData; +import com.m2049r.xmrwallet.data.TxDataBtc; +import com.m2049r.xmrwallet.model.Wallet; +import com.m2049r.xmrwallet.service.shift.ShiftCallback; +import com.m2049r.xmrwallet.service.shift.ShiftError; +import com.m2049r.xmrwallet.service.shift.ShiftException; +import com.m2049r.xmrwallet.service.shift.sideshift.api.QueryOrderParameters; +import com.m2049r.xmrwallet.service.shift.sideshift.api.SideShiftApi; +import com.m2049r.xmrwallet.service.shift.sideshift.network.SideShiftApiImpl; +import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.ServiceHelper; +import com.m2049r.xmrwallet.widget.ExchangeOtherEditText; +import com.m2049r.xmrwallet.widget.SendProgressView; + +import java.text.NumberFormat; +import java.util.Locale; + +import timber.log.Timber; + +public class SendBtcAmountWizardFragment extends SendWizardFragment { + + public static SendBtcAmountWizardFragment newInstance(SendAmountWizardFragment.Listener listener) { + SendBtcAmountWizardFragment instance = new SendBtcAmountWizardFragment(); + instance.setSendListener(listener); + return instance; + } + + SendAmountWizardFragment.Listener sendListener; + + public SendBtcAmountWizardFragment setSendListener(SendAmountWizardFragment.Listener listener) { + this.sendListener = listener; + return this; + } + + private TextView tvFunds; + private ExchangeOtherEditText etAmount; + + private TextView tvXmrToParms; + private SendProgressView evParams; + private View llXmrToParms; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + Timber.d("onCreateView() %s", (String.valueOf(savedInstanceState))); + + sendListener = (SendAmountWizardFragment.Listener) getParentFragment(); + + View view = inflater.inflate(R.layout.fragment_send_btc_amount, container, false); + + tvFunds = view.findViewById(R.id.tvFunds); + + evParams = view.findViewById(R.id.evXmrToParms); + llXmrToParms = view.findViewById(R.id.llXmrToParms); + + tvXmrToParms = view.findViewById(R.id.tvXmrToParms); + + etAmount = view.findViewById(R.id.etAmount); + etAmount.requestFocus(); + + return view; + } + + @Override + public boolean onValidateFields() { + Timber.i(maxBtc + "/" + minBtc); + if (!etAmount.validate(maxBtc, minBtc)) { + return false; + } + if (orderParameters == null) { + return false; // this should never happen + } + if (sendListener != null) { + TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData(); + String btcString = etAmount.getNativeAmount(); + if (btcString != null) { + try { + double btc = Double.parseDouble(btcString); + Timber.d("setBtcAmount %f", btc); + txDataBtc.setBtcAmount(btc); + txDataBtc.setAmount(btc / orderParameters.getPrice()); + } catch (NumberFormatException ex) { + Timber.d(ex.getLocalizedMessage()); + txDataBtc.setBtcAmount(0); + } + } else { + txDataBtc.setBtcAmount(0); + } + } + return true; + } + + double maxBtc = 0; + double minBtc = 0; + + @Override + public void onPauseFragment() { + llXmrToParms.setVisibility(View.INVISIBLE); + } + + @Override + public void onResumeFragment() { + super.onResumeFragment(); + Timber.d("onResumeFragment()"); + final String btcSymbol = ((TxDataBtc) sendListener.getTxData()).getBtcSymbol(); + if (!btcSymbol.toLowerCase().equals(ServiceHelper.ASSET)) + throw new IllegalStateException("Asset Symbol is wrong!"); + final long funds = getTotalFunds(); + if (!sendListener.getActivityCallback().isStreetMode()) { + tvFunds.setText(getString(R.string.send_available, + Wallet.getDisplayAmount(funds))); + //TODO + } else { + tvFunds.setText(getString(R.string.send_available, + getString(R.string.unknown_amount))); + } + etAmount.setAmount(""); + final BarcodeData data = sendListener.popBarcodeData(); + if (data != null) { + if (data.amount != null) { + etAmount.setAmount(data.amount); + } + } + etAmount.setBaseCurrency(btcSymbol); + callXmrTo(); + } + + long getTotalFunds() { + return sendListener.getActivityCallback().getTotalFunds(); + } + + private QueryOrderParameters orderParameters = null; + + private void processOrderParms(final QueryOrderParameters orderParameters) { + this.orderParameters = orderParameters; + getView().post(() -> { + final double price = orderParameters.getPrice(); + etAmount.setExchangeRate(1 / price); + maxBtc = price * orderParameters.getUpperLimit(); + minBtc = price * orderParameters.getLowerLimit(); + Timber.d("minBtc=%f / maxBtc=%f", minBtc, maxBtc); + NumberFormat df = NumberFormat.getInstance(Locale.US); + df.setMaximumFractionDigits(6); + String min = df.format(minBtc); + String max = df.format(maxBtc); + String rate = df.format(price); + final TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData(); + Spanned xmrParmText = Html.fromHtml(getString(R.string.info_send_xmrto_parms, + min, max, rate, txDataBtc.getBtcSymbol())); + tvXmrToParms.setText(xmrParmText); + + final long funds = getTotalFunds(); + double availableXmr = 1.0 * funds / Helper.ONE_XMR; + + String availBtcString; + String availXmrString; + if (!sendListener.getActivityCallback().isStreetMode()) { + availBtcString = df.format(availableXmr * price); + availXmrString = df.format(availableXmr); + } else { + availBtcString = getString(R.string.unknown_amount); + availXmrString = availBtcString; + } + tvFunds.setText(getString(R.string.send_available_btc, + availXmrString, + availBtcString, + ((TxDataBtc) sendListener.getTxData()).getBtcSymbol())); + llXmrToParms.setVisibility(View.VISIBLE); + evParams.hideProgress(); + }); + } + + private void processOrderParmsError(final Exception ex) { + etAmount.setExchangeRate(0); + orderParameters = null; + maxBtc = 0; + minBtc = 0; + Timber.e(ex); + getView().post(() -> { + if (ex instanceof ShiftException) { + ShiftException xmrEx = (ShiftException) ex; + ShiftError xmrErr = xmrEx.getError(); + if (xmrErr != null) { + if (xmrErr.isRetryable()) { + evParams.showMessage(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(), + getString(R.string.text_retry)); + evParams.setOnClickListener(v -> { + evParams.setOnClickListener(null); + callXmrTo(); + }); + } else { + evParams.showMessage(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(), + getString(R.string.text_noretry)); + } + } else { + evParams.showMessage(getString(R.string.label_generic_xmrto_error), + getString(R.string.text_generic_xmrto_error, xmrEx.getCode()), + getString(R.string.text_noretry)); + } + } else { + evParams.showMessage(getString(R.string.label_generic_xmrto_error), + ex.getLocalizedMessage(), + getString(R.string.text_noretry)); + } + }); + } + + private void callXmrTo() { + evParams.showProgress(getString(R.string.label_send_progress_queryparms)); + getXmrToApi().queryOrderParameters(new ShiftCallback() { + @Override + public void onSuccess(final QueryOrderParameters orderParameters) { + processOrderParms(orderParameters); + } + + @Override + public void onError(final Exception e) { + processOrderParmsError(e); + } + }); + } + + private SideShiftApi xmrToApi = null; + + private SideShiftApi getXmrToApi() { + if (xmrToApi == null) { + synchronized (this) { + if (xmrToApi == null) { + xmrToApi = new SideShiftApiImpl(ServiceHelper.getXmrToBaseUrl()); + } + } + } + return xmrToApi; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcConfirmWizardFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcConfirmWizardFragment.java new file mode 100644 index 0000000..66a66c2 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcConfirmWizardFragment.java @@ -0,0 +1,551 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.fragment.send; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +import com.m2049r.xmrwallet.R; +import com.m2049r.xmrwallet.data.TxData; +import com.m2049r.xmrwallet.data.TxDataBtc; +import com.m2049r.xmrwallet.model.PendingTransaction; +import com.m2049r.xmrwallet.model.Wallet; +import com.m2049r.xmrwallet.service.shift.ShiftCallback; +import com.m2049r.xmrwallet.service.shift.ShiftError; +import com.m2049r.xmrwallet.service.shift.ShiftException; +import com.m2049r.xmrwallet.service.shift.sideshift.api.CreateOrder; +import com.m2049r.xmrwallet.service.shift.sideshift.api.RequestQuote; +import com.m2049r.xmrwallet.service.shift.sideshift.api.SideShiftApi; +import com.m2049r.xmrwallet.service.shift.sideshift.network.SideShiftApiImpl; +import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.ServiceHelper; +import com.m2049r.xmrwallet.widget.SendProgressView; + +import java.text.NumberFormat; +import java.util.Locale; + +import timber.log.Timber; + +public class SendBtcConfirmWizardFragment extends SendWizardFragment implements SendConfirm { + public static SendBtcConfirmWizardFragment newInstance(SendConfirmWizardFragment.Listener listener) { + SendBtcConfirmWizardFragment instance = new SendBtcConfirmWizardFragment(); + instance.setSendListener(listener); + return instance; + } + + SendConfirmWizardFragment.Listener sendListener; + + public void setSendListener(SendConfirmWizardFragment.Listener listener) { + this.sendListener = listener; + } + + private View llStageA; + private SendProgressView evStageA; + private View llStageB; + private SendProgressView evStageB; + private View llStageC; + private SendProgressView evStageC; + private TextView tvTxBtcAmount; + private TextView tvTxBtcRate; + private TextView tvTxBtcAddress; + private TextView tvTxBtcAddressLabel; + private TextView tvTxXmrToKey; + private TextView tvTxFee; + private TextView tvTxTotal; + private View llConfirmSend; + private Button bSend; + private View pbProgressSend; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + Timber.d("onCreateView(%s)", (String.valueOf(savedInstanceState))); + + View view = inflater.inflate( + R.layout.fragment_send_btc_confirm, container, false); + + tvTxBtcAddress = view.findViewById(R.id.tvTxBtcAddress); + tvTxBtcAddressLabel = view.findViewById(R.id.tvTxBtcAddressLabel); + tvTxBtcAmount = view.findViewById(R.id.tvTxBtcAmount); + tvTxBtcRate = view.findViewById(R.id.tvTxBtcRate); + tvTxXmrToKey = view.findViewById(R.id.tvTxXmrToKey); + + tvTxFee = view.findViewById(R.id.tvTxFee); + tvTxTotal = view.findViewById(R.id.tvTxTotal); + + llStageA = view.findViewById(R.id.llStageA); + evStageA = view.findViewById(R.id.evStageA); + llStageB = view.findViewById(R.id.llStageB); + evStageB = view.findViewById(R.id.evStageB); + llStageC = view.findViewById(R.id.llStageC); + evStageC = view.findViewById(R.id.evStageC); + + tvTxXmrToKey.setOnClickListener(v -> { + Helper.clipBoardCopy(getActivity(), getString(R.string.label_copy_xmrtokey), tvTxXmrToKey.getText().toString()); + Toast.makeText(getActivity(), getString(R.string.message_copy_xmrtokey), Toast.LENGTH_SHORT).show(); + }); + + llConfirmSend = view.findViewById(R.id.llConfirmSend); + pbProgressSend = view.findViewById(R.id.pbProgressSend); + + bSend = view.findViewById(R.id.bSend); + bSend.setEnabled(false); + + bSend.setOnClickListener(v -> { + Timber.d("bSend.setOnClickListener"); + bSend.setEnabled(false); + preSend(); + }); + + return view; + } + + int inProgress = 0; + final static int STAGE_X = 0; + final static int STAGE_A = 1; + final static int STAGE_B = 2; + final static int STAGE_C = 3; + + private void showProgress(int stage, String progressText) { + Timber.d("showProgress(%d)", stage); + inProgress = stage; + switch (stage) { + case STAGE_A: + evStageA.showProgress(progressText); + break; + case STAGE_B: + evStageB.showProgress(progressText); + break; + case STAGE_C: + evStageC.showProgress(progressText); + break; + default: + throw new IllegalStateException("unknown stage " + stage); + } + } + + public void hideProgress() { + Timber.d("hideProgress(%d)", inProgress); + switch (inProgress) { + case STAGE_A: + evStageA.hideProgress(); + llStageA.setVisibility(View.VISIBLE); + break; + case STAGE_B: + evStageB.hideProgress(); + llStageB.setVisibility(View.VISIBLE); + break; + case STAGE_C: + evStageC.hideProgress(); + llStageC.setVisibility(View.VISIBLE); + break; + default: + throw new IllegalStateException("unknown stage " + inProgress); + } + inProgress = STAGE_X; + } + + public void showStageError(String code, String message, String solution) { + switch (inProgress) { + case STAGE_A: + evStageA.showMessage(code, message, solution); + break; + case STAGE_B: + evStageB.showMessage(code, message, solution); + break; + case STAGE_C: + evStageC.showMessage(code, message, solution); + break; + default: + throw new IllegalStateException("unknown stage"); + } + inProgress = STAGE_X; + } + + PendingTransaction pendingTransaction = null; + + void send() { + Timber.d("SEND @%d", sendCountdown); + if (sendCountdown <= 0) { + Timber.i("User waited too long in password dialog."); + Toast.makeText(getContext(), getString(R.string.send_xmrto_timeout), Toast.LENGTH_SHORT).show(); + return; + } + sendListener.getTxData().getUserNotes().setXmrtoOrder(xmrtoOrder); // note the transaction in the TX notes + ((TxDataBtc) sendListener.getTxData()).setXmrtoOrderId(xmrtoOrder.getOrderId()); // remember the order id for later + // TODO make method in TxDataBtc to set both of the above in one go + sendListener.commitTransaction(); + getActivity().runOnUiThread(() -> pbProgressSend.setVisibility(View.VISIBLE)); + } + + @Override + public void sendFailed(String error) { + pbProgressSend.setVisibility(View.INVISIBLE); + Toast.makeText(getContext(), getString(R.string.status_transaction_failed, error), Toast.LENGTH_LONG).show(); + } + + @Override + // callback from wallet when PendingTransaction created (started by prepareSend() here + public void transactionCreated(final String txTag, final PendingTransaction pendingTransaction) { + if (isResumed + && (inProgress == STAGE_C) + && (xmrtoOrder != null) + && (xmrtoOrder.getOrderId().equals(txTag))) { + this.pendingTransaction = pendingTransaction; + getView().post(() -> { + hideProgress(); + tvTxFee.setText(Wallet.getDisplayAmount(pendingTransaction.getFee())); + tvTxTotal.setText(Wallet.getDisplayAmount( + pendingTransaction.getFee() + pendingTransaction.getAmount())); + updateSendButton(); + }); + } else { + this.pendingTransaction = null; + sendListener.disposeTransaction(); + } + } + + @Override + public void createTransactionFailed(String errorText) { + Timber.e("CREATE TX FAILED"); + if (pendingTransaction != null) { + throw new IllegalStateException("pendingTransaction is not null"); + } + showStageError(getString(R.string.send_create_tx_error_title), + errorText, + getString(R.string.text_noretry_monero)); + } + + @Override + public boolean onValidateFields() { + return true; + } + + private boolean isResumed = false; + + @Override + public void onPauseFragment() { + isResumed = false; + stopSendTimer(); + sendListener.disposeTransaction(); + pendingTransaction = null; + inProgress = STAGE_X; + updateSendButton(); + super.onPauseFragment(); + } + + @Override + public void onResumeFragment() { + super.onResumeFragment(); + Timber.d("onResumeFragment()"); + if (sendListener.getMode() != SendFragment.Mode.BTC) { + throw new IllegalStateException("Mode is not BTC!"); + } + if (!((TxDataBtc) sendListener.getTxData()).getBtcSymbol().toLowerCase().equals(ServiceHelper.ASSET)) + throw new IllegalStateException("Asset Symbol is wrong!"); + Helper.hideKeyboard(getActivity()); + llStageA.setVisibility(View.INVISIBLE); + evStageA.hideProgress(); + llStageB.setVisibility(View.INVISIBLE); + evStageB.hideProgress(); + llStageC.setVisibility(View.INVISIBLE); + evStageC.hideProgress(); + isResumed = true; + if ((pendingTransaction == null) && (inProgress == STAGE_X)) { + stageA(); + } // otherwise just sit there blank + // TODO: don't sit there blank - can this happen? should we just die? + } + + private int sendCountdown = 0; + private static final int XMRTO_COUNTDOWN_STEP = 1; // 1 second + + Runnable updateRunnable = null; + + void startSendTimer(int timeout) { + Timber.d("startSendTimer()"); + sendCountdown = timeout; + updateRunnable = new Runnable() { + @Override + public void run() { + if (!isAdded()) + return; + Timber.d("updateTimer()"); + if (sendCountdown <= 0) { + bSend.setEnabled(false); + sendCountdown = 0; + Toast.makeText(getContext(), getString(R.string.send_xmrto_timeout), Toast.LENGTH_SHORT).show(); + } + int minutes = sendCountdown / 60; + int seconds = sendCountdown % 60; + String t = String.format("%d:%02d", minutes, seconds); + bSend.setText(getString(R.string.send_send_timed_label, t)); + if (sendCountdown > 0) { + sendCountdown -= XMRTO_COUNTDOWN_STEP; + getView().postDelayed(this, XMRTO_COUNTDOWN_STEP * 1000); + } + } + }; + getView().post(updateRunnable); + } + + void stopSendTimer() { + getView().removeCallbacks(updateRunnable); + } + + void updateSendButton() { + Timber.d("updateSendButton()"); + if (pendingTransaction != null) { + llConfirmSend.setVisibility(View.VISIBLE); + bSend.setEnabled(sendCountdown > 0); + } else { + llConfirmSend.setVisibility(View.GONE); + bSend.setEnabled(false); + } + } + + public void preSend() { + Helper.promptPassword(getContext(), getActivityCallback().getWalletName(), false, new Helper.PasswordAction() { + @Override + public void act(String walletName, String password, boolean fingerprintUsed) { + send(); + } + + public void fail(String walletName) { + getActivity().runOnUiThread(() -> { + bSend.setEnabled(sendCountdown > 0); // allow to try again + }); + } + }); + } + + // creates a pending transaction and calls us back with transactionCreated() + // or createTransactionFailed() + void prepareSend() { + if (!isResumed) return; + if ((xmrtoOrder == null)) { + throw new IllegalStateException("xmrtoOrder is null"); + } + showProgress(3, getString(R.string.label_send_progress_create_tx)); + final TxData txData = sendListener.getTxData(); + txData.setDestinationAddress(xmrtoOrder.getXmrAddress()); + txData.setAmount(xmrtoOrder.getXmrAmount()); + getActivityCallback().onPrepareSend(xmrtoOrder.getOrderId(), txData); + } + + SendFragment.Listener getActivityCallback() { + return sendListener.getActivityCallback(); + } + + private RequestQuote xmrtoQuote = null; + + private void processStageA(final RequestQuote requestQuote) { + Timber.d("processCreateOrder %s", requestQuote.getId()); + TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData(); + // verify the BTC amount is correct + if (requestQuote.getBtcAmount() != txDataBtc.getBtcAmount()) { + Timber.d("Failed to get quote"); + getView().post(() -> showStageError(ShiftError.Error.SERVICE.toString(), + getString(R.string.shift_noquote), + getString(R.string.shift_checkamount))); + return; // just stop for now + } + xmrtoQuote = requestQuote; + txDataBtc.setAmount(xmrtoQuote.getXmrAmount()); + getView().post(() -> { + // show data from the actual quote as that is what is used to + NumberFormat df = NumberFormat.getInstance(Locale.US); + df.setMaximumFractionDigits(12); + final String btcAmount = df.format(xmrtoQuote.getBtcAmount()); + final String xmrAmountTotal = df.format(xmrtoQuote.getXmrAmount()); + tvTxBtcAmount.setText(getString(R.string.text_send_btc_amount, + btcAmount, xmrAmountTotal, txDataBtc.getBtcSymbol())); + final String xmrPriceBtc = df.format(xmrtoQuote.getPrice()); + tvTxBtcRate.setText(getString(R.string.text_send_btc_rate, xmrPriceBtc, txDataBtc.getBtcSymbol())); + hideProgress(); + }); + stageB(requestQuote.getId()); + } + + private void processStageAError(final Exception ex) { + Timber.e("processStageAError %s", ex.getLocalizedMessage()); + getView().post(() -> { + if (ex instanceof ShiftException) { + ShiftException xmrEx = (ShiftException) ex; + ShiftError xmrErr = xmrEx.getError(); + if (xmrErr != null) { + if (xmrErr.isRetryable()) { + showStageError(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(), + getString(R.string.text_retry)); + evStageA.setOnClickListener(v -> { + evStageA.setOnClickListener(null); + stageA(); + }); + } else { + showStageError(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(), + getString(R.string.text_noretry)); + } + } else { + showStageError(getString(R.string.label_generic_xmrto_error), + getString(R.string.text_generic_xmrto_error, xmrEx.getCode()), + getString(R.string.text_noretry)); + } + } else { + evStageA.showMessage(getString(R.string.label_generic_xmrto_error), + ex.getLocalizedMessage(), + getString(R.string.text_noretry)); + } + }); + } + + private void stageA() { + if (!isResumed) return; + Timber.d("Request Quote"); + xmrtoQuote = null; + xmrtoOrder = null; + showProgress(1, getString(R.string.label_send_progress_xmrto_create)); + TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData(); + + ShiftCallback callback = new ShiftCallback() { + @Override + public void onSuccess(RequestQuote requestQuote) { + if (!isResumed) return; + if (xmrtoQuote != null) { + Timber.w("another ongoing request quote request"); + return; + } + processStageA(requestQuote); + } + + @Override + public void onError(Exception ex) { + if (!isResumed) return; + if (xmrtoQuote != null) { + Timber.w("another ongoing request quote request"); + return; + } + processStageAError(ex); + } + }; + + getXmrToApi().requestQuote(txDataBtc.getBtcAmount(), callback); + } + + private CreateOrder xmrtoOrder = null; + + private void processStageB(final CreateOrder order) { + Timber.d("processCreateOrder %s for %s", order.getOrderId(), order.getQuoteId()); + TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData(); + // verify amount & destination + if ((order.getBtcAmount() != txDataBtc.getBtcAmount()) + || (!txDataBtc.validateAddress(order.getBtcAddress()))) { + throw new IllegalStateException("Order does not fulfill quote!"); // something is terribly wrong - die + } + xmrtoOrder = order; + getView().post(() -> { + tvTxXmrToKey.setText(order.getOrderId()); + tvTxBtcAddress.setText(order.getBtcAddress()); + tvTxBtcAddressLabel.setText(getString(R.string.label_send_btc_address, txDataBtc.getBtcSymbol())); + hideProgress(); + Timber.d("Expires @ %s", order.getExpiresAt().toString()); + final int timeout = (int) (order.getExpiresAt().getTime() - order.getCreatedAt().getTime()) / 1000 - 60; // -1 minute buffer + startSendTimer(timeout); + prepareSend(); + }); + } + + private void processStageBError(final Exception ex) { + Timber.e("processCreateOrderError %s", ex.getLocalizedMessage()); + getView().post(() -> { + if (ex instanceof ShiftException) { + ShiftException xmrEx = (ShiftException) ex; + ShiftError xmrErr = xmrEx.getError(); + if (xmrErr != null) { + if (xmrErr.isRetryable()) { + showStageError(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(), + getString(R.string.text_retry)); + evStageB.setOnClickListener(v -> { + evStageB.setOnClickListener(null); + stageB(xmrtoOrder.getOrderId()); + }); + } else { + showStageError(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(), + getString(R.string.text_noretry)); + } + } else { + showStageError(getString(R.string.label_generic_xmrto_error), + getString(R.string.text_generic_xmrto_error, xmrEx.getCode()), + getString(R.string.text_noretry)); + } + } else { + evStageB.showMessage(getString(R.string.label_generic_xmrto_error), + ex.getLocalizedMessage(), + getString(R.string.text_noretry)); + } + }); + } + + private void stageB(final String quoteId) { + Timber.d("createOrder(%s)", quoteId); + if (!isResumed) return; + final String btcAddress = ((TxDataBtc) sendListener.getTxData()).getBtcAddress(); + getView().post(() -> { + xmrtoOrder = null; + showProgress(2, getString(R.string.label_send_progress_xmrto_query)); + getXmrToApi().createOrder(quoteId, btcAddress, new ShiftCallback() { + @Override + public void onSuccess(CreateOrder order) { + if (!isResumed) return; + if (xmrtoQuote == null) return; + if (!order.getQuoteId().equals(xmrtoQuote.getId())) { + Timber.d("Quote ID does not match"); + // ignore (we got a response to a stale request) + return; + } + if (xmrtoOrder != null) + throw new IllegalStateException("xmrtoOrder must be null here!"); + processStageB(order); + } + + @Override + public void onError(Exception ex) { + if (!isResumed) return; + processStageBError(ex); + } + }); + }); + } + + private SideShiftApi xmrToApi = null; + + private SideShiftApi getXmrToApi() { + if (xmrToApi == null) { + synchronized (this) { + if (xmrToApi == null) { + xmrToApi = new SideShiftApiImpl(ServiceHelper.getXmrToBaseUrl()); + } + } + } + return xmrToApi; + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcSuccessWizardFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcSuccessWizardFragment.java new file mode 100644 index 0000000..41c13db --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcSuccessWizardFragment.java @@ -0,0 +1,262 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.fragment.send; + +import android.content.Intent; +import android.graphics.Paint; +import android.net.Uri; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import com.m2049r.xmrwallet.R; +import com.m2049r.xmrwallet.data.Crypto; +import com.m2049r.xmrwallet.data.PendingTx; +import com.m2049r.xmrwallet.data.TxDataBtc; +import com.m2049r.xmrwallet.service.shift.ShiftCallback; +import com.m2049r.xmrwallet.service.shift.ShiftException; +import com.m2049r.xmrwallet.service.shift.sideshift.api.QueryOrderStatus; +import com.m2049r.xmrwallet.service.shift.sideshift.api.SideShiftApi; +import com.m2049r.xmrwallet.service.shift.sideshift.network.SideShiftApiImpl; +import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.ServiceHelper; +import com.m2049r.xmrwallet.util.ThemeHelper; + +import java.text.NumberFormat; +import java.util.Locale; + +import timber.log.Timber; + +public class SendBtcSuccessWizardFragment extends SendWizardFragment { + + public static SendBtcSuccessWizardFragment newInstance(SendSuccessWizardFragment.Listener listener) { + SendBtcSuccessWizardFragment instance = new SendBtcSuccessWizardFragment(); + instance.setSendListener(listener); + return instance; + } + + SendSuccessWizardFragment.Listener sendListener; + + public void setSendListener(SendSuccessWizardFragment.Listener listener) { + this.sendListener = listener; + } + + ImageButton bCopyTxId; + private TextView tvTxId; + private TextView tvTxAddress; + private TextView tvTxAmount; + private TextView tvTxFee; + private TextView tvXmrToAmount; + private ImageView ivXmrToIcon; + private TextView tvXmrToStatus; + private ImageView ivXmrToStatus; + private ImageView ivXmrToStatusBig; + private ProgressBar pbXmrto; + private TextView tvTxXmrToKey; + private TextView tvXmrToSupport; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + Timber.d("onCreateView() %s", (String.valueOf(savedInstanceState))); + + View view = inflater.inflate( + R.layout.fragment_send_btc_success, container, false); + + bCopyTxId = view.findViewById(R.id.bCopyTxId); + bCopyTxId.setEnabled(false); + bCopyTxId.setOnClickListener(v -> { + Helper.clipBoardCopy(getActivity(), getString(R.string.label_send_txid), tvTxId.getText().toString()); + Toast.makeText(getActivity(), getString(R.string.message_copy_txid), Toast.LENGTH_SHORT).show(); + }); + + tvXmrToAmount = view.findViewById(R.id.tvXmrToAmount); + ivXmrToIcon = view.findViewById(R.id.ivXmrToIcon); + tvXmrToStatus = view.findViewById(R.id.tvXmrToStatus); + ivXmrToStatus = view.findViewById(R.id.ivXmrToStatus); + ivXmrToStatusBig = view.findViewById(R.id.ivXmrToStatusBig); + + tvTxId = view.findViewById(R.id.tvTxId); + tvTxAddress = view.findViewById(R.id.tvTxAddress); + tvTxAmount = view.findViewById(R.id.tvTxAmount); + tvTxFee = view.findViewById(R.id.tvTxFee); + + pbXmrto = view.findViewById(R.id.pbXmrto); + pbXmrto.getIndeterminateDrawable().setColorFilter(0x61000000, android.graphics.PorterDuff.Mode.MULTIPLY); + + tvTxXmrToKey = view.findViewById(R.id.tvTxXmrToKey); + tvTxXmrToKey.setOnClickListener(v -> { + Helper.clipBoardCopy(getActivity(), getString(R.string.label_copy_xmrtokey), tvTxXmrToKey.getText().toString()); + Toast.makeText(getActivity(), getString(R.string.message_copy_xmrtokey), Toast.LENGTH_SHORT).show(); + }); + + tvXmrToSupport = view.findViewById(R.id.tvXmrToSupport); + tvXmrToSupport.setPaintFlags(tvXmrToSupport.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG); + + return view; + } + + @Override + public boolean onValidateFields() { + return true; + } + + private boolean isResumed = false; + + @Override + public void onPauseFragment() { + isResumed = false; + super.onPauseFragment(); + } + + TxDataBtc btcData = null; + + @Override + public void onResumeFragment() { + super.onResumeFragment(); + Timber.d("onResumeFragment()"); + Helper.hideKeyboard(getActivity()); + isResumed = true; + + btcData = (TxDataBtc) sendListener.getTxData(); + tvTxAddress.setText(btcData.getDestinationAddress()); + + final PendingTx committedTx = sendListener.getCommittedTx(); + if (committedTx != null) { + tvTxId.setText(committedTx.txId); + bCopyTxId.setEnabled(true); + tvTxAmount.setText(getString(R.string.send_amount, Helper.getDisplayAmount(committedTx.amount))); + tvTxFee.setText(getString(R.string.send_fee, Helper.getDisplayAmount(committedTx.fee))); + if (btcData != null) { + NumberFormat df = NumberFormat.getInstance(Locale.US); + df.setMaximumFractionDigits(12); + String btcAmount = df.format(btcData.getBtcAmount()); + tvXmrToAmount.setText(getString(R.string.info_send_xmrto_success_btc, btcAmount, btcData.getBtcSymbol())); + //TODO btcData.getBtcAddress(); + tvTxXmrToKey.setText(btcData.getXmrtoOrderId()); + final Crypto crypto = Crypto.withSymbol(btcData.getBtcSymbol()); + ivXmrToIcon.setImageResource(crypto.getIconEnabledId()); + tvXmrToSupport.setOnClickListener(v -> { + Uri orderUri = getXmrToApi().getQueryOrderUri(btcData.getXmrtoOrderId()); + Intent intent = new Intent(Intent.ACTION_VIEW, orderUri); + startActivity(intent); + }); + queryOrder(); + } else { + throw new IllegalStateException("btcData is null"); + } + } + sendListener.enableDone(); + } + + private void processQueryOrder(final QueryOrderStatus status) { + Timber.d("processQueryOrder %s for %s", status.getState().toString(), status.getOrderId()); + if (!btcData.getXmrtoOrderId().equals(status.getOrderId())) + throw new IllegalStateException("UUIDs do not match!"); + if (isResumed && (getView() != null)) + getView().post(() -> { + showXmrToStatus(status); + if (!status.isTerminal()) { + getView().postDelayed(this::queryOrder, SideShiftApi.QUERY_INTERVAL); + } + }); + } + + private void queryOrder() { + Timber.d("queryOrder(%s)", btcData.getXmrtoOrderId()); + if (!isResumed) return; + getXmrToApi().queryOrderStatus(btcData.getXmrtoOrderId(), new ShiftCallback() { + @Override + public void onSuccess(QueryOrderStatus status) { + if (!isAdded()) return; + processQueryOrder(status); + } + + @Override + public void onError(final Exception ex) { + if (!isResumed) return; + Timber.w(ex); + getActivity().runOnUiThread(() -> { + if (ex instanceof ShiftException) { + Toast.makeText(getActivity(), ((ShiftException) ex).getError().getErrorMsg(), Toast.LENGTH_LONG).show(); + } else { + Toast.makeText(getActivity(), ex.getLocalizedMessage(), Toast.LENGTH_LONG).show(); + } + }); + } + }); + } + + void showXmrToStatus(final QueryOrderStatus status) { + int statusResource = 0; + if (status.isError()) { + tvXmrToStatus.setText(getString(R.string.info_send_xmrto_error, status.toString())); + statusResource = R.drawable.ic_error_red_24dp; + pbXmrto.getIndeterminateDrawable().setColorFilter( + ThemeHelper.getThemedColor(getContext(), android.R.attr.colorError), + android.graphics.PorterDuff.Mode.MULTIPLY); + } else if (status.isSent() || status.isPaid()) { + tvXmrToStatus.setText(getString(R.string.info_send_xmrto_sent, btcData.getBtcSymbol())); + statusResource = R.drawable.ic_success; + pbXmrto.getIndeterminateDrawable().setColorFilter( + ThemeHelper.getThemedColor(getContext(), R.attr.positiveColor), + android.graphics.PorterDuff.Mode.MULTIPLY); + } else if (status.isWaiting()) { + tvXmrToStatus.setText(getString(R.string.info_send_xmrto_unpaid)); + statusResource = R.drawable.ic_pending; + pbXmrto.getIndeterminateDrawable().setColorFilter( + ThemeHelper.getThemedColor(getContext(), R.attr.neutralColor), + android.graphics.PorterDuff.Mode.MULTIPLY); + } else if (status.isPending()) { + tvXmrToStatus.setText(getString(R.string.info_send_xmrto_paid)); + statusResource = R.drawable.ic_pending; + pbXmrto.getIndeterminateDrawable().setColorFilter( + ThemeHelper.getThemedColor(getContext(), R.attr.neutralColor), + android.graphics.PorterDuff.Mode.MULTIPLY); + } else { + throw new IllegalStateException("status is broken: " + status.toString()); + } + ivXmrToStatus.setImageResource(statusResource); + if (status.isTerminal()) { + pbXmrto.setVisibility(View.INVISIBLE); + ivXmrToIcon.setVisibility(View.GONE); + ivXmrToStatus.setVisibility(View.GONE); + ivXmrToStatusBig.setImageResource(statusResource); + ivXmrToStatusBig.setVisibility(View.VISIBLE); + } + } + + private SideShiftApi xmrToApi = null; + + private SideShiftApi getXmrToApi() { + if (xmrToApi == null) { + synchronized (this) { + if (xmrToApi == null) { + xmrToApi = new SideShiftApiImpl(ServiceHelper.getXmrToBaseUrl()); + } + } + } + return xmrToApi; + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendConfirm.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendConfirm.java new file mode 100644 index 0000000..69e97c8 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendConfirm.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.fragment.send; + +import com.m2049r.xmrwallet.model.PendingTransaction; + +interface SendConfirm { + void sendFailed(String errorText); + + void createTransactionFailed(String errorText); + + void transactionCreated(String txTag, PendingTransaction pendingTransaction); +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendConfirmWizardFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendConfirmWizardFragment.java new file mode 100644 index 0000000..f4d334e --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendConfirmWizardFragment.java @@ -0,0 +1,247 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.fragment.send; + +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.appcompat.app.AlertDialog; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.textfield.TextInputLayout; +import com.m2049r.xmrwallet.R; +import com.m2049r.xmrwallet.data.TxData; +import com.m2049r.xmrwallet.data.UserNotes; +import com.m2049r.xmrwallet.model.PendingTransaction; +import com.m2049r.xmrwallet.model.Wallet; +import com.m2049r.xmrwallet.util.Helper; + +import timber.log.Timber; + +public class SendConfirmWizardFragment extends SendWizardFragment implements SendConfirm { + + public static SendConfirmWizardFragment newInstance(Listener listener) { + SendConfirmWizardFragment instance = new SendConfirmWizardFragment(); + instance.setSendListener(listener); + return instance; + } + + Listener sendListener; + + public SendConfirmWizardFragment setSendListener(Listener listener) { + this.sendListener = listener; + return this; + } + + interface Listener { + SendFragment.Listener getActivityCallback(); + + TxData getTxData(); + + void commitTransaction(); + + void disposeTransaction(); + + SendFragment.Mode getMode(); + } + + private TextView tvTxAddress; + private TextView tvTxNotes; + private TextView tvTxAmount; + private TextView tvTxFee; + private TextView tvTxTotal; + private View llProgress; + private View bSend; + private View llConfirmSend; + private View pbProgressSend; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + Timber.d("onCreateView() %s", (String.valueOf(savedInstanceState))); + + View view = inflater.inflate( + R.layout.fragment_send_confirm, container, false); + + tvTxAddress = view.findViewById(R.id.tvTxAddress); + tvTxNotes = view.findViewById(R.id.tvTxNotes); + tvTxAmount = view.findViewById(R.id.tvTxAmount); + tvTxFee = view.findViewById(R.id.tvTxFee); + tvTxTotal = view.findViewById(R.id.tvTxTotal); + + llProgress = view.findViewById(R.id.llProgress); + pbProgressSend = view.findViewById(R.id.pbProgressSend); + llConfirmSend = view.findViewById(R.id.llConfirmSend); + + bSend = view.findViewById(R.id.bSend); + bSend.setEnabled(false); + bSend.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Timber.d("bSend.setOnClickListener"); + bSend.setEnabled(false); + preSend(); + } + }); + return view; + } + + boolean inProgress = false; + + public void hideProgress() { + llProgress.setVisibility(View.INVISIBLE); + inProgress = false; + } + + public void showProgress() { + llProgress.setVisibility(View.VISIBLE); + inProgress = true; + } + + PendingTransaction pendingTransaction = null; + + @Override + // callback from wallet when PendingTransaction created + public void transactionCreated(String txTag, PendingTransaction pendingTransaction) { + // ignore txTag - the app flow ensures this is the correct tx + // TODO: use the txTag + hideProgress(); + if (isResumed) { + this.pendingTransaction = pendingTransaction; + refreshTransactionDetails(); + } else { + sendListener.disposeTransaction(); + } + } + + void send() { + sendListener.commitTransaction(); + getActivity().runOnUiThread(() -> pbProgressSend.setVisibility(View.VISIBLE)); + } + + @Override + public void sendFailed(String errorText) { + pbProgressSend.setVisibility(View.INVISIBLE); + showAlert(getString(R.string.send_create_tx_error_title), errorText); + } + + @Override + public void createTransactionFailed(String errorText) { + hideProgress(); + showAlert(getString(R.string.send_create_tx_error_title), errorText); + } + + private void showAlert(String title, String message) { + AlertDialog.Builder builder = new MaterialAlertDialogBuilder(getActivity()); + builder.setCancelable(true). + setTitle(title). + setMessage(message). + create(). + show(); + } + + @Override + public boolean onValidateFields() { + return true; + } + + private boolean isResumed = false; + + @Override + public void onPauseFragment() { + isResumed = false; + pendingTransaction = null; + sendListener.disposeTransaction(); + refreshTransactionDetails(); + super.onPauseFragment(); + } + + @Override + public void onResumeFragment() { + super.onResumeFragment(); + Timber.d("onResumeFragment()"); + Helper.hideKeyboard(getActivity()); + isResumed = true; + + final TxData txData = sendListener.getTxData(); + tvTxAddress.setText(txData.getDestinationAddress()); + UserNotes notes = sendListener.getTxData().getUserNotes(); + if ((notes != null) && (!notes.note.isEmpty())) { + tvTxNotes.setText(notes.note); + } else { + tvTxNotes.setText("-"); + } + refreshTransactionDetails(); + if ((pendingTransaction == null) && (!inProgress)) { + showProgress(); + prepareSend(txData); + } + } + + void refreshTransactionDetails() { + Timber.d("refreshTransactionDetails()"); + if (pendingTransaction != null) { + llConfirmSend.setVisibility(View.VISIBLE); + bSend.setEnabled(true); + tvTxFee.setText(Wallet.getDisplayAmount(pendingTransaction.getFee())); + if (getActivityCallback().isStreetMode() + && (sendListener.getTxData().getAmount() == Wallet.SWEEP_ALL)) { + tvTxAmount.setText(getString(R.string.street_sweep_amount)); + tvTxTotal.setText(getString(R.string.street_sweep_amount)); + } else { + tvTxAmount.setText(Wallet.getDisplayAmount(pendingTransaction.getAmount())); + tvTxTotal.setText(Wallet.getDisplayAmount( + pendingTransaction.getFee() + pendingTransaction.getAmount())); + } + } else { + llConfirmSend.setVisibility(View.GONE); + bSend.setEnabled(false); + } + } + + public void preSend() { + Helper.promptPassword(getContext(), getActivityCallback().getWalletName(), false, new Helper.PasswordAction() { + @Override + public void act(String walletName, String password, boolean fingerprintUsed) { + send(); + } + + public void fail(String walletName) { + getActivity().runOnUiThread(() -> { + bSend.setEnabled(true); // allow to try again + }); + } + }); + } + + // creates a pending transaction and calls us back with transactionCreated() + // or createTransactionFailed() + void prepareSend(TxData txData) { + getActivityCallback().onPrepareSend(null, txData); + } + + SendFragment.Listener getActivityCallback() { + return sendListener.getActivityCallback(); + } +} 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 new file mode 100644 index 0000000..ce82795 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendFragment.java @@ -0,0 +1,558 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.fragment.send; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.text.InputType; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentStatePagerAdapter; +import androidx.viewpager.widget.ViewPager; + +import com.google.android.material.transition.MaterialContainerTransform; +import com.m2049r.xmrwallet.OnBackPressedListener; +import com.m2049r.xmrwallet.OnUriScannedListener; +import com.m2049r.xmrwallet.R; +import com.m2049r.xmrwallet.WalletActivity; +import com.m2049r.xmrwallet.data.BarcodeData; +import com.m2049r.xmrwallet.data.PendingTx; +import com.m2049r.xmrwallet.data.TxData; +import com.m2049r.xmrwallet.data.TxDataBtc; +import com.m2049r.xmrwallet.data.UserNotes; +import com.m2049r.xmrwallet.layout.SpendViewPager; +import com.m2049r.xmrwallet.model.PendingTransaction; +import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.Notice; +import com.m2049r.xmrwallet.util.ThemeHelper; +import com.m2049r.xmrwallet.widget.DotBar; +import com.m2049r.xmrwallet.widget.Toolbar; + +import java.lang.ref.WeakReference; + +import timber.log.Timber; + +public class SendFragment extends Fragment + implements SendAddressWizardFragment.Listener, + SendAmountWizardFragment.Listener, + SendConfirmWizardFragment.Listener, + SendSuccessWizardFragment.Listener, + OnBackPressedListener, OnUriScannedListener { + + final static public int MIXIN = 0; + + private Listener activityCallback; + + public interface Listener { + SharedPreferences getPrefs(); + + long getTotalFunds(); + + boolean isStreetMode(); + + void onPrepareSend(String tag, TxData data); + + String getWalletName(); + + void onSend(UserNotes notes); + + void onDisposeRequest(); + + void onFragmentDone(); + + void setToolbarButton(int type); + + void setTitle(String title); + + void setSubtitle(String subtitle); + + void setOnUriScannedListener(OnUriScannedListener onUriScannedListener); + } + + private View llNavBar; + private DotBar dotBar; + private Button bPrev; + private Button bNext; + + private Button bDone; + + static private final int MAX_FALLBACK = Integer.MAX_VALUE; + + public static SendFragment newInstance(String uri) { + SendFragment f = new SendFragment(); + Bundle args = new Bundle(); + args.putString(WalletActivity.REQUEST_URI, uri); + f.setArguments(args); + return f; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + final View view = inflater.inflate(R.layout.fragment_send, container, false); + + llNavBar = view.findViewById(R.id.llNavBar); + bDone = view.findViewById(R.id.bDone); + + dotBar = view.findViewById(R.id.dotBar); + bPrev = view.findViewById(R.id.bPrev); + bNext = view.findViewById(R.id.bNext); + + ViewGroup llNotice = view.findViewById(R.id.llNotice); + Notice.showAll(llNotice, ".*_send"); + + spendViewPager = view.findViewById(R.id.pager); + pagerAdapter = new SpendPagerAdapter(getChildFragmentManager()); + spendViewPager.setOffscreenPageLimit(pagerAdapter.getCount()); // load & keep all pages in cache + spendViewPager.setAdapter(pagerAdapter); + + spendViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { + private int fallbackPosition = MAX_FALLBACK; + private int currentPosition = 0; + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + } + + @Override + public void onPageSelected(int newPosition) { + Timber.d("onPageSelected=%d/%d", newPosition, fallbackPosition); + if (fallbackPosition < newPosition) { + spendViewPager.setCurrentItem(fallbackPosition); + } else { + pagerAdapter.getFragment(currentPosition).onPauseFragment(); + pagerAdapter.getFragment(newPosition).onResumeFragment(); + updatePosition(newPosition); + currentPosition = newPosition; + fallbackPosition = MAX_FALLBACK; + } + } + + @Override + public void onPageScrollStateChanged(int state) { + if (state == ViewPager.SCROLL_STATE_DRAGGING) { + if (!spendViewPager.validateFields(spendViewPager.getCurrentItem())) { + fallbackPosition = spendViewPager.getCurrentItem(); + } else { + fallbackPosition = spendViewPager.getCurrentItem() + 1; + } + } + } + }); + + bPrev.setOnClickListener(v -> spendViewPager.previous()); + + bNext.setOnClickListener(v -> spendViewPager.next()); + + bDone.setOnClickListener(v -> { + Timber.d("bDone.onClick"); + activityCallback.onFragmentDone(); + }); + + updatePosition(0); + + final EditText etDummy = view.findViewById(R.id.etDummy); + etDummy.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + etDummy.requestFocus(); + Helper.hideKeyboard(getActivity()); + + Bundle args = getArguments(); + if (args != null) { + String uri = args.getString(WalletActivity.REQUEST_URI); + Timber.d("URI: %s", uri); + if (uri != null) { + barcodeData = BarcodeData.fromString(uri); + Timber.d("barcodeData: %s", barcodeData != null ? barcodeData.toString() : "null"); + } + } + + return view; + } + + void updatePosition(int position) { + dotBar.setActiveDot(position); + CharSequence nextLabel = pagerAdapter.getPageTitle(position + 1); + bNext.setText(nextLabel); + if (nextLabel != null) { + bNext.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_navigate_next, 0); + } else { + bNext.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); + } + CharSequence prevLabel = pagerAdapter.getPageTitle(position - 1); + bPrev.setText(prevLabel); + if (prevLabel != null) { + bPrev.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_navigate_prev, 0, 0, 0); + } else { + bPrev.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); + } + } + + @Override + public void onResume() { + super.onResume(); + Timber.d("onResume"); + activityCallback.setSubtitle(getString(R.string.send_title)); + if (spendViewPager.getCurrentItem() == SpendPagerAdapter.POS_SUCCESS) { + activityCallback.setToolbarButton(Toolbar.BUTTON_NONE); + } else { + activityCallback.setToolbarButton(Toolbar.BUTTON_CANCEL); + } + } + + @Override + public void onAttach(@NonNull Context context) { + Timber.d("onAttach %s", context); + super.onAttach(context); + if (context instanceof Listener) { + activityCallback = (Listener) context; + activityCallback.setOnUriScannedListener(this); + } else { + throw new ClassCastException(context.toString() + + " must implement Listener"); + } + } + + @Override + public void onDetach() { + activityCallback.setOnUriScannedListener(null); + super.onDetach(); + } + + private SpendViewPager spendViewPager; + private SpendPagerAdapter pagerAdapter; + + @Override + public boolean onBackPressed() { + if (isComitted()) return true; // no going back + if (spendViewPager.getCurrentItem() == 0) { + return false; + } else { + spendViewPager.previous(); + return true; + } + } + + @Override + public boolean onUriScanned(BarcodeData barcodeData) { + if (spendViewPager.getCurrentItem() == SpendPagerAdapter.POS_ADDRESS) { + final SendWizardFragment fragment = pagerAdapter.getFragment(SpendPagerAdapter.POS_ADDRESS); + if (fragment instanceof SendAddressWizardFragment) { + ((SendAddressWizardFragment) fragment).processScannedData(barcodeData); + return true; + } + } + return false; + } + + enum Mode { + XMR, BTC + } + + Mode mode = Mode.XMR; + + @Override + public void setMode(Mode aMode) { + if (mode != aMode) { + mode = aMode; + switch (aMode) { + case XMR: + txData = new TxData(); + break; + case BTC: + txData = new TxDataBtc(); + break; + default: + throw new IllegalArgumentException("Mode " + String.valueOf(aMode) + " unknown!"); + } + getView().post(() -> pagerAdapter.notifyDataSetChanged()); + Timber.d("New Mode = %s", mode.toString()); + } + } + + @Override + public Mode getMode() { + return mode; + } + + public class SpendPagerAdapter extends FragmentStatePagerAdapter { + private static final int POS_ADDRESS = 0; + private static final int POS_AMOUNT = 1; + private static final int POS_CONFIRM = 2; + private static final int POS_SUCCESS = 3; + private int numPages = 3; + + SparseArray> myFragments = new SparseArray<>(); + + public SpendPagerAdapter(FragmentManager fm) { + super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); + } + + public void addSuccess() { + numPages++; + notifyDataSetChanged(); + } + + @Override + public int getCount() { + return numPages; + } + + @NonNull + @Override + public Object instantiateItem(@NonNull ViewGroup container, int position) { + Timber.d("instantiateItem %d", position); + SendWizardFragment fragment = (SendWizardFragment) super.instantiateItem(container, position); + myFragments.put(position, new WeakReference<>(fragment)); + return fragment; + } + + @Override + public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + Timber.d("destroyItem %d", position); + myFragments.remove(position); + super.destroyItem(container, position, object); + } + + public SendWizardFragment getFragment(int position) { + WeakReference ref = myFragments.get(position); + if (ref != null) + return myFragments.get(position).get(); + else + return null; + } + + @NonNull + @Override + public SendWizardFragment getItem(int position) { + Timber.d("getItem(%d) CREATE", position); + Timber.d("Mode=%s", mode.toString()); + if (mode == Mode.XMR) { + switch (position) { + case POS_ADDRESS: + return SendAddressWizardFragment.newInstance(SendFragment.this); + case POS_AMOUNT: + return SendAmountWizardFragment.newInstance(SendFragment.this); + case POS_CONFIRM: + return SendConfirmWizardFragment.newInstance(SendFragment.this); + case POS_SUCCESS: + return SendSuccessWizardFragment.newInstance(SendFragment.this); + default: + throw new IllegalArgumentException("no such send position(" + position + ")"); + } + } else if (mode == Mode.BTC) { + switch (position) { + case POS_ADDRESS: + return SendAddressWizardFragment.newInstance(SendFragment.this); + case POS_AMOUNT: + return SendBtcAmountWizardFragment.newInstance(SendFragment.this); + case POS_CONFIRM: + return SendBtcConfirmWizardFragment.newInstance(SendFragment.this); + case POS_SUCCESS: + return SendBtcSuccessWizardFragment.newInstance(SendFragment.this); + default: + throw new IllegalArgumentException("no such send position(" + position + ")"); + } + } else { + throw new IllegalStateException("Unknown mode!"); + } + } + + @Override + public CharSequence getPageTitle(int position) { + Timber.d("getPageTitle(%d)", position); + if (position >= numPages) return null; + switch (position) { + case POS_ADDRESS: + return getString(R.string.send_address_title); + case POS_AMOUNT: + return getString(R.string.send_amount_title); + case POS_CONFIRM: + return getString(R.string.send_confirm_title); + case POS_SUCCESS: + return getString(R.string.send_success_title); + default: + return null; + } + } + + @Override + public int getItemPosition(@NonNull Object object) { + Timber.d("getItemPosition %s", String.valueOf(object)); + if (object instanceof SendAddressWizardFragment) { + // keep these pages + return POSITION_UNCHANGED; + } else { + return POSITION_NONE; + } + } + } + + @Override + public TxData getTxData() { + return txData; + } + + private TxData txData = new TxData(); + + private BarcodeData barcodeData; + + // Listeners + @Override + public void setBarcodeData(BarcodeData data) { + barcodeData = data; + } + + @Override + public BarcodeData getBarcodeData() { + return barcodeData; + } + + @Override + public BarcodeData popBarcodeData() { + Timber.d("POPPED"); + BarcodeData data = barcodeData; + barcodeData = null; + return data; + } + + boolean isComitted() { + return committedTx != null; + } + + PendingTx committedTx; + + @Override + public PendingTx getCommittedTx() { + return committedTx; + } + + + @Override + public void commitTransaction() { + Timber.d("REALLY SEND"); + disableNavigation(); // committed - disable all navigation + activityCallback.onSend(txData.getUserNotes()); + committedTx = pendingTx; + } + + void disableNavigation() { + spendViewPager.allowSwipe(false); + } + + void enableNavigation() { + spendViewPager.allowSwipe(true); + } + + @Override + public void enableDone() { + llNavBar.setVisibility(View.INVISIBLE); + bDone.setVisibility(View.VISIBLE); + } + + public Listener getActivityCallback() { + return activityCallback; + } + + + // callbacks from send service + + public void onTransactionCreated(final String txTag, final PendingTransaction pendingTransaction) { + final SendConfirm confirm = getSendConfirm(); + if (confirm != null) { + pendingTx = new PendingTx(pendingTransaction); + confirm.transactionCreated(txTag, pendingTransaction); + } else { + // not in confirm fragment => dispose & move on + disposeTransaction(); + } + } + + @Override + public void disposeTransaction() { + pendingTx = null; + activityCallback.onDisposeRequest(); + } + + PendingTx pendingTx; + + public PendingTx getPendingTx() { + return pendingTx; + } + + public void onCreateTransactionFailed(String errorText) { + final SendConfirm confirm = getSendConfirm(); + if (confirm != null) { + confirm.createTransactionFailed(errorText); + } + } + + SendConfirm getSendConfirm() { + final SendWizardFragment fragment = pagerAdapter.getFragment(SpendPagerAdapter.POS_CONFIRM); + if (fragment instanceof SendConfirm) { + return (SendConfirm) fragment; + } else { + return null; + } + } + + public void onTransactionSent(final String txId) { + Timber.d("txid=%s", txId); + pagerAdapter.addSuccess(); + Timber.d("numPages=%d", spendViewPager.getAdapter().getCount()); + activityCallback.setToolbarButton(Toolbar.BUTTON_NONE); + spendViewPager.setCurrentItem(SpendPagerAdapter.POS_SUCCESS); + } + + public void onSendTransactionFailed(final String error) { + Timber.d("error=%s", error); + committedTx = null; + final SendConfirm confirm = getSendConfirm(); + if (confirm != null) { + confirm.sendFailed(getString(R.string.status_transaction_failed, error)); + } + enableNavigation(); + } + + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + final MaterialContainerTransform transform = new MaterialContainerTransform(); + transform.setDrawingViewId(R.id.fragment_container); + transform.setDuration(getResources().getInteger(R.integer.tx_item_transition_duration)); + transform.setAllContainerColors(ThemeHelper.getThemedColor(getContext(), android.R.attr.colorBackground)); + setSharedElementEnterTransition(transform); + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.send_menu, menu); + super.onCreateOptionsMenu(menu, inflater); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendSuccessWizardFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendSuccessWizardFragment.java new file mode 100644 index 0000000..34f3339 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendSuccessWizardFragment.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.fragment.send; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.TextView; +import android.widget.Toast; + +import com.m2049r.xmrwallet.R; +import com.m2049r.xmrwallet.data.PendingTx; +import com.m2049r.xmrwallet.data.TxData; +import com.m2049r.xmrwallet.model.Wallet; +import com.m2049r.xmrwallet.util.Helper; + +import timber.log.Timber; + +public class SendSuccessWizardFragment extends SendWizardFragment { + + public static SendSuccessWizardFragment newInstance(Listener listener) { + SendSuccessWizardFragment instance = new SendSuccessWizardFragment(); + instance.setSendListener(listener); + return instance; + } + + Listener sendListener; + + public SendSuccessWizardFragment setSendListener(Listener listener) { + this.sendListener = listener; + return this; + } + + interface Listener { + TxData getTxData(); + + PendingTx getCommittedTx(); + + void enableDone(); + + SendFragment.Mode getMode(); + + SendFragment.Listener getActivityCallback(); + } + + ImageButton bCopyTxId; + private TextView tvTxId; + private TextView tvTxAddress; + private TextView tvTxPaymentId; + private TextView tvTxAmount; + private TextView tvTxFee; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + Timber.d("onCreateView() %s", (String.valueOf(savedInstanceState))); + + View view = inflater.inflate( + R.layout.fragment_send_success, container, false); + + bCopyTxId = view.findViewById(R.id.bCopyTxId); + bCopyTxId.setEnabled(false); + bCopyTxId.setOnClickListener(v -> { + Helper.clipBoardCopy(getActivity(), getString(R.string.label_send_txid), tvTxId.getText().toString()); + Toast.makeText(getActivity(), getString(R.string.message_copy_txid), Toast.LENGTH_SHORT).show(); + }); + + tvTxId = view.findViewById(R.id.tvTxId); + tvTxAddress = view.findViewById(R.id.tvTxAddress); + tvTxPaymentId = view.findViewById(R.id.tvTxPaymentId); + tvTxAmount = view.findViewById(R.id.tvTxAmount); + tvTxFee = view.findViewById(R.id.tvTxFee); + + return view; + } + + @Override + public boolean onValidateFields() { + return true; + } + + @Override + public void onPauseFragment() { + super.onPauseFragment(); + } + + @Override + public void onResumeFragment() { + super.onResumeFragment(); + Timber.d("onResumeFragment()"); + Helper.hideKeyboard(getActivity()); + + final TxData txData = sendListener.getTxData(); + tvTxAddress.setText(txData.getDestinationAddress()); + + final PendingTx committedTx = sendListener.getCommittedTx(); + if (committedTx != null) { + tvTxId.setText(committedTx.txId); + bCopyTxId.setEnabled(true); + + if (sendListener.getActivityCallback().isStreetMode() + && (sendListener.getTxData().getAmount() == Wallet.SWEEP_ALL)) { + tvTxAmount.setText(getString(R.string.street_sweep_amount)); + } else { + tvTxAmount.setText(getString(R.string.send_amount, Helper.getDisplayAmount(committedTx.amount))); + } + tvTxFee.setText(getString(R.string.send_fee, Helper.getDisplayAmount(committedTx.fee))); + } + sendListener.enableDone(); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendWizardFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendWizardFragment.java new file mode 100644 index 0000000..5848ad8 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendWizardFragment.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.fragment.send; + +import androidx.fragment.app.Fragment; + +import com.m2049r.xmrwallet.layout.SpendViewPager; + +abstract public class SendWizardFragment extends Fragment + implements SpendViewPager.OnValidateFieldsListener { + + @Override + public boolean onValidateFields() { + return true; + } + + public void onPauseFragment() { + } + + public void onResumeFragment() { + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/layout/DiffCallback.java b/app/src/main/java/com/m2049r/xmrwallet/layout/DiffCallback.java new file mode 100644 index 0000000..231f7c1 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/layout/DiffCallback.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2021 yorha-0x + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.layout; + +import androidx.recyclerview.widget.DiffUtil; + +import java.util.List; + +public abstract class DiffCallback extends DiffUtil.Callback { + + protected final List mOldList; + protected final List mNewList; + + public DiffCallback(List oldList, List newList) { + this.mOldList = oldList; + this.mNewList = newList; + } + + @Override + public int getOldListSize() { + return mOldList.size(); + } + + @Override + public int getNewListSize() { + return mNewList.size(); + } + + public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition); + + public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition); +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/layout/NodeInfoAdapter.java b/app/src/main/java/com/m2049r/xmrwallet/layout/NodeInfoAdapter.java new file mode 100644 index 0000000..baae49e --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/layout/NodeInfoAdapter.java @@ -0,0 +1,261 @@ +/* + * Copyright (c) 2018 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.layout; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.fragment.app.FragmentActivity; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.RecyclerView; + +import com.m2049r.xmrwallet.R; +import com.m2049r.xmrwallet.data.NodeInfo; +import com.m2049r.xmrwallet.dialog.HelpFragment; +import com.m2049r.xmrwallet.util.NetCipherHelper; + +import java.net.HttpURLConnection; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +public class NodeInfoAdapter extends RecyclerView.Adapter { + public interface OnInteractionListener { + void onInteraction(View view, NodeInfo item); + + boolean onLongInteraction(View view, NodeInfo item); + } + + private final List nodeItems = new ArrayList<>(); + private final OnInteractionListener listener; + + private final FragmentActivity activity; + + public NodeInfoAdapter(FragmentActivity activity, OnInteractionListener listener) { + this.activity = activity; + this.listener = listener; + } + + public void notifyItemChanged(NodeInfo nodeInfo) { + final int pos = nodeItems.indexOf(nodeInfo); + if (pos >= 0) notifyItemChanged(pos); + } + + private static class NodeDiff extends DiffCallback { + + public NodeDiff(List oldList, List newList) { + super(oldList, newList); + } + + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { + return mOldList.get(oldItemPosition).equals(mNewList.get(newItemPosition)); + } + + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { + final NodeInfo oldItem = mOldList.get(oldItemPosition); + final NodeInfo newItem = mNewList.get(newItemPosition); + return (oldItem.getTimestamp() == newItem.getTimestamp()) + && (oldItem.isTested() == newItem.isTested()) + && (oldItem.isValid() == newItem.isValid()) + && (oldItem.getResponseTime() == newItem.getResponseTime()) + && (oldItem.isSelected() == newItem.isSelected()) + && (oldItem.getName().equals(newItem.getName())); + } + } + + @Override + public @NonNull + ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_node, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(final @NonNull ViewHolder holder, int position) { + holder.bind(position); + } + + @Override + public int getItemCount() { + return nodeItems.size(); + } + + public void addNode(NodeInfo node) { + List newItems = new ArrayList<>(nodeItems); + if (!nodeItems.contains(node)) + newItems.add(node); + setNodes(newItems); // in case the nodeinfo has changed + } + + public void setNodes(Collection newItemsCollection) { + List newItems; + if (newItemsCollection != null) { + newItems = new ArrayList<>(newItemsCollection); + Collections.sort(newItems, NodeInfo.BestNodeComparator); + } else { + newItems = new ArrayList<>(); + } + final NodeInfoAdapter.NodeDiff diffCallback = new NodeInfoAdapter.NodeDiff(nodeItems, newItems); + final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback); + nodeItems.clear(); + nodeItems.addAll(newItems); + diffResult.dispatchUpdatesTo(this); + } + + public void setNodes() { + setNodes(nodeItems); + } + + private boolean itemsClickable = true; + + public void allowClick(boolean clickable) { + itemsClickable = clickable; + notifyDataSetChanged(); + } + + class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener { + final ImageButton ibBookmark; + final View pbBookmark; + final TextView tvName; + final TextView tvInfo; + final ImageView ivPing; + NodeInfo nodeItem; + + ViewHolder(View itemView) { + super(itemView); + ibBookmark = itemView.findViewById(R.id.ibBookmark); + pbBookmark = itemView.findViewById(R.id.pbBookmark); + tvName = itemView.findViewById(R.id.tvName); + tvInfo = itemView.findViewById(R.id.tvInfo); + ivPing = itemView.findViewById(R.id.ivPing); + ibBookmark.setOnClickListener(v -> { + nodeItem.toggleFavourite(); + showStar(); + if (!nodeItem.isFavourite()) { + nodeItem.setSelected(false); + setNodes(nodeItems); + } + }); + itemView.setOnClickListener(this); + itemView.setOnLongClickListener(this); + } + + private void showStar() { + if (nodeItem.isFavourite()) { + ibBookmark.setImageResource(R.drawable.ic_favorite_24dp); + } else { + ibBookmark.setImageResource(R.drawable.ic_favorite_border_24dp); + } + } + + void bind(int position) { + nodeItem = nodeItems.get(position); + tvName.setText(nodeItem.getName()); + ivPing.setImageResource(getPingIcon(nodeItem)); + if (nodeItem.isTested()) { + if (nodeItem.isValid()) { + nodeItem.showInfo(tvInfo); + } else { + nodeItem.showInfo(tvInfo, getResponseErrorText(activity, nodeItem.getResponseCode()), true); + } + } else { + nodeItem.showInfo(tvInfo); + } + itemView.setSelected(nodeItem.isSelected()); + itemView.setClickable(itemsClickable); + itemView.setEnabled(itemsClickable); + ibBookmark.setClickable(itemsClickable); + pbBookmark.setVisibility(nodeItem.isSelecting() ? View.VISIBLE : View.INVISIBLE); + showStar(); + } + + @Override + public void onClick(View view) { + if (listener != null) { + int position = getAdapterPosition(); // gets item position + if (position != RecyclerView.NO_POSITION) { // Check if an item was deleted, but the user clicked it before the UI removed it + final NodeInfo node = nodeItems.get(position); + if (node.isOnion()) { + switch (NetCipherHelper.getStatus()) { + case NOT_INSTALLED: + HelpFragment.display(activity.getSupportFragmentManager(), R.string.help_tor); + return; + case DISABLED: + HelpFragment.display(activity.getSupportFragmentManager(), R.string.help_tor_enable); + return; + } + } + node.setSelecting(true); + allowClick(false); + listener.onInteraction(view, node); + } + } + } + + @Override + public boolean onLongClick(View view) { + if (listener != null) { + int position = getAdapterPosition(); // gets item position + if (position != RecyclerView.NO_POSITION) { // Check if an item was deleted, but the user clicked it before the UI removed it + return listener.onLongInteraction(view, nodeItems.get(position)); + } + } + return false; + } + } + + static public int getPingIcon(NodeInfo nodeInfo) { + if (nodeInfo.isUnauthorized()) { + return R.drawable.ic_wifi_lock; + } + if (nodeInfo.isValid()) { + final double ping = nodeInfo.getResponseTime(); + if (ping < NodeInfo.PING_GOOD) { + return R.drawable.ic_wifi_4_bar; + } else if (ping < NodeInfo.PING_MEDIUM) { + return R.drawable.ic_wifi_3_bar; + } else if (ping < NodeInfo.PING_BAD) { + return R.drawable.ic_wifi_2_bar; + } else { + return R.drawable.ic_wifi_1_bar; + } + } else { + return R.drawable.ic_wifi_off; + } + } + + static public String getResponseErrorText(Context ctx, int responseCode) { + if (responseCode == 0) { + return ctx.getResources().getString(R.string.node_general_error); + } else if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) { + return ctx.getResources().getString(R.string.node_auth_error); + } else if (responseCode == 418) { + return ctx.getResources().getString(R.string.node_tor_error); + } else { + return ctx.getResources().getString(R.string.node_test_error, responseCode); + } + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/layout/SpendViewPager.java b/app/src/main/java/com/m2049r/xmrwallet/layout/SpendViewPager.java new file mode 100644 index 0000000..71b7e0c --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/layout/SpendViewPager.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.layout; + +import android.content.Context; +import androidx.viewpager.widget.ViewPager; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import com.m2049r.xmrwallet.fragment.send.SendFragment; + +public class SpendViewPager extends ViewPager { + + public interface OnValidateFieldsListener { + boolean onValidateFields(); + } + + public SpendViewPager(Context context) { + super(context); + } + + public SpendViewPager(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void next() { + int pos = getCurrentItem(); + if (validateFields(pos)) { + setCurrentItem(pos + 1); + } + } + + public void previous() { + setCurrentItem(getCurrentItem() - 1); + } + + private boolean allowSwipe = true; + + public void allowSwipe(boolean allow) { + allowSwipe = allow; + } + + public boolean validateFields(int position) { + OnValidateFieldsListener c = ((SendFragment.SpendPagerAdapter) getAdapter()).getFragment(position); + return c.onValidateFields(); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + if (allowSwipe) return super.onInterceptTouchEvent(event); + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (allowSwipe) return super.onTouchEvent(event); + return false; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/layout/SubaddressInfoAdapter.java b/app/src/main/java/com/m2049r/xmrwallet/layout/SubaddressInfoAdapter.java new file mode 100644 index 0000000..6dab349 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/layout/SubaddressInfoAdapter.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.layout; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.RecyclerView; + +import com.m2049r.xmrwallet.R; +import com.m2049r.xmrwallet.data.Subaddress; +import com.m2049r.xmrwallet.util.Helper; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import timber.log.Timber; + +public class SubaddressInfoAdapter extends RecyclerView.Adapter { + public interface OnInteractionListener { + void onInteraction(View view, Subaddress item); + + boolean onLongInteraction(View view, Subaddress item); + } + + private final List items; + private final OnInteractionListener listener; + + Context context; + + public SubaddressInfoAdapter(Context context, OnInteractionListener listener) { + this.context = context; + this.items = new ArrayList<>(); + this.listener = listener; + } + + private static class SubaddressInfoDiff extends DiffCallback { + + public SubaddressInfoDiff(List oldList, List newList) { + super(oldList, newList); + } + + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { + return mOldList.get(oldItemPosition).getAddress().equals(mNewList.get(newItemPosition).getAddress()); + } + + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { + return mOldList.get(oldItemPosition).equals(mNewList.get(newItemPosition)); + } + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_subaddress, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(final ViewHolder holder, int position) { + holder.bind(position); + } + + @Override + public int getItemCount() { + return items.size(); + } + + public Subaddress getItem(int position) { + return items.get(position); + } + + public void setInfos(List newItems) { + if (newItems == null) { + newItems = new ArrayList<>(); + Timber.d("setInfos null"); + } else { + Timber.d("setInfos %s", newItems.size()); + } + Collections.sort(newItems); + final DiffCallback diffCallback = new SubaddressInfoDiff(items, newItems); + final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback); + items.clear(); + items.addAll(newItems); + diffResult.dispatchUpdatesTo(this); + } + + class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener { + final TextView tvName; + final TextView tvAddress; + final TextView tvAmount; + Subaddress item; + + ViewHolder(View itemView) { + super(itemView); + tvName = itemView.findViewById(R.id.tvName); + tvAddress = itemView.findViewById(R.id.tvAddress); + tvAmount = itemView.findViewById(R.id.tx_amount); + itemView.setOnClickListener(this); + itemView.setOnLongClickListener(this); + } + + void bind(int position) { + item = getItem(position); + itemView.setTransitionName(context.getString(R.string.subaddress_item_transition_name, item.getAddressIndex())); + + final String label = item.getDisplayLabel(); + final String address = context.getString(R.string.subbaddress_info_subtitle, + item.getAddressIndex(), item.getSquashedAddress()); + tvName.setText(label.isEmpty() ? address : label); + tvAddress.setText(address); + final long amount = item.getAmount(); + if (amount > 0) + tvAmount.setText(context.getString(R.string.tx_list_amount_positive, + Helper.getDisplayAmount(amount, Helper.DISPLAY_DIGITS_INFO))); + else + tvAmount.setText(""); + } + + @Override + public void onClick(View view) { + if (listener != null) { + int position = getAdapterPosition(); // gets item position + if (position != RecyclerView.NO_POSITION) { // Check if an item was deleted, but the user clicked it before the UI removed it + listener.onInteraction(view, getItem(position)); + } + } + } + + @Override + public boolean onLongClick(View view) { + if (listener != null) { + int position = getAdapterPosition(); // gets item position + if (position != RecyclerView.NO_POSITION) { // Check if an item was deleted, but the user clicked it before the UI removed it + return listener.onLongInteraction(view, getItem(position)); + } + } + return true; + } + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/layout/TransactionInfoAdapter.java b/app/src/main/java/com/m2049r/xmrwallet/layout/TransactionInfoAdapter.java new file mode 100644 index 0000000..c4ce06e --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/layout/TransactionInfoAdapter.java @@ -0,0 +1,278 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.layout; + +import android.content.Context; +import android.text.Html; +import android.text.Spanned; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.progressindicator.CircularProgressIndicator; +import com.m2049r.xmrwallet.R; +import com.m2049r.xmrwallet.data.Crypto; +import com.m2049r.xmrwallet.data.UserNotes; +import com.m2049r.xmrwallet.model.TransactionInfo; +import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.ThemeHelper; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.TimeZone; + +import timber.log.Timber; + +public class TransactionInfoAdapter extends RecyclerView.Adapter { + private final static SimpleDateFormat DATETIME_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + + private final int outboundColour; + private final int inboundColour; + private final int pendingColour; + private final int failedColour; + + public interface OnInteractionListener { + void onInteraction(View view, TransactionInfo item); + } + + private final List infoItems; + private final OnInteractionListener listener; + + private final Context context; + + public TransactionInfoAdapter(Context context, OnInteractionListener listener) { + this.context = context; + inboundColour = ThemeHelper.getThemedColor(context, R.attr.positiveColor); + outboundColour = ThemeHelper.getThemedColor(context, R.attr.negativeColor); + pendingColour = ThemeHelper.getThemedColor(context, R.attr.neutralColor); + failedColour = ThemeHelper.getThemedColor(context, R.attr.neutralColor); + infoItems = new ArrayList<>(); + this.listener = listener; + Calendar cal = Calendar.getInstance(); + TimeZone tz = cal.getTimeZone(); //get the local time zone. + DATETIME_FORMATTER.setTimeZone(tz); + } + + public boolean needsTransactionUpdateOnNewBlock() { + return (infoItems.size() > 0) && !infoItems.get(0).isConfirmed(); + } + + private static class TransactionInfoDiff extends DiffCallback { + + public TransactionInfoDiff(List oldList, List newList) { + super(oldList, newList); + } + + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { + return mOldList.get(oldItemPosition).hash.equals(mNewList.get(newItemPosition).hash); + } + + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { + final TransactionInfo oldItem = mOldList.get(oldItemPosition); + final TransactionInfo newItem = mNewList.get(newItemPosition); + return (oldItem.direction == newItem.direction) + && (oldItem.isPending == newItem.isPending) + && (oldItem.isFailed == newItem.isFailed) + && ((oldItem.confirmations == newItem.confirmations) || (oldItem.isConfirmed())) + && (oldItem.subaddressLabel.equals(newItem.subaddressLabel)) + && (Objects.equals(oldItem.notes, newItem.notes)); + } + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_transaction, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(final ViewHolder holder, int position) { + holder.bind(position); + } + + @Override + public int getItemCount() { + return infoItems.size(); + } + + public void setInfos(List newItems) { + if (newItems == null) { + newItems = new ArrayList<>(); + Timber.d("setInfos null"); + } else { + Timber.d("setInfos %s", newItems.size()); + } + Collections.sort(newItems); + final DiffCallback diffCallback = new TransactionInfoAdapter.TransactionInfoDiff(infoItems, newItems); + final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback); + infoItems.clear(); + infoItems.addAll(newItems); + diffResult.dispatchUpdatesTo(this); + } + + public void removeItem(int position) { + List newItems = new ArrayList<>(infoItems); + if (newItems.size() > position) + newItems.remove(position); + setInfos(newItems); // in case the nodeinfo has changed + } + + public TransactionInfo getItem(int position) { + return infoItems.get(position); + } + + class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { + final ImageView ivTxType; + final TextView tvAmount; + final TextView tvFailed; + final TextView tvPaymentId; + final TextView tvDateTime; + final CircularProgressIndicator pbConfirmations; + final TextView tvConfirmations; + TransactionInfo infoItem; + + ViewHolder(View itemView) { + super(itemView); + ivTxType = itemView.findViewById(R.id.ivTxType); + tvAmount = itemView.findViewById(R.id.tx_amount); + tvFailed = itemView.findViewById(R.id.tx_failed); + tvPaymentId = itemView.findViewById(R.id.tx_paymentid); + tvDateTime = itemView.findViewById(R.id.tx_datetime); + pbConfirmations = itemView.findViewById(R.id.pbConfirmations); + pbConfirmations.setMax(TransactionInfo.CONFIRMATION); + tvConfirmations = itemView.findViewById(R.id.tvConfirmations); + } + + private String getDateTime(long time) { + return DATETIME_FORMATTER.format(new Date(time * 1000)); + } + + private void setTxColour(int clr) { + tvAmount.setTextColor(clr); + } + + void bind(int position) { + infoItem = infoItems.get(position); + itemView.setTransitionName(context.getString(R.string.tx_item_transition_name, infoItem.hash)); + + UserNotes userNotes = new UserNotes(infoItem.notes); + if (userNotes.xmrtoKey != null) { + final Crypto crypto = Crypto.withSymbol(userNotes.xmrtoCurrency); + if (crypto != null) { + ivTxType.setImageResource(crypto.getIconEnabledId()); + ivTxType.setVisibility(View.VISIBLE); + } else {// otherwirse pretend we don't know it's a shift + ivTxType.setVisibility(View.GONE); + } + } else { + ivTxType.setVisibility(View.GONE); + } + + String displayAmount = Helper.getDisplayAmount(infoItem.amount, Helper.DISPLAY_DIGITS_INFO); + if (infoItem.direction == TransactionInfo.Direction.Direction_Out) { + tvAmount.setText(context.getString(R.string.tx_list_amount_negative, displayAmount)); + } else { + tvAmount.setText(context.getString(R.string.tx_list_amount_positive, displayAmount)); + } + + tvFailed.setVisibility(View.GONE); + if (infoItem.isFailed) { + this.tvAmount.setText(context.getString(R.string.tx_list_amount_failed, displayAmount)); + tvFailed.setVisibility(View.VISIBLE); + setTxColour(failedColour); + pbConfirmations.setVisibility(View.GONE); + tvConfirmations.setVisibility(View.GONE); + } else if (infoItem.isPending) { + setTxColour(pendingColour); + pbConfirmations.setVisibility(View.GONE); + pbConfirmations.setIndeterminate(true); + pbConfirmations.setVisibility(View.VISIBLE); + tvConfirmations.setVisibility(View.GONE); + } else if (infoItem.direction == TransactionInfo.Direction.Direction_In) { + setTxColour(inboundColour); + if (!infoItem.isConfirmed()) { + pbConfirmations.setVisibility(View.VISIBLE); + final int confirmations = (int) infoItem.confirmations; + pbConfirmations.setProgressCompat(confirmations, true); + final String confCount = Integer.toString(confirmations); + tvConfirmations.setText(confCount); + if (confCount.length() == 1) // we only have space for character in the progress circle + tvConfirmations.setVisibility(View.VISIBLE); + else + tvConfirmations.setVisibility(View.GONE); + } else { + pbConfirmations.setVisibility(View.GONE); + tvConfirmations.setVisibility(View.GONE); + } + } else { + setTxColour(outboundColour); + pbConfirmations.setVisibility(View.GONE); + tvConfirmations.setVisibility(View.GONE); + } + + String tag = null; + String info = ""; + if ((infoItem.addressIndex != 0) && (infoItem.direction == TransactionInfo.Direction.Direction_In)) + tag = infoItem.getDisplayLabel(); + if ((userNotes.note.isEmpty())) { + if (!infoItem.paymentId.equals("0000000000000000")) { + info = infoItem.paymentId; + } + } else { + info = userNotes.note; + } + if (tag == null) { + tvPaymentId.setText(info); + } else { + Spanned label = Html.fromHtml(context.getString(R.string.tx_details_notes, + Integer.toHexString(ThemeHelper.getThemedColor(context, R.attr.positiveColor) & 0xFFFFFF), + Integer.toHexString(ThemeHelper.getThemedColor(context, android.R.attr.colorBackground) & 0xFFFFFF), + tag, info.isEmpty() ? "" : ("  " + info))); + tvPaymentId.setText(label); + } + + this.tvDateTime.setText(getDateTime(infoItem.timestamp)); + + itemView.setOnClickListener(this); + } + + @Override + public void onClick(View view) { + if (listener != null) { + int position = getAdapterPosition(); // gets item position + if (position != RecyclerView.NO_POSITION) { // Check if an item was deleted, but the user clicked it before the UI removed it + listener.onInteraction(view, infoItems.get(position)); + } + } + } + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/layout/WalletInfoAdapter.java b/app/src/main/java/com/m2049r/xmrwallet/layout/WalletInfoAdapter.java new file mode 100644 index 0000000..ad885e7 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/layout/WalletInfoAdapter.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.layout; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.widget.PopupMenu; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.RecyclerView; + +import com.m2049r.xmrwallet.R; +import com.m2049r.xmrwallet.model.WalletManager; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.TimeZone; + +import timber.log.Timber; + +public class WalletInfoAdapter extends RecyclerView.Adapter { + + private final SimpleDateFormat DATETIME_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + + public interface OnInteractionListener { + void onInteraction(View view, WalletManager.WalletInfo item); + + boolean onContextInteraction(MenuItem item, WalletManager.WalletInfo infoItem); + } + + private final List infoItems; + private final OnInteractionListener listener; + + Context context; + + public WalletInfoAdapter(Context context, OnInteractionListener listener) { + this.context = context; + this.infoItems = new ArrayList<>(); + this.listener = listener; + Calendar cal = Calendar.getInstance(); + TimeZone tz = cal.getTimeZone(); //get the local time zone. + DATETIME_FORMATTER.setTimeZone(tz); + } + + private static class WalletInfoDiff extends DiffCallback { + + public WalletInfoDiff(List oldList, List newList) { + super(oldList, newList); + } + + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { + return mOldList.get(oldItemPosition).getName().equals(mNewList.get(newItemPosition).getName()); + } + + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { + return mOldList.get(oldItemPosition).compareTo(mNewList.get(newItemPosition)) == 0; + } + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + return new ViewHolder( + LayoutInflater.from(parent.getContext()).inflate(R.layout.item_wallet, parent, false) + ); + } + + @Override + public void onBindViewHolder(final ViewHolder holder, int position) { + holder.bind(position); + } + + @Override + public int getItemCount() { + return infoItems.size(); + } + + public WalletManager.WalletInfo getItem(int position) { + return infoItems.get(position); + } + + public void setInfos(List newItems) { + if (newItems == null) { + newItems = new ArrayList<>(); + Timber.d("setInfos null"); + } else { + Timber.d("setInfos %s", newItems.size()); + } + Collections.sort(newItems); + final DiffCallback diffCallback = new WalletInfoAdapter.WalletInfoDiff(infoItems, newItems); + final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback); + infoItems.clear(); + infoItems.addAll(newItems); + diffResult.dispatchUpdatesTo(this); + } + + class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { + final TextView tvName; + final ImageButton ibOptions; + WalletManager.WalletInfo infoItem; + boolean popupOpen = false; + + ViewHolder(View itemView) { + super(itemView); + tvName = itemView.findViewById(R.id.tvName); + ibOptions = itemView.findViewById(R.id.ibOptions); + ibOptions.setOnClickListener(view -> { + if (popupOpen) return; + //creating a popup menu + PopupMenu popup = new PopupMenu(context, ibOptions); + //inflating menu from xml resource + popup.inflate(R.menu.list_context_menu); + popupOpen = true; + //adding click listener + popup.setOnMenuItemClickListener(item -> { + if (listener != null) { + return listener.onContextInteraction(item, infoItem); + } + return false; + }); + //displaying the popup + popup.show(); + popup.setOnDismissListener(menu -> popupOpen = false); + + }); + itemView.setOnClickListener(this); + } + + private String getDateTime(long time) { + return DATETIME_FORMATTER.format(new Date(time * 1000)); + } + + void bind(int position) { + infoItem = infoItems.get(position); + tvName.setText(infoItem.getName()); + } + + @Override + public void onClick(View view) { + if (listener != null) { + int position = getAdapterPosition(); // gets item position + if (position != RecyclerView.NO_POSITION) { // Check if an item was deleted, but the user clicked it before the UI removed it + listener.onInteraction(view, infoItems.get(position)); + } + } + } + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/ledger/Instruction.java b/app/src/main/java/com/m2049r/xmrwallet/ledger/Instruction.java new file mode 100644 index 0000000..52c8a6e --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/ledger/Instruction.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2018 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.ledger; + +public enum Instruction { + + INS_NONE(0x00), + INS_RESET(0x02), + INS_GET_KEY(0x20), + INS_DISPLAY_ADDRESS(0x21), + INS_PUT_KEY(0x22), + INS_GET_CHACHA8_PREKEY(0x24), + INS_VERIFY_KEY(0x26), + INS_MANAGE_SEEDWORDS(0x28), + + INS_SECRET_KEY_TO_PUBLIC_KEY(0x30), + INS_GEN_KEY_DERIVATION(0x32), + INS_DERIVATION_TO_SCALAR(0x34), + INS_DERIVE_PUBLIC_KEY(0x36), + INS_DERIVE_SECRET_KEY(0x38), + INS_GEN_KEY_IMAGE(0x3A), + + INS_SECRET_KEY_ADD(0x3C), + INS_SECRET_KEY_SUB(0x3E), + INS_GENERATE_KEYPAIR(0x40), + INS_SECRET_SCAL_MUL_KEY(0x42), + INS_SECRET_SCAL_MUL_BASE(0x44), + + INS_DERIVE_SUBADDRESS_PUBLIC_KEY(0x46), + INS_GET_SUBADDRESS(0x48), + INS_GET_SUBADDRESS_SPEND_PUBLIC_KEY(0x4A), + INS_GET_SUBADDRESS_SECRET_KEY(0x4C), + + INS_OPEN_TX(0x70), + INS_SET_SIGNATURE_MODE(0x72), + INS_GET_ADDITIONAL_KEY(0x74), + INS_STEALTH(0x76), + INS_GEN_COMMITMENT_MASK(0x77), + INS_BLIND(0x78), + INS_UNBLIND(0x7A), + INS_GEN_TXOUT_KEYS(0x7B), + INS_VALIDATE(0x7C), + INS_PREFIX_HASH(0x7D), + INS_MLSAG(0x7E), + INS_CLOSE_TX(0x80), + + INS_GET_TX_PROOF(0xA0), + + INS_GET_RESPONSE(0xC0), + + INS_UNDEFINED(0xFF); + + public static Instruction fromByte(byte n) { + switch (n & 0xFF) { + case 0x00: + return INS_NONE; + case 0x02: + return INS_RESET; + + case 0x20: + return INS_GET_KEY; + case 0x22: + return INS_PUT_KEY; + case 0x24: + return INS_GET_CHACHA8_PREKEY; + case 0x26: + return INS_VERIFY_KEY; + + case 0x30: + return INS_SECRET_KEY_TO_PUBLIC_KEY; + case 0x32: + return INS_GEN_KEY_DERIVATION; + case 0x34: + return INS_DERIVATION_TO_SCALAR; + case 0x36: + return INS_DERIVE_PUBLIC_KEY; + case 0x38: + return INS_DERIVE_SECRET_KEY; + case 0x3A: + return INS_GEN_KEY_IMAGE; + case 0x3C: + return INS_SECRET_KEY_ADD; + case 0x3E: + return INS_SECRET_KEY_SUB; + case 0x40: + return INS_GENERATE_KEYPAIR; + case 0x42: + return INS_SECRET_SCAL_MUL_KEY; + case 0x44: + return INS_SECRET_SCAL_MUL_BASE; + + case 0x46: + return INS_DERIVE_SUBADDRESS_PUBLIC_KEY; + case 0x48: + return INS_GET_SUBADDRESS; + case 0x4A: + return INS_GET_SUBADDRESS_SPEND_PUBLIC_KEY; + case 0x4C: + return INS_GET_SUBADDRESS_SECRET_KEY; + + case 0x70: + return INS_OPEN_TX; + case 0x72: + return INS_SET_SIGNATURE_MODE; + case 0x74: + return INS_GET_ADDITIONAL_KEY; + case 0x76: + return INS_STEALTH; + case 0x78: + return INS_BLIND; + case 0x7A: + return INS_UNBLIND; + case 0x7C: + return INS_VALIDATE; + case 0x7E: + return INS_MLSAG; + case 0x80: + return INS_CLOSE_TX; + + case 0xc0: + return INS_GET_RESPONSE; + + default: + return INS_UNDEFINED; + } + } + + public int getValue() { + return value; + } + + public byte getByteValue() { + return (byte) (value & 0xFF); + } + + private int value; + + Instruction(int value) { + this.value = value; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/ledger/Ledger.java b/app/src/main/java/com/m2049r/xmrwallet/ledger/Ledger.java new file mode 100644 index 0000000..0358282 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/ledger/Ledger.java @@ -0,0 +1,240 @@ +/* + ******************************************************************************* + * BTChip Bitcoin Hardware Wallet Java API + * (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn + * Copyright (c) 2018 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************** + */ + +package com.m2049r.xmrwallet.ledger; + +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbManager; + +import com.btchip.BTChipException; +import com.btchip.comm.BTChipTransport; +import com.btchip.comm.android.BTChipTransportAndroidHID; +import com.m2049r.xmrwallet.BuildConfig; +import com.m2049r.xmrwallet.model.WalletManager; +import com.m2049r.xmrwallet.util.Helper; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import timber.log.Timber; + +public class Ledger { + static final public boolean ENABLED = true; + // 5:20 is same as wallet2.cpp::restore() + static public final int LOOKAHEAD_ACCOUNTS = 5; + static public final int LOOKAHEAD_SUBADDRESSES = 20; + static public final String SUBADDRESS_LOOKAHEAD = LOOKAHEAD_ACCOUNTS + ":" + LOOKAHEAD_SUBADDRESSES; + + private static final byte PROTOCOL_VERSION = 0x03; + public static final int SW_OK = 0x9000; + public static final int SW_INS_NOT_SUPPORTED = 0x6D00; + public static final int OK[] = {SW_OK}; + public static final int MINIMUM_LEDGER_VERSION = (1 << 16) + (8 << 8) + (0); // 1.6.0 + + public static UsbDevice findDevice(UsbManager usbManager) { + if (!ENABLED) return null; + return BTChipTransportAndroidHID.getDevice(usbManager); + } + + static private Ledger Instance = null; + + static public String connect(UsbManager usbManager, UsbDevice usbDevice) throws IOException { + if (Instance != null) { + disconnect(); + } + Instance = new Ledger(usbManager, usbDevice); + return Name(); + } + + static public void disconnect() { + // this is not synchronized so as to close immediately + if (Instance != null) { + Instance.close(); + Instance = null; + } + } + + static public boolean isConnected() { + //TODO synchronize with connect/disconnect? + return Instance != null; + } + + static public String Name() { + if (Instance != null) { + return Instance.name; + } else { + return null; + } + } + + static public byte[] Exchange(byte[] apdu) { + if (Instance != null) { + Timber.d("INS: %s", Instruction.fromByte(apdu[1])); + return Instance.exchangeRaw(apdu); + } else { + return null; + } + } + + static public boolean check() { + if (Instance == null) return false; + byte[] moneroVersion = WalletManager.moneroVersion().getBytes(StandardCharsets.US_ASCII); + + try { + byte[] resp = Instance.exchangeApduNoOpt(Instruction.INS_RESET, moneroVersion, OK); + int deviceVersion = (resp[0] << 16) + (resp[1] << 8) + (resp[2]); + if (deviceVersion < MINIMUM_LEDGER_VERSION) + return false; + } catch (BTChipException ex) { // comm error - probably wrong app started on device + return false; + } + return true; + } + + final private BTChipTransport transport; + final private String name; + private int lastSW = 0; + + private Ledger(UsbManager usbManager, UsbDevice usbDevice) throws IOException { + final BTChipTransport transport = BTChipTransportAndroidHID.open(usbManager, usbDevice); + Timber.d("transport opened = %s", transport.toString()); + transport.setDebug(BuildConfig.DEBUG); + this.transport = transport; + this.name = usbDevice.getManufacturerName() + " " + usbDevice.getProductName(); + initKey(); + } + + synchronized private void close() { + initKey(); // don't leak key after we disconnect + transport.close(); + Timber.d("transport closed"); + lastSW = 0; + } + + synchronized private byte[] exchangeRaw(byte[] apdu) { + if (transport == null) + throw new IllegalStateException("No transport (probably closed previously)"); + Timber.d("exchangeRaw %02x", apdu[1]); + Instruction ins = Instruction.fromByte(apdu[1]); + if (listener != null) listener.onInstructionSend(ins, apdu); + sniffOut(ins, apdu); + byte[] data = transport.exchange(apdu); + if (listener != null) listener.onInstructionReceive(ins, data); + sniffIn(data); + return data; + } + + private byte[] exchange(byte[] apdu) throws BTChipException { + byte[] response = exchangeRaw(apdu); + if (response.length < 2) { + throw new BTChipException("Truncated response"); + } + lastSW = ((response[response.length - 2] & 0xff) << 8) | + response[response.length - 1] & 0xff; + byte[] result = new byte[response.length - 2]; + System.arraycopy(response, 0, result, 0, response.length - 2); + return result; + } + + private byte[] exchangeCheck(byte[] apdu, int acceptedSW[]) throws BTChipException { + byte[] response = exchange(apdu); + if (acceptedSW == null) { + return response; + } + for (int SW : acceptedSW) { + if (lastSW == SW) { + return response; + } + } + throw new BTChipException("Invalid status", lastSW); + } + + private byte[] exchangeApduNoOpt(Instruction instruction, byte[] data, int acceptedSW[]) + throws BTChipException { + byte[] apdu = new byte[data.length + 6]; + apdu[0] = PROTOCOL_VERSION; + apdu[1] = instruction.getByteValue(); + apdu[2] = 0; // p1 + apdu[3] = 0; // p2 + apdu[4] = (byte) (data.length + 1); // +1 because the opt byte is part of the data + apdu[5] = 0; // opt + System.arraycopy(data, 0, apdu, 6, data.length); + return exchangeCheck(apdu, acceptedSW); + } + + public interface Listener { + void onInstructionSend(Instruction ins, byte[] apdu); + + void onInstructionReceive(Instruction ins, byte[] data); + } + + Listener listener; + + static public void setListener(Listener listener) { + if (Instance != null) { + Instance.listener = listener; + } + } + + static public void unsetListener(Listener listener) { + if ((Instance != null) && (Instance.listener == listener)) + Instance.listener = null; + } + + // very stupid hack to extract the view key + // without messing around with monero core code + // NB: as all the ledger comm can be sniffed off the USB cable - there is no security issue here + private boolean snoopKey = false; + private byte[] key; + + private void initKey() { + key = Helper.hexToBytes("0000000000000000000000000000000000000000000000000000000000000000"); + } + + static public String Key() { + if (Instance != null) { + return Helper.bytesToHex(Instance.key).toLowerCase(); + } else { + return null; + } + } + + private void sniffOut(Instruction ins, byte[] apdu) { + if (ins == Instruction.INS_GET_KEY) { + snoopKey = (apdu[2] == 2); + } + } + + private void sniffIn(byte[] data) { + // stupid hack to extract the view key + // without messing around with monero core code + if (snoopKey) { + if (data.length == 34) { // 32 key + result code 9000 + long sw = ((data[data.length - 2] & 0xff) << 8) | + (data[data.length - 1] & 0xff); + Timber.e("WS %d", sw); + if (sw == SW_OK) { + System.arraycopy(data, 0, key, 0, 32); + } + } + snoopKey = false; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/ledger/LedgerProgressDialog.java b/app/src/main/java/com/m2049r/xmrwallet/ledger/LedgerProgressDialog.java new file mode 100644 index 0000000..0c81cb2 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/ledger/LedgerProgressDialog.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2018 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.ledger; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; + +import com.m2049r.xmrwallet.R; +import com.m2049r.xmrwallet.dialog.ProgressDialog; + +import timber.log.Timber; + +public class LedgerProgressDialog extends ProgressDialog implements Ledger.Listener { + + static public final int TYPE_DEBUG = 0; + static public final int TYPE_RESTORE = 1; + static public final int TYPE_SUBADDRESS = 2; + static public final int TYPE_ACCOUNT = 3; + static public final int TYPE_SEND = 4; + + private final int type; + private Handler uiHandler = new Handler(Looper.getMainLooper()); + + public LedgerProgressDialog(Context context, int type) { + super(context); + this.type = type; + setCancelable(false); + if (type == TYPE_SEND) + setMessage(context.getString(R.string.info_prepare_tx)); + else + setMessage(context.getString(R.string.progress_ledger_progress)); + } + + @Override + public void onBackPressed() { + // prevent back button + } + + private int firstSubaddress = Integer.MAX_VALUE; + + private boolean validate = false; + private boolean validated = false; + + @Override + public void onInstructionSend(final Instruction ins, final byte[] apdu) { + Timber.d("LedgerProgressDialog SEND %s", ins); + uiHandler.post(new Runnable() { + @Override + public void run() { + if (type > TYPE_DEBUG) { + validate = false; + switch (ins) { + case INS_RESET: // ledger may ask for confirmation - maybe a bug? + case INS_GET_KEY: // ledger asks for confirmation to send keys + case INS_DISPLAY_ADDRESS: + setIndeterminate(true); + setMessage(getContext().getString(R.string.progress_ledger_confirm)); + break; + case INS_GET_SUBADDRESS_SPEND_PUBLIC_KEY: // lookahead + //00 4a 00 00 09 00 01000000 30000000 + // 0 1 2 3 4 5 6 7 8 9 a b c d + int account = bytesToInteger(apdu, 6); + int subaddress = bytesToInteger(apdu, 10); + Timber.d("fetching subaddress (%d, %d)", account, subaddress); + switch (type) { + case TYPE_RESTORE: + setProgress(account * Ledger.LOOKAHEAD_SUBADDRESSES + subaddress + 1, + Ledger.LOOKAHEAD_ACCOUNTS * Ledger.LOOKAHEAD_SUBADDRESSES); + setIndeterminate(false); + break; + case TYPE_ACCOUNT: + final int requestedSubaddress = account * Ledger.LOOKAHEAD_SUBADDRESSES + subaddress; + if (firstSubaddress > requestedSubaddress) { + firstSubaddress = requestedSubaddress; + } + setProgress(requestedSubaddress - firstSubaddress + 1, + Ledger.LOOKAHEAD_ACCOUNTS * Ledger.LOOKAHEAD_SUBADDRESSES); + setIndeterminate(false); + break; + case TYPE_SUBADDRESS: + if (firstSubaddress > subaddress) { + firstSubaddress = subaddress; + } + setProgress(subaddress - firstSubaddress + 1, Ledger.LOOKAHEAD_SUBADDRESSES); + setIndeterminate(false); + break; + default: + setIndeterminate(true); + break; + } + setMessage(getContext().getString(R.string.progress_ledger_lookahead)); + break; + case INS_VERIFY_KEY: + setIndeterminate(true); + setMessage(getContext().getString(R.string.progress_ledger_verify)); + break; + case INS_OPEN_TX: + setIndeterminate(true); + setMessage(getContext().getString(R.string.progress_ledger_opentx)); + break; + case INS_MLSAG: + if (validated) { + setIndeterminate(true); + setMessage(getContext().getString(R.string.progress_ledger_mlsag)); + } + break; + case INS_PREFIX_HASH: + if ((apdu[2] != 1) || (apdu[3] != 0)) break; + setIndeterminate(true); + setMessage(getContext().getString(R.string.progress_ledger_confirm)); + break; + case INS_VALIDATE: + if ((apdu[2] != 1) || (apdu[3] != 1)) break; + validate = true; + uiHandler.postDelayed(new Runnable() { + @Override + public void run() { + if (validate) { + setIndeterminate(true); + setMessage(getContext().getString(R.string.progress_ledger_confirm)); + validated = true; + } + } + }, 250); + break; + default: + // ignore others and maintain state + } + } else { + setMessage(ins.name()); + } + } + }); + } + + @Override + public void onInstructionReceive(final Instruction ins, final byte[] data) { + Timber.d("LedgerProgressDialog RECV %s", ins); + uiHandler.post(new Runnable() { + @Override + public void run() { + if (type > TYPE_DEBUG) { + switch (ins) { + case INS_GET_SUBADDRESS_SPEND_PUBLIC_KEY: // lookahead + case INS_VERIFY_KEY: + case INS_GET_CHACHA8_PREKEY: + break; + default: + if (type != TYPE_SEND) + setMessage(getContext().getString(R.string.progress_ledger_progress)); + } + } else { + setMessage("Returned from " + ins.name()); + } + } + }); + } + + // TODO: we use ints in Java but the are signed; accounts & subaddresses are unsigned ... + private int bytesToInteger(byte[] bytes, int offset) { + int result = 0; + for (int i = 3; i >= 0; i--) { + result <<= 8; + result |= (bytes[offset + i] & 0xFF); + } + return result; + } +} \ No newline at end of file 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 0000000..ae1c84f --- /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 new file mode 100644 index 0000000..6ad620a --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/model/PendingTransaction.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.model; + +public class PendingTransaction { + static { + System.loadLibrary("monerujo"); + } + + public long handle; + + PendingTransaction(long handle) { + this.handle = handle; + } + + public enum Status { + Status_Ok, + Status_Error, + Status_Critical + } + + public enum Priority { + Priority_Default(0), + Priority_Low(1), + Priority_Medium(2), + Priority_High(3), + Priority_Last(4); + + public static Priority fromInteger(int n) { + switch (n) { + case 0: + return Priority_Default; + case 1: + return Priority_Low; + case 2: + return Priority_Medium; + case 3: + return Priority_High; + } + return null; + } + + public int getValue() { + return value; + } + + private int value; + + Priority(int value) { + this.value = value; + } + + + } + + public Status getStatus() { + return Status.values()[getStatusJ()]; + } + + public native int getStatusJ(); + + public native String getErrorString(); + + // commit transaction or save to file if filename is provided. + public native boolean commit(String filename, boolean overwrite); + + public native long getAmount(); + + public native long getDust(); + + public native long getFee(); + + public String getFirstTxId() { + String id = getFirstTxIdJ(); + if (id == null) + throw new IndexOutOfBoundsException(); + return id; + } + + public native String getFirstTxIdJ(); + + public native long getTxCount(); + +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/model/TransactionHistory.java b/app/src/main/java/com/m2049r/xmrwallet/model/TransactionHistory.java new file mode 100644 index 0000000..08245f6 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/model/TransactionHistory.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.model; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import timber.log.Timber; + +public class TransactionHistory { + static { + System.loadLibrary("monerujo"); + } + + private long handle; + + int accountIndex; + + public void setAccountFor(Wallet wallet) { + if (accountIndex != wallet.getAccountIndex()) { + this.accountIndex = wallet.getAccountIndex(); + refreshWithNotes(wallet); + } + } + + public TransactionHistory(long handle, int accountIndex) { + this.handle = handle; + this.accountIndex = accountIndex; + } + + private void loadNotes(Wallet wallet) { + for (TransactionInfo info : transactions) { + info.notes = wallet.getUserNote(info.hash); + } + } + + public native int getCount(); // over all accounts/subaddresses + + //private native long getTransactionByIndexJ(int i); + + //private native long getTransactionByIdJ(String id); + + public List getAll() { + return transactions; + } + + private List transactions = new ArrayList<>(); + + void refreshWithNotes(Wallet wallet) { + refresh(); + loadNotes(wallet); + } + + private void refresh() { + List transactionInfos = refreshJ(); + Timber.d("refresh size=%d", transactionInfos.size()); + for (Iterator iterator = transactionInfos.iterator(); iterator.hasNext(); ) { + TransactionInfo info = iterator.next(); + if (info.accountIndex != accountIndex) { + iterator.remove(); + } + } + transactions = transactionInfos; + } + + private native List refreshJ(); +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/model/TransactionInfo.java b/app/src/main/java/com/m2049r/xmrwallet/model/TransactionInfo.java new file mode 100644 index 0000000..4b904e1 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/model/TransactionInfo.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.model; + +import android.os.Parcel; +import android.os.Parcelable; + +import com.m2049r.xmrwallet.data.Subaddress; + +import java.util.List; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +// this is not the TransactionInfo from the API as that is owned by the TransactionHistory +// this is a POJO for the TransactionInfoAdapter +public class TransactionInfo implements Parcelable, Comparable { + public static final int CONFIRMATION = 10; // blocks + + @RequiredArgsConstructor + public enum Direction { + Direction_In(0), + Direction_Out(1); + + public static Direction fromInteger(int n) { + switch (n) { + case 0: + return Direction_In; + case 1: + return Direction_Out; + } + return null; + } + + @Getter + private final int value; + } + + public Direction direction; + public boolean isPending; + public boolean isFailed; + public long amount; + public long fee; + public long blockheight; + public String hash; + public long timestamp; + public String paymentId; + public int accountIndex; + public int addressIndex; + public long confirmations; + public String subaddressLabel; + public List transfers; + + public String txKey = null; + public String notes = null; + public String address = null; + + public TransactionInfo( + int direction, + boolean isPending, + boolean isFailed, + long amount, + long fee, + long blockheight, + String hash, + long timestamp, + String paymentId, + int accountIndex, + int addressIndex, + long confirmations, + String subaddressLabel, + List transfers) { + this.direction = Direction.values()[direction]; + this.isPending = isPending; + this.isFailed = isFailed; + this.amount = amount; + this.fee = fee; + this.blockheight = blockheight; + this.hash = hash; + this.timestamp = timestamp; + this.paymentId = paymentId; + this.accountIndex = accountIndex; + this.addressIndex = addressIndex; + this.confirmations = confirmations; + this.subaddressLabel = subaddressLabel; + this.transfers = transfers; + } + + public boolean isConfirmed() { + return confirmations >= CONFIRMATION; + } + + public String getDisplayLabel() { + if (subaddressLabel.isEmpty() || (Subaddress.DEFAULT_LABEL_FORMATTER.matcher(subaddressLabel).matches())) + return ("#" + addressIndex); + else + return subaddressLabel; + } + + public String toString() { + return direction + "@" + blockheight + " " + amount; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(direction.getValue()); + out.writeByte((byte) (isPending ? 1 : 0)); + out.writeByte((byte) (isFailed ? 1 : 0)); + out.writeLong(amount); + out.writeLong(fee); + out.writeLong(blockheight); + out.writeString(hash); + out.writeLong(timestamp); + out.writeString(paymentId); + out.writeInt(accountIndex); + out.writeInt(addressIndex); + out.writeLong(confirmations); + out.writeString(subaddressLabel); + out.writeList(transfers); + out.writeString(txKey); + out.writeString(notes); + out.writeString(address); + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public TransactionInfo createFromParcel(Parcel in) { + return new TransactionInfo(in); + } + + public TransactionInfo[] newArray(int size) { + return new TransactionInfo[size]; + } + }; + + private TransactionInfo(Parcel in) { + direction = Direction.fromInteger(in.readInt()); + isPending = in.readByte() != 0; + isFailed = in.readByte() != 0; + amount = in.readLong(); + fee = in.readLong(); + blockheight = in.readLong(); + hash = in.readString(); + timestamp = in.readLong(); + paymentId = in.readString(); + accountIndex = in.readInt(); + addressIndex = in.readInt(); + confirmations = in.readLong(); + subaddressLabel = in.readString(); + transfers = in.readArrayList(Transfer.class.getClassLoader()); + txKey = in.readString(); + notes = in.readString(); + address = in.readString(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public int compareTo(TransactionInfo another) { + long b1 = this.timestamp; + long b2 = another.timestamp; + if (b1 > b2) { + return -1; + } else if (b1 < b2) { + return 1; + } else { + return this.hash.compareTo(another.hash); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/model/Transfer.java b/app/src/main/java/com/m2049r/xmrwallet/model/Transfer.java new file mode 100644 index 0000000..27ce6a0 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/model/Transfer.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.model; + +import android.os.Parcel; +import android.os.Parcelable; + +public class Transfer implements Parcelable { + public long amount; + public String address; + + public Transfer(long amount, String address) { + this.amount = amount; + this.address = address; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeLong(amount); + out.writeString(address); + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public Transfer createFromParcel(Parcel in) { + return new Transfer(in); + } + + public Transfer[] newArray(int size) { + return new Transfer[size]; + } + }; + + private Transfer(Parcel in) { + amount = in.readLong(); + address = in.readString(); + } + + @Override + public int describeContents() { + return 0; + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/model/Wallet.java b/app/src/main/java/com/m2049r/xmrwallet/model/Wallet.java new file mode 100644 index 0000000..e85d0f8 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/model/Wallet.java @@ -0,0 +1,507 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.model; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.m2049r.xmrwallet.data.Subaddress; +import com.m2049r.xmrwallet.data.TxData; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import timber.log.Timber; + +public class Wallet { + final static public long SWEEP_ALL = Long.MAX_VALUE; + + static { + System.loadLibrary("monerujo"); + } + + static public class Status { + Status(int status, String errorString) { + this.status = StatusEnum.values()[status]; + this.errorString = errorString; + } + + final private StatusEnum status; + final private String errorString; + @Nullable + private ConnectionStatus connectionStatus; // optional + + public StatusEnum getStatus() { + return status; + } + + public String getErrorString() { + return errorString; + } + + public void setConnectionStatus(@Nullable ConnectionStatus connectionStatus) { + this.connectionStatus = connectionStatus; + } + + @Nullable + public ConnectionStatus getConnectionStatus() { + return connectionStatus; + } + + public boolean isOk() { + return (getStatus() == StatusEnum.Status_Ok) + && ((getConnectionStatus() == null) || + (getConnectionStatus() == ConnectionStatus.ConnectionStatus_Connected)); + } + + @Override + @NonNull + public String toString() { + return "Wallet.Status: " + status + "/" + errorString + "/" + connectionStatus; + } + } + + private int accountIndex = 0; + + public int getAccountIndex() { + return accountIndex; + } + + public void setAccountIndex(int accountIndex) { + Timber.d("setAccountIndex(%d)", accountIndex); + this.accountIndex = accountIndex; + getHistory().setAccountFor(this); + } + + public String getName() { + return new File(getPath()).getName(); + } + + private long handle = 0; + private long listenerHandle = 0; + + Wallet(long handle) { + this.handle = handle; + } + + Wallet(long handle, int accountIndex) { + this.handle = handle; + this.accountIndex = accountIndex; + } + + @RequiredArgsConstructor + @Getter + public enum Device { + Device_Undefined(0, 0), + Device_Software(50, 200), + Device_Ledger(5, 20); + private final int accountLookahead; + private final int subaddressLookahead; + } + + public enum StatusEnum { + Status_Ok, + Status_Error, + Status_Critical + } + + public enum ConnectionStatus { + ConnectionStatus_Disconnected, + ConnectionStatus_Connected, + ConnectionStatus_WrongVersion + } + + public native String getSeed(String offset); + + public native String getSeedLanguage(); + + public native void setSeedLanguage(String language); + + public Status getStatus() { + return statusWithErrorString(); + } + + public Status getFullStatus() { + Wallet.Status walletStatus = statusWithErrorString(); + walletStatus.setConnectionStatus(getConnectionStatus()); + return walletStatus; + } + + private native Status statusWithErrorString(); + + public native synchronized boolean setPassword(String password); + + public String getAddress() { + return getAddress(accountIndex); + } + + public String getAddress(int accountIndex) { + return getAddressJ(accountIndex, 0); + } + + public String getSubaddress(int addressIndex) { + return getAddressJ(accountIndex, addressIndex); + } + + public String getSubaddress(int accountIndex, int addressIndex) { + return getAddressJ(accountIndex, addressIndex); + } + + private native String getAddressJ(int accountIndex, int addressIndex); + + public Subaddress getSubaddressObject(int accountIndex, int subAddressIndex) { + return new Subaddress(accountIndex, subAddressIndex, + getSubaddress(subAddressIndex), getSubaddressLabel(subAddressIndex)); + } + + public Subaddress getSubaddressObject(int subAddressIndex) { + Subaddress subaddress = getSubaddressObject(accountIndex, subAddressIndex); + long amount = 0; + for (TransactionInfo info : getHistory().getAll()) { + if ((info.addressIndex == subAddressIndex) + && (info.direction == TransactionInfo.Direction.Direction_In)) { + amount += info.amount; + } + } + subaddress.setAmount(amount); + return subaddress; + } + + public native String getPath(); + + public NetworkType getNetworkType() { + return NetworkType.fromInteger(nettype()); + } + + public native int nettype(); + +//TODO virtual void hardForkInfo(uint8_t &version, uint64_t &earliest_height) const = 0; +//TODO virtual bool useForkRules(uint8_t version, int64_t early_blocks) const = 0; + + public native String getIntegratedAddress(String payment_id); + + public native String getSecretViewKey(); + + public native String getSecretSpendKey(); + + public boolean store() { + return store(""); + } + + public native synchronized boolean store(String path); + + public boolean close() { + disposePendingTransaction(); + return WalletManager.getInstance().close(this); + } + + public native String getFilename(); + + // virtual std::string keysFilename() const = 0; + public boolean init(long upper_transaction_size_limit) { + return initJ(WalletManager.getInstance().getDaemonAddress(), upper_transaction_size_limit, + WalletManager.getInstance().getDaemonUsername(), + WalletManager.getInstance().getDaemonPassword()); + } + + private native boolean initJ(String daemon_address, long upper_transaction_size_limit, + String daemon_username, String daemon_password); + +// virtual bool createWatchOnly(const std::string &path, const std::string &password, const std::string &language) const = 0; +// virtual void setRefreshFromBlockHeight(uint64_t refresh_from_block_height) = 0; + + public native void setRestoreHeight(long height); + + public native long getRestoreHeight(); + + // virtual void setRecoveringFromSeed(bool recoveringFromSeed) = 0; +// virtual bool connectToDaemon() = 0; + + public ConnectionStatus getConnectionStatus() { + int s = getConnectionStatusJ(); + return Wallet.ConnectionStatus.values()[s]; + } + + private native int getConnectionStatusJ(); + +//TODO virtual void setTrustedDaemon(bool arg) = 0; +//TODO virtual bool trustedDaemon() const = 0; + + public native boolean setProxy(String address); + + public long getBalance() { + return getBalance(accountIndex); + } + + public native long getBalance(int accountIndex); + + public native long getBalanceAll(); + + public long getUnlockedBalance() { + return getUnlockedBalance(accountIndex); + } + + public native long getUnlockedBalanceAll(); + + public native long getUnlockedBalance(int accountIndex); + + public native boolean isWatchOnly(); + + public native long getBlockChainHeight(); + + public native long getApproximateBlockChainHeight(); + + public native long getDaemonBlockChainHeight(); + + public native long getDaemonBlockChainTargetHeight(); + + boolean synced = false; + + public boolean isSynchronized() { + return synced; + } + + public void setSynchronized() { + this.synced = true; + } + + public static native String getDisplayAmount(long amount); + + public static native long getAmountFromString(String amount); + + public static native long getAmountFromDouble(double amount); + + public static native String generatePaymentId(); + + public static native boolean isPaymentIdValid(String payment_id); + + public static boolean isAddressValid(String address) { + return isAddressValid(address, WalletManager.getInstance().getNetworkType().getValue()); + } + + public static native boolean isAddressValid(String address, int networkType); + + public static native String getPaymentIdFromAddress(String address, int networkType); + + public static native long getMaximumAllowedAmount(); + + public native void startRefresh(); + + public native void pauseRefresh(); + + public native boolean refresh(); + + public native void refreshAsync(); + + public native void rescanBlockchainAsyncJ(); + + public void rescanBlockchainAsync() { + synced = false; + rescanBlockchainAsyncJ(); + } + +//TODO virtual void setAutoRefreshInterval(int millis) = 0; +//TODO virtual int autoRefreshInterval() const = 0; + + + private PendingTransaction pendingTransaction = null; + + public PendingTransaction getPendingTransaction() { + return pendingTransaction; + } + + public void disposePendingTransaction() { + if (pendingTransaction != null) { + disposeTransaction(pendingTransaction); + pendingTransaction = null; + } + } + + public PendingTransaction createTransaction(TxData txData) { + return createTransaction( + txData.getDestinationAddress(), + txData.getAmount(), + txData.getMixin(), + txData.getPriority()); + } + + public PendingTransaction createTransaction(String dst_addr, + long amount, int mixin_count, + PendingTransaction.Priority priority) { + disposePendingTransaction(); + int _priority = priority.getValue(); + long txHandle = + (amount == SWEEP_ALL ? + createSweepTransaction(dst_addr, "", mixin_count, _priority, + accountIndex) : + createTransactionJ(dst_addr, "", amount, mixin_count, _priority, + accountIndex)); + pendingTransaction = new PendingTransaction(txHandle); + return pendingTransaction; + } + + private native long createTransactionJ(String dst_addr, String payment_id, + long amount, int mixin_count, + int priority, int accountIndex); + + private native long createSweepTransaction(String dst_addr, String payment_id, + int mixin_count, + int priority, int accountIndex); + + + public PendingTransaction createSweepUnmixableTransaction() { + disposePendingTransaction(); + long txHandle = createSweepUnmixableTransactionJ(); + pendingTransaction = new PendingTransaction(txHandle); + return pendingTransaction; + } + + private native long createSweepUnmixableTransactionJ(); + +//virtual UnsignedTransaction * loadUnsignedTx(const std::string &unsigned_filename) = 0; +//virtual bool submitTransaction(const std::string &fileName) = 0; + + public native void disposeTransaction(PendingTransaction pendingTransaction); + +//virtual bool exportKeyImages(const std::string &filename) = 0; +//virtual bool importKeyImages(const std::string &filename) = 0; + + +//virtual TransactionHistory * history() const = 0; + + private TransactionHistory history = null; + + public TransactionHistory getHistory() { + if (history == null) { + history = new TransactionHistory(getHistoryJ(), accountIndex); + } + return history; + } + + private native long getHistoryJ(); + + public void refreshHistory() { + getHistory().refreshWithNotes(this); + } + +//virtual AddressBook * addressBook() const = 0; +//virtual void setListener(WalletListener *) = 0; + + private native long setListenerJ(WalletListener listener); + + public void setListener(WalletListener listener) { + this.listenerHandle = setListenerJ(listener); + } + + public native int getDefaultMixin(); + + public native void setDefaultMixin(int mixin); + + public native boolean setUserNote(String txid, String note); + + public native String getUserNote(String txid); + + public native String getTxKey(String txid); + +//virtual std::string signMessage(const std::string &message) = 0; +//virtual bool verifySignedMessage(const std::string &message, const std::string &addres, const std::string &signature) const = 0; + +//virtual bool parse_uri(const std::string &uri, std::string &address, std::string &payment_id, uint64_t &tvAmount, std::string &tx_description, std::string &recipient_name, std::vector &unknown_parameters, std::string &error) = 0; +//virtual bool rescanSpent() = 0; + + private static final String NEW_ACCOUNT_NAME = "Untitled account"; // src/wallet/wallet2.cpp:941 + + public void addAccount() { + addAccount(NEW_ACCOUNT_NAME); + } + + public native void addAccount(String label); + + public String getAccountLabel() { + return getAccountLabel(accountIndex); + } + + public String getAccountLabel(int accountIndex) { + String label = getSubaddressLabel(accountIndex, 0); + if (label.equals(NEW_ACCOUNT_NAME)) { + String address = getAddress(accountIndex); + int len = address.length(); + label = address.substring(0, 6) + + "\u2026" + address.substring(len - 6, len); + } + return label; + } + + public String getSubaddressLabel(int addressIndex) { + return getSubaddressLabel(accountIndex, addressIndex); + } + + public native String getSubaddressLabel(int accountIndex, int addressIndex); + + public void setAccountLabel(String label) { + setAccountLabel(accountIndex, label); + } + + public void setAccountLabel(int accountIndex, String label) { + setSubaddressLabel(accountIndex, 0, label); + } + + public void setSubaddressLabel(int addressIndex, String label) { + setSubaddressLabel(accountIndex, addressIndex, label); + refreshHistory(); + } + + public native void setSubaddressLabel(int accountIndex, int addressIndex, String label); + + public native int getNumAccounts(); + + public int getNumSubaddresses() { + return getNumSubaddresses(accountIndex); + } + + public native int getNumSubaddresses(int accountIndex); + + public String getNewSubaddress() { + return getNewSubaddress(accountIndex); + } + + public String getNewSubaddress(int accountIndex) { + String timeStamp = new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss", Locale.US).format(new Date()); + addSubaddress(accountIndex, timeStamp); + String subaddress = getLastSubaddress(accountIndex); + Timber.d("%d: %s", getNumSubaddresses(accountIndex) - 1, subaddress); + return subaddress; + } + + public native void addSubaddress(int accountIndex, String label); + + public String getLastSubaddress(int accountIndex) { + return getSubaddress(accountIndex, getNumSubaddresses(accountIndex) - 1); + } + + public Wallet.Device getDeviceType() { + int device = getDeviceTypeJ(); + return Wallet.Device.values()[device + 1]; // mapping is monero+1=android + } + + private native int getDeviceTypeJ(); + +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/model/WalletListener.java b/app/src/main/java/com/m2049r/xmrwallet/model/WalletListener.java new file mode 100644 index 0000000..f7ee66f --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/model/WalletListener.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.model; + +public interface WalletListener { + /** + * moneySpent - called when money spent + * @param txId - transaction id + * @param amount - tvAmount + */ + void moneySpent(String txId, long amount); + + /** + * moneyReceived - called when money received + * @param txId - transaction id + * @param amount - tvAmount + */ + void moneyReceived(String txId, long amount); + + /** + * unconfirmedMoneyReceived - called when payment arrived in tx pool + * @param txId - transaction id + * @param amount - tvAmount + */ + void unconfirmedMoneyReceived(String txId, long amount); + + /** + * newBlock - called when new block received + * @param height - block height + */ + void newBlock(long height); + + /** + * updated - generic callback, called when any event (sent/received/block reveived/etc) happened with the wallet; + */ + void updated(); + + /** + * refreshed - called when wallet refreshed by background thread or explicitly refreshed by calling "refresh" synchronously + */ + void refreshed(); + +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/model/WalletManager.java b/app/src/main/java/com/m2049r/xmrwallet/model/WalletManager.java new file mode 100644 index 0000000..f5aa743 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/model/WalletManager.java @@ -0,0 +1,341 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.model; + +import com.m2049r.xmrwallet.XmrWalletApplication; +import com.m2049r.xmrwallet.data.Node; +import com.m2049r.xmrwallet.ledger.Ledger; +import com.m2049r.xmrwallet.util.RestoreHeight; + +import java.io.File; +import java.io.FilenameFilter; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +import lombok.Getter; +import timber.log.Timber; + +public class WalletManager { + + static { + System.loadLibrary("monerujo"); + } + + // no need to keep a reference to the REAL WalletManager (we get it every tvTime we need it) + private static WalletManager Instance = null; + + public static synchronized WalletManager getInstance() { + if (WalletManager.Instance == null) { + WalletManager.Instance = new WalletManager(); + } + + return WalletManager.Instance; + } + + public String addressPrefix() { + return addressPrefix(getNetworkType()); + } + + static public String addressPrefix(NetworkType networkType) { + switch (networkType) { + case NetworkType_Testnet: + return "9A-"; + case NetworkType_Mainnet: + return "4-"; + case NetworkType_Stagenet: + return "5-"; + default: + throw new IllegalStateException("Unsupported Network: " + networkType); + } + } + + private Wallet managedWallet = null; + + public Wallet getWallet() { + return managedWallet; + } + + private void manageWallet(Wallet wallet) { + Timber.d("Managing %s", wallet.getName()); + managedWallet = wallet; + } + + private void unmanageWallet(Wallet wallet) { + if (wallet == null) { + throw new IllegalArgumentException("Cannot unmanage null!"); + } + if (getWallet() == null) { + throw new IllegalStateException("No wallet under management!"); + } + if (getWallet() != wallet) { + throw new IllegalStateException(wallet.getName() + " not under management!"); + } + Timber.d("Unmanaging %s", managedWallet.getName()); + managedWallet = null; + } + + public Wallet createWallet(File aFile, String password, String language, long height) { + long walletHandle = createWalletJ(aFile.getAbsolutePath(), password, language, getNetworkType().getValue()); + Wallet wallet = new Wallet(walletHandle); + manageWallet(wallet); + if (wallet.getStatus().isOk()) { + // (Re-)Estimate restore height based on what we know + final long oldHeight = wallet.getRestoreHeight(); + // Go back 4 days if we don't have a precise restore height + Calendar restoreDate = Calendar.getInstance(); + restoreDate.add(Calendar.DAY_OF_MONTH, -4); + final long restoreHeight = + (height > -1) ? height : RestoreHeight.getInstance().getHeight(restoreDate.getTime()); + wallet.setRestoreHeight(restoreHeight); + Timber.d("Changed Restore Height from %d to %d", oldHeight, wallet.getRestoreHeight()); + wallet.setPassword(password); // this rewrites the keys file (which contains the restore height) + } else + Timber.e(wallet.getStatus().toString()); + return wallet; + } + + private native long createWalletJ(String path, String password, String language, int networkType); + + public Wallet openAccount(String path, int accountIndex, String password) { + long walletHandle = openWalletJ(path, password, getNetworkType().getValue()); + Wallet wallet = new Wallet(walletHandle, accountIndex); + manageWallet(wallet); + return wallet; + } + + public Wallet openWallet(String path, String password) { + long walletHandle = openWalletJ(path, password, getNetworkType().getValue()); + Wallet wallet = new Wallet(walletHandle); + manageWallet(wallet); + return wallet; + } + + private native long openWalletJ(String path, String password, int networkType); + + public Wallet recoveryWallet(File aFile, String password, + String mnemonic, String offset, + long restoreHeight) { + long walletHandle = recoveryWalletJ(aFile.getAbsolutePath(), password, + mnemonic, offset, + getNetworkType().getValue(), restoreHeight); + Wallet wallet = new Wallet(walletHandle); + manageWallet(wallet); + return wallet; + } + + private native long recoveryWalletJ(String path, String password, + String mnemonic, String offset, + int networkType, long restoreHeight); + + public Wallet createWalletWithKeys(File aFile, String password, String language, long restoreHeight, + String addressString, String viewKeyString, String spendKeyString) { + long walletHandle = createWalletFromKeysJ(aFile.getAbsolutePath(), password, + language, getNetworkType().getValue(), restoreHeight, + addressString, viewKeyString, spendKeyString); + Wallet wallet = new Wallet(walletHandle); + manageWallet(wallet); + return wallet; + } + + private native long createWalletFromKeysJ(String path, String password, + String language, + int networkType, + long restoreHeight, + String addressString, + String viewKeyString, + String spendKeyString); + + public Wallet createWalletFromDevice(File aFile, String password, long restoreHeight, + String deviceName) { + long walletHandle = createWalletFromDeviceJ(aFile.getAbsolutePath(), password, + getNetworkType().getValue(), deviceName, restoreHeight, + Ledger.SUBADDRESS_LOOKAHEAD); + Wallet wallet = new Wallet(walletHandle); + manageWallet(wallet); + return wallet; + } + + private native long createWalletFromDeviceJ(String path, String password, + int networkType, + String deviceName, + long restoreHeight, + String subaddressLookahead); + + + public native boolean closeJ(Wallet wallet); + + public boolean close(Wallet wallet) { + unmanageWallet(wallet); + boolean closed = closeJ(wallet); + if (!closed) { + // in case we could not close it + // we manage it again + manageWallet(wallet); + } + return closed; + } + + public boolean walletExists(File aFile) { + return walletExists(aFile.getAbsolutePath()); + } + + public native boolean walletExists(String path); + + public native boolean verifyWalletPassword(String keys_file_name, String password, boolean watch_only); + + public boolean verifyWalletPasswordOnly(String keys_file_name, String password) { + return queryWalletDeviceJ(keys_file_name, password) >= 0; + } + + public Wallet.Device queryWalletDevice(String keys_file_name, String password) { + int device = queryWalletDeviceJ(keys_file_name, password); + return Wallet.Device.values()[device + 1]; // mapping is monero+1=android + } + + private native int queryWalletDeviceJ(String keys_file_name, String password); + + //public native List findWallets(String path); // this does not work - some error in boost + + public class WalletInfo implements Comparable { + @Getter + final private File path; + @Getter + final private String name; + + public WalletInfo(File wallet) { + path = wallet.getParentFile(); + name = wallet.getName(); + } + + @Override + public int compareTo(WalletInfo another) { + return name.toLowerCase().compareTo(another.name.toLowerCase()); + } + } + + public List findWallets(File path) { + List wallets = new ArrayList<>(); + Timber.d("Scanning: %s", path.getAbsolutePath()); + File[] found = path.listFiles(new FilenameFilter() { + public boolean accept(File dir, String filename) { + return filename.endsWith(".keys"); + } + }); + for (int i = 0; i < found.length; i++) { + String filename = found[i].getName(); + File f = new File(found[i].getParent(), filename.substring(0, filename.length() - 5)); // 5 is length of ".keys"+1 + wallets.add(new WalletInfo(f)); + } + return wallets; + } + +//TODO virtual bool checkPayment(const std::string &address, const std::string &txid, const std::string &txkey, const std::string &daemon_address, uint64_t &received, uint64_t &height, std::string &error) const = 0; + + private String daemonAddress = null; + private final NetworkType networkType = XmrWalletApplication.getNetworkType(); + + public NetworkType getNetworkType() { + return networkType; + } + + // this should not be called on the main thread as it connects to the node (and takes a long time) + public void setDaemon(Node node) { + if (node != null) { + this.daemonAddress = node.getAddress(); + if (networkType != node.getNetworkType()) + throw new IllegalArgumentException("network type does not match"); + this.daemonUsername = node.getUsername(); + this.daemonPassword = node.getPassword(); + setDaemonAddressJ(daemonAddress); + } else { + this.daemonAddress = null; + this.daemonUsername = ""; + this.daemonPassword = ""; + //setDaemonAddressJ(""); // don't disconnect as monero code blocks for many seconds! + //TODO: need to do something about that later + } + } + + public String getDaemonAddress() { + if (daemonAddress == null) { + throw new IllegalStateException("use setDaemon() to initialise daemon and net first!"); + } + return this.daemonAddress; + } + + private native void setDaemonAddressJ(String address); + + private String daemonUsername = ""; + + public String getDaemonUsername() { + return daemonUsername; + } + + private String daemonPassword = ""; + + public String getDaemonPassword() { + return daemonPassword; + } + + public native int getDaemonVersion(); + + public native long getBlockchainHeight(); + + public native long getBlockchainTargetHeight(); + + public native long getNetworkDifficulty(); + + public native double getMiningHashRate(); + + public native long getBlockTarget(); + + public native boolean isMining(); + + public native boolean startMining(String address, boolean background_mining, boolean ignore_battery); + + public native boolean stopMining(); + + public native String resolveOpenAlias(String address, boolean dnssec_valid); + + public native boolean setProxy(String address); + +//TODO static std::tuple checkUpdates(const std::string &software, const std::string &subdir); + + static public native void initLogger(String argv0, String defaultLogBaseName); + + //TODO: maybe put these in an enum like in monero core - but why? + static public int LOGLEVEL_SILENT = -1; + static public int LOGLEVEL_WARN = 0; + static public int LOGLEVEL_INFO = 1; + static public int LOGLEVEL_DEBUG = 2; + static public int LOGLEVEL_TRACE = 3; + static public int LOGLEVEL_MAX = 4; + + static public native void setLogLevel(int level); + + static public native void logDebug(String category, String message); + + static public native void logInfo(String category, String message); + + static public native void logWarning(String category, String message); + + static public native void logError(String category, String message); + + static public native String moneroVersion(); +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/onboarding/OnBoardingActivity.java b/app/src/main/java/com/m2049r/xmrwallet/onboarding/OnBoardingActivity.java new file mode 100644 index 0000000..c2793a1 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/onboarding/OnBoardingActivity.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2018-2020 EarlOfEgo, m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.onboarding; + +import android.content.Intent; +import android.os.Bundle; +import android.util.TypedValue; +import android.view.View; +import android.widget.Button; +import android.widget.LinearLayout; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.viewpager.widget.ViewPager; + +import com.google.android.material.tabs.TabLayout; +import com.m2049r.xmrwallet.LoginActivity; +import com.m2049r.xmrwallet.R; +import com.m2049r.xmrwallet.util.KeyStoreHelper; + +public class OnBoardingActivity extends AppCompatActivity implements OnBoardingAdapter.Listener { + + private OnBoardingViewPager pager; + private OnBoardingAdapter pagerAdapter; + private Button nextButton; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_on_boarding); + + nextButton = findViewById(R.id.buttonNext); + + pager = findViewById(R.id.pager); + pagerAdapter = new OnBoardingAdapter(this, this); + pager.setAdapter(pagerAdapter); + int pixels = (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, 16, getResources().getDisplayMetrics()); + pager.setPageMargin(pixels); + pager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { + @Override + public void onPageSelected(int position) { + setButtonState(position); + } + }); + + final TabLayout tabLayout = (TabLayout) findViewById(R.id.tabLayout); + if (pagerAdapter.getCount() > 1) { + tabLayout.setupWithViewPager(pager, true); + LinearLayout tabStrip = ((LinearLayout) tabLayout.getChildAt(0)); + for (int i = 0; i < tabStrip.getChildCount(); i++) { + tabStrip.getChildAt(i).setClickable(false); + } + } else { + tabLayout.setVisibility(View.GONE); + } + + nextButton.setOnClickListener(v -> { + final int item = pager.getCurrentItem(); + if (item + 1 >= pagerAdapter.getCount()) { + finishOnboarding(); + } else { + pager.setCurrentItem(item + 1); + } + }); + + // let old users who have fingerprint wallets already agree for fingerprint sending + OnBoardingScreen.FPSEND.setMustAgree(KeyStoreHelper.hasStoredPasswords(this)); + + for (int i = 0; i < OnBoardingScreen.values().length; i++) { + agreed[i] = !OnBoardingScreen.values()[i].isMustAgree(); + } + + setButtonState(0); + } + + private void finishOnboarding() { + nextButton.setEnabled(false); + OnBoardingManager.setOnBoardingShown(getApplicationContext()); + startActivity(new Intent(this, LoginActivity.class)); + finish(); + } + + boolean[] agreed = new boolean[OnBoardingScreen.values().length]; + + @Override + public void setAgreeClicked(int position, boolean isChecked) { + agreed[position] = isChecked; + setButtonState(position); + } + + @Override + public boolean isAgreeClicked(int position) { + return agreed[position]; + } + + @Override + public void setButtonState(int position) { + nextButton.setEnabled(agreed[position]); + if (nextButton.isEnabled()) + pager.setAllowedSwipeDirection(OnBoardingViewPager.SwipeDirection.ALL); + else + pager.setAllowedSwipeDirection(OnBoardingViewPager.SwipeDirection.LEFT); + if (pager.getCurrentItem() + 1 == pagerAdapter.getCount()) { // last page + nextButton.setText(R.string.onboarding_button_ready); + } else { + nextButton.setText(R.string.onboarding_button_next); + } + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/onboarding/OnBoardingAdapter.java b/app/src/main/java/com/m2049r/xmrwallet/onboarding/OnBoardingAdapter.java new file mode 100644 index 0000000..adfc7d9 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/onboarding/OnBoardingAdapter.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2018-2020 EarlOfEgo, m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.onboarding; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.viewpager.widget.PagerAdapter; + +import com.m2049r.xmrwallet.R; + +import timber.log.Timber; + +public class OnBoardingAdapter extends PagerAdapter { + + interface Listener { + void setAgreeClicked(int position, boolean isChecked); + + boolean isAgreeClicked(int position); + + void setButtonState(int position); + } + + private final Context context; + private Listener listener; + + OnBoardingAdapter(final Context context, final Listener listener) { + this.context = context; + this.listener = listener; + } + + @NonNull + @Override + public Object instantiateItem(@NonNull ViewGroup collection, int position) { + LayoutInflater inflater = LayoutInflater.from(context); + final View view = inflater.inflate(R.layout.view_onboarding, collection, false); + final OnBoardingScreen onBoardingScreen = OnBoardingScreen.values()[position]; + + final Drawable drawable = ContextCompat.getDrawable(context, onBoardingScreen.getDrawable()); + ((ImageView) view.findViewById(R.id.onboardingImage)).setImageDrawable(drawable); + ((TextView) view.findViewById(R.id.onboardingTitle)).setText(onBoardingScreen.getTitle()); + ((TextView) view.findViewById(R.id.onboardingInformation)).setText(onBoardingScreen.getInformation()); + if (onBoardingScreen.isMustAgree()) { + final CheckBox agree = ((CheckBox) view.findViewById(R.id.onboardingAgree)); + agree.setVisibility(View.VISIBLE); + agree.setChecked(listener.isAgreeClicked(position)); + agree.setOnClickListener(v -> { + listener.setAgreeClicked(position, ((CheckBox) v).isChecked()); + }); + } + collection.addView(view); + return view; + } + + @Override + public int getCount() { + return OnBoardingScreen.values().length; + } + + @Override + public void destroyItem(@NonNull ViewGroup collection, int position, @NonNull Object view) { + Timber.d("destroy " + position); + collection.removeView((View) view); + } + + @Override + public boolean isViewFromObject(@NonNull final View view, @NonNull final Object object) { + return view == object; + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/onboarding/OnBoardingManager.java b/app/src/main/java/com/m2049r/xmrwallet/onboarding/OnBoardingManager.java new file mode 100644 index 0000000..eb28331 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/onboarding/OnBoardingManager.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2018-2020 EarlOfEgo, m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.onboarding; + +import android.content.Context; +import android.content.SharedPreferences; + +import com.m2049r.xmrwallet.util.KeyStoreHelper; + +import java.util.Date; + +import timber.log.Timber; + +public class OnBoardingManager { + + private static final String PREFS_ONBOARDING = "PREFS_ONBOARDING"; + private static final String ONBOARDING_SHOWN = "ONBOARDING_SHOWN"; + + public static boolean shouldShowOnBoarding(final Context context) { + return !getSharedPreferences(context).contains(ONBOARDING_SHOWN); + } + + public static void setOnBoardingShown(final Context context) { + Timber.d("Set onboarding shown."); + SharedPreferences sharedPreferences = getSharedPreferences(context); + sharedPreferences.edit().putLong(ONBOARDING_SHOWN, new Date().getTime()).apply(); + } + + public static void clearOnBoardingShown(final Context context) { + SharedPreferences sharedPreferences = getSharedPreferences(context); + sharedPreferences.edit().remove(ONBOARDING_SHOWN).apply(); + } + + private static SharedPreferences getSharedPreferences(final Context context) { + return context.getSharedPreferences(PREFS_ONBOARDING, Context.MODE_PRIVATE); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/onboarding/OnBoardingScreen.java b/app/src/main/java/com/m2049r/xmrwallet/onboarding/OnBoardingScreen.java new file mode 100644 index 0000000..c6227d5 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/onboarding/OnBoardingScreen.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2018-2020 EarlOfEgo, m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.onboarding; + +import com.m2049r.xmrwallet.R; + +enum OnBoardingScreen { + WELCOME(R.string.onboarding_welcome_title, R.string.onboarding_welcome_information, R.drawable.ic_onboarding_welcome, false), + SEED(R.string.onboarding_seed_title, R.string.onboarding_seed_information, R.drawable.ic_onboarding_seed, true), + FPSEND(R.string.onboarding_fpsend_title, R.string.onboarding_fpsend_information, R.drawable.ic_onboarding_fingerprint, false), + XMRTO(R.string.onboarding_xmrto_title, R.string.onboarding_xmrto_information, R.drawable.ic_onboarding_xmrto, false), + NODES(R.string.onboarding_nodes_title, R.string.onboarding_nodes_information, R.drawable.ic_onboarding_nodes, false); + + private final int title; + private final int information; + private final int drawable; + private boolean mustAgree; + + OnBoardingScreen(final int title, final int information, final int drawable, final boolean mustAgree) { + this.title = title; + this.information = information; + this.drawable = drawable; + this.mustAgree = mustAgree; + } + + public int getTitle() { + return title; + } + + public int getInformation() { + return information; + } + + public int getDrawable() { + return drawable; + } + + public boolean isMustAgree() { + return mustAgree; + } + + public boolean setMustAgree(boolean mustAgree) { + return this.mustAgree = mustAgree; + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/onboarding/OnBoardingViewPager.java b/app/src/main/java/com/m2049r/xmrwallet/onboarding/OnBoardingViewPager.java new file mode 100644 index 0000000..3e26352 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/onboarding/OnBoardingViewPager.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2020 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// based on https://stackoverflow.com/a/34076649 + +package com.m2049r.xmrwallet.onboarding; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import androidx.viewpager.widget.ViewPager; + +public class OnBoardingViewPager extends ViewPager { + + public enum SwipeDirection { + ALL, LEFT, RIGHT, NONE; + } + + private float initialXValue; + private SwipeDirection direction; + + public OnBoardingViewPager(Context context, AttributeSet attrs) { + super(context, attrs); + this.direction = SwipeDirection.ALL; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (this.IsSwipeAllowed(event)) { + return super.onTouchEvent(event); + } + + return false; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + if (this.IsSwipeAllowed(event)) { + return super.onInterceptTouchEvent(event); + } + + return false; + } + + private boolean IsSwipeAllowed(MotionEvent event) { + if (this.direction == SwipeDirection.ALL) return true; + + if (direction == SwipeDirection.NONE)//disable any swipe + return false; + + if (event.getAction() == MotionEvent.ACTION_DOWN) { + initialXValue = event.getX(); + return true; + } + + if (event.getAction() == MotionEvent.ACTION_MOVE) { + float diffX = event.getX() - initialXValue; + if (diffX > 0 && direction == SwipeDirection.RIGHT) { + // swipe from left to right detected + return false; + } else if (diffX < 0 && direction == SwipeDirection.LEFT) { + // swipe from right to left detected + return false; + } + } + + return true; + } + + public void setAllowedSwipeDirection(SwipeDirection direction) { + this.direction = direction; + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/service/MoneroHandlerThread.java b/app/src/main/java/com/m2049r/xmrwallet/service/MoneroHandlerThread.java new file mode 100644 index 0000000..79ac246 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/service/MoneroHandlerThread.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.service; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.Process; + + +/** + * Handy class for starting a new thread that has a looper. The looper can then be + * used to create handler classes. Note that start() must still be called. + * The started Thread has a stck size of STACK_SIZE (=5MB) + */ +public class MoneroHandlerThread extends Thread { + // from src/cryptonote_config.h + static public final long THREAD_STACK_SIZE = 5 * 1024 * 1024; + private int mPriority; + private int mTid = -1; + private Looper mLooper; + + public MoneroHandlerThread(String name) { + super(null, null, name, THREAD_STACK_SIZE); + mPriority = Process.THREAD_PRIORITY_DEFAULT; + } + + /** + * Constructs a MoneroHandlerThread. + * + * @param name + * @param priority The priority to run the thread at. The value supplied must be from + * {@link android.os.Process} and not from java.lang.Thread. + */ + MoneroHandlerThread(String name, int priority) { + super(null, null, name, THREAD_STACK_SIZE); + mPriority = priority; + } + + /** + * Call back method that can be explicitly overridden if needed to execute some + * setup before Looper loops. + */ + + private void onLooperPrepared() { + } + + @Override + public void run() { + mTid = Process.myTid(); + Looper.prepare(); + synchronized (this) { + mLooper = Looper.myLooper(); + notifyAll(); + } + Process.setThreadPriority(mPriority); + onLooperPrepared(); + Looper.loop(); + mTid = -1; + } + + /** + * This method returns the Looper associated with this thread. If this thread not been started + * or for any reason is isAlive() returns false, this method will return null. If this thread + * has been started, this method will block until the looper has been initialized. + * + * @return The looper. + */ + Looper getLooper() { + if (!isAlive()) { + return null; + } + + // If the thread has been started, wait until the looper has been created. + synchronized (this) { + while (isAlive() && mLooper == null) { + try { + wait(); + } catch (InterruptedException e) { + } + } + } + return mLooper; + } + + /** + * Quits the handler thread's looper. + *

    + * Causes the handler thread's looper to terminate without processing any + * more messages in the message queue. + *

    + * Any attempt to post messages to the queue after the looper is asked to quit will fail. + * For example, the {@link Handler#sendMessage(Message)} method will return false. + *

    + * Using this method may be unsafe because some messages may not be delivered + * before the looper terminates. Consider using {@link #quitSafely} instead to ensure + * that all pending work is completed in an orderly manner. + *

    + * + * @return True if the looper looper has been asked to quit or false if the + * thread had not yet started running. + * @see #quitSafely + */ + public boolean quit() { + Looper looper = getLooper(); + if (looper != null) { + looper.quit(); + return true; + } + return false; + } + + /** + * Quits the handler thread's looper safely. + *

    + * Causes the handler thread's looper to terminate as soon as all remaining messages + * in the message queue that are already due to be delivered have been handled. + * Pending delayed messages with due times in the future will not be delivered. + *

    + * Any attempt to post messages to the queue after the looper is asked to quit will fail. + * For example, the {@link Handler#sendMessage(Message)} method will return false. + *

    + * If the thread has not been started or has finished (that is if + * {@link #getLooper} returns null), then false is returned. + * Otherwise the looper is asked to quit and true is returned. + *

    + * + * @return True if the looper looper has been asked to quit or false if the + * thread had not yet started running. + */ + public boolean quitSafely() { + Looper looper = getLooper(); + if (looper != null) { + looper.quitSafely(); + return true; + } + return false; + } + + /** + * Returns the identifier of this thread. See Process.myTid(). + */ + public int getThreadId() { + return mTid; + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/service/WalletService.java b/app/src/main/java/com/m2049r/xmrwallet/service/WalletService.java new file mode 100644 index 0000000..ece003e --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/service/WalletService.java @@ -0,0 +1,595 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.service; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Binder; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.Process; + +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.core.app.NotificationCompat; + +import com.m2049r.xmrwallet.R; +import com.m2049r.xmrwallet.WalletActivity; +import com.m2049r.xmrwallet.data.TxData; +import com.m2049r.xmrwallet.model.PendingTransaction; +import com.m2049r.xmrwallet.model.Wallet; +import com.m2049r.xmrwallet.model.WalletListener; +import com.m2049r.xmrwallet.model.WalletManager; +import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.LocaleHelper; +import com.m2049r.xmrwallet.util.NetCipherHelper; + +import timber.log.Timber; + +public class WalletService extends Service { + public static boolean Running = false; + + final static int NOTIFICATION_ID = 2049; + final static String CHANNEL_ID = "m_service"; + + public static final String REQUEST_WALLET = "wallet"; + public static final String REQUEST = "request"; + + public static final String REQUEST_CMD_LOAD = "load"; + public static final String REQUEST_CMD_LOAD_PW = "walletPassword"; + + public static final String REQUEST_CMD_STORE = "store"; + + public static final String REQUEST_CMD_TX = "createTX"; + public static final String REQUEST_CMD_TX_DATA = "data"; + public static final String REQUEST_CMD_TX_TAG = "tag"; + + public static final String REQUEST_CMD_SWEEP = "sweepTX"; + + public static final String REQUEST_CMD_SEND = "send"; + public static final String REQUEST_CMD_SEND_NOTES = "notes"; + + public static final int START_SERVICE = 1; + public static final int STOP_SERVICE = 2; + + private MyWalletListener listener = null; + + private class MyWalletListener implements WalletListener { + boolean updated = true; + + void start() { + Timber.d("MyWalletListener.start()"); + Wallet wallet = getWallet(); + if (wallet == null) throw new IllegalStateException("No wallet!"); + wallet.setListener(this); + wallet.startRefresh(); + } + + void stop() { + Timber.d("MyWalletListener.stop()"); + Wallet wallet = getWallet(); + if (wallet == null) throw new IllegalStateException("No wallet!"); + wallet.pauseRefresh(); + wallet.setListener(null); + } + + // WalletListener callbacks + public void moneySpent(String txId, long amount) { + Timber.d("moneySpent() %d @ %s", amount, txId); + } + + public void moneyReceived(String txId, long amount) { + Timber.d("moneyReceived() %d @ %s", amount, txId); + } + + public void unconfirmedMoneyReceived(String txId, long amount) { + Timber.d("unconfirmedMoneyReceived() %d @ %s", amount, txId); + } + + private long lastBlockTime = 0; + private int lastTxCount = 0; + + public void newBlock(long height) { + final Wallet wallet = getWallet(); + if (wallet == null) throw new IllegalStateException("No wallet!"); + // don't flood with an update for every block ... + if (lastBlockTime < System.currentTimeMillis() - 2000) { + lastBlockTime = System.currentTimeMillis(); + Timber.d("newBlock() @ %d with observer %s", height, observer); + if (observer != null) { + boolean fullRefresh = false; + updateDaemonState(wallet, wallet.isSynchronized() ? height : 0); + if (!wallet.isSynchronized()) { + updated = true; + // we want to see our transactions as they come in + wallet.refreshHistory(); + int txCount = wallet.getHistory().getCount(); + if (txCount > lastTxCount) { + // update the transaction list only if we have more than before + lastTxCount = txCount; + fullRefresh = true; + } + } + if (observer != null) + observer.onRefreshed(wallet, fullRefresh); + } + } + } + + public void updated() { + Timber.d("updated()"); + Wallet wallet = getWallet(); + if (wallet == null) throw new IllegalStateException("No wallet!"); + updated = true; + } + + public void refreshed() { // this means it's synced + Timber.d("refreshed()"); + final Wallet wallet = getWallet(); + if (wallet == null) throw new IllegalStateException("No wallet!"); + wallet.setSynchronized(); + if (updated) { + updateDaemonState(wallet, wallet.getBlockChainHeight()); + wallet.refreshHistory(); + if (observer != null) { + updated = !observer.onRefreshed(wallet, true); + } + } + } + } + + private long lastDaemonStatusUpdate = 0; + private long daemonHeight = 0; + private Wallet.ConnectionStatus connectionStatus = Wallet.ConnectionStatus.ConnectionStatus_Disconnected; + private static final long STATUS_UPDATE_INTERVAL = 120000; // 120s (blocktime) + + private void updateDaemonState(Wallet wallet, long height) { + long t = System.currentTimeMillis(); + if (height > 0) { // if we get a height, we are connected + daemonHeight = height; + connectionStatus = Wallet.ConnectionStatus.ConnectionStatus_Connected; + lastDaemonStatusUpdate = t; + } else { + if (t - lastDaemonStatusUpdate > STATUS_UPDATE_INTERVAL) { + lastDaemonStatusUpdate = t; + // these calls really connect to the daemon - wasting time + daemonHeight = wallet.getDaemonBlockChainHeight(); + if (daemonHeight > 0) { + // if we get a valid height, then obviously we are connected + connectionStatus = Wallet.ConnectionStatus.ConnectionStatus_Connected; + } else { + connectionStatus = Wallet.ConnectionStatus.ConnectionStatus_Disconnected; + } + } + } + } + + public long getDaemonHeight() { + return this.daemonHeight; + } + + public Wallet.ConnectionStatus getConnectionStatus() { + return this.connectionStatus; + } + + ///////////////////////////////////////////// + // communication back to client (activity) // + ///////////////////////////////////////////// + // NB: This allows for only one observer, i.e. only a single activity bound here + + private Observer observer = null; + + public void setObserver(Observer anObserver) { + observer = anObserver; + Timber.d("setObserver %s", observer); + } + + public interface Observer { + boolean onRefreshed(Wallet wallet, boolean full); + + void onProgress(String text); + + void onProgress(int n); + + void onWalletStored(boolean success); + + void onTransactionCreated(String tag, PendingTransaction pendingTransaction); + + void onTransactionSent(String txid); + + void onSendTransactionFailed(String error); + + void onWalletStarted(Wallet.Status walletStatus); + + void onWalletOpen(Wallet.Device device); + } + + String progressText = null; + int progressValue = -1; + + private void showProgress(String text) { + progressText = text; + if (observer != null) { + observer.onProgress(text); + } + } + + private void showProgress(int n) { + progressValue = n; + if (observer != null) { + observer.onProgress(n); + } + } + + public String getProgressText() { + return progressText; + } + + public int getProgressValue() { + return progressValue; + } + + // + public Wallet getWallet() { + return WalletManager.getInstance().getWallet(); + } + + ///////////////////////////////////////////// + ///////////////////////////////////////////// + + private WalletService.ServiceHandler mServiceHandler; + + private boolean errorState = false; + + // Handler that receives messages from the thread + private final class ServiceHandler extends Handler { + ServiceHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + Timber.d("Handling %s", msg.arg2); + if (errorState) { + Timber.i("In error state."); + // also, we have already stopped ourselves + return; + } + switch (msg.arg2) { + case START_SERVICE: { + Bundle extras = msg.getData(); + String cmd = extras.getString(REQUEST, null); + switch (cmd) { + case REQUEST_CMD_LOAD: + String walletId = extras.getString(REQUEST_WALLET, null); + String walletPw = extras.getString(REQUEST_CMD_LOAD_PW, null); + Timber.d("LOAD wallet %s", walletId); + if (walletId != null) { + showProgress(getString(R.string.status_wallet_loading)); + showProgress(10); + Wallet.Status walletStatus = start(walletId, walletPw); + if (observer != null) observer.onWalletStarted(walletStatus); + if ((walletStatus == null) || !walletStatus.isOk()) { + errorState = true; + stop(); + } + } + break; + case REQUEST_CMD_STORE: { + Wallet myWallet = getWallet(); + if (myWallet == null) break; + Timber.d("STORE wallet: %s", myWallet.getName()); + boolean rc = myWallet.store(); + Timber.d("wallet stored: %s with rc=%b", myWallet.getName(), rc); + if (!rc) { + Timber.w("Wallet store failed: %s", myWallet.getStatus().getErrorString()); + } + if (observer != null) observer.onWalletStored(rc); + break; + } + case REQUEST_CMD_TX: { + Wallet myWallet = getWallet(); + if (myWallet == null) break; + Timber.d("CREATE TX for wallet: %s", myWallet.getName()); + myWallet.disposePendingTransaction(); // remove any old pending tx + + TxData txData = extras.getParcelable(REQUEST_CMD_TX_DATA); + String txTag = extras.getString(REQUEST_CMD_TX_TAG); + PendingTransaction pendingTransaction = myWallet.createTransaction(txData); + PendingTransaction.Status status = pendingTransaction.getStatus(); + Timber.d("transaction status %s", status); + if (status != PendingTransaction.Status.Status_Ok) { + Timber.w("Create Transaction failed: %s", pendingTransaction.getErrorString()); + } + if (observer != null) { + observer.onTransactionCreated(txTag, pendingTransaction); + } else { + myWallet.disposePendingTransaction(); + } + break; + } + case REQUEST_CMD_SWEEP: { + Wallet myWallet = getWallet(); + if (myWallet == null) break; + Timber.d("SWEEP TX for wallet: %s", myWallet.getName()); + myWallet.disposePendingTransaction(); // remove any old pending tx + + String txTag = extras.getString(REQUEST_CMD_TX_TAG); + PendingTransaction pendingTransaction = myWallet.createSweepUnmixableTransaction(); + PendingTransaction.Status status = pendingTransaction.getStatus(); + Timber.d("transaction status %s", status); + if (status != PendingTransaction.Status.Status_Ok) { + Timber.w("Create Transaction failed: %s", pendingTransaction.getErrorString()); + } + if (observer != null) { + observer.onTransactionCreated(txTag, pendingTransaction); + } else { + myWallet.disposePendingTransaction(); + } + break; + } + case REQUEST_CMD_SEND: { + Wallet myWallet = getWallet(); + if (myWallet == null) break; + Timber.d("SEND TX for wallet: %s", myWallet.getName()); + PendingTransaction pendingTransaction = myWallet.getPendingTransaction(); + if (pendingTransaction == null) { + throw new IllegalArgumentException("PendingTransaction is null"); // die + } + if (pendingTransaction.getStatus() != PendingTransaction.Status.Status_Ok) { + Timber.e("PendingTransaction is %s", pendingTransaction.getStatus()); + final String error = pendingTransaction.getErrorString(); + myWallet.disposePendingTransaction(); // it's broken anyway + if (observer != null) observer.onSendTransactionFailed(error); + return; + } + final String txid = pendingTransaction.getFirstTxId(); // tx ids vanish after commit()! + + boolean success = pendingTransaction.commit("", true); + if (success) { + myWallet.disposePendingTransaction(); + if (observer != null) observer.onTransactionSent(txid); + String notes = extras.getString(REQUEST_CMD_SEND_NOTES); + if ((notes != null) && (!notes.isEmpty())) { + myWallet.setUserNote(txid, notes); + } + boolean rc = myWallet.store(); + Timber.d("wallet stored: %s with rc=%b", myWallet.getName(), rc); + if (!rc) { + Timber.w("Wallet store failed: %s", myWallet.getStatus().getErrorString()); + } + if (observer != null) observer.onWalletStored(rc); + listener.updated = true; + } else { + final String error = pendingTransaction.getErrorString(); + myWallet.disposePendingTransaction(); + if (observer != null) observer.onSendTransactionFailed(error); + return; + } + break; + } + } + } + break; + case STOP_SERVICE: + stop(); + break; + default: + Timber.e("UNKNOWN %s", msg.arg2); + } + } + } + + @Override + public void onCreate() { + // We are using a HandlerThread and a Looper to avoid loading and closing + // concurrency + MoneroHandlerThread thread = new MoneroHandlerThread("WalletService", + Process.THREAD_PRIORITY_BACKGROUND); + thread.start(); + + // Get the HandlerThread's Looper and use it for our Handler + final Looper serviceLooper = thread.getLooper(); + mServiceHandler = new WalletService.ServiceHandler(serviceLooper); + + Timber.d("Service created"); + } + + @Override + public void onDestroy() { + Timber.d("onDestroy()"); + if (this.listener != null) { + Timber.w("onDestroy() with active listener"); + // no need to stop() here because the wallet closing should have been triggered + // through onUnbind() already + } + } + + @Override + protected void attachBaseContext(Context context) { + super.attachBaseContext(LocaleHelper.setPreferredLocale(context)); + } + + public class WalletServiceBinder extends Binder { + public WalletService getService() { + return WalletService.this; + } + } + + private final IBinder mBinder = new WalletServiceBinder(); + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Running = true; + // when the activity starts the service, it expects to start it for a new wallet + // the service is possibly still occupied with saving the last opened wallet + // so we queue the open request + // this should not matter since the old activity is not getting updates + // and the new one is not listening yet (although it will be bound) + Timber.d("onStartCommand()"); + // For each start request, send a message to start a job and deliver the + // start ID so we know which request we're stopping when we finish the job + Message msg = mServiceHandler.obtainMessage(); + msg.arg2 = START_SERVICE; + if (intent != null) { + msg.setData(intent.getExtras()); + mServiceHandler.sendMessage(msg); + return START_STICKY; + } else { + // process restart - don't do anything - let system kill it again + stop(); + return START_NOT_STICKY; + } + } + + @Override + public IBinder onBind(Intent intent) { + // Very first client binds + Timber.d("onBind()"); + return mBinder; + } + + @Override + public boolean onUnbind(Intent intent) { + Timber.d("onUnbind()"); + // All clients have unbound with unbindService() + Message msg = mServiceHandler.obtainMessage(); + msg.arg2 = STOP_SERVICE; + mServiceHandler.sendMessage(msg); + Timber.d("onUnbind() message sent"); + return true; // true is important so that onUnbind is also called next time + } + + @Nullable + private Wallet.Status start(String walletName, String walletPassword) { + Timber.d("start()"); + startNotfication(); + showProgress(getString(R.string.status_wallet_loading)); + showProgress(10); + if (listener == null) { + Timber.d("start() loadWallet"); + Wallet aWallet = loadWallet(walletName, walletPassword); + if (aWallet == null) return null; + Wallet.Status walletStatus = aWallet.getFullStatus(); + if (!walletStatus.isOk()) { + aWallet.close(); + return walletStatus; + } + listener = new MyWalletListener(); + listener.start(); + showProgress(100); + } + showProgress(getString(R.string.status_wallet_connecting)); + showProgress(101); + // if we try to refresh the history here we get occasional segfaults! + // doesnt matter since we update as soon as we get a new block anyway + Timber.d("start() done"); + return getWallet().getFullStatus(); + } + + public void stop() { + Timber.d("stop()"); + setObserver(null); // in case it was not reset already + if (listener != null) { + listener.stop(); + Wallet myWallet = getWallet(); + Timber.d("stop() closing"); + myWallet.close(); + Timber.d("stop() closed"); + listener = null; + } + stopForeground(true); + stopSelf(); + Running = false; + } + + private Wallet loadWallet(String walletName, String walletPassword) { + Wallet wallet = openWallet(walletName, walletPassword); + if (wallet != null) { + Timber.d("Using daemon %s", WalletManager.getInstance().getDaemonAddress()); + showProgress(55); + wallet.init(0); + wallet.setProxy(NetCipherHelper.getProxy()); + showProgress(90); + } + return wallet; + } + + private Wallet openWallet(String walletName, String walletPassword) { + String path = Helper.getWalletFile(getApplicationContext(), walletName).getAbsolutePath(); + showProgress(20); + Wallet wallet = null; + WalletManager walletMgr = WalletManager.getInstance(); + Timber.d("WalletManager network=%s", walletMgr.getNetworkType().name()); + showProgress(30); + if (walletMgr.walletExists(path)) { + Timber.d("open wallet %s", path); + Wallet.Device device = WalletManager.getInstance().queryWalletDevice(path + ".keys", walletPassword); + Timber.d("device is %s", device.toString()); + if (observer != null) observer.onWalletOpen(device); + wallet = walletMgr.openWallet(path, walletPassword); + showProgress(60); + Timber.d("wallet opened"); + Wallet.Status walletStatus = wallet.getStatus(); + if (!walletStatus.isOk()) { + Timber.d("wallet status is %s", walletStatus); + WalletManager.getInstance().close(wallet); // TODO close() failed? + wallet = null; + // TODO what do we do with the progress?? + // TODO tell the activity this failed + // this crashes in MyWalletListener(Wallet aWallet) as wallet == null + } + } + return wallet; + } + + private void startNotfication() { + Intent notificationIntent = new Intent(this, WalletActivity.class); + PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_IMMUTABLE : 0); + + String channelId = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? createNotificationChannel() : ""; + Notification notification = new NotificationCompat.Builder(this, channelId) + .setContentTitle(getString(R.string.service_description)) + .setOngoing(true) + .setSmallIcon(R.drawable.ic_monerujo) + .setPriority(NotificationCompat.PRIORITY_MIN) + .setCategory(NotificationCompat.CATEGORY_SERVICE) + .setContentIntent(pendingIntent) + .build(); + startForeground(NOTIFICATION_ID, notification); + } + + @RequiresApi(Build.VERSION_CODES.O) + private String createNotificationChannel() { + NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + NotificationChannel channel = new NotificationChannel(CHANNEL_ID, getString(R.string.service_description), + NotificationManager.IMPORTANCE_LOW); + channel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE); + notificationManager.createNotificationChannel(channel); + return CHANNEL_ID; + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/service/exchange/api/ExchangeApi.java b/app/src/main/java/com/m2049r/xmrwallet/service/exchange/api/ExchangeApi.java new file mode 100644 index 0000000..7b7972a --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/service/exchange/api/ExchangeApi.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.service.exchange.api; + + +import androidx.annotation.NonNull; + + +public interface ExchangeApi { + + /** + * Queries the exchnage rate + * + * @param baseCurrency base currency + * @param quoteCurrency quote currency + * @param callback the callback with the exchange rate + */ + void queryExchangeRate(@NonNull final String baseCurrency, @NonNull final String quoteCurrency, + @NonNull final ExchangeCallback callback); + +} + diff --git a/app/src/main/java/com/m2049r/xmrwallet/service/exchange/api/ExchangeCallback.java b/app/src/main/java/com/m2049r/xmrwallet/service/exchange/api/ExchangeCallback.java new file mode 100644 index 0000000..c5b939c --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/service/exchange/api/ExchangeCallback.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.service.exchange.api; + +public interface ExchangeCallback { + + void onSuccess(ExchangeRate exchangeRate); + + void onError(Exception ex); + +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/service/exchange/api/ExchangeException.java b/app/src/main/java/com/m2049r/xmrwallet/service/exchange/api/ExchangeException.java new file mode 100644 index 0000000..905819d --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/service/exchange/api/ExchangeException.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.service.exchange.api; + +public class ExchangeException extends Exception { + private final int code; + private final String errorMsg; + + public String getErrorMsg() { + return errorMsg; + } + + public ExchangeException(final int code) { + super(); + this.code = code; + this.errorMsg = null; + } + + public ExchangeException(final String errorMsg) { + super(); + this.code = 0; + this.errorMsg = errorMsg; + } + + public ExchangeException(final int code, final String errorMsg) { + super(); + this.code = code; + this.errorMsg = errorMsg; + } + + public int getCode() { + return code; + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/service/exchange/api/ExchangeRate.java b/app/src/main/java/com/m2049r/xmrwallet/service/exchange/api/ExchangeRate.java new file mode 100644 index 0000000..3c0fadf --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/service/exchange/api/ExchangeRate.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2017 m2049r et al. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.service.exchange.api; + +public interface ExchangeRate { + + String getServiceName(); + + String getBaseCurrency(); + + String getQuoteCurrency(); + + double getRate(); + +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/service/exchange/ecb/ExchangeApiImpl.java b/app/src/main/java/com/m2049r/xmrwallet/service/exchange/ecb/ExchangeApiImpl.java new file mode 100644 index 0000000..e355ff5 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/service/exchange/ecb/ExchangeApiImpl.java @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2019 m2049r@monerujo.io + * + * 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. + */ + +// https://developer.android.com/training/basics/network-ops/xml + +package com.m2049r.xmrwallet.service.exchange.ecb; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import com.m2049r.xmrwallet.service.exchange.api.ExchangeApi; +import com.m2049r.xmrwallet.service.exchange.api.ExchangeCallback; +import com.m2049r.xmrwallet.service.exchange.api.ExchangeException; +import com.m2049r.xmrwallet.service.exchange.api.ExchangeRate; +import com.m2049r.xmrwallet.util.NetCipherHelper; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import okhttp3.Call; +import okhttp3.HttpUrl; +import okhttp3.Response; +import timber.log.Timber; + +public class ExchangeApiImpl implements ExchangeApi { + @NonNull + private final HttpUrl baseUrl; + + //so we can inject the mockserver url + @VisibleForTesting + public ExchangeApiImpl(@NonNull final HttpUrl baseUrl) { + this.baseUrl = baseUrl; + } + + public ExchangeApiImpl() { + this(HttpUrl.parse("https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml")); + // data is daily and is refreshed around 16:00 CET every working day + } + + public static boolean isSameDay(Calendar calendar, Calendar anotherCalendar) { + return (calendar.get(Calendar.YEAR) == anotherCalendar.get(Calendar.YEAR)) && + (calendar.get(Calendar.DAY_OF_YEAR) == anotherCalendar.get(Calendar.DAY_OF_YEAR)); + } + + @Override + public void queryExchangeRate(@NonNull final String baseCurrency, @NonNull final String quoteCurrency, + @NonNull final ExchangeCallback callback) { + if (!baseCurrency.equals("EUR")) { + callback.onError(new IllegalArgumentException("Only EUR supported as base")); + return; + } + + if (baseCurrency.equals(quoteCurrency)) { + callback.onSuccess(new ExchangeRateImpl(quoteCurrency, 1.0, new Date())); + return; + } + + if (fetchDate != null) { // we have data + boolean useCache = false; + // figure out if we can use the cached values + // data is daily and is refreshed around 16:00 CET every working day + Calendar now = Calendar.getInstance(TimeZone.getTimeZone("CET")); + + int fetchWeekday = fetchDate.get(Calendar.DAY_OF_WEEK); + int fetchDay = fetchDate.get(Calendar.DAY_OF_YEAR); + int fetchHour = fetchDate.get(Calendar.HOUR_OF_DAY); + + int today = now.get(Calendar.DAY_OF_YEAR); + int nowHour = now.get(Calendar.HOUR_OF_DAY); + + if ( + // was it fetched today before 16:00? assume no new data iff now < 16:00 as well + ((today == fetchDay) && (fetchHour < 16) && (nowHour < 16)) + // was it fetched after, 17:00? we can assume there is no newer data + || ((today == fetchDay) && (fetchHour > 17)) + || ((today == fetchDay + 1) && (fetchHour > 17) && (nowHour < 16)) + // is the data itself from today? there can be no newer data + || (fxDate.get(Calendar.DAY_OF_YEAR) == today) + // was it fetched Sat/Sun? we can assume there is no newer data + || ((fetchWeekday == Calendar.SATURDAY) || (fetchWeekday == Calendar.SUNDAY)) + ) { // return cached rate + try { + callback.onSuccess(getRate(quoteCurrency)); + } catch (ExchangeException ex) { + callback.onError(ex); + } + return; + } + } + + final NetCipherHelper.Request httpRequest = new NetCipherHelper.Request(baseUrl); + httpRequest.enqueue(new okhttp3.Callback() { + @Override + public void onFailure(final Call call, final IOException ex) { + callback.onError(ex); + } + + @Override + public void onResponse(final Call call, final Response response) throws IOException { + if (response.isSuccessful()) { + try { + DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); + Document doc = dBuilder.parse(response.body().byteStream()); + doc.getDocumentElement().normalize(); + parse(doc); + try { + callback.onSuccess(getRate(quoteCurrency)); + } catch (ExchangeException ex) { + callback.onError(ex); + } + } catch (ParserConfigurationException | SAXException ex) { + Timber.w(ex); + callback.onError(new ExchangeException(ex.getLocalizedMessage())); + } + } else { + callback.onError(new ExchangeException(response.code(), response.message())); + } + } + }); + } + + final private Map fxEntries = new HashMap<>(); + private Calendar fxDate = null; + private Calendar fetchDate = null; + + synchronized private ExchangeRate getRate(String currency) throws ExchangeException { + Timber.d("Getting %s", currency); + final Double rate = fxEntries.get(currency); + if (rate == null) throw new ExchangeException(404, "Currency not supported: " + currency); + return new ExchangeRateImpl(currency, rate, fxDate.getTime()); + } + + private final static SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.US); + + { + DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC")); + } + + private void parse(final Document xmlRootDoc) { + final Map entries = new HashMap<>(); + Calendar date = Calendar.getInstance(TimeZone.getTimeZone("CET")); + try { + NodeList cubes = xmlRootDoc.getElementsByTagName("Cube"); + for (int i = 0; i < cubes.getLength(); i++) { + Node node = cubes.item(i); + if (node.getNodeType() == Node.ELEMENT_NODE) { + Element cube = (Element) node; + if (cube.hasAttribute("time")) { // a time Cube + final Date time = DATE_FORMAT.parse(cube.getAttribute("time")); + date.setTime(time); + } else if (cube.hasAttribute("currency") + && cube.hasAttribute("rate")) { // a rate Cube + String currency = cube.getAttribute("currency"); + double rate = Double.valueOf(cube.getAttribute("rate")); + entries.put(currency, rate); + } // else an empty Cube - ignore + } + } + } catch (ParseException ex) { + Timber.d(ex); + } + synchronized (this) { + if (date != null) { + fetchDate = Calendar.getInstance(TimeZone.getTimeZone("CET")); + fxDate = date; + fxEntries.clear(); + fxEntries.putAll(entries); + } + // else don't change what we have + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/service/exchange/ecb/ExchangeRateImpl.java b/app/src/main/java/com/m2049r/xmrwallet/service/exchange/ecb/ExchangeRateImpl.java new file mode 100644 index 0000000..4691dfa --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/service/exchange/ecb/ExchangeRateImpl.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2019 m2049r et al. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.service.exchange.ecb; + +import androidx.annotation.NonNull; + +import com.m2049r.xmrwallet.service.exchange.api.ExchangeRate; + +import java.util.Date; + +class ExchangeRateImpl implements ExchangeRate { + private final Date date; + private final String baseCurrency = "EUR"; + private final String quoteCurrency; + private final double rate; + + @Override + public String getServiceName() { + return "ecb.europa.eu"; + } + + @Override + public String getBaseCurrency() { + return baseCurrency; + } + + @Override + public String getQuoteCurrency() { + return quoteCurrency; + } + + @Override + public double getRate() { + return rate; + } + + ExchangeRateImpl(@NonNull final String quoteCurrency, double rate, @NonNull final Date date) { + super(); + this.quoteCurrency = quoteCurrency; + this.rate = rate; + this.date = date; + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/service/exchange/kraken/ExchangeApiImpl.java b/app/src/main/java/com/m2049r/xmrwallet/service/exchange/kraken/ExchangeApiImpl.java new file mode 100644 index 0000000..ab36259 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/service/exchange/kraken/ExchangeApiImpl.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2017-2019 m2049r et al. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.service.exchange.kraken; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import com.m2049r.xmrwallet.service.exchange.api.ExchangeApi; +import com.m2049r.xmrwallet.service.exchange.api.ExchangeCallback; +import com.m2049r.xmrwallet.service.exchange.api.ExchangeException; +import com.m2049r.xmrwallet.service.exchange.api.ExchangeRate; +import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.NetCipherHelper; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; + +import okhttp3.Call; +import okhttp3.HttpUrl; +import okhttp3.Response; +import timber.log.Timber; + +public class ExchangeApiImpl implements ExchangeApi { + + private final HttpUrl baseUrl; + + //so we can inject the mockserver url + @VisibleForTesting + public ExchangeApiImpl(final HttpUrl baseUrl) { + this.baseUrl = baseUrl; + } + + public ExchangeApiImpl() { + this(HttpUrl.parse("https://api.kraken.com/0/public/Ticker")); + } + + @Override + public void queryExchangeRate(@NonNull final String baseCurrency, @NonNull final String quoteCurrency, + @NonNull final ExchangeCallback callback) { + + if (baseCurrency.equals(quoteCurrency)) { + callback.onSuccess(new ExchangeRateImpl(baseCurrency, quoteCurrency, 1.0)); + return; + } + + boolean invertQuery; + + + if (Helper.BASE_CRYPTO.equals(baseCurrency)) { + invertQuery = false; + } else if (Helper.BASE_CRYPTO.equals(quoteCurrency)) { + invertQuery = true; + } else { + callback.onError(new IllegalArgumentException("no crypto specified")); + return; + } + + Timber.d("queryExchangeRate: i %b, b %s, q %s", invertQuery, baseCurrency, quoteCurrency); + final boolean invert = invertQuery; + final String base = invert ? quoteCurrency : baseCurrency; + final String quote = invert ? baseCurrency : quoteCurrency; + + final HttpUrl url = baseUrl.newBuilder() + .addQueryParameter("pair", base + (quote.equals("BTC") ? "XBT" : quote)) + .build(); + + final NetCipherHelper.Request httpRequest = new NetCipherHelper.Request(url); + httpRequest.enqueue(new okhttp3.Callback() { + @Override + public void onFailure(final Call call, final IOException ex) { + callback.onError(ex); + } + + @Override + public void onResponse(final Call call, final Response response) throws IOException { + if (response.isSuccessful()) { + try { + final JSONObject json = new JSONObject(response.body().string()); + final JSONArray jsonError = json.getJSONArray("error"); + if (jsonError.length() > 0) { + final String errorMsg = jsonError.getString(0); + callback.onError(new ExchangeException(response.code(), errorMsg)); + } else { + final JSONObject jsonResult = json.getJSONObject("result"); + reportSuccess(jsonResult, invert, callback); + } + } catch (JSONException ex) { + callback.onError(new ExchangeException(ex.getLocalizedMessage())); + } + } else { + callback.onError(new ExchangeException(response.code(), response.message())); + } + } + }); + } + + void reportSuccess(JSONObject jsonObject, boolean swapAssets, ExchangeCallback callback) { + try { + final ExchangeRate exchangeRate = new ExchangeRateImpl(jsonObject, swapAssets); + callback.onSuccess(exchangeRate); + } catch (JSONException ex) { + callback.onError(new ExchangeException(ex.getLocalizedMessage())); + } catch (ExchangeException ex) { + callback.onError(ex); + } + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/service/exchange/kraken/ExchangeRateImpl.java b/app/src/main/java/com/m2049r/xmrwallet/service/exchange/kraken/ExchangeRateImpl.java new file mode 100644 index 0000000..e3afb5f --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/service/exchange/kraken/ExchangeRateImpl.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2017 m2049r et al. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.service.exchange.kraken; + +import androidx.annotation.NonNull; + +import com.m2049r.xmrwallet.service.exchange.api.ExchangeException; +import com.m2049r.xmrwallet.service.exchange.api.ExchangeRate; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.NoSuchElementException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +class ExchangeRateImpl implements ExchangeRate { + + private final String baseCurrency; + private final String quoteCurrency; + private final double rate; + + @Override + public String getServiceName() { + return "kraken.com"; + } + + @Override + public String getBaseCurrency() { + return baseCurrency; + } + + @Override + public String getQuoteCurrency() { + return quoteCurrency; + } + + @Override + public double getRate() { + return rate; + } + + ExchangeRateImpl(@NonNull final String baseCurrency, @NonNull final String quoteCurrency, double rate) { + super(); + this.baseCurrency = baseCurrency; + this.quoteCurrency = quoteCurrency; + this.rate = rate; + } + + ExchangeRateImpl(final JSONObject jsonObject, final boolean swapAssets) throws JSONException, ExchangeException { + try { + final String key = jsonObject.keys().next(); // we expect only one + Pattern pattern = Pattern.compile("^X(.*?)Z(.*?)$"); + Matcher matcher = pattern.matcher(key); + if (matcher.find()) { + baseCurrency = swapAssets ? matcher.group(2) : matcher.group(1); + quoteCurrency = swapAssets ? matcher.group(1) : matcher.group(2); + } else { + throw new ExchangeException("no pair returned!"); + } + + JSONObject pair = jsonObject.getJSONObject(key); + JSONArray close = pair.getJSONArray("c"); + String closePrice = close.getString(0); + if (closePrice != null) { + try { + double rate = Double.parseDouble(closePrice); + this.rate = swapAssets ? (1 / rate) : rate; + } catch (NumberFormatException ex) { + throw new ExchangeException(ex.getLocalizedMessage()); + } + } else { + throw new ExchangeException("no close price returned!"); + } + } catch (NoSuchElementException ex) { + throw new ExchangeException(ex.getLocalizedMessage()); + } + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/service/exchange/krakenEcb/ExchangeApiImpl.java b/app/src/main/java/com/m2049r/xmrwallet/service/exchange/krakenEcb/ExchangeApiImpl.java new file mode 100644 index 0000000..b8021b9 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/service/exchange/krakenEcb/ExchangeApiImpl.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2019 m2049r@monerujo.io + * + * 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. + */ + +// https://developer.android.com/training/basics/network-ops/xml + +package com.m2049r.xmrwallet.service.exchange.krakenEcb; + +import androidx.annotation.NonNull; + +import com.m2049r.xmrwallet.service.exchange.api.ExchangeApi; +import com.m2049r.xmrwallet.service.exchange.api.ExchangeCallback; +import com.m2049r.xmrwallet.service.exchange.api.ExchangeRate; +import com.m2049r.xmrwallet.util.Helper; + +import okhttp3.OkHttpClient; +import timber.log.Timber; + +/* + Gets the XMR/EUR rate from kraken and then gets the EUR/fiat rate from the ECB + */ + +public class ExchangeApiImpl implements ExchangeApi { + static public final String BASE_FIAT = "EUR"; + + @Override + public void queryExchangeRate(@NonNull final String baseCurrency, @NonNull final String quoteCurrency, + @NonNull final ExchangeCallback callback) { + Timber.d("B=%s Q=%s", baseCurrency, quoteCurrency); + if (baseCurrency.equals(quoteCurrency)) { + Timber.d("BASE=QUOTE=1"); + callback.onSuccess(new ExchangeRateImpl(baseCurrency, quoteCurrency, 1.0)); + return; + } + + if (!Helper.BASE_CRYPTO.equals(baseCurrency) + && !Helper.BASE_CRYPTO.equals(quoteCurrency)) { + callback.onError(new IllegalArgumentException("no " + Helper.BASE_CRYPTO + " specified")); + return; + } + + final String quote = Helper.BASE_CRYPTO.equals(baseCurrency) ? quoteCurrency : baseCurrency; + + final ExchangeApi krakenApi = + new com.m2049r.xmrwallet.service.exchange.kraken.ExchangeApiImpl(); + krakenApi.queryExchangeRate(Helper.BASE_CRYPTO, BASE_FIAT, new ExchangeCallback() { + @Override + public void onSuccess(final ExchangeRate krakenRate) { + Timber.d("kraken = %f", krakenRate.getRate()); + final ExchangeApi ecbApi = + new com.m2049r.xmrwallet.service.exchange.ecb.ExchangeApiImpl(); + ecbApi.queryExchangeRate(BASE_FIAT, quote, new ExchangeCallback() { + @Override + public void onSuccess(final ExchangeRate ecbRate) { + Timber.d("ECB = %f", ecbRate.getRate()); + double rate = ecbRate.getRate() * krakenRate.getRate(); + Timber.d("Q=%s QC=%s", quote, quoteCurrency); + if (!quote.equals(quoteCurrency)) rate = 1.0d / rate; + Timber.d("rate = %f", rate); + final ExchangeRate exchangeRate = + new ExchangeRateImpl(baseCurrency, quoteCurrency, rate); + callback.onSuccess(exchangeRate); + } + + @Override + public void onError(Exception ex) { + Timber.d(ex); + callback.onError(ex); + } + }); + } + + @Override + public void onError(Exception ex) { + Timber.d(ex); + callback.onError(ex); + } + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/service/exchange/krakenEcb/ExchangeRateImpl.java b/app/src/main/java/com/m2049r/xmrwallet/service/exchange/krakenEcb/ExchangeRateImpl.java new file mode 100644 index 0000000..48b8ef0 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/service/exchange/krakenEcb/ExchangeRateImpl.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2019 m2049r et al. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.service.exchange.krakenEcb; + +import androidx.annotation.NonNull; + +import com.m2049r.xmrwallet.service.exchange.api.ExchangeRate; + +class ExchangeRateImpl implements ExchangeRate { + private final String baseCurrency; + private final String quoteCurrency; + private final double rate; + + @Override + public String getServiceName() { + return "kraken+ecb"; + } + + @Override + public String getBaseCurrency() { + return baseCurrency; + } + + @Override + public String getQuoteCurrency() { + return quoteCurrency; + } + + @Override + public double getRate() { + return rate; + } + + ExchangeRateImpl(@NonNull final String baseCurrency, @NonNull final String quoteCurrency, double rate) { + super(); + this.baseCurrency = baseCurrency; + this.quoteCurrency = quoteCurrency; + this.rate = rate; + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/service/shift/NetworkCallback.java b/app/src/main/java/com/m2049r/xmrwallet/service/shift/NetworkCallback.java new file mode 100644 index 0000000..f77128c --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/service/shift/NetworkCallback.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2017 m2049r et al. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.service.shift; + +import org.json.JSONObject; + +public interface NetworkCallback { + + void onSuccess(JSONObject jsonObject); + + void onError(Exception ex); + +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/service/shift/ShiftApiCall.java b/app/src/main/java/com/m2049r/xmrwallet/service/shift/ShiftApiCall.java new file mode 100644 index 0000000..c4daeb3 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/service/shift/ShiftApiCall.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2017 m2049r et al. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.service.shift; + +import androidx.annotation.NonNull; + +import org.json.JSONObject; + +public interface ShiftApiCall { + + void call(@NonNull final String path, @NonNull final NetworkCallback callback); + + void call(@NonNull final String path, final JSONObject request, @NonNull final NetworkCallback callback); +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/service/shift/ShiftCallback.java b/app/src/main/java/com/m2049r/xmrwallet/service/shift/ShiftCallback.java new file mode 100644 index 0000000..4dee50d --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/service/shift/ShiftCallback.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2017 m2049r et al. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.service.shift; + +public interface ShiftCallback { + + void onSuccess(T t); + + void onError(Exception ex); +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/service/shift/ShiftError.java b/app/src/main/java/com/m2049r/xmrwallet/service/shift/ShiftError.java new file mode 100644 index 0000000..d789ec5 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/service/shift/ShiftError.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2017-2021 m2049r et al. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.service.shift; + +import androidx.annotation.NonNull; + +import org.json.JSONException; +import org.json.JSONObject; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class ShiftError { + @Getter + private final Error errorType; + @Getter + private final String errorMsg; + + public enum Error { + SERVICE, + INFRASTRUCTURE + } + + public boolean isRetryable() { + return errorType == Error.INFRASTRUCTURE; + } + + public ShiftError(final JSONObject jsonObject) throws JSONException { + final JSONObject errorObject = jsonObject.getJSONObject("error"); + errorType = Error.SERVICE; + errorMsg = errorObject.getString("message"); + } + + @Override + @NonNull + public String toString() { + return getErrorMsg(); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/service/shift/ShiftException.java b/app/src/main/java/com/m2049r/xmrwallet/service/shift/ShiftException.java new file mode 100644 index 0000000..3a750b1 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/service/shift/ShiftException.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2017-2021 m2049r et al. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.service.shift; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class ShiftException extends Exception { + @Getter + private final int code; + @Getter + private final ShiftError error; + + public ShiftException(int code) { + this.code = code; + this.error = null; + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/api/CreateOrder.java b/app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/api/CreateOrder.java new file mode 100644 index 0000000..f738956 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/api/CreateOrder.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2017-2021 m2049r et al. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.service.shift.sideshift.api; + +import java.util.Date; + +public interface CreateOrder { + String TAG = "side"; + + String getBtcCurrency(); + + double getBtcAmount(); + + String getBtcAddress(); + + String getQuoteId(); + + String getOrderId(); + + double getXmrAmount(); + + String getXmrAddress(); + + Date getCreatedAt(); // createdAt + + Date getExpiresAt(); // expiresAt + +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/api/QueryOrderParameters.java b/app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/api/QueryOrderParameters.java new file mode 100644 index 0000000..ebd2d2f --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/api/QueryOrderParameters.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2017-2021 m2049r et al. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.service.shift.sideshift.api; + +public interface QueryOrderParameters { + + double getLowerLimit(); + + double getPrice(); + + double getUpperLimit(); + +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/api/QueryOrderStatus.java b/app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/api/QueryOrderStatus.java new file mode 100644 index 0000000..acb201c --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/api/QueryOrderStatus.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2017-2021 m2049r et al. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.service.shift.sideshift.api; + +import java.util.Date; + +public interface QueryOrderStatus { + enum State { + WAITING, // Waiting for mempool + PENDING, // Detected (waiting for confirmations) + SETTLING, // Settlement in progress + SETTLED, // Settlement completed + // no refunding in monerujo so theese are ignored: +// REFUND, // Queued for refund +// REFUNDING, // Refund in progress +// REFUNDED // Refund completed + UNDEFINED + } + + boolean isCreated(); + + boolean isTerminal(); + + boolean isWaiting(); + + boolean isPending(); + + boolean isSent(); + + boolean isPaid(); + + boolean isError(); + + QueryOrderStatus.State getState(); + + String getOrderId(); + + Date getCreatedAt(); + + Date getExpiresAt(); + + double getBtcAmount(); + + String getBtcAddress(); + + double getXmrAmount(); + + String getXmrAddress(); + + double getPrice(); +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/api/RequestQuote.java b/app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/api/RequestQuote.java new file mode 100644 index 0000000..cbbb36f --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/api/RequestQuote.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2017-2021 m2049r et al. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.service.shift.sideshift.api; + +import java.util.Date; + +public interface RequestQuote { + + double getBtcAmount(); // settleAmount + + String getId(); // id + + Date getCreatedAt(); // createdAt + + Date getExpiresAt(); // expiresAt + + double getXmrAmount(); // depositAmount + + double getPrice(); // rate +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/api/SideShiftApi.java b/app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/api/SideShiftApi.java new file mode 100644 index 0000000..6c9331c --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/api/SideShiftApi.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2017-2021 m2049r et al. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.service.shift.sideshift.api; + +import android.net.Uri; + +import androidx.annotation.NonNull; + +import com.m2049r.xmrwallet.service.shift.ShiftCallback; + +public interface SideShiftApi { + int QUERY_INTERVAL = 5000; // ms + + /** + * Queries the order parameter. + * + * @param callback the callback with the OrderParameter object + */ + void queryOrderParameters(@NonNull final ShiftCallback callback); + + /** + * Creates an order + * + * @param xmrAmount the desired XMR amount + */ + void requestQuote(final double xmrAmount, @NonNull final ShiftCallback callback); + + /** + * Creates an order + * + * @param quoteId the desired XMR amount + * @param btcAddress the target bitcoin address + */ + void createOrder(final String quoteId, @NonNull final String btcAddress, @NonNull final ShiftCallback callback); + + /** + * Queries the order status for given current order + * + * @param orderId the order ID + * @param callback the callback with the OrderStatus object + */ + void queryOrderStatus(@NonNull final String orderId, @NonNull final ShiftCallback callback); + + /* + * Returns the URL for manually querying the order status + * + * @param orderId the order ID + */ + Uri getQueryOrderUri(String orderId); +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/network/CreateOrderImpl.java b/app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/network/CreateOrderImpl.java new file mode 100644 index 0000000..258cf4d --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/network/CreateOrderImpl.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2017-2021 m2049r et al. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.service.shift.sideshift.network; + +import androidx.annotation.NonNull; + +import com.m2049r.xmrwallet.BuildConfig; +import com.m2049r.xmrwallet.service.shift.NetworkCallback; +import com.m2049r.xmrwallet.service.shift.ShiftApiCall; +import com.m2049r.xmrwallet.service.shift.ShiftCallback; +import com.m2049r.xmrwallet.service.shift.sideshift.api.CreateOrder; +import com.m2049r.xmrwallet.service.shift.sideshift.api.SideShiftApi; +import com.m2049r.xmrwallet.util.DateHelper; +import com.m2049r.xmrwallet.util.ServiceHelper; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.text.ParseException; +import java.util.Date; + +import lombok.Getter; + +class CreateOrderImpl implements CreateOrder { + @Getter + private final String btcCurrency; + @Getter + private final double btcAmount; + @Getter + private final String btcAddress; + @Getter + private final String quoteId; + @Getter + private final String orderId; + @Getter + private final double xmrAmount; + @Getter + private final String xmrAddress; + @Getter + private final Date createdAt; + @Getter + private final Date expiresAt; + + CreateOrderImpl(final JSONObject jsonObject) throws JSONException { + // sanity checks + final String depositMethod = jsonObject.getString("depositMethodId"); + final String settleMethod = jsonObject.getString("settleMethodId"); + if (!"xmr".equals(depositMethod) || !ServiceHelper.ASSET.equals(settleMethod)) + throw new IllegalStateException(); + + btcCurrency = settleMethod.toUpperCase(); + btcAmount = jsonObject.getDouble("settleAmount"); + JSONObject settleAddress = jsonObject.getJSONObject("settleAddress"); + btcAddress = settleAddress.getString("address"); + + xmrAmount = jsonObject.getDouble("depositAmount"); + JSONObject depositAddress = jsonObject.getJSONObject("depositAddress"); + xmrAddress = depositAddress.getString("address"); + + quoteId = jsonObject.getString("quoteId"); + + orderId = jsonObject.getString("orderId"); + + try { + final String created = jsonObject.getString("createdAtISO"); + createdAt = DateHelper.parse(created); + final String expires = jsonObject.getString("expiresAtISO"); + expiresAt = DateHelper.parse(expires); + } catch (ParseException ex) { + throw new JSONException(ex.getLocalizedMessage()); + } + } + + public static void call(@NonNull final ShiftApiCall api, final String quoteId, @NonNull final String btcAddress, + @NonNull final ShiftCallback callback) { + try { + final JSONObject request = createRequest(quoteId, btcAddress); + api.call("orders", request, new NetworkCallback() { + @Override + public void onSuccess(JSONObject jsonObject) { + try { + callback.onSuccess(new CreateOrderImpl(jsonObject)); + } catch (JSONException ex) { + callback.onError(ex); + } + } + + @Override + public void onError(Exception ex) { + callback.onError(ex); + } + }); + } catch (JSONException ex) { + callback.onError(ex); + } + } + + static JSONObject createRequest(final String quoteId, final String address) throws JSONException { + final JSONObject jsonObject = new JSONObject(); + jsonObject.put("type", "fixed"); + jsonObject.put("quoteId", quoteId); + jsonObject.put("settleAddress", address); + if (!BuildConfig.ID_A.isEmpty() && !"null".equals(BuildConfig.ID_A)) { + jsonObject.put("affiliateId", BuildConfig.ID_A); + } + return jsonObject; + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/network/QueryOrderParametersImpl.java b/app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/network/QueryOrderParametersImpl.java new file mode 100644 index 0000000..afa5527 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/network/QueryOrderParametersImpl.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2017-2021 m2049r et al. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.service.shift.sideshift.network; + +import androidx.annotation.NonNull; + +import com.m2049r.xmrwallet.service.shift.NetworkCallback; +import com.m2049r.xmrwallet.service.shift.ShiftApiCall; +import com.m2049r.xmrwallet.service.shift.sideshift.api.QueryOrderParameters; +import com.m2049r.xmrwallet.service.shift.sideshift.api.SideShiftApi; +import com.m2049r.xmrwallet.service.shift.ShiftCallback; +import com.m2049r.xmrwallet.util.ServiceHelper; + +import org.json.JSONException; +import org.json.JSONObject; + +class QueryOrderParametersImpl implements QueryOrderParameters { + + private double lowerLimit; + private double price; + private double upperLimit; + + public double getLowerLimit() { + return lowerLimit; + } + + public double getPrice() { + return price; + } + + public double getUpperLimit() { + return upperLimit; + } + + QueryOrderParametersImpl(final JSONObject jsonObject) throws JSONException { + lowerLimit = jsonObject.getDouble("min"); + price = jsonObject.getDouble("rate"); + upperLimit = jsonObject.getDouble("max"); + } + + public static void call(@NonNull final ShiftApiCall api, + @NonNull final ShiftCallback callback) { + api.call("pairs/xmr/" + ServiceHelper.ASSET, new NetworkCallback() { + @Override + public void onSuccess(JSONObject jsonObject) { + try { + callback.onSuccess(new QueryOrderParametersImpl(jsonObject)); + } catch (JSONException ex) { + callback.onError(ex); + } + } + + @Override + public void onError(Exception ex) { + callback.onError(ex); + } + }); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/network/QueryOrderStatusImpl.java b/app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/network/QueryOrderStatusImpl.java new file mode 100644 index 0000000..439cb93 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/network/QueryOrderStatusImpl.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2017-2021 m2049r et al. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.service.shift.sideshift.network; + +import androidx.annotation.NonNull; + +import com.m2049r.xmrwallet.service.shift.NetworkCallback; +import com.m2049r.xmrwallet.service.shift.ShiftApiCall; +import com.m2049r.xmrwallet.util.DateHelper; +import com.m2049r.xmrwallet.service.shift.sideshift.api.QueryOrderStatus; +import com.m2049r.xmrwallet.service.shift.ShiftCallback; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.text.ParseException; +import java.util.Date; + +import lombok.Getter; +import timber.log.Timber; + +class QueryOrderStatusImpl implements QueryOrderStatus { + + @Getter + private QueryOrderStatus.State state; + @Getter + private final String orderId; + @Getter + private final Date createdAt; + @Getter + private final Date expiresAt; + @Getter + private final double btcAmount; + @Getter + private final String btcAddress; + @Getter + private final double xmrAmount; + @Getter + private final String xmrAddress; + + public boolean isCreated() { + return true; + } + + public boolean isTerminal() { + return (state.equals(State.SETTLED) || isError()); + } + + public boolean isError() { + return state.equals(State.UNDEFINED); + } + + public boolean isWaiting() { + return state.equals(State.WAITING); + } + + public boolean isPending() { + return state.equals(State.PENDING); + } + + public boolean isSent() { + return state.equals(State.SETTLING); + } + + public boolean isPaid() { + return state.equals(State.SETTLED); + } + + public double getPrice() { + return btcAmount / xmrAmount; + } + + QueryOrderStatusImpl(final JSONObject jsonObject) throws JSONException { + try { + String created = jsonObject.getString("createdAtISO"); + createdAt = DateHelper.parse(created); + String expires = jsonObject.getString("expiresAtISO"); + expiresAt = DateHelper.parse(expires); + } catch (ParseException ex) { + throw new JSONException(ex.getLocalizedMessage()); + } + orderId = jsonObject.getString("orderId"); + + btcAmount = jsonObject.getDouble("settleAmount"); + JSONObject settleAddress = jsonObject.getJSONObject("settleAddress"); + btcAddress = settleAddress.getString("address"); + + xmrAmount = jsonObject.getDouble("depositAmount"); + JSONObject depositAddress = jsonObject.getJSONObject("depositAddress"); + xmrAddress = settleAddress.getString("address"); + + JSONArray deposits = jsonObject.getJSONArray("deposits"); + // we only create one deposit, so die if there are more than one: + if (deposits.length() > 1) + throw new IllegalStateException("more than one deposits"); + + state = State.UNDEFINED; + if (deposits.length() == 0) { + state = State.WAITING; + } else if (deposits.length() == 1) { + // sanity check + if (!orderId.equals(deposits.getJSONObject(0).getString("orderId"))) + throw new IllegalStateException("deposit has different order id!"); + String stateName = deposits.getJSONObject(0).getString("status"); + try { + state = State.valueOf(stateName.toUpperCase()); + } catch (IllegalArgumentException ex) { + state = State.UNDEFINED; + } + } + } + + public static void call(@NonNull final ShiftApiCall api, @NonNull final String orderId, + @NonNull final ShiftCallback callback) { + api.call("orders/" + orderId, new NetworkCallback() { + @Override + public void onSuccess(JSONObject jsonObject) { + try { + callback.onSuccess(new QueryOrderStatusImpl(jsonObject)); + } catch (JSONException ex) { + callback.onError(ex); + } + } + + @Override + public void onError(Exception ex) { + callback.onError(ex); + } + }); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/network/RequestQuoteImpl.java b/app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/network/RequestQuoteImpl.java new file mode 100644 index 0000000..1cbdf24 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/network/RequestQuoteImpl.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2017-2021 m2049r et al. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.service.shift.sideshift.network; + +import androidx.annotation.NonNull; + +import com.m2049r.xmrwallet.service.shift.NetworkCallback; +import com.m2049r.xmrwallet.service.shift.ShiftApiCall; +import com.m2049r.xmrwallet.service.shift.ShiftCallback; +import com.m2049r.xmrwallet.service.shift.sideshift.api.RequestQuote; +import com.m2049r.xmrwallet.util.DateHelper; +import com.m2049r.xmrwallet.util.ServiceHelper; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.ParseException; +import java.util.Date; +import java.util.Locale; + +import lombok.Getter; + +class RequestQuoteImpl implements RequestQuote { + @Getter + private final double btcAmount; + @Getter + private final String id; + @Getter + private final Date createdAt; + @Getter + private final Date expiresAt; + @Getter + private final double xmrAmount; + @Getter + private final double price; + + // TODO do something with errors - they always seem to send us 500 + + RequestQuoteImpl(final JSONObject jsonObject) throws JSONException { + // sanity checks + final String depositMethod = jsonObject.getString("depositMethod"); + final String settleMethod = jsonObject.getString("settleMethod"); + if (!"xmr".equals(depositMethod) || !ServiceHelper.ASSET.equals(settleMethod)) + throw new IllegalStateException(); + + btcAmount = jsonObject.getDouble("settleAmount"); + id = jsonObject.getString("id"); + + try { + final String created = jsonObject.getString("createdAt"); + createdAt = DateHelper.parse(created); + final String expires = jsonObject.getString("expiresAt"); + expiresAt = DateHelper.parse(expires); + } catch (ParseException ex) { + throw new JSONException(ex.getLocalizedMessage()); + } + xmrAmount = jsonObject.getDouble("depositAmount"); + price = jsonObject.getDouble("rate"); + } + + public static void call(@NonNull final ShiftApiCall api, final double btcAmount, + @NonNull final ShiftCallback callback) { + try { + final JSONObject request = createRequest(btcAmount); + api.call("quotes", request, new NetworkCallback() { + @Override + public void onSuccess(JSONObject jsonObject) { + try { + callback.onSuccess(new RequestQuoteImpl(jsonObject)); + } catch (JSONException ex) { + callback.onError(ex); + } + } + + @Override + public void onError(Exception ex) { + callback.onError(ex); + } + }); + } catch (JSONException ex) { + callback.onError(ex); + } + } + + /** + * Create JSON request object + * + * @param btcAmount how much XMR to shift to BTC + */ + + static JSONObject createRequest(final double btcAmount) throws JSONException { + final JSONObject jsonObject = new JSONObject(); + jsonObject.put("depositMethod", "xmr"); + jsonObject.put("settleMethod", ServiceHelper.ASSET); + // #sideshift is silly and likes numbers as strings + String amount = AmountFormatter.format(btcAmount); + jsonObject.put("settleAmount", amount); + return jsonObject; + } + + static final DecimalFormat AmountFormatter; + + static { + AmountFormatter = new DecimalFormat(); + AmountFormatter.setDecimalFormatSymbols(new DecimalFormatSymbols(Locale.US)); + AmountFormatter.setMinimumIntegerDigits(1); + AmountFormatter.setMaximumFractionDigits(12); + AmountFormatter.setGroupingUsed(false); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/network/SideShiftApiImpl.java b/app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/network/SideShiftApiImpl.java new file mode 100644 index 0000000..c22e322 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/service/shift/sideshift/network/SideShiftApiImpl.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2017-2021 m2049r et al. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.service.shift.sideshift.network; + +import android.net.Uri; + +import androidx.annotation.NonNull; + +import com.m2049r.xmrwallet.service.shift.NetworkCallback; +import com.m2049r.xmrwallet.service.shift.ShiftApiCall; +import com.m2049r.xmrwallet.service.shift.ShiftCallback; +import com.m2049r.xmrwallet.service.shift.ShiftError; +import com.m2049r.xmrwallet.service.shift.ShiftException; +import com.m2049r.xmrwallet.service.shift.sideshift.api.CreateOrder; +import com.m2049r.xmrwallet.service.shift.sideshift.api.QueryOrderParameters; +import com.m2049r.xmrwallet.service.shift.sideshift.api.QueryOrderStatus; +import com.m2049r.xmrwallet.service.shift.sideshift.api.RequestQuote; +import com.m2049r.xmrwallet.service.shift.sideshift.api.SideShiftApi; +import com.m2049r.xmrwallet.util.NetCipherHelper; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; + +import okhttp3.Call; +import okhttp3.HttpUrl; +import okhttp3.Response; +import timber.log.Timber; + +public class SideShiftApiImpl implements SideShiftApi, ShiftApiCall { + + private final HttpUrl baseUrl; + + public SideShiftApiImpl(final HttpUrl baseUrl) { + this.baseUrl = baseUrl; + } + + @Override + public void queryOrderParameters(@NonNull final ShiftCallback callback) { + QueryOrderParametersImpl.call(this, callback); + } + + @Override + public void requestQuote(final double btcAmount, @NonNull final ShiftCallback callback) { + RequestQuoteImpl.call(this, btcAmount, callback); + } + + @Override + public void createOrder(final String quoteId, @NonNull final String btcAddress, + @NonNull final ShiftCallback callback) { + CreateOrderImpl.call(this, quoteId, btcAddress, callback); + } + + @Override + public void queryOrderStatus(@NonNull final String uuid, + @NonNull final ShiftCallback callback) { + QueryOrderStatusImpl.call(this, uuid, callback); + } + + @Override + public Uri getQueryOrderUri(String orderId) { + return Uri.parse("https://sideshift.ai/orders/" + orderId); + } + + @Override + public void call(@NonNull final String path, @NonNull final NetworkCallback callback) { + call(path, null, callback); + } + + @Override + public void call(@NonNull final String path, final JSONObject request, @NonNull final NetworkCallback callback) { + final HttpUrl url = baseUrl.newBuilder() + .addPathSegments(path) + .build(); + + NetCipherHelper.Request httpRequest = new NetCipherHelper.Request(url, request); + httpRequest.enqueue(new okhttp3.Callback() { + @Override + public void onFailure(final Call call, final IOException ex) { + callback.onError(ex); + } + + @Override + public void onResponse(@NonNull final Call call, @NonNull final Response response) throws IOException { + Timber.d("onResponse code=%d", response.code()); + if (response.isSuccessful()) { + try { + final JSONObject json = new JSONObject(response.body().string()); + callback.onSuccess(json); + } catch (JSONException ex) { + callback.onError(ex); + } + } else { + try { + final JSONObject json = new JSONObject(response.body().string()); + Timber.d(json.toString(2)); + final ShiftError error = new ShiftError(json); + Timber.w("%s says %d/%s", CreateOrder.TAG, response.code(), error.toString()); + callback.onError(new ShiftException(response.code(), error)); + } catch (JSONException ex) { + callback.onError(new ShiftException(response.code())); + } + } + } + }); + } +} 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 0000000..37f5329 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/CrazyPassEncoder.java @@ -0,0 +1,68 @@ +/* + * 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 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/DateHelper.java b/app/src/main/java/com/m2049r/xmrwallet/util/DateHelper.java new file mode 100644 index 0000000..e791cea --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/DateHelper.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2017 m2049r er al. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.util; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class DateHelper { + public static final SimpleDateFormat DATETIME_FORMATTER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); + + public static Date parse(String dateString) throws ParseException { + return DATETIME_FORMATTER.parse(dateString.replaceAll("Z$", "+0000")); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/DayNightMode.java b/app/src/main/java/com/m2049r/xmrwallet/util/DayNightMode.java new file mode 100644 index 0000000..6e8cdff --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/DayNightMode.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.util; + +import androidx.appcompat.app.AppCompatDelegate; + +public enum DayNightMode { + // order must match R.array.daynight_themes + AUTO(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM), + DAY(AppCompatDelegate.MODE_NIGHT_NO), + NIGHT(AppCompatDelegate.MODE_NIGHT_YES), + UNKNOWN(AppCompatDelegate.MODE_NIGHT_UNSPECIFIED); + + final private int nightMode; + + DayNightMode(int nightMode) { + this.nightMode = nightMode; + } + + public int getNightMode() { + return nightMode; + } + + static public DayNightMode getValue(int nightMode) { + for (DayNightMode mode : DayNightMode.values()) { + if (mode.nightMode == nightMode) + return mode; + } + return UNKNOWN; + } +} 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 0000000..906dba8 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/FingerprintHelper.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2018-2020 m2049r et al. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.util; + +import android.app.KeyguardManager; +import android.content.Context; +import android.hardware.fingerprint.FingerprintManager; +import android.os.Build; +import android.os.CancellationSignal; + +public class FingerprintHelper { + + public static boolean isDeviceSupported(Context context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return false; + } + + FingerprintManager fingerprintManager = context.getSystemService(FingerprintManager.class); + KeyguardManager keyguardManager = context.getSystemService(KeyguardManager.class); + + return (keyguardManager != null) && (fingerprintManager != null) && + keyguardManager.isKeyguardSecure() && + fingerprintManager.isHardwareDetected() && + fingerprintManager.hasEnrolledFingerprints(); + } + + public static boolean isFingerPassValid(Context context, String wallet) { + try { + KeyStoreHelper.loadWalletUserPass(context, wallet); + return true; + } catch (KeyStoreHelper.BrokenPasswordStoreException ex) { + return false; + } + } + + public static void authenticate(Context context, CancellationSignal cancelSignal, + FingerprintManager.AuthenticationCallback callback) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return; + } + + FingerprintManager manager = context.getSystemService(FingerprintManager.class); + if (manager != null) { + manager.authenticate(null, cancelSignal, 0, 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 new file mode 100644 index 0000000..4cd90cf --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/Helper.java @@ -0,0 +1,609 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.util; + +import android.Manifest; +import android.app.Activity; +import android.app.Dialog; +import android.content.ClipData; +import android.content.ClipDescription; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.VectorDrawable; +import android.hardware.fingerprint.FingerprintManager; +import android.os.AsyncTask; +import android.os.Build; +import android.os.CancellationSignal; +import android.os.StrictMode; +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 androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.textfield.TextInputLayout; +import com.m2049r.xmrwallet.BuildConfig; +import com.m2049r.xmrwallet.R; +import com.m2049r.xmrwallet.data.Crypto; +import com.m2049r.xmrwallet.model.WalletManager; + +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.MalformedURLException; +import java.net.SocketTimeoutException; +import java.net.URL; +import java.util.Calendar; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.net.ssl.HttpsURLConnection; + +import timber.log.Timber; + +public class Helper { + static public final String NOCRAZYPASS_FLAGFILE = ".nocrazypass"; + + static public final String BASE_CRYPTO = Crypto.XMR.getSymbol(); + static public final int XMR_DECIMALS = 12; + static public final long ONE_XMR = Math.round(Math.pow(10, Helper.XMR_DECIMALS)); + + static public final boolean SHOW_EXCHANGERATES = true; + static public boolean ALLOW_SHIFT = false; + + static private final String WALLET_DIR = "wallets"; + static private final String MONERO_DIR = "monero"; + + static public int DISPLAY_DIGITS_INFO = 5; + + static public File getWalletRoot(Context context) { + return getStorage(context, WALLET_DIR); + } + + static public File getStorage(Context context, String folderName) { + File dir = new File(context.getFilesDir(), folderName); + if (!dir.exists()) { + Timber.i("Creating %s", dir.getAbsolutePath()); + dir.mkdirs(); // try to make it + } + if (!dir.isDirectory()) { + String msg = "Directory " + dir.getAbsolutePath() + " does not exist."; + Timber.e(msg); + throw new IllegalStateException(msg); + } + return dir; + } + + static public final int PERMISSIONS_REQUEST_CAMERA = 7; + + static public boolean getCameraPermission(Activity context) { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + if (context.checkSelfPermission(Manifest.permission.CAMERA) + == PackageManager.PERMISSION_DENIED) { + Timber.w("Permission denied for CAMERA - requesting it"); + String[] permissions = {Manifest.permission.CAMERA}; + context.requestPermissions(permissions, PERMISSIONS_REQUEST_CAMERA); + return false; + } else { + return true; + } + } else { + return true; + } + } + + static public File getWalletFile(Context context, String aWalletName) { + File walletDir = getWalletRoot(context); + File f = new File(walletDir, aWalletName); + Timber.d("wallet=%s size= %d", f.getAbsolutePath(), f.length()); + return f; + } + + static public void showKeyboard(Activity act) { + InputMethodManager imm = (InputMethodManager) act.getSystemService(Context.INPUT_METHOD_SERVICE); + final View focus = act.getCurrentFocus(); + if (focus != null) + imm.showSoftInput(focus, InputMethodManager.SHOW_IMPLICIT); + } + + static public void hideKeyboard(Activity act) { + if (act == null) return; + if (act.getCurrentFocus() == null) { + act.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN); + } else { + InputMethodManager imm = (InputMethodManager) act.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow((null == act.getCurrentFocus()) ? null : act.getCurrentFocus().getWindowToken(), + InputMethodManager.HIDE_NOT_ALWAYS); + } + } + + static public void showKeyboard(Dialog dialog) { + dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); + } + + static public void hideKeyboardAlways(Activity act) { + act.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); + } + + static public BigDecimal getDecimalAmount(long amount) { + return new BigDecimal(amount).scaleByPowerOfTen(-XMR_DECIMALS); + } + + static public String getDisplayAmount(long amount) { + return getDisplayAmount(amount, XMR_DECIMALS); + } + + static public String getDisplayAmount(long amount, int maxDecimals) { + // a Java bug does not strip zeros properly if the value is 0 + if (amount == 0) return "0.00"; + BigDecimal d = getDecimalAmount(amount) + .setScale(maxDecimals, BigDecimal.ROUND_HALF_UP) + .stripTrailingZeros(); + if (d.scale() < 2) + d = d.setScale(2, BigDecimal.ROUND_UNNECESSARY); + return d.toPlainString(); + } + + static public String getFormattedAmount(double amount, boolean isCrypto) { + // at this point selection is XMR in case of error + String displayB; + if (isCrypto) { + if ((amount >= 0) || (amount == 0)) { + displayB = String.format(Locale.US, "%,.5f", amount); + } else { + displayB = null; + } + } else { // not crypto + displayB = String.format(Locale.US, "%,.2f", amount); + } + return displayB; + } + + static public String getDisplayAmount(double amount) { + // a Java bug does not strip zeros properly if the value is 0 + BigDecimal d = new BigDecimal(amount) + .setScale(XMR_DECIMALS, BigDecimal.ROUND_HALF_UP) + .stripTrailingZeros(); + if (d.scale() < 1) + d = d.setScale(1, BigDecimal.ROUND_UNNECESSARY); + return d.toPlainString(); + } + + static public Bitmap getBitmap(Context context, int drawableId) { + Drawable drawable = ContextCompat.getDrawable(context, drawableId); + if (drawable instanceof BitmapDrawable) { + return BitmapFactory.decodeResource(context.getResources(), drawableId); + } else if (drawable instanceof VectorDrawable) { + return getBitmap((VectorDrawable) drawable); + } else { + throw new IllegalArgumentException("unsupported drawable type"); + } + } + + static private Bitmap getBitmap(VectorDrawable vectorDrawable) { + Bitmap bitmap = Bitmap.createBitmap(vectorDrawable.getIntrinsicWidth(), + vectorDrawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + vectorDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + vectorDrawable.draw(canvas); + return bitmap; + } + + static final int HTTP_TIMEOUT = 5000; + + static public String getUrl(String httpsUrl) { + HttpsURLConnection urlConnection = null; + try { + URL url = new URL(httpsUrl); + urlConnection = (HttpsURLConnection) url.openConnection(); + urlConnection.setConnectTimeout(HTTP_TIMEOUT); + urlConnection.setReadTimeout(HTTP_TIMEOUT); + InputStreamReader in = new InputStreamReader(urlConnection.getInputStream()); + StringBuffer sb = new StringBuffer(); + final int BUFFER_SIZE = 512; + char[] buffer = new char[BUFFER_SIZE]; + int length = in.read(buffer, 0, BUFFER_SIZE); + while (length >= 0) { + sb.append(buffer, 0, length); + length = in.read(buffer, 0, BUFFER_SIZE); + } + return sb.toString(); + } catch (SocketTimeoutException ex) { + Timber.w("C %s", ex.getLocalizedMessage()); + } catch (MalformedURLException ex) { + Timber.e("A %s", ex.getLocalizedMessage()); + } catch (IOException ex) { + Timber.e("B %s", ex.getLocalizedMessage()); + } finally { + if (urlConnection != null) { + urlConnection.disconnect(); + } + } + return null; + } + + static public void clipBoardCopy(Context context, String label, String text) { + ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText(label, text); + clipboardManager.setPrimaryClip(clip); + } + + static public String getClipBoardText(Context context) { + final ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + try { + if (clipboardManager.hasPrimaryClip() + && clipboardManager.getPrimaryClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) { + final ClipData.Item item = clipboardManager.getPrimaryClip().getItemAt(0); + return item.getText().toString(); + } + } catch (NullPointerException ex) { + // if we have don't find a text in the clipboard + return null; + } + return null; + } + + static private Animation ShakeAnimation; + + static public Animation getShakeAnimation(Context context) { + if (ShakeAnimation == null) { + synchronized (Helper.class) { + if (ShakeAnimation == null) { + ShakeAnimation = AnimationUtils.loadAnimation(context, R.anim.shake); + } + } + } + return ShakeAnimation; + } + + 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 ""; + } + + public static byte[] hexToBytes(String hex) { + final int len = hex.length(); + final byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) + + Character.digit(hex.charAt(i + 1), 16)); + } + return data; + } + + static public void setMoneroHome(Context context) { + try { + String home = getStorage(context, MONERO_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, MONERO_DIR).getAbsolutePath(); + WalletManager.initLogger(home + "/monerujo", "monerujo.log"); + if (level >= WalletManager.LOGLEVEL_SILENT) + WalletManager.setLogLevel(level); + } + + static public boolean useCrazyPass(Context context) { + File flagFile = new File(getWalletRoot(context), NOCRAZYPASS_FLAGFILE); + return !flagFile.exists(); + } + + // 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().verifyWalletPasswordOnly(walletPath, password)) { + return password; + } + + // maybe this is a malformed CrAzYpass? + String possibleCrazyPass = CrazyPassEncoder.reformat(password); + if (possibleCrazyPass != null) { // looks like a CrAzYpass + if (WalletManager.getInstance().verifyWalletPasswordOnly(walletPath, possibleCrazyPass)) { + return possibleCrazyPass; + } + } + + // generate & try with CrAzYpass + String crazyPass = KeyStoreHelper.getCrazyPass(context, password); + if (WalletManager.getInstance().verifyWalletPasswordOnly(walletPath, crazyPass)) { + return crazyPass; + } + + // or maybe it is a broken CrAzYpass? (of which we have two variants) + String brokenCrazyPass2 = KeyStoreHelper.getBrokenCrazyPass(context, password, 2); + if ((brokenCrazyPass2 != null) + && WalletManager.getInstance().verifyWalletPasswordOnly(walletPath, brokenCrazyPass2)) { + return brokenCrazyPass2; + } + String brokenCrazyPass1 = KeyStoreHelper.getBrokenCrazyPass(context, password, 1); + if ((brokenCrazyPass1 != null) + && WalletManager.getInstance().verifyWalletPasswordOnly(walletPath, brokenCrazyPass1)) { + return brokenCrazyPass1; + } + + return null; + } + + static AlertDialog openDialog = null; // for preventing opening of multiple dialogs + static AsyncTask passwordTask = null; + + 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 MaterialAlertDialogBuilder(context); + alertDialogBuilder.setView(promptsView); + + final TextInputLayout etPassword = promptsView.findViewById(R.id.etPassword); + etPassword.setHint(context.getString(R.string.prompt_password, wallet)); + + final TextView tvOpenPrompt = promptsView.findViewById(R.id.tvOpenPrompt); + final Drawable icFingerprint = context.getDrawable(R.drawable.ic_fingerprint); + final Drawable icError = context.getDrawable(R.drawable.ic_error_red_36dp); + final Drawable icInfo = context.getDrawable(R.drawable.ic_info_white_24dp); + + final boolean fingerprintAuthCheck = FingerprintHelper.isFingerPassValid(context, wallet); + + final boolean fingerprintAuthAllowed = !fingerprintDisabled && fingerprintAuthCheck; + final CancellationSignal cancelSignal = new CancellationSignal(); + + final AtomicBoolean incorrectSavedPass = new AtomicBoolean(false); + + class PasswordTask extends AsyncTask { + private String pass; + private boolean fingerprintUsed; + + PasswordTask(String pass, boolean fingerprintUsed) { + this.pass = pass; + this.fingerprintUsed = fingerprintUsed; + } + + @Override + protected void onPreExecute() { + tvOpenPrompt.setCompoundDrawablesRelativeWithIntrinsicBounds(icInfo, null, null, null); + tvOpenPrompt.setText(context.getText(R.string.prompt_open_wallet)); + tvOpenPrompt.setVisibility(View.VISIBLE); + } + + @Override + protected Boolean doInBackground(Void... unused) { + return processPasswordEntry(context, wallet, pass, fingerprintUsed, action); + } + + @Override + protected void onPostExecute(Boolean result) { + if (result) { + Helper.hideKeyboardAlways((Activity) context); + cancelSignal.cancel(); + openDialog.dismiss(); + openDialog = null; + } else { + if (fingerprintUsed) { + incorrectSavedPass.set(true); + tvOpenPrompt.setCompoundDrawablesRelativeWithIntrinsicBounds(icError, null, null, null); + tvOpenPrompt.setText(context.getText(R.string.bad_saved_password)); + } else { + if (!fingerprintAuthAllowed) { + tvOpenPrompt.setVisibility(View.GONE); + } else if (incorrectSavedPass.get()) { + tvOpenPrompt.setCompoundDrawablesRelativeWithIntrinsicBounds(icError, null, null, null); + tvOpenPrompt.setText(context.getText(R.string.bad_password)); + } else { + tvOpenPrompt.setCompoundDrawablesRelativeWithIntrinsicBounds(icFingerprint, null, null, null); + tvOpenPrompt.setText(context.getText(R.string.prompt_fingerprint_auth)); + } + etPassword.setError(context.getString(R.string.bad_password)); + } + } + passwordTask = null; + } + } + + 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), + (dialog, id) -> { + action.fail(wallet); + Helper.hideKeyboardAlways((Activity) context); + cancelSignal.cancel(); + if (passwordTask != null) { + passwordTask.cancel(true); + passwordTask = null; + } + dialog.cancel(); + openDialog = null; + }); + openDialog = alertDialogBuilder.create(); + + final FingerprintManager.AuthenticationCallback fingerprintAuthCallback; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + fingerprintAuthCallback = null; + } else { + fingerprintAuthCallback = new FingerprintManager.AuthenticationCallback() { + @Override + public void onAuthenticationError(int errMsgId, CharSequence errString) { + tvOpenPrompt.setCompoundDrawablesRelativeWithIntrinsicBounds(icError, null, null, null); + tvOpenPrompt.setText(errString); + } + + @Override + public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) { + tvOpenPrompt.setCompoundDrawablesRelativeWithIntrinsicBounds(icError, null, null, null); + tvOpenPrompt.setText(helpString); + } + + @Override + public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) { + try { + String userPass = KeyStoreHelper.loadWalletUserPass(context, wallet); + if (passwordTask == null) { + passwordTask = new PasswordTask(userPass, true); + passwordTask.execute(); + } + } catch (KeyStoreHelper.BrokenPasswordStoreException ex) { + etPassword.setError(context.getString(R.string.bad_password)); + // TODO: better error message here - what would it be? + } + } + + @Override + public void onAuthenticationFailed() { + tvOpenPrompt.setCompoundDrawablesRelativeWithIntrinsicBounds(icError, null, null, null); + tvOpenPrompt.setText(context.getString(R.string.bad_fingerprint)); + } + }; + } + + openDialog.setOnShowListener(dialog -> { + if (fingerprintAuthAllowed && fingerprintAuthCallback != null) { + tvOpenPrompt.setCompoundDrawablesRelativeWithIntrinsicBounds(icFingerprint, null, null, null); + tvOpenPrompt.setText(context.getText(R.string.prompt_fingerprint_auth)); + tvOpenPrompt.setVisibility(View.VISIBLE); + FingerprintHelper.authenticate(context, cancelSignal, fingerprintAuthCallback); + } else { + etPassword.requestFocus(); + } + Button button = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE); + button.setOnClickListener(view -> { + String pass = etPassword.getEditText().getText().toString(); + if (passwordTask == null) { + passwordTask = new PasswordTask(pass, false); + passwordTask.execute(); + } + }); + }); + + // accept keyboard "ok" + etPassword.getEditText().setOnEditorActionListener((v, actionId, event) -> { + if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN)) + || (actionId == EditorInfo.IME_ACTION_DONE)) { + String pass = etPassword.getEditText().getText().toString(); + if (passwordTask == null) { + passwordTask = new PasswordTask(pass, false); + passwordTask.execute(); + } + return true; + } + return false; + }); + + if (Helper.preventScreenshot()) { + openDialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE); + } + + Helper.showKeyboard(openDialog); + openDialog.show(); + } + + public interface PasswordAction { + void act(String walletName, String password, boolean fingerprintUsed); + + void fail(String walletName); + } + + 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.act(walletName, walletPassword, fingerprintUsed); + return true; + } else { + action.fail(walletName); + return false; + } + } + + public interface Action { + boolean run(); + } + + static public boolean runWithNetwork(Action action) { + StrictMode.ThreadPolicy currentPolicy = StrictMode.getThreadPolicy(); + StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitNetwork().build(); + StrictMode.setThreadPolicy(policy); + try { + return action.run(); + } finally { + StrictMode.setThreadPolicy(currentPolicy); + } + } + + static public boolean preventScreenshot() { + return !(BuildConfig.DEBUG || BuildConfig.FLAVOR_type.equals("alpha")); + } +} 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 0000000..896d2d0 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/KeyStoreHelper.java @@ -0,0 +1,351 @@ +/* + * 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.content.SharedPreferences; +import android.os.Build; +import android.security.KeyPairGeneratorSpec; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; +import android.util.Base64; + +import androidx.annotation.NonNull; + +import java.io.IOException; +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.security.UnrecoverableEntryException; +import java.security.cert.CertificateException; +import java.util.Calendar; +import java.util.GregorianCalendar; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.security.auth.x500.X500Principal; + +import timber.log.Timber; + +public class KeyStoreHelper { + + static { + System.loadLibrary("monerujo"); + } + + public static native byte[] slowHash(byte[] data, int brokenVariant); + + static final private String RSA_ALIAS = "MonerujoRSA"; + + private static String getCrazyPass(Context context, String password, int brokenVariant) { + byte[] data = password.getBytes(StandardCharsets.UTF_8); + byte[] sig = null; + try { + KeyStoreHelper.createKeys(context, RSA_ALIAS); + sig = KeyStoreHelper.signData(RSA_ALIAS, data); + byte[] hash = slowHash(sig, brokenVariant); + if (hash == null) { + throw new IllegalStateException("Slow Hash is null!"); + } + return CrazyPassEncoder.encode(hash); + } catch (NoSuchProviderException | NoSuchAlgorithmException | + InvalidAlgorithmParameterException | KeyStoreException | + InvalidKeyException | SignatureException ex) { + throw new IllegalStateException(ex); + } + } + + public static String getCrazyPass(Context context, String password) { + if (Helper.useCrazyPass(context)) + return getCrazyPass(context, password, 0); + else + return password; + } + + public static String getBrokenCrazyPass(Context context, String password, int brokenVariant) { + // due to a link bug in the initial implementation, some crazypasses were built with + // prehash & variant == 1 + // since there are wallets out there, we need to keep this here + // yes, it's a mess + if (isArm32() && (brokenVariant != 2)) return null; + return getCrazyPass(context, password, brokenVariant); + } + + private static Boolean isArm32 = null; + + public static boolean isArm32() { + if (isArm32 != null) return isArm32; + synchronized (KeyStoreException.class) { + if (isArm32 != null) return isArm32; + isArm32 = Build.SUPPORTED_ABIS[0].equals("armeabi-v7a"); + return isArm32; + } + } + + public static boolean saveWalletUserPass(@NonNull 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); + SharedPreferences.Editor e = context.getSharedPreferences(SecurityConstants.WALLET_PASS_PREFS_NAME, Context.MODE_PRIVATE).edit(); + if (encrypted == null) { + e.remove(wallet).apply(); + return false; + } + e.putString(wallet, Base64.encodeToString(encrypted, Base64.DEFAULT)).apply(); + return true; + } catch (NoSuchProviderException | NoSuchAlgorithmException | + InvalidAlgorithmParameterException | KeyStoreException ex) { + Timber.w(ex); + return false; + } + } + + static public class BrokenPasswordStoreException extends Exception { + BrokenPasswordStoreException() { + super(); + } + + BrokenPasswordStoreException(Throwable cause) { + super(cause); + } + } + + public static boolean hasStoredPasswords(@NonNull Context context) { + SharedPreferences prefs = context.getSharedPreferences(SecurityConstants.WALLET_PASS_PREFS_NAME, Context.MODE_PRIVATE); + return prefs.getAll().size() > 0; + } + + public static String loadWalletUserPass(@NonNull Context context, String wallet) throws BrokenPasswordStoreException { + String walletKeyAlias = SecurityConstants.WALLET_PASS_KEY_PREFIX + wallet; + String encoded = context.getSharedPreferences(SecurityConstants.WALLET_PASS_PREFS_NAME, Context.MODE_PRIVATE) + .getString(wallet, ""); + if (encoded.isEmpty()) throw new BrokenPasswordStoreException(); + byte[] data = Base64.decode(encoded, Base64.DEFAULT); + byte[] decrypted = KeyStoreHelper.decrypt(walletKeyAlias, data); + if (decrypted == null) throw new BrokenPasswordStoreException(); + 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); + } catch (KeyStoreException ex) { + Timber.w(ex); + } + context.getSharedPreferences(SecurityConstants.WALLET_PASS_PREFS_NAME, Context.MODE_PRIVATE).edit() + .remove(wallet).apply(); + } + + public static void copyWalletUserPass(Context context, String srcWallet, String dstWallet) throws BrokenPasswordStoreException { + final String pass = loadWalletUserPass(context, srcWallet); + saveWalletUserPass(context, dstWallet, pass); + } + + /** + * 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. + */ + private 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 (IOException | CertificateException ex) { + 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); + } + } + } + + private static boolean deleteKeys(String alias) throws KeyStoreException { + KeyStore keyStore = KeyStore.getInstance(SecurityConstants.KEYSTORE_PROVIDER_ANDROID_KEYSTORE); + try { + keyStore.load(null); + keyStore.deleteEntry(alias); + return true; + } catch (IOException | NoSuchAlgorithmException | CertificateException ex) { + Timber.w(ex); + return false; + } + } + + public static boolean keyExists(String wallet) throws BrokenPasswordStoreException { + try { + KeyStore keyStore = KeyStore.getInstance(KeyStoreHelper.SecurityConstants.KEYSTORE_PROVIDER_ANDROID_KEYSTORE); + keyStore.load(null); + return keyStore.containsAlias(KeyStoreHelper.SecurityConstants.WALLET_PASS_KEY_PREFIX + wallet); + } catch (IOException | NoSuchAlgorithmException | CertificateException | KeyStoreException ex) { + throw new BrokenPasswordStoreException(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) throws NoSuchProviderException, + NoSuchAlgorithmException, InvalidAlgorithmParameterException { + 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"); + } + + private static PrivateKey getPrivateKey(String alias) { + try { + KeyStore ks = KeyStore + .getInstance(SecurityConstants.KEYSTORE_PROVIDER_ANDROID_KEYSTORE); + ks.load(null); + //KeyStore.Entry entry = ks.getEntry(alias, null); + PrivateKey privateKey = (PrivateKey) ks.getKey(alias, null); + + if (privateKey == null) { + Timber.w("No key found under alias: %s", alias); + return null; + } + + return privateKey; + } catch (IOException | NoSuchAlgorithmException | CertificateException + | UnrecoverableEntryException | KeyStoreException ex) { + throw new IllegalStateException(ex); + } + } + + private static PublicKey getPublicKey(String alias) { + try { + KeyStore ks = KeyStore + .getInstance(SecurityConstants.KEYSTORE_PROVIDER_ANDROID_KEYSTORE); + ks.load(null); + + PublicKey publicKey = ks.getCertificate(alias).getPublicKey(); + + if (publicKey == null) { + Timber.w("No public key"); + return null; + } + return publicKey; + } catch (IOException | NoSuchAlgorithmException | CertificateException + | KeyStoreException ex) { + throw new IllegalStateException(ex); + } + } + + private static byte[] encrypt(String alias, byte[] data) { + try { + PublicKey publicKey = getPublicKey(alias); + Cipher cipher = Cipher.getInstance(SecurityConstants.CIPHER_RSA_ECB_PKCS1); + + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + return cipher.doFinal(data); + } catch (InvalidKeyException | IllegalBlockSizeException | BadPaddingException + | NoSuchAlgorithmException | NoSuchPaddingException ex) { + Timber.e(ex); + return null; + } + } + + private static byte[] decrypt(String alias, byte[] data) { + try { + PrivateKey privateKey = getPrivateKey(alias); + if (privateKey == null) return null; + Cipher cipher = Cipher.getInstance(SecurityConstants.CIPHER_RSA_ECB_PKCS1); + + cipher.init(Cipher.DECRYPT_MODE, privateKey); + return cipher.doFinal(data); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | + IllegalBlockSizeException | BadPaddingException ex) { + Timber.e(ex); + return null; + } + } + + /** + * 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 + */ + private static byte[] signData(String alias, byte[] data) throws NoSuchAlgorithmException, + InvalidKeyException, SignatureException { + PrivateKey privateKey = getPrivateKey(alias); + if (privateKey == null) return null; + Signature s = Signature.getInstance(SecurityConstants.SIGNATURE_SHA256withRSA); + s.initSign(privateKey); + s.update(data); + return s.sign(); + } + + 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/LegacyStorageHelper.java b/app/src/main/java/com/m2049r/xmrwallet/util/LegacyStorageHelper.java new file mode 100644 index 0000000..20ea61c --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/LegacyStorageHelper.java @@ -0,0 +1,170 @@ +package com.m2049r.xmrwallet.util; + +import android.Manifest; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Environment; +import android.preference.PreferenceManager; + +import com.m2049r.xmrwallet.BuildConfig; +import com.m2049r.xmrwallet.model.WalletManager; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import lombok.RequiredArgsConstructor; +import timber.log.Timber; + +@RequiredArgsConstructor +public class LegacyStorageHelper { + final private File srcDir; + final private File dstDir; + + static public void migrateWallets(Context context) { + try { + if (isStorageMigrated(context)) return; + if (!hasReadPermission(context)) { + // can't migrate - don't remember this, as the user may turn on permissions later + return; + } + final File oldRoot = getWalletRoot(); + if (!oldRoot.exists()) { + // nothing to migrate, so don't try again + setStorageMigrated(context); + return; + } + final File newRoot = Helper.getWalletRoot(context); + (new LegacyStorageHelper(oldRoot, newRoot)).migrate(); + setStorageMigrated(context); // done it once - don't try again + } catch (IllegalStateException ex) { + Timber.d(ex); + // nothing we can do here + } + } + + public void migrate() { + String addressPrefix = WalletManager.getInstance().addressPrefix(); + File[] wallets = srcDir.listFiles((dir, filename) -> filename.endsWith(".keys")); + if (wallets == null) return; + for (File wallet : wallets) { + final String walletName = wallet.getName().substring(0, wallet.getName().length() - ".keys".length()); + if (addressPrefix.indexOf(getAddress(walletName).charAt(0)) < 0) { + Timber.d("skipping %s", walletName); + continue; + } + try { + copy(walletName); + } catch (IOException ex) { // something failed - try to clean up + deleteDst(walletName); + } + } + } + + // return "@" by default so we don't need to deal with null stuff + private String getAddress(String walletName) { + File addressFile = new File(srcDir, walletName + ".address.txt"); + if (!addressFile.exists()) return "@"; + try (BufferedReader addressReader = new BufferedReader(new FileReader(addressFile))) { + return addressReader.readLine(); + } catch (IOException ex) { + Timber.d(ex.getLocalizedMessage()); + } + return "@"; + } + + private void copy(String walletName) throws IOException { + final String dstName = getUniqueName(dstDir, walletName); + copyFile(new File(srcDir, walletName), new File(dstDir, dstName)); + copyFile(new File(srcDir, walletName + ".keys"), new File(dstDir, dstName + ".keys")); + } + + private void deleteDst(String walletName) { + // do our best, but if it fails, it fails + (new File(dstDir, walletName)).delete(); + (new File(dstDir, walletName + ".keys")).delete(); + } + + private void copyFile(File src, File dst) throws IOException { + if (!src.exists()) return; + Timber.d("%s => %s", src.getAbsolutePath(), dst.getAbsolutePath()); + try (FileChannel inChannel = new FileInputStream(src).getChannel(); + FileChannel outChannel = new FileOutputStream(dst).getChannel()) { + inChannel.transferTo(0, inChannel.size(), outChannel); + } + } + + private static boolean isExternalStorageWritable() { + String state = Environment.getExternalStorageState(); + return Environment.MEDIA_MOUNTED.equals(state); + } + + private static File getWalletRoot() { + if (!isExternalStorageWritable()) + throw new IllegalStateException(); + + // wallet folder for legacy (pre-Q) installations + final String FLAVOR_SUFFIX = + (BuildConfig.FLAVOR.startsWith("prod") ? "" : "." + BuildConfig.FLAVOR) + + (BuildConfig.DEBUG ? "-debug" : ""); + final String WALLET_DIR = "monerujo" + FLAVOR_SUFFIX; + + File dir = new File(Environment.getExternalStorageDirectory(), WALLET_DIR); + if (!dir.exists() || !dir.isDirectory()) + throw new IllegalStateException(); + return dir; + } + + private static boolean hasReadPermission(Context context) { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + return context.checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_DENIED; + } else { + return true; + } + } + + private static final Pattern WALLET_PATTERN = Pattern.compile("^(.+) \\(([0-9]+)\\).keys$"); + + private static String getUniqueName(File root, String name) { + if (!(new File(root, name + ".keys")).exists()) // does not exist => it's ok to use + return name; + + File[] wallets = root.listFiles( + (dir, filename) -> { + Matcher m = WALLET_PATTERN.matcher(filename); + if (m.find()) + return m.group(1).equals(name); + else return false; + }); + if (wallets.length == 0) return name + " (1)"; + int maxIndex = 0; + for (File wallet : wallets) { + try { + final Matcher m = WALLET_PATTERN.matcher(wallet.getName()); + if (!m.find()) + throw new IllegalStateException("this must match as it did before"); + final int index = Integer.parseInt(m.group(2)); + if (index > maxIndex) maxIndex = index; + } catch (NumberFormatException ex) { + // this cannot happen & we can ignore it if it does + } + } + return name + " (" + (maxIndex + 1) + ")"; + } + + private static final String MIGRATED_KEY = "migrated_legacy_storage"; + + public static boolean isStorageMigrated(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(MIGRATED_KEY, false); + } + + public static void setStorageMigrated(Context context) { + PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(MIGRATED_KEY, true).apply(); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/LocaleHelper.java b/app/src/main/java/com/m2049r/xmrwallet/util/LocaleHelper.java new file mode 100644 index 0000000..fb79cf9 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/LocaleHelper.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2018-2020 m2049r et al. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.util; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.Configuration; +import android.preference.PreferenceManager; + +import com.m2049r.xmrwallet.R; + +import java.util.ArrayList; +import java.util.Locale; + +public class LocaleHelper { + private static Locale SYSTEM_DEFAULT_LOCALE = Locale.getDefault(); + + public static ArrayList getAvailableLocales(Context context) { + ArrayList locales = new ArrayList<>(); + // R.string.available_locales gets generated in build.gradle by enumerating values-* folders + String[] availableLocales = context.getString(R.string.available_locales).split(","); + + for (String localeName : availableLocales) { + locales.add(Locale.forLanguageTag(localeName)); + } + + return locales; + } + + public static String getDisplayName(Locale locale, boolean sentenceCase) { + String displayName = locale.getDisplayName(locale); + + if (sentenceCase) { + displayName = toSentenceCase(displayName, locale); + } + + return displayName; + } + + public static Context setPreferredLocale(Context context) { + return setLocale(context, getPreferredLanguageTag(context)); + } + + public static Context setAndSaveLocale(Context context, String langaugeTag) { + savePreferredLangaugeTag(context, langaugeTag); + return setLocale(context, langaugeTag); + } + + private static Context setLocale(Context context, String languageTag) { + Locale locale = (languageTag.isEmpty()) ? SYSTEM_DEFAULT_LOCALE : Locale.forLanguageTag(languageTag); + Locale.setDefault(locale); + + Configuration configuration = context.getResources().getConfiguration(); + configuration.setLocale(locale); + configuration.setLayoutDirection(locale); + + return context.createConfigurationContext(configuration); + } + + public static void updateSystemDefaultLocale(Locale locale) { + SYSTEM_DEFAULT_LOCALE = locale; + } + + private static String toSentenceCase(String str, Locale locale) { + if (str.isEmpty()) { + return str; + } + + int firstCodePointLen = str.offsetByCodePoints(0, 1); + return str.substring(0, firstCodePointLen).toUpperCase(locale) + + str.substring(firstCodePointLen); + } + + public static Locale getPreferredLocale(Context context) { + String languageTag = getPreferredLanguageTag(context); + return languageTag.isEmpty() ? SYSTEM_DEFAULT_LOCALE : Locale.forLanguageTag(languageTag); + } + + public static String getPreferredLanguageTag(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context) + .getString("preferred_locale", ""); + // cannot access getString here as it's done BEFORE string locale is set + } + + @SuppressLint("ApplySharedPref") + private static void savePreferredLangaugeTag(Context context, String locale) { + PreferenceManager.getDefaultSharedPreferences(context).edit() + .putString(context.getString(R.string.preferred_locale), locale).commit(); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/MoneroThreadPoolExecutor.java b/app/src/main/java/com/m2049r/xmrwallet/util/MoneroThreadPoolExecutor.java new file mode 100644 index 0000000..27bb64c --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/MoneroThreadPoolExecutor.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.util; + +import com.m2049r.xmrwallet.service.MoneroHandlerThread; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + + +public class MoneroThreadPoolExecutor { + private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); + private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4)); + private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1; + private static final int KEEP_ALIVE_SECONDS = 30; + + private static final ThreadFactory sThreadFactory = new ThreadFactory() { + private final AtomicInteger mCount = new AtomicInteger(1); + + public Thread newThread(Runnable r) { + return new Thread(null, r, "MoneroTask #" + mCount.getAndIncrement(), MoneroHandlerThread.THREAD_STACK_SIZE); + } + }; + + private static final BlockingQueue sPoolWorkQueue = + new LinkedBlockingQueue<>(128); + + public static final Executor MONERO_THREAD_POOL_EXECUTOR; + + static { + ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( + CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS, + sPoolWorkQueue, sThreadFactory); + threadPoolExecutor.allowCoreThreadTimeOut(true); + MONERO_THREAD_POOL_EXECUTOR = threadPoolExecutor; + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/NetCipherHelper.java b/app/src/main/java/com/m2049r/xmrwallet/util/NetCipherHelper.java new file mode 100644 index 0000000..418e97f --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/NetCipherHelper.java @@ -0,0 +1,393 @@ +/* + * Copyright (c) 2021 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.util; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; + +import com.burgstaller.okhttp.AuthenticationCacheInterceptor; +import com.burgstaller.okhttp.CachingAuthenticatorDecorator; +import com.burgstaller.okhttp.digest.CachingAuthenticator; +import com.burgstaller.okhttp.digest.Credentials; +import com.burgstaller.okhttp.digest.DigestAuthenticator; + +import org.json.JSONObject; + +import java.io.IOException; +import java.net.Proxy; +import java.util.ArrayList; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +import info.guardianproject.netcipher.client.StrongOkHttpClientBuilder; +import info.guardianproject.netcipher.proxy.OrbotHelper; +import info.guardianproject.netcipher.proxy.SignatureUtils; +import info.guardianproject.netcipher.proxy.StatusCallback; +import lombok.RequiredArgsConstructor; +import lombok.ToString; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.HttpUrl; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.RequestBody; +import okhttp3.Response; +import timber.log.Timber; + +@RequiredArgsConstructor +public class NetCipherHelper implements StatusCallback { + public static final String USER_AGENT = "Monerujo/1.0"; + public static final int HTTP_TIMEOUT = 1000; //ms + public static final int TOR_TIMEOUT_CONNECT = 5000; //ms + public static final int TOR_TIMEOUT = 2000; //ms + + public interface OnStatusChangedListener { + void connected(); + + void disconnected(); + + void notInstalled(); + + void notEnabled(); + } + + final private Context context; + final private OrbotHelper orbot; + + @SuppressLint("StaticFieldLeak") + private static NetCipherHelper Instance; + + public static void createInstance(Context context) { + if (Instance == null) { + synchronized (NetCipherHelper.class) { + if (Instance == null) { + final Context applicationContext = context.getApplicationContext(); + Instance = new NetCipherHelper(applicationContext, OrbotHelper.get(context).statusTimeout(5000)); + } + } + } + } + + public static NetCipherHelper getInstance() { + if (Instance == null) throw new IllegalStateException("NetCipherHelper is null"); + return Instance; + } + + private OkHttpClient client; + + private void createTorClient(Intent statusIntent) { + String orbotStatus = statusIntent.getStringExtra(OrbotHelper.EXTRA_STATUS); + if (orbotStatus == null) throw new IllegalStateException("status is null"); + if (!orbotStatus.equals(OrbotHelper.STATUS_ON)) + throw new IllegalStateException("Orbot is not ON"); + try { + final OkHttpClient.Builder okBuilder = new OkHttpClient.Builder() + .connectTimeout(TOR_TIMEOUT_CONNECT, TimeUnit.MILLISECONDS) + .writeTimeout(TOR_TIMEOUT, TimeUnit.MILLISECONDS) + .readTimeout(TOR_TIMEOUT, TimeUnit.MILLISECONDS); + client = new StrongOkHttpClientBuilder(context) + .withSocksProxy() + .applyTo(okBuilder, statusIntent) + .build(); + Helper.ALLOW_SHIFT = false; // no shifting with Tor + } catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + + private void createClearnetClient() { + try { + client = new OkHttpClient.Builder() + .connectTimeout(HTTP_TIMEOUT, TimeUnit.MILLISECONDS) + .writeTimeout(HTTP_TIMEOUT, TimeUnit.MILLISECONDS) + .readTimeout(HTTP_TIMEOUT, TimeUnit.MILLISECONDS) + .build(); + Helper.ALLOW_SHIFT = true; + } catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + + private OnStatusChangedListener onStatusChangedListener; + + public static void deregister() { + getInstance().onStatusChangedListener = null; + } + + public static void register(OnStatusChangedListener listener) { + final NetCipherHelper me = getInstance(); + me.onStatusChangedListener = listener; + + // NOT_INSTALLED is dealt with through the callbacks + me.orbot.removeStatusCallback(me) // make sure we are registered just once + .addStatusCallback(me); + + // deal with org.torproject.android.intent.action.STATUS = STARTS_DISABLED + me.context.registerReceiver(orbotStatusReceiver, new IntentFilter(OrbotHelper.ACTION_STATUS)); + + me.startTor(); + } + + // for StatusCallback + public enum Status { + STARTING, + ENABLED, + STOPPING, + DISABLED, + NOT_INSTALLED, + NOT_ENABLED, + UNKNOWN; + } + + private Status status = Status.UNKNOWN; + + @Override + public void onStarting() { + Timber.d("onStarting"); + status = Status.STARTING; + } + + @Override + public void onEnabled(Intent statusIntent) { + Timber.d("onEnabled"); + if (getTorPref() != Status.ENABLED) return; // do we want Tor? + createTorClient(statusIntent); + status = Status.ENABLED; + if (onStatusChangedListener != null) { + new Thread(() -> onStatusChangedListener.connected()).start(); + } + } + + @Override + public void onStopping() { + Timber.d("onStopping"); + status = Status.STOPPING; + } + + @Override + public void onDisabled() { + Timber.d("onDisabled"); + createClearnetClient(); + status = Status.DISABLED; + if (onStatusChangedListener != null) { + new Thread(() -> onStatusChangedListener.disconnected()).start(); + } + } + + @Override + public void onStatusTimeout() { + Timber.d("onStatusTimeout"); + createClearnetClient(); + // (timeout does not not change the status) + if (onStatusChangedListener != null) { + new Thread(() -> onStatusChangedListener.disconnected()).start(); + } + orbotInit = false; // do init() next time we try to open Tor + } + + @Override + public void onNotYetInstalled() { + Timber.d("onNotYetInstalled"); + // never mind then + orbot.removeStatusCallback(this); + createClearnetClient(); + status = Status.NOT_INSTALLED; + if (onStatusChangedListener != null) { + new Thread(() -> onStatusChangedListener.notInstalled()).start(); + } + } + + // user has not enabled background Orbot starts + public void onNotEnabled() { + Timber.d("onNotEnabled"); + // keep the callback in case they turn it on manually + setTorPref(Status.DISABLED); + createClearnetClient(); + status = Status.NOT_ENABLED; + if (onStatusChangedListener != null) { + new Thread(() -> onStatusChangedListener.notEnabled()).start(); + } + } + + static public Status getStatus() { + return getInstance().status; + } + + public void toggle() { + switch (getStatus()) { + case ENABLED: + onDisabled(); + setTorPref(Status.DISABLED); + break; + case DISABLED: + setTorPref(Status.ENABLED); + startTor(); + break; + } + } + + private boolean orbotInit = false; + + private void startTor() { + if (!isOrbotInstalled()) { + onNotYetInstalled(); + } else if (getTorPref() == Status.DISABLED) { + onDisabled(); + } else if (!orbotInit) { + orbotInit = orbot.init(); + } else { + orbot.requestStart(context); + } + } + + // extracted from OrbotHelper + private boolean isOrbotInstalled() { + ArrayList hashes = new ArrayList<>(); + // Tor Project signing key + hashes.add("A4:54:B8:7A:18:47:A8:9E:D7:F5:E7:0F:BA:6B:BA:96:F3:EF:29:C2:6E:09:81:20:4F:E3:47:BF:23:1D:FD:5B"); + // f-droid.org signing key + hashes.add("A7:02:07:92:4F:61:FF:09:37:1D:54:84:14:5C:4B:EE:77:2C:55:C1:9E:EE:23:2F:57:70:E1:82:71:F7:CB:AE"); + + return null != SignatureUtils.validateBroadcastIntent(context, + OrbotHelper.getOrbotStartIntent(context), + hashes, false); + } + + + static public boolean hasClient() { + return getInstance().client != null; + } + + static public boolean isTor() { + return getStatus() == Status.ENABLED; + } + + static public String getProxy() { + if (!isTor()) return ""; + final Proxy proxy = getInstance().client.proxy(); + if (proxy == null) return ""; + return proxy.address().toString().substring(1); + } + + @ToString + static public class Request { + final HttpUrl url; + final String json; + final String username; + final String password; + + public Request(final HttpUrl url, final String json, final String username, final String password) { + this.url = url; + this.json = json; + this.username = username; + this.password = password; + } + + public Request(final HttpUrl url, final JSONObject json) { + this(url, json == null ? null : json.toString(), null, null); + } + + public Request(final HttpUrl url) { + this(url, null, null, null); + } + + public void enqueue(Callback callback) { + newCall().enqueue(callback); + } + + public Response execute() throws IOException { + return newCall().execute(); + } + + private Call newCall() { + return getClient().newCall(getRequest()); + } + + private OkHttpClient getClient() { + if (mockClient != null) return mockClient; // Unit-test mode + final OkHttpClient client = getInstance().client; + if ((username != null) && (!username.isEmpty())) { + final DigestAuthenticator authenticator = new DigestAuthenticator(new Credentials(username, password)); + final Map authCache = new ConcurrentHashMap<>(); + return client.newBuilder() + .authenticator(new CachingAuthenticatorDecorator(authenticator, authCache)) + .addInterceptor(new AuthenticationCacheInterceptor(authCache)) + .build(); + // TODO: maybe cache & reuse the client for these credentials? + } else { + return client; + } + } + + private okhttp3.Request getRequest() { + final okhttp3.Request.Builder builder = + new okhttp3.Request.Builder() + .url(url) + .header("User-Agent", USER_AGENT); + if (json != null) { + builder.post(RequestBody.create(json, MediaType.parse("application/json"))); + } else { + builder.get(); + } + return builder.build(); + } + + // for unit tests only + static public OkHttpClient mockClient = null; + } + + private static final String PREFS_NAME = "tor"; + private static final String PREFS_STATUS = "status"; + private Status currentPref = Status.UNKNOWN; + + private Status getTorPref() { + if (currentPref != Status.UNKNOWN) return currentPref; + currentPref = Status.valueOf(context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .getString(PREFS_STATUS, "DISABLED")); + return currentPref; + } + + private void setTorPref(Status status) { + if (getTorPref() == status) return; // no change + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .edit() + .putString(PREFS_STATUS, status.name()) + .apply(); + currentPref = status; + } + + private static final BroadcastReceiver orbotStatusReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + Timber.d("%s/%s", intent.getAction(), intent.getStringExtra(OrbotHelper.EXTRA_STATUS)); + if (OrbotHelper.ACTION_STATUS.equals(intent.getAction())) { + if (OrbotHelper.STATUS_STARTS_DISABLED.equals(intent.getStringExtra(OrbotHelper.EXTRA_STATUS))) { + getInstance().onNotEnabled(); + } + } + } + }; + + public void installOrbot(Activity host) { + host.startActivity(OrbotHelper.getOrbotInstallIntent(context)); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/NightmodeHelper.java b/app/src/main/java/com/m2049r/xmrwallet/util/NightmodeHelper.java new file mode 100644 index 0000000..afe4ce4 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/NightmodeHelper.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2020 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.util; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +import androidx.appcompat.app.AppCompatDelegate; + +import com.m2049r.xmrwallet.R; + +public class NightmodeHelper { + public static DayNightMode getPreferredNightmode(Context context) { + return DayNightMode.valueOf(PreferenceManager.getDefaultSharedPreferences(context) + .getString(context.getString(R.string.preferred_nightmode), "UNKNOWN")); + } + + public static void setPreferredNightmode(Context context) { + final DayNightMode mode = DayNightMode.valueOf(PreferenceManager.getDefaultSharedPreferences(context) + .getString(context.getString(R.string.preferred_nightmode), "UNKNOWN")); + if (mode == DayNightMode.UNKNOWN) + setAndSavePreferredNightmode(context, DayNightMode.AUTO); + else + setNightMode(mode); + } + + public static void setAndSavePreferredNightmode(Context context, DayNightMode mode) { + PreferenceManager.getDefaultSharedPreferences(context).edit() + .putString(context.getString(R.string.preferred_nightmode), mode.name()).apply(); + setNightMode(mode); + } + + @SuppressLint("WrongConstant") + public static void setNightMode(DayNightMode mode) { + AppCompatDelegate.setDefaultNightMode(mode.getNightMode()); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/NodePinger.java b/app/src/main/java/com/m2049r/xmrwallet/util/NodePinger.java new file mode 100644 index 0000000..e353de1 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/NodePinger.java @@ -0,0 +1,53 @@ +/* + * 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 com.m2049r.xmrwallet.data.NodeInfo; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import timber.log.Timber; + +public class NodePinger { + static final public int NUM_THREADS = 10; + static final public long MAX_TIME = 5L; // seconds + + public interface Listener { + void publish(NodeInfo node); + } + + static public void execute(Collection nodes, final Listener listener) { + final ExecutorService exeService = Executors.newFixedThreadPool(NUM_THREADS); + List> taskList = new ArrayList<>(); + for (NodeInfo node : nodes) { + taskList.add(() -> node.testRpcService(listener)); + } + + try { + exeService.invokeAll(taskList, MAX_TIME, TimeUnit.SECONDS); + } catch (InterruptedException ex) { + Timber.w(ex); + } + exeService.shutdownNow(); + } +} 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 0000000..727c395 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/Notice.java @@ -0,0 +1,127 @@ +/* + * 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.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; + +import com.m2049r.xmrwallet.R; +import com.m2049r.xmrwallet.dialog.HelpFragment; +import com.m2049r.xmrwallet.ledger.Ledger; + +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_SEND = "notice_xmrto_enabled_send"; + private static final String NOTICE_SHOW_LEDGER = "notice_ledger_enabled_login"; + + private static void init() { + synchronized (Notice.class) { + if (notices != null) return; + notices = new ArrayList<>(); + if (Helper.ALLOW_SHIFT) + notices.add( + new Notice(NOTICE_SHOW_XMRTO_ENABLED_SEND, + R.string.info_xmrto_enabled, + R.string.help_xmrto, + 1) + ); + if (Ledger.ENABLED) + notices.add( + new Notice(NOTICE_SHOW_LEDGER, + R.string.info_ledger_enabled, + R.string.help_create_ledger, + 1) + ); + } + } + + 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(v -> HelpFragment.display(fragmentManager, helpResId)); + + ImageButton ib = 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/OnionHelper.java b/app/src/main/java/com/m2049r/xmrwallet/util/OnionHelper.java new file mode 100644 index 0000000..f5a416a --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/OnionHelper.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2021 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.util; + +public class OnionHelper { + + public static boolean isOnionHost(String hostname) { + return hostname.endsWith(".onion"); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/OpenAliasHelper.java b/app/src/main/java/com/m2049r/xmrwallet/util/OpenAliasHelper.java new file mode 100644 index 0000000..b710fbc --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/OpenAliasHelper.java @@ -0,0 +1,244 @@ +/* + * Copyright (c) 2018 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Specs from https://openalias.org/ + +package com.m2049r.xmrwallet.util; + +import android.os.AsyncTask; + +import com.m2049r.xmrwallet.data.BarcodeData; +import com.m2049r.xmrwallet.data.Crypto; + +import org.jitsi.dnssec.validator.ValidatingResolver; +import org.xbill.DNS.DClass; +import org.xbill.DNS.Flags; +import org.xbill.DNS.Message; +import org.xbill.DNS.Name; +import org.xbill.DNS.RRset; +import org.xbill.DNS.Rcode; +import org.xbill.DNS.Record; +import org.xbill.DNS.Section; +import org.xbill.DNS.SimpleResolver; +import org.xbill.DNS.TXTRecord; +import org.xbill.DNS.Type; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +import timber.log.Timber; + +public class OpenAliasHelper { + public static final String OA1_SCHEME = "oa1:"; + public static final String OA1_ASSET = "asset"; + public static final String OA1_ADDRESS = "recipient_address"; + public static final String OA1_NAME = "recipient_name"; + public static final String OA1_DESCRIPTION = "tx_description"; + public static final String OA1_AMOUNT = "tx_amount"; + + public static final int DNS_LOOKUP_TIMEOUT = 2500; // ms + + public static void resolve(String name, OnResolvedListener resolvedListener) { + new DnsTxtResolver(resolvedListener).execute(name); + } + + public static Map parse(String oaString) { + return new OpenAliasParser(oaString).parse(); + } + + public interface OnResolvedListener { + void onResolved(Map dataMap); + + void onFailure(); + } + + private static class DnsTxtResolver extends AsyncTask { + List txts = new ArrayList<>(); + boolean dnssec = false; + + private final OnResolvedListener resolvedListener; + + private DnsTxtResolver(OnResolvedListener resolvedListener) { + this.resolvedListener = resolvedListener; + } + + // trust anchor of the root zone + // http://data.iana.org/root-anchors/root-anchors.xml + final String ROOT = + ". IN DS 19036 8 2 49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5\n" + + ". IN DS 20326 8 2 E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D"; + final String[] DNSSEC_SERVERS = { + "4.2.2.1", // Level3 + "4.2.2.2", // Level3 + "4.2.2.6", // Level3 + "1.1.1.1", // cloudflare + "9.9.9.9", // quad9 + "8.8.4.4", // google + "8.8.8.8" // google + }; + + @Override + protected Boolean doInBackground(String... args) { + //main(); + if (args.length != 1) return false; + String name = args[0]; + if ((name == null) || (name.isEmpty())) + return false; //pointless trying to lookup nothing + Timber.d("Resolving %s", name); + try { + SimpleResolver sr = new SimpleResolver(DNSSEC_SERVERS[new Random().nextInt(DNSSEC_SERVERS.length)]); + ValidatingResolver vr = new ValidatingResolver(sr); + vr.setTimeout(0, DNS_LOOKUP_TIMEOUT); + vr.loadTrustAnchors(new ByteArrayInputStream(ROOT.getBytes("ASCII"))); + Record qr = Record.newRecord(Name.fromConstantString(name + "."), Type.TXT, DClass.IN); + Message response = vr.send(Message.newQuery(qr)); + final int rcode = response.getRcode(); + if (rcode != Rcode.NOERROR) { + Timber.i("Rcode: %s", Rcode.string(rcode)); + for (RRset set : response.getSectionRRsets(Section.ADDITIONAL)) { + if (set.getName().equals(Name.root) && set.getType() == Type.TXT + && set.getDClass() == ValidatingResolver.VALIDATION_REASON_QCLASS) { + Timber.i("Reason: %s", ((TXTRecord) set.first()).getStrings().get(0)); + } + } + return false; + } else { + dnssec = response.getHeader().getFlag(Flags.AD); + for (Record record : response.getSectionArray(Section.ANSWER)) { + if (record.getType() == Type.TXT) { + txts.addAll(((TXTRecord) record).getStrings()); + } + } + } + } catch (IOException | IllegalArgumentException ex) { + return false; + } + return true; + } + + @Override + public void onPostExecute(Boolean success) { + if (resolvedListener != null) + if (success) { + Map dataMap = new HashMap<>(); + for (String txt : txts) { + BarcodeData bc = BarcodeData.parseOpenAlias(txt, dnssec); + if (bc != null) { + if (!dataMap.containsKey(bc.asset)) { + dataMap.put(bc.asset, bc); + } + } + } + resolvedListener.onResolved(dataMap); + } else { + resolvedListener.onFailure(); + } + } + } + + private static class OpenAliasParser { + int currentPos = 0; + final String oaString; + StringBuilder sb = new StringBuilder(); + + OpenAliasParser(String oaString) { + this.oaString = oaString; + } + + Map parse() { + if ((oaString == null) || !oaString.startsWith(OA1_SCHEME)) return null; + if (oaString.charAt(oaString.length() - 1) != ';') return null; + + Map oaAttributes = new HashMap<>(); + + final int assetEnd = oaString.indexOf(' '); + if (assetEnd > 20) return null; // random sanity check + String asset = oaString.substring(OA1_SCHEME.length(), assetEnd); + oaAttributes.put(OA1_ASSET, asset); + + boolean inQuote = false; + boolean inKey = true; + String key = null; + for (currentPos = assetEnd; currentPos < oaString.length() - 1; currentPos++) { + char c = currentChar(); + if (inKey) { + if ((sb.length() == 0) && Character.isWhitespace(c)) continue; + if ((c == '\\') || (c == ';')) return null; + if (c == '=') { + key = sb.toString(); + if (oaAttributes.containsKey(key)) return null; // no duplicate keys allowed + sb.setLength(0); + inKey = false; + } else { + sb.append(c); + } + continue; + } + + // now we are in the value + if ((sb.length() == 0) && (c == '"')) { + inQuote = true; + continue; + } + if ((!inQuote || ((sb.length() > 0) && (c == '"'))) && (nextChar() == ';')) { + if (!inQuote) appendCurrentEscapedChar(); + oaAttributes.put(key, sb.toString()); + sb.setLength(0); + currentPos++; // skip the next ; + inQuote = false; + inKey = true; + key = null; + continue; + } + appendCurrentEscapedChar(); + } + if (inQuote) return null; + + if (key != null) { + oaAttributes.put(key, sb.toString()); + } + + return oaAttributes; + } + + char currentChar() { + return oaString.charAt(currentPos); + } + + char nextChar() throws IndexOutOfBoundsException { + int pos = currentPos; + char c = oaString.charAt(pos); + if (c == '\\') { + pos++; + } + return oaString.charAt(pos + 1); + } + + void appendCurrentEscapedChar() throws IndexOutOfBoundsException { + char c = oaString.charAt(currentPos); + if (c == '\\') { + c = oaString.charAt(++currentPos); + } + sb.append(c); + } + + } +} \ 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 new file mode 100644 index 0000000..00e5952 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/RestoreHeight.java @@ -0,0 +1,211 @@ +/* + * 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 java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; + +public class RestoreHeight { + static final int DIFFICULTY_TARGET = 120; // seconds + + static private RestoreHeight Singleton = null; + + static public RestoreHeight getInstance() { + if (Singleton == null) { + synchronized (RestoreHeight.class) { + if (Singleton == null) { + Singleton = new RestoreHeight(); + } + } + } + return Singleton; + } + + private Map blockheight = new HashMap<>(); + + RestoreHeight() { + blockheight.put("2014-05-01", 18844L); + blockheight.put("2014-06-01", 65406L); + blockheight.put("2014-07-01", 108882L); + blockheight.put("2014-08-01", 153594L); + blockheight.put("2014-09-01", 198072L); + blockheight.put("2014-10-01", 241088L); + blockheight.put("2014-11-01", 285305L); + blockheight.put("2014-12-01", 328069L); + blockheight.put("2015-01-01", 372369L); + blockheight.put("2015-02-01", 416505L); + blockheight.put("2015-03-01", 456631L); + blockheight.put("2015-04-01", 501084L); + blockheight.put("2015-05-01", 543973L); + blockheight.put("2015-06-01", 588326L); + blockheight.put("2015-07-01", 631187L); + blockheight.put("2015-08-01", 675484L); + blockheight.put("2015-09-01", 719725L); + blockheight.put("2015-10-01", 762463L); + blockheight.put("2015-11-01", 806528L); + blockheight.put("2015-12-01", 849041L); + blockheight.put("2016-01-01", 892866L); + blockheight.put("2016-02-01", 936736L); + blockheight.put("2016-03-01", 977691L); + blockheight.put("2016-04-01", 1015848L); + blockheight.put("2016-05-01", 1037417L); + blockheight.put("2016-06-01", 1059651L); + blockheight.put("2016-07-01", 1081269L); + blockheight.put("2016-08-01", 1103630L); + blockheight.put("2016-09-01", 1125983L); + blockheight.put("2016-10-01", 1147617L); + blockheight.put("2016-11-01", 1169779L); + blockheight.put("2016-12-01", 1191402L); + blockheight.put("2017-01-01", 1213861L); + blockheight.put("2017-02-01", 1236197L); + blockheight.put("2017-03-01", 1256358L); + blockheight.put("2017-04-01", 1278622L); + blockheight.put("2017-05-01", 1300239L); + blockheight.put("2017-06-01", 1322564L); + blockheight.put("2017-07-01", 1344225L); + blockheight.put("2017-08-01", 1366664L); + blockheight.put("2017-09-01", 1389113L); + blockheight.put("2017-10-01", 1410738L); + blockheight.put("2017-11-01", 1433039L); + 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); + blockheight.put("2018-05-01", 1562861L); + blockheight.put("2018-06-01", 1585135L); + blockheight.put("2018-07-01", 1606715L); + blockheight.put("2018-08-01", 1629017L); + blockheight.put("2018-09-01", 1651347L); + blockheight.put("2018-10-01", 1673031L); + blockheight.put("2018-11-01", 1695128L); + blockheight.put("2018-12-01", 1716687L); + blockheight.put("2019-01-01", 1738923L); + blockheight.put("2019-02-01", 1761435L); + blockheight.put("2019-03-01", 1781681L); + blockheight.put("2019-04-01", 1803081L); + blockheight.put("2019-05-01", 1824671L); + blockheight.put("2019-06-01", 1847005L); + blockheight.put("2019-07-01", 1868590L); + blockheight.put("2019-08-01", 1890878L); + blockheight.put("2019-09-01", 1913201L); + blockheight.put("2019-10-01", 1934732L); + blockheight.put("2019-11-01", 1957051L); + blockheight.put("2019-12-01", 1978433L); + blockheight.put("2020-01-01", 2001315L); + blockheight.put("2020-02-01", 2023656L); + blockheight.put("2020-03-01", 2044552L); + blockheight.put("2020-04-01", 2066806L); + blockheight.put("2020-05-01", 2088411L); + blockheight.put("2020-06-01", 2110702L); + blockheight.put("2020-07-01", 2132318L); + blockheight.put("2020-08-01", 2154590L); + blockheight.put("2020-09-01", 2176790L); + blockheight.put("2020-10-01", 2198370L); + blockheight.put("2020-11-01", 2220670L); + blockheight.put("2020-12-01", 2242241L); + blockheight.put("2021-01-01", 2264584L); + blockheight.put("2021-02-01", 2286892L); + blockheight.put("2021-03-01", 2307079L); + blockheight.put("2021-04-01", 2329385L); + blockheight.put("2021-05-01", 2351004L); + blockheight.put("2021-06-01", 2373306L); + blockheight.put("2021-07-01", 2394882L); + blockheight.put("2021-08-01", 2417162L); + blockheight.put("2021-09-01", 2439490L); + blockheight.put("2021-10-01", 2461020L); + blockheight.put("2021-11-01", 2483377L); + blockheight.put("2021-12-01", 2504932L); + blockheight.put("2022-01-01", 2527316L); + blockheight.put("2022-02-01", 2549605L); + blockheight.put("2022-03-01", 2569711L); + } + + public long getHeight(String date) { + SimpleDateFormat parser = new SimpleDateFormat("yyyy-MM-dd"); + parser.setTimeZone(TimeZone.getTimeZone("UTC")); + parser.setLenient(false); + try { + return getHeight(parser.parse(date)); + } catch (ParseException ex) { + throw new IllegalArgumentException(ex); + } + } + + public long getHeight(final Date date) { + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + cal.set(Calendar.DST_OFFSET, 0); + cal.setTime(date); + cal.add(Calendar.DAY_OF_MONTH, -4); // give it some leeway + if (cal.get(Calendar.YEAR) < 2014) + return 0; + if ((cal.get(Calendar.YEAR) == 2014) && (cal.get(Calendar.MONTH) <= 3)) + // before May 2014 + return 0; + + Calendar query = (Calendar) cal.clone(); + + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd"); + formatter.setTimeZone(TimeZone.getTimeZone("UTC")); + + String queryDate = formatter.format(date); + + cal.set(Calendar.DAY_OF_MONTH, 1); + long prevTime = cal.getTimeInMillis(); + String prevDate = formatter.format(prevTime); + // lookup blockheight at first of the month + Long prevBc = blockheight.get(prevDate); + if (prevBc == null) { + // if too recent, go back in time and find latest one we have + while (prevBc == null) { + cal.add(Calendar.MONTH, -1); + if (cal.get(Calendar.YEAR) < 2014) { + throw new IllegalStateException("endless loop looking for blockheight"); + } + prevTime = cal.getTimeInMillis(); + prevDate = formatter.format(prevTime); + prevBc = blockheight.get(prevDate); + } + } + long height = prevBc; + // now we have a blockheight & a date ON or BEFORE the restore date requested + if (queryDate.equals(prevDate)) return height; + // see if we have a blockheight after this date + cal.add(Calendar.MONTH, 1); + long nextTime = cal.getTimeInMillis(); + String nextDate = formatter.format(nextTime); + Long nextBc = blockheight.get(nextDate); + if (nextBc != null) { // we have a range - interpolate the blockheight we are looking for + long diff = nextBc - prevBc; + long diffDays = TimeUnit.DAYS.convert(nextTime - prevTime, TimeUnit.MILLISECONDS); + long days = TimeUnit.DAYS.convert(query.getTimeInMillis() - prevTime, + TimeUnit.MILLISECONDS); + height = Math.round(prevBc + diff * (1.0 * days / diffDays)); + } else { + long days = TimeUnit.DAYS.convert(query.getTimeInMillis() - prevTime, + TimeUnit.MILLISECONDS); + height = Math.round(prevBc + 1.0 * days * (24f * 60 * 60 / DIFFICULTY_TARGET)); + } + return height; + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/ServiceHelper.java b/app/src/main/java/com/m2049r/xmrwallet/util/ServiceHelper.java new file mode 100644 index 0000000..2762291 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/ServiceHelper.java @@ -0,0 +1,24 @@ +package com.m2049r.xmrwallet.util; + +import com.m2049r.xmrwallet.model.NetworkType; +import com.m2049r.xmrwallet.model.WalletManager; +import com.m2049r.xmrwallet.service.exchange.api.ExchangeApi; + +import okhttp3.HttpUrl; + +public class ServiceHelper { + public static String ASSET = null; + + static public HttpUrl getXmrToBaseUrl() { + if ((WalletManager.getInstance() == null) + || (WalletManager.getInstance().getNetworkType() != NetworkType.NetworkType_Mainnet)) { + throw new IllegalStateException("Only mainnet not supported"); + } else { + return HttpUrl.parse("https://sideshift.ai/api/v1/"); + } + } + + static public ExchangeApi getExchangeApi() { + return new com.m2049r.xmrwallet.service.exchange.krakenEcb.ExchangeApiImpl(); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/ThemeHelper.java b/app/src/main/java/com/m2049r/xmrwallet/util/ThemeHelper.java new file mode 100644 index 0000000..21ff733 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/ThemeHelper.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2019 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.app.Activity; +import android.content.Context; +import android.graphics.Color; +import android.preference.PreferenceManager; +import android.util.TypedValue; + +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; + +import com.m2049r.xmrwallet.R; + +public class ThemeHelper { + static public int getThemedResourceId(Context ctx, int attrId) { + final TypedValue typedValue = new TypedValue(); + if (ctx.getTheme().resolveAttribute(attrId, typedValue, true)) + return typedValue.resourceId; + else + return 0; + } + + @ColorInt + static public int getThemedColor(Context ctx, int attrId) { + final TypedValue typedValue = new TypedValue(); + if (ctx.getTheme().resolveAttribute(attrId, typedValue, true)) + return typedValue.data; + else + return Color.BLACK; + } + + public static void setTheme(@NonNull Activity activity, @NonNull String theme) { + switch (theme) { + case "Classic": + activity.setTheme(R.style.MyMaterialThemeClassic); + break; + case "Oled": + activity.setTheme(R.style.MyMaterialThemeOled); + break; + } + } + + public static void setPreferred(Activity activity) { + final String theme = PreferenceManager.getDefaultSharedPreferences(activity) + .getString(activity.getString(R.string.preferred_theme), "Classic"); + setTheme(activity, theme); + } + +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/ZipBackup.java b/app/src/main/java/com/m2049r/xmrwallet/util/ZipBackup.java new file mode 100644 index 0000000..cfdc705 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/ZipBackup.java @@ -0,0 +1,64 @@ +package com.m2049r.xmrwallet.util; + +import android.content.Context; +import android.net.Uri; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class ZipBackup { + final private Context context; + final private String walletName; + + private ZipOutputStream zip; + + public void writeTo(Uri zipUri) throws IOException { + if (zip != null) + throw new IllegalStateException("zip already initialized"); + try { + zip = new ZipOutputStream(context.getContentResolver().openOutputStream(zipUri)); + + final File walletRoot = Helper.getWalletRoot(context); + addFile(new File(walletRoot, walletName + ".keys")); + addFile(new File(walletRoot, walletName)); + + zip.close(); + } finally { + if (zip != null) zip.close(); + } + } + + private void addFile(File file) throws IOException { + if (!file.exists()) return; // ignore missing files (e.g. the cache file might not exist) + ZipEntry entry = new ZipEntry(file.getName()); + zip.putNextEntry(entry); + writeFile(file); + zip.closeEntry(); + } + + private void writeFile(File source) throws IOException { + try (InputStream is = new FileInputStream(source)) { + byte[] buffer = new byte[8192]; + int length; + while ((length = is.read(buffer)) > 0) { + zip.write(buffer, 0, length); + } + } + } + + private static final SimpleDateFormat DATETIME_FORMATTER = + new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss"); + + public String getBackupName() { + return walletName + " " + DATETIME_FORMATTER.format(new Date()); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/ZipRestore.java b/app/src/main/java/com/m2049r/xmrwallet/util/ZipRestore.java new file mode 100644 index 0000000..1b9148b --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/ZipRestore.java @@ -0,0 +1,139 @@ +package com.m2049r.xmrwallet.util; + +import android.content.Context; +import android.net.Uri; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import lombok.RequiredArgsConstructor; +import timber.log.Timber; + +@RequiredArgsConstructor +public class ZipRestore { + final private Context context; + final private Uri zipUri; + + private File walletRoot; + + private ZipInputStream zip; + + public boolean restore() throws IOException { + walletRoot = Helper.getWalletRoot(context); + String walletName = testArchive(); + if (walletName == null) return false; + + walletName = getUniqueName(walletName); + + if (zip != null) + throw new IllegalStateException("zip already initialized"); + try { + zip = new ZipInputStream(context.getContentResolver().openInputStream(zipUri)); + for (ZipEntry entry = zip.getNextEntry(); entry != null; zip.closeEntry(), entry = zip.getNextEntry()) { + File destination; + final String name = entry.getName(); + if (name.endsWith(".keys")) { + destination = new File(walletRoot, walletName + ".keys"); + } else if (name.endsWith(".address.txt")) { + destination = new File(walletRoot, walletName + ".address.txt"); + } else { + destination = new File(walletRoot, walletName); + } + writeFile(destination); + } + } finally { + if (zip != null) zip.close(); + } + return true; + } + + private void writeFile(File destination) throws IOException { + try (OutputStream os = new FileOutputStream(destination)) { + byte[] buffer = new byte[8192]; + int length; + while ((length = zip.read(buffer)) > 0) { + os.write(buffer, 0, length); + } + } + } + + // test the archive to contain files we expect & return the name of the contained wallet or null + private String testArchive() { + String walletName = null; + boolean keys = false; + ZipInputStream zipStream = null; + try { + zipStream = new ZipInputStream(context.getContentResolver().openInputStream(zipUri)); + for (ZipEntry entry = zipStream.getNextEntry(); entry != null; + zipStream.closeEntry(), entry = zipStream.getNextEntry()) { + if (entry.isDirectory()) + return null; + final String name = entry.getName(); + if ((new File(name)).getParentFile() != null) + return null; + if (walletName == null) { + if (name.endsWith(".keys")) { + walletName = name.substring(0, name.length() - ".keys".length()); + keys = true; // we have they keys + } else if (name.endsWith(".address.txt")) { + walletName = name.substring(0, name.length() - ".address.txt".length()); + } else { + walletName = name; + } + } else { // we have a wallet name + if (name.endsWith(".keys")) { + if (!name.equals(walletName + ".keys")) return null; + keys = true; // we have they keys + } else if (name.endsWith(".address.txt")) { + if (!name.equals(walletName + ".address.txt")) return null; + } else if (!name.equals(walletName)) return null; + } + } + } catch (IOException ex) { + return null; + } finally { + try { + if (zipStream != null) zipStream.close(); + } catch (IOException ex) { + Timber.w(ex); + } + } + // we need the keys at least + if (keys) return walletName; + else return null; + } + + final static Pattern WALLET_PATTERN = Pattern.compile("^(.+) \\(([0-9]+)\\).keys$"); + + private String getUniqueName(String name) { + if (!(new File(walletRoot, name + ".keys")).exists()) // does not exist => it's ok to use + return name; + + File[] wallets = walletRoot.listFiles( + (dir, filename) -> { + Matcher m = WALLET_PATTERN.matcher(filename); + if (m.find()) + return m.group(1).equals(name); + else return false; + }); + if (wallets.length == 0) return name + " (1)"; + int maxIndex = 0; + for (File wallet : wallets) { + try { + final Matcher m = WALLET_PATTERN.matcher(wallet.getName()); + m.find(); + final int index = Integer.parseInt(m.group(2)); + if (index > maxIndex) maxIndex = index; + } catch (NumberFormatException ex) { + // this cannot happen & we can ignore it if it does + } + } + return name + " (" + (maxIndex + 1) + ")"; + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/ledger/ECsecp256k1.java b/app/src/main/java/com/m2049r/xmrwallet/util/ledger/ECsecp256k1.java new file mode 100644 index 0000000..1aef005 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/ledger/ECsecp256k1.java @@ -0,0 +1,81 @@ +/* + * Based on + * https://stackoverflow.com/a/19943894 + * + * Curve parameters from + * https://en.bitcoin.it/wiki/Secp256k1 + * + * Copyright (c) 2019 m2049r + * Copyright (c) 2013 ChiaraHsieh + * + * 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.ledger; + +import java.math.BigInteger; +import java.security.spec.ECPoint; + +public class ECsecp256k1 { + static private final BigInteger TWO = new BigInteger("2"); + static private final BigInteger THREE = new BigInteger("3"); + static public final BigInteger p = new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F", 16); + static public final BigInteger a = new BigInteger("0000000000000000000000000000000000000000000000000000000000000000", 16); + static public final BigInteger b = new BigInteger("0000000000000000000000000000000000000000000000000000000000000007", 16); + static public final BigInteger n = new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", 16); + static public final ECPoint G = new ECPoint( + new BigInteger("79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", 16), + new BigInteger("483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8", 16)); + + public static ECPoint scalmult(BigInteger kin, ECPoint P) { + ECPoint R = ECPoint.POINT_INFINITY, S = P; + BigInteger k = kin.mod(n); // not necessary b/c that's how curves work + int length = k.bitLength(); + byte[] binarray = new byte[length]; + for (int i = 0; i <= length - 1; i++) { + binarray[i] = k.mod(TWO).byteValue(); + k = k.divide(TWO); + } + for (int i = length - 1; i >= 0; i--) { + // i should start at length-1 not -2 because the MSB of binary may not be 1 + R = doublePoint(R); + if (binarray[i] == 1) + R = addPoint(R, S); + } + return R; + } + + public static ECPoint addPoint(ECPoint r, ECPoint s) { + if (r.equals(s)) + return doublePoint(r); + else if (r.equals(ECPoint.POINT_INFINITY)) + return s; + else if (s.equals(ECPoint.POINT_INFINITY)) + return r; + BigInteger slope = (r.getAffineY().subtract(s.getAffineY())) + .multiply(r.getAffineX().subtract(s.getAffineX()).modInverse(p)); + BigInteger Xout = (slope.modPow(TWO, p).subtract(r.getAffineX())).subtract(s.getAffineX()).mod(p); + BigInteger Yout = s.getAffineY().negate().add(slope.multiply(s.getAffineX().subtract(Xout))).mod(p); + return new ECPoint(Xout, Yout); + } + + public static ECPoint doublePoint(ECPoint r) { + if (r.equals(ECPoint.POINT_INFINITY)) + return r; + BigInteger slope = (r.getAffineX().pow(2)).multiply(THREE).add(a) + .multiply((r.getAffineY().multiply(TWO)).modInverse(p)); + BigInteger Xout = slope.pow(2).subtract(r.getAffineX().multiply(TWO)).mod(p); + BigInteger Yout = (r.getAffineY().negate()).add(slope.multiply(r.getAffineX().subtract(Xout))).mod(p); + return new ECPoint(Xout, Yout); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/ledger/Monero.java b/app/src/main/java/com/m2049r/xmrwallet/util/ledger/Monero.java new file mode 100644 index 0000000..f4a3fdd --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/ledger/Monero.java @@ -0,0 +1,1866 @@ +/* + * A quick and hacky Java implementation of most of + * https://github.com/LedgerHQ/ledger-app-monero/blob/master/tools/python/src/ledger/monero/seedconv.py + * + * Copyright (c) 2019 m2049r + * Copyright 2018 Cedric Mesnil , Ledger SAS + * + * 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.ledger; + +import com.theromus.sha.Keccak; +import com.theromus.sha.Parameters; + +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.ECPoint; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.text.Normalizer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.zip.CRC32; + +import javax.crypto.Mac; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; + +import timber.log.Timber; + +public class Monero { + + static public String convert(String mnemonic, String passphrase) { + String[] words = mnemonic.toLowerCase().split("\\s"); + StringBuilder normalizedMnemonic = new StringBuilder(); + int wordCount = 0; + for (String word : words) { + if (word.length() == 0) continue; + if (wordCount > 0) normalizedMnemonic.append(" "); + wordCount++; + normalizedMnemonic.append(word); + } + if ((wordCount != 12) && (wordCount != 24) && (wordCount != 18)) return null; + Monero seed = new Monero(); + try { + return seed.getMnemonic(normalizedMnemonic.toString(), passphrase); + } catch (IllegalStateException | IllegalArgumentException ex) { + return null; + } + } + + private byte[] seed; + private byte[] mkey; + private byte[] mchain; + private byte[] monero_ki; + private byte[] monero_ci; + private byte[] view_key; + private byte[] spend_key; + + private static byte[] NKFDbytes(String str) { + return Normalizer.normalize(str, Normalizer.Form.NFKD).getBytes(); + } + + private static char[] NKFDchars(String str) { + return Normalizer.normalize(str, Normalizer.Form.NFKD).toCharArray(); + } + + private static byte[] fixByteArray32(byte[] b) { + if ((b.length > 33)) throw new IllegalStateException(); + if ((b.length == 33) && (b[0] != 0)) throw new IllegalStateException(); + if (b.length == 33) + return Arrays.copyOfRange(b, 1, 33); + else + return b; + } + + private static byte[] intToBytes(int i, int bytes) { + ByteBuffer buffer = ByteBuffer.allocate(bytes); + buffer.putInt(i); + return buffer.array(); + } + + private static void reverse(byte[] b) { + for (int i = 0; i < b.length / 2; i++) { + byte temp = b[i]; + b[i] = b[b.length - i - 1]; + b[b.length - i - 1] = temp; + } + } + + private void derive(String path) + throws NoSuchAlgorithmException, InvalidKeyException { + byte[] kpar = Arrays.copyOf(mkey, 32); + byte[] cpar = Arrays.copyOf(mchain, 32); + + String[] pathSegments = path.split("/"); + if (!pathSegments[0].equals("m")) + throw new IllegalArgumentException("Path must start with 'm'"); + for (int i = 1; i < pathSegments.length; i++) { + String child = pathSegments[i]; + boolean hardened = child.charAt(child.length() - 1) == '\''; + + byte[] data = new byte[33 + 4]; + if (hardened) { + int c = Integer.parseInt(child.substring(0, child.length() - 1)); + c += 0x80000000; + data[0] = 0; + System.arraycopy(kpar, 0, data, 1, kpar.length); + System.arraycopy(intToBytes(c, 4), 0, data, 1 + kpar.length, 4); + } else { + int c = Integer.parseInt(child); + BigInteger k = new BigInteger(1, kpar); + ECPoint kG = ECsecp256k1.scalmult(k, ECsecp256k1.G); + byte[] xBytes = fixByteArray32(kG.getAffineX().toByteArray()); + byte[] Wpar = new byte[33]; + System.arraycopy(xBytes, 0, Wpar, 33 - xBytes.length, xBytes.length); + byte[] yBytes = fixByteArray32(kG.getAffineY().toByteArray()); + if ((yBytes[yBytes.length - 1] & 1) == 0) + Wpar[0] = 0x02; + else + Wpar[0] = 0x03; + System.arraycopy(Wpar, 0, data, 0, Wpar.length); + System.arraycopy(intToBytes(c, 4), 0, data, Wpar.length, 4); + } + + SecretKeySpec keySpec = new SecretKeySpec(cpar, "HmacSHA512"); + Mac mac = Mac.getInstance("HmacSHA512"); + mac.init(keySpec); + byte[] I = mac.doFinal(data); + BigInteger Il = new BigInteger(1, Arrays.copyOfRange(I, 0, 32)); + BigInteger kparInt = new BigInteger(1, kpar); + Il = Il.add(kparInt).mod(ECsecp256k1.n); + byte[] IlBytes = fixByteArray32(Il.toByteArray()); + kpar = new byte[32]; + System.arraycopy(IlBytes, 0, kpar, 0, 32); + System.arraycopy(I, 32, cpar, 0, I.length - 32); + } + monero_ki = kpar; + monero_ci = cpar; + } + + private void makeSeed(String mnemonic, String passphrase) + throws NoSuchAlgorithmException, InvalidKeySpecException { + SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2withHmacSHA512"); + KeySpec spec = new PBEKeySpec(NKFDchars(mnemonic), NKFDbytes("mnemonic" + passphrase), 2048, 512); + seed = skf.generateSecret(spec).getEncoded(); + } + + private void makeMasterKey() + throws NoSuchAlgorithmException, InvalidKeyException { + SecretKeySpec keySpec = new SecretKeySpec( + NKFDbytes("Bitcoin seed"), + "HmacSHA512"); + Mac mac = Mac.getInstance("HmacSHA512"); + mac.init(keySpec); + byte[] result = mac.doFinal(seed); + mkey = Arrays.copyOfRange(result, 0, 32); + mchain = Arrays.copyOfRange(result, 32, 64); + } + + private void makeKeys() { + BigInteger l = new BigInteger("1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3ed", 16); + Keccak keccak = new Keccak(); + final byte[] b = keccak.getHash(monero_ki, Parameters.KECCAK_256); + reverse(b); + BigInteger ble = new BigInteger(1, b).mod(l); + spend_key = fixByteArray32(ble.toByteArray()); + reverse(spend_key); + byte[] a = keccak.getHash(spend_key, Parameters.KECCAK_256); + reverse(a); + BigInteger ale = new BigInteger(1, a).mod(l); + view_key = fixByteArray32(ale.toByteArray()); + reverse(view_key); + } + + private String getWords() { + if (spend_key.length != 32) throw new IllegalArgumentException(); + String[] wordList = ENGLISH_WORDS; + List words = new ArrayList<>(); + for (int i = 0; i < spend_key.length / 4; i++) { + long val = ((long) (spend_key[i * 4 + 0] & 0xff) << 0) | + ((long) (spend_key[i * 4 + 1] & 0xff) << 8) | + ((long) (spend_key[i * 4 + 2] & 0xff) << 16) | + ((long) (spend_key[i * 4 + 3] & 0xff) << 24); + long w1 = val % wordList.length; + long w2 = ((val / wordList.length) + w1) % wordList.length; + long w3 = (((val / wordList.length) / wordList.length) + w2) % wordList.length; + + words.add(wordList[(int) w1]); + words.add(wordList[(int) w2]); + words.add(wordList[(int) w3]); + } + + StringBuilder mnemonic = new StringBuilder(); + StringBuilder trimmedWords = new StringBuilder(); + + for (String word : words) { + mnemonic.append(word).append(" "); + trimmedWords.append(word.substring(0, ENGLISH_PREFIX_LENGTH)); + } + CRC32 crc32 = new CRC32(); + crc32.update(trimmedWords.toString().getBytes(StandardCharsets.UTF_8)); + long checksum = crc32.getValue(); + mnemonic.append(words.get((int) (checksum % 24))); + return mnemonic.toString(); + } + + private String getMnemonic(String ledgerMnemonic, String passphrase) { + try { + makeSeed(ledgerMnemonic, passphrase); + makeMasterKey(); + derive("m/44'/128'/0'/0/0"); + makeKeys(); + return getWords(); + } catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException ex) { + Timber.e(ex); + } + return null; + } + + public static final int ENGLISH_PREFIX_LENGTH = 3; + public static final String[] ENGLISH_WORDS = { + "abbey", + "abducts", + "ability", + "ablaze", + "abnormal", + "abort", + "abrasive", + "absorb", + "abyss", + "academy", + "aces", + "aching", + "acidic", + "acoustic", + "acquire", + "across", + "actress", + "acumen", + "adapt", + "addicted", + "adept", + "adhesive", + "adjust", + "adopt", + "adrenalin", + "adult", + "adventure", + "aerial", + "afar", + "affair", + "afield", + "afloat", + "afoot", + "afraid", + "after", + "against", + "agenda", + "aggravate", + "agile", + "aglow", + "agnostic", + "agony", + "agreed", + "ahead", + "aided", + "ailments", + "aimless", + "airport", + "aisle", + "ajar", + "akin", + "alarms", + "album", + "alchemy", + "alerts", + "algebra", + "alkaline", + "alley", + "almost", + "aloof", + "alpine", + "already", + "also", + "altitude", + "alumni", + "always", + "amaze", + "ambush", + "amended", + "amidst", + "ammo", + "amnesty", + "among", + "amply", + "amused", + "anchor", + "android", + "anecdote", + "angled", + "ankle", + "annoyed", + "answers", + "antics", + "anvil", + "anxiety", + "anybody", + "apart", + "apex", + "aphid", + "aplomb", + "apology", + "apply", + "apricot", + "aptitude", + "aquarium", + "arbitrary", + "archer", + "ardent", + "arena", + "argue", + "arises", + "army", + "around", + "arrow", + "arsenic", + "artistic", + "ascend", + "ashtray", + "aside", + "asked", + "asleep", + "aspire", + "assorted", + "asylum", + "athlete", + "atlas", + "atom", + "atrium", + "attire", + "auburn", + "auctions", + "audio", + "august", + "aunt", + "austere", + "autumn", + "avatar", + "avidly", + "avoid", + "awakened", + "awesome", + "awful", + "awkward", + "awning", + "awoken", + "axes", + "axis", + "axle", + "aztec", + "azure", + "baby", + "bacon", + "badge", + "baffles", + "bagpipe", + "bailed", + "bakery", + "balding", + "bamboo", + "banjo", + "baptism", + "basin", + "batch", + "bawled", + "bays", + "because", + "beer", + "befit", + "begun", + "behind", + "being", + "below", + "bemused", + "benches", + "berries", + "bested", + "betting", + "bevel", + "beware", + "beyond", + "bias", + "bicycle", + "bids", + "bifocals", + "biggest", + "bikini", + "bimonthly", + "binocular", + "biology", + "biplane", + "birth", + "biscuit", + "bite", + "biweekly", + "blender", + "blip", + "bluntly", + "boat", + "bobsled", + "bodies", + "bogeys", + "boil", + "boldly", + "bomb", + "border", + "boss", + "both", + "bounced", + "bovine", + "bowling", + "boxes", + "boyfriend", + "broken", + "brunt", + "bubble", + "buckets", + "budget", + "buffet", + "bugs", + "building", + "bulb", + "bumper", + "bunch", + "business", + "butter", + "buying", + "buzzer", + "bygones", + "byline", + "bypass", + "cabin", + "cactus", + "cadets", + "cafe", + "cage", + "cajun", + "cake", + "calamity", + "camp", + "candy", + "casket", + "catch", + "cause", + "cavernous", + "cease", + "cedar", + "ceiling", + "cell", + "cement", + "cent", + "certain", + "chlorine", + "chrome", + "cider", + "cigar", + "cinema", + "circle", + "cistern", + "citadel", + "civilian", + "claim", + "click", + "clue", + "coal", + "cobra", + "cocoa", + "code", + "coexist", + "coffee", + "cogs", + "cohesive", + "coils", + "colony", + "comb", + "cool", + "copy", + "corrode", + "costume", + "cottage", + "cousin", + "cowl", + "criminal", + "cube", + "cucumber", + "cuddled", + "cuffs", + "cuisine", + "cunning", + "cupcake", + "custom", + "cycling", + "cylinder", + "cynical", + "dabbing", + "dads", + "daft", + "dagger", + "daily", + "damp", + "dangerous", + "dapper", + "darted", + "dash", + "dating", + "dauntless", + "dawn", + "daytime", + "dazed", + "debut", + "decay", + "dedicated", + "deepest", + "deftly", + "degrees", + "dehydrate", + "deity", + "dejected", + "delayed", + "demonstrate", + "dented", + "deodorant", + "depth", + "desk", + "devoid", + "dewdrop", + "dexterity", + "dialect", + "dice", + "diet", + "different", + "digit", + "dilute", + "dime", + "dinner", + "diode", + "diplomat", + "directed", + "distance", + "ditch", + "divers", + "dizzy", + "doctor", + "dodge", + "does", + "dogs", + "doing", + "dolphin", + "domestic", + "donuts", + "doorway", + "dormant", + "dosage", + "dotted", + "double", + "dove", + "down", + "dozen", + "dreams", + "drinks", + "drowning", + "drunk", + "drying", + "dual", + "dubbed", + "duckling", + "dude", + "duets", + "duke", + "dullness", + "dummy", + "dunes", + "duplex", + "duration", + "dusted", + "duties", + "dwarf", + "dwelt", + "dwindling", + "dying", + "dynamite", + "dyslexic", + "each", + "eagle", + "earth", + "easy", + "eating", + "eavesdrop", + "eccentric", + "echo", + "eclipse", + "economics", + "ecstatic", + "eden", + "edgy", + "edited", + "educated", + "eels", + "efficient", + "eggs", + "egotistic", + "eight", + "either", + "eject", + "elapse", + "elbow", + "eldest", + "eleven", + "elite", + "elope", + "else", + "eluded", + "emails", + "ember", + "emerge", + "emit", + "emotion", + "empty", + "emulate", + "energy", + "enforce", + "enhanced", + "enigma", + "enjoy", + "enlist", + "enmity", + "enough", + "enraged", + "ensign", + "entrance", + "envy", + "epoxy", + "equip", + "erase", + "erected", + "erosion", + "error", + "eskimos", + "espionage", + "essential", + "estate", + "etched", + "eternal", + "ethics", + "etiquette", + "evaluate", + "evenings", + "evicted", + "evolved", + "examine", + "excess", + "exhale", + "exit", + "exotic", + "exquisite", + "extra", + "exult", + "fabrics", + "factual", + "fading", + "fainted", + "faked", + "fall", + "family", + "fancy", + "farming", + "fatal", + "faulty", + "fawns", + "faxed", + "fazed", + "feast", + "february", + "federal", + "feel", + "feline", + "females", + "fences", + "ferry", + "festival", + "fetches", + "fever", + "fewest", + "fiat", + "fibula", + "fictional", + "fidget", + "fierce", + "fifteen", + "fight", + "films", + "firm", + "fishing", + "fitting", + "five", + "fixate", + "fizzle", + "fleet", + "flippant", + "flying", + "foamy", + "focus", + "foes", + "foggy", + "foiled", + "folding", + "fonts", + "foolish", + "fossil", + "fountain", + "fowls", + "foxes", + "foyer", + "framed", + "friendly", + "frown", + "fruit", + "frying", + "fudge", + "fuel", + "fugitive", + "fully", + "fuming", + "fungal", + "furnished", + "fuselage", + "future", + "fuzzy", + "gables", + "gadget", + "gags", + "gained", + "galaxy", + "gambit", + "gang", + "gasp", + "gather", + "gauze", + "gave", + "gawk", + "gaze", + "gearbox", + "gecko", + "geek", + "gels", + "gemstone", + "general", + "geometry", + "germs", + "gesture", + "getting", + "geyser", + "ghetto", + "ghost", + "giant", + "giddy", + "gifts", + "gigantic", + "gills", + "gimmick", + "ginger", + "girth", + "giving", + "glass", + "gleeful", + "glide", + "gnaw", + "gnome", + "goat", + "goblet", + "godfather", + "goes", + "goggles", + "going", + "goldfish", + "gone", + "goodbye", + "gopher", + "gorilla", + "gossip", + "gotten", + "gourmet", + "governing", + "gown", + "greater", + "grunt", + "guarded", + "guest", + "guide", + "gulp", + "gumball", + "gur", + "gusts", + "gutter", + "guys", + "gymnast", + "gypsy", + "gyrate", + "habitat", + "hacksaw", + "haggled", + "hairy", + "hamburger", + "happens", + "hashing", + "hatchet", + "haunted", + "having", + "hawk", + "haystack", + "hazard", + "hectare", + "hedgehog", + "heels", + "hefty", + "height", + "hemlock", + "hence", + "heron", + "hesitate", + "hexagon", + "hickory", + "hiding", + "highway", + "hijack", + "hiker", + "hills", + "himself", + "hinder", + "hippo", + "hire", + "history", + "hitched", + "hive", + "hoax", + "hobby", + "hockey", + "hoisting", + "hold", + "honked", + "hookup", + "hope", + "hornet", + "hospital", + "hotel", + "hounded", + "hover", + "howls", + "hubcaps", + "huddle", + "huge", + "hull", + "humid", + "hunter", + "hurried", + "husband", + "huts", + "hybrid", + "hydrogen", + "hyper", + "iceberg", + "icing", + "icon", + "identity", + "idiom", + "idled", + "idols", + "igloo", + "ignore", + "iguana", + "illness", + "imagine", + "imbalance", + "imitate", + "impel", + "inactive", + "inbound", + "incur", + "industrial", + "inexact", + "inflamed", + "ingested", + "initiate", + "injury", + "inkling", + "inline", + "inmate", + "innocent", + "inorganic", + "input", + "inquest", + "inroads", + "insult", + "intended", + "inundate", + "invoke", + "inwardly", + "ionic", + "irate", + "iris", + "irony", + "irritate", + "island", + "isolated", + "issued", + "italics", + "itches", + "items", + "itinerary", + "itself", + "ivory", + "jabbed", + "jackets", + "jaded", + "jagged", + "jailed", + "jamming", + "january", + "jargon", + "jaunt", + "javelin", + "jaws", + "jazz", + "jeans", + "jeers", + "jellyfish", + "jeopardy", + "jerseys", + "jester", + "jetting", + "jewels", + "jigsaw", + "jingle", + "jittery", + "jive", + "jobs", + "jockey", + "jogger", + "joining", + "joking", + "jolted", + "jostle", + "journal", + "joyous", + "jubilee", + "judge", + "juggled", + "juicy", + "jukebox", + "july", + "jump", + "junk", + "jury", + "justice", + "juvenile", + "kangaroo", + "karate", + "keep", + "kennel", + "kept", + "kernels", + "kettle", + "keyboard", + "kickoff", + "kidneys", + "king", + "kiosk", + "kisses", + "kitchens", + "kiwi", + "knapsack", + "knee", + "knife", + "knowledge", + "knuckle", + "koala", + "laboratory", + "ladder", + "lagoon", + "lair", + "lakes", + "lamb", + "language", + "laptop", + "large", + "last", + "later", + "launching", + "lava", + "lawsuit", + "layout", + "lazy", + "lectures", + "ledge", + "leech", + "left", + "legion", + "leisure", + "lemon", + "lending", + "leopard", + "lesson", + "lettuce", + "lexicon", + "liar", + "library", + "licks", + "lids", + "lied", + "lifestyle", + "light", + "likewise", + "lilac", + "limits", + "linen", + "lion", + "lipstick", + "liquid", + "listen", + "lively", + "loaded", + "lobster", + "locker", + "lodge", + "lofty", + "logic", + "loincloth", + "long", + "looking", + "lopped", + "lordship", + "losing", + "lottery", + "loudly", + "love", + "lower", + "loyal", + "lucky", + "luggage", + "lukewarm", + "lullaby", + "lumber", + "lunar", + "lurk", + "lush", + "luxury", + "lymph", + "lynx", + "lyrics", + "macro", + "madness", + "magically", + "mailed", + "major", + "makeup", + "malady", + "mammal", + "maps", + "masterful", + "match", + "maul", + "maverick", + "maximum", + "mayor", + "maze", + "meant", + "mechanic", + "medicate", + "meeting", + "megabyte", + "melting", + "memoir", + "men", + "merger", + "mesh", + "metro", + "mews", + "mice", + "midst", + "mighty", + "mime", + "mirror", + "misery", + "mittens", + "mixture", + "moat", + "mobile", + "mocked", + "mohawk", + "moisture", + "molten", + "moment", + "money", + "moon", + "mops", + "morsel", + "mostly", + "motherly", + "mouth", + "movement", + "mowing", + "much", + "muddy", + "muffin", + "mugged", + "mullet", + "mumble", + "mundane", + "muppet", + "mural", + "musical", + "muzzle", + "myriad", + "mystery", + "myth", + "nabbing", + "nagged", + "nail", + "names", + "nanny", + "napkin", + "narrate", + "nasty", + "natural", + "nautical", + "navy", + "nearby", + "necklace", + "needed", + "negative", + "neither", + "neon", + "nephew", + "nerves", + "nestle", + "network", + "neutral", + "never", + "newt", + "nexus", + "nibs", + "niche", + "niece", + "nifty", + "nightly", + "nimbly", + "nineteen", + "nirvana", + "nitrogen", + "nobody", + "nocturnal", + "nodes", + "noises", + "nomad", + "noodles", + "northern", + "nostril", + "noted", + "nouns", + "novelty", + "nowhere", + "nozzle", + "nuance", + "nucleus", + "nudged", + "nugget", + "nuisance", + "null", + "number", + "nuns", + "nurse", + "nutshell", + "nylon", + "oaks", + "oars", + "oasis", + "oatmeal", + "obedient", + "object", + "obliged", + "obnoxious", + "observant", + "obtains", + "obvious", + "occur", + "ocean", + "october", + "odds", + "odometer", + "offend", + "often", + "oilfield", + "ointment", + "okay", + "older", + "olive", + "olympics", + "omega", + "omission", + "omnibus", + "onboard", + "oncoming", + "oneself", + "ongoing", + "onion", + "online", + "onslaught", + "onto", + "onward", + "oozed", + "opacity", + "opened", + "opposite", + "optical", + "opus", + "orange", + "orbit", + "orchid", + "orders", + "organs", + "origin", + "ornament", + "orphans", + "oscar", + "ostrich", + "otherwise", + "otter", + "ouch", + "ought", + "ounce", + "ourselves", + "oust", + "outbreak", + "oval", + "oven", + "owed", + "owls", + "owner", + "oxidant", + "oxygen", + "oyster", + "ozone", + "pact", + "paddles", + "pager", + "pairing", + "palace", + "pamphlet", + "pancakes", + "paper", + "paradise", + "pastry", + "patio", + "pause", + "pavements", + "pawnshop", + "payment", + "peaches", + "pebbles", + "peculiar", + "pedantic", + "peeled", + "pegs", + "pelican", + "pencil", + "people", + "pepper", + "perfect", + "pests", + "petals", + "phase", + "pheasants", + "phone", + "phrases", + "physics", + "piano", + "picked", + "pierce", + "pigment", + "piloted", + "pimple", + "pinched", + "pioneer", + "pipeline", + "pirate", + "pistons", + "pitched", + "pivot", + "pixels", + "pizza", + "playful", + "pledge", + "pliers", + "plotting", + "plus", + "plywood", + "poaching", + "pockets", + "podcast", + "poetry", + "point", + "poker", + "polar", + "ponies", + "pool", + "popular", + "portents", + "possible", + "potato", + "pouch", + "poverty", + "powder", + "pram", + "present", + "pride", + "problems", + "pruned", + "prying", + "psychic", + "public", + "puck", + "puddle", + "puffin", + "pulp", + "pumpkins", + "punch", + "puppy", + "purged", + "push", + "putty", + "puzzled", + "pylons", + "pyramid", + "python", + "queen", + "quick", + "quote", + "rabbits", + "racetrack", + "radar", + "rafts", + "rage", + "railway", + "raking", + "rally", + "ramped", + "randomly", + "rapid", + "rarest", + "rash", + "rated", + "ravine", + "rays", + "razor", + "react", + "rebel", + "recipe", + "reduce", + "reef", + "refer", + "regular", + "reheat", + "reinvest", + "rejoices", + "rekindle", + "relic", + "remedy", + "renting", + "reorder", + "repent", + "request", + "reruns", + "rest", + "return", + "reunion", + "revamp", + "rewind", + "rhino", + "rhythm", + "ribbon", + "richly", + "ridges", + "rift", + "rigid", + "rims", + "ringing", + "riots", + "ripped", + "rising", + "ritual", + "river", + "roared", + "robot", + "rockets", + "rodent", + "rogue", + "roles", + "romance", + "roomy", + "roped", + "roster", + "rotate", + "rounded", + "rover", + "rowboat", + "royal", + "ruby", + "rudely", + "ruffled", + "rugged", + "ruined", + "ruling", + "rumble", + "runway", + "rural", + "rustled", + "ruthless", + "sabotage", + "sack", + "sadness", + "safety", + "saga", + "sailor", + "sake", + "salads", + "sample", + "sanity", + "sapling", + "sarcasm", + "sash", + "satin", + "saucepan", + "saved", + "sawmill", + "saxophone", + "sayings", + "scamper", + "scenic", + "school", + "science", + "scoop", + "scrub", + "scuba", + "seasons", + "second", + "sedan", + "seeded", + "segments", + "seismic", + "selfish", + "semifinal", + "sensible", + "september", + "sequence", + "serving", + "session", + "setup", + "seventh", + "sewage", + "shackles", + "shelter", + "shipped", + "shocking", + "shrugged", + "shuffled", + "shyness", + "siblings", + "sickness", + "sidekick", + "sieve", + "sifting", + "sighting", + "silk", + "simplest", + "sincerely", + "sipped", + "siren", + "situated", + "sixteen", + "sizes", + "skater", + "skew", + "skirting", + "skulls", + "skydive", + "slackens", + "sleepless", + "slid", + "slower", + "slug", + "smash", + "smelting", + "smidgen", + "smog", + "smuggled", + "snake", + "sneeze", + "sniff", + "snout", + "snug", + "soapy", + "sober", + "soccer", + "soda", + "software", + "soggy", + "soil", + "solved", + "somewhere", + "sonic", + "soothe", + "soprano", + "sorry", + "southern", + "sovereign", + "sowed", + "soya", + "space", + "speedy", + "sphere", + "spiders", + "splendid", + "spout", + "sprig", + "spud", + "spying", + "square", + "stacking", + "stellar", + "stick", + "stockpile", + "strained", + "stunning", + "stylishly", + "subtly", + "succeed", + "suddenly", + "suede", + "suffice", + "sugar", + "suitcase", + "sulking", + "summon", + "sunken", + "superior", + "surfer", + "sushi", + "suture", + "swagger", + "swept", + "swiftly", + "sword", + "swung", + "syllabus", + "symptoms", + "syndrome", + "syringe", + "system", + "taboo", + "tacit", + "tadpoles", + "tagged", + "tail", + "taken", + "talent", + "tamper", + "tanks", + "tapestry", + "tarnished", + "tasked", + "tattoo", + "taunts", + "tavern", + "tawny", + "taxi", + "teardrop", + "technical", + "tedious", + "teeming", + "tell", + "template", + "tender", + "tepid", + "tequila", + "terminal", + "testing", + "tether", + "textbook", + "thaw", + "theatrics", + "thirsty", + "thorn", + "threaten", + "thumbs", + "thwart", + "ticket", + "tidy", + "tiers", + "tiger", + "tilt", + "timber", + "tinted", + "tipsy", + "tirade", + "tissue", + "titans", + "toaster", + "tobacco", + "today", + "toenail", + "toffee", + "together", + "toilet", + "token", + "tolerant", + "tomorrow", + "tonic", + "toolbox", + "topic", + "torch", + "tossed", + "total", + "touchy", + "towel", + "toxic", + "toyed", + "trash", + "trendy", + "tribal", + "trolling", + "truth", + "trying", + "tsunami", + "tubes", + "tucks", + "tudor", + "tuesday", + "tufts", + "tugs", + "tuition", + "tulips", + "tumbling", + "tunnel", + "turnip", + "tusks", + "tutor", + "tuxedo", + "twang", + "tweezers", + "twice", + "twofold", + "tycoon", + "typist", + "tyrant", + "ugly", + "ulcers", + "ultimate", + "umbrella", + "umpire", + "unafraid", + "unbending", + "uncle", + "under", + "uneven", + "unfit", + "ungainly", + "unhappy", + "union", + "unjustly", + "unknown", + "unlikely", + "unmask", + "unnoticed", + "unopened", + "unplugs", + "unquoted", + "unrest", + "unsafe", + "until", + "unusual", + "unveil", + "unwind", + "unzip", + "upbeat", + "upcoming", + "update", + "upgrade", + "uphill", + "upkeep", + "upload", + "upon", + "upper", + "upright", + "upstairs", + "uptight", + "upwards", + "urban", + "urchins", + "urgent", + "usage", + "useful", + "usher", + "using", + "usual", + "utensils", + "utility", + "utmost", + "utopia", + "uttered", + "vacation", + "vague", + "vain", + "value", + "vampire", + "vane", + "vapidly", + "vary", + "vastness", + "vats", + "vaults", + "vector", + "veered", + "vegan", + "vehicle", + "vein", + "velvet", + "venomous", + "verification", + "vessel", + "veteran", + "vexed", + "vials", + "vibrate", + "victim", + "video", + "viewpoint", + "vigilant", + "viking", + "village", + "vinegar", + "violin", + "vipers", + "virtual", + "visited", + "vitals", + "vivid", + "vixen", + "vocal", + "vogue", + "voice", + "volcano", + "vortex", + "voted", + "voucher", + "vowels", + "voyage", + "vulture", + "wade", + "waffle", + "wagtail", + "waist", + "waking", + "wallets", + "wanted", + "warped", + "washing", + "water", + "waveform", + "waxing", + "wayside", + "weavers", + "website", + "wedge", + "weekday", + "weird", + "welders", + "went", + "wept", + "were", + "western", + "wetsuit", + "whale", + "when", + "whipped", + "whole", + "wickets", + "width", + "wield", + "wife", + "wiggle", + "wildly", + "winter", + "wipeout", + "wiring", + "wise", + "withdrawn", + "wives", + "wizard", + "wobbly", + "woes", + "woken", + "wolf", + "womanly", + "wonders", + "woozy", + "worry", + "wounded", + "woven", + "wrap", + "wrist", + "wrong", + "yacht", + "yahoo", + "yanks", + "yard", + "yawning", + "yearbook", + "yellow", + "yesterday", + "yeti", + "yields", + "yodel", + "yoga", + "younger", + "yoyo", + "zapped", + "zeal", + "zebra", + "zero", + "zesty", + "zigzags", + "zinger", + "zippers", + "zodiac", + "zombie", + "zones", + "zoom" + }; +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/validator/BitcoinAddressType.java b/app/src/main/java/com/m2049r/xmrwallet/util/validator/BitcoinAddressType.java new file mode 100644 index 0000000..a1415b1 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/validator/BitcoinAddressType.java @@ -0,0 +1,51 @@ +package com.m2049r.xmrwallet.util.validator; + +import lombok.Getter; + +public enum BitcoinAddressType { + BTC(Type.BTC, Type.BTC_BECH32_PREFIX), + LTC(Type.LTC, Type.LTC_BECH32_PREFIX), + DASH(Type.DASH, null), + DOGE(Type.DOGE, null); + + @Getter + private final byte[] production; + @Getter + private final byte[] testnet; + + @Getter + private final String productionBech32Prefix; + @Getter + private final String testnetBech32Prefix; + + public boolean hasBech32() { + return productionBech32Prefix != null; + } + + public String getBech32Prefix(boolean testnet) { + return testnet ? testnetBech32Prefix : productionBech32Prefix; + } + + BitcoinAddressType(byte[][] types, String[] bech32Prefix) { + production = types[0]; + testnet = types[1]; + if (bech32Prefix != null) { + productionBech32Prefix = bech32Prefix[0]; + testnetBech32Prefix = bech32Prefix[1]; + } else { + productionBech32Prefix = null; + testnetBech32Prefix = null; + } + } + + // Java is silly and doesn't allow array initializers in the construction + private static class Type { + private static final byte[][] BTC = {{0x00, 0x05}, {0x6f, (byte) 0xc4}}; + private static final String[] BTC_BECH32_PREFIX = {"bc", "tb"}; + private static final byte[][] LTC = {{0x30, 0x05, 0x32}, {0x6f, (byte) 0xc4, 0x3a}}; + private static final String[] LTC_BECH32_PREFIX = {"ltc", "tltc"}; + private static final byte[][] DASH = {{0x4c, 0x10}, {(byte) 0x8c, 0x13}}; + private static final byte[][] DOGE = {{0x1e, 0x16}, {0x71, (byte) 0xc4}}; + } + +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/validator/BitcoinAddressValidator.java b/app/src/main/java/com/m2049r/xmrwallet/util/validator/BitcoinAddressValidator.java new file mode 100644 index 0000000..ce5cec0 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/validator/BitcoinAddressValidator.java @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2017 m2049r er al. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.util.validator; + +// mostly based on https://rosettacode.org/wiki/Bitcoin/address_validation#Java + +import com.m2049r.xmrwallet.data.Crypto; +import com.m2049r.xmrwallet.model.NetworkType; +import com.m2049r.xmrwallet.model.WalletManager; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +public class BitcoinAddressValidator { + private static final String ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + + public static Crypto validate(String address) { + for (BitcoinAddressType type : BitcoinAddressType.values()) { + if (validate(address, type)) + return Crypto.valueOf(type.name()); + } + return null; + } + + // just for tests + public static boolean validateBTC(String addrress, boolean testnet) { + return validate(addrress, BitcoinAddressType.BTC, testnet); + } + + public static boolean validate(String addrress, BitcoinAddressType type, boolean testnet) { + if (validate(addrress, testnet ? type.getTestnet() : type.getProduction())) + return true; + if (type.hasBech32()) + return validateBech32Segwit(addrress, type, testnet); + else + return false; + } + + public static boolean validate(String addrress, BitcoinAddressType type) { + final boolean testnet = WalletManager.getInstance().getNetworkType() != NetworkType.NetworkType_Mainnet; + return validate(addrress, type, testnet); + } + + public static boolean validate(String addrress, byte[] addressTypes) { + if (addrress.length() < 26 || addrress.length() > 35) + return false; + byte[] decoded = decodeBase58To25Bytes(addrress); + if (decoded == null) + return false; + int v = decoded[0] & 0xFF; + boolean nok = true; + for (byte b : addressTypes) { + nok = nok && (v != (b & 0xFF)); + } + if (nok) return false; + + byte[] hash1 = sha256(Arrays.copyOfRange(decoded, 0, 21)); + byte[] hash2 = sha256(hash1); + + return Arrays.equals(Arrays.copyOfRange(hash2, 0, 4), Arrays.copyOfRange(decoded, 21, 25)); + } + + private static byte[] decodeBase58To25Bytes(String input) { + BigInteger num = BigInteger.ZERO; + for (char t : input.toCharArray()) { + int p = ALPHABET.indexOf(t); + if (p == -1) + return null; + num = num.multiply(BigInteger.valueOf(58)).add(BigInteger.valueOf(p)); + } + + byte[] result = new byte[25]; + byte[] numBytes = num.toByteArray(); + if (num.bitLength() > 200) return null; + + if (num.bitLength() == 200) { + System.arraycopy(numBytes, 1, result, 0, 25); + } else { + System.arraycopy(numBytes, 0, result, result.length - numBytes.length, numBytes.length); + } + return result; + } + + private static byte[] sha256(byte[] data) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + md.update(data); + return md.digest(); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } + } + + // + // validate Bech32 segwit + // see https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki for spec + // + + private static final String DATA_CHARS = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; + + public static boolean validateBech32Segwit(String bech32, BitcoinAddressType type, boolean testnet) { + if (!bech32.equals(bech32.toLowerCase()) && !bech32.equals(bech32.toUpperCase())) { + return false; // mixing upper and lower case not allowed + } + bech32 = bech32.toLowerCase(); + + if (!bech32.startsWith(type.getBech32Prefix(testnet))) return false; + + final int hrpLength = type.getBech32Prefix(testnet).length(); + + if ((bech32.length() < (12 + hrpLength)) || (bech32.length() > (72 + hrpLength))) + return false; + int mod = (bech32.length() - hrpLength) % 8; + if ((mod == 6) || (mod == 1) || (mod == 3)) return false; + + int sep = -1; + final byte[] bytes = bech32.getBytes(StandardCharsets.US_ASCII); + for (int i = 0; i < bytes.length; i++) { + if ((bytes[i] < 33) || (bytes[i] > 126)) { + return false; + } + if (bytes[i] == 49) sep = i; // 49 := '1' in ASCII + } + + if (sep != hrpLength) return false; + if (sep > bytes.length - 7) { + return false; // min 6 bytes data + } + if (bytes.length < 8) { // hrp{min}=1 + sep=1 + data{min}=6 := 8 + return false; // too short + } + if (bytes.length > 90) { + return false; // too long + } + + final byte[] hrp = Arrays.copyOfRange(bytes, 0, sep); + + final byte[] data = Arrays.copyOfRange(bytes, sep + 1, bytes.length); + for (int i = 0; i < data.length; i++) { + int b = DATA_CHARS.indexOf(data[i]); + if (b < 0) return false; // invalid character + data[i] = (byte) b; + } + + if (!validateBech32Data(data)) return false; + + return verifyChecksum(hrp, data); + } + + private static int polymod(byte[] values) { + final int[] GEN = {0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3}; + int chk = 1; + for (byte v : values) { + byte b = (byte) (chk >> 25); + chk = ((chk & 0x1ffffff) << 5) ^ v; + for (int i = 0; i < 5; i++) { + chk ^= ((b >> i) & 1) == 1 ? GEN[i] : 0; + } + } + return chk; + } + + private static byte[] hrpExpand(byte[] hrp) { + final byte[] expanded = new byte[(2 * hrp.length) + 1]; + int i = 0; + for (byte b : hrp) { + expanded[i++] = (byte) (b >> 5); + } + expanded[i++] = 0; + for (byte b : hrp) { + expanded[i++] = (byte) (b & 0x1f); + } + return expanded; + } + + private static boolean verifyChecksum(byte[] hrp, byte[] data) { + final byte[] hrpExpanded = hrpExpand(hrp); + final byte[] values = new byte[hrpExpanded.length + data.length]; + System.arraycopy(hrpExpanded, 0, values, 0, hrpExpanded.length); + System.arraycopy(data, 0, values, hrpExpanded.length, data.length); + return (polymod(values) == 1); + } + + private static boolean validateBech32Data(final byte[] data) { + if ((data[0] < 0) || (data[0] > 16)) return false; // witness version + final int programLength = data.length - 1 - 6; // 1-byte version at beginning & 6-byte checksum at end + + // since we are coming from our own decoder, we don't need to verify data is 5-bit bytes + + final int convertedSize = programLength * 5 / 8; + final int remainderSize = programLength * 5 % 8; + + if ((convertedSize < 2) || (convertedSize > 40)) return false; + + if ((data[0] == 0) && (convertedSize != 20) && (convertedSize != 32)) return false; + + if (remainderSize >= 5) return false; + // ignore checksum at end and get last byte of program + if ((data[data.length - 1 - 6] & ((1 << remainderSize) - 1)) != 0) return false; + + return true; + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/validator/EthAddressValidator.java b/app/src/main/java/com/m2049r/xmrwallet/util/validator/EthAddressValidator.java new file mode 100644 index 0000000..3e4c476 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/validator/EthAddressValidator.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2017 m2049r er al. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.util.validator; + +// mostly based on https://github.com/ognus/wallet-address-validator/blob/master/src/ethereum_validator.js + +import com.theromus.sha.Keccak; +import com.theromus.sha.Parameters; + +import java.nio.charset.StandardCharsets; +import java.util.regex.Pattern; + +public class EthAddressValidator { + static private final Pattern ETH_ADDRESS = Pattern.compile("^0x[0-9a-fA-F]{40}$"); + static private final Pattern ETH_ALLLOWER = Pattern.compile("^0x[0-9a-f]{40}$"); + static private final Pattern ETH_ALLUPPER = Pattern.compile("^0x[0-9A-F]{40}$"); + + public static boolean validate(String address) { + // Check if it has the basic requirements of an address + if (!ETH_ADDRESS.matcher(address).matches()) + return false; + + // If it's all small caps or all all caps, return true + if (ETH_ALLLOWER.matcher(address).matches() || ETH_ALLUPPER.matcher(address).matches()) { + return true; + } + + // Otherwise check each case + return validateChecksum(address); + } + + private static boolean validateChecksum(String address) { + // Check each case + address = address.substring(2); // strip 0x + + Keccak keccak = new Keccak(); + final byte[] addressHash = keccak.getHash( + address.toLowerCase().getBytes(StandardCharsets.US_ASCII), + Parameters.KECCAK_256); + for (int i = 0; i < 40; i++) { + boolean upper = (addressHash[i / 2] & ((i % 2) == 0 ? 128 : 8)) != 0; + char c = address.charAt(i); + if (Character.isAlphabetic(c)) { + if (Character.isUpperCase(c) && !upper) return false; + if (Character.isLowerCase(c) && upper) return false; + } + } + return true; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/widget/CTextInputLayout.java b/app/src/main/java/com/m2049r/xmrwallet/widget/CTextInputLayout.java new file mode 100644 index 0000000..59b884d --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/widget/CTextInputLayout.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// based on from https://stackoverflow.com/a/45325876 (which did not work for me) + +package com.m2049r.xmrwallet.widget; + +import android.content.Context; +import com.google.android.material.textfield.TextInputLayout; +import android.util.AttributeSet; +import android.widget.EditText; + +public class CTextInputLayout extends TextInputLayout { + public CTextInputLayout(Context context) { + super(context); + } + + public CTextInputLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public CTextInputLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + public int getBaseline() { + EditText editText = getEditText(); + return editText.getBaseline() - (getMeasuredHeight() - editText.getMeasuredHeight()); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/widget/DotBar.java b/app/src/main/java/com/m2049r/xmrwallet/widget/DotBar.java new file mode 100644 index 0000000..0baf2d3 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/widget/DotBar.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2017 m2049r et al. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// based on https://github.com/marcokstephen/StepProgressBar + +package com.m2049r.xmrwallet.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.view.View; + +import com.m2049r.xmrwallet.R; + +import timber.log.Timber; + +public class DotBar extends View { + + final private int inactiveColor; + final private int activeColor; + + final private float dotSize; + private float dotSpacing; + + final private int numDots; + private int activeDot; + + final private Paint paint; + + public DotBar(Context context, AttributeSet attrs) { + super(context, attrs); + + TypedArray ta = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DotBar, 0, 0); + try { + inactiveColor = ta.getInt(R.styleable.DotBar_inactiveColor, 0); + activeColor = ta.getInt(R.styleable.DotBar_activeColor, 0); + dotSize = ta.getDimensionPixelSize(R.styleable.DotBar_dotSize, 8); + numDots = ta.getInt(R.styleable.DotBar_numberDots, 5); + activeDot = ta.getInt(R.styleable.DotBar_activeDot, 0); + } finally { + ta.recycle(); + } + + paint = new Paint(Paint.ANTI_ALIAS_FLAG); + paint.setStyle(Paint.Style.FILL); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int desiredWidth = (int) ((numDots * dotSize) + getPaddingLeft() + getPaddingRight()); + int desiredHeight = (int) (dotSize + getPaddingBottom() + getPaddingTop()); + + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + int width; + int height; + + //Measure Width + if (widthMode == MeasureSpec.EXACTLY) { + //Must be this size + width = widthSize; + } else if (widthMode == MeasureSpec.AT_MOST) { + //Can't be bigger than... + width = Math.min(desiredWidth, widthSize); + } else { + //Be whatever you want + width = desiredWidth; + } + + //Measure Height + if (heightMode == MeasureSpec.EXACTLY) { + //Must be this size + height = heightSize; + } else if (heightMode == MeasureSpec.AT_MOST) { + //Can't be bigger than... + height = Math.min(desiredHeight, heightSize); + } else { + //Be whatever you want + height = desiredHeight; + } + + dotSpacing = (int) (((1.0 * width - (getPaddingLeft() + getPaddingRight())) / numDots - dotSize) / (numDots - 1)); + + Timber.d("dotSpacing=%f", dotSpacing); + //MUST CALL THIS + setMeasuredDimension(width, height); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + // Centering the dots in the middle of the canvas + float singleDotSize = dotSpacing + dotSize; + float combinedDotSize = singleDotSize * numDots - dotSpacing; + int startingX = (int) ((canvas.getWidth() - combinedDotSize) / 2); + int startingY = (int) ((canvas.getHeight() - dotSize) / 2); + + for (int i = 0; i < numDots; i++) { + int x = (int) (startingX + i * singleDotSize); + if (i == activeDot) { + paint.setColor(activeColor); + } else { + paint.setColor(inactiveColor); + } + canvas.drawCircle(x + dotSize / 2, startingY + dotSize / 2, dotSize / 2, paint); + } + } + + public void next() { + if (activeDot < numDots - 2) { + activeDot++; + invalidate(); + } // else no next - stay stuck at end + } + + public void previous() { + if (activeDot >= 0) { + activeDot--; + invalidate(); + } // else no previous - stay stuck at beginning + } + + public void setActiveDot(int i) { + if ((i >= 0) && (i < numDots)) { + activeDot = i; + invalidate(); + } + } + + public int getActiveDot() { + return activeDot; + } + + public int getNumDots() { + return numDots; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/widget/DropDownEditText.java b/app/src/main/java/com/m2049r/xmrwallet/widget/DropDownEditText.java new file mode 100644 index 0000000..9a767f5 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/widget/DropDownEditText.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// https://stackoverflow.com/questions/2126717/android-autocompletetextview-show-suggestions-when-no-text-entered + +package com.m2049r.xmrwallet.widget; + +import android.content.Context; +import android.graphics.Rect; +import androidx.appcompat.widget.AppCompatAutoCompleteTextView; +import android.util.AttributeSet; + +public class DropDownEditText extends AppCompatAutoCompleteTextView { + + public DropDownEditText(Context context) { + super(context); + } + + public DropDownEditText(Context arg0, AttributeSet arg1) { + super(arg0, arg1); + } + + public DropDownEditText(Context arg0, AttributeSet arg1, int arg2) { + super(arg0, arg1, arg2); + } + + @Override + public boolean enoughToFilter() { + return true; + } + + @Override + protected void onFocusChanged(boolean focused, int direction, + Rect previouslyFocusedRect) { + super.onFocusChanged(focused, direction, previouslyFocusedRect); + if (focused && getAdapter() != null) { + performFiltering("", 0); + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeEditText.java b/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeEditText.java new file mode 100644 index 0000000..00b1b19 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeEditText.java @@ -0,0 +1,421 @@ +/* + * Copyright (c) 2017-2019 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. + */ + +// based on https://code.tutsplus.com/tutorials/creating-compound-views-on-android--cms-22889 + +package com.m2049r.xmrwallet.widget; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.Spinner; +import android.widget.TextView; + +import com.google.android.material.textfield.TextInputLayout; +import com.m2049r.xmrwallet.R; +import com.m2049r.xmrwallet.service.exchange.api.ExchangeApi; +import com.m2049r.xmrwallet.service.exchange.api.ExchangeCallback; +import com.m2049r.xmrwallet.service.exchange.api.ExchangeRate; +import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.ServiceHelper; +import com.m2049r.xmrwallet.util.ThemeHelper; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +import timber.log.Timber; + +public class ExchangeEditText extends LinearLayout { + + private double getEnteredAmount() { + String enteredAmount = etAmountA.getEditText().getText().toString(); + try { + return Double.parseDouble(enteredAmount); + } catch (NumberFormatException ex) { + Timber.i(ex.getLocalizedMessage()); + } + return 0; + } + + public boolean validate(double max, double min) { + Timber.d("inProgress=%b", isExchangeInProgress()); + if (isExchangeInProgress()) { + shakeExchangeField(); + return false; + } + boolean ok = true; + String nativeAmount = getNativeAmount(); + if (nativeAmount == null) { + ok = false; + } else { + try { + double amount = Double.parseDouble(nativeAmount); + if ((amount < min) || (amount > max)) { + ok = false; + } + } catch (NumberFormatException ex) { + // this cannot be + Timber.e(ex.getLocalizedMessage()); + ok = false; + } + } + if (!ok) { + shakeAmountField(); + } + return ok; + } + + void shakeAmountField() { + etAmountA.startAnimation(Helper.getShakeAnimation(getContext())); + } + + void shakeExchangeField() { + tvAmountB.startAnimation(Helper.getShakeAnimation(getContext())); + } + + public void setAmount(String nativeAmount) { + if (nativeAmount != null) { + etAmountA.getEditText().setText(nativeAmount); + tvAmountB.setText(null); + if (sCurrencyA.getSelectedItemPosition() != 0) + sCurrencyA.setSelection(0, true); // set native currency & trigger exchange + else + doExchange(); + } else { + tvAmountB.setText(null); + } + } + + public void setEditable(boolean editable) { + etAmountA.setEnabled(editable); + } + + public String getNativeAmount() { + if (isExchangeInProgress()) return null; + if (getCurrencyA() == 0) + return getCleanAmountString(etAmountA.getEditText().getText().toString()); + else + return getCleanAmountString(tvAmountB.getText().toString()); + } + + TextInputLayout etAmountA; + TextView tvAmountB; + Spinner sCurrencyA; + Spinner sCurrencyB; + ImageView evExchange; + ProgressBar pbExchange; + + public int getCurrencyA() { + return sCurrencyA.getSelectedItemPosition(); + } + + public int getCurrencyB() { + return sCurrencyB.getSelectedItemPosition(); + } + + public ExchangeEditText(Context context) { + super(context); + initializeViews(context); + } + + public ExchangeEditText(Context context, AttributeSet attrs) { + super(context, attrs); + initializeViews(context); + } + + public ExchangeEditText(Context context, + AttributeSet attrs, + int defStyle) { + super(context, attrs, defStyle); + initializeViews(context); + } + + /** + * Inflates the views in the layout. + * + * @param context the current context for the view. + */ + void initializeViews(Context context) { + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_exchange_edit, this); + } + + void setCurrencyAdapter(Spinner spinner) { + List currencies = new ArrayList<>(); + currencies.add(Helper.BASE_CRYPTO); + setCurrencyAdapter(spinner, currencies); + } + + protected void setCurrencyAdapter(Spinner spinner, List currencies) { + if (Helper.SHOW_EXCHANGERATES) + currencies.addAll(Arrays.asList(getResources().getStringArray(R.array.currency))); + ArrayAdapter spinnerAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_item, currencies); + spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinner.setAdapter(spinnerAdapter); + } + + void setInitialSpinnerSelections(Spinner baseSpinner, Spinner quoteSpinner) { + baseSpinner.setSelection(0, true); + quoteSpinner.setSelection(0, true); + } + + private boolean isInitialized = false; + + void postInitialize() { + setInitialSpinnerSelections(sCurrencyA, sCurrencyB); + isInitialized = true; + startExchange(); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + etAmountA = findViewById(R.id.etAmountA); + etAmountA.getEditText().addTextChangedListener(new TextWatcher() { + @Override + public void afterTextChanged(Editable s) { + doExchange(); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + }); + tvAmountB = findViewById(R.id.tvAmountB); + sCurrencyA = findViewById(R.id.sCurrencyA); + sCurrencyB = findViewById(R.id.sCurrencyB); + evExchange = findViewById(R.id.evExchange); + pbExchange = findViewById(R.id.pbExchange); + + setCurrencyAdapter(sCurrencyA); + setCurrencyAdapter(sCurrencyB); + + post(this::postInitialize); + + // make progress circle gray + pbExchange.getIndeterminateDrawable(). + setColorFilter(ThemeHelper.getThemedColor(getContext(), R.attr.colorPrimaryVariant), + android.graphics.PorterDuff.Mode.MULTIPLY); + + sCurrencyA.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parentView, View selectedItemView, int position, long id) { + if (!isInitialized) return; + if (position != 0) { // if not native, select native on other + sCurrencyB.setSelection(0, true); + } + doExchange(); + } + + @Override + public void onNothingSelected(AdapterView parentView) { + // nothing + } + }); + + sCurrencyB.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(final AdapterView parentView, View selectedItemView, int position, long id) { + if (!isInitialized) return; + if (position != 0) { // if not native, select native on other + sCurrencyA.setSelection(0, true); + } + doExchange(); + } + + @Override + public void onNothingSelected(AdapterView parentView) { + // nothing + } + }); + } + + private boolean exchangeRateCacheIsUsable() { + return (exchangeRateCache != null) && + ((exchangeRateCache.getBaseCurrency().equals(sCurrencyA.getSelectedItem()) && + exchangeRateCache.getQuoteCurrency().equals(sCurrencyB.getSelectedItem())) || + (exchangeRateCache.getBaseCurrency().equals(sCurrencyB.getSelectedItem()) && + exchangeRateCache.getQuoteCurrency().equals(sCurrencyA.getSelectedItem()))); + } + + private double exchangeRateFromCache() { + if (!exchangeRateCacheIsUsable()) return 0; + if (exchangeRateCache.getBaseCurrency().equals(sCurrencyA.getSelectedItem())) { + return exchangeRateCache.getRate(); + } else { + return 1.0d / exchangeRateCache.getRate(); + } + } + + public void doExchange() { + if (!isInitialized) return; + tvAmountB.setText(null); + if (getCurrencyA() == getCurrencyB()) { + exchange(1); + return; + } + // use cached exchange rate if we have it + if (!isExchangeInProgress()) { + double rate = exchangeRateFromCache(); + if (rate > 0) { + if (prepareExchange()) { + exchange(rate); + } + } else { + startExchange(); + } + } + } + + private final ExchangeApi exchangeApi = ServiceHelper.getExchangeApi(); + + // starts exchange through exchange api + void startExchange() { + String currencyA = (String) sCurrencyA.getSelectedItem(); + String currencyB = (String) sCurrencyB.getSelectedItem(); + if ((currencyA == null) || (currencyB == null)) return; // nothing to do + execExchange(currencyA, currencyB); + } + + void execExchange(String currencyA, String currencyB) { + showProgress(); + queryExchangeRate(currencyA, currencyB, + new ExchangeCallback() { + @Override + public void onSuccess(final ExchangeRate exchangeRate) { + if (isAttachedToWindow()) + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + exchange(exchangeRate); + } + }); + } + + @Override + public void onError(final Exception e) { + Timber.e(e.getLocalizedMessage()); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + exchangeFailed(); + } + }); + } + }); + } + + void queryExchangeRate(final String base, final String quote, ExchangeCallback callback) { + exchangeApi.queryExchangeRate(base, quote, callback); + } + + private void exchange(double rate) { + double amount = getEnteredAmount(); + if (rate > 0) { + tvAmountB.setText(Helper.getFormattedAmount(rate * amount, getCurrencyB() == 0)); + } else { + tvAmountB.setText("--"); + Timber.d("No rate!"); + } + } + + private static final String CLEAN_FORMAT = "%." + Helper.XMR_DECIMALS + "f"; + + private String getCleanAmountString(String enteredAmount) { + try { + double amount = Double.parseDouble(enteredAmount); + if (amount >= 0) { + return String.format(Locale.US, CLEAN_FORMAT, amount); + } else { + return null; + } + } catch (NumberFormatException ex) { + return null; + } + } + + boolean prepareExchange() { + Timber.d("prepareExchange()"); + String enteredAmount = etAmountA.getEditText().getText().toString(); + if (!enteredAmount.isEmpty()) { + String cleanAmount = getCleanAmountString(enteredAmount); + Timber.d("cleanAmount = %s", cleanAmount); + if (cleanAmount == null) { + shakeAmountField(); + return false; + } + } else { + return false; + } + return true; + } + + public void exchangeFailed() { + hideProgress(); + exchange(0); + } + + // cache for exchange rate + ExchangeRate exchangeRateCache = null; + + public void exchange(ExchangeRate exchangeRate) { + hideProgress(); + // make sure this is what we want + if (!exchangeRate.getBaseCurrency().equals(sCurrencyA.getSelectedItem()) || + !exchangeRate.getQuoteCurrency().equals(sCurrencyB.getSelectedItem())) { + // something's wrong + Timber.i("Currencies don't match! A: %s==%s B: %s==%s", + exchangeRate.getBaseCurrency(), sCurrencyA.getSelectedItem(), + exchangeRate.getQuoteCurrency(), sCurrencyB.getSelectedItem()); + return; + } + + exchangeRateCache = exchangeRate; + if (prepareExchange()) { + exchange(exchangeRate.getRate()); + } + } + + void showProgress() { + pbExchange.setVisibility(View.VISIBLE); + } + + private boolean isExchangeInProgress() { + return pbExchange.getVisibility() == View.VISIBLE; + } + + private void hideProgress() { + pbExchange.setVisibility(View.INVISIBLE); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeOtherEditText.java b/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeOtherEditText.java new file mode 100644 index 0000000..3dd344e --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeOtherEditText.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2017-2019 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. + */ + +// based on https://code.tutsplus.com/tutorials/creating-compound-views-on-android--cms-22889 + +package com.m2049r.xmrwallet.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Handler; +import android.os.Looper; +import android.util.AttributeSet; +import android.widget.Spinner; + +import androidx.annotation.NonNull; + +import com.m2049r.xmrwallet.R; +import com.m2049r.xmrwallet.service.exchange.api.ExchangeCallback; +import com.m2049r.xmrwallet.service.exchange.api.ExchangeRate; +import com.m2049r.xmrwallet.util.Helper; + +import java.util.ArrayList; +import java.util.List; + +import timber.log.Timber; + +public class ExchangeOtherEditText extends ExchangeEditText { + /* + all exchanges are done through XMR + baseCurrency is the native currency + */ + + String baseCurrency = null; // not XMR + private double exchangeRate = 0; // baseCurrency to XMR + + public void setExchangeRate(double rate) { + exchangeRate = rate; + post(this::startExchange); + } + + public void setBaseCurrency(@NonNull String symbol) { + if (symbol.equals(baseCurrency)) return; + baseCurrency = symbol; + setCurrencyAdapter(sCurrencyA); + setCurrencyAdapter(sCurrencyB); + post(this::postInitialize); + } + + private void setBaseCurrency(Context context, AttributeSet attrs) { + TypedArray ta = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ExchangeEditText, 0, 0); + try { + baseCurrency = ta.getString(R.styleable.ExchangeEditText_baseSymbol); + if (baseCurrency == null) + throw new IllegalArgumentException("base currency must be set"); + } finally { + ta.recycle(); + } + } + + public ExchangeOtherEditText(Context context, AttributeSet attrs) { + super(context, attrs); + setBaseCurrency(context, attrs); + } + + public ExchangeOtherEditText(Context context, + AttributeSet attrs, + int defStyle) { + super(context, attrs, defStyle); + setBaseCurrency(context, attrs); + } + + @Override + void setCurrencyAdapter(Spinner spinner) { + List currencies = new ArrayList<>(); + if (!baseCurrency.equals(Helper.BASE_CRYPTO)) currencies.add(baseCurrency); + currencies.add(Helper.BASE_CRYPTO); + setCurrencyAdapter(spinner, currencies); + } + + @Override + void setInitialSpinnerSelections(Spinner baseSpinner, Spinner quoteSpinner) { + baseSpinner.setSelection(0, true); + quoteSpinner.setSelection(1, true); + } + + private void localExchange(final String base, final String quote, final double rate) { + exchange(new ExchangeRate() { + @Override + public String getServiceName() { + return "Local"; + } + + @Override + public String getBaseCurrency() { + return base; + } + + @Override + public String getQuoteCurrency() { + return quote; + } + + @Override + public double getRate() { + return rate; + } + }); + } + + @Override + void execExchange(String currencyA, String currencyB) { + if (!currencyA.equals(baseCurrency) && !currencyB.equals(baseCurrency)) { + throw new IllegalStateException("I can only exchange " + baseCurrency); + } + + showProgress(); + + Timber.d("execExchange(%s, %s)", currencyA, currencyB); + + // first deal with XMR/baseCurrency & baseCurrency/XMR + + if (currencyA.equals(Helper.BASE_CRYPTO) && (currencyB.equals(baseCurrency))) { + localExchange(currencyA, currencyB, 1.0d / exchangeRate); + return; + } + if (currencyA.equals(baseCurrency) && (currencyB.equals(Helper.BASE_CRYPTO))) { + localExchange(currencyA, currencyB, exchangeRate); + return; + } + + // next, deal with XMR/baseCurrency + + if (currencyA.equals(baseCurrency)) { + queryExchangeRate(Helper.BASE_CRYPTO, currencyB, exchangeRate, true); + } else { + queryExchangeRate(currencyA, Helper.BASE_CRYPTO, 1.0d / exchangeRate, false); + } + } + + private void queryExchangeRate(final String base, final String quote, final double factor, + final boolean baseIsBaseCrypto) { + queryExchangeRate(base, quote, + new ExchangeCallback() { + @Override + public void onSuccess(final ExchangeRate exchangeRate) { + if (isAttachedToWindow()) + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + ExchangeRate xchange = new ExchangeRate() { + @Override + public String getServiceName() { + return exchangeRate.getServiceName() + "+" + baseCurrency; + } + + @Override + public String getBaseCurrency() { + return baseIsBaseCrypto ? baseCurrency : base; + } + + @Override + public String getQuoteCurrency() { + return baseIsBaseCrypto ? quote : baseCurrency; + } + + @Override + public double getRate() { + return exchangeRate.getRate() * factor; + } + }; + exchange(xchange); + } + }); + } + + @Override + public void onError(final Exception e) { + Timber.e(e.getLocalizedMessage()); + new Handler(Looper.getMainLooper()).post(() -> exchangeFailed()); + } + }); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeView.java b/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeView.java new file mode 100644 index 0000000..3208f72 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeView.java @@ -0,0 +1,469 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// based on https://code.tutsplus.com/tutorials/creating-compound-views-on-android--cms-22889 + +package com.m2049r.xmrwallet.widget; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.Spinner; +import android.widget.TextView; + +import com.google.android.material.textfield.TextInputLayout; +import com.m2049r.xmrwallet.R; +import com.m2049r.xmrwallet.model.Wallet; +import com.m2049r.xmrwallet.service.exchange.api.ExchangeApi; +import com.m2049r.xmrwallet.service.exchange.api.ExchangeCallback; +import com.m2049r.xmrwallet.service.exchange.api.ExchangeRate; +import com.m2049r.xmrwallet.util.ThemeHelper; +import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.ServiceHelper; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +import timber.log.Timber; + +public class ExchangeView extends LinearLayout { + String xmrAmount = null; + String notXmrAmount = null; + + public void enable(boolean enable) { + etAmount.setEnabled(enable); + sCurrencyA.setEnabled(enable); + sCurrencyB.setEnabled(enable); + } + + void setXmr(String xmr) { + xmrAmount = xmr; + if (onNewAmountListener != null) { + onNewAmountListener.onNewAmount(xmr); + } + } + + public void setAmount(String xmrAmount) { + if (xmrAmount != null) { + setCurrencyA(0); + etAmount.getEditText().setText(xmrAmount); + setXmr(xmrAmount); + this.notXmrAmount = null; + doExchange(); + } else { + setXmr(null); + this.notXmrAmount = null; + tvAmountB.setText("--"); + } + } + + public String getAmount() { + return xmrAmount; + } + + public void setError(String msg) { + etAmount.setError(msg); + } + + TextInputLayout etAmount; + TextView tvAmountB; + Spinner sCurrencyA; + Spinner sCurrencyB; + ImageView evExchange; + ProgressBar pbExchange; + + + public void setCurrencyA(int currency) { + if ((currency != 0) && (getCurrencyB() != 0)) { + setCurrencyB(0); + } + sCurrencyA.setSelection(currency, true); + doExchange(); + } + + public void setCurrencyB(int currency) { + if ((currency != 0) && (getCurrencyA() != 0)) { + setCurrencyA(0); + } + sCurrencyB.setSelection(currency, true); + doExchange(); + } + + public int getCurrencyA() { + return sCurrencyA.getSelectedItemPosition(); + } + + public int getCurrencyB() { + return sCurrencyB.getSelectedItemPosition(); + } + + public ExchangeView(Context context) { + super(context); + initializeViews(context); + } + + public ExchangeView(Context context, AttributeSet attrs) { + super(context, attrs); + initializeViews(context); + } + + public ExchangeView(Context context, + AttributeSet attrs, + int defStyle) { + super(context, attrs, defStyle); + initializeViews(context); + } + + /** + * Inflates the views in the layout. + * + * @param context the current context for the view. + */ + private void initializeViews(Context context) { + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_exchange, this); + } + + void setCurrencyAdapter(Spinner spinner) { + List currencies = new ArrayList<>(); + currencies.add(Helper.BASE_CRYPTO); + if (Helper.SHOW_EXCHANGERATES) + currencies.addAll(Arrays.asList(getResources().getStringArray(R.array.currency))); + ArrayAdapter spinnerAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_item, currencies); + spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinner.setAdapter(spinnerAdapter); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + etAmount = findViewById(R.id.etAmount); + tvAmountB = findViewById(R.id.tvAmountB); + sCurrencyA = findViewById(R.id.sCurrencyA); + sCurrencyB = findViewById(R.id.sCurrencyB); + evExchange = findViewById(R.id.evExchange); + pbExchange = findViewById(R.id.pbExchange); + + setCurrencyAdapter(sCurrencyA); + setCurrencyAdapter(sCurrencyB); + + // make progress circle gray + pbExchange.getIndeterminateDrawable(). + setColorFilter(ThemeHelper.getThemedColor(getContext(), R.attr.colorPrimaryVariant), + android.graphics.PorterDuff.Mode.MULTIPLY); + + sCurrencyA.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parentView, View selectedItemView, int position, long id) { + if (position != 0) { // if not XMR, select XMR on other + sCurrencyB.setSelection(0, true); + } + doExchange(); + } + + @Override + public void onNothingSelected(AdapterView parentView) { + // nothing (yet?) + } + }); + + sCurrencyB.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(final AdapterView parentView, View selectedItemView, int position, long id) { + if (position != 0) { // if not XMR, select XMR on other + sCurrencyA.setSelection(0, true); + } + doExchange(); + } + + @Override + public void onNothingSelected(AdapterView parentView) { + // nothing + } + }); + + etAmount.getEditText().setOnFocusChangeListener(new OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (!hasFocus) { + doExchange(); + } + } + }); + + etAmount.getEditText().setOnEditorActionListener(new TextView.OnEditorActionListener() { + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN)) + || (actionId == EditorInfo.IME_ACTION_DONE)) { + doExchange(); + return true; + } + return false; + } + }); + + + etAmount.getEditText().addTextChangedListener(new TextWatcher() { + @Override + public void afterTextChanged(Editable editable) { + etAmount.setError(null); + clearAmounts(); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + }); + + } + + final static double MAX_AMOUNT_XMR = 1000; + final static double MAX_AMOUNT_NOTXMR = 100000; + + public boolean checkEnteredAmount() { + boolean ok = true; + Timber.d("checkEnteredAmount"); + String amountEntry = etAmount.getEditText().getText().toString(); + if (!amountEntry.isEmpty()) { + try { + double a = Double.parseDouble(amountEntry); + double maxAmount = (getCurrencyA() == 0) ? MAX_AMOUNT_XMR : MAX_AMOUNT_NOTXMR; + if (a > (maxAmount)) { + etAmount.setError(getResources(). + getString(R.string.receive_amount_too_big, + String.format(Locale.US, "%,.0f", maxAmount))); + ok = false; + } else if (a < 0) { + etAmount.setError(getResources().getString(R.string.receive_amount_negative)); + ok = false; + } + } catch (NumberFormatException ex) { + etAmount.setError(getResources().getString(R.string.receive_amount_nan)); + ok = false; + } + } + if (ok) { + etAmount.setError(null); + } + return ok; + } + + public void doExchange() { + tvAmountB.setText("--"); + // use cached exchange rate if we have it + if (!isExchangeInProgress()) { + String enteredCurrencyA = (String) sCurrencyA.getSelectedItem(); + String enteredCurrencyB = (String) sCurrencyB.getSelectedItem(); + if ((enteredCurrencyA + enteredCurrencyB).equals(assetPair)) { + if (prepareExchange()) { + exchange(assetRate); + } else { + clearAmounts(); + } + } else { + clearAmounts(); + startExchange(); + } + } else { + clearAmounts(); + } + } + + private void clearAmounts() { + if ((xmrAmount != null) || (notXmrAmount != null)) { + tvAmountB.setText("--"); + setXmr(null); + notXmrAmount = null; + } + } + + private final ExchangeApi exchangeApi = ServiceHelper.getExchangeApi(); + + void startExchange() { + showProgress(); + String currencyA = (String) sCurrencyA.getSelectedItem(); + String currencyB = (String) sCurrencyB.getSelectedItem(); + + exchangeApi.queryExchangeRate(currencyA, currencyB, + new ExchangeCallback() { + @Override + public void onSuccess(final ExchangeRate exchangeRate) { + if (isAttachedToWindow()) + new Handler(Looper.getMainLooper()).post(() -> exchange(exchangeRate)); + } + + @Override + public void onError(final Exception e) { + Timber.e(e.getLocalizedMessage()); + new Handler(Looper.getMainLooper()).post(() -> exchangeFailed()); + } + }); + } + + public void exchange(double rate) { + if (getCurrencyA() == 0) { + if (xmrAmount == null) return; + if (!xmrAmount.isEmpty() && (rate > 0)) { + double amountB = rate * Double.parseDouble(xmrAmount); + notXmrAmount = Helper.getFormattedAmount(amountB, getCurrencyB() == 0); + } else { + notXmrAmount = ""; + } + tvAmountB.setText(notXmrAmount); + } else if (getCurrencyB() == 0) { + if (notXmrAmount == null) return; + if (!notXmrAmount.isEmpty() && (rate > 0)) { + double amountB = rate * Double.parseDouble(notXmrAmount); + setXmr(Helper.getFormattedAmount(amountB, true)); + } else { + setXmr(""); + } + tvAmountB.setText(xmrAmount); + } else { // no XMR currency - cannot happen! + throw new IllegalStateException("No XMR currency!"); + } + if (rate == 0) + tvAmountB.setText("--"); + } + + boolean prepareExchange() { + Timber.d("prepareExchange()"); + if (checkEnteredAmount()) { + String enteredAmount = etAmount.getEditText().getText().toString(); + if (!enteredAmount.isEmpty()) { + String cleanAmount = ""; + if (getCurrencyA() == 0) { + // sanitize the input + cleanAmount = Helper.getDisplayAmount(Wallet.getAmountFromString(enteredAmount)); + setXmr(cleanAmount); + notXmrAmount = null; + Timber.d("cleanAmount = %s", cleanAmount); + } else if (getCurrencyB() == 0) { // we use B & 0 here for the else below ... + // sanitize the input + double amountA = Double.parseDouble(enteredAmount); + cleanAmount = String.format(Locale.US, "%.2f", amountA); + setXmr(null); + notXmrAmount = cleanAmount; + } else { // no XMR currency - cannot happen! + Timber.e("No XMR currency!"); + setXmr(null); + notXmrAmount = null; + return false; + } + Timber.d("prepareExchange() %s", cleanAmount); + } else { + setXmr(""); + notXmrAmount = ""; + } + return true; + } else { + setXmr(null); + notXmrAmount = null; + return false; + } + } + + public void exchangeFailed() { + hideProgress(); + exchange(0); + if (onFailedExchangeListener != null) { + onFailedExchangeListener.onFailedExchange(); + } + } + + String assetPair = null; + double assetRate = 0; + + public void exchange(ExchangeRate exchangeRate) { + hideProgress(); + // first, make sure this is what we want + String enteredCurrencyA = (String) sCurrencyA.getSelectedItem(); + String enteredCurrencyB = (String) sCurrencyB.getSelectedItem(); + if (!exchangeRate.getBaseCurrency().equals(enteredCurrencyA) + || !exchangeRate.getQuoteCurrency().equals(enteredCurrencyB)) { + // something's wrong + Timber.e("Currencies don't match!"); + return; + } + assetPair = enteredCurrencyA + enteredCurrencyB; + assetRate = exchangeRate.getRate(); + if (prepareExchange()) { + exchange(exchangeRate.getRate()); + } + } + + private void showProgress() { + pbExchange.setVisibility(View.VISIBLE); + } + + private boolean isExchangeInProgress() { + return pbExchange.getVisibility() == View.VISIBLE; + } + + private void hideProgress() { + pbExchange.setVisibility(View.INVISIBLE); + } + + // Hooks + public interface OnNewAmountListener { + void onNewAmount(String xmr); + } + + OnNewAmountListener onNewAmountListener; + + public void setOnNewAmountListener(OnNewAmountListener listener) { + onNewAmountListener = listener; + } + + public interface OnAmountInvalidatedListener { + void onAmountInvalidated(); + } + + OnAmountInvalidatedListener onAmountInvalidatedListener; + + public void setOnAmountInvalidatedListener(OnAmountInvalidatedListener listener) { + onAmountInvalidatedListener = listener; + } + + public interface OnFailedExchangeListener { + void onFailedExchange(); + } + + OnFailedExchangeListener onFailedExchangeListener; + + public void setOnFailedExchangeListener(OnFailedExchangeListener listener) { + onFailedExchangeListener = listener; + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/widget/PasswordEntryView.java b/app/src/main/java/com/m2049r/xmrwallet/widget/PasswordEntryView.java new file mode 100644 index 0000000..6d5fee1 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/widget/PasswordEntryView.java @@ -0,0 +1,73 @@ +package com.m2049r.xmrwallet.widget; + +import android.content.Context; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.android.material.textfield.TextInputLayout; +import com.m2049r.xmrwallet.R; +import com.nulabinc.zxcvbn.Zxcvbn; + +public class PasswordEntryView extends TextInputLayout implements TextWatcher { + final private Zxcvbn zxcvbn = new Zxcvbn(); + + public PasswordEntryView(@NonNull Context context) { + super(context, null); + } + + public PasswordEntryView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs, R.attr.textInputStyle); + } + + public PasswordEntryView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + public void addView(@NonNull View child, int index, @NonNull final ViewGroup.LayoutParams params) { + super.addView(child, index, params); + final EditText et = getEditText(); + if (et != null) + et.addTextChangedListener(this); + } + + @Override + public void afterTextChanged(Editable s) { + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + String password = s.toString(); + int icon = 0; + if (!password.isEmpty()) { + final double strength = Math.min(zxcvbn.measure(password).getGuessesLog10(), 15) / 3 * 20; // 0-100% + if (strength < 21) + icon = R.drawable.ic_smiley_sad_filled; + else if (strength < 40) + icon = R.drawable.ic_smiley_meh_filled; + else if (strength < 60) + icon = R.drawable.ic_smiley_neutral_filled; + else if (strength < 80) + icon = R.drawable.ic_smiley_happy_filled; + else if (strength < 99) + icon = R.drawable.ic_smiley_ecstatic_filled; + else + icon = R.drawable.ic_smiley_gunther_filled; + } + setErrorIconDrawable(icon); + if (icon != 0) + setError(" "); + else setError(null); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/widget/SendProgressView.java b/app/src/main/java/com/m2049r/xmrwallet/widget/SendProgressView.java new file mode 100644 index 0000000..11d5355 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/widget/SendProgressView.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.m2049r.xmrwallet.R; + +public class SendProgressView extends LinearLayout { + + public SendProgressView(Context context, AttributeSet attrs) { + super(context, attrs); + initializeViews(context); + } + + public SendProgressView(Context context, + AttributeSet attrs, + int defStyle) { + super(context, attrs, defStyle); + initializeViews(context); + } + + private void initializeViews(Context context) { + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_send_progress, this); + } + + + View pbProgress; + View llMessage; + TextView tvCode; + TextView tvMessage; + TextView tvSolution; + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + pbProgress = findViewById(R.id.pbProgress); + llMessage = findViewById(R.id.llMessage); + tvCode = findViewById(R.id.tvCode); + tvMessage = findViewById(R.id.tvMessage); + tvSolution = findViewById(R.id.tvSolution); + } + + public void showProgress(String progressText) { + pbProgress.setVisibility(VISIBLE); + tvCode.setVisibility(INVISIBLE); + tvMessage.setText(progressText); + llMessage.setVisibility(VISIBLE); + tvSolution.setVisibility(INVISIBLE); + } + + public void hideProgress() { + pbProgress.setVisibility(INVISIBLE); + llMessage.setVisibility(INVISIBLE); + } + + public void showMessage(String code, String message, String solution) { + tvCode.setText(code); + tvMessage.setText(message); + tvSolution.setText(solution); + tvCode.setVisibility(VISIBLE); + llMessage.setVisibility(VISIBLE); + tvSolution.setVisibility(VISIBLE); + pbProgress.setVisibility(INVISIBLE); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/widget/Toolbar.java b/app/src/main/java/com/m2049r/xmrwallet/widget/Toolbar.java new file mode 100644 index 0000000..768f8a8 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/widget/Toolbar.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// based on https://code.tutsplus.com/tutorials/creating-compound-views-on-android--cms-22889 + +package com.m2049r.xmrwallet.widget; + +import android.content.Context; +import android.os.Build; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; + +import com.google.android.material.appbar.MaterialToolbar; +import com.m2049r.xmrwallet.R; + +import timber.log.Timber; + +public class Toolbar extends MaterialToolbar { + public interface OnButtonListener { + void onButton(int type); + } + + OnButtonListener onButtonListener; + + public void setOnButtonListener(OnButtonListener listener) { + onButtonListener = listener; + } + + ImageView toolbarImage; + TextView toolbarTitle; + TextView toolbarSubtitle; + ImageButton bSettings; + + public Toolbar(Context context) { + super(context); + initializeViews(context); + } + + public Toolbar(Context context, AttributeSet attrs) { + super(context, attrs); + initializeViews(context); + } + + public Toolbar(Context context, + AttributeSet attrs, + int defStyle) { + super(context, attrs, defStyle); + initializeViews(context); + } + + /** + * Inflates the views in the layout. + * + * @param context the current context for the view. + */ + private void initializeViews(Context context) { + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_toolbar, this); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + toolbarImage = findViewById(R.id.toolbarImage); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + // the vector image does not work well for androis < Nougat + toolbarImage.getLayoutParams().width = (int) getResources().getDimension(R.dimen.logo_width); + toolbarImage.setImageResource(R.drawable.logo_horizontol_xmrujo); + } + + toolbarTitle = findViewById(R.id.toolbarTitle); + toolbarSubtitle = findViewById(R.id.toolbarSubtitle); + bSettings = findViewById(R.id.bSettings); + bSettings.setOnClickListener(v -> { + if (onButtonListener != null) { + onButtonListener.onButton(buttonType); + } + }); + } + + public void setTitle(String title, String subtitle) { + setTitle(title); + setSubtitle(subtitle); + } + + public void setTitle(String title) { + toolbarTitle.setText(title); + if (title != null) { + toolbarImage.setVisibility(View.INVISIBLE); + toolbarTitle.setVisibility(View.VISIBLE); + } else { + toolbarImage.setVisibility(View.VISIBLE); + toolbarTitle.setVisibility(View.INVISIBLE); + } + } + + 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_SETTINGS = 3; + public final static int BUTTON_CANCEL = 4; + + int buttonType = BUTTON_SETTINGS; + + public void setButton(int type) { + switch (type) { + case BUTTON_BACK: + Timber.d("BUTTON_BACK"); + bSettings.setImageResource(R.drawable.ic_arrow_back); + bSettings.setVisibility(View.VISIBLE); + break; + case BUTTON_CLOSE: + Timber.d("BUTTON_CLOSE"); + bSettings.setImageResource(R.drawable.ic_close_white_24dp); + bSettings.setVisibility(View.VISIBLE); + break; + case BUTTON_SETTINGS: + Timber.d("BUTTON_SETTINGS"); + bSettings.setImageResource(R.drawable.ic_settings); + bSettings.setVisibility(View.VISIBLE); + break; + case BUTTON_CANCEL: + Timber.d("BUTTON_CANCEL"); + bSettings.setImageResource(R.drawable.ic_close_white_24dp); + bSettings.setVisibility(View.VISIBLE); + break; + case BUTTON_NONE: + default: + Timber.d("BUTTON_NONE"); + bSettings.setVisibility(View.INVISIBLE); + } + buttonType = type; + } + + public void setSubtitle(String subtitle) { + toolbarSubtitle.setText(subtitle); + if (subtitle != null) { + toolbarSubtitle.setVisibility(View.VISIBLE); + } else { + toolbarSubtitle.setVisibility(View.INVISIBLE); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/theromus/sha/Keccak.java b/app/src/main/java/com/theromus/sha/Keccak.java new file mode 100644 index 0000000..0163a31 --- /dev/null +++ b/app/src/main/java/com/theromus/sha/Keccak.java @@ -0,0 +1,170 @@ +package com.theromus.sha; + +import static com.theromus.utils.HexUtils.leftRotate64; +import static com.theromus.utils.HexUtils.convertToUint; +import static com.theromus.utils.HexUtils.convertFromLittleEndianTo64; +import static com.theromus.utils.HexUtils.convertFrom64ToLittleEndian; +import static java.lang.Math.min; +import static java.lang.System.arraycopy; +import static java.util.Arrays.fill; + +import java.io.ByteArrayOutputStream; +import java.math.BigInteger; + + +/** + * Keccak implementation. + * + * @author romus + */ +public class Keccak { + + private static BigInteger BIT_64 = new BigInteger("18446744073709551615"); + + /** + * Do hash. + * + * @param message input data + * @param parameter keccak param + * @return byte-array result + */ + public byte[] getHash(final byte[] message, final Parameters parameter) { + int[] uState = new int[200]; + int[] uMessage = convertToUint(message); + + + int rateInBytes = parameter.getRate() / 8; + int blockSize = 0; + int inputOffset = 0; + + // Absorbing phase + while (inputOffset < uMessage.length) { + blockSize = min(uMessage.length - inputOffset, rateInBytes); + for (int i = 0; i < blockSize; i++) { + uState[i] = uState[i] ^ uMessage[i + inputOffset]; + } + + inputOffset = inputOffset + blockSize; + if (blockSize == rateInBytes) { + doKeccakf(uState); + blockSize = 0; + } + } + + // Padding phase + uState[blockSize] = uState[blockSize] ^ parameter.getD(); + if ((parameter.getD() & 0x80) != 0 && blockSize == (rateInBytes - 1)) { + doKeccakf(uState); + } + + uState[rateInBytes - 1] = uState[rateInBytes - 1] ^ 0x80; + doKeccakf(uState); + + // Squeezing phase + ByteArrayOutputStream byteResults = new ByteArrayOutputStream(); + int tOutputLen = parameter.getOutputLen() / 8; + while (tOutputLen > 0) { + blockSize = min(tOutputLen, rateInBytes); + for (int i = 0; i < blockSize; i++) { + byteResults.write((byte) uState[i]); + } + + tOutputLen -= blockSize; + if (tOutputLen > 0) { + doKeccakf(uState); + } + } + + return byteResults.toByteArray(); + } + + private void doKeccakf(final int[] uState) { + BigInteger[][] lState = new BigInteger[5][5]; + + for (int i = 0; i < 5; i++) { + for (int j = 0; j < 5; j++) { + int[] data = new int[8]; + arraycopy(uState, 8 * (i + 5 * j), data, 0, data.length); + lState[i][j] = convertFromLittleEndianTo64(data); + } + } + roundB(lState); + + fill(uState, 0); + for (int i = 0; i < 5; i++) { + for (int j = 0; j < 5; j++) { + int[] data = convertFrom64ToLittleEndian(lState[i][j]); + arraycopy(data, 0, uState, 8 * (i + 5 * j), data.length); + } + } + + } + + /** + * Permutation on the given state. + * + * @param state state + */ + private void roundB(final BigInteger[][] state) { + int LFSRstate = 1; + for (int round = 0; round < 24; round++) { + BigInteger[] C = new BigInteger[5]; + BigInteger[] D = new BigInteger[5]; + + // θ step + for (int i = 0; i < 5; i++) { + C[i] = state[i][0].xor(state[i][1]).xor(state[i][2]).xor(state[i][3]).xor(state[i][4]); + } + + for (int i = 0; i < 5; i++) { + D[i] = C[(i + 4) % 5].xor(leftRotate64(C[(i + 1) % 5], 1)); + } + + for (int i = 0; i < 5; i++) { + for (int j = 0; j < 5; j++) { + state[i][j] = state[i][j].xor(D[i]); + } + } + + //ρ and π steps + int x = 1, y = 0; + BigInteger current = state[x][y]; + for (int i = 0; i < 24; i++) { + int tX = x; + x = y; + y = (2 * tX + 3 * y) % 5; + + BigInteger shiftValue = current; + current = state[x][y]; + + state[x][y] = leftRotate64(shiftValue, (i + 1) * (i + 2) / 2); + } + + //χ step + for (int j = 0; j < 5; j++) { + BigInteger[] t = new BigInteger[5]; + for (int i = 0; i < 5; i++) { + t[i] = state[i][j]; + } + + for (int i = 0; i < 5; i++) { + // ~t[(i + 1) % 5] + BigInteger invertVal = t[(i + 1) % 5].xor(BIT_64); + // t[i] ^ ((~t[(i + 1) % 5]) & t[(i + 2) % 5]) + state[i][j] = t[i].xor(invertVal.and(t[(i + 2) % 5])); + } + } + + //ι step + for (int i = 0; i < 7; i++) { + LFSRstate = ((LFSRstate << 1) ^ ((LFSRstate >> 7) * 0x71)) % 256; + // pow(2, i) - 1 + int bitPosition = (1 << i) - 1; + if ((LFSRstate & 2) != 0) { + state[0][0] = state[0][0].xor(new BigInteger("1").shiftLeft(bitPosition)); + } + } + } + } + +} diff --git a/app/src/main/java/com/theromus/sha/Parameters.java b/app/src/main/java/com/theromus/sha/Parameters.java new file mode 100644 index 0000000..6835b5a --- /dev/null +++ b/app/src/main/java/com/theromus/sha/Parameters.java @@ -0,0 +1,51 @@ +package com.theromus.sha; + +/** + * The parameters defining the standard FIPS 202. + * + * @author romus + */ +public enum Parameters { + KECCAK_224 (1152, 0x01, 224), + KECCAK_256 (1088, 0x01, 256), + KECCAK_384 (832, 0x01, 384), + KECCAK_512 (576, 0x01, 512), + + SHA3_224 (1152, 0x06, 224), + SHA3_256 (1088, 0x06, 256), + SHA3_384 (832, 0x06, 384), + SHA3_512 (576, 0x06, 512), + + SHAKE128 (1344, 0x1F, 256), + SHAKE256 (1088, 0x1F, 512); + + private final int rate; + + /** + * Delimited suffix. + */ + public final int d; + + /** + * Output length (bits). + */ + public final int outputLen; + + Parameters(int rate, int d, int outputLen) { + this.rate = rate; + this.d = d; + this.outputLen = outputLen; + } + + public int getRate() { + return rate; + } + + public int getD() { + return d; + } + + public int getOutputLen() { + return outputLen; + } +} diff --git a/app/src/main/java/com/theromus/utils/HexUtils.java b/app/src/main/java/com/theromus/utils/HexUtils.java new file mode 100644 index 0000000..d1fc903 --- /dev/null +++ b/app/src/main/java/com/theromus/utils/HexUtils.java @@ -0,0 +1,97 @@ +package com.theromus.utils; + + +import java.io.ByteArrayOutputStream; +import java.math.BigInteger; + +/** + * Hex-utils. + * + * @author romus + */ +public class HexUtils { + + private static final byte[] ENCODE_BYTE_TABLE = { + (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', (byte) '5', (byte) '6', (byte) '7', + (byte) '8', (byte) '9', (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f' + }; + + /** + * Convert byte array to unsigned array. + * + * @param data byte array + * @return unsigned array + */ + public static int[] convertToUint(final byte[] data) { + int[] converted = new int[data.length]; + for (int i = 0; i < data.length; i++) { + converted[i] = data[i] & 0xFF; + } + + return converted; + } + + /** + * Convert LE to 64-bit value (unsigned long). + * + * @param data data + * @return 64-bit value (unsigned long) + */ + public static BigInteger convertFromLittleEndianTo64(final int[] data) { + BigInteger uLong = new BigInteger("0"); + for (int i = 0; i < 8; i++) { + uLong = uLong.add(new BigInteger(Integer.toString(data[i])).shiftLeft(8 * i)); + } + + return uLong; + } + + /** + * Convert 64-bit (unsigned long) value to LE. + * + * @param uLong 64-bit value (unsigned long) + * @return LE + */ + public static int[] convertFrom64ToLittleEndian(final BigInteger uLong) { + int[] data = new int[8]; + BigInteger mod256 = new BigInteger("256"); + for (int i = 0; i < 8; i++) { + data[i] = uLong.shiftRight((8 * i)).mod(mod256).intValue(); + } + + return data; + } + + /** + * Bitwise rotate left. + * + * @param value unsigned long value + * @param rotate rotate left + * @return result + */ + public static BigInteger leftRotate64(final BigInteger value, final int rotate) { + BigInteger lp = value.shiftRight(64 - (rotate % 64)); + BigInteger rp = value.shiftLeft(rotate % 64); + + return lp.add(rp).mod(new BigInteger("18446744073709551616")); + } + + /** + * Convert bytes to string. + * + * @param data bytes array + * @return string + */ + public static String convertBytesToString(final byte[] data) { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + for (int i = 0; i < data.length; i++) { + int uVal = data[i] & 0xFF; + + buffer.write(ENCODE_BYTE_TABLE[(uVal >>> 4)]); + buffer.write(ENCODE_BYTE_TABLE[uVal & 0xF]); + } + + return new String(buffer.toByteArray()); + } + +} diff --git a/app/src/main/java/info/guardianproject/netcipher/client/StrongOkHttpClientBuilder.java b/app/src/main/java/info/guardianproject/netcipher/client/StrongOkHttpClientBuilder.java new file mode 100644 index 0000000..60ee5a4 --- /dev/null +++ b/app/src/main/java/info/guardianproject/netcipher/client/StrongOkHttpClientBuilder.java @@ -0,0 +1,115 @@ +/* + * Copyright 2012-2016 Nathan Freitas + * Copyright 2015 str4d + * Portions Copyright (c) 2016 CommonsWare, LLC + * + * 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 info.guardianproject.netcipher.client; + +import android.content.Context; +import android.content.Intent; +import javax.net.ssl.SSLSocketFactory; +import okhttp3.OkHttpClient; +import okhttp3.Request; + +/** + * Creates an OkHttpClient using NetCipher configuration. Use + * build() if you have no other OkHttpClient configuration + * that you need to perform. Or, use applyTo() to augment an + * existing OkHttpClient.Builder with NetCipher. + */ +public class StrongOkHttpClientBuilder extends + StrongBuilderBase { + /** + * Creates a StrongOkHttpClientBuilder using the strongest set + * of options for security. Use this if the strongest set of + * options is what you want; otherwise, create a + * builder via the constructor and configure it as you see fit. + * + * @param context any Context will do + * @return a configured StrongOkHttpClientBuilder + * @throws Exception + */ + static public StrongOkHttpClientBuilder forMaxSecurity(Context context) + throws Exception { + return(new StrongOkHttpClientBuilder(context) + .withBestProxy()); + } + + /** + * Creates a builder instance. + * + * @param context any Context will do; builder will hold onto + * Application context + */ + public StrongOkHttpClientBuilder(Context context) { + super(context); + } + + /** + * Copy constructor. + * + * @param original builder to clone + */ + public StrongOkHttpClientBuilder(StrongOkHttpClientBuilder original) { + super(original); + } + + /** + * OkHttp3 does not support SOCKS proxies: + * https://github.com/square/okhttp/issues/2315 + * + * @return false + */ + @Override + public boolean supportsSocksProxy() { + return(true); + } + + /** + * {@inheritDoc} + */ + @Override + public OkHttpClient build(Intent status) { + return(applyTo(new OkHttpClient.Builder(), status).build()); + } + + /** + * Adds NetCipher configuration to an existing OkHttpClient.Builder, + * in case you have additional configuration that you wish to + * perform. + * + * @param builder a new or partially-configured OkHttpClient.Builder + * @return the same builder + */ + public OkHttpClient.Builder applyTo(OkHttpClient.Builder builder, Intent status) { + SSLSocketFactory factory=buildSocketFactory(); + + if (factory!=null) { + builder.sslSocketFactory(factory); + } + + return(builder + .proxy(buildProxy(status))); + } + + @Override + protected String get(Intent status, OkHttpClient connection, + String url) throws Exception { + Request request=new Request.Builder().url(url).build(); + + return(connection.newCall(request).execute().body().string()); + } +} diff --git a/app/src/main/res/anim/cycle_7.xml b/app/src/main/res/anim/cycle_7.xml new file mode 100644 index 0000000..4bfb143 --- /dev/null +++ b/app/src/main/res/anim/cycle_7.xml @@ -0,0 +1,3 @@ + + \ No newline at end of file diff --git a/app/src/main/res/anim/fab_close.xml b/app/src/main/res/anim/fab_close.xml new file mode 100644 index 0000000..7a5c735 --- /dev/null +++ b/app/src/main/res/anim/fab_close.xml @@ -0,0 +1,18 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/fab_close_screen.xml b/app/src/main/res/anim/fab_close_screen.xml new file mode 100644 index 0000000..ec4ee1a --- /dev/null +++ b/app/src/main/res/anim/fab_close_screen.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/fab_open.xml b/app/src/main/res/anim/fab_open.xml new file mode 100644 index 0000000..03c4c30 --- /dev/null +++ b/app/src/main/res/anim/fab_open.xml @@ -0,0 +1,18 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/fab_open_screen.xml b/app/src/main/res/anim/fab_open_screen.xml new file mode 100644 index 0000000..ceb2831 --- /dev/null +++ b/app/src/main/res/anim/fab_open_screen.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/fab_pulse.xml b/app/src/main/res/anim/fab_pulse.xml new file mode 100644 index 0000000..fb4e3ac --- /dev/null +++ b/app/src/main/res/anim/fab_pulse.xml @@ -0,0 +1,15 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/rotate_backward.xml b/app/src/main/res/anim/rotate_backward.xml new file mode 100644 index 0000000..fed9d93 --- /dev/null +++ b/app/src/main/res/anim/rotate_backward.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/rotate_forward.xml b/app/src/main/res/anim/rotate_forward.xml new file mode 100644 index 0000000..47877dd --- /dev/null +++ b/app/src/main/res/anim/rotate_forward.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/shake.xml b/app/src/main/res/anim/shake.xml new file mode 100644 index 0000000..816ef0a --- /dev/null +++ b/app/src/main/res/anim/shake.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/app/src/main/res/color/btn_color_selector.xml b/app/src/main/res/color/btn_color_selector.xml new file mode 100644 index 0000000..a863d40 --- /dev/null +++ b/app/src/main/res/color/btn_color_selector.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable-night/ic_emptygunther.xml b/app/src/main/res/drawable-night/ic_emptygunther.xml new file mode 100644 index 0000000..f6534b6 --- /dev/null +++ b/app/src/main/res/drawable-night/ic_emptygunther.xml @@ -0,0 +1,1083 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-night/ic_gunther_streetmode.xml b/app/src/main/res/drawable-night/ic_gunther_streetmode.xml new file mode 100644 index 0000000..93ff431 --- /dev/null +++ b/app/src/main/res/drawable-night/ic_gunther_streetmode.xml @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-night/ic_onboarding_fingerprint.xml b/app/src/main/res/drawable-night/ic_onboarding_fingerprint.xml new file mode 100644 index 0000000..2b07a1c --- /dev/null +++ b/app/src/main/res/drawable-night/ic_onboarding_fingerprint.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-night/ic_onboarding_nodes.xml b/app/src/main/res/drawable-night/ic_onboarding_nodes.xml new file mode 100644 index 0000000..7c8c5bb --- /dev/null +++ b/app/src/main/res/drawable-night/ic_onboarding_nodes.xml @@ -0,0 +1,231 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-night/ic_onboarding_seed.xml b/app/src/main/res/drawable-night/ic_onboarding_seed.xml new file mode 100644 index 0000000..c7b7934 --- /dev/null +++ b/app/src/main/res/drawable-night/ic_onboarding_seed.xml @@ -0,0 +1,393 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-night/ic_onboarding_welcome.xml b/app/src/main/res/drawable-night/ic_onboarding_welcome.xml new file mode 100644 index 0000000..65c3886 --- /dev/null +++ b/app/src/main/res/drawable-night/ic_onboarding_welcome.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-night/ic_onboarding_xmrto.xml b/app/src/main/res/drawable-night/ic_onboarding_xmrto.xml new file mode 100644 index 0000000..28054d3 --- /dev/null +++ b/app/src/main/res/drawable-night/ic_onboarding_xmrto.xml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-v24/ic_check_circle.xml b/app/src/main/res/drawable-v24/ic_check_circle.xml new file mode 100644 index 0000000..7c0c48e --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_check_circle.xml @@ -0,0 +1,24 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable-v24/ic_check_circle_xmr.xml b/app/src/main/res/drawable-v24/ic_check_circle_xmr.xml new file mode 100644 index 0000000..342a70b --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_check_circle_xmr.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-v24/ic_xmrto_btc_off.xml b/app/src/main/res/drawable-v24/ic_xmrto_btc_off.xml new file mode 100644 index 0000000..bacf2e4 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_xmrto_btc_off.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable-v24/ic_xmrto_dash_off.xml b/app/src/main/res/drawable-v24/ic_xmrto_dash_off.xml new file mode 100644 index 0000000..8d52acf --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_xmrto_dash_off.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable-v24/ic_xmrto_doge_off.xml b/app/src/main/res/drawable-v24/ic_xmrto_doge_off.xml new file mode 100644 index 0000000..dc12f6b --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_xmrto_doge_off.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable-v24/ic_xmrto_eth_off.xml b/app/src/main/res/drawable-v24/ic_xmrto_eth_off.xml new file mode 100644 index 0000000..8e58eca --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_xmrto_eth_off.xml @@ -0,0 +1,38 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable-v24/ic_xmrto_ltc_off.xml b/app/src/main/res/drawable-v24/ic_xmrto_ltc_off.xml new file mode 100644 index 0000000..ffdc3bb --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_xmrto_ltc_off.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/backgound_all.xml b/app/src/main/res/drawable/backgound_all.xml new file mode 100644 index 0000000..7e94c2e --- /dev/null +++ b/app/src/main/res/drawable/backgound_all.xml @@ -0,0 +1,352 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/backgound_seed.xml b/app/src/main/res/drawable/backgound_seed.xml new file mode 100644 index 0000000..b15241e --- /dev/null +++ b/app/src/main/res/drawable/backgound_seed.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/app/src/main/res/drawable/backgound_toolbar_mainnet.xml b/app/src/main/res/drawable/backgound_toolbar_mainnet.xml new file mode 100644 index 0000000..87168c3 --- /dev/null +++ b/app/src/main/res/drawable/backgound_toolbar_mainnet.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/backgound_toolbar_streetmode.xml b/app/src/main/res/drawable/backgound_toolbar_streetmode.xml new file mode 100644 index 0000000..d354b60 --- /dev/null +++ b/app/src/main/res/drawable/backgound_toolbar_streetmode.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/button_green.xml b/app/src/main/res/drawable/button_green.xml new file mode 100644 index 0000000..7bcc594 --- /dev/null +++ b/app/src/main/res/drawable/button_green.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/button_selector_green.xml b/app/src/main/res/drawable/button_selector_green.xml new file mode 100644 index 0000000..3eb712d --- /dev/null +++ b/app/src/main/res/drawable/button_selector_green.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dot_dark.xml b/app/src/main/res/drawable/dot_dark.xml new file mode 100644 index 0000000..fdd253f --- /dev/null +++ b/app/src/main/res/drawable/dot_dark.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/dot_light.xml b/app/src/main/res/drawable/dot_light.xml new file mode 100644 index 0000000..8699969 --- /dev/null +++ b/app/src/main/res/drawable/dot_light.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/gradient_all.xml b/app/src/main/res/drawable/gradient_all.xml new file mode 100644 index 0000000..0c1e537 --- /dev/null +++ b/app/src/main/res/drawable/gradient_all.xml @@ -0,0 +1,9 @@ + + + + diff --git a/app/src/main/res/drawable/gradient_oval.xml b/app/src/main/res/drawable/gradient_oval.xml new file mode 100644 index 0000000..a2ec4b7 --- /dev/null +++ b/app/src/main/res/drawable/gradient_oval.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/gradient_street.xml b/app/src/main/res/drawable/gradient_street.xml new file mode 100644 index 0000000..13d38c7 --- /dev/null +++ b/app/src/main/res/drawable/gradient_street.xml @@ -0,0 +1,9 @@ + + + + diff --git a/app/src/main/res/drawable/gradient_street_efab.xml b/app/src/main/res/drawable/gradient_street_efab.xml new file mode 100644 index 0000000..fecef8c --- /dev/null +++ b/app/src/main/res/drawable/gradient_street_efab.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/gunther_24dp.png b/app/src/main/res/drawable/gunther_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..33e0c0f5b2942e3a6ce0113d9472f8dde48f91c7 GIT binary patch literal 1294 zcmbVMZD<>19KSNxq?OK1bhPU9yb4YC)tM!QqNapsXTNOX4FhtZ3oj9}uOJUQkc7j8P;tMWzmikMamFVwjX?~y?!?2dv9x;j5R{L=_qW5AcR7Fc0jHF>oAA%*>1XwtyX8|57 z%6X6ka&GL!&p{BwwiHw;4b$;HUeSwg+17EFiv~htSg^Bf$jUH)coyVUEkyis{$m1H zb0K1%FHXe`0Tk3d<0eRr_e;w7u)^ht&JH|S<`F>=Kp8I=M>LBshln*^9_?*0N#JWL za5zM)JC%+na6vZ#?sI!w3Psa6%erZ=k7ennaE788l0rA@qP@J2%nk8L zFbb4YwKP ztUd}u1oCt}grX6TZxCx%Em0_CWLY*y+D(zgq8-mUpF?@wxlrwkV#dI&lXfP&*2=v9BQ*%7!4WKOgy_BD!*Z@rjsDLNz z=b|3k<7Gr4T<6#fD|B;VArg*?oWRj^#2;XNLcr%?17VNX*B#x!#xx7cngZ%}Rb;n@ zjocB-3nq}EZc4g7QtyC7L5I3k&<$KjvH0$|tf-nTc)~8wdb}bq)t5jnYU)LNEyKLJ zi3c85V5kV|75pL>@uSGS4A&iI{ESy*>8QXEHEizxWQ|15kam0gr(NnI)PweRqxsQc zV|;*yX2wLrapSjkG@lK@m>8DIm$QGbu6Ay5KK9%c)zy&hio_niIFOVw_+7-w1!n1( zLY(>Q>WU-N=gwp+bVh7`d-cM-Gcy-6Z@zx~{yxhzCe$4}WXkpUlY*4Ebj_iC`KPEJ z{G;0R>|o_~VR30<``jf-`OsY1dBFGk8y}rIeNX>hXQQXO_0efDkv=>kL|(qUI6bxX z1R=}eAD6rK_c(?hdiAv_Wxl#R-#hujwwqVJaNhc4^1ji=@A2lF9Y+?xPrdIr&z|ay ze%9tVk)ORjUxh2dBfAc4e;-yHXV1BM+ZP;D%ZM% zH6DHD@WAuiEZY$G(%kmIexNu#*4g&{vLI`k>?d()HUP s?0)5uj>^u?yJvp5_N%r$+t4-9fbDMqfxZiG&DwvcSfpQkw>vZW52{+YtpET3 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/gunther_coder.png b/app/src/main/res/drawable/gunther_coder.png new file mode 100644 index 0000000000000000000000000000000000000000..bc6508c91bbe58c6c24235c7e0a8945dc5f2f6b2 GIT binary patch literal 3622 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 B%QpZ3 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/gunther_csi_24dp.png b/app/src/main/res/drawable/gunther_csi_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..a527514b8f5cd0459e3c813287c678c0a94819e4 GIT binary patch literal 1296 zcmbVMTWr%-7&hv?tn1iTZUSUnG(aQqC5e+*ONcD<@J`D%!z_n&K2}9cNzAAsRuNTJpLe_5%yefIe9bU=J@| z#!y)bVBMZ55!E@6mD>vj=qhw3#6rL5m#~&*v?}b=;=}jiXM%w5Ub{LqF~DKyrAQxlfV_lj%!idwBq1jH&)d)6T>>dOAO)m!7O^+gqI>{A4&QM zpNsYS!!FY0rg)Amajc5Cold_$_FhAWY3N7fb?XGevGZjv5u2Hic7(BloX=es}Pv}v45LKKZ3Yly86z|pVTDt|Vv zy|wMz!;``Gqo2J)Bu|bUsC!{#hB$AHm0hU6T^ydO9xHn{SigP)v3dW^#;f-yA0w3y zW-mQ^aO&#s+P-*0GqNW+S@?VF!;jzCf|PGW2Ja4S_;P%^X8)rip-%qOVEWiU7bmyh literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/gunther_desaturated.png b/app/src/main/res/drawable/gunther_desaturated.png new file mode 100644 index 0000000000000000000000000000000000000000..2f14e52c7d4edc6b38fcf1bc18525364268b7b01 GIT binary patch literal 5302 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 Ym3a8n?_S_#5&}76?PyhQ;q}M=0f1bY-~a#s literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/ic_account_balance_wallet_black_24dp.xml b/app/src/main/res/drawable/ic_account_balance_wallet_black_24dp.xml new file mode 100644 index 0000000..23c3e5d --- /dev/null +++ b/app/src/main/res/drawable/ic_account_balance_wallet_black_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml new file mode 100644 index 0000000..e249c3e --- /dev/null +++ b/app/src/main/res/drawable/ic_add.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_add_circle.xml b/app/src/main/res/drawable/ic_add_circle.xml new file mode 100644 index 0000000..2c0d372 --- /dev/null +++ b/app/src/main/res/drawable/ic_add_circle.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_all_inclusive.xml b/app/src/main/res/drawable/ic_all_inclusive.xml new file mode 100644 index 0000000..92f9867 --- /dev/null +++ b/app/src/main/res/drawable/ic_all_inclusive.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_back.xml b/app/src/main/res/drawable/ic_arrow_back.xml new file mode 100644 index 0000000..858c605 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_back.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_check_circle.xml b/app/src/main/res/drawable/ic_check_circle.xml new file mode 100644 index 0000000..d820d9b --- /dev/null +++ b/app/src/main/res/drawable/ic_check_circle.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_check_circle_xmr.xml b/app/src/main/res/drawable/ic_check_circle_xmr.xml new file mode 100644 index 0000000..645bdf1 --- /dev/null +++ b/app/src/main/res/drawable/ic_check_circle_xmr.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_close_white_24dp.xml b/app/src/main/res/drawable/ic_close_white_24dp.xml new file mode 100644 index 0000000..927a942 --- /dev/null +++ b/app/src/main/res/drawable/ic_close_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_content_copy_24dp.xml b/app/src/main/res/drawable/ic_content_copy_24dp.xml new file mode 100644 index 0000000..1ba50d3 --- /dev/null +++ b/app/src/main/res/drawable/ic_content_copy_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_content_paste_24dp.xml b/app/src/main/res/drawable/ic_content_paste_24dp.xml new file mode 100644 index 0000000..01dae40 --- /dev/null +++ b/app/src/main/res/drawable/ic_content_paste_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_done_all.xml b/app/src/main/res/drawable/ic_done_all.xml new file mode 100644 index 0000000..80c1ad8 --- /dev/null +++ b/app/src/main/res/drawable/ic_done_all.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_emptygunther.xml b/app/src/main/res/drawable/ic_emptygunther.xml new file mode 100644 index 0000000..3f45d8e --- /dev/null +++ b/app/src/main/res/drawable/ic_emptygunther.xml @@ -0,0 +1,1113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_error_red_24dp.xml b/app/src/main/res/drawable/ic_error_red_24dp.xml new file mode 100644 index 0000000..70e38a9 --- /dev/null +++ b/app/src/main/res/drawable/ic_error_red_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_error_red_36dp.xml b/app/src/main/res/drawable/ic_error_red_36dp.xml new file mode 100644 index 0000000..6d30b10 --- /dev/null +++ b/app/src/main/res/drawable/ic_error_red_36dp.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_eye.xml b/app/src/main/res/drawable/ic_eye.xml new file mode 100644 index 0000000..c7f3ef7 --- /dev/null +++ b/app/src/main/res/drawable/ic_eye.xml @@ -0,0 +1,19 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_eye_black_24dp.xml b/app/src/main/res/drawable/ic_eye_black_24dp.xml new file mode 100644 index 0000000..e02f1d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_eye_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_favorite_24dp.xml b/app/src/main/res/drawable/ic_favorite_24dp.xml new file mode 100644 index 0000000..933147e --- /dev/null +++ b/app/src/main/res/drawable/ic_favorite_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_favorite_border_24dp.xml b/app/src/main/res/drawable/ic_favorite_border_24dp.xml new file mode 100644 index 0000000..a26a6ba --- /dev/null +++ b/app/src/main/res/drawable/ic_favorite_border_24dp.xml @@ -0,0 +1,9 @@ + + + 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 0000000..30d9186 --- /dev/null +++ b/app/src/main/res/drawable/ic_fingerprint.xml @@ -0,0 +1,25 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_gunther_streetmode.xml b/app/src/main/res/drawable/ic_gunther_streetmode.xml new file mode 100644 index 0000000..4f8a59a --- /dev/null +++ b/app/src/main/res/drawable/ic_gunther_streetmode.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_hand.xml b/app/src/main/res/drawable/ic_hand.xml new file mode 100644 index 0000000..c8f20cf --- /dev/null +++ b/app/src/main/res/drawable/ic_hand.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_help_white_24dp.xml b/app/src/main/res/drawable/ic_help_white_24dp.xml new file mode 100644 index 0000000..1f73200 --- /dev/null +++ b/app/src/main/res/drawable/ic_help_white_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_import.xml b/app/src/main/res/drawable/ic_import.xml new file mode 100644 index 0000000..62c1d5e --- /dev/null +++ b/app/src/main/res/drawable/ic_import.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_info_outline_black_24dp.xml b/app/src/main/res/drawable/ic_info_outline_black_24dp.xml new file mode 100644 index 0000000..f942316 --- /dev/null +++ b/app/src/main/res/drawable/ic_info_outline_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_info_outline_gray_24dp.xml b/app/src/main/res/drawable/ic_info_outline_gray_24dp.xml new file mode 100644 index 0000000..c7841dd --- /dev/null +++ b/app/src/main/res/drawable/ic_info_outline_gray_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_info_white_24dp.xml b/app/src/main/res/drawable/ic_info_white_24dp.xml new file mode 100644 index 0000000..9f60ef9 --- /dev/null +++ b/app/src/main/res/drawable/ic_info_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_key.xml b/app/src/main/res/drawable/ic_key.xml new file mode 100644 index 0000000..6252c72 --- /dev/null +++ b/app/src/main/res/drawable/ic_key.xml @@ -0,0 +1,31 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_keyboard_arrow_down.xml b/app/src/main/res/drawable/ic_keyboard_arrow_down.xml new file mode 100644 index 0000000..d6ed308 --- /dev/null +++ b/app/src/main/res/drawable/ic_keyboard_arrow_down.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_keyboard_arrow_up.xml b/app/src/main/res/drawable/ic_keyboard_arrow_up.xml new file mode 100644 index 0000000..5df167a --- /dev/null +++ b/app/src/main/res/drawable/ic_keyboard_arrow_up.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_launch_external.xml b/app/src/main/res/drawable/ic_launch_external.xml new file mode 100644 index 0000000..761c15c --- /dev/null +++ b/app/src/main/res/drawable/ic_launch_external.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..85c122d --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2c2fd4b --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_ledger_restore.xml b/app/src/main/res/drawable/ic_ledger_restore.xml new file mode 100644 index 0000000..be8b691 --- /dev/null +++ b/app/src/main/res/drawable/ic_ledger_restore.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_logo_horizontol_xmrujo.xml b/app/src/main/res/drawable/ic_logo_horizontol_xmrujo.xml new file mode 100644 index 0000000..ec2cdd4 --- /dev/null +++ b/app/src/main/res/drawable/ic_logo_horizontol_xmrujo.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_monero.xml b/app/src/main/res/drawable/ic_monero.xml new file mode 100644 index 0000000..b50ba67 --- /dev/null +++ b/app/src/main/res/drawable/ic_monero.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_monero_bw.xml b/app/src/main/res/drawable/ic_monero_bw.xml new file mode 100644 index 0000000..2910012 --- /dev/null +++ b/app/src/main/res/drawable/ic_monero_bw.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_monero_logo_b.png b/app/src/main/res/drawable/ic_monero_logo_b.png new file mode 100644 index 0000000000000000000000000000000000000000..2b3dd2beecca165676b43faf5954fb85e6d77509 GIT binary patch literal 1246 zcmeAS@N?(olHy`uVBq!ia0vp^5g^RL1SJ1`U!Dx4BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFsEgPM3hAM`dB6B=jtVb)aX^@765fKFxc2v6eK2Rr0UTq__OB&@Hb09I0xZL0)vRD^GUf^&XRs)DJWv2L<~p`n7AnVzAE zshOFfj)IYap^?4;5Si&3npl~dSs9rtK!Fm_wxX0Ys~{IQs9ivwtx`rwNr9EVetCJh zUb(Seeo?xx^|#0uTKVr7^KE~&-IMVSR9nfZANAQKal@=Hr> zm4GgVcpk{1iHq`zbF$JDTz5Q`N^fZsd*)yF1AWQ8NHOu6e|-KOJ@@& zM+;XoX9HJrLqkg=69X3`HxmmBb3->rCuf)$*z_7ZTUc0FI+{atx|&9`~MqaQy4E>*58}h*W0(j6s+!ulckc5)EV)M|7Dl%S)dHj z+wuOi;QL+IC+PJ%?bF!vtK_oaHa4(%5tV%%Vb|Yodi>bp*r%f=smFisJKx8Dc!6>b zFW9<`pJW2hiO0V99e;}33hIU*`(*-GE&bHg2X)oG8OrNE`|Igc=?A+*UHWjmdKI;Vst05#5+!~g&Q literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/ic_monerujo.xml b/app/src/main/res/drawable/ic_monerujo.xml new file mode 100644 index 0000000..88d4306 --- /dev/null +++ b/app/src/main/res/drawable/ic_monerujo.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_navigate_next.xml b/app/src/main/res/drawable/ic_navigate_next.xml new file mode 100644 index 0000000..435fc04 --- /dev/null +++ b/app/src/main/res/drawable/ic_navigate_next.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_navigate_prev.xml b/app/src/main/res/drawable/ic_navigate_prev.xml new file mode 100644 index 0000000..8460325 --- /dev/null +++ b/app/src/main/res/drawable/ic_navigate_prev.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_network_clearnet.xml b/app/src/main/res/drawable/ic_network_clearnet.xml new file mode 100644 index 0000000..bc817b3 --- /dev/null +++ b/app/src/main/res/drawable/ic_network_clearnet.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_network_tor_on.xml b/app/src/main/res/drawable/ic_network_tor_on.xml new file mode 100644 index 0000000..23640f0 --- /dev/null +++ b/app/src/main/res/drawable/ic_network_tor_on.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_new.xml b/app/src/main/res/drawable/ic_new.xml new file mode 100644 index 0000000..0c861ae --- /dev/null +++ b/app/src/main/res/drawable/ic_new.xml @@ -0,0 +1,32 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_nfc.xml b/app/src/main/res/drawable/ic_nfc.xml new file mode 100644 index 0000000..2821ae8 --- /dev/null +++ b/app/src/main/res/drawable/ic_nfc.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_onboarding_fingerprint.xml b/app/src/main/res/drawable/ic_onboarding_fingerprint.xml new file mode 100644 index 0000000..9ec70a9 --- /dev/null +++ b/app/src/main/res/drawable/ic_onboarding_fingerprint.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_onboarding_nodes.xml b/app/src/main/res/drawable/ic_onboarding_nodes.xml new file mode 100644 index 0000000..391d346 --- /dev/null +++ b/app/src/main/res/drawable/ic_onboarding_nodes.xml @@ -0,0 +1,279 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_onboarding_seed.xml b/app/src/main/res/drawable/ic_onboarding_seed.xml new file mode 100644 index 0000000..3481157 --- /dev/null +++ b/app/src/main/res/drawable/ic_onboarding_seed.xml @@ -0,0 +1,393 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_onboarding_welcome.xml b/app/src/main/res/drawable/ic_onboarding_welcome.xml new file mode 100644 index 0000000..0158b09 --- /dev/null +++ b/app/src/main/res/drawable/ic_onboarding_welcome.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_onboarding_xmrto.xml b/app/src/main/res/drawable/ic_onboarding_xmrto.xml new file mode 100644 index 0000000..6476a20 --- /dev/null +++ b/app/src/main/res/drawable/ic_onboarding_xmrto.xml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_pending.xml b/app/src/main/res/drawable/ic_pending.xml new file mode 100644 index 0000000..bca0e0a --- /dev/null +++ b/app/src/main/res/drawable/ic_pending.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_renew.xml b/app/src/main/res/drawable/ic_renew.xml new file mode 100644 index 0000000..adae305 --- /dev/null +++ b/app/src/main/res/drawable/ic_renew.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_scan.xml b/app/src/main/res/drawable/ic_scan.xml new file mode 100644 index 0000000..a06451e --- /dev/null +++ b/app/src/main/res/drawable/ic_scan.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_seed.xml b/app/src/main/res/drawable/ic_seed.xml new file mode 100644 index 0000000..6d714f6 --- /dev/null +++ b/app/src/main/res/drawable/ic_seed.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_send.xml b/app/src/main/res/drawable/ic_send.xml new file mode 100644 index 0000000..d42e154 --- /dev/null +++ b/app/src/main/res/drawable/ic_send.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml new file mode 100644 index 0000000..a7c7678 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_share.xml b/app/src/main/res/drawable/ic_share.xml new file mode 100644 index 0000000..c2a4926 --- /dev/null +++ b/app/src/main/res/drawable/ic_share.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_sideshift_circle.xml b/app/src/main/res/drawable/ic_sideshift_circle.xml new file mode 100644 index 0000000..f894205 --- /dev/null +++ b/app/src/main/res/drawable/ic_sideshift_circle.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_sideshift_white.xml b/app/src/main/res/drawable/ic_sideshift_white.xml new file mode 100644 index 0000000..549c445 --- /dev/null +++ b/app/src/main/res/drawable/ic_sideshift_white.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_smiley_ecstatic_filled.xml b/app/src/main/res/drawable/ic_smiley_ecstatic_filled.xml new file mode 100644 index 0000000..d636152 --- /dev/null +++ b/app/src/main/res/drawable/ic_smiley_ecstatic_filled.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_smiley_gunther_filled.xml b/app/src/main/res/drawable/ic_smiley_gunther_filled.xml new file mode 100644 index 0000000..73a1202 --- /dev/null +++ b/app/src/main/res/drawable/ic_smiley_gunther_filled.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_smiley_happy_filled.xml b/app/src/main/res/drawable/ic_smiley_happy_filled.xml new file mode 100644 index 0000000..ebd638d --- /dev/null +++ b/app/src/main/res/drawable/ic_smiley_happy_filled.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_smiley_meh_filled.xml b/app/src/main/res/drawable/ic_smiley_meh_filled.xml new file mode 100644 index 0000000..6936876 --- /dev/null +++ b/app/src/main/res/drawable/ic_smiley_meh_filled.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_smiley_neutral_filled.xml b/app/src/main/res/drawable/ic_smiley_neutral_filled.xml new file mode 100644 index 0000000..ca1b805 --- /dev/null +++ b/app/src/main/res/drawable/ic_smiley_neutral_filled.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_smiley_sad_filled.xml b/app/src/main/res/drawable/ic_smiley_sad_filled.xml new file mode 100644 index 0000000..1f7a498 --- /dev/null +++ b/app/src/main/res/drawable/ic_smiley_sad_filled.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_statsup.xml b/app/src/main/res/drawable/ic_statsup.xml new file mode 100644 index 0000000..1a553ba --- /dev/null +++ b/app/src/main/res/drawable/ic_statsup.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_success.xml b/app/src/main/res/drawable/ic_success.xml new file mode 100644 index 0000000..0bc71ee --- /dev/null +++ b/app/src/main/res/drawable/ic_success.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_wifi_1_bar.xml b/app/src/main/res/drawable/ic_wifi_1_bar.xml new file mode 100644 index 0000000..fa525ec --- /dev/null +++ b/app/src/main/res/drawable/ic_wifi_1_bar.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_wifi_2_bar.xml b/app/src/main/res/drawable/ic_wifi_2_bar.xml new file mode 100644 index 0000000..9a96709 --- /dev/null +++ b/app/src/main/res/drawable/ic_wifi_2_bar.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_wifi_3_bar.xml b/app/src/main/res/drawable/ic_wifi_3_bar.xml new file mode 100644 index 0000000..4087257 --- /dev/null +++ b/app/src/main/res/drawable/ic_wifi_3_bar.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_wifi_4_bar.xml b/app/src/main/res/drawable/ic_wifi_4_bar.xml new file mode 100644 index 0000000..0277e81 --- /dev/null +++ b/app/src/main/res/drawable/ic_wifi_4_bar.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_wifi_lock.xml b/app/src/main/res/drawable/ic_wifi_lock.xml new file mode 100644 index 0000000..63f47c9 --- /dev/null +++ b/app/src/main/res/drawable/ic_wifi_lock.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_wifi_off.xml b/app/src/main/res/drawable/ic_wifi_off.xml new file mode 100644 index 0000000..a4905df --- /dev/null +++ b/app/src/main/res/drawable/ic_wifi_off.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_xmrto_btc.xml b/app/src/main/res/drawable/ic_xmrto_btc.xml new file mode 100644 index 0000000..60efbe2 --- /dev/null +++ b/app/src/main/res/drawable/ic_xmrto_btc.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/ic_xmrto_btc_off.xml b/app/src/main/res/drawable/ic_xmrto_btc_off.xml new file mode 100644 index 0000000..a0d6714 --- /dev/null +++ b/app/src/main/res/drawable/ic_xmrto_btc_off.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/ic_xmrto_dash.xml b/app/src/main/res/drawable/ic_xmrto_dash.xml new file mode 100644 index 0000000..a3ace15 --- /dev/null +++ b/app/src/main/res/drawable/ic_xmrto_dash.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_xmrto_dash_off.xml b/app/src/main/res/drawable/ic_xmrto_dash_off.xml new file mode 100644 index 0000000..e183626 --- /dev/null +++ b/app/src/main/res/drawable/ic_xmrto_dash_off.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_xmrto_doge.xml b/app/src/main/res/drawable/ic_xmrto_doge.xml new file mode 100644 index 0000000..c8bfcd7 --- /dev/null +++ b/app/src/main/res/drawable/ic_xmrto_doge.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/ic_xmrto_doge_off.xml b/app/src/main/res/drawable/ic_xmrto_doge_off.xml new file mode 100644 index 0000000..c6c6a3b --- /dev/null +++ b/app/src/main/res/drawable/ic_xmrto_doge_off.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/ic_xmrto_eth.xml b/app/src/main/res/drawable/ic_xmrto_eth.xml new file mode 100644 index 0000000..9569125 --- /dev/null +++ b/app/src/main/res/drawable/ic_xmrto_eth.xml @@ -0,0 +1,38 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_xmrto_eth_off.xml b/app/src/main/res/drawable/ic_xmrto_eth_off.xml new file mode 100644 index 0000000..55c7161 --- /dev/null +++ b/app/src/main/res/drawable/ic_xmrto_eth_off.xml @@ -0,0 +1,38 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_xmrto_logo.xml b/app/src/main/res/drawable/ic_xmrto_logo.xml new file mode 100644 index 0000000..17058e2 --- /dev/null +++ b/app/src/main/res/drawable/ic_xmrto_logo.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_xmrto_ltc.xml b/app/src/main/res/drawable/ic_xmrto_ltc.xml new file mode 100644 index 0000000..5bb3470 --- /dev/null +++ b/app/src/main/res/drawable/ic_xmrto_ltc.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/ic_xmrto_ltc_off.xml b/app/src/main/res/drawable/ic_xmrto_ltc_off.xml new file mode 100644 index 0000000..a78f33f --- /dev/null +++ b/app/src/main/res/drawable/ic_xmrto_ltc_off.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/logo_horizontol_xmrujo.png b/app/src/main/res/drawable/logo_horizontol_xmrujo.png new file mode 100644 index 0000000000000000000000000000000000000000..fd45253973f584d51fe193ce6819edcb32dc7f56 GIT binary patch literal 9814 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�p;(U6U!R+NR_>#v-_v4 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 T)%X{Q14ahu7-`pN!lM2kQ=j{` literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/onboarding_dots.xml b/app/src/main/res/drawable/onboarding_dots.xml new file mode 100644 index 0000000..d35efb8 --- /dev/null +++ b/app/src/main/res/drawable/onboarding_dots.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/selector_login.xml b/app/src/main/res/drawable/selector_login.xml new file mode 100644 index 0000000..b62ad95 --- /dev/null +++ b/app/src/main/res/drawable/selector_login.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml new file mode 100644 index 0000000..c6cdff3 --- /dev/null +++ b/app/src/main/res/layout/activity_login.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/app/src/main/res/layout/activity_on_boarding.xml b/app/src/main/res/layout/activity_on_boarding.xml new file mode 100644 index 0000000..cfaf918 --- /dev/null +++ b/app/src/main/res/layout/activity_on_boarding.xml @@ -0,0 +1,57 @@ + + + +

    k;bv_VD0&cuKeRxM|g|JE+A_u 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

  • EbJ&A){>|bD@~8`tNnvCaR0W1!}1i32Gth zD}6Kt=RA2|hxP+Yc$P+uOMhjb)bp|=3i|xzs1mW|!8&U8UQXF^i@+DBV}19pim51% z-y|-KKd&Z(4?jP-2y-^y;k+uk_<8Vuv0r+B%kxA=n0gl@xTvVX!Bfiexkb8nhrwC8 zwHM{;g0i}@jN3n*XI{)mdziU=sP3?{W1&{xr!)O?YBqZsCmXkK=f<5&ZT{^xAmyeS z#jR!wqxi`|Z-5kI&TlvGVw(aP#FvF~qZo!+M zzD!~syMEeav19hVgD*3bZvI}r?xRMxckj6#hX424#@(A1(jqSYSh6WOL&-x=$)j57 zq@L2jEysO~PwawNhC3(Uyo_A2M!jM0nbh}!KiAfEJg`%D8ync|bwmpWo)4QvGZzeW z2=7hUPX3)Z@%Qh<=}Fmjb)x|@<*4p253?E0S~&;p?Xtf$8MX}Z&F#Ld3wV8EgLdv= z`(^DG&9j1Wz6~AYtJ>_*tDf?1%8z*}}L} z+u2^n43xU+e^y71*6F{#TIkHTNIQWv%9C#dwl;sxc|C8~%`u;Jrex}co)2dopiL^5 zh5i?G#$a8^eX{sHdy&+T{wfycuM;ZP>cq7YIx0d|aBENSJ}#~4^iKHwsIxGEnw#lZ z^z!)AZtoz`a^VwyRmqzRG8OF=H#&!0H!@GM)BM-PHRrA??Eib;2rp0SJn3$`?10(8 zu4A$l9f_*yxMRq^#=36~_;mC(lkxHDhf3={_>E8ritKCZx$Hj)oE@EnM90$3GQ9GP z<(K@1(8}=7)Wl8hpBJ(DW@C4a7uTFzDwxV|=&Vi?$>9Gz+*s)cy!oT~9gJ-|@nNmL zyWp_;VRB;om8o&YuScRyvVv(7y&C>*zlkbOrobe>Ypl7{Q6RRfq)%6``t++JW((hb zdAYx_F4d05j@!OKhu3d57PL0z#I(tx=|kbQRkOw2N6%x|3r9`zb%?s7cY8bCvivh#?|&eZBtD;Y;lv$?c?sHI~EB&@z0!6!TLV+b9y!E=X43i z&+SpUPg!Lm`{GEheev%8PsL5IxiGjDi%N5i3Q%34I%K*V*b1MaBS8dew4x=-DGvI>-)ozu#(qZk0h_V1x{Y<`PNU+nuP z5`SrzhOOV!trvmc_3bO%#gXr!a1AS-7$-1rp!KF5KwZBv~53{4;Bvb8*y)SG{`3!%x(#;au}40PR_tSMUuR~WJ=5P; zTR*8;&$FTb>9Oh<^6XMB!+rTxz0{?WdzYB~KFz_$kPwO+ZPW1ig>wfH7YUPH3kKPT#~l@fln_*orcimCsoqjK^SuB|tf4m-kf z7Mp)Zxlimn3{O4x*V{4i;z_s|-{`jQs3JWW?^vTIc<4FtNi9e&#dRk6<9v?`ODmcl zY+L36vTgtJv+>@rd9qq~V4KORqhpwX_H*~`cJsfgfm+p)B*gJ^Aqh(7C%m>6MT}v? zPIw;-f{nU^Gzfn!e%SiF2{R{ic zwzoWYZvY)1LRU1=3HxjVmmCmvTt9^Q?@=a>YHvl>rDU`o6io_V{5?g+WTd_dCH3)3 zo+Q!SPNq{$LDbS|q_1YK0~5oQ0&G$4u3N;F#|iwJ`CYm(d?INJrJFFSA<-}^lL(TB zVzOw!u~`9lU#=TQ0WEU{hb5^hHLJ2;4*4d4z}(;3S_7GOzGdnkfL{s24vMNSGW~wq zp_J6Em&Hd!ir|6*@6RoMM`NUkAts9~Rw2Hp6wEj;9y>P&c|%|0u;0Qer$b*Q1%XRT zbyewF5O=Rc))OoLVlX)SC=L0?gl2^dPsxVWr~s>8exKZ@!U2~Hv;(m6X0Tp-w|q-;)KP%@&Q-PVk^&qLCIEoB8$&U5j@h!s?aB5kZzPS&ZZ$e-PR1~WzP zZc&wJi`MhktBK@8{mT-B3{_NHgy^9pw1+4Rkm|bDst`g~^+POU_9EWKn)AX- zL+1ltyIK+zZE4*QiUdnUND%iFoP!eDJ(31PKW?{eb@U=d*?~^qTsE<`J zfNhXQLpw~QX{m>>N0Kft+6D>b{kkqI)pIjRZ$*l*mj?%j3eDZ`riO@bKi@$vX#SFM z<&U@BLv9`YJ<`|OR=G!-xiblm{MF;Nv0^A<7)Ga&dr7CkkI7(MVZ7D@T7ZwMap{6G z{iY7b%2~4X4`9=`%$xGhd9dZCZr3U(n~4G#yTvytv=PLis2xCxn~TJ0d5;d)nM4N> za|KFdflTz|;`@Fdut%bVFrz%`OnTa^$pixhQWYFQ42bO)Sbopx! zmMKZ#0Y#$Fvu$Ek%}NMVRz2eY`R>ysdC8xdW7Dw*5Yi+ZLg!#$GWf@}1zg6_ZAe81 z`F^AP%-kv3B=#!%`j`pxk?GUuN*Kwbcx<|6C-pzchWXOIEj~qdZ6cs52mc3YG)0|WPWI&@qOuVuC9F$h2gg5}t*(ggd9sT^Eu<|>cF398h zvdPD4=uFa-%sh`tA6t>Q#!)nXg!V%$7kX{iOuAHx;8pJIlHpJ(>YkwRS|Ygm_6&?R zKb!Vl;6LlxO{*1_)3BYpdk9PyM_SlAuNelJOk|-v1{WT+GnVT1diu3dAUZXA@)iy!4Y8{Io>|46n15k4@{a!Z+=MG3%C~b*loQrA@5L;? zC}omaLvG(18_!?r28b+g1M0e}7WDptk&s&aGE>Skdq8k!_*Dm4o|xK^&~|8%D-&6B zWmod!M_lYlg~eFz)KnjLam9>(*4R&Jlm_JC0#t8zk1Sbw=nLTI$e}PcF}1p&kK5;U zrDndtw9P~hysGaV$74FOkN0t4sJjZTU+(r%ObP5wXZ+h?RK+v>8ss#I7bAkjz5{IMyD_ZQ(J`WuF(4?TZJRgl z9bMcG(Cw=0FR*=al9_STxPg)gRW!KdiNdY}#%DycZ;9c60j#8d|BBQB- zmrZ2rDdo4!lZ9l~vpJAxAhCsbwD5VTyqfC+=SpPvOC40dG`;v-`huAXsPj#ho&EKW`uC_Akxzr=yI-JoIDCi5M7fa+N( zMK2l;7DN>1*P2PGGHe)cAo+&=M?WYqdfrA;3FxEMzJPP4y^V`jz;8U^8EMb z`<~c}QQshGxUbR!CFknL7TeUR0OqE0qbFDK7$eMT;uyegXgcB}hsJ8Ix)Z!bF<(r= z@h~%Ce2p(hInIeM`aZ;d&|Im4g;fISrwuztSt00H>J_Mqo01jI#4xVTMXWaE?ua$O z8nUH6j!M+qMPhl0OwEE^h|WdU=vTxDGky^nt!?~y6SlqMOc@>2aFT>DUtpMGPbqT60MX3Ba38i8B;Nnw#O4jPSOIYUse_jd6 ztwvA&YD8+H!B4gBp%<7+==C$C@?j-KjnJG0v*3F(;;8Gl<8KHP)Nn8%kKZB%4o&OL zZsIfrl`h|cT3DdIqqapZtH*Y}l1>2aZa2Hnh3_P+>2hC=yq^>rLy$3utFwxW%#d&S zQ**pZiBz)#ZZS*_mWIDSQ`lY><5i>0)b{_r`Bwmx%B7rw?>S^azy~f9g}m%ydu`|q(;{c2dxzMwZM|s z7?CPhK(Eib>A^OoU2rq8$!<7?)zeB$^60ZB z#SiWS4YR5AZv9_3z3XZ$68Ne}Dn!*xdl>(fy8$ zZ}z683m+*9@zm5?{Kep_onLT33`Ft;P9XRC1y{fNjfi1ttq$nZ<>bsT68%@Zw89@| z`J)`kmH5rW&pww_`*>^yV0p9=ZSFp1@txNuA0>#)9Fe5=ay6Yc&_IzO&J(P*J5C{z zomxO{2><^31;;$IfQbJE3m48kP5KZ*Au|HvzSU7AB_9bI!Y%{no7g#n@zw%X z>lyUa38j1zhkVJk4+nVC$GO)J6D>_I0V>*GLrU31vM%wbA3VF>0p+!j5vX%&UJ>xj z>DIUz33Cg&n1yhTo)!`}OP~GIzOhXujNJDGXaoS7TpLg9z0{P-kWI|^ioMPRr@=Py z?*w;qLXxW1Le`K&OT1}LG6@PUz*v0MBa&@e2TkI1`m_BT%=)(<_}N}{V?%nu9>+tB?$6a9z_=Pedt z6eBM%LAGUW3JGQR0A9#sk2kGx ze19wc@IxTpKPneGCkQKVzr?1l@g~!MRgd1Ks4-6qsYgbKG!GuP9KPuOBjood9h>u$d{oFJo~7e2mw!z-ghq zO-CHFddqW}$cy!Bn|3e#K)pWOlXna_6(>lhqsmm`ICNN4-t_-UBM;w`Z-2NyzV(_M zX_xcoAiiS%E!?5QHUZ?+H_3EGB{VniWB=g71v`d~caZ4E=A--j|J3GjQqQmMpH^a~ zE6^2KD=WxhuIQT8!T3s0{L-^>H=DsVZqJxpJxj7gS)59y%MYCb8k;iWJtA3)M4>Lc zn?E|$XJL(p9;xmEOAY_no3B0-A}5?N;A*S~3eC;_Cq4o77Jn+sw-hmd6mEJ?3)w;} zm!fw-%V-D&zM1qj2zB|N{nRT_29E7!e&aOe`VrS8li00>$fBOX04)TJF)Un|w;n%p zb1+$*kxN$LhnCeu!k-ZCg1qLw!RI<-Rx(hb5XUXE4g-ojDm z(9}e=C3hN1K!1BZsQ-k)^s$fB=W@7Gqg^JYBlkT=%L%H+e_|7aie_eSq^bcl)$g0S z8xZ}RMrv}H?r}g!v+i1NIkM-CDbi@gbLRO(J%$Sd1dZ0#U?+p~#QnJ=M_2?7vt`KR zPR}tlVRMtLDara78F`b+>u(U3kNA<1!dygGW0?nhaYQ;)V5Z<$(bbq&QaoDPItfzw z)<(;#UmAy>IDN-pmwUqbdhk&0X!R&&UfBp>;@4v)iA$YHC-cc@sU+$*+n1mC0)me= zyr)zEdt%LOwBLE(&`8MmdhSW~%%^1UZuX3S&`cZpepj&x zY{ofXpO9IfoGqgT>DoN4rTAG}F4b)lO4(k$awbV`LGvrwT1aMcTXZvMJHJ&y-lFW= z#|DNgh_N9b3G#EW=l0r@GfYv*n-~(EbX|seo^gI!vGWTtD^fDOJk`G-C6UB(5hXSn z2GLO%_E{P&1R_n#yyJ*>ehSNQyG`68q4qr_)JBMsCW+^lG$ju=c4^Zp&nZFPQ~9$a za0~QH(%(lMqvq;KfrvU>s}@&=F^qJ*xSf-ERu+({g(YLt76sUM{!+x!7xk1x%v~>$ zJnyAw?)>{tahFZI971ois_(aZ0T~!!gXf98%pa2x@j~3zqeZz)H1)Rq{lv1dy_w%d z?>H1aOl#6_5Fl<=!^c`xuMb>zla_4|)tqu4kYo)DIdeFZ%7d}ga+`^YV}o8UlZyAU zKV$e#RcNJZ&Mi`hkYZcKu5}vZ*+Q(}53vxOI6qce$f19V|j6cddF$P9pjjX{5I%;9l@T z*MIM)-Ly&vF{QDa0dDIVVX(mT)k3tfc!Y99mJ{_LsM!tkqg$D$6;Ea6n4TM%8?i4@ zo_b%f`GebTxVCAgQ}Itf#T{WvBmI0k0rp0hLDE%4Dz*IlX{wezFNM>1U5@v;s!O2_xnQxL_956Jb*Wyuv#k)qZ!(1_75zJA!z6#5u;ZdR*3WLKS z+Ww$4;;;ZF=XLkBW};_Rv_~Tmr_w18B!87ipvL;2>Z$C!{xzS<2Sne(;~G8OhH>S; zyKYpfpescXX0+b!`2MsxbS?-XtyNjCgJK^DO-SgUu#L{8enmXLzViWb=7f6zpSU4I zQSsgFh%2I6BWu}Y4|#eWn|(UY#SlT?YtfXqnS2;!H5JKTDIOMr(eQl~gPZf@kqnxX zP5ZE#nH}iTFXt@a{G49HQ3V?qkKi0VMM@c_yqli_Flp@% zmBbDs{-C)79sJK)bOv!(R0*+S>bW2u$b+M7(9h-UIIG$NvOve4-3jx%WqzVN?eDNwE$D2>JiYseO>eG z8^Fs10cON2K5<{F3VSLo&O94`oS1=X;k0GA7NbFz;dZlu&l*xJxI(o&N7sfRHW4_V zgf>y1FNcA-mJp0v2gN(utjw^a@k;|c=)62<@oZdI58i}VWSw&rXmPcG%9M1?0Ep%5<(xdycM~tjO$Fp73MT#5E1;d=wRL|HEBa+R`)~;V6jUEH_7& zJGj85Io$`^XO(LYM2C#V#%;NrSA(Hk9sYNLoioeo+FZ+v!Pf7wyHw5v@N$>Rp+JA? zj(IH~jA{9OCTo~R6OV*7N+1{<>}kfsG!4dmR+FrgJ-fkj7-fZ-LOPt){Tc2168^}$ zpcFf#)9^Y}>z?+-SQ!pD7>t#WGZ|&M@@K1vFQ}os83>5Wko&ll3Plb8Tau(Qso%$; zlSC;K5vnX3K_U#b$5$*9;A2)GMn&Jxv^-|6#Pjcy%npxfZbk9_!DS!4apQcT0 zWLoS_g3Q1pQYbn=5~GAhKHQi$x^@cBa_#*$V}z1nA6nVQg~}k)g=ezLJFWR20&@a< zf{`STlDkWV_5(AI<4Po-T3OcoI-G=Rvd=HMzn#EdzYuRQyntXZZH#zFFIZJKeI@fW zR8cd_onA=Xo^^ck478_dJ`?!j_7~0gW&B)%F3bDx8D!nxc}<}^nsZaoyX7|14sPM) zD4<{YC>Kos>__~XKKbxUud*MLYo$;KN?z|FjM?Hkb?cl=QZG|(Zc4KSe(ayQMXtF7`duDz8fo-E%Zc~#c+1t=GR(fN zjoUjoJV}EcQU?;ROE(ZeFEBx8`6gPJK}ZlcEy%J+dKKt<)SSPf(>4a#aT@mUi9 zg@72uV^ZkOoy}rLGwFYWo_T68?>$5RkmqiObRK(-5?4n)OzcA%=qGkHmoHl2%FOQ$ zea$@8Q}~!a4IdGV$W=}{JmB9`us-=Jn3NJR;TSV{b<~V$c>{{z$x^cp z#sO08GOs<4m8QS=7J-!J%c?f%w|0M+QC80@Y(V|b(w>h#J|X*h`pobm`X#xsG_W!8 zO?>KRU#Gt1E<3IUHA&lqgWz;P(IQP$Fc&pMGcwg!3wmv3JQ7vWjSbIRBVwrzk{WPe zJ7B{dzJgEN97GqSUHQw(3aabjj@BT`L*G=EIrw^7KvNXt^;0!Dipwk^}y|eUN(AiTw@)@xM_~`^s6*|9$hC49<<4sO)Qa@uf0z3 z9KM;#e4v07D_#iUg?O@VEy=nPXb=XC+?olPBOF*`4+s=RE9)*JFA1f!qn^P{xuE5B zU~Mxvx_iNGGO{9bZKMRsqM}R274@V>N5q2H2}E;2!4h|N^@jaNko9FUD!TR!(>=QavbF_mK7e+?h(al*lWjbMjLS9|i}Hfp_KKBC{Hn4_2t7Jp(iCd?5U z0ZUR8_j0Zp9Cq%{%;hLG87zHwK^O!-R*SO%zk7=I_!r-%&n)D+G)h^-ODuAj9e72= zEP_enAi$u;pmwaljn=()rRtzjEh!f4>aZtXiaKIP|2>g~5)~U{hG~)k$3svKZMSOU zxNkBKY~fKxko4<5W}vk|4AE2j4QVTax|Tf^i5>~p2H+QteeC0`eR7M^f*geG`1c&| z(eO#?BfbZlBMRO!PeQV}<0R7nvpwoHNncINb4q6gEGveueNm-5x=miV|I^5pdL^xo zy8c?st@*NFhiE_0$({SrJoIz(Te)*!h+PPrLD!S^Up=XI0E%G^C?ooIy3h4(5cK)> zhdPeGE%>^TdT=aJt(oz1Gzj}go?&wwx;oyvFb(KT?2L6 zJ1Qbor0gZAov?v5iw5pHr95i)0XpVn;Ty)msh(k&a2!561eclbtN1kkqDbvbtC=!t zu#dosHfoQ@w=9GrL_czX=4igi@#8<88j=P-qy{b}?a{)XiB3WmBjCIB34l0tTYN%1 zr%y}cN%RF>EtR{Uq4M3YAMQNogy;o?Vl0>iVfIPA&xm{Z@G?NRx>4R1a7aCzI7=!q zLMb058%u}d7VBYGL~LH*);O3KL;gUn=twcACHp<6v(PpPHy!T#Sx0D2SIMyy&`Qmz zPnowLJDW{-S%WF|0Wqrmo)cyojz1|9&m^PEdeFrgqUJYb-;kbz)92s(%tkt;K#ky^ zEgO_n$*5!p{nOa3GRf#$a76kEWOe@HDFn>7wKfu)tT8@3aO^Fu3(Z>05{?l(y*lxPO^%4Gzpdr1noK73Kc^D*ilQhE1iZjvNgBAKZYcK`V= z<^bN#XLl@hg-=*Tf4nU8TZP7&eWqwryV}H5$ z2shh<)h|k{GRd_1$dfy&oC$eLB#x7}~@22z<*=Du34%Ov}d3QAgY>0|55EXlv-goG|v^!do37BbHid9-k! z!>rNT5=g;~Wc{p9o~jd8idF#x8NvVliknW#)(~6=2gCkT5|YCDrxU9we!FZ~y0XYp zcH(eA2&&+`i96xkYJ*Dp+7ps)g79olNa=nOm)a zZWvdua&xzSoB6(;nKT+)j1`Ve)-*75ae4P}v+1`Gizg;YLl%%KaEUnCB0}Y=64X=R z?u-+dcb{Io9OzgwA=i~P5Y4Udu@%gt6s)N>*3kA`D?2brkVq!`4SEd zymY>Qp$6)>zk}NS>nrLx<^YPHgWK(Goz}PUOpoC8|pLq0Bm~7H-7JEb91UY5`3HXZ6=) zeUe;B<5IKOLNbb!9D9S|hT;7`ILCKS0J44ZX>1 zH_pn{U7<$Ql(V$Hn9GuCe&0?#aV>tY61&2jgPCC53cs1Wab@(jb7S3z-&e0x1r&g* z;Mo>kg*xczKLd2fKX8oXVA1K>#3_UEM3VIvbmddF-mXFAy-TI67nFi<#j=OF>ws=I zMGnC};cJseKr1KJ9D@ntTPg^P{i&~~;+Dx$^(A&1XqWb*MpeYzOIlN->+z7I%jaJU zF~^%{7B|-@k*=gE_*QO+Be6dGBa^3G-cYfpaAyHFjE8qEKzY>)V5SmjO0mp;XZH($ zv>UY)$rB?p3%JJ7ouDZ;l|-n~k4)U0!wlDDwZolNW?WMMp(%ii`%LbtjW{rQER_Q@ zfSBbhvHM8`13<>(G2r2C7vX=Ob0g+3Z<2JPrQvS1k@1K&59+(Dhlj=g1!DY0bxWRA z#fI>uMV`0$ReZS(gyXuI$VE+#UVOlO%Y+k7?Yw1=**}@@Tkfg}3ZwcFb03mXYF`vU z@2OvOct?RX3KoE85ph!SSmffVB~q+pO~nyGy0K|Tu}gsXbhXK)R*E|Q(`I`w=xhor zAMw|6`8{6UW6Rnq2SvyD$~UoJPEd>(mWh=R1{CY!_&G!sq%FlWV+uuWZ{4J&?^ZG` z@2;ly3}{rkiX~2iHJhoo+%ttWb8Kj^FzYW9c%=s2E@0~6-Ox_p_vl5+b(Pc*agu>5 zT0I(JM*Sx7;3fhS%R+Lb30vdYk-P7**a>9hq6c9?J9f)>goj~aoL?h-IZy9{Db!!N zG!eE52f?*H)714a^oQp2QI07}>gg+kU3@-pfb?gNsw4dsX>0E&4CtvzVuA%qNC3sd zVO4k%rQ@ivk1;hwlMYQ?I~jr$YVHeIDvx)*P>?n;CERS8MK zrE7nHWEuVYlM?G;FsTc~UUaWZsw_m0b8FsDDtVK&YfAK`8IX`9L~FiIiIwC=snN-F zEk)RlQ;d;?_}3d~)UK7a)=p57$B4<;YV`Cxb8OOGkE>fR^m6?f_1mUjm-mn9D$db9 z-J&d^9VcQwdCHw$7eUMpdX6W|)v^O*yY$qiZInp06Dx~r5S znfMd`@StB*1M!vsB7lxST9!cMf_9S)MB;lX>i_^FlSStq-lc94b8o1E|GZ=cLS=ZK zaJ3E#@a8_m?dZXIa1=bE{QECaeVSfT5zA?x@lkH%8CuJHbb21j>#rIxc^^;xC@Rzf z;FSKWv&vrCFMG*oFAFb@&UvVZx{{D}JYrqFKW%TXr$$Dvq;!NP>iYi>)-Z^eb*!5r zp-CZyc}VLxvUrPpmnOGDtFZ~xUpie)p71Rlu_+F62PrIXtlE>xLgf;#m2#|2-0r*7 zciqZJ{_g7YvhGW|xqpl(*+C2cPK2(3UBP~F{Hl=W2%><2!^DeD^ypOQ66AY37`s90+TLvY42g?QNF|?bcWAcBSY`PD0*gqs zseOip1HLkw!LCp+pid0L3oPnm%Y=CdeX%pF!+6`z1bjoe6{4j(z@>cY{GZOU6;^*K z&7PQsE zS@fp$`=h)<6Q+Rb{s&z)%%UoLNoa?Jp**&C6;3F;VcmdD-z#$7Y*;g_H=1I4H3G|_ z{pCkl7ZPjty0#miu^HM=LtDXTZ4QI@pR^aG6V)V?LDua_ zMZb&4={34)An56%fyqC4}4MMKpCjIAJL}^+U6vR0HOwS*u@0xvFpZD`Sxx@;t+9~FI=GcBl!APhjJ-mSH zo>(vhY;mE)ylC)!9@Kgtb;WK5yADIGt0WO*DD0X`A+JKL{i#0?{_vbEy<*-@z+MnC zhh!f8vG76GS1k{DApWgjqPV(xf@BR>!BH9HqgsXZ8HYc!MazJ*Yj?ES# zf`mSX_m4&NsUh-*$HX0n+fzX;qxLl43ZJ|87CJHaTo!m$9}*Vs^Lu}ovG^IbnIP?p zyw2K_sQa2$JaFo>aB-7eG4=Dpo}js7Is3%(pV1OWfCuV8kK-H>$2^=DJHum~;vt{s z`al>Z^Yk9cnp(wbdw%KSyMm|>@N}Pmiz!1x>R@_~xH=X^??9-h1}ByXEp1paN+d*A z?^8iq?S;Ly5jQjDOZFC1cS~{}^3P%NPFA+~=MJ1Sfpi9aoa6%2M3Uko3Ocd>y)ZRH z?MD}5ra=o5gr;My)r_*}CvVxQ>i88+s7Bww!uS|!xL=RO%FsvGuiqE_G6-6)QfY+x zy^@=vsOtz4nR^CZDLlM`Tz-;F?;DyYo$`r@w?(Y&vh-MJg*PC_))h|-smV5oUdM$$ zU;v6F`Su4>`==rvkG9waEy{ew^ojVv~Js4Ik}lYW%16Ek&%cFObzqyXnurl3Um zRwk8Qd252ubta}q*-NK6Ti19?^)3^VUIhB2jBZ%lpz$eagOu-ISk4IV9gnLE;A|cb zjr?OL%evFEVMY(ll$IU!hix4D=3f&t890$dO^_Uv$?!w;e|KqiAzER-2d+Lc(p$73 zo63w_jBk=bTP1yc%_)!`ef8=D3A*+*7-A(J6|Y3rLP|2uhE0jf)VRe0D@blPnt6dp z>@U^?^LgwHlym`@`T<`>b|1}q%+QB4%4XaY^MY@rwTjJZ>qC41b(c1X=ltMLR_+XH zGtsxBHKoVXpRH^8>(ly&68#2Ccp{9NA-{Fpefhoofxtm$1CQw7eu@)w@)*RxP6n)V zXGVqiA72KVUj_8>Ta#kO^ETxG+XQq>n_wQ3Y&S!L-x$Vf9Cp-%fGEJ@8*6El$Yvo8&;7_-{16-&VQV?iKZ0(INTi?$4L9 zY8N_>7Z2>I)U6nXoW36mm>jCnGumc;z+1<8NbS9Smgorn9m#9(1`^nWW>5oJWx>p<{=i z>zIsipxZG;0ugINE2%4igqU|lvGz7y-pTmRXX8C@+~bG&5!{K~$XW{W_ykUgg8mW) zD0hXX*aADjhtJgA_)~bbT{C(%-Fh|?)EKWPdtu(vx3%()xJCnhE-noi0_f_+=zA1b zIPh*x5c`F$=o#L8+;k!rv?!g@h`IVBg1^PVdn5()AHTXkiF%r_m|pLRtj~HUn|#2% zW8;r|>Mo8f>GDcq*h;8ZQr0LSQ`%<2VmupJZxP$}FzjsT?rTN5P17fl{NNAjAG`%8 z;!)SDCtE8hAyk($vhBFwYOkl%7f6zROa#3=UG1ZPgb9gSleD$#BpDQsdm7Z;M17Gf z>Y~Z!nPe%Tl8Nh#u!o6XD7J96Nu)<))^~~nNHh1vPquo}cZS@mk6qQ6vq+-oHV37g zwVW(Aj{8B#DIqTz>95B0{cRJR)4K++IZYx}rBL`e-X(?8) zrG@M&<2{HGQ*EVB(m*Qnkc-w1k=(>3>ZM2uU28t+1= z-s4qUs23*}CAx*Pq$BApqk$)k2v zlqUS~y4PRWPKgU4{MIWdYJ@Qp-IzT6jdOats0Lys=?1ILwbj}(H(G=BK~@I6o{!dK zFQmWytAy4Ucp+B!8)c+Oqd7-zW*Tn4BSnXEz7SwC8xEHzEHpe|tVb_BNiG-uI@8-3 zzhzd%U*&tW4}J{m_5-K*im%zyXac3Qb0S7CS(4Gj3=nRkM`j3h;O(tV6ZuN~zn>)SxI*-H7(y ziEgy#ZQSb4bAIpd?=R!=&?DXVd7szoIoAQ;ZNFWByxihBPxHWY$ux(KCsE^6DJJVY z9ys=5Klg`EeMh(Wh0=ues=U4=7#zUu$2dM7N>YQRFf=QEU# z^kL@)khSs#xdTI!-}mZ#)*0fq2s2Wr2)DW3^MOB9o$%l-FWGm})Sq25I`?(FW3u(0 zR)uql77owPW#`iw$;(q2^aGdJ+7AtVyg$*IH(rX}oQbKrrU|xQ%%Z=NAgRnnwS+!p z@?-^PgM41xAek3+Fv0?k(5VhR9A0JK>i-KEpeLl`mc%hzNCJdMs%IdCiw8^@irvifqcqi1UI_ zTBqSSj~h0Jw^{b5ofi(EEH8Kz$mVPK)UOCEI0zN11}QIRA*e2P-7Vs(!?P&&HVG4=&6rTA@&3}D`IZStmmpgG7J z`fzF{9ptZ4sVY2m6EwXpSODx)7ChrogMZJS=K>r;r)O@ zW8G%|&@%#OZjREA@hAdaI(+1J&b!6Ra#`5vmPV+(`P`ijZ_<0hj?0_WL%QM9y9iR# zgI&OZS%;JHfmWTlNA^uxS&A5Yph4wlan(O{-{7n76NO4~DCMOn*r&AcX^VGkxO?)l z=_v9Is8!h~FNw84h2kXNvMD1f5xK;Uq(6^f4G0K7&w(F$%qoFyH3qzwj|+lrs5M;< zKE_S@(?ZK@Gs~44(`9yZtU7l;ZrDmGV#KsU+2hEK?9irg-C+1hWV zi2Eln&W4;P)$QNplzkm){Q0KER=`ehY*hnX_%3v7K4;;BDRK)C)-#VzPoc_s#*ee^^Nm9lo;N&6OoQ~=A zFVJAiYa2f~`{(3^bg*EQg#&w6Fevza$`t^eBq|__f=;f}QjZNhTj!FDOQ%IGyUh8? zwOfU^<&8LKSe5{?-Z&qqen52p$e(J0icfJ=@|KB9!a!+R=h0JGq-L=k#xn1z;{%Dn zf*$53M85nC<-k`g7sf+ELA@LH_R1p1+9KA7XYL6M|6n@YKXJR&A@;id^z<8?p<_U) z3Tu7B2JV?1Z1=dbnG+;IaI+i9{fc&Q!)gpt3O);@Hi8Zw+``3q9O{c>8wJ5u?$J2n zc}y)dzG^{AfPAB8F)C;K6saO(kua_r0_O4-RX2im0@;Avw7+R;t0^xB;{ylEayEfW zc#5ZOBY1BFhFb);@R~X)dVE0gC#bFmjQrHihvUOpm~A!+=Z)9SAbLAFBwV>ZhDvX< z!Q2JpCcHuF0ruZ&f(h^3glQSo{2`DqETaznMRJdVjRt2qcz#Fp(8x?Y1J$=-D)?hV zchd@enpqj^)wt_0Ua?#gP8xlf;hz&dbZ? zgGlf><+m-{YgJcBE&_o*(Z1EAsGP@i6P$#80WufD?-~B{P zx`+T+phm}j5nDorjDXVKs9v^(;7!eSspC=GFpFi?0~vqUa$1A0x;X|pI6*mMpdGT6 zai5hzsQ#5#%8`kHFZo9>diwqKh=kyCZEQZg%V?2;8@f@O=NMnUG%=F>?)YnDlx}Oh zK|6>8FSaE^ViMAcsf8GE<88YSCxRW2!R-0`=*l~XO-B|U%3`-+BfG826lt3WC2Za6U)n$?v(U13&YePxRYZ+3*p5IT> zo))rdn710GY~bfL3}~$97pb9Ea+n)sO-S^N%x-ec-66xPlMqjXEa|E=xVyYJbE9}z zhe+;e<7MZr&XuyC7!^!#oG zj*b88^L70AKDu?s7$O4Rv^1ry9b%Jo_dyW(;ZWlKuP=z{%KiIxX9(}zwRKUse zu-n?GgRA#jhqped?h|lnlX$uh=h75QbpENThqjCpu8xx`_S$RU;I+{KNkIZR$Y+ZH zKhSVnw;4dcquVm!wLF|M+kZ=W!ZQkz3d#&D6M3`VtC~2+3?tM%udm}?ucHL$g=Ob~ z(%_D3*FvF6VCAf2B)hM3X3WbSc5g6|)+gBVcPQtfUgdd8g1aBC7M=pa;2eITv^I+K zm=Wl(mGum77zVi)BcA6F&4XFOAUqu!WNS|?k**6QW#_GB zjL^#L_H&N1iHFe`HpS^1x00Pl^h6UDbzbqL|L~r{zn(X(qncBejVm2Aa2zq9mc0BY z`wrWoGMs8UO?hl#ILoX%>0pG0!kOmHSUw&1Qfy)B9+bBc2SOAgiWOH=TS;-wEAifzx1J}YjRHIm>#%%0m{NfNYRj7II}x{)i|5_VNxg=on#6DrWQyjh;kZ*GB5Qj}@kGx}*=DtAjTUEnC(Zu1`gT z0VGK4Zr|ny?aZ}H8?ta3|G=Mv@)q%z-!ntU8&7p*a`TP=uUc(kJEZyF1MHT35vYEHIrb<{u?anU zP8BeX^N>TZ=TPr@wM4av?C=TNHX$Il1z2mJBVw}^hSh9oprsCG+nhmtTG}=n#+{%0 z8OIuAPx9mddCUNAjEq=fg8n$xF&oA}hPHtGNPP8a2q0LqN6h-^1lF(K6O!N{Mizu~ zK29K#9~l1c0tR4qEnWbDjDbNlmQO#ugA@>4Kzt3lSN9BBmci|KE3}%f68wwO9m_mX zInU!64tU^KC~sFMEbC$U30}yKNqYrS#|a}s;0^Qu^+8p~!A|UzF~g<6bE)PP)ynqO ziS@%fXS~~d)k8tG=BvkyeAhI??_Wqs?~VXUIIg;8pYjhN&TQJURZR7j2XQKNW`qB4 zq3^ad$EZfU(Z5KpN;CuMgk6Ch+fJ;gs=*`B##UIW6GXJwX+x@m*lJ=cE4{;qYA_MB z7R;V3lH8}xpW4xu!y3Fp+f{-xw;G5itx$R)ZPC+~n?Xv@Hvw=&+IG>OR1}F?ECy1;=KnQ)_ai`kurN`!=ntr`-iNr*epLXsz$*AKVBQ zDh+bnsEhia#t0o%9aBXY6(1}{O=J{09J>Om%>+PsL*K8XrLrQr{m{2{?guz`VnpQy zu60@zYVM2IgR{j$%@C;406x1w_3eCznFEl5ufNE)A6H~MM53ftJYckJ0iU;y1`dmK zgoi??%(JT(ac+Yu&O@FWZ~4hr00v#5V-BJ{E(NFKStvYFvzUk3e|{XH&CNs&PoBrR z=Qu-%>Z{R_AL44?%;Q0|qHnJzcwZ|GqL+v{cINJRH!@Cpqw!u_K)(zy3D5-IZRu-! zEvJb|*cG>BA)tR48i~8<6SDYd+}&eI(CT$t1auKAPC?dNg}1nCb`6Hb#`Dxxg=#3B zW}$H1Z3d}caCGnM-DR_&@CW`*8++$4MuJOv$|DFnijWi;9>j7eX+YsBR*&cJXF}tJ zIC%u|{OxldI2ubp%!5xB0DOovpUb+t@nYCq?Es_~cny+d_SDRQT-|*uN?+e$!<#2FL zaU6M_-N`lB3{vNdQVn}hi#1BONN77_qBA4($vJ#=o`VZ`V*!-HJ@ejQJixVqyRjsW zT-|$Cdw;kQPZ<2i54_B5!g}gFtB@?iHt8+zNb#zUaRjd? zyB4G##Q{&=rQ~_jZhBr6+$nMJ4J+FBSH4|u0H3f9JRH>E2Sa)1ma(>%iX!~Otgrue z-OJM;pIUKVp<|MT=?jq2XjKc^59t8$UUibSqb8oV4;TSE34e+;SKP%Hw{_?(t8Ck7q@4BXjI) zSAOss}O(S-TUdJB+wW0tsEHl7Nw)p#0qh zLEqW0_wihY8t7;KVf~x|Hz6aC2{&a<`gl<(`)4E2hOV(|4TSWdx9H-0y;Tfk!P*%HcX9Y z1P-+X#^Zn(=>!Jz{!f)QLVdL(6EVM_iJ7}9AIWVY23`&X+MUZ=;I&XCM3pbWu2lXZ z*r94TGBh>=$L*Zsqdr|yyx)OT#L*CExb5+VK8=oLDlM>Qh1i78k)=8mF%MqivG=Pl zLn*7(L`cl~n+uz3iQdm~WP3i)Of;dl1IHrMOO6WmjzBu!ZqF39*|KcWT?|y-(<0imK%{41fmwi@h19EyNDkK?95A(y;B3PN1lON6Dic36y*2wTuLflTt4D8A_@H*F*IUx)(ui54{FklsPl*Z$MH`N%BHdFAxv zw*Nu?@AUZK_o&TH6HQG7Qk_jL;Zw~+t~+0VU2K>FGNg+7MS@{im^iz2PJxH)k09?m ztZ<<8)*$$^oe@fCrof@ahWYWd=LOs3FJIM5~#mMv&(tTHe_LSk|CMq*BNj* z8{If}c*zW;cJb8U8tfznT%ohhy(V4(5fBB&nNFw$m~;V-11c-UGc?P~TlvU41po|b zII%`?v+sZ*wUEN|4ph}Soz3lnX%uZ1U||y+Plu-X)$zs0`w`4Jae{S zV0i_Z4GFDdB(}h(e`U4@xmO#a!o?6rjo`QJ2o-SWe~!{VJM`vX)&2yt7==#~2Ip(2 zN_qXWny!^aEg#3)JQy`OL^o_(;40ED&IgEm zpArP))W|0)GgCVeB){S%9Kg0#e$lG3!c}v;QTqEK7jV}?7Q{*8V>ADn1H&dVdCz}a zU`sHiZF-V)0H!sA*5ubN*KHJ`=P&a}P12Xyz8IA_ zP(14{M1r+oze}EF5l?IZ(XUAtYADZH*T8~*o~R+&%%I}>OWC#40KDK=6Hptfdh^>} z6vs7MrrrH&i+|LL9o2^Kf5UF9a61;1xxEAn-^C7_LXx3>Z>`Txs_Q2oBxIkToxLo( z4TB6^Ik)S}3~rrM13|=@T#sj>bPYlBKs;Ca`vce~c|;j^gM0Fy^^VKSQpihoHZftYzX#nVF%&g%@mp{mbBxgpe) z71QIyQc%9~gsKd=m>~3_kmZl4deX1%#V9sa7I)ExJYz=gTL;ekn=PFBOgKY>uum=G z`D>2eyf03JEb~8LX^2h~02fM#+UVa06#F(dWCwgp7M-EM-r}Sv(Dnk8lTI}sPDIe$ zT)=-az$|sS2qT2Rf*-oTis{@V`%0<|E%AL8ZbgiQE;TGmwlWx`G9*_qiogYnuH18Q}fV z^Jqx^RS^x`%#_PYfnU~rtQozWIN2csA!?w+S8=IIF>&5hyIK^qO?g05hjqu^*TQ#g zO)=FM1zA?(;^0A$M#E6-8E1_pox`BtpNF&;UP9AF$+xM61t)Kf!~F&aM>9^$0Giz3 z#DxRa{2!yjvzE(I39@h)yHWW|D&~kIu!q#uaF_}8E;vV1XhC{D9SZ~|nKPs>E}-yZ zI_{<-NL|0M?FC4b#=}+m%l1#0F`n-P=QtinT22Uo%LbnxKnMESJ$F4Y|6++McM8KMt0n>_{UGJa z6>_>p>gIK<&~>JHqPWeXpmh7e2_(}UwcwP|FIXUr0KV?~k^m$J-ha9tb$I?^CE8nI zJx6mh;{8unY28SRwz~sfOpYO$x5kS$a2==2KlpJuo0^G)?~9UCOe-6xScK-R%bm3(y(r4#dwwVRs%6B0&orC*0{>Pt6(?`!3Z7?CX=?oX zHCaTj|L8R3 zV{h}9D{T(21y+ zzob*x$NfHA2OUiBtT@LaAG`*Rew-*-aBizA!4y3->kRJ!95^KkCZROlLG>E;?K{5M zx4Q_WstJR7n2K|%80ePZtH0iR?Zj05aHZ|2wqYe=6}?{*RlOw9ML#9-c7XmuafN$@@?)(XQ|ELUJ&~MrSSFI=b zU~tlRL{N5OlGdAaoNut}Czs~DOiEf2Cjy?SGy{_{pDGiTapx z15*NiyPK|%RG5py^=&ZU=ZT_Tw=wk`T#xph}XM1 z)}ai7>U?GYwz-$8WGs6#AFc-rSL5qYLqin_Lmn5!7oN}K4FAJekZ^Y;$9;X( z+2t=XnX4xNk+0^LSVG1TNVgk48v9*#PL*Q;iyu}9;la-Xk)0vAv^I^2sDo58c`@0u z$LB6K8y?y|D>MQb8G9^eBx4*YpK+5N%p?OT-euDpS@mU7xr&CDxcOAuyxwz`+rpu5IXcF;E4Ewm@o92G&vGWHTG*fkuZ@iH-Z5N>r0^P?BP|@}GnI zt!IK{MFFsF+i0;hbX8mP39ww;;nr^*0TV9Ja~o6c`R!CqIv_dP4tqEZH^90LoO@@G zD5&%87BqALyF{YnOz4dHmn`%?g&R#lQcol7`)}D|X^ZsEsGb1km)je_umVc#XY5lw zH;RzHvtbI~s)7Z1%Xk~}z>>o$<}Ak?O`-9)q91l*TjGeM`Dc`lo@_MkJ6CimpbIN_ zLaDTLlN<)* zl2t!r@rWnp!Ik?v5$(EdMU!7t>$(K&M+-9}zh-QZ!#I{+W6uo8u<)b(ohaU8DJV@h zUNDqR(DY-`u=|AzB@RPaO5uA!Y{Jy<;8t`L=jfD>Ma0&dVM|11W;i{Zi(m!x|eUC#~7(JG%m^$xe_#V-gx zil3qVD(vNcpX5^nWLA_h&QW*3ruXb~+<}adt#dVuqK{CT^2h)h^rkWC8T;yLx0T5A zK#%+3FJ0Mw1bQ7CfgsR#wvk7$0>S&?x9>m*ByW%!kcKClT1ROxW-sg}PleRx(pbf_ zLjIUz)G6LbqQj{3IJZWFhET*pQr@ime2g#z9BWCo0UYyFk6?St?<0Gc2&WiPa=`N& zVVYZdT<5~%Ml=Y{#(P_$YLOQ>eC3CaB%?M~Mk~GZdnBowa|<@xU10^Ci(r3aZk)zxI=SOj(hm9OZ4M`?8vtn%413XJ`sP%DV;<*toNs&fnM<61ofMs`zX>+v4(_HY zrnweLW-U$&DPKGNuNkS{NX3(P`j)Vw7Q3v@Zz6ydSSfb;ziZH6`X+)_h;w`|9zpt| zb@_!UA2&1>DrVqvWh1#03-HyP3<<4jX^Zz8#bk>C9t62mkqqSwnztL0*KlI+)XO0O zknzD~Z0L=bSTDjd1E0!KV}ge75Y8c)a!$p!N8&*~w9WO+Gc2wo6m8SGiP zb$8S_@*JE`7a+(RFQj&B0t9o;9Mn`@n}!ifO@;DO?og|K|e|76a#78$Y^gB z4UCV#_yoxTHgLN81{Q1u6mhYW4Ov^Yx%95oNn(2WchH_q!|~sxV{{r37=<=~YH_uD zH0|QjT_?+l+pwY4*J7l&>va%g5|Gjp%hx8B>qx_Isj@k@;e9t(_r`LmoRA0ud6hfb(d3{IZva9Y}&O?FkH_B%dl8%UTbU(4feL_~;wP40A$Fen3I%PV5_r zl~x}*YbpFZ%A#KH=*A+Qymo$)j{4c7p-))rjn8P1QrzqhHZ{_0dfFC3{avE7D5Raj zz(lfoSshlS*^mc-{1SpljzoE|AWzXjHFw7y69u80!6UyKVlnVw9B)Acf$~m2j1Lc? z!*!_{p%3xo+KADA6L)!mxmX4=d~EKzD7;NP22Qj-yrcbun$d%sAsu6#9gT#~$! zH7{^?;|ex@S)C)zAMC(aO*_p{9mnW+`JNNV+@IQCUK6YO)b6yD?6d9>>a!G7hQ z$FI4!*9LQx@HRah%X$M@-s>H~0W1I3C!#G;P|Gn%V#mwktBNDAfXq9*n&BgJnsO)* z`aGL`hf^fE?&%!c!R#ogewgco3PJ)dY9FcgC(_K$_(1p+s)fRP#T5j^YXqrcub-`M z!Q&@!HvH<_as2DkmSi3;K@v@^?f36vmuaOC)eX-070cK=}^c{OD#?)b?TVlHO1tkXpLI*m&}=XVMt z<>!j=aYyp9q5?^bkMt@d@YH-+;U~RMC^UNGO?gdnjY0VUp#w35F8pm+`7+UP*!j?c zrUX9$2Mml`d6e{efACpy70f>0&i>YwEyb;`MJ^N&Wx&^YX4I9{eP?D@Ra5-C3CU>x zs(ZZ}2}k}X4bE(lYKkP$##^Ah5Wd?~zHLgyzg{CAXv|(O2#hekSb^$ss)`JDxMm!? zdPO_Au`M=-*n#YXKi(a4gd<5WkExEm?bH;-t4fWvmvH;^Euv4hNx5}sa)V3d!>Mhr z+EgW6JJe46(mSHEGpMB+okmRmpU zj1*oumAfdv?O;2VH=BDMSJ%~%>KYy29NG-JVu|0%uyLVjSI zQj3=@8;uRPZBS!Tfmd;NK7dV3ERfs|2$YQ)Z}Hsswmo@;D^h#x+hLVakH*Y%B{sUF zo{f?M%s&GOnRVEC<8QV7{lsZhiSH@P=hCqY!=F`9i*X-5pj8V4Hd8yXdj89W7iyza zfT81O*Em2PF+3nPe$sKvPhp%>-i*B7UL#ded@dhvM+QO9!!pRq`_OvPwrd62*5k`LCbO1TYr*8}_`^ z$K|@b`C6cM=ZZvsY0B2*nFeS%HoI%F2qsaJL>uxUSgV^J;?1f#Cch4lW01FKo!+YsBU4CZmhBs% z_Ln~@{0QaGijL1FnXh(3IYK0iS2zUj_NKFe;{s$16D2!-J`@IL=j=~K^{5QzKpj?< zruz|aVo@l13Hzn7g#GmWPygeC?_rX;g4HPlQoL0D;U8k4%K$Tk4)((25MK9OH?~>!M z?qQ*>vboBm|6`C`=-AcW&aK4Qv!u!ep<^wy+mQ14V!c5-)T3hJf);BGLfvLiFILWL za&0#EM2nZoyiLlGOPpVu&Tk>To_226hJrF!xnZR+Oz1z3)a~<=J$c8bISm94adteheP+!2 zbAhGSuS+K%wv73uD@C+QWOv#03O&yC%hgI2Y}D}htpD(dwG?ytuCe0EGRN^=dGng{ z2ZHwhkj%=xaf*92Jw6I2b3Z&rOzIFlo_ga8C$mcOMQk5>_qHDHst?>1;e8~@ zBe+SnxLDDeFQUPBtnE*u$x!JN{p)ufjz2VO&>DK9qKWl$!}gP&E~(ED?U$R(FGD{c zrRK#rA+g!vAb0oaBeZu?Tr45)`e}Y=OW^ z!=LFK2vkOdprU-*U75%kzzn>Yh}`Ok+PLUE5OqCDcC5x53a|x5rG-MX=n#ubuMd#y zG^N?8Id`k(PjL2V@#hE{9{d%SWl%8~bR#-|hO?xbUuHP3iwHenFR0dus)wAC&}|;4 zag>+-+=e{#dSEz5+(u#%5m?Q4Ej`N@t@6<(d%`c|Iso}NG34O^ zPf&e_x^L{7mc`37kZOV>7o6I*bgSDXwo*|6_GS0RD-ZHa-u1uV+>8Wv6g_EC*#P<{ zWk8k;*lGHX6@N@X?ojA<>KNgBJ*D7mCi5!e2}^DTf?=zik?JT_9Q^2%?xF<*y?CAseE!CDcMEpDzT((LdWajg z3=JDV-kD)6U+K_TY4k&$i#kuors!f}R=hO-m0Vjhx(0aT2#K*QQl$R}gVzUoiqg{N zgu&&2%&z`4fU)t(ooMGUvE17>7fqOFrjd7N{6J zCv2@KMyj!kkAXxAm-n)Cw_ld=lvC0)j(IcO785B!YO`Rhh85#10OXE!%)mjX2JmLE z=oFylU6Bs~ZMJHHpUEjjl*3?M5~x0bnQpf|d!bUqC>>YJI|GZ z-A{u5w$k)l^&?47R4uf5QDM$Uh5I@sv!m@@8P9k_n-=N#JT!T(T@fOCK}okUl1sNb zQAbrcz?a&|+pdkGztd&nAy)go!Peo;d<1>f3Pxg(%5vehbyMmcA2JlXX@DN(NA|9LO3j8Yna%f}{J9APt*=^k+heQ(vQDD{BIi8TT>`7_B?9il#}7F6Q33hQa)duO14Vj^ZSQcC zT4;yH75cbm_Jzg_)mhW-y6Xd@JG)Mf5OVG63iz;UA!B?kQ_fbz186W7C zN7G3VkP@DpBo2^op}HFQ6D#J4dKh0hi(muZa*+*y8)jg<%Hvi*K!gdzb=kVYW%HX@ ziDlvVg2Q7nH%91#cZ%;QUH@}5#VxXVu(ToQ%%Jg*BsV{)q9B`OxmVK2UiO*2W%gTB3eB}Ivtkb}a$&=#on-Vs1} z*nj`jGiG*FuhKQ}bZbF+E-zC;&-P?jz{TA;vER$$ySl$^5~f+4?FzW$Y5AVHY(FOP zIr-|G}QuKPv->rjgwSblU%O4@Z&q<(hf zcns%6N(DT4Nc3@Mjs|POM(bnvQ8ypK<2X4@FAsBBvDs6Z-~QzKxz;I84^};62V`}g zGv6z=_O@Y-_f@;2zUrdvz}x@F&ydosvo33l=n=TDxHWddX&}hX=SaIdy)U>r_ijVYAsYu$s1CDZLsWv?AbTNYw2 zZxoEmXsTanMml?@>i39%qlj?|^k19SVJ}$W+igtJeD-Gy>%@0L#AyoC`}pWn0+;mR z(RafB%we5Z=O)v3`>h;4ZA>)&WGxZuvGE?N+RHqU4)!h8JE6Dp>3L01c2%}m^0;`^ zw2E`3;ydk1HBh5e(B7DbhT{2m(yof*M1#$bJdA3M+RK<@F*+RfO!`=p4pR!3G#4iI z{|lzOg;H%&zWHc6>VFTs*wD6Q;2WoZoV&cRcH=zzT@q|6OsZdd&(`M^*+BU_fd~xR z#;HkSp*jKs3rFlqp>$K8)O1G}l4A4E*AG1Cy*}zS_bMQBo5el5!%Fo}=!1>5L#@&6 zUy?IYu5-5?KWZ12K>F;Jl+Rizo~q%7)Rtt}Ps5Ro4S1$ZBU&6a8} z-O(1|zbhk{rB@d1?3JOPt+$2$^Dmw7zk|P~$JTe1mD+fuTOK#`+AkNBnQ$b9=gm}T z=@I3()av{$-u|6-9TwA^KUza3&zq=+7EK}^`Asld4M#pJNM1b^nVQ$|sq)Di6+8Fc zx@Ea*4cpY0DH4{qM=eaD3Jr?b|5qU+U#$kZ2p@tup06jp#$t&s0HolwN!}|pcMxxu zriA_@`aYvH1xt&OsQ)5{ZohwC5!PREl^r14=|-~Jjpe_LNwpL~rmvYUP~j)a9P z-!xS=USM^(p7KVoh6>UTC_-Beef8KA@=go#81?kg>h73e-_-FoUt*7VwePluG5T$f zV3Oj%2A2+g#B?<)oE5d^)~*KQwK9l9Nwp?zhVKtQX4|NRtfOSqw#gz1CY!_L1;bLz zv;yTHtOBSKNX9r}LU3ibrd9skDB ziKREmF(=f}2=7sDC>FRIR8y`$pK2SJ#6)RD8P$9<^>yOn=mkMCVkJI|H5Nz&M}He` z+rs~7`;Q5`h9KJh(JyO~xcd!3z0osGaA>;hU|W+#G1>sCrDO!rN4Q~;ch}zRa9oId zsdYD7gSQV>$x_IG3hNSJnH|i-B4_&jP2HrySLlwtHb_iCf8gYQ0CE=f&~l$JB06Ga z&L`-IR#7jj)DrK(uVx2ZKucJpcrzsLr^UBfKH4!}%GF^DbY}aNw;MS6`FiZ^0mg^F zstfJMKFv-PZT@7)A4kxyAh+1N;<<$V^28c)6a@{tI=!sCWCh6gmLzJhq=ZEhBfyq) zIO-Tl-~#nEQa3YA8#>dKzwY7DXzV#QsG|-%x=0Mg`*^y7PNRD;5nY6s8H|8vK`Ja8 z>T2oNhalWzvfqN#%uO>Kyp!nZ<~&rc(8{gZcKFBWLvOAc$-z#FRAvUd#-&K}spq89 zNs-Kfg%4wGgS{pOk=)~d>@C34M6_4(2@btI_^Y9-<5*Q;uM%K>wbT7a+v!Fa3D5v{ z%S)rPFN6r`{+jtYdzB5q;3W|oR>(2K)K?}03qm^k)=7KhrFOiMz4Xa*-uW9>=n$KD zRa*L2sZA0x6OUv(yDlkvtsuIdHuTd1fic-3eUY}>Tw5G2DBxg{f$8%9taN>?o{@|h zqZ`^J7rY}kx-2Y%b44V{0IDbjwoV1DS>3+qklhAzmT@DKlYpTDU^XU6iE1yTlRGAZgLSu4^w2evFTxWCTfL!`lIn!|o@r>;||SSy8K53l4F|HVeTo?HETD z0numY&9mQas&?-b0KpWiMD7{cdj@qhlD6gZE9qE}^%@>%^v~i1z310lPcWiX44FfI z+Cji@uK@jIgeFNTn3NaExG+{tke7#~M3tFDsAHEXSf>tk!QY|Xm2>DnYCv)ViTO^h zo&@dG+Ur2|5EzO<%tpNMMm%CcJM z6SC^!a=1E>E)Uw0gq@OgTPJNes`>|Q%8jC_ZnbW8qjH^bhK+brYp&lB>^OUJy2l)P zk|gd<&y4aCH3E%z7J#XpgdT4?q~V|#gQSN@8%Ts&8}sh_sKf8Hs!#(!RzyiYo-5gO zlJ-s|A(XlCQy;OG_&pK}vlwwW^bU&E#Dlv5PMd2w-ZW92v6oHoxJUh2%d6KHCRyQd zJJB2#_6M48>K#hHkxXSmxAJolbsYb(t&658J4m)aLjW0lzA(__MD@+jCj%Uxx?dnpb6#!j5HIJkbZsb2ZI@*~ zxhx+JePx#Nry8Vah?bxQCQ)iqEkzC~Oca9X%I)@RM#*zS!1?rNbty zV=V)tHyU?NjGqx+`s%D{`kt#>aZm^Dt-*DYXv&z2fUSn|??%rH!XFgN-YaWo#=LtS zQ4nF~<8@|7uZu{M*9GA(b_1V(b-gV0HELe^lo?ZSZ?l&|M2WuL1?hjxw$jY#cVhOd zavqf^H?hCbwH zZWb$RzlNeOJoaNjHphL6hpr}jTc38U@!?5az?CW`l7H9cQH;J@#Sb!4lZ$5WC)XRBL(hzP5cb_(6*oqG7?@4-X1 zLwCX}23dbTh3pr&^XJ&#KV|I_RP#CO(=mS(=WLqbU>_KyK`lyaKMbygL6q zXp`}(VqF6*t{F<&&V1OHjO;xwI7&C9X5I@>VxP1vI+DRL-TpB2I-(nL` zu*ZKboclm7TpREl(4rk-bjy8Ej(^T(#xD~KJb03%G zw02&vj-GuuzB6p@^yH`UpNu>EK0b{+sG2?@E7f^BNvb0}KbysyYyD}ljP!kdamB;- zg!#b(sZvR5J0Zj|R6!Obx&!9qWrpCF*^S{(E}|3-i6E^>6LZr>95#cHCoe z8jg!7`3fZ!*#zX#gH6Y~?$U#Ox2LpWBn`^$s3u~$vUyT z`iLd>%PO2X9gY5>6GLBbydBQ=aZ#eiWZ7KcPJHLGpz(rXb6h8y-lenPaGURo4f@q* zm_n1M)lGTzS>dq1KKRl4BUOXcW04%ui7ltQ9GprhU*EsT7uc=0uQwb}r~-$)Whn-J|=IUmd}!I-QnE9@xn$S7clO)m_%6 zBR{RtxV8n06LFayf+~(c*-^Y9bDyy^t~sqER$taVqIiH%@`WHd(LR>$eU1CDwPb+e zoPpHn*(V&`V?&g9dwLD?x_6CnbcY^mWaY|HSxn{eP|kr}GgMx*H@2!!TymgS^3CjyG zIHHC7mrg9_Zu=zDxf1mRN$PdkR>*U$&mj&i8cmQ4DJ%XqoujFQMC@h{VIqWQCIgLo z-mfHNIJd3OyS(&qDuJ$(KY?GOvSUb9y1Vykaj$+PXkwdJN%U7dO|CtZ=LDU*--Bd`>Z$Q5U=`7Xd{HOKqx};)e(m(Hy1Y1XFId*T8U^ ztoEGj8L+D>jH_?|^$FEIqv&kbnpZdH*{{)@cE+0Tm?xV|Gq<@|hRGaR3d^DrW@1N! z{4mP|vXAdUEO`USyWRrBO)e2S$rV{kH z>RZTeh~KJnA99xCltK*Rg=}jOJ+jE*MLa=X&-E$5{=>4;zp*q=-G2|oNNI&I4DWF4 zu76SbpDj`f=Pf%b(F4xQwAH)!T$I~|g|8pyOZ8-yOj{r-yw}rLwqM7;nZ8m>+n@P& zHM5)QuPpZd$>IKwgBQDKVRn*TC$-rk%w{)Io~!z{3Hp%N`2cVw&*4PTL6x^w)~b*c z@fUIZNmqR*1qC&NuzFyu9#d|@v(4s$JBn=j*Q+rGDb3&PBi1LflN3e=YTSmt8a6xh zu;OFkm7IQ>)GdH4rxTZNU?=~GJ{2SNGlbHXiGAGne+l2m5I@%cygM18ldENO7KeE| zW*K#dh$$CLePS8Zjl8f8%~g}8ZYUvdH*)mIL{zpc??QNp;)(_F-4Gh4OKRqXL3bSW z3ge+Vwf(Poj2D*8DMSD%t8-3l^_NZSe8s;f|ACH>gm!m?@hin&&oeS(%mFJ*3!Z2Hh@?iqeUdlep zJ3DPg%7A6A*U``?_mx>Uk`s==q+^T>!RCilxn79jzmF7Tg?#i^7$A&C^UjrabK3fD zeQX0aBQwzO{0pBF)}6~D72hbp19bl4HiJEN>ye-m6Pe1Zs5I;Axgs*CC1u7SI=dQz zh*UV2D(bQW)W8`}0IOyU*N}1^M|)30J;l)Q>jMC)@1E8$`?xE{;Q%+cAaqsx;C$Es z<`_1_&Ys}x@MhnhG)B&mU4MS2%q}_X4&v4PQ5Og(RHSxub;+1G;%|r8O+W5@rRgO%%3?^3V12LAl`7 z66BlsO-0B$@URDG_*CA;9|RStJBBBEmZ!e#7s+geFnrz2cvq3 zA8>82Pe_}Elv!Ou0|#cWa{rH~?~bSXkN^FgV~;vyuZ|H>ipVbK*gKU@Xj4aia^-CR(z(2f;%ENeMz zhiZdKfglOrL_z-TzOiny)-@aX!hpqg=taG7o7T)CUfb2~_PM7Xes?TnYUC=M zU0#~_ik0(rz)8kInPB!0kj(+@who*JS-@(+sm<0NbR;2f)_n?V-4*m%C(GlWQ@i*Y;r|nstO!`Z2)|h4}kgh%}xZ{2)0AN^w z8h1wvAJXpM+Z%0TB%%ioDH&h1dd&0RCbXF=z21XM=2v6v!U6xK3#$)Yj=3zf6Wk<$ z6dTV)H`WdXF;4*LxsY#r`ew;e2#>FJH^x3XH2f+5GO;i?O2|( zG$}6Sp@cC^6^Si~{cdpBqFu2%WaRTmajUL)eTRL4AB5GH! zy?(I#*;<#$9a&fs8`^%|P3SBnNQCJ2^_C%Y4BD8T3bc!hmoAPMQ^_PSM!CY~(N}bL#0p zZbZc^MjefW_oba=81g|U5>)5~fKT;LFQw-XaDYKf^ULSk7ocF`O5lO3qP0MxDll5L zty7f@6E4^H>|^aqsn1S?{#{5l;C}Tv8nDC~LWVgyxpZFjX#X>qcc*w!4PxHAZQg{> z*D*Ic`X1iDkDS30*t($kyaU7FP{+hJENC>n{?K-dXm$R2(OF7(V1E({F;cRQdB++u z$7T|}X3FR)Bpj)nZoA(1l$g^9*+i`!@!L-7rhK7{j)%; z5UtIHgQB0N6MN#9Tp;Q?Z~*&}V}&{n2>wp{DC8tK&|+ZWMcbU4lHC(F0ox(VFzk=J z$LqhHe0w`UclQ|++Ve-!gq#1oJQkL{3BN^CJs<6F(Lq8)2QbG$xf^)bTi?G40E4fw3ky#rfE-5n*xwu*GZ6O~Knc(KQJo*AkOVA!7*#pc)> z<29#gV|*}mOlwi6sTFmlE``;x9pLT?iWy%x34AhpaX2l3Q+B9!b)k3wjb4!UxmB@q z?c^wpcPwc*Z-d}^jW$Dnt`45QO);g<}^^=0X@#8ccL#{%L#`-3FEp3xbkhpT|INR0xDGmz7_7?<@Xw$PpmY*M*BXtS7fRG z8k?DjDfTM(;ZauczW>`*@$Cx!L5&lC>B5}l=PPo~(LM&rlD`Wd4TWafO>JIt;Jrj? zZdY^y3re14)bmfpS++GBZ-`YX4>ha;pF+;a2*otYi_v++KuIx%VbP;Ux)|hY0vj;I zNZB#rn5LhJ&!576y!Rf;gdX35SUYIl*`+?N9dntu(Jw<3p+?Y4a<#(ZFw%%H!B`jLEK66!u)Oh?jX3fRh-8j%n~?^{U9ODp2XXS8E1 zfYy^CyuEPQp+OupmDFv+&+orv=XUPx%2))L8kMsdk}9yYRVhyYlpj+5<6xdL_0wF6_AM$9Yu;1 z1^*ETpJ+dQxpC^6T4Kn^@7hX;_~6U;R^IIWX>P!m6z|Oh4Qq~zU6+?RA*r$n&BKnm zSWFN_9G0Dl#j{j_P9p65%x^|g7;#`!`Gz2wtPIUl>sseS zPh?Xf2Uaq$oTv$krb0HNp;gh@u zQ_8!i4t6C zQjWaGkDaB80J!TY$;Ll<36TEiO~ks=3##xY@9tHI4OU#BtGO0qg?(0nm47ryVEcUN zc840YT8O$$SIq%NzzMdS8T90-HVmV^%@fA6T}SGB{35gaKSv)i;5Ttx&*Sz%Zj@|3 z>GMuCsv@p;kzbjcfBUd_Rj6MXrS!Q!X^RSE>*msR!6>PWp0|m|y_5cO(GEYcEJ0Wp79KsM#B_u^4{gT^XuA_`u^#qTu47$k=utj}5a;`^(o1 zNv?j~0Y;JLca&|i!X!wFf?A1?Nk~=6ii2NeC4B;Ta)QlAsKhgkr-#zD8>XpQO8K1DMAa_ zl2=FG;T`tl+p#z+B%plPQE4GbfDlb-?<;0~sQVD+MX?`U>l>Q-+2^#R!W9y&z@-c% zNc0r;%Q&8YAQ_`Oy~McavS|-|dYTvMwNXM$$PA^)b@pQ!w&3Ous=DaR5++mar>IvD((4Q1Yj{u2p*hhGZ8HD)z$N@t*D=pfwj-92u4Pr>F%Lg1wYTH zJ6C^>yPby}RF@~G)sHTtI(d|bTWmqPn8WI=ohgX_0`Q!N>=Y#4erQ0t$Akrc7(#I) zm6gZpFR%hSuP)ABmNiXWWg^f92gJAXQk-rom2BMB3U`^=CNIL4Pm_ zQgf9_u!1jkyY3=Dn2!cumVLV75Qkbo(4WyXQ3>SeZL=O%Z?-Vr%tG0g;6R@ z!xv50OJj7WTocHX;!Z2g0FQ3*0O+voug>`O6!m7uXl~O-GEl@H8+o+{YYv%x`_VRB z_jN-gU8iC`{0lgC5*NuZwPW14wiLnfpZVTy&-(eGJ7*t2s0?6csCT8^8;ThuCumVYZ z@BDnH9=`Z_dNNEiB}3K`_m3h9vQ!zJAF{Dxx=qsD+Qr;ubvf0&5Qj2JWPI-T7}|uJ zfG1f1m0xRpt$!%0zxHe)&e@M9>tGW8_fKlw?VYw!8j+42qi&FjyFOBhrSMyOQ@o1) zd5416CDVVWr(XKpP*-}>(a1UEjze;8d8yM-NSMpy%*KcGJ(Uu)D6>ANTJk$fZxz`z z4m`DPG5pf#)#ke;CqcUSZ+IJ{2m;y^KXIRd2;Mm)D zH!Y|>2k&W)cFZ}7fj0#hpd;|+J3~-ALW%KiaZlhDW;)&caMn?ZkI}DXxkCHsdl#vF zp-4kOm5lUBRhr>*1)k|1#qA}r?Fo()EgZg=35w*HG1+qy{vCg*chhJQULXl{YVDvJ za!$VGG;|?oTty2@La}RZj6ItVljk3crgypV!4iuPm98#hUs7Vbkov$Q;3m)~nnhBB z0k*hwR68;S+hZpJwX#YMqgRu4nHxo7fhF@cc%2<<8s=;o_7bie?k>X92_yC<|C%ZU za2(3wF2JPW3W_sx-XfBGVXM=)(&K0Y z|47)e*+?a%0idi@ zkfMBxqEme!_u&SwP22C?=(09u5>W{_%A$D16E)1@vDEI8Cy%%N1(!VRO$lCm*MCLj zeu5_A-16dN2*PdGhorq&URUBfC(lccq3v!g6pKIZJCvl~6a>N#C#+(Pr>w{csr@oW za?>Q?BXQtnAyb&@z1{Ge!{@bYeajoS9WQIGyGxs4;wJB!KqENbQ^EofPHtnu=;4j@fkGY<5@t-3vZq*s2Y5B2rFo7~l z=iBkNUtd3&_qHq# zQXEB%zfas!>oS|I6d&qWzG%uJOi?ZOa1lsX~7U{sHGfP&omXz?Ii05AdU6rKZTeP7^uEE+ixxN@{ z82iJYfUXqtaWVZ6>m%>SPd-g{E!%A|Mp-dS_8(ZaKt%tz{OGc6X4F9+yS3PqkW`+qw;+2E6!OI z6E~DJwf;HnPyx|+E(Nx88>L9E;9aLASGKr|6j!)V^qd#Nab|<;7h41 z_HT^q%}V_Ik&i3~ypX@78m8(Gq8tG6 zRog%;-?Gq}RD7p-=3|!e^2rh4`p%WsLTcRu$!TY{bal@1G>>{L*_(hIV!DRcqyo;@rJSX8;}--;=(1QR6i?5{z8a*o|uvu zq}~!=J~`2Bc#)#cwsiq~$D6KxJrzs4*lff=jZ<+G-AP+1Te_eEs}!w84|gX4WEdDljAQ?F{B5 zK6zK2S4S_2m^-FrHIfxyKQ$#jqm>xQ`*Wsg;I*OzRr-Z`gjdj0Uz=#}WFF`Si9Atc z<@ABaupG5U{$fTY-_N9dap30fPTt219Xymth#A!E#3#T|QOHI&u~tTz2uo9S{19q| zHo6gO36vwgKVm_1@}b&M2HcEggIj}h-v}|PUcc~~Rfu(;TocH9IRtv!y0dryMOKob z`ZP~GF7Gs%8Xzu}9JVQ`Jx$D35s{?91z~Yu5Lr9&T7YikZZt9lh66NPPrmb_9Ogc% ztuN7RlxQP49w^EM%TZ>?$4s(FK@QAwJsTZA^-`m^;m?ir2DB&%S zt7oy1I&(^Xv=lqw{NrkC5T~GjjBw7G17^)J&1e+I?Qwrdv_4_*NM{euG4uVMZQIEH zifG^JkWOC2wbL8hBKv30Jxn-blg-n&IrBG)ZCXOt!?Zu;oXl0}t0#3Utmi7OHip!n zP9?26PmP>t@AGZ^vckOR^WoS8%I);K7Td({aH`{UbZmsjH26o?~Wk760(V8A^b zK?7BK6@M<@C@tlo4`oOmmKRY7!A_#0 zJdIP-P_GnYJxVA&EaHiW{Dr72mShF&nRVi)2?gLWdrftb@2)|jnIOKT(8iI87V@IIxf9(OqHK0l|goTIKM@jGNvaf{g(p-=DlKIg!c& zm@vp^RDcfuP08q{rYT=m({Kj~0M0fV3oT;A=E*)Tb#{?~7Op>i0^9r9`x%EQM4 zI5xBedDTFhBRBR7kzcMSTEJiX!zDm%U+K!x!-vZVrYPk?P^=V8cpBLo@|$p z2CAYhDKURh>WxX9&2`U?HU6t|nPf({8mPA4RdeBmxf2ilAv?WYJXCx38&+u(6q@ZK z+qQjKs&u2I9li1(zyyVAeyq(9B_A!rl*eGs=d>p2GG42_eY}c1iTg}=rMwKXEjKSG zokwwF@50@SE<*uH8jst9QSF4Lf_X%d`nb?#(0z~3AWDA%wu@Y$^8i(TGx*`f0Q1N92J@ud33umRcDw1?uRDIZCG1-?S`7u5?7^x6f7$r;ROdOt?z62CFmCt zHB{A*xbsw5*3d!^MaVL`-DT2<8yGKQMmLq8Ebs0s8@u$1P=Cspem&+TFggrv2SR1K zN?o!@QBGO(*MEXhaln}tB{r(&rY%FG{H1i`TiPTPB>|Q}3A%QmGOcMWX&iOQkWtl? z$v$ePNWJ%Qe^QnO=!R@gV)pJyPi}g{gGFjw1B09N5-Cau(+~UYkM^ zB$_{^FWj^8jCWY*X;r;*8A2q-zb8Ef-r-k!2&uKJ-pG zJZIZ=nZnk6I+FbhGVXX0rj{RgX>paqU4a>`0UmcL@wF9_f=dF=1FxzxLK!y(#U?Ph}S*C zNsVN)AtR~W-1b80XEH8=gW{e ztR6*p?{eKYzN=;bE(-x(`j{8oxRhQq^`w zAdmNTJ2P-;pY0$1$ch~98tOtCJV%T{u>;e%_urjPIdwYxe&dz&WxmOG!9$o4lSulF3br4uIau;XOjPcD^; zOwoE{cfu~3^@LId$XR2T=28+_&}pyNMwt1gvnSgr*X>mZ<4;b-e5RQkrQW(lVuF&4 z*w)Ulf}_o{zjxi5q;p)$4$JHnu)?4r0sHv+q1ep4@1ei{7yK6NLN|V_DFyPIUv-+w zB@m|{U9Xz5Uxf%MZd|RP@po&IK|N9z_l*Vj@i2xY_OoCNF-M$WCF6x^`qN-r>;U4z z*0f(9#UKj?8T@oh*Y@`*Cg3g_B@(VD?5;~0)BNTT*0^vZM9P#>7%$`#0`S1vIp5fn zVdUA~aHYZ_Eqgwg!Xob=-vtX-1nvt{nY6hEtX~g0vI9dj!qr=hNxxo(t_JLP;)|yt zyvvMw3)ah4p#uWctg*_xcG~C5z|4}{`m0PwapQ3tUw@HXj==W!~!fk^exX+$^B1#jy zL19|PUzdj2Cin0%ZpbmB1YzM1-ztcC0kXg`;f^4>eCVqRnRNv3mXaV4DSjMxH=$e~ z+LiW2oqMFHHgCV+GdyH^kl)v_7i(p?=Jy591Gq@|CZtzkRQa$mqK90u(4(18)uUDN z|8Dq86kfAiL}c_6#?Jk-K1$`yrrUG{)DCOtA0?Hvw!6s!YnI?*Sa@|UWPVGZQ$}VT znEXQAZxcjmQpM7j5%`iTrPwMCZ1KK*X$02bF+co|lnNIv0vq$l>;Z5$IHzBgtx*QJ zu+KKYkF`zjhXBZ zm&A~5P9>681uPi&ic-nQ3FKoyx%>|^E-4#|jaW1Y@t?boH@>=AVZVXN^V@BHjN(4L zWd>e8%((mk(|8BY7*)DaMw9ThTvHxZS*vX-5xo`yV{Uy)vc%!x#q0BhZO{bIJ z;p4=WqG#}g!z$xYp0X%OY#j9D9yz;#1$wb@xQK?~JVhRi42sZyPZYn$4kZ0g6HyM3 zdaldeQUK6kfyH+nW^|r8dG`BVN5p`~*NP8Rrv1$e=;4Y`+h$Z~D!D*F5%`|+u7mKZ zZ)HA}WPK*370rX~jWQ#}fhjwEE|Mu~Zm$4$V4{cudU#VSc^Y{nwYa8BMRfOC&Ide4 z>=n#}*a7?>SM|c3kTo|KP$?klomFBa)@nFO!*ekCg+KCT3Z$jrWu2$-m^M+~U)we@iv27*yUsgM;`y-&DwD}1bowjIozeO?_ zA`YyLd}K$iMt+_|+`;AwC_4<(s)`A=we&mx9WPZax;IJIaowC~cHg|{BZn3+_wg00 zR~q$3WysBLztN;Kc4rT7xh}4)2UJW#!KzscCVRvm4beDKRpoz;6f=(P_=CVBGZWO( zl0e(AwkT}(PMfKl6>$E;Yl1jw&?O(-KkYeAteKUnq^-KBMDL$nc5`m(I|rB=GhTMt zuqF>OUMOuqXm2pD&%c{-fEFmUj*(^bVkF9HO7_|cc0Af@XW8S{*BM*+CK8qGrXFN-c?#+e^_ z{7{NR7NYhJ41ggvi$S~|_fAS7YKWfw_a4<_0gAiZm!26>r_~D6kN=~(OzAVDfy|$8 zm~iKt@p`<3&rkZTgJH$($XnRj1^d^aB(S!6RucX-l<-Nc0n33vL9s3k(Vh;(<5fmKgZD;*}=?(mNHl@Em;($Z_BW z)G+H)g}MPoIX`|2ns_}j-=`br>Pfbf?=3`%1&eNHwN~g21tn(Ua(29L|mwXtNz|awx z@kv)`h@D@1=6@N8s(IRb_GP|X<)^yLW$dMwOw)VS<>|Mus!TD_C&*zZ`QGI*Z&ieq z{3~_2LZRJ1b!6p~G!|&adY(iJb^27c@i$5f0xa?QRECXYCm`OH+hUZE_W?C}2hy%Kf$^2M>Mdlei&H_7BJ;T;Rklpe5ZKCJOw`CR8Z z2BNaPWby48 zQ+fV@@6grxJGF~wH~z8dX4U39vwC@qd4PG=aO}?U>xM^UO_W?eWhGpxp*uczo50&O zKef58>Cpn1za2|^arbC_Aq%?YSnVjA6L?AKRXxM>&}kzC4xe62-MHC{a%Q}eKybp` zy(F1{3`R$bOUnuL9+<%z^8PY}*PUHc%!Lv;O@y>38Cft)`h_-oLx!0z-D4&RFXPV$ z1i5%~+TGmm+IL2uNmsnuVozHfa(1=+Mr2r~65;`L5>_HDdeF%dbgHw19Pgm@vgx9~ zDtK;)tryZ4uH8vEe5ru->2k9eL`p~IlWZ5;irsinKVZt#TP4ucVF?WU$A zXY}nw4TsCYBA6TZUkvnNtV=E%Lt1nONQN`8=5eAIZ}{g{ zhAZ&K3ziH~+#~mZd*Ap0G(3&A@tLmBX$4FuFyrpdpr7NFak;IBMGm@YixXzyh_gki z_y&v6(s?R9i)%+y^v6pUH0n9@B^Ss8km?Y#HaGP(+X#1;QosjXdjYCmv(7QJ0u0f> zo)BD8CQ#V%be~HQHukixL$l$T4x|B=hb+Sc;SOt?vFrfPwgK?Q<9^!%H%+hy@(}~K zU_qBs7C3-oJ&tA z;w-a!TwN&a25dv=L!j0JmbAjV+(vp62J?RqPozvOQRsnI|OM;e#okLwRK(P3{3FKKo6~4m(NJh|r@gC2L z6zocUm?krNC;y=(NMHVYkFJ230Wi9_0b0QOJi;itaP~Q3dQU zDqM2%iq}0#5Z-iuIaE5EpGL5B*WW46$<~{mb=Ad(9^aAbl(D~Ez+(#o_qWeCl;#db zuEX!F{yq6$EjMIfAFHtFu0n_iEU^AjN9DB)w8(CuRy#88#!iyYPSSC$gqgNU^F{DL z$nUP-cTc$F-EN!bH(|-Vnos=Bny^k+5crsp5T$D*k1g;#@f%?3z4orrlR!JF2kHZs z3t^U^=t072oBn$O4**YNvk83w{8=Ay+46zIlGP8d$uSzf`sgFgU*8XBM$dem;_s7| z424P^0kxIO0!%E$BAxU&1|#j(eqz%3FBHE7H1|v;)zpN`%M^#p|A?0~(}@N{4F6Z1 z?Z{n5*Hr>akb1@ajgkpNP5JOM(3pc~6$HFxffWZJhlt#zO97WyLjFMq1^MAt)jdM^ zDN5f>*e85Sdp|Z^lpMc@;?emVn+plByYx@}d**fJ)p4Uxr`fAi5jCOp#o{`yKc4_) zY<nQ}|sbEGg{8Y$jVanmaJrD$JbB9qjLv+=Jq}+-RR!+3?zT!Fsf2A?u>O8n^y+s>1;$`urLvT&TItwv@Xz4pE}k5YtyCs! zsC?qe!IZrr`M}2l4A{^f2O{dHz3`ZQ%K6XiY2Iz zAyNp#t3*MvMyVxaZG5vCnSf#ZH7jHgcXM|)Bc8D%d$9@oEXf==0PP5PqiX0a=TT~7 z6}QW-@9;^nz;L{TOy;;9+@1Phs!UnZUV|~|fwvo0hLF*>>L59qN$N?s4t}I(IbDQ9 zgLb)fW>=Sd$e&~8!dehiZ;U9au~ng1&-kaXWa_?7XnWzD4#bIR{CENgM;S4r71K~W zs&@ei?sq1MYFCNm{lI0h7jrhtZo2tHxVl@1H5h)?_k?V~G)q1168&Huiq0*jwpp%N zM4mj5oh%Rp2B|hG%#G=;`hfHEV~xwGA9}aG2#hJMT0iq=v!jU$!n)qVCGT89X_K3{ zQ0@HTV{EYc>eo9$`haOHYK2<7&*U)2)P`bZpdpvo3eVJvn-Iu|UDrWy?gWrlTt0-~ zc(McsmSy(ce$>P_p8InUi@}!7Z{nVVgV1a@0Df0Py?-Q;WE=j|-t*L%+RBUIx)Fm# zVTeSUyg7s4&ro4 zk9_#gGoJ^Zxp(qi$#HBc5A5t9ZT$5*Qm&eIquqL!5t=?%!Hg^YOSlGw+U>cOUI)<| zuS`!)(<-=AZsxdVfzFd)@jBOKBwj#Dr89OGdC0o|L;H#mqnNQ<_x!h5@c{>f0YK+f zNR*%R`If}mtNFAvelk-WEiaRF@2#?*=6b!4u)uo6YOXn8J9}5~S4B8b9tZKZaL-U? zZ$C=3!C@7Xl=o?n4T*K{emArx{?`Vo6D25C+kOAv%kV<%jh5#*|(F%x~{a1 zL}YwnUCj8)OcrC%bw$A1v^{Kej30_)&Ir(9T;%ZSd zxW9Rd{r7@TVau5;)e~e0dvU2{WDo05p^vWi;~~+>7tCSf zKk<~nA|Btk^iLn_9*U0q-*L>jO_MffZS)s{(j}vLFyjgR>6+2$1^~OB@@pcSCB6S5 zwH^4CguFyGKJtF_Tq^Beq0*Zu$BRH9C5BYxKlxhAvkX!jq@?v27cgitvOV9e;&3O7 zK=ew2)D=nwq9Fr_dp8=Jdvd=Zy+uapI`D=zBQww=`U{RO1U-t(VS!uH7Qoeej9hxx z=6z#|B4GEE&2}L#&5?P(D0=Ywom4)rQ7abPp=j_75>E<@jbQA-C|gO6Aozxvc}z zmG3U-FQhuF2zjN|D#!OJhWGVZ6|~U8N)nz;p?i7`{0|vHf>H-e#N>USYO@{0{(Ct= zAdzT_!0o5CyszO&J={9Q5REjK{;>uYSy8abe%H|YQ+Oz*yXmoZ$3 zii3GF+02>BU%q=kmo;lRj=DS7Bi(5&s`_XL8Hqc3kt*Dh zP;ZnD+%!3oSJU6D`MM#o*J!i0m>d1ZoX%u6`_#^S+VjJAZWo{c8to;6)}1SduYgj8Uq+%E;XJm%lUD=D8AX9o5Zftx-@ z`B@u1Wr4uAP4GsN3=3cx-B!(bG(d~2gmVXsh3@-Og<(Lb>emBB%9(K4e5N12A6(`943RtGR*Tk!p?8=4f;HKD4 zK}GJ^`#KZp202`uRd@KYeu#o z|K(PpP92kRfl|XZl6>9#_x%%+6&O!#=4ptR*>rBd=zt!&_}J#2H1#Z~%UvhB9eg;Mht+yqXog6mb(_Z!3<3Mj=q4ddCoo=W%+)M?y*o`%PobIf9X@`CIp zWhn1~x51QI#PY;DhD{~o!3$cf?U1S5x~+l2Q&iAgt<_CvM<)n8iIqPJ31mUiX9H&P zN!8$oX~iolp1`GOP)85EH#`3c?K!+u*KkdrN#+j5Wn9uO(t|a*;EKiH&vQdA6W;#%40o(GhfD~rU!CPSM(H#hr zo>F4!a1MQR6oti0(oYD&Y|WX)9l}ql@k@MHXKut}jN8;-{$#e!4VND4k%!}h0a`^L;Gcw;ZT zQI5GWv@(iTGr9(ae(8MI{9QAZ?Kf;%SzY?4;k9sMDC#Tq)(ghBR_f2?ip0Y^ecJ(- zy-|A<9d2BLF>3_fs&m5r%V(ynsWrplFw)_d9*YZnGm=-nQJR>57~~3tsP>o0uYel) z+7Mk$P^VP8dcns}VE^av;WeH2`RHb`oo6#^zmADo5$u(W#vC6@VD04O$Q6D8O2JcG zC%UEGg=N`#q@#NuLK%|5d`~Y=WJ>8XpLp>SE;+Blfg4nWBIGtDM%;L?7d$pcVWt~|{KmprM0>-M?yRg2w}573qZD0VaH7PAnyB`&K2dzP1M!Q{TP zhs48hX6xM=*uwh3$c`l_V6=T&U-&J4)BB@BVC|(4Er}TlbTMQ zx;~Kz!3xG>FqdTqcl*!BY7|Xh?6!y!XXCxFC}Zl+rIXdvkoO<%AmUUSu@LBg_O$e% zbwFO{RoyG0_-aU2Rn_qH%m6bx?{8Aatm4rhziNmJAsg_V@Vx4{16z2Y#5UJCoGzBD zl>q_j4~W`oJ$=Q}n0g1;n)s%Fyf1M(={|aY{OC{e=J)ZDJ5M&771^gky@7;Nsy0Of zjGK>@@)R>=Xgs$lrS=lQkxrhtVuINpu!9e;a`Dw z;opG^@agZWjFyeO$69GD%Q3XWa|aWu2}kcljAluZ-0KX06JG!vdt6r3Ndd;c7^L%| z0Ni~hP>*s>5uV$|EjUsl4|8tiMyIaqCrKpTh6Vz6IPD&MdalCFBUeH?4mlg0C$}ds zqO}Tdw~MIUmvC3i_gk^yJhs~2`RwY2<<(0rP8U#-VLBd z2nbXH^3YgE0PaM&EHA-7Zm?{J@g4e-Pyfm!%HA)7K!8h%8~V_KBNx3F>Zwuj$l8CzK$H!VTt{+bW7o1rzOe|2YX*)DO? zd^`}-X6Myu^bH0s9;SbL#Pa8+pgI?u4P3^n1L%xUiAy-ig)yX{;_a>-DSF%zt#B@m zr8l=B-)SI;b7~4be0_{?x2!BbucD%8Xg*rOO6pcP(%j)(@}9{+c?0kODS|G4{@T zfOxjU%{?CR7CRjK(fUi^=$yfU4+#@w@gQwpXv#C<5lhpTsUgx=jVyd>UpGL^es_@j z-Rf=o&WZR(Zpsf)%_%f*kQU?`e0=ohA@}Sc4a~YU^rL_e<^M24+jj7%lAkBceXRAt z3p~A2CePrs1AY~Kgs_CRa+-_Y=v!@)iA_(w(a7u_Qc?r347f6G7QfANSvH(POn+>5ZOW{}B4CmFS7Gs)*O0?uvz5Q^sy40E)Z78X#W#N^zb=WT z`?-@mdCF zFgAVrx3&D4`#c3@NcrUN7#@M>O7aQkRLMo}PX|NhaOp%_3(y(5Nw+E=(vyPfg#lHgID`m7)I9VBhOL#L^m`4#h40rwI+ zFqvDiQYccwtA#L4&;{Ugmx%{RfGC+ltI+ut-zp*DC?A}^a7YIK4^!VAPv!sqf1hKA ztTLjGJwjHJ#5rZ}jFQMvNC=f3&N-z*WRD~rk))C$dmM%AO_DhF=GbSS^SgV0zJL7w z!{gzf`!%lXbv+jg@9}Hj>l#_cL(9*GSF<>i>Q?lHd2qLAPw~t}`E$&f=g?l5`b;1$ z_2m?9aNBA+=zpbGPPHTTPQ(aW(SPN%1+Rn}E8>wL&^Ccyu<@kvb>`rrEzBQ!NX)Bg zB-unYq^}F_CJO^47i4sQG{g=`pGpS9PB*+W>UK_9xKe~HZtjeglHvscOY|22fMgj* z+pGw9JO<3bk#Qa6%ES4~@HfLu%KME|m^t!WZt=ExzS*TRmu`gw%n0w?i)OftgHLK} zj(K+?AQ|4qB!$t_L#!8HTwt!Tah9nt!0BfIBMBJo;lR{573<%VO5Jrqp9oO{sSX|B>Ka zix+W*xv(NqPFQ*$^Tb1vSeV_;->;5&63QLJrAc|%mU-RuG;f(`g3-axqn@Q_iQsU1 zm+&s5HcTBQtwB)8Xj2zba7&ei;}+=dPtRE%hgS>Ta9K}{;kJ%DZQkrt-r;>bZXqp=Od*~N^2+d$9%?7A<6~^LcMAqK9Yv?|Y_{n> zCi|v*_Vm}pj*Lb^T@uZ|J;;nXbMeE2o@6WBveoncQ2B|)zLC9GFm3;X!l%W6{3_Ag zE@;Q*T$xAIE^8jM2#XjNHmCGj?Sz`l1xXAtf5i6mYo4KAV!`g?rp@TtR_kgth4&I@ zuY9yj_i>&-&;ncc4R^V%{^mAzls_rkUYCo%Wirg2_kKI#XO@j;)YGrELg>#zIo0oX zjSX~n0xxn;*B>vrVacJX-_OcWj$h2q`Q`%jThnyAEv**Iz9m0h3Y6|v3zXjF*0J0D zNL$yQ{(0s`WV~_nu1?y%R}(sO`{DoIKWPlLQw2wu_ii@9A20_QB3k!Fi|`_5@8(M5 zd+gmzoZ(1YE{~_WKS|KK`07iSdp2RRfW7=-g~=Kz!8tii5$mSJbb#ZKtmeUUf^Fo_ z{bD+PnDut4O5wHZJbB?vudb`-re-LnROAoY+Fv4ysNEIrWa#U5X-{9|v%J5im5_rX zQ3IjkgEtvI(QPVdjefLm;2)h5kv7}Zgy@0cWGT2(5S0reY1F*>reGnEFz=S3UMf=5 zbNl(=oxKSz}6eIflp8(31y4d6VdOj?B%?7o+J(jSZ>j zs-sT##0DBOs=shAaCD_Mj_=lAx&lJGE*OobAzIj-H&L;fO`SpK96pFHP2|`#M!+xA z=;zKrcJSnP1$57FFM-RSw+n}r8=tJf77bJg7qD%lS8U%K`UMYgz;um{VAu)ebSU_4py|s4h8H}~Iqw|@B}x`Kf1Khl%jS)2 zg%Sk z+4X%6NdU38q07+tQ+=)`MuIx=^CS}3rE7riTa-E;Pcbts$Bo4g+JWX(>yq?m{N^N+ zg@ZD#%wb0BEA%&riCHx~zmoB$8flp4ON;@U-@$u`?Quw84h8vGSM@o_U^o_!xJ+~Y zSj;;BPR$ek(X#1(X9%XJ zt)6lTJ*`b;9u)vi`9&AD^W{g+QSHjRuOT0e_ww^3W)f@tNJ6P9qKqZVg6cKPRN0gW`wa#j!2S6pm71Pfr#H=(f zPnhdZ^gwC+5X_anp?-WJX`Dqd^Z}oR-9~i0R=qG8= zsMEnQD;zQW^Opl-U7$}DeZW~e!Z9D{xXaKO=Mykkq=8T&JN94;IHYLB$Y?SuB7EMDM-7Apx0cPrXC+ z^B3Q*%-C%;%s9PyO9zsoxpL;*{$L-WmRmBJD_m5rQV>W#g#}*Bp8^Es59bH|+%Nns z>_QJ7<^bV+S(W-H1=ruveCErfe136u(MLT-F6~WP-o3_!@rgEXkK2Y>P+`}NjROtL z+x(}O#ceN5t=Vo-72bza9UOP4y^cGvy^gec?-Ej}0_zZr^X;Sq_0sGfObz&Sv?w?@ z=sk$BA0+4XrQ!r3UiX-k!}w;LlD|!$&JIU8?1kLo95S;rO9e<<3VoBIo5!|Y+?V-p z;rJm|w1;V=AW>%(&J>F6{4)GrlG@S6uc>`ewdp5=$C82DXGT)w7+U9cr#Bp*%oseQ zS$|(95`~B9AI`3hefAp*H~+LRJ-4gX*$Qp$xlP&ASV{p$JkvYXO$+Uh#~!q*VHl_k z$lYHb&CywRGoa_5#iM{lN_N?bWI2SN%_LilY3)16XF`7g^{ zC=~9D^NcebWbQ;N#CK{O=tO_>^FJxn5RYWyU3N*ME)d|Eld4%*={d)!3BgJYLC z!rDBotO&4Q8j{U{G+)F%HuvWSCc9i#*0%2tBrB7>S^wIQb03w^;Ap8e;%R|8aMf(y za!eW?jTh0mXJZ4EK-f7%*2QB8ssPE`F+0$Td$kf!h~F-a%N&z_y4 zLKhwT6?~}OeuzEvi&I{~Q@DX4g(KFk?PSiT&e;NV;04Mre%+tdDAf)u{^K%o+Bh7j z;uaVHS%@UR#?~IDiF{;fFW(7_uN^f|e^z9mimF}noE##yBtB@Db?C^pxN)nR_djBV zJQ*_j;TPT>a4+F|N6V4Xi~EKeW>%>I>XhmvW6a>!`C|HZQ{2Hl>Dj;l?PyKRl%N#2OfQ<}vPm=%{$RhYT@m&Dh=s2dpvh8v zDTEeQZ8KT{#Cbz7To{mlivSUMm8SDhpb(ROy#t0=g@?`p&vMT_E-2=CGrwY1!si8* zF!A{wrpK@E&)H^m+=^qYw>Zr|cV$mOuOqkt^Xc*e{5G`UjWuK-R?``9M9#}L58Qz9RhrJqRn(mY!id64lJ)oPY)!Bw8n%8cwlEqd0FK{OyGXHnVA_>7k3*d6it;Z?)9Lg+!jw%fKLiH< z`2%wCp}?XXg(QwogErTFw=~+R+WG|rN|@I!GuuNVpW?20kqle$Z>y+pK7;Ol>~g?| zM|ni{qx)kYj{~8Zi(~MP`KcrTSZUddt;6XS(8&zx9lrEbkkcSOp+nQIkHZ{vAe%wv zG<5o962oqdKCAUC_e`3g34UE3&|I{=0kv7YeHZtBf31R89e)m0>{1_h(}WNF5Kry5 zf2od)xYOLkFpuxW_z+$^j5;hm`M6sFuqg-P^3@JW?k0^RZW)fcW(%%BCKzuGD#%Mq@-r%h6mOV>#R3DG5Cvw+ zC1-S=gTtYcCUwJEMHw|~M86`L?sr6|8s65o{PW@uC192%Hl>KRcUb^XvZB=K<|(1F zU~+w%J#e%+Lz41X7ggE;H`ecu4q?fo?EKjW^6BN<^HLw^d(Izt2-Zi55!}U0^O*zU zjYruP47;VV0(37qc^P+$w~7+skj@Xd9fkl`4I-=(DM$OqBI6CvZ<)P;q!cN^r*NOW zmJVe553CtTUY}3CeZsD(@pubX*VTkx)lqev3HZ`6&Z!2g41S2b*Jey-nXe3X;;PYl z8is4&-7TtL30hnzidU#XyBuVX=`t(3wvowpxU~S1O*=Ze+X|yhj@10eaw%=Gx3d|y zJuiKW-kyAHQ64zmnk;65#A_LSTJz6YZ?H#aMO^;tbfkYCrqfv8LR;!!RnIZCC748hUH6WFEj44Qlyg>S^F&d3RMeT! zTRP7c*Nm8(7dcz|R(c2bs7^EjdD~xE$u1Qh7mWtu+zw$LkE4#LzP8-4%~gQ22)PTg zh+%}r-g14(x#QL5N$WrSA?m@&qh(u#Lzxnp2_1MBezdL-Q}<_1=FoPs3g&VuPPNA+ z16UP(U19{F9e$g9i4H967FcIC^;P#F(Na1LQXVG4?$bKU;)XAeyf`ca( z$JTF>DwJDbk1s|hpxpPykTNBiP|uk`ianS2GP5cJOVFxknm^P zsq_!yPV<8ZUakCF2Zn!ONhtn$YgIDjyWidGq?pJ!K$P)vE72V1GsoN-qw{HNs`US- zPI7H|-*3>II_ODx%4P6Q71HdP^d6-qMB7X?d@Oof2@n(;EZh!I+?dn4(ZgTcC&C^5 zZp!8;o@Qtz*6;-owTPc%=N8~^19?k-^SkANKGe3}v)lnBh~bJrQjR`YU_LC9EjQ4b`^tsT`o4b`gt2Gc^925bWmCOIzPRzluw&^V1xl}B;*VZvizdZ57KWlTY_=#jvqJf!mnZ~n)oDp zkw<5PNq!sX->crOOBsbYd8<=ojk>e6wgQ+45uU}!XUTsCcM@X2N4{*LNp7!AOeX(t z;_geqeNDAt+~PIf(v8U(oxbVU}U=O@$Jl&UL|VroNtuD?gnGLx9ikuf_|a|kz)jtuevtV9 zh20Sgqk}XW*eKh#>TUn(g=)^Fi?npMbtSvlm3TK?gd9PNR|~ z@Hv2aOXPRv9IUz0x|Xp&Q$EU9g5D#yEbu*w<@{0-iO6M{2H+Lo%klZTaZ<9 z-PHdI)p5FulO+eiWm=LpN#rRyYbu-{@)9-=>-LsNkFtdD>#-B2A)+T)5kG!Q9sc(c zpubt?KJ<@C_7!AacL1`iHu*2Rf3ZnEM@KrdwiX^yuLb=F#+}qzQo32STvJf5_#ur- zWWht25nUEzIt(Be_dVuthiFT{?_WsMrEiO8Xa)kt*6Gk@XZE|W^>nB`->UWYce!}}6!c(dI}H0qK7ISyAq;p|^6RCX0d*SBX{i)j zL4udCyUt^P`%%p}s7$2YRLH9zg65B(;>mt~)U-sN;&5*FkMBvh;n-){kq!(&wR4p} za=T9NAXV4M&0?r4l@`DDwRfsN{6QB9f_(N2DF6OiOe))!pfhH5H1GXN8`v@!1@aJN ztuU%=fG^`V`Qza%JDgh)E!Po?F1$GU=dfcZW&-vj-@xh^qqS?wY;*!9O}O3(Yd#P7 z7r-c#jkQ0*0LI_jRAarpYV*faI1l-Pb925lb1`(4#?J-{*(%pa+4~^8RSx{x*QCUX zZiWBXQy4!#HKjU>*SW74-7PH{y=`^!K{R(>IFXmbBD4{-5o(+L+r`A-W)4pDb5A{p zFaO1UXa^6{YCBWeM1LTe6gD=*?Asd!2iVhK;h5)&YP<`*TqNfv%!d))SFl>O1v#>F z1hf|$<{=w!9yli-E&0?KNU7Zm5C%lX*soHm`IJD(;!v=$9<) zo0ep?OcwMl^?MbZh*Ee+0PflfwX-E4UqwQnxtZmrcyD|#;63@#T{@>_y&^N%6}$b!JQNaD5_eW2OFV~dXQ*Y zwq}&26V*}G+pYvi=OJ)#Uo!6-|76OOv;4OVP#mI}A8~GZyf^OufVp4(k?)iDSk7J$ zr)&Rf&yM{zD6EjI2fp1fkfd-|(#1xWPI;1I=0MJ(X)FHo9QMYfq!wr(VZEk*{a-i2 zJNH`+dshg!R~X9$xx4Jt|4o-dq*|TOqt{=*`5E2wLh6V%dl8_Ehx_fHFBjX}IE3*O z2Hw*ZK%%l4xDkv#nB(@K(|bttn&x^jysaE)KF^Giak58yvl~`FM7CM5P zH9%OC<+{?Te0W|0nR?WP-|+`jGM)>zk2WH`*yq~`xLh0AwVY`tuQ-vlLxyeqt55FAy@ zmpA{OnQ;-qjrrl_7EEzd`*2l`4X|h4rOq|o&(xxDfZ=P*OR5f%GK_b3orxUkTO4vg z3-~Uk^n%n3nF(O9KaeXEn!j=!IS2v4AWI15Hq3#6`59MbjlN4CtJyo4xJ-c&!D9^! z95@L?rGv~`zVvr+)qOrEIA#i64GIo0Rlh{m9_YNUL%p!ttzMzvF%NKesKeD`!-OF! zK0gW~Ei}MR<{pE$h$?9T9qgwg{>et~s{V+guzH<(wFycc+j$CW#AH_1aZPtB$pmV_ zDXk}mPZ321nN5Q+if()Rd~d!vhdCJkeI3e%3D9JR@Y)*y@jY@vC1+$-$~iHfAhB#n z)`!!Iuc;gaSU2y^K+JNcOnVM&U;fIS6~P-Xi0E@J8$R2IN!l~Fha)dZ%+-V1%ua;D z6#8{ME<1Nc4RtHb0v$ZgX8j5;cW4)x7|aRqcB()R<0#Wdm02Xv-g0srW}rpLn3E@D zNE^a0nR?Fl81sNaJtPINQ?{li!lv&k z<#*ugH?NRv4t=)8b^#>+;Jds` zc=;#31(h|E2+Js=w@ATN%hCSOH#-XT1LN4RcRz{@f?t~Bf{VaR*G*)XqT8mNE8{&i zVT9=$Xh)6ZRCI6#K1!o=AH^5aEsQ9BdC+E!O_AD)IIh=Eg*It>~_-SfKcpU zs%o-*`uNDAWIlL|x92$QdTJg@)5~(h$*j#v!hqR7*;E`TzW0W>H-dy~UH8XkR6i(S z#(ncm#usXVK5&~QF-N|Tbk@M3$q;y9bE5F?bB6yphAoqtp76mVdxOkf)VsEE3VJK7 zC^27Zmg-i*gIHhh`@#A`eZq-{bfF6_gMdO2-S0Nua$Cb}YuG`9POq$~I}G0;0=xSBCFd+y$6KDpjqVY9>2=>q!gxq@UX&w%3Nx7n#QhnAaVQnhR4yR%NPv7`ufTJ|(UC7RfcepHmd4xQJ z{TruA$2FWwH`a0k|KaujPI~Y6Yc7TnaJ=MS=I0Gm=bY&!MwYeb5-wk4cMVIPXR*i& ztruMr*9-mP(MtGUxoeI-TWk&C*GXk#;l{lG%A#b>g#pk1z*iBJjRHI>_nAR>!ZWnY zz1f!P9VkHn`+$@s}mF92WVPKc%mHI8ha&)nJCxN8Cc8ejl;80O|0o|(+uHUQ#+F7Kq4fGl+V3O5=i>wlpC)t3M-5&ct zy#$8-aIWzq9b=7^c8!^K^;*Kh+WYaTZ-G^^(Z^+;q;|<-f4=1t;Md`MOfcjSfu_-h zg#kAeXoDh}%a>*leDZJdYE^L2pQSqcwmFm|zw9t$;nX{L%J~NjjXjP>h#-1G6>ijC zGNT-LzZhxxULNrLfWJYB*@gE+e}u6jb_0}vPuO#8h}{R{Yy%D5*)Ky$_FgsX`#Opa zu%2jj=6L2xzulqRpzE`~{V~R>%QK*NuiCAO>H{j2>dw4k@+7?68k4mEj(f6CHy%!! zLM8Y@S^-HgZA0=l=-S~Oi9It=tm#fV3S zCBMU&KIbg1t`lC-ieJ%6h6x3lV8rjQ=}_5Z=7rs{IbJG-|FfK0D!B(Uz<>G{{$ie%~W$n-62 zgV1f)pvDZldkbRUrYA-=?M-vWfKppf^wbe4Z)QZQ7g*^(5O|A={VIMDQ^;tIFql1& zij!oJHD@_iSmG#Jq#oya>)OPIHo+QOZ;db5ewrlgTrU*CjB+v&`S5q%v>j}o&XaFyV{>oY#TgMXb@FzO@06FxQjs-^D44)Q(4z0`icW+{L<1%yG=4U?d^UKm95 z=n`Pi>FY=iEAew&Ew2~%Oxc_BZ;|IddD&e$Uhtgae|)9zAEsegqer&vP(^qRaw4VXv9m( zpuW+yZ-<~j#X<(06*9ym@%$gl@6WX(H#G;pZj?EUEYEzaHnv4C&aQZts^G#EDkg1W zPq?l9UKL3ez^On7@K|aqyJj}lUk;d6IDB+ISrD**fY_d0EO0=!`56mtIyDl}M^PeZ<}4{-q_bSY@Ty953t z)Hg_#9I$l<-(&DC@IjbHgQ*Pq&Ym*!*rVRiL9gucNp!)UT#O4BrYHZ6WA2HNar9I% zNN4+?2s6U!BHg&haL@Lncuv1%CE1@AI$uEb2>b$&CJsL|1aXT+i`nG7m=ObedKxKR zERG_y*crC6E4M=MmY9x%C)~r7r5^O(7=nNM?P5eT%`)?bC!#p60uqlnBp=7q$NL=V zR^A^XhWa(;IYOAnMt6#xKw}QZGAkhQ8J%5+vm2o;oM+Z)Yts2(nCCJe27Cl~cPYE~ zpMv)}*1)9FFQ-)sdIm(f{KGLLv(GmaMos(=@vmeZju-6Wxh~Jnbg$t9g;GDVv9+G~Vj3P<^XcHY z)z?39c+6MF3i#zjjgX^2osuCNNTmg*|JMM(JW*c-I8g7bmPN;Mx(_iQ+<|MX@SS2F zur^O|MSD%+y-Eq0M}RFrwolw9Ql6N$$?TQ|4Y^(nria>Y*yQl_1_m8&F2UnYf%z;n zHF=s`PX<)dd(u%;*z@|a?+$6U{l0`^drOt# zNxQ-0`6Kp%x+(%%0J?LCVeIA6lFV^QiyH;D?nd#Bxa2sR$1TgDWGv`}_qy{N^MWyL zJR$1rEOF*G-t;Y93+hz|E1_SMXBK!FSZx^u2bU!CLzkqZ&2dHCqws!rf&Axt`d@!o z@@x+(7h;>|75?>iSwbX%{e&6l|4ow z&ve}{q6IewoU(vh;ioKu6C;*w3ILL%3=ZfuYv`R%CHa3jZgK6{z5-tfb}xXAu_t8) zW>s|`EO(atHFXBwlqRPH1SAVf=W{O@toQ$rWokwOSq>Cv&Z_Ai4t>G<-22T7430%G z!srF%#ehQwCxRWsZ2C3HET-Ur*8I-$b+zJaP&Dhxobw@;0^lO36QWB`s>L%0Zmw*> z8$_XqTfm_lQbM+mDoQZc`ACMz{b1(Be|9V-i%M`*(lS1Q4RZu~j4VD@XG@9UM6BX8 zdKwo452x;7&=lK7{tj#Q+RLxn$ia#>OcPLuKx%CS>+Xu**O-Y ziFNdyPj+LWyG0+{r2X%l%`kqz2zgjXSCSxGiY6p;h1FEB^_o#)XgMrOj*ZBR=zRZa zt;^I9$H<5MBPRy7%iaiKwIzD}M~7~!!Y#FFVhBEo4E1?$jPTA; zgkWeDFZL=}^c>D`V_qTPAdvO%*X_SfoDX9jmb0eh*8wREH7fP+$*2IO^!hS{d;*&h zHC|#RW-sW)e2_@jZ5U#nZ)dEJh1+hID4wIwI2 zg}h_-Pern?Ty}FV#9Trgm#w_7?9Ig^cp?AqYCz@vtZTXok2s}0?liln2W`10uPNij z`ty$UM_nfNMR}mlT$FrxAEE0VvWW^hTsj_Bx)jkzeXa9t-6g~?F`C-`A*e4Xsl4}E zB%zXcxFIcDopx`4Z;k1c!8wr6?>=H%439@rpSwi1`hx0I3 zmIFrQ5t5`e1xJ6Z9H8b$4XhlLo}POPmVmk!x`?cR9nlL_cv53i41=$g=W61nHsJ>E7X6t`-SlP10!I?S+D#$)^j&dPxbhLmWY# zdIjW|XSARN&bvp>p0V~I%u9^3NPKU6B*1bW?*9i+0{RSrE!x&;PBu&qP)8?P4=SND z7qNp^1n8a4bqCDJ)BZbpxBfEPxBEVju|IX$Fxr2ML*E4Ke!M<>g$wAnjF6l^uQg+< zM!GR#+X9sbyk;8Cw!TVa%E;#Jk zYSUJ}q5qy~#~<4g!vDQE$|%Gx^8ghr!SpfJz|``#WC1uA{v?Y!_oWI~TT(@n$4Bf~ zO5NBu;^6w2`!#0NtzqnhCp*d7-MXB0`g)L1>kn)D`W^Xg{RtJ|B^yHP{@4wm_yC`tpJWl}pt6Tbk_Zi7M*J!_wHd_G#SH>UZ<{C9~LJmRw zNXhM-m=s^W2J>0*fHV`yZ=3Sl;DHByd=hV+o>NQk!}g^MiT~;Tnz^yi^C$HF!uJFF z?YkT$58lQ&!Vlfqlytfm8sZT7O6>fnvPw0cls`@=$)jphQe&^zl@VM8lhZNMqLR13kbCj&@vDaN9?ONp7C=;ak>pmw#)Q1zj0%KaW zv&Nk~>yh61R8At;b%BP;VrZk^fcuUpFI%~|6F;hpJ{edpEYE!Hc~$GFBfWyz#E z1<8^ulEP8HuR!6x%Bk3SvvqIXA<}~+Bl6}#l_1^6#Wk?S*`+q1UEIHD8}>}LZr3nA zn)=3-IVW736o-Ti;y%#D1^6Rb3ZP4aV-wS3eln0*h0->@a1@UQh|?99${n0*ZF4Ez zo_iQ}m+!G#;FCZp#i@WN6^>gfTfWL%j3WiJyqw8l_ZR`)JgrktUyzg@w~B|Oa9;L6 zRG&BJQ=*F*tx;Hzx&% zL9`)PZJW5wu#S5&CIy}9PX9>^(l!7LdNR*`W?&y};d@dCTxwU3Md_KU?#vj$llB%K z;D7R^E2A#w;&H;A`!W0g)ga|}l`7XT_V^xzb?WEQo06ncW&9jic>(5&qDQ|3ql%wD z#>XJStDirf3Y1CZlM*!uC|x`-HNcpKmu1#fUm;)PL~}+xCX$|RM04G@WwA!DGn1n} z5ss3Xk8YFiJGwUphvX#AIo0h6mrFe)UiKvwRt0OOT>}$eCLbHgjXU_cec_@T6W34by%34 zems23fxq!#b_fbQiU9NDF-RPy!-f2e7G1XZAySX*!r7F{Ed~5QtC=%|3(j)m3flky z7IIKt;9jEyTi@Q&eeBQVG$DoxWFUOnFg{>XdXT8a*1T^U$nw?i@zn6+KoNk|7=qtE z#(dmr3mLd3fEJCz%(KLiz(=@UO7yzNb4C6KFe0|fK7_u-5qALhxyE`I>dq-GNfR(? zUQ(?{i`;YI#3T#M7_zn+blWhv?lbp#}I<(*c4`HYnHXFla^{^?rsJ*e%a z#Jzwn0b-2qETn;NA&ww5H8=4*93^U}#~*&plx{>S=I7w6JgUPV!P81NN@~3mXMoBL zKfx{l56x|@{x%w}$%VgVv_g`vV#Dh{yDh<~|5Uk&=pP`H6gv?o-j8ZXKF_to2!G_t zTxEIk8>cVzy0=u0=IQ4Hjw2t@;z>cJZJ(m*gHxL1^&T6A0@kng!7RfAHS}>{K$6lWK#RRXv&rN8I{AX z=u=0Z!|RT+;_XdIXpZw}%R@K&C#4z1h~NExrC+Po7_SbyFb7FNkUh*P!;vL>b_4vl53tfhH4q!?}qL>z@5(S zV^rB!d?_7)hAj9OHNA>K?U}&GqoQfe+Rw)&9wjL6!R-cqGHE(IMS0-IiN+pL*4?NZxwA zvg5Q7eJf9m7p>j>tpcfEHsJl2(%xn;(|fL*5B2+^G_112b*|ooZ{IGLd6(dNLmo2a z{sYEA`?8;$#JocoUo>q;&zYn!>vCHwkB#k9bn7nQV{<0}a8v)+GN^ojV->s+>#`)v zn%A%1JZY}1jj-1cv7@?L?-Wb1szC1EJA-%Wu*>H>tMl={Pbfgl+zonr=)JrdskwH% zy5ykAGHCx@lP_3X;wEd~m)<8KUw^D7rIH#GRyG5bQJ9+;7S!)u!rzfIc7)`Htm13T zSC#?kT{~x1Eu@-PPy)lNB3i1bbs(

    iVW(0qD7Pu(NxtT94`EcZoGTd*wA_OL6rzqx?4)-9(@Rrty~#nJ0)Oo{RT-d@c0yEe+8=kZai5wlTeI-EQ~-BW zQCv$npAGI8Gw_4Trm5HfJ8hKoRsi?tuO+Qgw42txn&%?qanCBAaDqt9 zC}Q1P`+cH=0v*IKe*8cpMRz%NurV@qbAPE7o@~spoUi~mPmSi%&+hkEC@DA-(E4fu z+ZCf_sMOcXORj&fRnqT$wQW^7n9*9I7W0wgC5OC)n{V7=Omt$W&5(XCk+ji!_vu7} zvJvwFHxNMl*se`FjDnrsfxOu-Y3)}ATlz6i*j`JKfd%WQzEjY%{ch3ei+ ztu~9KR~LS+TGkp34@`YA7wErJp$WKYaY*)5RMr*s+iZri-XYSSUPN%S9G|5A@=f!W z$B1Emv-3Vzs*R#174o`CcLc8WL+z&%u8%b9Ncyi%Q2WvTU%Uu zRFnSxWpM@M00HkP)> z^P9|#PjfSHB>98lrtO&?f{k={*jMps=4e^wh8~S#hcoHH(#PmQXa;;g^3EzTs1Tof zv03ik^q*p_g*}zy+fF^BtjOnUZO*!@wKpD`H1cNd2G=nKV@ZG$f_C&NDQBnbRaraf z+FQbDAJmvM-3rp2h#7lJiRfF#U9+YQ7owdB3fn?ADt@mvt3B`MB)RRNkFail*M$FW6Hb2;2BJx27ygdbYf?B-_iR@+w7PQ6}acai`dw1Jx%cDgY!8NJXg5IsWIF>di}&zLEX z{K+|fS<}j&Ms106+u3N0=A|e`-3wDAQ(jM%?+4Icg-=BU20o{zO$)<6?~e92dM`jg+uXLX%A36!p+Cx{5-xONggc+e>(ustM8 z%_J;Ktvxu)-(&9@{`#E5_YfRfVp4#h*XiV;*V)H)7CWHChEF2My0iyplzQlOIn5pN z46|$JiPD1*wl~qq89tJ8@W&0a%cyfG3zZ$SV+^l8YCG%w9PZxy4d?MS zhmN~syFYgk+i{=QhPZ3+Lgs=IBei<>?e=~HE7`??E=tzRT2?`vFI|MWl~hwlRfYx) zWYB`}Y&z?|)3nFJ>hv}Z_vH7-Iav~JDqGaJIdC1CU-W_gaSv{U2#z_i);-SX0a|PJ zV&_ozkQI)mloJ)x?OvNKCP%`pfg8_hml z5G%S)!0ngQrAp|T5FKxnw@g^F`L^;fzHS{V`7l{t2Xwp+f!sZ)RV`ope8HRtCW}X~ zf>X7%!*LJQPy@*8Vb7;^Ss@)*sQVJGF>K2+{dljxvJD)SV-{#R-fs8+$Tk^@^p z^y1JuW+-BZV^m0>8o6KK<$PGO6&n}RGNnO~g}9$l+Q303u$xgPHrJ%^i6djBz!(X| zdq;fIAry4Z=z+=O`^2Q{C4o?u1{51)OD+xz#awh>GfR8$X1!`gToB9|I_!!=<=5A3 ziut#Q(nP1bIlgg{4mqVUPH=l@hq6qVgr&j_2E+)d9+JC>|4AqX6wdV@_(Pz{-ni8ub^y*>G&waA!gz32~&T!6o zMpzCMb4iQov{u>lX$gsY$eAw!rdrYEEucPc`#PpRfZ5fL17xA7#&`d#5(%&-nMefX zkj&{&sUhZSAEaE!$$)_2ms|Li5Ykl^YEYdh1+5FS4#Hosf^*T>DrKOb|eTnV2*(tsjqG zo0JT4B_^%QL=tS-WSFHyOsl{O;gRI;NVFjM@h1=zS(-lIURs$HRFO3tCl19tE_;lr z)nq}ei;cM)s5M+a$i|1dI`l-1eX=U=h0?o^4 zj!mLmj_HY`AYlwo@?UllE$C0 zedeKs1KTnl3_xoRMwlo3oH|Gr5nV zk`%F#qR?{7HAL=v?B0H#-=CO|+2i$oyz~uVbKwt)x`&YdOI^};AaOTbP z-YD@n%KUt>(z_5!%Wp=wHyRp=-oWHY0T)`ik*so0#KzM^ zHcR6iMYNBC!|-6Ccy<_mR@}Dnyjag?b1{3EwCKxb+F~cqkDbLqa{2T(7}b#xN92L& z@|%eGf;4V}1avHLf`|4u^dzxX<2S-$H^h_MbV$yqV(_;%c}iO zeG!Y~oljD9-Y=5wj|9Zffc$}p^Vd;t`Fc^UU|M1MI#3Y6e?*@(eVgEf=VSvmHh-(yNzzQX{9!>Mk-6uxal?3vckHS-U8= z7ownT^|vhOJ_h3G$*e3PGv*aV=RS!$)dB z(pLBC_99@5h>Kq*6Qtp*CL7G37CcX%NW)uS#nPE5UBPwy>fU;|0UWY1aeiXM z+AtC%mG|ns%3;_?ndM`EGN)Mri2Hdm(DS)1xa)8*jiZptHc90v;)kz3p%VA}NFV>) zn_@~t(94{Kqwjr_0K9t1;2u4yhoOA+S;%jNR8YQd{q&9(Sloae$=o@{FdR`m&fw{l{Q@#*nh{NYI zi_klQ69UN3^%J~7Nreuu0u4vm_3Sh(agI*-Uw`=vHgFJyX5K(_(L0U-KPc}$)IWCp zcqs(?-1rnbaL|9SD5Hu@xC)&8dx=Pw6Mf#)pSX1oiif`*Rt4%uyhFNHesNuXDDdv) zaXrFrC6Q-0&0uo~D4XGpLUVAp=@^`(p zQT?o^`u&7`(_^lz(bjdg2SnsSO3ox!i;?hpSOGBOO4iZiZUfaEa)UmjK#kPN<^4GZ zaECx^?#Rc#Vk#x#9v}h`^Da=#If<1-=FWy;V zlRuh&7Y$3L(;yW5hWKR1y6)~oIR~KFFr(%3!)oQTBWqVQOOicRGhQ=bd?ReBnr( z$ReVB=Pgu;6G*?`YHtXI1RU^O;%yv=eJ4kGdfT$l{`&kKi4z4W$zAObDK_8L+b9?* zZSz0uIyZ$W>l1$U7q1Fz0i_g>?HCCnft{H!Evjn^19X6~j*tP0bXZ0)k{-O0WsjsJ4@>gWo=-as~5seK8NO&-=MTZ_bskF9O3P5UG)(#^!+M{FTnvf@atK7Mq(>? zoho0{f9fu-KL!kvBaOEklWyZ5u#FYgj+#rJSi@B9A@%;KG&q|ri$BZQKf+gpikUBV zatyq6v0&xqET+toa0{%)*TwB#1$4U1-Uqoq-yw!Vc2VG+_xlC9q4N3%xdt2aXO*}b zNZwDxev2LPGgbQNA#T3#w#4du^!QGFM9cl1rjw`4Z$sF~DBg_9m!u#Us$KMy+sLI- z8eh^I3ry@R?skC!6O4ZyK!w+NQgr@C{;0Ud%bsqSJeB`lDsMzI(=bUKBbpA@9)uqOn`9fS%jSxt)H=et*o^NyEfOj7w`HUWrNkF zpBO}7GLHf?xgY+Jo8FgRT~~p*6XQ1w3_{?-OwfyMPpSxAzC>i2D!u%ia^d_ZKP8d~`7okJKT+-Pee`Njph z`I_vp4iO7_GMh%xu^;4yorkUPJ)1mZaUBtl1%vb*i@$UVegSXEK!WDrBH(H~F;=0!CdwH)|As@)gn+_ZirrO)c zTL_nJl-C)j0~4-kC?~EoANwDO5zrTmA3U-~?Ay`E`oO!E%H=v3nfz;>I~;KLuD>!Y z;^*eOX6a}9cG8rS?q`0TYv@WSMWsd+TQb8@zdXP23u47}fa1e|P~gycTWgDNfvHCQ zTwUIwZ_UQq)~zQ}jh8uRI)c-`B3XMcOy~qikCgiIkPlLMn@W#dFCSJP7VC36WiMv8 zQdJab#*9O+bZkC~Ijlw(apbRD&;ISc3zdeQMLF~tM*mekeemsY2}fLJxo&T1rGcj- zkChnQjw|}$B=n5DqGUcydvWvNW|$}>KthtY>Gk%Z#X%Yb~obE*Yvn`@f=pV)%~ zrXYoT&8%&`@IAzum%d_WeP6o+-&}f{FW^QZKUqxs$?=C0xQ{(o=Qa{;tBwUcx_HLW zP;XyH*#Z@{%ur#c_m9fXst3U4t!W5Cj(SJgzu19o7b>Xpz{(1u>@Khc%HHBAo^ol` z5U@J>Pf5}InRQoMzdSt_PMrA$tz8vx{*VW#`0MuaYq0?CWIIGF#;B>vxuN3stkja7gd z{^q@%vpn$!s>}$n$0mN2M~2^E8w+Y#+`X|_^jh-N%|#`W2;VrCWj2RwKwo6F|_QzYOK%&Hx#j`Z0|0((#ccQLmhzjO{~=mqhG zBKl(vh^FgqKkPO|MdqM-_iQ{APB;huWWYztVnCnJUBr2p^Jg0R*tSkmZ-2ovga)LE zJ2{P%uK28+iJ+i& zUZ1d^rgIu~1EgIionZ3OfPtPbKJO6#y{=NA&O`q?CSMs4u_W&Nf+!9HBVh}TdNhWy zO621~=OgQrX zw=(28`+hv0^S80fC*)FA^w+(O)y^oBHq_1QMc;j<&cw%ly%2w=Blb5~FY z;Xdl;K@*d`XMUbA1veIkT)bf`V+bw{Ly>&q0U`X?c&1(E^qCtEqT60M9_k_RPb0Jj zm8NtlHj9M;at851i4bE#&bY}1j)2KRIeOQ-EY$unxz34w!bvc%mb&qZ0syuPmZm1o zD=Os*|Jg^ti777m$kQfEbnc37QU3Sfr~RMQE1lqZ7xU)T1NaS09}vpcE!UE%b~6z| zw{Tu&1$pf+RdBvfu*N#dr=sh;y0vv+*1Uecfcw`JI*I5s;`YmDNYAUw-hw27K5?xf zxMG`n78At5`#Bc7#s%8Fp00omcz|?S|C!LIZ_7#Y4Y4fQ#~{b~&?oF&Ys$G7k{46f zWj8xqOJaJ7kqP?&#qm?dp}8J$fPJ`*c?j_7UNcuq40^e$pOv;RqC~rVXVWn1C*msa zzRqk1@r%o)dEBj+yD-?Lj~Ab9UM+UwnZjo=;_t=3ls2#ef20JxdB&Ax>;>Nd<69jZ zEOt{%{QHlOhJ7!8Oh>8 zGqgQ#?&hHW1B#={=;xQ8E92{;y50jukZ8=CwC@(j&jWN0)-Y@>kG)Lf`rWJp=o*p$ zy(4Jyj#S}%7*ESv;nUR?aC6;f|B5^TTgoW|$yEZ0!$>2iy51r!Q%3@rH+OD3Il)cehneV$+Q|v74Q6?%jp4axNC6O+!w1oOk<6G+q zGv-bD_!X_>b>(^d*P9VX@($Ydt(U?JXfkSmDrdH@stJE)Ze{DtEGbLj=oYxLqw7h5 zU@F~x8>QEJdO0m-d++Ao|DQNi!t$Q8>h`Y#<&>rtU_!9}n@WH`yZY|GH6e8qW(ROB zdym1X&-PmP{$%KdgS_DBBQBX{+eODNIzC|5IoVExR@RrSvXrNtgOaRl!mZ9_gt zz@j4C$Zib|DOBbqIi(w%DMgnuwX?j9&B^j`_UPjbZ*-F-3JPf zaSCMf%}1d}i9RVjoBB*EztdfF<&9ko^~N8IFX;~@IeRnd_syC_@^Wqc(FKXScCicE zzq&K6^z)#9(l_kvx|}LGH~WLDcm@UJ=nd^(Kb^djxesxHmujE_%saCt@H^K@mN@S} z>pYKW10*nxf@I8E@)hBJ0dHZz52L%W!b+qEY>fs~6J-CQCGIt=f7R|Q;R3rCHhMIm z1vL+;@ZQVlA2(N=ntRcrI=Vqvyo0(5y!}_J&RJAMkc)SZ z?cbZypUslk$6FA8M>m3P)&#V9+gc9JxF@nxl$peEp8aRwJt_pT zr&S_8TJgbL^eZ75!~rOzFo8$45Q=U!=wLatPr-g6mrzj2fCnm_-UG8?nZZ21 zbLLLOTbnRdxEh~`269T*f95|3+2lCBqhdenZ)<%%8H#U9aNrEfvP7^}S- zhfGNiQ+Y3oj30zJqAJ~jWCF_DEKS*Qihx!Oy;gcN_nMGvLc%8jTiXRi^9~y6lu@9U zeO^0538a_tb)=}=GxM&c(@r`m6_|8o{sD#Kp2?Z z*WX$#$1z&VO2cIm`TGt-53FMHi~ei<(P#Sr0-YKiuyuS_SnYhjm2%3IbUPj>xccO{ zfnW?uppSU^O|gfLLBay}?5`S|j%_M59(rgIlXI!CK-;b#I(0IhJ1X+Dn)Ih~)n)(G zR{0u^Rk)>Dcx*$9$&xBFlwJC|+W${H4Xvm90W%LEm+z z-e!mNsG7tH2jfW2O$lIgpIqzhh9kuUVM%Pr=TJ_E2xIS44Iim5Hc#odOAwD#YydYS zu10VQZX{#Lo9bWBJkf&@mITQQefDA_F=yT`og#FPo%Fg}8c=y?#th!A+a)MMhZ>w` z5fPYLnyCMP7OqwiN0fx>at1XAY1azg^xee1jHPsMl(SH|@VA#Cv3|ilsK&JnwF~Gw zPxYaFe1+g~(u4$9-H0Fz!vP^k8O`5dk=(sqUb$}8+dTG9;Ep5gOGKszF~XpjE3P0J z_f;`Y9Faj`|2yRhvhkP%0Po8p_U$3zIKw#AE1Tr96&sJ0BA&z>j{AV$yA8>-V za01DSU$6ASTaO zB}A1rz9tOR9ISbV{p2i7bExEQBiU66&81at#iQpe{Z*m1cE6ACgi>)KmfBApi^`o} zP@UT`4zFu76@l<;MhOeA0%T`#V(pj&R^6w9j-U{CDVKDx;jz&&0i3~{0{0j*u=iC^ zI98t`lCgB;KSx2y05PSh%kHNtbH;5hu)CqoGc6H`X8Qv6KfZRNRHLBj=nWK~r2T)a zoJv!fN`9>uo4Hy`jGwpinYCG??!IL{&NBtQxKhKWC(G`pOjncL`nidcwaOMp)-6Q* zivmZzH|Yc23o0&1Mo|1hwrWMeEAnCf!$6gH@Qa*mQZn@ONgH8^5Z?DJChu}#e2bUb z@oHqWRsWfaPD0Go*&<$*dpN2ocHsVkQV3bb_w0yE6;jC7?w-1g#NzNTAHqIYq?lGr zXZ61ieA5k1CD_}STAcr8D@~n^=L!`?O5s*LJPXY9)@rwm;usaO+=q@j+IGJ4>J9<} z)-j$xwV()VP7Lw!d;gT)=LNB;Y?+ zlEtOk6K+KkCN|95ysHrx@VD1)&e~ls^krAq|FP=>`X*?>CGM{Tc=6hOxP*66~l!rL3L^3?ZfJLC>SiO?xY{1IwKcdmHKLr zt-f+(>z&nJPD3AxCzN1fS(m3g0>diNFEHXo4fY_oi9bC)+=CAY4@eyVo|(r@QDi9A znlChvI4@wSe5UEhgiElUqCH9!T&{3mD6^|EW}wM}Z|z`Ns8e=yk_0rQzwzSNU{N!c zG?3seaaNft>Fz=m*+0T^R+iCB(K;sw$d;=QPj|L;B5=}7@BYGP|)ff^_@iecBvRmH0X$QuoqrwN~wq?BoV2n#OsoP z>>#lnkItaCkeuqeol7VoU>ifD1Y&B#Ui?0oFMSxkKp&?3-b`6I^cZm^ zL&%Jgw@MZPtRU7b9nQM*BF+f3c7~k%K~Z`{>H8SJyA!1xk2<8w);l)zc-`%FF}CA| z=~fpt6vc~`;P?053*;;qbx~gs=~dSKn7b~^rp<&j?443uR>#w2)O{UJX*$t`Rlb>0 zfTu6%LNj&Jj3eWgA z&wjS_HK!=<6#HlLldjjeN^TCL0RuEU)LC)MyNbv!N#rXb!~~vq9>DFJ#r&ty&)G5f z`}yVMjJH(EsJ6&>?;bcIz?LW)wrtr+z2Bt z9a-x@v&Bt-y9&p&;LuDd$i&=93yNS{n5;+cxXn>X#+mZ4y~Dwp&>H+k{-?&xm4aTI zsxsfUu$Ywzox=z2l9AytX~vcm!}F+9*28JU$V*Z?$xHKLlEeK`-&h`~zV+Z@e3?>b zVMn{kB#7}w9z3l$!Fo4@sk0Yff7MF9NINCBk$pbu)3|zp7I0~))L56(yHWm+rQM%~ z8e`MtCz8XE+>yvoNNUb*Iq>^D{-huGdjDU4Nt6|ENuT7$2K1~hSAn6^JLpsWXm~*j zm_KPW<0{GkcOAbx$4;yaBH(wX^t(Iw951Bj`O;eOMjzQ8__U~KlADP#?hqLDC93%ym|9}Uq;ew-{ zcNXirwNJjiXik^x7cKk=Q)8B6NSB4$LrvODh^=w`CB0YEO}Li?NTpVpvtdM5{+G?~ z2d{wT8O;v(aB(kih>=45m+SwG?F2GBY%WFe)uhHgaS@mHb%jWQfHR%AERcoTn7ny? z{P>yAQ%_wR{gsgI)X`4LiTf2&kbO7#6C&FrHi0jfkW2o+X5@e@T&O}`;JNw0 zy2mcS6+e0T(e|38e{Z3*a}3*u9Td%OZZ{P@wI<6+XERn{z3EiHqz^zv>&rzBKk3Ri z=8Gz)6mo8A>2b?N;P_0f=?jn>j)_ftUopdp>rXF)LB}$C+U<3sFuH=U`M|S|)MqEURqSxdg-4 zM#HZ}IURut&B?(jCra%f^`q|~>7zd;O8GZ_tsRM0f0DQ!2!C(RADi5iM8+@jPD`{z z6h!++Ure6?f3;c1P0W+Ou$t~2%Ga0}R%XlFYixWT_Evf-YjV$edIh1TpLahVpuE;P zx)i3wq>(SSHN$V*6y{zlSXAbEM^sZ=_O`DR@eY!{5!^>fibhy zUeDQQDyqf3vBKM@RnRj;5s1A`bu%5YcCg$-_>qUxspXN{rp6xBCl8nI=D^sL`dNR zB(HMhYDx_v>x-3mBUe6Y4IjapaUi?9xkJWskp4YD9Uu+Z*1xk$$Bh?C&f5N7B+nTK zGwS|<$iP+D@6~lT(+75{IN_f%Z$F4)lhv^I={Mc+?hu}GgC+f5{L|r0zm~N0kWTRl z-X9xW<46BN)_INR|4@mLGH!8oh!?&LH8=xO{m4heMNT==Q;pQ%NMmY9zMUR=3?RQO z?lvD&&M`Oa|6vQKo|U z2#J>3JMrgwKD8cgUe}O&J*8a)M}Ii&ZSaD7_`fN3Q=qX0dvE$3ptqmydgc{TDFXKn zvbO9(r69`oJ;?@dch3$=mbU_^nd0$ipq2np=mfiC$0pb@`$H$hIw>AhUTC| z;)6&;%&x)M$cQ$V(Swp$f1ksH^*HTc(Dd{Tn>BELOuI(Gl>M4^CQ#IRF(3Wrhybi? z>kD{4zw7bv~UYjqtKCrx0uzn@=ZLt#ChRF9MOsE1lL+DpZ(%q{bMwU5<%7 zOSUZ%n;+~yo#Y=1g46Xnr>z2PGgUH7oX-ls+fm_;+tK!S9Lwj5|7r~j(hP=Xhdp1) zKE1l0^`;Ke`Y>N407WcrY@82m58Lx#PfY8n;XJbTZ@1LP2bHFNCX3CC54g8!%Uqukpb1sERxlide7FufzNjjHW=$ zsT3#@b1vzwTi)X!la?x$&QfiDIfyisqu!e;e^A0a(w4E9-FYU3s|M2Q)p!Q)taG^r zb184Fv75K$q#@Qjb#p0|+$<%o3gcAH3u5LrOD5=Pt-|hCW=qxL!8UJxyb>~>4a!s& z=-#vISBA_CnGebLaIbpk4=4Y#f4CP{mN`~sf%>APGiSF|i|M5Nb^9uw zn}FSaga3>FcxpGln<~VQ?H?Xh=KglG`G$|kr}siY8E<)M=>=qiz+EJi_zl-^l7WR0 z@T^dvg&M#6mv^w)J@Bl8J@Kn!N1~rcN;6!esYZYAyH=clA`Q4=g0`QGASer>uSn-@ zU5lu;i7sVLc;0bFAc79P&Zal`mc9)4Y*l z6&OS!$G~8ZF&ux{lqn4)@5X+MRk<~Fat=Ur64FrpoNLPbF{*s+dJe)d_KSE}4B*6l z%^N-kvj-ll?VMW3L<1FhBLcU^?ApXBn!P=8uYN?SIuE&Cb!X8s->Lk*q6nW6-|M8i zq#nJZw)6bAw9w^pIA<()=OKSFYlKmd>&K9qe#oTj(o0GSIHXXflI@yPS#q+T6K-JCRN)fbc z0)9mk*7G^Y%=H2rY-YP{m0$*5$0sPV#1t(@oqUHVnS|~WZj)Dww0B=lZA*CAQ5}1Y z_n-kN@?_?MO4>s?$oLr~kR@0lKsBt{PKWbB@?1>7N5mSKu+#Qr!~cH4>cdE8>@V7( z=?i}UNlo;*6d`d+UkYP?Lc{0lGds`OFee$O=hk)G$&Pp{qj@W7D6-K)+2j6gV5`*Y zenAz>+=MNZxkw1rnzVlr+QQWVX`#>An#cE& z9`fXumyb1+(HC2d#nQRn`|+B%(5(g%6(z$kweCWkV)QQBb%1O}KTjb8nsHkQ__!O` zh`0bu@N^++zMPc59P`h8^y|-H*Zefc{+Xbr=*r<>TCycm zP7XXE(E3(BVV(bQKWKz^{rqnIUXy>Ho~XMo)S_p~LHSXiYujy(C{y*sXTm3S6BX;$ z6pHS-G~7yWlj_M*f2cJ_hp(`+syGqho2`!9=!ztx(gvv7(T!}4VL3n~% ze^Ibxc(OSY7Wjs5I(-*k1YwHd!b4qe?oJQWa0J3n(Zd9JErr&HaG6e z?2#z~oBFe5T>t!zOQGrX6xPwlgw^71?Bg)~mTZCU^%pVlDN`ygBqBPIUM&1x8Txx+ z-n7K7Ek$b}{*MGY7%?(_p-nqX29#sZo?v5to>7^?kSvJ#rauF}HSeVvdn6!>I9ZA; z=4XbKd-D9Tjci?1LO$3?KNquMKLdv_`KGmd(%&xa3=3n=X>6--)%E~fd6W*+)BUKM zRAFz*6XJqn*UIoEP#nQaX1Z^e9|!x>tfl!-)U&OpIh}-(+6HF5!z{wx>)^&sWt^}y zaCDW}t^ZF3oKrD36xwVUJ-RioPmGtN_g^mEz3TNHmCwUE4U-S-G+oZ+c!bZpl{xYL zM6my`z;l_yAnFHw7B6P;=P)Plm7WIL)~AQBl>fkpLN8a30f#j1yQmSrA5$Xbu-WIk zKtND(!yZJwP=xg5G?HjPPadEmc$c}6^zu6exA|F{qDtrF+ePoOgb zrHt)(Y%=o%h{w}4+xJiM)t@y3^Lrw0Eg%6;O+QdVViG$_cz!PJD0?MUK2`goJwO6s z*kTd=rQZSUz$7J%m$}xgZR3-bI4R_#A>JY%ymB6EX*~p=YPs%<5;QjOQ2c%jh^Ii6 zv2vUPJKHV$Y1|*pszO^SMv}+-4-2iin1981=cA zva%uZvDEXz!2{$0;|XxXc%=1yyrl6Dl$a#?D$sl%STEKpVziyfKl7;PD3 zA#{rHg-257@ONQ$^F;Cij)4HOcuzpAS+Qbxx!yPJBw*^43N{g_7s@J*&P9e6Tv5hx za7qU~dxQgW14)62i47e;b`o#enDMN6!vh1Iug=HK5_k+k>Fd`kEnbif&VInq2U;lb zyP>G)#+gjRkviZHj1&_8nEsKcnfCCTlpHQjiPw1IE}wjr64x1mx4^X>pIcLB)b*s) z<1~RMM-4!a>%C7ZL;ykv2UmEJqr>#a!wV1mhI$?^dK;5pS;R#^zw~E=8ybuZRW1Og;Iua|!F}c<_nOxMN{3Ae=&`**kAhaYKPeDNd!wtv^!`0)Wf)M5rO4l7Zj)EDT!41#MuHFb z$)7)__IM`X zP+gGKcG14D=X9CbY9>hL-f~a_jhjffKhidTzD9Sa68G3rgN}8(gy5`T5{eSo1-{;45Equ0(M_G=#5knb$}o@S`XigoWnkm3)Z?9a~T%wN3IPerBZylz>^*?)UVwz z*#?vohx^hd323>=LA_V&3MGm*!U|1E<#fGa^Csa;7%jbY-q><1;0YRT35%dI_h z#tpjni5d(a7PH0?-|y~yzTK%r6c6!O?s+*|3nM9M0ymaB;)B)#2A5<$rlBOE@>Seg zWgNnSI6A=2{=baoKF=Q1#3b+fu&W`Pposn-;;;?a{p)hy)l%GVn4%Mb1r6>ZN42Dg zCpkk2FQ==i|G(K&Jjp^SLilwH2Xc3vugDz+T!2^>XyFp^p8ArW@da<9>dPpoyD|{9 zZ+2~A-HU}H_?Cxz98cxa4oQP6x^h4VK#8c&_O~H1DPaz>0Pj&Eu(~3ybyvIh!9DuT zBX~*Ft3DpSpGw=7J4{x=X@KJe{qH-zOW{%=!JOk2KJ}c+GY0$To%=0#}0L zM=zY71IKBEbs?9_P-47yjS#oL<-IAkxEgARwrvRW)F_jE{`(H$IAnB*6Q~Ny68z2K z+JaUpRPs0D_&iZ z5S*wbdz-Osb^d^ezL_#qUZdNTq`PI8JwuZiTi>{8OwQv&Bk;qfJ43rI%Lo0>Zk@W- z#ea+S-T$<}jQDvHXMVZwfY|tPW;tVhp|{u<@>^K@%i9Ex|AQys2I>|R6p1(tG=ih0 zAEp7X?3$0RoxLdroov29Hm zjXSl+|6}W+eRUeG4%Kiw_${eE`FrAjEv8j$o7+qf3(`4AsFIG;4~kd@Q1u#)vkiSL zF&C6W7#?8gm;!KP-G;gx>U@D8rrmF0`zzR7xjpjw&0w8{b^eJ9eDK9TqKEJpL<0wR&7Ba% z+;~e)|M)Q4l~ckT?!Mc1FC z51zDM0g0#;FG1XcBq>N9Utu11Ii!|2*w+!!?6O`i;rZC#kX!5%FWW%88M1Xn$@3pj zp=0aEo;TO|$2ql+)6b?=V>m|vG1*&tfvX}Nv;Uu^d<}e>vxDQ$6`uOdYant~zyJJs z@wys*=~=tK`4gew0W0cLM2z&h!#!*;tNIw)L2b#IiHko;a}Wtv6R|wPcf3!0%$LJ{ z<}FYCn|1uS!+e7V zwNpW1-nh40mrhg##!DkU*+mIPGFWOq2)*2H?$I3tHvCgN;^3=3j%aDK^rrdKY4U$M zG<4{(()VD(g#55z4P{ zZ&)+57Yd6ahtICAfs{K~Lh}|hCREvO>MktrcCs9?3l8yuz+e8;_RlOGs;z_Utjr0- z%$;5(XaPz|+fGWjGLGS93LkD{8;o|h5_R?kO~~8R+9q7n;0rYa=KuqPZ{ns1x4V~l zcSE?QW0dzRPl(80pGP`wW-tTFa05fT<(FCemtWxYzhWMh>On_dZ zLmh`Hv7OHk0fOK|^;XG^MD3)-+FmN7Dbr7qw9}Hj`^FgO*Q_nT);%_O4<`+s9vmVe zY<2^cp;9#`8)m?V+zPmoz)l(LTd27Yf-Cc2d3Tp{lgiub(ni^do$zp^tHVf|;CL9(6}F+> zT*@%NBRmT^g#U#OvjR7+h#E%j-raZQ^c^8<8HgHxLVn$tKQgEp)zXJOGV4Fc=6E%Nqeyz+npHhb(J!+8BP^#$ zNWu55u?_7kz96dX!`Hs{QuVs1K_O7A)e@WYDXhV zG#dQCl(}zz!qBj8vJ<#kJFsjzB^C=ZyEm*>yoesEBH?q^xC|&e33N50U-S~C;t^&H zk9Di?%!MJ;WDp?nA3DG?^eaJbyUDa+_rx4owUJ4ch^rlOKkM-49tXP}Z^n1GfT!RI3&DGDXp(wq6 z$^qcuu=g%gC&xAR(Ir`P;+tO-`lnq$q8l*Sc#6CCzJXGwb_7=Rue*wNMY1JoKl=3R zP!kv9kz`yZ?`CtmpZ2XoG(QacD>5w>=f>iF)5cEU8LS$#&jl9le=3cg&iVKx^71H* z)bsz~Z4mk0mwcmJ4TylqPaa>$51TxKiei2Mu|#QcC!_t0Q>xIKrhF+DRdX{#EYt6% zM15`su>?3!<;(uRv&1u8c@@Vm7NN{LZOSar?ZAc4c;5ckq+dgn+e()PiuWlcnN?il zq2CoyJ6QG!m8{%QBLB->1ik`S_<-5JNe)GQWCpfj-s;)_2kJ2AEirn3QjEBZ_jwXi z$5x9Ym=mSrkl$B!7}b|iFf_GSQVI{m-*2%hXPB^8*1>60Zz)s<>&aaxeg`}Fufqi8 z^kI{H=CgMUTNAt_mt2i3%NhMYD4@H`Mu*Xx>HE#e1^+JL|no*61o5l#9DB=}GOg6VIDSZ~aP!Edy8SBA< z4von7-c*Ec6empt@*4t0+tGvMic;h{P*1V<>lWf}*=>a(f0F<0`s;$hHe>!3(F0~q zTM6UVMXWUsWJuDmcQ2Fyp0i->U@pq*^^1BVhcahNrS(Yc@&B-ocaZ)WJ9OzYPnIzT zx;Ot2hZQXFFzG^4XGNo=fmbdzS={bIgzc8{>k7(sHMWDu#x;=T2!;H1oDuik63=zU z-LVM;Auobi%aeUMQwRN#P!BIL^y`EiVim1+XpMKrDF~bifv;PPM2A=w`QL5h*u5fI zqdo6sOmcPo@o{Y{1j38u7Usd{C8&H==0YAi&`}p+V87L&mxcb z7628LW4R7bC#=W#Cvy3sUyXgd$I_&oKBcQ0Kd4|a@(rzThn_{?6nBD>(9~TpTBmET z8FB^GeR-D>QFn`?;;MGk5AiOIQZBC*?;%ZG2JXtgS=$L9M1#gu#ZhsR5Uy1IzjC;} zCieR#S&~>^#$DG}#`RBdGyc>T!P6oG3CFyMSl=(&7k~tcOI0yD9yEL5#3?j7wfb%f z+kvd#Gg|!;R~pj$_>bK|F578jw_Fq_dfUixl(Q0GyZ9=D11tA_PTbSTsa9K#HkO5Y zV(k)80%l-$mw?%-M!w9s22n#Pl1iwrQny}GZphBI06Mrl}|WsJ6JKM;&j}iYcA3L?c zb(1LVBHVr2wr01h5%-S>uJ_yMta%+`g6sQbUFjEKA;&%UeG_3D$X*u2d90n^8cyfD z7Y2q{QAr`oh%0P^DfK(!yy?=2x8myT%4s93_kuwF(tH*s5&u?p`{1eKsGY-}`R%ad5sX9`)&>F!UpRP&-z_(`)MQB`MBhzi34TeQ;V!haXjLxpS(f z>)LjTTpZCqWLE)Kz&IvJdQK7@qFr8%iq>=g2S|ANq(lw)JJn!!R{5`v(X!T0wzsLV z+2oxU%fv{|6FpoK*rL}~RXtZWf7VIlv4~yfh$yPSgD2C^TAo!?Mz9! z7ghS>GRl&8zv$Xlzx}v&^%z(kdJDR;cOCS20XbtR`?7gf&`-<(^4;~I^U|i?J8{5) znj#*l3~_j-!4#eP_mNd()n{(>kBCmYb~7$)nX7P~YrQQ{ejAKdTiV|Mf`vW=-9gM= zFsJulAh8uB06HrwB2@}fkUilXhBio=PAMUuAii>sAf<$I!fYe~6>J8S-rZ^}4SA(2 zIaTn-T^3@wJN>!wABdKhv)q+PhSkcQg-KGtW+oC^uwJ^v$)h3uy+BEzz_Ds?cG(BgQd&+3s5^9TeDLo__pst!TLnWt9^%C`Z$bP1P6E6dp zk`*L?1F7ZPl+iWs9>D$}Cu5q0IPVe7jwMqa^>FH)`EUPM-IxDE^?&i-8T%GOmSQYL zv>;oujIvbkWC^Ln2&s2MS<5naO4bOecZ7(_lC2a<%#?kdWM49NV#e5KpZmRizCV2b zhVMM)$9wL1-Pbv<*W;Y$*$SfHvE741KG`21{9?#XrD!57OcIPZrQf{sAkIxe^r%Jv}H$-tSvxuv~%h3ahU|CR^PDPT9UikfG}nb>L5 z#g9&KJJ)L2hBEUvo-hVQu*duJGOq3`t=^t*n(`8F?+9ny162E{csFc?zdhK>S}Osr!90lK^}u!QAxlN z@9weRvsVo$%9L086+%!%wL-tTERe9hAJ`m zr&q5XFT_u$p|1nCVPrPO4p*~)(s(aUcoVWpCsz|aCK{t7Px03MUPT|0%EwWE)Hm*d zW)B@OrFf)qbPn(@(QAj9P3jvrG;!nX5vFXfDo{6C=@kXCPY2gE!!~3(93V@*%c!^Q zLHEsy4`g$>A#M1gM`oOeCd?b5HZZ1Sd-(Y!fAB2as2X$Y0_0RTP*gjRPL|}fp-@ND zPg5_zV_lIJVyvu*b6EiA-4N_iA8vx3v79y8j#J!=xq1DZn>(BKU8_4Z96NC?6EnMI zgjC)kdmaE(tLwA5p!;U+W8fY8e!$?4CNfj#&ZsSDO9{@=R)lP6{adGxcNb$|&tN{# z8V-I5tt5LsBO2nTgxUMhkAQm!P*wsV&9{dk)j3tGtO=CCcBj*#>yhZzZR*R6u|@kd zG7Nw8z_5fR);X7C#rO|!P@O92)#4$DVfSrkr?3+fmLZ}P>4#}U>#!B;&t<@dBbmWW zJ@UDpyG=kUI;`?B$Z2m4Ed{ec&T`}gEy zC7n362bMX1RB<-=xJT34{adm|Huy2?a+ij@wct^r@NX*BAebZN#JxDqXW<6G-T!Je z+d$?w(61UL2Ig$I*3S(!d*$ViE}yRAB`l_u&`wv2G5+mj1Y%aJKg2epTZlZ85={nG zju~M07M%O@FPQ%Mh%>cuu;0F*HiA=bfS2aG4b6sMMd^UGwe0`W;jR> zH|yb*i;$-C`as5K*e|l)C>?trW!c&-Ti0iif1YlPiuoB8Szsj&*;T}IjP`SH&)J*P zdjhd)+6`O3R^|koWX=yloBsTSg$q-@`hfs^cv&|Dl+?q%8jW-~91Eaqp}j6OS{ zPD8~V7?{AbP1r?+>Vz7>v0V>5{E%d^fzj7Nflx-L`9%#8C`!gn|8*KIT!iaDLG{H( zdAoQyUWZ5Ow67P|q9ZUV>|;G*q6BAuAVkqZVk2Jurq~6bgaCL}+54=EpKB)Vo`FCw z3H{%L}Tdd47A6Ig0iK>AP9YpHAM6m+hjx#l;VY~vF|&T7xI|%Fb51t zlms6!b$=6Oz;pW+Hs8wO+MlLdOHE0$Rkl*qu!ZM(uA$c>BlfUVwn#N0b+6WBGZxFw z>9z!3`VP#+mMnZw9cnrk!J;H$;pFe@ndBS7Y>DO5jVOM=8XTD~EG8-dCS$VJpdbsL zfS|LYhSPDS0)7&?&0*Z4h_g2N?*YnzL_G< zaU2D3aOINTy!d@6$7vlEJ5qTxmn@)$%LUzjQo(?2<>*X%;(DX+PkUi&5Wi;{Dwlh~ z+0ZjhaKLNwT8UZcOF(v8loy)xE_0s+wi+qb5<3$xo)=uEJxPp{Ve{2 zI0wVX=p#zYF*hxoMWXC^ejecmyig*x7Q=B>1?cMfc%iBzAjNPCe>6dq*AlDh*A>t7 zKLO-*NQ)9^BK&{}m;f@h&kN@VyEAgYHs-G=u(9S|vbHKx2hmRRmXiqLzN3-ITt~8i zUfQJ32B%zH-U;Vtp03_*09WiOK2_ufw>UO3@{>?btTbd5JgfZ`K61Bbge5_+2tK7UHQ9Si2hOAbm?G#45z~}>`Xw=)MA6e9~;NaI^(NbG#<53{r$j%gCMJL87edXReOp?XURbA zP;KSWB=aEV;;K;_b1VrYb0v3ZZLES3aSr$3j^!YjO<{@4uV=djssf`VkD?8M%8cAv zN$wK?+a=UAm#mPn(zeMS)8TK4uAxv7!DFZ&+<;Y-G5Co$EH(k1U=Fp%SbwKpxe>wj zr=m|FLaZ!`i`W#oo6Tq)z;FvC#)O)Lxmz9B?@!PxpV)8V0#6I+ooQbD8Epnu8VHlN zqp`5!Jb;XBVe*>=2~H5V;z@}G6;#HE~(cqBT(OqeI*q(0EVx$%gyp{Id$Ec~TWQ%o+w zyD**%nG4|$aoZVc$mH|N%}|F%o~h7z!B5}CAw39`A3|)sF zx*8b;-(%r3R(Q9^860d#^)6_|0dC=wv}W&kRu@qccPn=#u)bfBvXV@9zx9OX|Je*u zk%1)ckynXRf}}{BxL#V@1T=6g_$4Zg)hLvp%vV)*d`@7KLoDA{@vG0^P6kv!%rd=dF2^PoXgGrgMuqN}Fzdg)^vlw$-)|+8cR$1g+K0{{P}j~_R1BfoHYTdG;O^Z zSJa>P0?^fRYgC*vv)@lxgZ_F@y!o2BLSwc(51j9 z)FYWd8!(sJPAfN{j@#j?PNEG-s1LJImUcT`f!tRW3-MDo9x9?AnYp>?^D>l4fsc9C zjm;=<3u(Wn?rf2`FNMBo;=Gd-&S^SYL6mWJAW?VBof(YCw4oB{AY!Dm`X!e^?Ilt$k6u1@41b8}7X z!W4Y;1D!p+dc5v7hrgeA73mT|YuJb?As^e*g{-@@CpGq?<)worDX|+7>2i;)wbYtF zhkeigjEz94@~&Dj-d8cnuX)B}Au&%M?Hds()9(?s87 z{{E@V@q6)|{+Elt?r2L*7Vbu4R7D7sk8XAD=moZm;mRGy@Y}!sA=9t#{*DR+k=7v@~S2VV7)m+}ll4n-mQzRX8YN_dV_!5_O zpnHhuOgX>7D4HkDWHGctEwH_D@@AQmHZ^v`w@HTX_+GPPmZ37o-Ssq*hwPn-R_<6; zUu9*@e^T9BRFYHWKkhvE)w+9J`$m#Pw@j}igGbNBwcokt_N?>Zq>l9grL59l8*Flo zf{ggGS=uV%8dp`!Kx_17t-nk$Y z+_S-G)q4J{Evjq(;DPS;&7F5zU(6lfzHG@68P8TPCHHDQ2L~3do{t(kwuIR}qv|?x zk4ibexLVyk&G|1;Ytr=OXTvA1iug3@W+bE9&i2CZwz<2*yq30wQ6UeL+J}p-w05n$ zYMw~Cxq0t-#DM9$`Ox8jJ6F@aZjNWSmDDKCIv2E7*LknraP)y{!B* zc_`|B)Ppa1UA}KxqTV^d@Y;pYc!^;+rPamf#@kLx*yOmk@x}SrlHVWijv8D%FsFMo zwN9z^(>E|c!yIkHwL{QE?D^S|!AFizYrF8S3$8&#XmGa-csHiAkt?eOQK9^=_E7Dz zoy~a{=>8ZpYPz-_n;Zkd{^gc0wY+xnLR%`z^>1~`ZUu>tER@vER>lvg8^S!K;O1MA z@ivG%JOjhSm1g%he2lbC-i;v$Dj&gRw^oX^)A~Z6?mzpKWLm!F%@d!p&oH!@76EZ+ zgMO6_?{=ZgCmI|j62LFN@o4S~4ZuVFUm#p7-%S*rk$hE3$GH%Dl!ri68(D>6l#>vSks4z4#g7IR={9^oK#|`qlra= zBjV})j&qhW6LuN53D$xrt1L@zP6hyQOqI3An7k>Dl4rkq`S-k;nJV#E6^!Y*9-PCWsQ;XM8P_ zfG1K$`EJM0cw0Q(KMI}-Y*AV{JmgkZe+lesP02cXyj9jue`wig@}8wEBXOcE!=Ns5zp(I+9wRt^xq6x)$yaez&K8M6g2{xk*49?VASuo?jC{d~#hBqI zyVDVG1b_bXP(muebS_Imh0v355D{Pt%jYAQ5Gu2B$PiQ9_v7p13G*SZ1oI@8$NhPk zoVlW1@P&%m-rhF(5I4U4Ety{b8XTtu9)f;%fY<6l1zgvDVV!18!7V35!%JXV^iFS% z@i`@sy?hH@$I;mzx5?( zpu}kz4#o8e(3HP{qiooZ$V?_oXp>u-T|nk7`T7AP1Fy$C#FF>)0! zlF=zmRnL!?1P*CwEtFmQFr$%@l7b(t4J4sEQ)s9l9xQ{sNz`bG4r_X#WESD9XdJ{V zQv|d1K|2hM&Q%l5BNhBsrR+`D$95>=G;K^H8&TG=sz4C}G0osJS*G|3YjG7imWZVD zJy^Yye=S-c;dH71(ch4LI9U(Y!H&wH(;xjOO$06A?lh%kempdj?|P5vXxKf zwafJa#I+K{OC-!9@HDyU6z5t!x75=gwMbz%HIs-ey)8)b44>#m60+$RqL{b*u#{Mp zjdHi>GdnneLyUMN#^)IOhhbKd}&;Az5j!{%Jgppt*RDS*Faae84k2-kY}WT5Tp+Hqr!Zu zQurUqKUi1sB`7?+MU+c}sX6Du%X6hwnX_Gj_}|;(l6a%5IJzDcls}E=v_{?z$N5X{ zo3Y}FGvRwUBsemF3*;|Tsd|^+sVDH4bxWVSo5qQU)W?lt+iugfvDl_ucPQcf~~^uH5OZz|b(Z z;RL#Sd|P~{8CpJO3?@P0BfKMfP8uDM@Z}8%y1>&Ye@6@^_1+f@pV+e=roTUje$t>f zE{!{Y#?#@J+<^C}7D!kh5ui^f7pv>lMH(9D9IpFdb1%;GS;o;jJqTE5M$z9|gjP~F zc)>?%Ov8b|Sm=cIFuZS?T-P+AJM}!B);zbb5K;t36cB#=aS?F@13opy|Hto9_3D-H zsO5ylo^|oCN2H$ZcnY(rlmk=W)Spt&R&|h^I1`iGj`I^N)2wPG%4}w1 zuom++$n*TY(xA%$imUz<^#rT|PqZ%o-$g(lD_4%4jGFcR-!Tk3BccD7v-JIk%7XvD d{Tt2M=+sZiTT3}FP3aemMsIufDHs=5(rtmWV5?D*&N7bLs=jRSvCnJ z*boTr1_Q>pN|q}&mSlq^Ys$TM?i9(!@4o+?v82H{XA>fF4mr#@-?=jyjivwb`@P@y z{r|PEx#ru)w~uch-#)&5e6t?_u4UZt4;I&s7o6+6klh*7YW>R`*`)ff)h7|Q3NxcKsOmmCfUy1Tm@$FBLW0d%dZ zs{d}uT&=+wBplPc8cjLYfOajd1{YMg;rKBEs;HKQ8XT(!jyb3ZQdlKZsA@zxBCRY; zE1SM%6b{o$))x~5-=lIYu^9|1fvKgW7s5g~7Q&GdQXnS^KuH8-;DQM$VcL^u}0k-`ZfoZg`) zP!AkyR5s*&nbb8X=S5wY#9Zx=kmR*eAu$7x*Q`_!^q8snx74O zSuRbPDq~vNsT`GX9JLu#Od{+RiBPNb3>}L z#+;X=DMu|gK=vrd?KxqDM6)gob1SDHh~FQ`$`)`1(67ESpn-vbXDvQ8Osnbr+vG|K9rT?&;2N6OUzG!v0574vjWJm~yg~R#)JdlJtZV zk53Y#(VZ9t(~sBVi!Z+L3g;uYvy_KxdweR=5?ju4PV-G?S--rFw3Am1RLH_<-u-CIG zEFzQ=s)%W&lm!zAUsqE>74)7;DyZ6zsv0K+iL^5Nm5-V-8$mgu#nbDs5oE;@hAb_5 zfU2q1(n?e-i^r7ZSoSgTtZJV5N)q#xXM_;Fj_8sloq$0&$r$K?_Bk9OZ29b{ z+3T27|*Z7gJ_m7GtN6b5N&rPkAj z%9&mzzK}~7z*asVN`=ojnKS`e2uQ$@2uI9UN@TNE&c{|OWibK8R*epy5u#d&vQ2pU zqS12hX#~XxA?w^DgiOy-1)vlHOQa!-Mli`eeA+mCN}l1B=s^S&E~*s=P*y854cWx`%B0K3Ky(H!=&~`G!`>;UghOP>$OxhJ zIq4i5Qq$ah4&j)Rb?kLGKIEstRKm)4=TgXPI$*trW6m8d7+ARwru&v)aAhN`s~Vx) z-3Z%{9)RO}i(p?<&tPMEFB?-)!RR@*NX!J5E)M)ym{w0#iUSOoU?+ zX%*FoK~CnTB#t00YcSZd8p?nE3eGpS!TI_-aJ>E}3@%-bzK9I;(QhmdJr2hQAK{W} zcbn;@OMI?kX!jndk3Oa;N6Fv}mYRYA153Ff9(@M(Ig1;hzVRA{&z;pibH$7=DKcE| zZ-;&TY69d)!7)NAWZ(0h7T6bZiZ2b(s+Lm56f33QEabJym*(w;qf{`e#tD!dwun`# zMN2Cim7_#+e9*NqW$Mr$U4^sklKa>QO3%OIRZ%{xTY;W}CfN6PUR8>}92bwDfUT_+ zgT`!}%TC3aJo?ne*W3-@|M%HF1l5+t3Ka$Cqpff*jUgn$Y7T{C5xr+oFdQ8rurCc{ zrKLf)`N7^!Sj!fSQxzLCd}<>E(@J8sIvMzMGsMCA{(FD%=d{mueN44*D80W4PQsCj{6=~mxoiPp(SNGvYs#@v zIdat;rrzvv0yGv*uzOF(5?hCJAx0bt%EYdKWNh+Jz^SKx>Gexn_a7R0ZaH`ehaUa` zb~ZF)2V>tu55u~5?^O#mZP|pLXP+8PrQu-=ZhHs&*4~YsEtS~WT7!e@*TH<~>d(4< z^CVNOk?IN6;K-$8Dprdh)LI|N^c|akQ$3r2W{ne|F>tIQp79~B7QG=kI24qIZ2^hc z=%>Tcr(U@FZ!ls6U`;TSDE+5fw`_WFOcU z`$DOmr@>J?aOB#V;{#|c98u1#VFVOkgWk|=91KpwmVgAj>KBJ2Pd$GXp#GMUQH(gq;f_!jJ=K%>E2tI99S3>LAeNKD^F@x?(Qux6c`;KFE_+ zR?l9t=#nK}6?(!laUdiGn*;TD)jx(Q=B*(0g%>dpmjaW4kar+r^EhO zF)rxvN_a5-H#`u7hYoms*7C@`7|e=97zWq~p3Vxy-ZVdKO7g)*!#wOR ziC{{)M*mW}cEd?H5|v}CaQ8Y2)5_jTCDk*vYG*Ok-0D&4r;n2dg%)_h(KfvFmFNr4 z#33qYOAr@IK*yf;2BfT6tL^K^$(-ENvbC*J6T9Sd33?pW#o`dVUpLJ5f}=D{8!tfaz2qzNl?EuG0=;Yk4~8adfHnli;P{sSNsE_hBZQp7xl44( zp7W~BW zEfPI|!jiBhI9>yE@)>VHa!aeW>19jJfQ?^a;UH24#_Q1n(EpBp zmVTrZ>D-%MVmGiZRy&0d^VNm?0Gu}Z;iC*6ypuWy@8Zd8k?tvrI zlj!eg)a1WrM=syd{l90soN{1~o zgOfR@^OOXbIbZc8F(or~cqcXp8)5>`^XhNC0LjH1I&;IcQwX(A44=WUaWXd*1mM$L zU+l@8hfQg7u#12!Ujih$WC2I^z)_i2>N5Jz!WnMm5RQ%Nk((DE|g~77zVk4MqLw#BW1qWUEqVgWvoueMl0aOv&Z4Mi)DQod!Oa> zo#jlm6|>=3&FjjUEbTL(-Z7mq4fa*z+{Mdys3LZp;%NKHjoWFoeP#$rP- zdz5FN^9HnJv3t`iGZ{)w%E;lKB|R3ssgXEgjKyYMAl``cqk_CW$W|6ZkYZ0K2+k(X zSj7QQOZ=%IUp9_D*pod6@1)Pdd--#ypnYRf>ZHyt?I|!zQ>Y?u_O}A1- zlc}P~;{?ca@;Ua1#+p)$zL*poicG-Puo!F%;ex*eNNruDd5$eBnbnAp(&M!=b8|*C zdeb5ZD2C%h05-<^qUW`jyZ}jM(VFMjivr=`@OkAF!d$$7DMsZO=U{WjOzbL{1M3lQ zK=u{$SkB#W>VGn9j81hyplVxxXO74$#ejbdfEmR5FF zD+l3dd&w0(Fqh3kPw{N*%bSVK+0*b|@eD5yxX56v7cFpK@j%0VSd{HXhp|Bk^X00Q9VPAJpEYnX)}k2YZ1| zgCp+MG3SJ9rw}JNnQu;9fHxC-(DMgxK(;FSjwh`gO9C}GV!pDJ&&8RN+1Qgi16y*Y z;r-I90XbOCMOxVi$L=v@btTiQ&Fwo5!m%yBg#+w30n$wQigG;1!ctZ)`g9366dQ}J zkr4z$MLg#XNNI0$-^i55>QxvI$#X-2DJKlQS;06_7=|r|`FJzY2Yp`#RLw5hvm59T z+ZL5Ruvg8)VEG*Ml+MDw{OQ=7GnIg*dwI~Mo^GZV)5-(K*+SK@TG{U;q&DF@6KK}A zz||@M-8@c!Jjxk?BaUdYj5w=LKxbSmwnY(8gQaPJ|W@2B#G;B6b!LHJ2FdrG+ ze!bKy08Q7bl`v$rWs+9Top%w`($UniExDOP?RWvwxlcYN&na&m;4HZzIBN{VCnX`+p5}|K1=0BI?J*}J z!z}Y{2JLW9Qx1nudzE`%$67TDy=61;u?NumWm7rLdjoRZHBBq!BkEyV*;nz}?uO%} za)f%+wwuNYkU>o1nH)AbaEkM*6napu5ub7RJZ^}`9zz`V)|KPb`bY5D+Pg6Lz)C3V z?qq3gU{_mBPpIMFY7IvP4z0!Ekcz|bd0{YmE5ooiKN$O#7;*HTTJ%4*6xM%U3Uz%e z?5jDR)G@v4=fk>=!ADh7Asri&Vtq8}`=-IUlyKTb zu%s`a2-m%n;95BWjuli-2i3FUCiY9?1&HNb5qDdpvmeS}qsYlzF3iPXRtnB#7;u8U zPIqc7dRDdLEa$89x$!U;&~vC5sf4Lj$-%Rh!RF6a840N}0v9Sm@mYO1jurajNRb~- zuwtEU2!OdM08*1bl!bmQ@xJto1*}SZ-s;1c3+s|OIJGMDgs(Jk>z~?oOe?}_Ylx(0$M#zfcna+DC)y67 z*UUgq#Wd_Inu5)_lkq_%VVL_z6?9}5TrE`BNYt+~oDW zLvK9^=eX z5GQ~3Z|M2;FWj#my$9CAqqEWeyX)aNe@;6H+|!x~weJ-6tt^85e2*5p+mbXuL((8R zzgLT{hwJGnpL@3pN8ZKR6`aBD*I`(?@aKOQs?hnHb~v9b#?YgaF!UWN=>7>9dVv1( zz|9!?0YQD2DYouLxE`W<7(?H??&}3KBum)aTmhHWf_)21Gzi`6Rx;&A6?CAZimFi*SZIuA^yR`}G3qUUxU_z3$sv%pdH)sh|BTj{ocT(7C1+ zN4|RpK7aG~=zgd}Q_aZPxoLn9M}6ktUFEQyK8j=OSB!k^#E(|;Yu%%Vh*Z!Ag_H3i z`=pPSC$K@Bh>uGr;7IiZ^skr%OS2n}O5Yj!VF6rgIcIs`h;nYHDG%MxxI?JtI!!$; z!f}4xfc{>gbZy7U#Zz&*V=hkL>5nt)kk77|%!!Yld)WluFJc0Q|BDKGj7nO| zs`Vh#>p`kV_|1Bz*TYl~eMfkY^Sjq#=#gv21<*4C@4SWcZ@+>5KfQs0KZ)x`3~c!$ z`rqMIiw%6f0p~Yuz`$0%-t;>983UU|`~erXy{^T;mOn6F)8fKbUU%~K)>pN-u#MLp zjP2t2D;T&E+h4}O@7JTZhEVDzY0=*}5vHbzkT{t;)=bjoE8#gV4xg?c=D|r*7Uf)2 zED=NW9M=O>&+TsIh;VdVqbbS*=&S!*jlsdeXOGsG;$UJJI+H@tnH++H$-(H#k3v^! z5IWNWaWE|a2h#n~neC6m#r#^<0(521$Kf1?(Fa{c^U+nx$en}3d9%@-KZ~EAg(IA= zx(a9DaM5&r&op$`%|Lh6G#oCO!tcKoos91C$>?1^6_%#Sur8bgsd*BV7WcM=y_KeX z-$b_|d&;@iC9h?AwTnK--RlU?aXmoKxx<}St`*m4fLxD6UKW53A3prm71ZC~|AhUe z7qpG`!OTd`UlA~6hr?PC2a_?B5rPZ3!7$|o!CVvsYb~dB&R1s6SK^4qT(W@UfiEoe zj57Czm!)Ekwryc;_kp!~7OWgTMI&f!ok_Eu0ZZLfnCquNYMcUVEdp314*y>;PS zbfrU{plx`$?&Ap2cJqiKYk)*rEpsbJc#dYuYp(#hMgwH$b@)#|{zCkNx{p5k=-ICf z=)(^`Tq^d6?GN03S;>yn_H@LVxkC(}o`0NXD+q>N9MO~p!O;+^onNV?eo)IiJA#Dc zbkwT3+Oe>sW;TCOA8o#}*SSw19PNC*WICtzsoI8@Lpy=E6+^41XxkQ7<}2qC z5yFVs-`s!8$oOz2t(>CQS>|p8HB%O5*!2xbbL&0;r@v1qb3 zTZug#*OG}EoFPw#>}V$-`i-;wCasi<4tZ$#4O%1UA{oF|Sipmji zM0)MtzrT}f^c8FUtN){cmtTHa0Gd`+Rb}kz>gu#wtr!{_(v0n!93rhGiB0a_ywr$&9*|B5Ct7BvP_U&HJ zzx?_y{hqJm&-7eh5x=`wgIJ4LlUQ4NdisRF2#|0tXNHtC4br%27Ip5?EYw)b)#%sQY$F=_;@$KW=$G4AfAK!?_{{gDO V2{AI|$Nm5S002ovPDHLkV1jN5xB>tG literal 0 HcmV?d00001 diff --git a/app/src/alpha/res/mipmap-mdpi/ic_launcher.png b/app/src/alpha/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..1de682046f8cb8c96a1429f6a6982f6e61c7d972 GIT binary patch literal 4506 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 literal 0 HcmV?d00001 diff --git a/app/src/alpha/res/mipmap-xhdpi/ic_launcher.png b/app/src/alpha/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..ce008263c507fe04284bd7b80db58f809cf4d6e0 GIT binary patch literal 14096 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 literal 0 HcmV?d00001 diff --git a/app/src/alpha/res/mipmap-xxhdpi/ic_launcher.png b/app/src/alpha/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..aaa34fdee03e04dd8c8796dbb523fce8964854ce GIT binary patch literal 28536 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 N002ovPDHLkV1kf26iomC literal 0 HcmV?d00001 diff --git a/app/src/alpha/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/alpha/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..7f7e70794dc6d1414f96814d799a24975749e9bf GIT binary patch literal 47939 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�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 UD9iS8><>jnK|{Vm)&lzf0Q1HI1poj5 literal 0 HcmV?d00001 diff --git a/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png new file mode 100755 index 0000000000000000000000000000000000000000..257fc8e0b052a0b75d9b3df81bc42cce9507fb12 GIT binary patch literal 28937 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 Vc~EFcI9&h$002ovPDHLkV1g5%Zejoc literal 0 HcmV?d00001 diff --git a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100755 index 0000000000000000000000000000000000000000..27e58bbc5248760224fc1f7ebeedd3514e5ecb12 GIT binary patch literal 49047 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℘>?#N=ZGq9#o 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 ZB($0jvLQ(k5d~mAs)||)RdQCJ{tuM^KL`K- literal 0 HcmV?d00001 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..4e9ab3e --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/licenses.html b/app/src/main/assets/licenses.html new file mode 100644 index 0000000..ff886b6 --- /dev/null +++ b/app/src/main/assets/licenses.html @@ -0,0 +1,829 @@ +