Browse Source

Initial commit

This initial commit includes HUSH specific changes starting at this commit:
d14637012c
main
fekt 1 year ago
commit
4adbc901a0
  1. 53
      .run/_app_androidTest.run.xml
  2. 53
      .run/app_androidTest.run.xml
  3. 23
      .run/dependencyUpdates.run.xml
  4. 208
      CHANGELOG.md
  5. 61
      CONDUCT.md
  6. 98
      CONTRIBUTING.md
  7. 21
      LICENSE
  8. 82
      README.md
  9. 1
      app/.gitignore
  10. 193
      app/build.gradle
  11. 12
      app/proguard-rules.pro
  12. 43
      app/src/androidTest/java/cash/z/ecc/android/MemoTest.kt
  13. 138
      app/src/androidTest/java/cash/z/ecc/android/integration/ConversionsTest.kt
  14. 122
      app/src/androidTest/java/cash/z/ecc/android/integration/IntegrationTest.kt
  15. 73
      app/src/androidTest/java/cash/z/ecc/android/integration/LockBoxTest.kt
  16. 41
      app/src/androidTest/java/cash/z/ecc/android/preference/PreferenceKeysTest.kt
  17. 46
      app/src/androidTest/java/cash/z/ecc/android/preference/PreferencesTest.kt
  18. 32
      app/src/androidTest/java/cash/z/ecc/android/test/FragmentNavigationScenario.kt
  19. 29
      app/src/androidTest/java/cash/z/ecc/android/test/UiTestPrerequisites.kt
  20. 154
      app/src/androidTest/java/cash/z/ecc/android/ui/home/AutoshieldingInformationFragmentTest.kt
  21. 43
      app/src/main/AndroidManifest.xml
  22. 7
      app/src/main/assets/saplingtree/mainnet/1225600.json
  23. 7
      app/src/main/assets/saplingtree/mainnet/1250000.json
  24. 7
      app/src/main/assets/saplingtree/mainnet/1290000.json
  25. 7
      app/src/main/assets/saplingtree/mainnet/1300000.json
  26. 7
      app/src/main/assets/saplingtree/mainnet/1335000.json
  27. 7
      app/src/main/assets/saplingtree/testnet/1380300.json
  28. 7
      app/src/main/assets/saplingtree/testnet/1450000.json
  29. 7
      app/src/main/assets/saplingtree/testnet/1454000.json
  30. BIN
      app/src/main/assets/sound_receive_small.mp3
  31. 60
      app/src/main/java/cash/z/ecc/android/StrictModeHelper.kt
  32. 117
      app/src/main/java/cash/z/ecc/android/ZcashWalletApp.kt
  33. 44
      app/src/main/java/cash/z/ecc/android/di/DependenciesHolder.kt
  34. 14
      app/src/main/java/cash/z/ecc/android/di/InitializerComponent.kt
  35. 53
      app/src/main/java/cash/z/ecc/android/ext/Const.kt
  36. 76
      app/src/main/java/cash/z/ecc/android/ext/CurrencyFormatter.kt
  37. 192
      app/src/main/java/cash/z/ecc/android/ext/Dialogs.kt
  38. 83
      app/src/main/java/cash/z/ecc/android/ext/EditText.kt
  39. 69
      app/src/main/java/cash/z/ecc/android/ext/Extensions.kt
  40. 9
      app/src/main/java/cash/z/ecc/android/ext/Fragment.kt
  41. 46
      app/src/main/java/cash/z/ecc/android/ext/Int.kt
  42. 14
      app/src/main/java/cash/z/ecc/android/ext/LifeCycleOwner.kt
  43. 20
      app/src/main/java/cash/z/ecc/android/ext/Spannable.kt
  44. 81
      app/src/main/java/cash/z/ecc/android/ext/View.kt
  45. 22
      app/src/main/java/cash/z/ecc/android/feedback/FeedbackConsole.kt
  46. 38
      app/src/main/java/cash/z/ecc/android/feedback/FeedbackFile.kt
  47. 255
      app/src/main/java/cash/z/ecc/android/feedback/Report.kt
  48. 6
      app/src/main/java/cash/z/ecc/android/preference/PreferenceKeys.kt
  49. 12
      app/src/main/java/cash/z/ecc/android/preference/Preferences.kt
  50. 9
      app/src/main/java/cash/z/ecc/android/preference/SharedPreferenceFactory.kt
  51. 35
      app/src/main/java/cash/z/ecc/android/preference/model/BooleanDefaultValue.kt
  52. 23
      app/src/main/java/cash/z/ecc/android/preference/model/DefaultValue.kt
  53. 33
      app/src/main/java/cash/z/ecc/android/preference/model/LongDefaultValue.kt
  54. 669
      app/src/main/java/cash/z/ecc/android/ui/MainActivity.kt
  55. 36
      app/src/main/java/cash/z/ecc/android/ui/MainViewModel.kt
  56. 95
      app/src/main/java/cash/z/ecc/android/ui/base/BaseFragment.kt
  57. 103
      app/src/main/java/cash/z/ecc/android/ui/history/HistoryFragment.kt
  58. 179
      app/src/main/java/cash/z/ecc/android/ui/history/HistoryViewModel.kt
  59. 46
      app/src/main/java/cash/z/ecc/android/ui/history/TransactionAdapter.kt
  60. 221
      app/src/main/java/cash/z/ecc/android/ui/history/TransactionFragment.kt
  61. 168
      app/src/main/java/cash/z/ecc/android/ui/history/TransactionViewHolder.kt
  62. 52
      app/src/main/java/cash/z/ecc/android/ui/history/TransactionsDrawableFooter.kt
  63. 48
      app/src/main/java/cash/z/ecc/android/ui/history/TransactionsFooter.kt
  64. 69
      app/src/main/java/cash/z/ecc/android/ui/home/AutoshieldingInformationFragment.kt
  65. 158
      app/src/main/java/cash/z/ecc/android/ui/home/BalanceDetailFragment.kt
  66. 147
      app/src/main/java/cash/z/ecc/android/ui/home/BalanceDetailViewModel.kt
  67. 596
      app/src/main/java/cash/z/ecc/android/ui/home/HomeFragment.kt
  68. 158
      app/src/main/java/cash/z/ecc/android/ui/home/HomeViewModel.kt
  69. 155
      app/src/main/java/cash/z/ecc/android/ui/home/MagicSnakeLoader.kt
  70. 288
      app/src/main/java/cash/z/ecc/android/ui/profile/AwesomeFragment.kt
  71. 208
      app/src/main/java/cash/z/ecc/android/ui/profile/ProfileFragment.kt
  72. 164
      app/src/main/java/cash/z/ecc/android/ui/profile/ProfileViewModel.kt
  73. 88
      app/src/main/java/cash/z/ecc/android/ui/receive/ReceiveTabFragment.kt
  74. 18
      app/src/main/java/cash/z/ecc/android/ui/receive/ReceiveViewModel.kt
  75. 66
      app/src/main/java/cash/z/ecc/android/ui/scan/QrAnalyzer.kt
  76. 216
      app/src/main/java/cash/z/ecc/android/ui/scan/ScanFragment.kt
  77. 29
      app/src/main/java/cash/z/ecc/android/ui/scan/ScanViewModel.kt
  78. 236
      app/src/main/java/cash/z/ecc/android/ui/send/AutoShieldFragment.kt
  79. 158
      app/src/main/java/cash/z/ecc/android/ui/send/AutoShieldViewModel.kt
  80. 37
      app/src/main/java/cash/z/ecc/android/ui/send/FundsAvailableFragment.kt
  81. 169
      app/src/main/java/cash/z/ecc/android/ui/send/SendFinalFragment.kt
  82. 373
      app/src/main/java/cash/z/ecc/android/ui/send/SendFragment.kt
  83. 121
      app/src/main/java/cash/z/ecc/android/ui/send/SendMemoFragment.kt
  84. 274
      app/src/main/java/cash/z/ecc/android/ui/send/SendViewModel.kt
  85. 151
      app/src/main/java/cash/z/ecc/android/ui/settings/SettingsFragment.kt
  86. 88
      app/src/main/java/cash/z/ecc/android/ui/settings/SettingsViewModel.kt
  87. 158
      app/src/main/java/cash/z/ecc/android/ui/setup/BackupFragment.kt
  88. 216
      app/src/main/java/cash/z/ecc/android/ui/setup/LandingFragment.kt
  89. 246
      app/src/main/java/cash/z/ecc/android/ui/setup/RestoreFragment.kt
  90. 103
      app/src/main/java/cash/z/ecc/android/ui/setup/SeedWordAdapter.kt
  91. 233
      app/src/main/java/cash/z/ecc/android/ui/setup/WalletSetupViewModel.kt
  92. 144
      app/src/main/java/cash/z/ecc/android/ui/tab_layout/TabLayoutFragment.kt
  93. 13
      app/src/main/java/cash/z/ecc/android/ui/tab_layout/ViewPagerAdapter.kt
  94. 28
      app/src/main/java/cash/z/ecc/android/ui/util/AddressPartNumberSpan.kt
  95. 22
      app/src/main/java/cash/z/ecc/android/ui/util/DebugFileTwig.kt
  96. 54
      app/src/main/java/cash/z/ecc/android/ui/util/MemoUtil.kt
  97. 47
      app/src/main/java/cash/z/ecc/android/ui/util/PermissionFragment.kt
  98. 200
      app/src/main/java/cash/z/ecc/android/util/Twig.kt
  99. 9
      app/src/main/res/anim/anim_enter_from_bottom.xml
  100. 9
      app/src/main/res/anim/anim_enter_from_left.xml
  101. 9
      app/src/main/res/anim/anim_enter_from_right.xml
  102. 10
      app/src/main/res/anim/anim_exit_to_left.xml
  103. 10
      app/src/main/res/anim/anim_exit_to_right.xml
  104. 6
      app/src/main/res/anim/anim_fade_in.xml
  105. 6
      app/src/main/res/anim/anim_fade_in_scanner.xml
  106. 6
      app/src/main/res/anim/anim_fade_out.xml
  107. 6
      app/src/main/res/anim/anim_fade_out_address.xml
  108. 6
      app/src/main/res/anim/anim_fade_out_medium.xml
  109. 5
      app/src/main/res/color/selector_button_text_dark.xml
  110. 5
      app/src/main/res/color/selector_button_text_light.xml
  111. 5
      app/src/main/res/color/selector_button_text_light_dimmed.xml
  112. 5
      app/src/main/res/color/selector_button_text_light_to_dimmed.xml
  113. 6
      app/src/main/res/color/selector_feedback_button.xml
  114. 8
      app/src/main/res/color/selector_primary_button_activatable.xml
  115. 8
      app/src/main/res/color/selector_secondary_button_activatable.xml
  116. 10
      app/src/main/res/drawable-anydpi/ic_settings.xml
  117. 23
      app/src/main/res/drawable-v24/ic_launcher_background.xml
  118. 12
      app/src/main/res/drawable-v24/ic_launcher_foreground.xml
  119. 6
      app/src/main/res/drawable/background_balance_detail_amounts_container.xml
  120. 5
      app/src/main/res/drawable/background_balance_details_total.xml
  121. 5
      app/src/main/res/drawable/background_balance_details_transparent.xml
  122. 7
      app/src/main/res/drawable/background_banner.xml
  123. 7
      app/src/main/res/drawable/background_banner_large.xml
  124. 10
      app/src/main/res/drawable/background_button_rounded.xml
  125. 9
      app/src/main/res/drawable/background_circle.xml
  126. 9
      app/src/main/res/drawable/background_circle_solid.xml
  127. 29
      app/src/main/res/drawable/background_footer.xml
  128. 8
      app/src/main/res/drawable/background_gradient_balance_details.xml
  129. 7
      app/src/main/res/drawable/background_gradient_bottom.xml
  130. 27
      app/src/main/res/drawable/background_header.xml
  131. 11
      app/src/main/res/drawable/background_home.xml
  132. 6
      app/src/main/res/drawable/background_indicator_failed.xml
  133. 10
      app/src/main/res/drawable/background_indicator_inbound.xml
  134. 9
      app/src/main/res/drawable/background_indicator_outbound.xml
  135. 9
      app/src/main/res/drawable/background_indicator_unknown.xml
  136. 11
      app/src/main/res/drawable/background_send_final.xml
  137. 7
      app/src/main/res/drawable/background_title_primary.xml
  138. 7
      app/src/main/res/drawable/bg_chip_view.xml
  139. 7
      app/src/main/res/drawable/chip_details_background.xml
  140. 5
      app/src/main/res/drawable/ic_account_circle.xml
  141. 138
      app/src/main/res/drawable/ic_address_qr.xml
  142. 9
      app/src/main/res/drawable/ic_arrow_back_black_24dp.xml
  143. 12
      app/src/main/res/drawable/ic_background_qr.xml
  144. 5
      app/src/main/res/drawable/ic_baseline_done_24.xml
  145. 5
      app/src/main/res/drawable/ic_baseline_keyboard_arrow_down_24.xml
  146. 5
      app/src/main/res/drawable/ic_baseline_launch_24.xml
  147. 5
      app/src/main/res/drawable/ic_cancel.xml
  148. 14
      app/src/main/res/drawable/ic_check_shield.xml
  149. 12
      app/src/main/res/drawable/ic_check_shielded.xml
  150. 9
      app/src/main/res/drawable/ic_close_black_24dp.xml
  151. 5
      app/src/main/res/drawable/ic_content_copy.xml
  152. 5
      app/src/main/res/drawable/ic_done_24dp.xml
  153. 24
      app/src/main/res/drawable/ic_expand_memo_enabled.xml
  154. 5
      app/src/main/res/drawable/ic_info_24dp.xml
  155. 48
      app/src/main/res/drawable/ic_logo_landing.xml
  156. 12
      app/src/main/res/drawable/ic_memo.xml
  157. 1252
      app/src/main/res/drawable/ic_profile_zebra_01.xml
  158. 1252
      app/src/main/res/drawable/ic_profile_zebra_02.xml
  159. 163
      app/src/main/res/drawable/ic_qr_scan.xml
  160. 10
      app/src/main/res/drawable/ic_qrcode_24dp.xml
  161. 9
      app/src/main/res/drawable/ic_receipt_24dp.xml
  162. 5
      app/src/main/res/drawable/ic_receive.xml
  163. 12
      app/src/main/res/drawable/ic_receive_funds.xml
  164. 496
      app/src/main/res/drawable/ic_sadzebra.xml
  165. 12
      app/src/main/res/drawable/ic_scan_corner.xml
  166. 36
      app/src/main/res/drawable/ic_scan_frame.xml
  167. 48
      app/src/main/res/drawable/ic_scan_overlay.xml
  168. 44
      app/src/main/res/drawable/ic_scan_overlay_edited.xml
  169. 14
      app/src/main/res/drawable/ic_shield.xml
  170. 166
      app/src/main/res/drawable/ic_shield_address.xml
  171. 12
      app/src/main/res/drawable/ic_shielded.xml
  172. 5
      app/src/main/res/drawable/ic_warning_24dp.xml
  173. 1252
      app/src/main/res/drawable/ic_zcash_primary.xml
  174. 1252
      app/src/main/res/drawable/ic_zcash_white.xml
  175. 1252
      app/src/main/res/drawable/ic_zcashlogo.xml
  176. 1252
      app/src/main/res/drawable/ic_zec_symbol.xml
  177. 1252
      app/src/main/res/drawable/ic_zec_symbol_right.xml
  178. 8
      app/src/main/res/drawable/ripple_button_circle.xml
  179. 6
      app/src/main/res/drawable/selector_pressed_ripple_circle.xml
  180. BIN
      app/src/main/res/font/inconsolata.ttf
  181. BIN
      app/src/main/res/font/zboto.otf
  182. 65
      app/src/main/res/layout/chip_view.xml
  183. 50
      app/src/main/res/layout/chip_view_filterable.xml
  184. 28
      app/src/main/res/layout/dialog_first_use_message.xml
  185. 132
      app/src/main/res/layout/dialog_solicit_feedback_rating.xml
  186. 6
      app/src/main/res/layout/footer_transactions.xml
  187. 19
      app/src/main/res/layout/fragment_address.xml
  188. 168
      app/src/main/res/layout/fragment_auto_shield.xml
  189. 108
      app/src/main/res/layout/fragment_auto_shield_information.xml
  190. 168
      app/src/main/res/layout/fragment_awesome.xml
  191. 390
      app/src/main/res/layout/fragment_backup.xml
  192. 226
      app/src/main/res/layout/fragment_balance_detail.xml
  193. 19
      app/src/main/res/layout/fragment_confirm.xml
  194. 100
      app/src/main/res/layout/fragment_funds_available.xml
  195. 224
      app/src/main/res/layout/fragment_history.xml
  196. 470
      app/src/main/res/layout/fragment_home.xml
  197. 84
      app/src/main/res/layout/fragment_landing.xml
  198. 19
      app/src/main/res/layout/fragment_memo.xml
  199. 252
      app/src/main/res/layout/fragment_profile.xml
  200. 200
      app/src/main/res/layout/fragment_restore.xml
  201. 224
      app/src/main/res/layout/fragment_scan.xml
  202. 530
      app/src/main/res/layout/fragment_send.xml
  203. 201
      app/src/main/res/layout/fragment_send_address.xml
  204. 155
      app/src/main/res/layout/fragment_send_final.xml
  205. 235
      app/src/main/res/layout/fragment_send_memo.xml
  206. 162
      app/src/main/res/layout/fragment_settings.xml
  207. 108
      app/src/main/res/layout/fragment_tab_layout.xml
  208. 179
      app/src/main/res/layout/fragment_tab_receive_shielded.xml
  209. 554
      app/src/main/res/layout/fragment_transaction.xml
  210. 19
      app/src/main/res/layout/fragment_wallet_import.xml
  211. 19
      app/src/main/res/layout/fragment_wallet_new.xml
  212. 141
      app/src/main/res/layout/item_transaction.xml
  213. 67
      app/src/main/res/layout/main_activity.xml
  214. 5
      app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  215. 5
      app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  216. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher.png
  217. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_back.png
  218. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_fore.png
  219. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher_round.png
  220. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher_round_adaptive_back.png
  221. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher_round_adaptive_fore.png
  222. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher.png
  223. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_back.png
  224. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_fore.png
  225. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher_round.png
  226. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher_round_adaptive_back.png
  227. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher_round_adaptive_fore.png
  228. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher.png
  229. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_back.png
  230. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png
  231. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
  232. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher_round_adaptive_back.png
  233. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher_round_adaptive_fore.png
  234. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  235. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_back.png
  236. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png
  237. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
  238. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher_round_adaptive_back.png
  239. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher_round_adaptive_fore.png
  240. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  241. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_back.png
  242. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png
  243. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
  244. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher_round_adaptive_back.png
  245. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher_round_adaptive_fore.png
  246. 310
      app/src/main/res/navigation/mobile_navigation.xml
  247. 1
      app/src/main/res/raw/lottie_button_forever.json
  248. 1
      app/src/main/res/raw/lottie_button_loading.json
  249. 1
      app/src/main/res/raw/lottie_button_loading_new.json
  250. 1
      app/src/main/res/raw/lottie_sending.json
  251. 1
      app/src/main/res/raw/lottie_shielding.json
  252. 1
      app/src/main/res/raw/lottie_success.json
  253. 16
      app/src/main/res/values-es/custom_translations.xml
  254. 74
      app/src/main/res/values-es/translated.xml
  255. 16
      app/src/main/res/values-it/custom_translations.xml
  256. 63
      app/src/main/res/values-it/translated.xml
  257. 16
      app/src/main/res/values-ko/custom_translations.xml
  258. 74
      app/src/main/res/values-ko/translated.xml
  259. 16
      app/src/main/res/values-ru/custom_translations.xml
  260. 69
      app/src/main/res/values-ru/translated.xml
  261. 16
      app/src/main/res/values-zh/custom_translations.xml
  262. 69
      app/src/main/res/values-zh/translated.xml
  263. 91
      app/src/main/res/values/colors.xml
  264. 19
      app/src/main/res/values/custom_translations.xml
  265. 10
      app/src/main/res/values/dimens.xml
  266. 6
      app/src/main/res/values/integer.xml
  267. 221
      app/src/main/res/values/missing_translation.xml
  268. 8
      app/src/main/res/values/missing_translation_urls.xml
  269. 4
      app/src/main/res/values/strings-urls.xml
  270. 293
      app/src/main/res/values/strings.xml
  271. 102
      app/src/main/res/values/styles.xml
  272. 75
      app/src/main/res/values/translated.xml
  273. 2053
      app/src/main/res/values/word_list_bip39.xml
  274. 5
      app/src/main/res/xml/file_paths.xml
  275. BIN
      app/src/qa/res/mipmap-hdpi/ic_launcher.png
  276. BIN
      app/src/qa/res/mipmap-hdpi/ic_launcher_round.png
  277. BIN
      app/src/qa/res/mipmap-mdpi/ic_launcher.png
  278. BIN
      app/src/qa/res/mipmap-mdpi/ic_launcher_round.png
  279. BIN
      app/src/qa/res/mipmap-xhdpi/ic_launcher.png
  280. BIN
      app/src/qa/res/mipmap-xhdpi/ic_launcher_round.png
  281. BIN
      app/src/qa/res/mipmap-xxhdpi/ic_launcher.png
  282. BIN
      app/src/qa/res/mipmap-xxhdpi/ic_launcher_round.png
  283. BIN
      app/src/qa/res/mipmap-xxxhdpi/ic_launcher.png
  284. BIN
      app/src/qa/res/mipmap-xxxhdpi/ic_launcher_round.png
  285. 6
      app/src/qa/res/values/colors.xml
  286. 3
      app/src/qa/res/values/strings.xml
  287. 54
      app/src/test/java/cash/z/ecc/android/ScratchPad.kt
  288. 112
      app/src/test/java/cash/z/ecc/android/SendViewModelTest.kt
  289. 1
      app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
  290. 4
      app/src/zcashmainnet/res/values/integers.xml
  291. 5
      app/src/zcashmainnet/res/values/strings.xml
  292. 4
      app/src/zcashtestnet/res/values/integers.xml
  293. 6
      app/src/zcashtestnet/res/values/strings.xml
  294. BIN
      app/src/zcashtestnetQa/res/mipmap-hdpi/ic_launcher.png
  295. BIN
      app/src/zcashtestnetQa/res/mipmap-hdpi/ic_launcher_round.png
  296. BIN
      app/src/zcashtestnetQa/res/mipmap-mdpi/ic_launcher.png
  297. BIN
      app/src/zcashtestnetQa/res/mipmap-mdpi/ic_launcher_round.png
  298. BIN
      app/src/zcashtestnetQa/res/mipmap-xhdpi/ic_launcher.png
  299. BIN
      app/src/zcashtestnetQa/res/mipmap-xhdpi/ic_launcher_round.png
  300. BIN
      app/src/zcashtestnetQa/res/mipmap-xxhdpi/ic_launcher.png
  301. BIN
      app/src/zcashtestnetQa/res/mipmap-xxhdpi/ic_launcher_round.png
  302. BIN
      app/src/zcashtestnetQa/res/mipmap-xxxhdpi/ic_launcher.png
  303. BIN
      app/src/zcashtestnetQa/res/mipmap-xxxhdpi/ic_launcher_round.png
  304. 6
      app/src/zcashtestnetQa/res/values/colors.xml
  305. 3
      app/src/zcashtestnetQa/res/values/strings.xml
  306. 23
      build-convention/build.gradle.kts
  307. 45
      build-convention/buildscript-gradle.lockfile
  308. 25
      build-convention/gradle.lockfile
  309. 31
      build-convention/settings.gradle.kts
  310. 44
      build-convention/src/main/kotlin/zcash.ktlint-conventions.gradle.kts
  311. 47
      build.gradle.kts
  312. 15
      buildSrc/build.gradle.kts
  313. 117
      buildSrc/src/main/java/cash/z/ecc/android/Dependencies.kt
  314. 1
      feedback/.gitignore
  315. 43
      feedback/build.gradle
  316. 0
      feedback/consumer-rules.pro
  317. 21
      feedback/proguard-rules.pro
  318. 2
      feedback/src/main/AndroidManifest.xml
  319. 284
      feedback/src/main/java/cash/z/ecc/android/feedback/Feedback.kt
  320. 128
      feedback/src/main/java/cash/z/ecc/android/feedback/FeedbackCoordinator.kt
  321. 45
      feedback/src/main/java/cash/z/ecc/android/feedback/util/CompositeJob.kt
  322. 31
      feedback/src/test/java/cash/z/ecc/android/feedback/CoroutineTestRule.kt
  323. 55
      feedback/src/test/java/cash/z/ecc/android/feedback/FeedbackObserverTest.kt
  324. 156
      feedback/src/test/java/cash/z/ecc/android/feedback/FeedbackTest.kt
  325. 19
      gradle.properties
  326. BIN
      gradle/wrapper/gradle-wrapper.jar
  327. 6
      gradle/wrapper/gradle-wrapper.properties
  328. 240
      gradlew
  329. 91
      gradlew.bat
  330. 1
      lockbox/.gitignore
  331. 49
      lockbox/build.gradle
  332. 0
      lockbox/consumer-rules.pro
  333. 21
      lockbox/proguard-rules.pro
  334. 52
      lockbox/src/androidTest/java/cash/z/ecc/android/lockbox/LockBoxText.kt
  335. 2
      lockbox/src/main/AndroidManifest.xml
  336. 133
      lockbox/src/main/java/cash/z/ecc/android/lockbox/LockBox.kt
  337. 1
      mnemonic/.gitignore
  338. 14
      mnemonic/build.gradle
  339. 18
      mnemonic/src/main/java/cash/z/ecc/kotlin/mnemonic/MnemonicExt.kt
  340. 25
      mnemonic/src/main/java/cash/z/ecc/kotlin/mnemonic/Mnemonics.kt
  341. 90
      mnemonic/src/test/java/cash/z/ecc/android/util/MnemonicTest.kt
  342. BIN
      placeholder.keystore
  343. 1
      qrecycler/.gitignore
  344. 39
      qrecycler/build.gradle
  345. 21
      qrecycler/proguard-rules.pro
  346. 4
      qrecycler/src/main/AndroidManifest.xml
  347. 62
      qrecycler/src/main/java/cash/z/android/qrecycler/QRecycler.kt
  348. 8
      qrecycler/src/main/java/cash/z/android/qrecycler/QScanner.kt
  349. 23
      qrecycler/src/main/res/layout/texture_view.xml
  350. 58
      qrecycler/src/main/res/values/attrs.xml
  351. 21
      qrecycler/src/main/res/values/public.xml
  352. 2
      qrecycler/src/main/res/values/strings.xml
  353. 24
      qrecycler/src/main/res/values/styles.xml
  354. 1
      responsible_disclosure.md
  355. 53
      settings.gradle.kts

53
.run/_app_androidTest.run.xml

@ -0,0 +1,53 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name=":app:androidTest" type="AndroidTestRunConfigurationType" factoryName="Android Instrumented Tests">
<module name="Zcash_Wallet.app" />
<option name="TESTING_TYPE" value="0" />
<option name="METHOD_NAME" value="" />
<option name="CLASS_NAME" value="" />
<option name="PACKAGE_NAME" value="" />
<option name="INSTRUMENTATION_RUNNER_CLASS" value="" />
<option name="EXTRA_OPTIONS" value="" />
<option name="INCLUDE_GRADLE_EXTRA_OPTIONS" value="true" />
<option name="CLEAR_LOGCAT" value="false" />
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
<option name="SKIP_NOOP_APK_INSTALLATIONS" value="true" />
<option name="FORCE_STOP_RUNNING_APP" value="true" />
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="2147483645" />
<option name="SELECTED_CLOUD_MATRIX_PROJECT_ID" value="api-9130115880275692386-873230" />
<option name="DEBUGGER_TYPE" value="Auto" />
<Auto>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Auto>
<Hybrid>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Hybrid>
<Java />
<Native>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Native>
<Profilers>
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
<option name="STARTUP_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Sample Java Methods" />
<option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
</Profilers>
<method v="2">
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
</method>
</configuration>
</component>

53
.run/app_androidTest.run.xml

@ -0,0 +1,53 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="app:androidTest" type="AndroidTestRunConfigurationType" factoryName="Android Instrumented Tests">
<module name="Zcash_Wallet.app" />
<option name="TESTING_TYPE" value="0" />
<option name="METHOD_NAME" value="" />
<option name="CLASS_NAME" value="" />
<option name="PACKAGE_NAME" value="" />
<option name="INSTRUMENTATION_RUNNER_CLASS" value="" />
<option name="EXTRA_OPTIONS" value="" />
<option name="INCLUDE_GRADLE_EXTRA_OPTIONS" value="true" />
<option name="CLEAR_LOGCAT" value="false" />
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
<option name="SKIP_NOOP_APK_INSTALLATIONS" value="true" />
<option name="FORCE_STOP_RUNNING_APP" value="true" />
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="2147483645" />
<option name="SELECTED_CLOUD_MATRIX_PROJECT_ID" value="api-9130115880275692386-873230" />
<option name="DEBUGGER_TYPE" value="Auto" />
<Auto>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Auto>
<Hybrid>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Hybrid>
<Java />
<Native>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Native>
<Profilers>
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
<option name="STARTUP_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Sample Java Methods" />
<option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
</Profilers>
<method v="2">
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
</method>
</configuration>
</component>

23
.run/dependencyUpdates.run.xml

@ -0,0 +1,23 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="dependencyUpdates" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="dependencyUpdates" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

208
CHANGELOG.md

@ -0,0 +1,208 @@
Change Log
==========
Version 1.0.0-alpha74 *(2021-06-21)*
------------------------------------
- New: Added workflow for automatically shielding funds.
- New: Automatically recover from more network failure states.
- New: Link to play store from the build number.
- New: Hide available/total toggle when there are no pending funds.
- New: Easter Egg to access the testnet faucets.
- New: Updated checkpoints for mainnet and testnet.
- Fix: Expand tappable area for showing the balance details.
- Fix: Off by one error when calculating confirmations.
- Fix: Do not show time in transaction details for pending transactions.
Version 1.0.0-alpha72 *(2021-06-07)*
------------------------------------
- New: Address tabs with t-address support [Credit @herou].
- New: Ktlint support [Credit @nighthawk24]
- New: Balance details screen [Credit @herou].
- New: Better balance information around unmined transactions.
- New: Add toggle to show available v. total funds.
- New: Auto-shielding via balance details screen.
- New: 'Ask Later' on feedback popup.
- Fix: Repaired QR scanning on older devices (below API 24).
- Fix: Several of the most frequent crashes reported in bugsnag.
- Fix: Corrected over-sized icon in history.
- Fix: History no longer displays negative balance during initial sync.
- Fix: Errors that prevented sync from working in some situations.
- Fix: Improved support for smaller screens and older devices.
Version 1.0.0-alpha67 *(2021-04-22)*
------------------------------------
- Fix: Crash after entering 24th seed word on certain devices
- Fix: Correct errors on certain devices around biometrics.
- Fix: Display information when invalid addresses are scanned.
- Fix: Prevent memo for transparent transactions [Credit @mandeepbhalothia].
- Fix: Security finding: remove aparent logging [Credit @mandeepbhalothia].
- New: View more info for failed sends [Credit @herou].
- New: Switch away from google libraries for QR code parsing [Credit @herou].
- New: Ability to make testnet releases [Credit @herou].
- New: Updated design for wallet history [Credit @mandeepbhalothia].
- New: Added responsible disclosure document for vulnerabilities [Credit: @zebambam]
- New: Update to latest SDK changes, including using one library instead of two.
- New: Auto-shielding Easter Egg.
- New: Periodically solicit user feedback.
- New: Wallet restore improvements including 'clear' feature.
- New: Rescan/Wipe feature for fixing wallet errors
- New: Add ability to copy the contents of the memo.
- New: Add ability to paste and parse an entire seed phrase at once
- New: Basic support for QRs prefixed with zcash:
- New: Made it easier to add vibration and leveraged it whenever text is copied.
- New: Adds cleanup and removal of failed transactions.
- New: Improved logic for determining the wallet birthday.
- New: Capture performance metrics for scanning.
- New: Additional messaging during a scan for better responsiveness.
- New: Improved handling of critical errors.
Version 1.0.0-alpha43 *(2020-12-20)*
------------------------------------
- Fix: Repaired the upgrade flow, which could not reorg because of missing birthday height
- Fix: Repaired create wallet flow which was being covered by the loading screen
Version 1.0.0-alpha42 *(2020-12-19)*
------------------------------------
- Fix: Correct race condition when launching the app
- Fix: Display loading screen while waiting for app to initialize
Version 1.0.0-alpha41 *(2020-12-19)*
------------------------------------
- New: Upgrade to the latest SDK.
- New: Implements ZIP-313, reducing the default fee from 10,000 to 1,000 zats.
- New: Adds authentication prior to viewing backup seed words.
- New: Adds blockchair as the transaction explorer.
- Fix: Authentication bugs on older devices that were preventing sends and mishandling cancels.
- Fix: Users can now upgrade from seed-only prior versions without crashing or needing to restore.
- Fix: Improved internal metrics for troubleshooting issues.
Version 1.0.0-alpha37 *(2020-10-07)*
------------------------------------
- New: Localization in 5 languages Russian, Italian, Spanish, Chinese and Korean.
- New: Store and sync using just the ViewingKey.
- New: Added QA build flavor for better testing.
- New: Ability to change servers (thanks @Nighthawk!)
- Fix: Critical bug in 3rd-party secure storage library impacting large strings.
- Fix: Devices without PIN can use the wallet again.
- Fix: Developer logs now work on all devices.
Version 1.0.0-alpha34 *(2020-08-28)*
------------------------------------
- New: Implemented transaction detail view.
- New: Updated receive screen and scan screen.
- New: Added optional blockchain explorer with privacy warning.
- Fix: Update key dependencies for performance.
- Fix: Iterated on send flow with lots of improvements and fixes.
- Fix: Trim improperly parsed characters from memos.
- Fix: Keypad stops working when navigating back to home screen.
- Fix: Prevent black screen after failed initialization.
Version 1.0.0-alpha33 *(2020-08-13)*
------------------------------------
- New: Fully removed crashlytics, in favor of bugsnag.
- New: Change the default lightwalletd server.
- New: Switched to the latest SDK.
Version 1.0.0-alpha32 *(2020-08-01)*
------------------------------------
- New: entirely revamped send flow
- New: added biometric authentication support
- New: add robust support for tx cancellation
- New: support precise birthday heights for faster restore
- New: switched to Reply-To standard for memos
- New: improved feedback while scanning QRs
- New: more compatible with memo reply-to formats
- New: update to latest librustzcash crates
- New: checkpoints
- Fix: amount not clearing on return to home screen
- Fix: address cursor resetting while typing
- Fix: app crash when opening application logs
- Fix: limit decimal places to 8 places
- Fix: wallet history now scrolls to the top
- Fix: consistent currency formatting
- Fix: security finding around compromised file system
Version 1.0.0-alpha31 *(2020-06-11)*
------------------------------------
- Source code now available on github!
- New: Improved mnemonic phrase handling and correctness
- Fix: mitigated several security findings
- New: Integrated with latest SDK, now available on jcenter
- New: Improved error handling in several areas
- New: Built-in support for the Heartwood consensus branch
- Fix: Wallet details screen now refreshes data
- Fix: Seed phrase display error
Version 1.0.0-alpha29 *(2020-06-10)*
------------------------------------
- Fix: Removed 3rd party mnemonic library due to restrictive license
- New: Verify checksum for imported mnemonics and warn user
- Fix: Validate address to mitigate security finding
- New: Integrated with latest SDK, now available on jcenter
- New: Improved error handling in several areas
- New: Updated all dependencies
- New: Built-in support for the Heartwood consensus branch
- Fix: Wallet details screen was not refreshing values
- Fix: Polling interval vulnerability
Version 1.0.0-alpha25 *(2020-03-27)*
------------------------------------
- New: added full memo support
- New: added feedback screen and related logging
- New: added full wallet restore, including outbound txs, outbound recipients and inbound memos
- New: show sender address in details list, when we can parse it from the memo
- New: long press transaction details to copy related address
- New: clarified UI for pending transactions
- New: improved the handling of disconnected state
- New: improved the behavior when returning from the background
- New: changed doc format to html instead of markdown
- New: iterated on button styles, including intial send button animation
- Fix: last digit of amount no longer lingers when returning to home screen
- Fix: database migration issues in certain versions
- Fix: added more detailed logs around network failures for future troubleshooting
- Fix: avoid negative numbers in the UI
- Fix: occasional crashes while closing the camera
- New: Simplified some SDK APIs so they are easier to use
- New: added more checkpoints so new wallets initialize faster
Version 1.0.0-alpha23 *(2020-02-21)*
------------------------------------
- Fix: reorg improvements, squashing critical bugs that disabled wallets
- New: extend analytics to include taps, screen views, and send flow.
- New: add crash reporting via Crashlytics.
- New: expose user logs and developer logs as files.
- New: improve feature for creating checkpoints.
- New: added DB schemas to the repository for tracking.
- Fix: numerous bug fixes, test fixes and cleanup.
- New: improved error handling and user experience
Version 1.0.0-alpha17 *(2020-02-07)*
------------------------------------
- New: implemented wallet import
- New: display the memo when tapping outbound transactions
- Fix: removed the sad zebra and softened wording for sending z->t
- Fix: removed restriction on smallest sendable ZEC amount
- Fix: removed "fund now"
- New: turned on developer logging to help with troubleshooting
- New: improved wallet details ability to handle small amounts of ZEC
- New: added ability to clear the memo
- Fix: changed "SEND WITHOUT MEMO" to "OMIT MEMO"
- Fix: corrected wording when the address is included in the memo
- New: display the approximate wallet birthday with the backup words
- New: improved crash reporting
- Fix: fixed bug when returning from the background
- New: added logging for failed transactions
- New: added logic to verify setup and offer explanation when the wallet is corrupted
- New: refactored and improved wallet initialization
- New: added ability to contribute 'plugins' to the SDK
- New: added tons more checkpoints to reduce startup/import time
- New: exposed logic to derive addresses directly from seeds
- Fix: fixed several crashes
Version 1.0.0-alpha11 *(2020-01-15)*
------------------------------------
- Initial ECC release
Version 1.0.0-alpha03 *(2019-12-18)*
------------------------------------
- Initial internal wallet team release

61
CONDUCT.md

@ -0,0 +1,61 @@
# Contributor Code of Conduct
As contributors and maintainers of this project, and in the interest of
fostering an open and welcoming community, we pledge to respect all people who
contribute through reporting issues, posting feature requests, updating
documentation, submitting pull requests or patches, and other activities.
We are committed to making participation in this project a harassment-free
experience for everyone, regardless of level of experience, gender, gender
identity and expression, sexual orientation, disability, personal appearance,
body size, race, ethnicity, age, religion, or nationality.
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery
* Personal attacks
* Trolling or insulting/derogatory comments
* Public or private harassment
* Publishing other's private information, such as physical or electronic
addresses, without explicit permission
* Other unethical or unprofessional conduct
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful. Note that contributors may be volunteers
who do not represent Electric Coin Company. They are free to express their own
opinions so long as they adhere to these guidelines.
By adopting this Code of Conduct, project maintainers commit themselves to
fairly and consistently applying these principles to every aspect of managing
this project. Project maintainers who do not follow or enforce the Code of
Conduct may be permanently removed from the project team.
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting a project maintainer (see below). All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. Maintainers are
obligated to maintain confidentiality with regard to the reporter of an
incident.
You may send reports to [our Conduct email](mailto:conduct@z.cash).
If you wish to contact specific maintainers directly, the following have made
themselves available for conduct issues:
- Daira Hopwood (daira at z.cash)
- Sean Bowe (sean at z.cash)
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 1.3.0, available at
[https://www.contributor-covenant.org/version/1/3/0/][version]
[homepage]: https://www.contributor-covenant.org
[version]: https://www.contributor-covenant.org/version/1/3/0/

98
CONTRIBUTING.md

@ -0,0 +1,98 @@
# Contributing Guidelines
This document contains information and guidelines about contributing to this project.
Please read it before you start participating.
**Topics**
* [Asking Questions](#asking-questions)
* [Reporting Security Issues](#reporting-security-issues)
* [Reporting Non Security Issues](#reporting-other-issues)
* [Developers Certificate of Origin](#developers-certificate-of-origin)
## Asking Questions
Questions are welcome! We encourage you to ask questions through GitHub issues.
Before doing so, please check that the project issues database doesn't already
include an answer to your question. Then open a new Issue and use the "Question"
label.
## Reporting Security Issues
If you have discovered an issue with this code that could present a security hazard or wish to discuss a sensitive issue with our security team, please contact security@z.cash [security.asc](https://z.cash/gpg-pubkeys/security.asc). Key fingerprint = AF85 0445 546C 18B7 86F9 2C62 88FB 8B86 D8B5 A68C
## Reporting Non Security Issues
A great way to contribute to the project
is to send a detailed issue when you encounter a problem.
We always appreciate a well-written, thorough bug report.
Check that the project issues database
doesn't already include that problem or suggestion before submitting an issue.
If you find a match, add a quick "+1" or "I have this problem too."
Doing this helps prioritize the most common problems and requests.
When reporting issues, please include the following:
* The Android API you're using
* The device you're targeting
* The full output of any stack trace or compiler error
* A code snippet that reproduces the described behavior, if applicable
* Any other details that would be useful in understanding the problem
This information will help us review and fix your issue faster.
## Pull Requests
We **love** pull requests!
All contributions _will_ be licensed under the MIT license.
Code/comments should adhere to the following rules:
* Every Pull request must have an Issue associated to it. PRs with not
associated with an Issue will be closed
* Code build and Code Lint must pass.
* Names should be descriptive and concise.
* Although they are not mandatory, PRs that include significant testing will be
prioritized.
* All enhancements and bug fixes need to be documented in the CHANGELOG.
* When writing comments, use properly constructed sentences, including
punctuation.
* When documenting APIs and/or source code, don't make assumptions or make
implications about race, gender, religion, political orientation or anything
else that isn't relevant to the project.
* Remember that source code usually gets written once and read often: ensure
the reader doesn't have to make guesses. Make sure that the purpose and inner
logic are either obvious to a reasonably skilled professional, or add a
comment that explains it.
## Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
- (a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
- (b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
- (c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
- (d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
This contribution guide is inspired on great projects like [AlamoFire](https://github.com/Alamofire/Foundation/blob/master/CONTRIBUTING.md) and [CocoaPods](https://github.com/CocoaPods/CocoaPods/blob/master/CONTRIBUTING.md)

21
LICENSE

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2017-2021 Electric Coin Company
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.

82
README.md

@ -0,0 +1,82 @@
# zcash-android-wallet
A sample Android wallet using the [Zcash Android SDK](https://github.com/zcash/zcash-android-wallet-sdk).
### Motivation
[Dogfooding](https://en.wikipedia.org/wiki/Eating_your_own_dog_food) - _transitive verb_ - is the practice of an organization using its own product. This app was created to help us learn.
Please take note: the wallet is not an official product by ECC, but rather a tool for learning about our libraries that it is built on. This means that we do not have robust infrasturcture or user support for this application. We open sourced it as a resource to make wallet development easier for the Zcash ecosystem.
# Disclaimers
There are some known areas for improvement:
- This app is mainly intended for learning and improving the related libraries that it uses. There may be bugs.
- Traffic analysis, like in other cryptocurrency wallets, can leak some privacy of the user.
- The wallet requires a trust in the server to display accurate transaction information.
See the [Wallet App Threat Model](https://zcash.readthedocs.io/en/latest/rtd_pages/wallet_threat_model.html)
for more information about the security and privacy limitations of the wallet.
If you'd like to sign up to help us test, reach out on discord and let us know! We're always happy to get feedback!
# Description
This a sample wallet for the following set of features:
- z2z transactions w/ encrypted memos
- reply-to formatted memos
- z2t transactions
- transparent receive-only
- autoshielding on threshold from receive only t-address
note: z means sapling shielded addresses.
# Prerequisites
- [The code](https://github.com/zcash/zcash-android-wallet)
- [Android Studio](https://developer.android.com/studio/index.html) or [adb](https://www.xda-developers.com/what-is-adb/)
- An Android device or emulator
# Building the App
To run, clone the repo, open it in Android Studio and press play. It should just work.™
## Install from Android Studio
1. [Install Android Studio](https://developer.android.com/studio/install) and setup an emulator
1a. If using a device, be sure to [put it in developer mode](https://developer.android.com/studio/debug/dev-options) to enable side-loading apps
2. `Import` the zcash-android-wallet folder.
It will be recognized as an Android project.
3. Press play (once it is done opening and indexing)
## OR Install from the command line
To build from the command line, [setup ADB](https://www.xda-developers.com/install-adb-windows-macos-linux/) and connect your device. Then simply run this and it will both build and install the app:
```bash
cd /path/to/zcash-android-wallet
./gradlew
```
Note: The lack of an explicit Gradle task is not a typo. A default task is configured via [build.gradle.kts](build.gradle.kts).
Tip: On macOS and Linux, Gradle is invoked with `./gradlew`. On Windows, Gradle is invoked with `gradlew`.
# Included builds
To simplify implementation of SDK features in conjunction with changes to the app, a Gradle [Included Build](https://docs.gradle.org/current/userguide/composite_builds.html) can be configured.
1. Check out the SDK to a directory path of `../zcash-android-sdk` relative to the root of this app's repo. For example:
parent/
zcash-android-wallet/
zcash-android-sdk/
1. Verify that the `zcash-android-sdk` builds correctly on its own
1. Build `zcash-android-wallet`, setting the Gradle property `IS_SDK_INCLUDED_BUILD=true`
There are some limitations of included builds:
1. Properties from `zcash-android-wallet` will override those set in `zcash-android-sdk` with the same name
1. Modules in each project cannot share the same name. For this reason, build-conventions have different names in each repo (`zcash-android-sdk/build-conventions` vs `secant-android-wallet/build-convention`)
1. Kotlin and KSP versions will need to be coordinated between the two projects, because KSP is tightly coupled to the Kotlin version
# Contributing
Contributions are very much welcomed! Please read our [Contributing Guidelines](/CONTRIBUTING.md) and [Code of Conduct](/CONDUCT.md). Our backlog has many Issues tagged with the `good first issue` label. Please fork the repo and make a pull request for us to review.
# Reporting an issue
If you wish to report a security issue, please follow our [Responsible Disclosure guidelines](https://github.com/zcash/zcash-android-wallet-sdk/blob/master/responsible_disclosure.md).
For other kind of inquiries, feel welcome to open an Issue if you encounter a bug or would like to request a feature.

1
app/.gitignore

@ -0,0 +1 @@
/build

193
app/build.gradle

@ -0,0 +1,193 @@
import cash.z.ecc.android.Deps
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: "androidx.navigation.safeargs.kotlin"
apply plugin: 'com.github.ben-manes.versions'
archivesBaseName = 'zcash-android-wallet'
group = 'cash.z.ecc.android'
version = Deps.versionName
android {
ndkVersion "21.1.6352462"
compileSdkVersion Deps.compileSdkVersion
defaultConfig {
applicationId Deps.packageName
minSdkVersion Deps.minSdkVersion
targetSdkVersion Deps.targetSdkVersion
versionCode = Deps.versionCode
versionName = Deps.versionName
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
if (Boolean.parseBoolean(isUseTestOrchestrator)) {
testInstrumentationRunnerArguments clearPackageData: 'true'
}
multiDexEnabled true
resValue 'string', 'bugsnag_api_key', "${(project.findProperty('BUGSNAG_API_KEY') ?: System.getenv('BUGSNAG_API_KEY')) ?: ''}"
// this setting allows using color resources in vector drawables, rather than hardcoded values (note: only works when minApi is 21)
// per https://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.VectorDrawablesOptions.html: If set to an empty collection, all special handling of vector drawables will be disabled.
vectorDrawables.generatedDensities = []
}
buildFeatures {
viewBinding true
}
flavorDimensions 'network'
productFlavors {
// would rather name them "testnet" and "mainnet" but product flavor names cannot start with the word "test"
zcashtestnet {
dimension 'network'
applicationId 'cash.z.ecc.android.testnet'
buildConfigField "String", "DEFAULT_SERVER_URL", '"lite2.hushpool.is"'
matchingFallbacks = ['zcashtestnet', 'debug']
}
zcashmainnet {
dimension 'network'
buildConfigField "String", "DEFAULT_SERVER_URL", '"lite2.hushpool.is"'
matchingFallbacks = ['zcashmainnet', 'release']
}
}
signingConfigs {
placeholder {
storeFile file("${rootProject.projectDir}/placeholder.keystore")
keyAlias "androiddebugkey"
keyPassword "android"
storePassword "android"
}
}
buildTypes {
release {
minifyEnabled false
shrinkResources false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.placeholder
}
debug {
minifyEnabled false
shrinkResources false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
// builds for testing only in the wallet team, typically unfinished features
// this flavor can be installed alongside the others
qa {
initWith debug
debuggable true
applicationIdSuffix ".internal"
matchingFallbacks = ['debug']
signingConfig signingConfigs.placeholder
}
}
compileOptions {
// enable support for new language APIs but also fix the issue with zxing on API < 24
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
freeCompilerArgs += "-opt-in=kotlin.time.ExperimentalTime"
// freeCompilerArgs += "-Xopt-in=kotlinx.coroutines.ObsoleteCoroutinesApi"
// freeCompilerArgs += "-Xopt-in=kotlinx.coroutines.FlowPreview"
}
testOptions {
if (Boolean.parseBoolean(isUseTestOrchestrator)) {
execution 'ANDROIDX_TEST_ORCHESTRATOR'
}
}
kapt {
arguments {
arg 'dagger.fastInit', 'enabled'
arg 'dagger.fullBindingGraphValidation', 'ERROR'
}
}
packagingOptions {
resources {
excludes += ['META-INF/AL2.0', 'META-INF/LGPL2.1']
}
}
namespace 'cash.z.ecc.android'
applicationVariants.all { variant ->
variant.outputs.all {
if (variant.buildType.name == "qa") {
it.versionNameOverride = "${Deps.versionName}-QA"
}
outputFileName = "$archivesBaseName-v${Deps.versionName}-${variant.buildType.name}.apk"
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':qrecycler')
implementation project(':feedback')
implementation project(':mnemonic')
implementation project(':lockbox')
// Zcash
implementation Deps.Zcash.ANDROID_WALLET_PLUGINS
implementation Deps.Zcash.SDK
// Kotlin
implementation Deps.Kotlin.STDLIB
// Android
implementation Deps.AndroidX.ANNOTATION
implementation Deps.AndroidX.APPCOMPAT
implementation Deps.AndroidX.BIOMETRICS
implementation Deps.AndroidX.CONSTRAINT_LAYOUT
implementation Deps.AndroidX.CORE_KTX
implementation Deps.AndroidX.FRAGMENT_KTX
implementation Deps.AndroidX.LEGACY
implementation Deps.AndroidX.PAGING
implementation Deps.AndroidX.RECYCLER
implementation Deps.AndroidX.CameraX.CAMERA2
implementation Deps.AndroidX.CameraX.CORE
implementation Deps.AndroidX.CameraX.LIFECYCLE
implementation Deps.AndroidX.CameraX.View.EXT
implementation Deps.AndroidX.CameraX.View.VIEW
implementation Deps.AndroidX.Lifecycle.LIFECYCLE_RUNTIME_KTX
implementation Deps.AndroidX.Navigation.FRAGMENT_KTX
implementation Deps.AndroidX.Navigation.UI_KTX
implementation Deps.AndroidX.Room.ROOM_KTX
kapt Deps.AndroidX.Room.ROOM_COMPILER
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
// Google
implementation Deps.Google.GUAVA
implementation Deps.Google.MATERIAL
// grpc-java
implementation Deps.Grpc.ANDROID
implementation Deps.Grpc.OKHTTP
implementation Deps.Grpc.PROTOBUG
implementation Deps.Grpc.STUB
implementation 'com.squareup.okio:okio:2.8.0'
implementation Deps.JavaX.JAVA_ANNOTATION
// Misc.
implementation Deps.Misc.LOTTIE
implementation Deps.Misc.CHIPS
implementation Deps.Misc.Plugins.QR_SCANNER
// Tests
testImplementation Deps.Test.JUNIT
testImplementation Deps.Test.MOKITO
testImplementation Deps.Test.MOKITO_KOTLIN
androidTestImplementation Deps.Kotlin.REFLECT
androidTestImplementation(Deps.Kotlin.Coroutines.TEST)
androidTestImplementation Deps.Test.Android.JUNIT
androidTestImplementation Deps.Test.Android.CORE
androidTestImplementation Deps.Test.Android.FRAGMENT
androidTestImplementation Deps.Test.Android.ESPRESSO
androidTestImplementation Deps.Test.Android.ESPRESSO_INTENTS
androidTestImplementation Deps.Test.Android.NAVIGATION
// androidTestImplementation is preferred, but then the androidx.fragment.app.testing.FragmentScenario$EmptyFragmentActivity isn't available
debugImplementation Deps.Test.Android.FRAGMENT
}
defaultTasks 'clean', 'assembleZcashmainnetRelease'

12
app/proguard-rules.pro

@ -0,0 +1,12 @@
-dontobfuscate
-keepattributes SourceFile,LineNumberTable
# Reports
-printusage build/outputs/logs/R8-removed-code-report.txt
-printseeds build/outputs/logs/R8-entry-points-report.txt
## Okio
# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java
-dontwarn org.codehaus.mojo.animal_sniffer.*
#-keep class cash.z.** { *; }

43
app/src/androidTest/java/cash/z/ecc/android/MemoTest.kt

@ -0,0 +1,43 @@
package cash.z.ecc.android
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.delay
import org.junit.Ignore
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@Ignore("It'd need additional implementation changes to have this one working.")
@RunWith(AndroidJUnit4::class)
// @RunWith(Parameterized::class)
class MemoTest(val input: String, val output: String) {
// @Test
// fun testExtractValidAddress() = runBlocking {
// val result = MemoUtil.findAddressInMemo(input, ::validateMemo)
// assertEquals(output, result)
// }
suspend fun validateMemo(memo: String): Boolean {
delay(20)
return true
}
companion object {
val validTaddr = "tmWGKMEpxSUf97H12MmGtgiER1drVbGjzWM"
val validZaddr = "ztestsapling1ukadr59p0hxcl2pq8mfagnfx3h74nsusdkm59gkys7hxze92whxj54mfdn3n37zusum7w4jlj35"
val invalidAddr = "ztestsaplinn9ukadr59p0hxcl2pq8mfagnfx3h74nsusdkm59gkys7hxze92whxj54mfdn3n37zusum7w4jlj35"
@JvmStatic
@Parameterized.Parameters
fun data() = listOf(
arrayOf(
"thanks for the food reply-to: $validZaddr",
validZaddr
),
arrayOf(
"thanks for the food reply-to: $validTaddr",
validTaddr
)
)
}
}

138
app/src/androidTest/java/cash/z/ecc/android/integration/ConversionsTest.kt

@ -0,0 +1,138 @@
package cash.z.ecc.android.integration
import androidx.test.ext.junit.runners.AndroidJUnit4
import cash.z.ecc.android.ext.WalletZecFormmatter
import cash.z.ecc.android.sdk.model.Zatoshi
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ConversionsTest {
@Test
fun testToZatoshi() {
val input = "1"
val result = WalletZecFormmatter.toZatoshi(input)
Assert.assertEquals(100_000_000L, result)
}
@Test
fun testToZecString_short() {
val input = Zatoshi(112_340_000L)
val result = WalletZecFormmatter.toZecStringShort(input)
Assert.assertEquals("1.1234", result)
}
@Test
fun testToZecString_shortRoundUp() {
val input = Zatoshi(112_355_600L)
val result = WalletZecFormmatter.toZecStringShort(input)
Assert.assertEquals("1.1236", result)
}
@Test
fun testToZecString_shortRoundDown() {
val input = Zatoshi(112_343_999L)
val result = WalletZecFormmatter.toZecStringShort(input)
Assert.assertEquals("1.1234", result)
}
@Test
fun testToZecString_shortRoundHalfEven() {
val input = Zatoshi(112_345_000L)
val result = WalletZecFormmatter.toZecStringShort(input)
Assert.assertEquals("1.1234", result)
}
@Test
fun testToZecString_shortRoundHalfOdd() {
val input = Zatoshi(112_355_000L)
val result = WalletZecFormmatter.toZecStringShort(input)
Assert.assertEquals("1.1236", result)
}
@Test
fun testToBigDecimal_noCommas() {
val input = "1000"
val result = WalletZecFormmatter.toBigDecimal(input)!!
Assert.assertEquals(1000, result.longValueExact())
}
@Test
fun testToBigDecimal_thousandComma() {
val input = "1,000"
val result = WalletZecFormmatter.toBigDecimal(input)!!
Assert.assertEquals(1000, result.longValueExact())
}
@Test
fun testToBigDecimal_thousandCommaWithDecimal() {
val input = "1,000.00"
val result = WalletZecFormmatter.toBigDecimal(input)!!
Assert.assertEquals(1000, result.longValueExact())
}
@Test
fun testToBigDecimal_oneDecimal() {
val input = "1.000"
val result = WalletZecFormmatter.toBigDecimal(input)!!
Assert.assertEquals(1, result.longValueExact())
}
@Test
fun testToBigDecimal_thousandWithThinSpace() {
val input = "1 000"
val result = WalletZecFormmatter.toBigDecimal(input)!!
Assert.assertEquals(1000, result.longValueExact())
}
@Test
fun testToBigDecimal_oneWithThinSpace() {
val input = "1.000 000"
val result = WalletZecFormmatter.toBigDecimal(input)!!
Assert.assertEquals(1, result.longValueExact())
}
@Test
fun testToBigDecimal_oneDecimalWithComma() {
val input = "1.000,00"
val result = WalletZecFormmatter.toBigDecimal(input)!!
Assert.assertEquals(1, result.longValueExact())
}
@Test
fun testToZecString_full() {
val input = Zatoshi(112_341_123L)
val result = WalletZecFormmatter.toZecStringFull(input)
Assert.assertEquals("1.12341123", result)
}
@Test
fun testToZecString_fullRoundUp() {
val input = Zatoshi(112_355_678L)
val result = WalletZecFormmatter.toZecStringFull(input)
Assert.assertEquals("1.12355678", result)
}
@Test
fun testToZecString_fullRoundDown() {
val input = Zatoshi(112_349_999L)
val result = WalletZecFormmatter.toZecStringFull(input)
Assert.assertEquals("1.12349999", result)
}
@Test
fun testToZecString_fullRoundHalfEven() {
val input = Zatoshi(112_250_009L)
val result = WalletZecFormmatter.toZecStringFull(input)
Assert.assertEquals("1.12250009", result)
}
@Test
fun testToZecString_fullRoundHalfOdd() {
val input = Zatoshi(112_350_004L)
val result = WalletZecFormmatter.toZecStringFull(input)
Assert.assertEquals("1.12350004", result)
}
}

122
app/src/androidTest/java/cash/z/ecc/android/integration/IntegrationTest.kt

@ -0,0 +1,122 @@
package cash.z.ecc.android.integration
import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.kotlin.mnemonic.Mnemonics
import kotlinx.coroutines.test.runTest
import okio.Buffer
import okio.GzipSink
import okio.buffer
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class IntegrationTest {
private lateinit var appContext: Context
private val network = ZcashNetwork.Testnet
private val mnemonics = Mnemonics()
private val phrase =
"human pulse approve subway climb stairs mind gentle raccoon warfare fog roast sponsor" +
" under absorb spirit hurdle animal original honey owner upper empower describe"
@Before
fun start() {
appContext = InstrumentationRegistry.getInstrumentation().targetContext
}
@Test
fun testSeed_generation() {
val seed = mnemonics.toSeed(phrase.toCharArray())
assertEquals(
"Generated incorrect BIP-39 seed!",
"f4e3d38d9c244da7d0407e19a93c80429614ee82dcf62c141235751c9f1228905d12a1f275f" +
"5c22f6fb7fcd9e0a97f1676e0eec53fdeeeafe8ce8aa39639b9fe",
seed.toHex()
)
}
@Test
fun testSeed_storage() {
val seed = mnemonics.toSeed(phrase.toCharArray())
val lb = LockBox(appContext)
lb.setBytes("seed", seed)
assertTrue(seed.contentEquals(lb.getBytes("seed")!!))
}
@Test
fun testPhrase_storage() {
val lb = LockBox(appContext)
val phraseChars = phrase.toCharArray()
lb.setCharsUtf8("phrase", phraseChars)
assertTrue(phraseChars.contentEquals(lb.getCharsUtf8("phrase")!!))
}
@Test
fun testPhrase_maxLengthStorage() {
val lb = LockBox(appContext)
// find and expose the max length
var acceptedSize = 256
while (acceptedSize > 0) {
try {
lb.setCharsUtf8("temp", nextString(acceptedSize).toCharArray())
break
} catch (t: Throwable) {
}
acceptedSize--
}
val maxSeedPhraseLength = 8 * 24 + 23 // 215 (max length of each word is 8)
assertTrue(
"LockBox does not support the maximum length seed phrase." +
" Expected: $maxSeedPhraseLength but was: $acceptedSize",
acceptedSize > maxSeedPhraseLength
)
}
@Test
@Ignore("It'd need additional implementation changes to have this one working.")
fun testAddress() = runTest {
val seed = mnemonics.toSeed(phrase.toCharArray())
val initializer = Initializer.new(appContext) { config ->
// config.newWallet(seed, network)
}
assertEquals(
"Generated incorrect z-address!",
"zs1gn2ah0zqhsxnrqwuvwmgxpl5h3ha033qexhsz8tems53fw877f4gug353eefd6z8z3n4zxty65c",
// initializer.rustBackend.getShieldedAddress()
)
initializer.erase()
}
private fun ByteArray.toHex(): String {
val sb = StringBuilder(size * 2)
for (b in this)
sb.append(String.format("%02x", b))
return sb.toString()
}
fun String.gzip(): ByteArray {
val result = Buffer()
val sink = GzipSink(result).buffer()
sink.use {
sink.write(toByteArray())
}
return result.readByteArray()
}
fun nextString(length: Int): String {
val allowedChars = "ACGT"
return (1..length)
.map { allowedChars.random() }
.joinToString("")
}
}

73
app/src/androidTest/java/cash/z/ecc/android/integration/LockBoxTest.kt

@ -0,0 +1,73 @@
package cash.z.ecc.android.integration
import android.content.Context
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.ecc.android.lockbox.LockBox
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
class LockBoxTest {
lateinit var lockBox: LockBox
lateinit var appContext: Context
private val hex = ('a'..'f') + ('0'..'9')
private val iterations = 50
@Before
fun setUp() {
appContext = InstrumentationRegistry.getInstrumentation().targetContext
lockBox = LockBox(appContext)
lockBox.clear()
}
@Test
@LargeTest
@Ignore("This test is extremely slow")
fun testLongString() {
var successCount = 0
repeat(iterations) {
val sampleHex = List(500) { hex.random() }.joinToString("")
lockBox["longStr"] = sampleHex
val actual: String = lockBox["longStr"]!!
if (sampleHex == actual) successCount++
lockBox.clear()
}
assertEquals(iterations, successCount)
}
@Test
@LargeTest
@Ignore("This test is extremely slow")
fun testShortString() {
var successCount = 0
repeat(iterations) {
val sampleHex = List(50) { hex.random() }.joinToString("")
lockBox["shortStr"] = sampleHex
val actual: String = lockBox["shortStr"]!!
if (sampleHex == actual) successCount++
lockBox.clear()
}
assertEquals(iterations, successCount)
}
@Test
@LargeTest
@Ignore("This test is extremely slow")
fun testGiantString() {
var successCount = 0
repeat(iterations) {
val sampleHex = List(2500) { hex.random() }.joinToString("")
lockBox["giantStr"] = sampleHex
val actual: String = lockBox["giantStr"]!!
if (sampleHex == actual) successCount++
lockBox.clear()
}
assertEquals(iterations, successCount)
}
}

41
app/src/androidTest/java/cash/z/ecc/android/preference/PreferenceKeysTest.kt

@ -0,0 +1,41 @@
package cash.z.ecc.android.preference
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import java.lang.reflect.Modifier
import kotlin.reflect.full.memberProperties
@RunWith(AndroidJUnit4::class)
class PreferenceKeysTest {
@SmallTest
@Test
@Throws(IllegalAccessException::class)
fun fields_public_static_and_final() {
PreferenceKeys::class.java.fields.forEach {
val modifiers = it.modifiers
assertThat(Modifier.isFinal(modifiers), equalTo(true))
assertThat(Modifier.isStatic(modifiers), equalTo(true))
assertThat(Modifier.isPublic(modifiers), equalTo(true))
}
}
// This test is primary to prevent copy-paste errors in preference keys
@SmallTest
@Test
fun key_values_unique() {
val fieldValueSet = mutableSetOf<String>()
PreferenceKeys::class.memberProperties
.map { it.getter.call() }
.map { it as String }
.forEach {
assertThat("Duplicate key $it", fieldValueSet.contains(it), equalTo(false))
fieldValueSet.add(it)
}
}
}

46
app/src/androidTest/java/cash/z/ecc/android/preference/PreferencesTest.kt

@ -0,0 +1,46 @@
package cash.z.ecc.android.preference
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import cash.z.ecc.android.preference.model.DefaultValue
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import java.lang.reflect.Modifier
import kotlin.reflect.full.memberProperties
@RunWith(AndroidJUnit4::class)
class PreferencesTest {
@SmallTest
@Test
@Throws(IllegalAccessException::class)
fun fields_public_static_and_final() {
Preferences::class.java.fields.forEach {
val modifiers = it.modifiers
assertThat(Modifier.isFinal(modifiers), equalTo(true))
assertThat(Modifier.isStatic(modifiers), equalTo(true))
assertThat(Modifier.isPublic(modifiers), equalTo(true))
}
}
// This test is primary to prevent copy-paste errors in preference keys
@SmallTest
@Test
fun key_values_unique() {
val fieldValueSet = mutableSetOf<String>()
Preferences::class.memberProperties
.map { it.getter.call(Preferences) }
.map { it as DefaultValue<*> }
.forEach {
assertThat(
"Duplicate key ${it.key}",
fieldValueSet.contains(it.key),
equalTo(false)
)
fieldValueSet.add(it.key)
}
}
}

32
app/src/androidTest/java/cash/z/ecc/android/test/FragmentNavigationScenario.kt

@ -0,0 +1,32 @@
package cash.z.ecc.android.test
import androidx.annotation.IdRes
import androidx.fragment.app.Fragment
import androidx.fragment.app.testing.FragmentScenario
import androidx.navigation.Navigation
import androidx.navigation.testing.TestNavHostController
import androidx.test.core.app.ApplicationProvider
data class FragmentNavigationScenario<T : Fragment>(
val fragmentScenario: FragmentScenario<T>,
val navigationController: TestNavHostController
) {
companion object {
fun <T : Fragment> new(
fragmentScenario: FragmentScenario<T>,
@IdRes currentDestination: Int
): FragmentNavigationScenario<T> {
val navController = TestNavHostController(ApplicationProvider.getApplicationContext())
fragmentScenario.onFragment {
navController.setGraph(cash.z.ecc.android.R.navigation.mobile_navigation)
navController.setCurrentDestination(currentDestination)
Navigation.setViewNavController(it.requireView(), navController)
}
return FragmentNavigationScenario(fragmentScenario, navController)
}
}
}

29
app/src/androidTest/java/cash/z/ecc/android/test/UiTestPrerequisites.kt

@ -0,0 +1,29 @@
package cash.z.ecc.android.test
import android.annotation.TargetApi
import android.content.Context
import android.os.Build
import android.os.PowerManager
import androidx.test.core.app.ApplicationProvider
import org.junit.Before
import java.lang.AssertionError
/**
* Subclass this for UI tests to ensure they run correctly. This helps when developers run tests
* against a physical device that might have gone to sleep.
*/
open class UiTestPrerequisites {
@Before
fun verifyScreenOn() {
if (!isScreenOn()) {
throw AssertionError("Screen must be on for UI tests to run") // $NON-NLS
}
}
@TargetApi(Build.VERSION_CODES.KITKAT_WATCH)
private fun isScreenOn(): Boolean {
val powerService = ApplicationProvider.getApplicationContext<Context>()
.getSystemService(Context.POWER_SERVICE) as PowerManager
return powerService.isInteractive
}
}

154
app/src/androidTest/java/cash/z/ecc/android/ui/home/AutoshieldingInformationFragmentTest.kt

@ -0,0 +1,154 @@
package cash.z.ecc.android.ui.home
import android.content.ComponentName
import android.content.Context
import androidx.fragment.app.testing.FragmentScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import cash.z.ecc.android.preference.Preferences
import cash.z.ecc.android.preference.SharedPreferenceFactory
import cash.z.ecc.android.preference.model.get
import cash.z.ecc.android.test.FragmentNavigationScenario
import cash.z.ecc.android.test.UiTestPrerequisites
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class AutoshieldingInformationFragmentTest : UiTestPrerequisites() {
@Test
@MediumTest
fun dismiss_returns_home_when_autoshield_not_available() {
val fragmentNavigationScenario = newScenario(isAutoshieldAvailable = false)
onView(withId(cash.z.ecc.android.R.id.button_autoshield_dismiss)).also {
it.perform(ViewActions.click())
}
assertThat(
fragmentNavigationScenario.navigationController.currentDestination?.id,
equalTo(cash.z.ecc.android.R.id.nav_home)
)
}
@Test
@MediumTest
fun dismiss_starts_autoshield_when_autoshield_available() {
val fragmentNavigationScenario = newScenario(isAutoshieldAvailable = true)
onView(withId(cash.z.ecc.android.R.id.button_autoshield_dismiss)).also {
it.perform(ViewActions.click())
}
assertThat(
fragmentNavigationScenario.navigationController.currentDestination?.id,
equalTo(cash.z.ecc.android.R.id.nav_shield_final)
)
}
@Test
@MediumTest
fun clicking_more_info_launches_browser() {
val fragmentNavigationScenario = newScenario(isAutoshieldAvailable = false)
onView(withId(cash.z.ecc.android.R.id.button_autoshield_more_info)).also {
it.perform(ViewActions.click())
}
assertThat(
fragmentNavigationScenario.navigationController.currentDestination?.id,
equalTo(cash.z.ecc.android.R.id.nav_autoshielding_info_details)
)
// Note: it is difficult to verify that the browser is launched, because of how the
// navigation component works.
}
@Test
@MediumTest
fun starting_fragment_does_not_launch_activities() {
Intents.init()
try {
val fragmentNavigationScenario = newScenario(isAutoshieldAvailable = false)
// The test framework launches an Activity to host the Fragment under test
// Since the class name is not a public API, this could break in the future with newer
// versions of the AndroidX Test libraries.
intended(
hasComponent(
ComponentName(
ApplicationProvider.getApplicationContext(),
"androidx.test.core.app.InstrumentationActivityInvoker\$BootstrapActivity"
)
)
)
// Verifying that no other Activities (e.g. the link view) are launched without explicit
// user interaction
Intents.assertNoUnverifiedIntents()
assertThat(
fragmentNavigationScenario.navigationController.currentDestination?.id,
equalTo(cash.z.ecc.android.R.id.nav_autoshielding_info)
)
} finally {
Intents.release()
}
}
@Test
@MediumTest
fun display_fragment_sets_preference() {
newScenario(isAutoshieldAvailable = false)
assertThat(
Preferences.isAcknowledgedAutoshieldingInformationPrompt.get(ApplicationProvider.getApplicationContext<Context>()),
equalTo(true)
)
}
@Test
@MediumTest
fun back_navigates_home() {
val fragmentNavigationScenario = newScenario(isAutoshieldAvailable = false)
fragmentNavigationScenario.fragmentScenario.onFragment {
// Probably closest we can come to simulating back with the navigation test framework
fragmentNavigationScenario.navigationController.navigateUp()
}
assertThat(
fragmentNavigationScenario.navigationController.currentDestination?.id,
equalTo(cash.z.ecc.android.R.id.nav_home)
)
}
companion object {
private fun newScenario(isAutoshieldAvailable: Boolean): FragmentNavigationScenario<AutoshieldingInformationFragment> {
// Clear preferences for each scenario, as this most closely reflects how this fragment
// is used in the app, as it is displayed usually on first launch
SharedPreferenceFactory.getSharedPreferences(ApplicationProvider.getApplicationContext())
.edit().clear().apply()
val scenario = FragmentScenario.launchInContainer(
AutoshieldingInformationFragment::class.java,
HomeFragmentDirections.actionNavHomeToAutoshieldingInfo(isAutoshieldAvailable).arguments,
cash.z.ecc.android.R.style.ZcashTheme,
null
)
return FragmentNavigationScenario.new(
scenario,
cash.z.ecc.android.R.id.nav_autoshielding_info
)
}
}
}

43
app/src/main/AndroidManifest.xml

@ -0,0 +1,43 @@
<manifest
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.CAMERA" />
<application
android:name="cash.z.ecc.android.ZcashWalletApp"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/ZcashTheme">
<activity android:name=".ui.MainActivity" android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true"
android:writePermission="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<!-- Mixpanel options -->
<meta-data android:name="com.mixpanel.android.MPConfig.AutoShowMixpanelUpdates" android:value="false" />
<meta-data android:name="com.mixpanel.android.MPConfig.EnableDebugLogging" android:value="false" />
<meta-data android:name="com.mixpanel.android.MPConfig.DisableDecideChecker" android:value="true" />
<meta-data android:name="com.mixpanel.android.MPConfig.DisableEmulatorBindingUI" android:value="true" />
<meta-data android:name="com.mixpanel.android.MPConfig.DisableGestureBindingUI" android:value="true" />
<meta-data android:name="com.mixpanel.android.MPConfig.DisableViewCrawler" android:value="true" />
<meta-data android:name="com.mixpanel.android.MPConfig.IgnoreInvisibleViewsVisualEditor" android:value="true" />
</application>
</manifest>

7
app/src/main/assets/saplingtree/mainnet/1225600.json

@ -0,0 +1,7 @@
{
"network": "main",
"height": 1225600,
"hash": "0000000000196bafb2472eb7a3b1aa85bccc00904d5650a7952dd437859fc38c",
"time": 1619215931,
"tree": "0128411e8cb2f543c46ca943736c96ab4fa86cab1e3e2e394ed458d56b395bd5050120303bbaf4f19e37a06c1e9ea815567fc23990cc65494c2be29f8e6e4a9d9a6c130001010e9388fdf9bf49e3adf4adb57d83e0b5ba34f63a2681eceb54d3aaaf236b210001c0920d177f77815c4f643c2b331bd6b86d291d6bc2c1c20f6bc501f49adcdb3b000001b7958828206f53c25465943d4173af16de3cee94ae01b2e17a32c51c06fde3630001b31ed2e29d0d894604f0d7bf4735d4bcf25dc9f859c5e296a5689af7ca8c94720134ca9a7c4309349dfe003f3b4b95898b4303631e9be3a25b4e917a4f3472b52f00000121c25bceccda091622bfac1b7973ffaa638abe1f334b3b56f48dc93dc549c9070001ece344ca21dbd3b681f167163d4792165efe8239390afc13378e50d044fee65a01089a1f9d50a037cc66aba4400b1703bcbb66f5f2993fd0dd3bb726e35940916700000118f64df255c9c43db708255e7bf6bffd481e5c2f38fe9ed8f3d189f7f9cf2644"
}

7
app/src/main/assets/saplingtree/mainnet/1250000.json

@ -0,0 +1,7 @@
{
"network": "main",
"height": 1250000,
"hash": "0000000000f3d2c352c395d66866032bcb67094228dd4a27e561b1c399ea612e",
"time": 1621056898,
"tree": "01c9a0dd6f6dfaaafe6ae4b432c2d1c41d2a73e564c8cb6d2c5ab637c7001a2456001300000000017da32b486a8ea9f13afb93b99d2b1de69aa969e7c2fd7b9ee958bece70c08d6b000001b3a4486b176dfcedc0b3d9287c0333ff464ecbd02bac7c89bcda7932e6a0a36100010d451c18b56877b8a11cb401ab7024c82b9669ede862a53e461087f57220035001a1c5260bc4dfe010510b8135209c6f64229965f71717f1e693abdcf88a58f36700012f0bf70e372e536fc3b76ecd7e2b69eebf2fbcf71b828c64b0a8b99390fbf754018e7922ca798cd3e26d3369ca2425ec19baa7d79407a979ec1090ae48fdcd094a01ece344ca21dbd3b681f167163d4792165efe8239390afc13378e50d044fee65a01089a1f9d50a037cc66aba4400b1703bcbb66f5f2993fd0dd3bb726e35940916700000118f64df255c9c43db708255e7bf6bffd481e5c2f38fe9ed8f3d189f7f9cf2644"
}

7
app/src/main/assets/saplingtree/mainnet/1290000.json

@ -0,0 +1,7 @@
{
"network": "main",
"height": 1290000,
"hash": "00000000014836c3cbc011276cbd3702a76a1fea7eb2c0c2c257321220376450",
"time": 1624075741,
"tree": "01accf4fc3dc4233bbe757f94e0d4cd23b4aa2e6ac472601f4f53ca4dc86a8a05901fae977171a6103a0338990e073ffe50e29fc8bf0400dcd3378ebfe7a146ed1481300014f7b33dd5159ac66f2670b7db8925065e7154e0199ff7ee7559b276ba56ad1200173e9881f21357e54027a4275114f0f6ad4ca17143554182f63c77f3288a23a20011d65465ab942440e200d429ef892452b4b05c5b21e9a6e6d968a719c67b5e85b000000000000000150926c74975e2d8ff095defb75a4a6d9f17007e87a74230a65a3265d8f45032900012ffde6dccbef68b60cd7b4e7a8fe7989f5954fa4bacad01b247d16b9bfa5084000000125911f4524469c00ccb1ba69e64f0ee7380c8d17bbfc76ecd238421b86eb6e09000118f64df255c9c43db708255e7bf6bffd481e5c2f38fe9ed8f3d189f7f9cf2644"
}

7
app/src/main/assets/saplingtree/mainnet/1300000.json

@ -0,0 +1,7 @@
{
"network": "main",
"height": 1300000,
"hash": "00000000027222bdbcf9c5f807f851f97312ac6e0dbbc2b93f2be21a69c59d44",
"time": 1624830312,
"tree": "01f5a97e2679a2bb9103caf37b825f92fcd73fff836234844dfcf1815394522b2c01526587b9b9e8aeb0eb572d81fec1f5127b8278ba0f57e451bd6b796596940a2213000131c7ff90fafff6159b8fb6544a2bcbba6c102903158fce8f9a9d3c6654abb23300013555cb7f4f79badeaca9bf2dca5a8704f0929053d50e95c03002f9a4d5286c3a01ad3557e11c1607ec888dc84f5f8899c3c79fb1f50b613946452ec7dd5e53763c0001c4583f4482b949390dba355fc8fa63019c83acd644ddd633cb50211d236f870600000001088da0d78eefd0c222507927e403b972d0890d0c31e08b02268fbe39ac4a6e170001edf82d4e2b4893ea2028ca8c5149e50a4c358b856d73f2de2b9a22034fa78f22012ffde6dccbef68b60cd7b4e7a8fe7989f5954fa4bacad01b247d16b9bfa5084000000125911f4524469c00ccb1ba69e64f0ee7380c8d17bbfc76ecd238421b86eb6e09000118f64df255c9c43db708255e7bf6bffd481e5c2f38fe9ed8f3d189f7f9cf2644"
}

7
app/src/main/assets/saplingtree/mainnet/1335000.json

@ -0,0 +1,7 @@
{
"network": "main",
"height": 1335000,
"hash": "00000000001d428474214f2844ac7adacab9c9b706f89ebb24e1e43189edff2d",
"time": 1627468889,
"tree": "01105d94f868041b1680f862dad6211ab815a30c79a63b839c2b2043ce6530834801e53ee3fef11ddfaef984c8653dffa0354929b79aad7321b00c10cb3b60c8b7111301f5693ff9b17a8fc0b032c192841d1fc08b7ec9fe4fcc2b628a550434af70886a01838a7001b5ed5dcdec7bce1ea4250bbeebe8c22aa27fd69e7baf343458e95c7101030f11dfda75a9e4a63bab19fe3bf92c545a3f58a57ca41ae7609290dad01436018923004af490f5718e834215ef61f2f60aee24685c1c2cffb3c686dff57ab82501eb86680f83fa0f9c47da3875645344a2734d56edcf1d99747ecbf25ea0e86e22000001cf6872911593b4f1af2fd03dce8a48d434af849ad1bc872442e7881bbc04e8610168fbde909e21c25e1a686fac9982ee11fb0d05da3568579bfba8b71f7632d62700012965494015cdab2ce010c1ae4ea88306c286128275de391dcf57d3fa85be7e1b01a090ee174239a34a5d684425d09006d238c6075a61c5842d0fc26043f09ccd7001a2b7ee187c7b8ce18ebda8600bed7695b12f7d35ac971ed6ee67184a7ceebd490001b35fe4a943a47404f68db220c77b0573e13c3378a65c6f2396f93be7609d8f2a000125911f4524469c00ccb1ba69e64f0ee7380c8d17bbfc76ecd238421b86eb6e09000118f64df255c9c43db708255e7bf6bffd481e5c2f38fe9ed8f3d189f7f9cf2644"
}

7
app/src/main/assets/saplingtree/testnet/1380300.json

@ -0,0 +1,7 @@
{
"network": "test",
"height": 1380300,
"hash": "00342c648fb9c5d109df4dd5b7849a4357f27f1dfdb8d3a0071e8254072d1a4a",
"time": 1619216615,
"tree": "01f5b47ef533c9b6240826210d7e66691f36b21ac1ce1e4a231399ff4f8b1286600198dc26bbe8f037c5dbd8a43e94c482bb513898bd1ee1a734c07c57450b9ec01b1000000001b18e52aa826dcf85a08ae15d1bb4c8559166fcd5cffd74b597a8b50bf32d311100018dc0c02e20384fcdc238a6c01a0e4598da69f546646acc177fd91b86a0f8236200000001ba0d7aa9e68417291c63b835fa64114f5899208238de59ee360f594c8b6c1b72018469338dcbdf2f7e54bca5bc3e1c5fad4a656f206040436d3d0433a901218b5e016d559de7a1a382349cf97fe01a2fba41a49bb5e3b306d9ff8c2bcc301c731c00000001f08f39275112dd8905b854170b7f247cf2df18454d4fa94e6e4f9320cca05f24011f8322ef806eb2430dc4a7a41c1b344bea5be946efc7b4349c1c9edb14ff9d39"
}

7
app/src/main/assets/saplingtree/testnet/1450000.json

@ -0,0 +1,7 @@
{
"network": "test",
"height": 1450000,
"hash": "000008a97bc133de13ca304e0c6a2a1b3f2facdceac2cde5b4141179f2a743cc",
"time": 1623815069,
"tree": "0175626cf9d8448de98f68fcc585dd7a276c946c11bbc3b192ee08db99c542b86b01acf5a110dc7ab911b534984c46bf56592f0c4cc8cf70dbd6a9cc4a5b47d2c81c1001c91f518ccb74093a217a640c537b69b095de058e0430046c8783f231caa1fa4201f7c982ce76b2c9343fb771e077357322f9a7dabfd7ab93b7adee32806c930d6600000170910ab6355ec614412fae56dad5fdc1747ce1b306a4b8ae03b77513b612b00800000000000000013d2fd009bf8a22d68f720eac19c411c99014ed9c5f85d5942e15d1fc039e28680001f08f39275112dd8905b854170b7f247cf2df18454d4fa94e6e4f9320cca05f24011f8322ef806eb2430dc4a7a41c1b344bea5be946efc7b4349c1c9edb14ff9d39"
}

7
app/src/main/assets/saplingtree/testnet/1454000.json

@ -0,0 +1,7 @@
{
"network": "test",
"height": 1454000,
"hash": "003254b452f221d36ba81d051a1a63edeb203de7ab457500d08b4110bcc86620",
"time": 1624073536,
"tree": "01007501338f9d31446b9c0228b87e81886555100fbb1b5bec7966617559d5400901d830393653a5379f1f071bffa191f9b56d0664859d9b19b9e9ae4e1c76f7d34f1001ca92ad0eeb818c3bb57ca30ed500dd58703fe14c4837f14ac8a1491622f0a8550001a1d6a89c888e46ce950d5af54739e9847fab81f383586ad5dc51dd00f65ed85d0160f01e9c484861b220f5a4650119f192217a89854ada30019fae9ab46ff4c4120001462c8d06a58ddec91ed309dcb041cdedcca73446889496332054d54e1561633b000000000000013d2fd009bf8a22d68f720eac19c411c99014ed9c5f85d5942e15d1fc039e28680001f08f39275112dd8905b854170b7f247cf2df18454d4fa94e6e4f9320cca05f24011f8322ef806eb2430dc4a7a41c1b344bea5be946efc7b4349c1c9edb14ff9d39"
}

BIN
app/src/main/assets/sound_receive_small.mp3

Binary file not shown.

60
app/src/main/java/cash/z/ecc/android/StrictModeHelper.kt

@ -0,0 +1,60 @@
package cash.z.ecc.android
import android.annotation.SuppressLint
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.StrictMode
object StrictModeHelper {
fun enableStrictMode() {
configureStrictMode()
// Workaround for Android bug
// https://issuetracker.google.com/issues/36951662
// Not needed if target O_MR1 and running on O_MR1
// Don't really need to check target, because of Google Play enforcement on targetSdkVersion for app updates
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) {
Handler(Looper.getMainLooper()).postAtFrontOfQueue { configureStrictMode() }
}
}
@SuppressLint("NewApi")
private fun configureStrictMode() {
StrictMode.enableDefaults()
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder().apply {
detectAll()
penaltyLog()
}.build()
)
// Don't enable missing network tags, because those are noisy.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder().apply {
detectActivityLeaks()
detectCleartextNetwork()
detectContentUriWithoutPermission()
detectFileUriExposure()
detectLeakedClosableObjects()
detectLeakedRegistrationObjects()
detectLeakedSqlLiteObjects()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// Disable because this is mostly flagging Android X and Play Services
// builder.detectNonSdkApiUsage();
}
}.build()
)
} else {
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder().apply {
detectAll()
penaltyLog()
}.build()
)
}
}
}

117
app/src/main/java/cash/z/ecc/android/ZcashWalletApp.kt

@ -0,0 +1,117 @@
package cash.z.ecc.android
import android.app.Application
import android.content.Context
import androidx.camera.camera2.Camera2Config
import androidx.camera.core.CameraXConfig
import cash.z.ecc.android.di.DependenciesHolder
import cash.z.ecc.android.ext.tryWithWarning
import cash.z.ecc.android.feedback.FeedbackCoordinator
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.util.twig
import kotlinx.coroutines.*
class ZcashWalletApp : Application(), CameraXConfig.Provider {
private val coordinator: FeedbackCoordinator
get() = DependenciesHolder.feedbackCoordinator
lateinit var defaultNetwork: ZcashNetwork
var creationTime: Long = 0
private set
var creationMeasured: Boolean = false
/** The amount of transparent funds that need to accumulate before autoshielding is triggered */
val autoshieldThreshold: Long = Zatoshi.ZATOSHI_PER_ZEC // 1 ZEC
/**
* Intentionally private Scope for use with launching Feedback jobs. The feedback object has the
* longest scope in the app because it needs to be around early in order to measure launch times
* and stick around late in order to catch crashes. We intentionally don't expose this because
* application objects can have odd lifecycles, given that there is no clear onDestroy moment in
* many cases.
*/
private var feedbackScope: CoroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
// Setting a global reference to the application object is icky; we should try to refactor
// this away if possible. Doing this in attachBaseContext instead of onCreate()
// to avoid any lifecycle issues, as certain components can run before Application.onCreate()
// (like ContentProvider initialization), but attachBaseContext will still run before that.
instance = this
}
override fun onCreate() {
super.onCreate()
// Register this before the uncaught exception handler, because we want to make sure the
// exception handler also doesn't do disk IO. Since StrictMode only applies for debug builds,
// we'll also see the crashes during development right away and won't miss them if they aren't
// reported by the crash reporting.
if (BuildConfig.DEBUG) {
StrictModeHelper.enableStrictMode()
cash.z.ecc.android.sdk.internal.Twig.enabled(true)
cash.z.ecc.android.util.Twig.enabled(true)
}
// Setup handler for uncaught exceptions.
Thread.getDefaultUncaughtExceptionHandler()?.let {
Thread.setDefaultUncaughtExceptionHandler(ExceptionReporter(it))
}
creationTime = System.currentTimeMillis()
defaultNetwork = ZcashNetwork.from(resources.getInteger(R.integer.zcash_network_id))
feedbackScope.launch {
coordinator.feedback.start()
}
}
override fun getCameraXConfig(): CameraXConfig {
return Camera2Config.defaultConfig()
}
companion object {
lateinit var instance: ZcashWalletApp
}
/**
* @param feedbackCoordinator inject a provider so that if a crash happens before configuration
* is complete, we can lazily initialize all the feedback objects at this moment so that we
* don't have to add any time to startup.
*/
inner class ExceptionReporter(private val ogHandler: Thread.UncaughtExceptionHandler) :
Thread.UncaughtExceptionHandler {
override fun uncaughtException(t: Thread?, e: Throwable?) {
twig("Uncaught Exception: $e caused by: ${e?.cause}")
// Things can get pretty crazy during a fatal exception
// so be cautious here to avoid freezing the app
tryWithWarning("Unable to report fatal crash") {
// note: these are the only reported crashes that set isFatal=true
coordinator.feedback.report(e, true)
}
tryWithWarning("Unable to flush the feedback coordinator") {
coordinator.flush()
}
try {
// can do this if necessary but first verify that we need it
runBlocking {
coordinator.await()
coordinator.feedback.stop()
}
} catch (t: Throwable) {
twig("WARNING: failed to wait for the feedback observers to complete.")
} finally {
// it's important that this always runs so we use the finally clause here
// rather than another tryWithWarning block
ogHandler.uncaughtException(t, e)
Thread.sleep(2000L)
}
}
}
}

44
app/src/main/java/cash/z/ecc/android/di/DependenciesHolder.kt

@ -0,0 +1,44 @@
package cash.z.ecc.android.di
import android.content.ClipboardManager
import android.content.Context
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.ext.Const
import cash.z.ecc.android.feedback.*
import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.ui.util.DebugFileTwig
import cash.z.ecc.android.util.SilentTwig
import cash.z.ecc.android.util.Twig
import cash.z.ecc.kotlin.mnemonic.Mnemonics
object DependenciesHolder {
fun provideAppContext(): Context = ZcashWalletApp.instance
val initializerComponent by lazy { InitializerComponent() }
val synchronizer by lazy { Synchronizer.newBlocking(initializerComponent.initializer) }
val clipboardManager by lazy { provideAppContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager }
val lockBox by lazy { LockBox(provideAppContext()) }
val prefs by lazy { LockBox(provideAppContext()) }
val feedback by lazy { Feedback() }
val feedbackCoordinator by lazy {
lockBox.getBoolean(Const.Pref.FEEDBACK_ENABLED).let { isEnabled ->
// observe nothing unless feedback is enabled
Twig.plant(if (isEnabled) DebugFileTwig() else SilentTwig())
FeedbackCoordinator(feedback)
}
}
val feedbackFile by lazy { FeedbackFile() }
val feedbackConsole by lazy { FeedbackConsole() }
val mnemonics by lazy { Mnemonics() }
}

14
app/src/main/java/cash/z/ecc/android/di/InitializerComponent.kt

@ -0,0 +1,14 @@
package cash.z.ecc.android.di
import cash.z.ecc.android.sdk.Initializer
class InitializerComponent {
lateinit var initializer: Initializer
private set
fun createInitializer(config: Initializer.Config) {
initializer = Initializer.newBlocking(DependenciesHolder.provideAppContext(), config)
}
}

53
app/src/main/java/cash/z/ecc/android/ext/Const.kt

@ -0,0 +1,53 @@
package cash.z.ecc.android.ext
import cash.z.ecc.android.BuildConfig
object Const {
/**
* Named objects for Dependency Injection.
*/
object Name {
/** application data other than cryptographic keys */
const val APP_PREFS = "const.name.app_prefs"
const val BEFORE_SYNCHRONIZER = "const.name.before_synchronizer"
const val SYNCHRONIZER = "const.name.synchronizer"
}
/**
* App preference key names.
*/
object Pref {
const val FIRST_USE_VIEW_TX = "const.pref.first_use_view_tx"
const val EASTER_EGG_TRIGGERED_SHIELDING = "const.pref.easter_egg_shielding"
const val FEEDBACK_ENABLED = "const.pref.feedback_enabled"
const val SERVER_HOST = "const.pref.server_host"
const val SERVER_PORT = "const.pref.server_port"
}
/**
* Constants used for wallet backup.
*/
object Backup {
const val SEED = "cash.z.ecc.android.SEED"
const val SEED_PHRASE = "cash.z.ecc.android.SEED_PHRASE"
const val HAS_SEED = "cash.z.ecc.android.HAS_SEED"
const val HAS_SEED_PHRASE = "cash.z.ecc.android.HAS_SEED_PHRASE"
const val HAS_BACKUP = "cash.z.ecc.android.HAS_BACKUP"
// Config
const val VIEWING_KEY = "cash.z.ecc.android.VIEWING_KEY"
const val PUBLIC_KEY = "cash.z.ecc.android.PUBLIC_KEY"
const val BIRTHDAY_HEIGHT = "cash.z.ecc.android.BIRTHDAY_HEIGHT"
}
/**
* Default values to use application-wide. Ideally, this set of values should remain very short.
*/
object Default {
object Server {
// If you've forked the ECC repo, change this to your hosted lightwalletd instance
const val HOST = BuildConfig.DEFAULT_SERVER_URL
const val PORT = 9067
}
}
}

76
app/src/main/java/cash/z/ecc/android/ext/CurrencyFormatter.kt

@ -0,0 +1,76 @@
package cash.z.ecc.android.ext
import cash.z.ecc.android.ext.ConversionsUniform.FULL_FORMATTER
import cash.z.ecc.android.ext.ConversionsUniform.LONG_SCALE
import cash.z.ecc.android.ext.ConversionsUniform.SHORT_FORMATTER
import cash.z.ecc.android.sdk.ext.Conversions
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.model.Zatoshi
import java.math.BigDecimal
import java.math.MathContext
import java.math.RoundingMode
import java.text.DecimalFormat
import java.text.NumberFormat
import java.util.Locale
/**
* Do the necessary conversions in one place
*
* "1.234" -> to zatoshi
* (zecStringToZatoshi)
* String.toZatoshi()
*
* 123123 -> to "1.2132"
* (zatoshiToZecString)
* Long.toZecString()
*
*/
object ConversionsUniform {
val ONE_ZEC_IN_ZATOSHI = BigDecimal(Zatoshi.ZATOSHI_PER_ZEC, MathContext.DECIMAL128)
val LONG_SCALE = 8
val SHORT_SCALE = 4
val SHORT_FORMATTER = from(SHORT_SCALE, SHORT_SCALE)
val FULL_FORMATTER = from(LONG_SCALE)
val roundingMode = RoundingMode.HALF_EVEN
private fun from(maxDecimals: Int = 8, minDecimals: Int = 0) = (NumberFormat.getNumberInstance(Locale("en", "USA")) as DecimalFormat).apply {
// applyPattern("###.##")
isParseBigDecimal = true
roundingMode = roundingMode
maximumFractionDigits = maxDecimals
minimumFractionDigits = minDecimals
minimumIntegerDigits = 1
}
}
object WalletZecFormmatter {
fun toZatoshi(zecString: String): Long? {
return toBigDecimal(zecString)?.multiply(Conversions.ONE_ZEC_IN_ZATOSHI, MathContext.DECIMAL128)?.toLong()
}
fun toZecStringShort(amount: Zatoshi?): String {
return SHORT_FORMATTER.format((amount ?: Zatoshi(0)).toZec())
}
fun toZecStringFull(amount: Zatoshi?): String {
return formatFull((amount ?: Zatoshi(0)).toZec())
}
fun formatFull(zec: BigDecimal): String {
return FULL_FORMATTER.format(zec)
}
fun toBigDecimal(zecString: String?): BigDecimal? {
if (zecString.isNullOrEmpty()) return BigDecimal.ZERO
return try {
// ignore commas and whitespace
var sanitizedInput = zecString.filter { it.isDigit() or (it == '.') }
BigDecimal.ZERO.max(FULL_FORMATTER.parse(sanitizedInput) as BigDecimal)
} catch (t: Throwable) {
return null
}
}
// convert a zatoshi value to ZEC as a BigDecimal
private fun Zatoshi?.toZec(): BigDecimal =
BigDecimal(this?.value ?: 0L, MathContext.DECIMAL128)
.divide(ConversionsUniform.ONE_ZEC_IN_ZATOSHI)
.setScale(LONG_SCALE, ConversionsUniform.roundingMode)
}

192
app/src/main/java/cash/z/ecc/android/ext/Dialogs.kt

@ -0,0 +1,192 @@
package cash.z.ecc.android.ext
import android.app.ActivityManager
import android.app.Dialog
import android.content.Context
import android.text.Html
import androidx.annotation.StringRes
import androidx.core.content.getSystemService
import cash.z.ecc.android.R
import com.google.android.material.dialog.MaterialAlertDialogBuilder
fun Context.showClearDataConfirmation(onDismiss: () -> Unit = {}, onCancel: () -> Unit = {}): Dialog {
return MaterialAlertDialogBuilder(this)
.setTitle(R.string.dialog_nuke_wallet_title)
.setMessage(R.string.dialog_nuke_wallet_message)
.setCancelable(false)
.setPositiveButton(R.string.dialog_nuke_wallet_button_positive) { dialog, _ ->
dialog.dismiss()
onDismiss()
onCancel()
}
.setNegativeButton(R.string.dialog_nuke_wallet_button_negative) { dialog, _ ->
dialog.dismiss()
onDismiss()
getSystemService<ActivityManager>()?.clearApplicationUserData()
}
.show()
}
fun Context.showUninitializedError(error: Throwable? = null, onDismiss: () -> Unit = {}): Dialog {
return MaterialAlertDialogBuilder(this)
.setTitle(R.string.dialog_error_uninitialized_title)
.setMessage(R.string.dialog_error_uninitialized_message)
.setCancelable(false)
.setPositiveButton(getString(R.string.dialog_error_uninitialized_button_positive)) { dialog, _ ->
dialog.dismiss()
onDismiss()
if (error != null) throw error
}
.setNegativeButton(getString(R.string.dialog_error_uninitialized_button_negative)) { dialog, _ ->
showClearDataConfirmation(
onDismiss,
onCancel = {
// do not let the user back into the app because we cannot recover from this case
showUninitializedError(error, onDismiss)
}
)
}
.show()
}
fun Context.showInvalidSeedPhraseError(error: Throwable? = null, onDismiss: () -> Unit = {}): Dialog {
return MaterialAlertDialogBuilder(this)
.setTitle(R.string.dialog_error_invalid_seed_phrase_title)
.setMessage(getString(R.string.dialog_error_invalid_seed_phrase_message, error?.message ?: ""))
.setCancelable(false)
.setPositiveButton(getString(R.string.dialog_error_invalid_seed_phrase_button_positive)) { dialog, _ ->
dialog.dismiss()
onDismiss()
}
.show()
}
fun Context.showScanFailure(error: Throwable?, onCancel: () -> Unit = {}, onDismiss: () -> Unit = {}): Dialog {
val message = if (error == null) {
"Unknown error"
} else {
"${error.message}${if (error.cause != null) "\n\nCaused by: ${error.cause}" else ""}"
}
return MaterialAlertDialogBuilder(this)
.setTitle(R.string.dialog_error_scan_failure_title)
.setMessage(message)
.setCancelable(true)
.setPositiveButton(R.string.dialog_error_scan_failure_button_positive) { d, _ ->
d.dismiss()
onDismiss()
}
.setNegativeButton(R.string.dialog_error_scan_failure_button_negative) { d, _ ->
d.dismiss()
onCancel()
onDismiss()
}
.show()
}
fun Context.showCriticalMessage(@StringRes titleResId: Int, @StringRes messageResId: Int, onDismiss: () -> Unit = {}): Dialog {
return showCriticalMessage(titleResId.toAppString(), messageResId.toAppString(), onDismiss)
}
fun Context.showCriticalMessage(title: String, message: String, onDismiss: () -> Unit = {}): Dialog {
return MaterialAlertDialogBuilder(this)
.setTitle(title)
.setMessage(message)
.setCancelable(false)
.setPositiveButton(android.R.string.ok) { d, _ ->
d.dismiss()
onDismiss()
}
.show()
}
fun Context.showCriticalProcessorError(error: Throwable?, onRetry: () -> Unit = {}): Dialog {
return MaterialAlertDialogBuilder(this)
.setTitle(R.string.dialog_error_processor_critical_title)
.setMessage(error?.message ?: getString(R.string.dialog_error_processor_critical_message))
.setCancelable(false)
.setPositiveButton(R.string.dialog_error_processor_critical_button_positive) { d, _ ->
d.dismiss()
onRetry()
}
.setNegativeButton(R.string.dialog_error_processor_critical_button_negative) { dialog, _ ->
dialog.dismiss()
throw error ?: RuntimeException("Critical error while processing blocks and the user chose to exit.")
}
.show()
}
fun Context.showUpdateServerCriticalError(userFacingMessage: String, onConfirm: () -> Unit = {}): Dialog {
return MaterialAlertDialogBuilder(this)
.setTitle(R.string.dialog_error_change_server_title)
.setMessage(userFacingMessage)
.setCancelable(false)
.setPositiveButton(R.string.dialog_error_change_server_button_positive) { d, _ ->
d.dismiss()
onConfirm()
}
.show()
}
fun Context.showUpdateServerDialog(positiveResId: Int = R.string.dialog_modify_server_button_positive, onCancel: () -> Unit = {}, onUpdate: () -> Unit = {}): Dialog {
return MaterialAlertDialogBuilder(this)
.setTitle(R.string.dialog_modify_server_title)
.setMessage(R.string.dialog_modify_server_message)
.setCancelable(false)
.setPositiveButton(positiveResId) { dialog, _ ->
dialog.dismiss()
onUpdate()
}
.setNegativeButton(R.string.dialog_modify_server_button_negative) { dialog, _ ->
dialog.dismiss()
onCancel()
}
.show()
}
fun Context.showRescanWalletDialog(quickDistance: String, quickEstimate: String, fullDistance: String, fullEstimate: String, onWipe: () -> Unit = {}, onFullRescan: () -> Unit = {}, onQuickRescan: () -> Unit = {}): Dialog {
return MaterialAlertDialogBuilder(this)
.setTitle(R.string.dialog_rescan_wallet_title)
.setMessage(Html.fromHtml(getString(R.string.dialog_rescan_wallet_message, quickDistance, quickEstimate, fullDistance, fullEstimate)))
.setCancelable(true)
.setPositiveButton(R.string.dialog_rescan_wallet_button_positive) { dialog, _ ->
dialog.dismiss()
onQuickRescan()
}
.setNeutralButton(R.string.dialog_rescan_wallet_button_neutral) { dialog, _ ->
dialog.dismiss()
onWipe()
}
.setNegativeButton(R.string.dialog_rescan_wallet_button_negative) { dialog, _ ->
dialog.dismiss()
onFullRescan()
}
.show()
}
fun Context.showConfirmation(title: String, message: String, positiveButton: String, negativeButton: String = "Cancel", onPositive: () -> Unit = {}): Dialog {
return MaterialAlertDialogBuilder(this)
.setTitle(title)
.setMessage(message)
.setPositiveButton(positiveButton) { dialog, _ ->
dialog.dismiss()
onPositive()
}
.setNegativeButton("Cancel") { dialog, _ ->
dialog.dismiss()
}
.show()
}
/**
* Error to show when the Rust libraries did not properly link. This problem can happen pretty often
* during development when a build of the SDK failed to compile and resulted in an AAR file with no
* shared libraries (*.so files) inside. In theory, this should never be seen by an end user but if
* it does occur it is better to show a clean message explaining the situation. Nothing can be done
* other than rebuilding the SDK or switching to a functional version.
* As a developer, this error probably means that you need to comment out mavenLocal() as a repo.
*/
fun Context.showSharedLibraryCriticalError(e: Throwable): Dialog = showCriticalMessage(
titleResId = R.string.dialog_error_critical_link_title,
messageResId = R.string.dialog_error_critical_link_message,
onDismiss = { throw e }
)

83
app/src/main/java/cash/z/ecc/android/ext/EditText.kt

@ -0,0 +1,83 @@
package cash.z.ecc.android.ext
import android.text.Editable
import android.text.TextWatcher
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
import android.widget.EditText
import android.widget.TextView
import cash.z.ecc.android.sdk.ext.convertZecToZatoshi
import cash.z.ecc.android.sdk.ext.safelyConvertToBigDecimal
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.util.twig
fun EditText.onEditorActionDone(block: (EditText) -> Unit) {
this.setOnEditorActionListener { _, actionId, _ ->
if (actionId == IME_ACTION_DONE) {
block(this)
true
} else {
false
}
}
}
inline fun EditText.limitDecimalPlaces(max: Int) {
val editText = this
addTextChangedListener(object : TextWatcher {
var previousValue = ""
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
// Cache the previous value
previousValue = text.toString()
}
override fun afterTextChanged(s: Editable?) {
var textStr = text.toString()
if (textStr.isNotEmpty()) {
val oldText = text.toString()
val number = textStr.safelyConvertToBigDecimal()
if (number != null && number.scale() > 8) {
// Prevent the user from adding a new decimal place somewhere in the middle if we're already at the limit
if (editText.selectionStart == editText.selectionEnd && editText.selectionStart != textStr.length) {
textStr = previousValue
} else {
textStr = WalletZecFormmatter.formatFull(number)
}
}
// Trim leading zeroes
textStr = textStr.trimStart('0')
// Append a zero if this results in an empty string or if the first symbol is not a digit
if (textStr.isEmpty() || !textStr.first().isDigit()) {
textStr = "0$textStr"
}
// Restore the cursor position
if (oldText != textStr) {
val cursorPosition = editText.selectionEnd
editText.setText(textStr)
editText.setSelection(
(cursorPosition - (oldText.length - textStr.length)).coerceIn(
0,
editText.text.toString().length
)
)
}
}
}
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
}
})
}
fun TextView.convertZecToZatoshi(): Zatoshi? {
return try {
text.toString().safelyConvertToBigDecimal()?.convertZecToZatoshi()
} catch (t: Throwable) {
twig("Failed to convert text to Zatoshi: $text")
null
}
}

69
app/src/main/java/cash/z/ecc/android/ext/Extensions.kt

@ -0,0 +1,69 @@
package cash.z.ecc.android.ext
import android.content.Context
import android.os.Build
import androidx.fragment.app.Fragment
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.util.Bush
import cash.z.ecc.android.util.Twig
import cash.z.ecc.android.util.twig
import java.util.*
import kotlin.math.roundToInt
/**
* Distribute a string into evenly-sized chunks and then execute a function with each chunk.
*
* @param chunks the number of chunks to create
* @param block a function to be applied to each zero-indexed chunk.
*/
fun <T> String.distribute(chunks: Int, block: (Int, String) -> T) {
val charsPerChunk = length / chunks.toFloat()
val wholeCharsPerChunk = charsPerChunk.toInt()
val chunksWithExtra = ((charsPerChunk - wholeCharsPerChunk) * chunks).roundToInt()
repeat(chunks) { i ->
val part = if (i < chunksWithExtra) {
substring(i * (wholeCharsPerChunk + 1), (i + 1) * (wholeCharsPerChunk + 1))
} else {
substring(i * wholeCharsPerChunk + chunksWithExtra, (i + 1) * wholeCharsPerChunk + chunksWithExtra)
}
block(i, part)
}
}
inline val WalletBalance.pending: Zatoshi
get() = (this.total - this.available)
inline fun <R> tryWithWarning(message: String = "", block: () -> R): R? {
return try {
block()
} catch (error: Throwable) {
twig("WARNING: $message")
null
}
}
inline fun <E : Throwable, R> failWith(specificErrorType: E, block: () -> R): R {
return try {
block()
} catch (error: Throwable) {
throw specificErrorType
}
}
inline fun Fragment.locale(): Locale = context?.locale() ?: Locale.getDefault()
inline fun Context.locale(): Locale {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
resources.configuration.locales.get(0)
} else {
//noinspection deprecation
resources.configuration.locale
}
}
// TODO: add this to the SDK and if the trunk is a CompositeTwig, search through there before returning null
inline fun <reified T> Twig.find(): T? {
return if (Bush.trunk::class.java.isAssignableFrom(T::class.java)) Bush.trunk as T
else null
}

9
app/src/main/java/cash/z/ecc/android/ext/Fragment.kt

@ -0,0 +1,9 @@
package cash.z.ecc.android.ext
import androidx.fragment.app.Fragment
/**
* A safer alternative to [Fragment.requireContext], as it avoids leaking Fragment or Activity context
* when Application context is often sufficient.
*/
fun Fragment.requireApplicationContext() = requireContext().applicationContext

46
app/src/main/java/cash/z/ecc/android/ext/Int.kt

@ -0,0 +1,46 @@
package cash.z.ecc.android.ext
import android.content.res.Resources
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.IntegerRes
import androidx.annotation.StringRes
import androidx.core.content.res.ResourcesCompat
import cash.z.ecc.android.ZcashWalletApp
/**
* Grab a color out of the application resources, using the default theme
*/
@ColorInt
internal inline fun @receiver:ColorRes Int.toAppColor(): Int {
return ResourcesCompat.getColor(ZcashWalletApp.instance.resources, this, ZcashWalletApp.instance.theme)
}
/**
* Grab a string from the application resources
*/
internal inline fun @receiver:StringRes Int.toAppString(lowercase: Boolean = false): String {
return ZcashWalletApp.instance.getString(this).let {
if (lowercase) it.toLowerCase() else it
}
}
/**
* Grab a formatted string from the application resources
*/
internal inline fun @receiver:StringRes Int.toAppStringFormatted(vararg formatArgs: Any): String {
return ZcashWalletApp.instance.getString(this, *formatArgs)
}
/**
* Grab an integer from the application resources
*/
internal inline fun @receiver:IntegerRes Int.toAppInt(): Int {
return ZcashWalletApp.instance.resources.getInteger(this)
}
fun Float.toPx() = this * Resources.getSystem().displayMetrics.density
fun Int.toPx() = (this * Resources.getSystem().displayMetrics.density + 0.5f).toInt()
fun Int.toDp() = (this / Resources.getSystem().displayMetrics.density + 0.5f).toInt()

14
app/src/main/java/cash/z/ecc/android/ext/LifeCycleOwner.kt

@ -0,0 +1,14 @@
package cash.z.ecc.android.ext
import android.view.View
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
fun <T : View> LifecycleOwner.onClick(view: T, throttle: Long = 250L, block: (T) -> Unit) {
view.clicks().debounce(throttle).onEach {
block(view)
}.launchIn(this.lifecycleScope)
}

20
app/src/main/java/cash/z/ecc/android/ext/Spannable.kt

@ -0,0 +1,20 @@
package cash.z.ecc.android.ext
import android.text.Spanned
import android.text.style.ForegroundColorSpan
import androidx.annotation.ColorRes
import androidx.core.text.toSpannable
fun CharSequence.toColoredSpan(@ColorRes colorResId: Int, coloredPortion: String): CharSequence {
return toSpannable().apply {
val start = this@toColoredSpan.indexOf(coloredPortion)
setSpan(ForegroundColorSpan(colorResId.toAppColor()), start, start + coloredPortion.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
fun CharSequence.toSplitColorSpan(@ColorRes startColorResId: Int, @ColorRes endColorResId: Int, startColorLength: Int): CharSequence {
return toSpannable().apply {
setSpan(ForegroundColorSpan(startColorResId.toAppColor()), 0, startColorLength - 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
setSpan(ForegroundColorSpan(endColorResId.toAppColor()), startColorLength, this@toSplitColorSpan.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}

81
app/src/main/java/cash/z/ecc/android/ext/View.kt

@ -0,0 +1,81 @@
package cash.z.ecc.android.ext
import android.view.View
import android.view.View.GONE
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import cash.z.ecc.android.ui.MainActivity
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.channelFlow
fun View.gone() {
visibility = GONE
}
fun View.invisible() {
visibility = INVISIBLE
}
fun View.visible() {
visibility = VISIBLE
}
// NOTE: avoid `visibleIf` function because the false case is ambiguous: would it be gone or invisible?
fun View.goneIf(isGone: Boolean) {
visibility = if (isGone) GONE else VISIBLE
}
fun View.invisibleIf(isInvisible: Boolean) {
visibility = if (isInvisible) INVISIBLE else VISIBLE
}
fun View.disabledIf(isDisabled: Boolean) {
isEnabled = !isDisabled
}
fun View.transparentIf(isTransparent: Boolean) {
alpha = if (isTransparent) 0.0f else 1.0f
}
fun View.onClickNavTo(navResId: Int, block: (() -> Any) = {}) {
setOnClickListener {
block()
(context as? MainActivity)?.safeNavigate(navResId)
?: throw IllegalStateException(
"Cannot navigate from this activity. " +
"Expected MainActivity but found ${context.javaClass.simpleName}"
)
}
}
fun View.onClickNavUp(block: (() -> Any) = {}) {
setOnClickListener {
block()
(context as? MainActivity)?.navController?.navigateUp()
?: throw IllegalStateException(
"Cannot navigate from this activity. " +
"Expected MainActivity but found ${context.javaClass.simpleName}"
)
}
}
fun View.onClickNavBack(block: (() -> Any) = {}) {
setOnClickListener {
block()
(context as? MainActivity)?.navController?.popBackStack()
?: throw IllegalStateException(
"Cannot navigate from this activity. " +
"Expected MainActivity but found ${context.javaClass.simpleName}"
)
}
}
fun View.clicks() = channelFlow<View> {
setOnClickListener {
trySend(this@clicks)
}
awaitClose {
setOnClickListener(null)
}
}

22
app/src/main/java/cash/z/ecc/android/feedback/FeedbackConsole.kt

@ -0,0 +1,22 @@
package cash.z.ecc.android.feedback
import android.util.Log
class FeedbackConsole : FeedbackCoordinator.FeedbackObserver {
override fun onMetric(metric: Feedback.Metric) {
log(metric.toString())
}
override fun onAction(action: Feedback.Action) {
log(action.toString())
}
override fun flush() {
// TODO: flush logs (once we have the real logging in place)
}
private fun log(message: String) {
Log.d("@TWIG", message)
}
}

38
app/src/main/java/cash/z/ecc/android/feedback/FeedbackFile.kt

@ -0,0 +1,38 @@
package cash.z.ecc.android.feedback
import cash.z.ecc.android.ZcashWalletApp
import okio.appendingSink
import okio.buffer
import java.io.File
import java.text.SimpleDateFormat
class FeedbackFile(fileName: String = "user_log.txt") :
FeedbackCoordinator.FeedbackObserver {
val file = File("${ZcashWalletApp.instance.filesDir}/logs", fileName)
private val format = SimpleDateFormat("MM-dd HH:mm:ss.SSS")
override fun initialize(): FeedbackCoordinator.FeedbackObserver = apply {
file.parentFile?.apply {
if (!exists()) mkdirs()
}
}
override fun onMetric(metric: Feedback.Metric) {
appendToFile(metric.toString())
}
override fun onAction(action: Feedback.Action) {
appendToFile(action.toString())
}
override fun flush() {
// TODO: be more sophisticated about how we open/close the file. And then flush it here.
}
private fun appendToFile(message: String) {
file.appendingSink().buffer().use {
it.writeUtf8("${format.format(System.currentTimeMillis())}|\t$message\n")
}
}
}

255
app/src/main/java/cash/z/ecc/android/feedback/Report.kt

@ -0,0 +1,255 @@
package cash.z.ecc.android.feedback
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.sdk.model.BlockHeight
object Report {
object Funnel {
sealed class Send(stepName: String, step: Int, vararg properties: Pair<String, Any>) : Feedback.Funnel("send", stepName, step, *properties) {
object AddressPageComplete : Send("addresspagecomplete", 10)
object MemoPageComplete : Send("memopagecomplete", 20)
object ConfirmPageComplete : Send("confirmpagecomplete", 30)
// Beginning of send
object SendSelected : Send("sendselected", 50)
object SpendingKeyFound : Send("keyfound", 60)
object Creating : Send("creating", 70)
object Cancelled : Send("cancelled", 72)
class Created(id: Long) : Send("created", 80, "id" to id)
object Submitted : Send("submitted", 90)
class Mined(minedHeight: Int) : Send("mined", 100, "minedHeight" to minedHeight)
// Errors
abstract class Error(stepName: String, step: Int, val errorCode: Int?, val errorMessage: String?, vararg properties: Pair<String, Any>) : Send("error.$stepName", step, "isError" to true, *properties)
object ErrorNotFound : Error("notfound", 51, null, "Key not found")
class ErrorEncoding(errorCode: Int? = null, errorMessage: String? = null) : Error(
"encode", 71, errorCode, errorMessage,
"errorCode" to (errorCode ?: -1),
"errorMessage" to (errorMessage ?: "None")
)
class ErrorSubmitting(errorCode: Int? = null, errorMessage: String? = null) : Error(
"submit", 81, errorCode, errorMessage,
"errorCode" to (errorCode ?: -1),
"errorMessage" to (errorMessage ?: "None")
)
}
sealed class Restore(stepName: String, step: Int, vararg properties: Pair<String, Any>) : Feedback.Funnel("restore", stepName, step, *properties) {
object Initiated : Restore("initiated", 0)
object SeedWordsStarted : Restore("wordsstarted", 10)
class SeedWordCount(wordCount: Int) : Restore("wordsmodified", 15, "seedWordCount" to wordCount)
object SeedWordsCompleted : Restore("wordscompleted", 20)
object Stay : Restore("stay", 21)
object Exit : Restore("stay", 22)
object Done : Restore("doneselected", 30)
object ImportStarted : Restore("importstarted", 40)
object ImportCompleted : Restore("importcompleted", 50)
object Success : Restore("success", 100)
}
sealed class UserFeedback(stepName: String, step: Int, vararg properties: Pair<String, Any>) : Feedback.Funnel("feedback", stepName, step, *properties) {
object Started : UserFeedback("started", 0)
object Cancelled : UserFeedback("cancelled", 1)
class Submitted(rating: Int, question1: String, question2: String, question3: String, isSolicited: Boolean) : UserFeedback("submitted", 100, "rating" to rating, "question1" to question1, "question2" to question2, "question3" to question3, "isSolicited" to isSolicited)
}
}
object Error {
object NonFatal {
class Reorg(errorBlockHeight: BlockHeight, rewindBlockHeight: BlockHeight) : Feedback.AppError(
"reorg",
"Chain error detected at height $errorBlockHeight, rewinding to $rewindBlockHeight",
false,
"errorHeight" to errorBlockHeight,
"rewindHeight" to rewindBlockHeight
) {
val errorHeight: Int by propertyMap
val rewindHeight: Int by propertyMap
}
class TxUpdateFailed(t: Throwable) : Feedback.AppError("txupdate", t, false)
abstract class TxError(action: String, val errorCode: Int?, val errorMessage: String?) : Feedback.AppError(
"tx.$action",
"Failed to $action transaction due to $errorMessage",
false,
"errorCode" to (errorCode ?: 1)
)
class TxEncodeError(errorCode: Int?, errorMessage: String?) : TxError("encode", errorCode, errorMessage)
class TxSubmitError(errorCode: Int?, errorMessage: String?) : TxError("submit", errorCode, errorMessage)
}
}
sealed class Performance(name: String, vararg properties: Pair<String, Any>) : Feedback.MappedAction(
"metricName" to name,
"isPerformanceMetric" to true,
*properties
) {
override val key = "performance.$name"
override fun toString() = "$key: ${toMap().let { if (it.size > 1) "${it.entries}" else "" }}"
class ScanRate(network: String, cumulativeItems: Int, cumulativeTime: Long, cumulativeIps: Float) : Performance("scan.bps", "network" to network, "totalBlocks" to cumulativeItems, "totalTime" to cumulativeTime, "blocksPerSecond" to cumulativeIps)
}
// placeholder for things that we want to monitor
sealed class Issue(name: String, vararg properties: Pair<String, Any>) : Feedback.MappedAction(
"issueName" to name,
"isIssue" to true,
*properties
) {
override val key = "issue.$name"
override fun toString() = "occurrence of ${key.replace('.', ' ')}${toMap().let { if (it.size > 1) " with ${it.entries}" else "" }}"
// Issues with sending worth monitoring
object SelfSend : Issue("self.send")
object TinyAmount : Issue("tiny.amount")
object MicroAmount : Issue("micro.amount")
object MinimumAmount : Issue("minimum.amount")
class TruncatedMemo(memoSize: Int) : Issue("truncated.memo", "memoSize" to memoSize)
class LargeMemo(memoSize: Int) : Issue("large.memo", "memoSize" to memoSize)
class MissingViewkey(recovered: Boolean, needle: String, haystack: String, hasKey: Boolean) : Issue(
"missing.viewkey", "wasAbleToRecover" to recovered, "needle" to needle, "haystack" to haystack, "hasKey" to hasKey
)
}
enum class Screen(val id: String? = null) : Feedback.Action {
BACKUP,
HOME,
HISTORY("wallet.history"),
TRANSACTION("wallet.transaction"),
LANDING,
PROFILE,
AWESOME,
FEEDBACK,
RECEIVE,
RESTORE,
SCAN,
AUTO_SHIELD_FINAL("autoshield.final"),
AUTO_SHIELD_AVAILABLE("autoshield.available"),
AUTO_SHIELD_INFORMATION("autoshield.information"),
SEND_ADDRESS("send.address"),
SEND_CONFIRM("send.confirm"),
SEND_FINAL("send.final"),
SEND_MEMO("send.memo");
override val key = "screen.${id ?: name.toLowerCase()}"
override fun toString() = "viewed the ${key.substring(7).replace('.', ' ')} screen"
}
enum class Tap(val id: String) : Feedback.Action {
BACKUP_DONE("backup.done"),
BACKUP_VERIFY("backup.verify"),
DEVELOPER_WALLET_PROMPT("landing.devwallet.prompt"),
DEVELOPER_WALLET_IMPORT("landing.devwallet.import"),
DEVELOPER_WALLET_CANCEL("landing.devwallet.cancel"),
LANDING_RESTORE("landing.restore"),
LANDING_NEW("landing.new"),
LANDING_BACKUP("landing.backup"),
LANDING_BACKUP_SKIPPED_1("landing.backup.skip.1"),
LANDING_BACKUP_SKIPPED_2("landing.backup.skip.2"),
LANDING_BACKUP_SKIPPED_3("landing.backup.skip.3"),
HOME_PROFILE("home.profile"),
HOME_HISTORY("home.history"),
HOME_RECEIVE("home.receive"),
HOME_BALANCE_DETAIL("home.balance.detail"),
TAB_LAYOUT("tab.layout"),
HOME_SCAN("home.scan"),
HOME_SEND("home.send"),
HOME_FUND_NOW("home.fund.now"),
HOME_CLEAR_AMOUNT("home.clear.amount"),
HISTORY_BACK("history.back"),
TRANSACTION_BACK("transaction.back"),
PROFILE_CLOSE("profile.close"),
AWESOME_OPEN("profile.awesome"),
AWESOME_CLOSE("awesome.close"),
AWESOME_SHIELD("awesome.shield"),
PROFILE_BACKUP("profile.backup"),
PROFILE_RESCAN("profile.rescan"),
PROFILE_VIEW_USER_LOGS("profile.view.user.logs"),
PROFILE_VIEW_DEV_LOGS("profile.view.dev.logs"),
PROFILE_SEND_FEEDBACK("profile.send.feedback"),
FEEDBACK_CANCEL("feedback.cancel"),
FEEDBACK_SUBMIT("feedback.submit"),
RECEIVE_BACK("receive.back"),
RESTORE_DONE("restore.done"),
RESTORE_CLEAR("restore.clear"),
RESTORE_SUCCESS("restore.success"),
RESTORE_BACK("restore.back"),
SCAN_BACK("scan.back"),
AUTO_SHIELD_FINAL_CLOSE("autoshield.final.close"),
AUTO_SHIELD_FINAL_DONE("autoshield.final.done"),
SEND_ADDRESS_MAX("send.address.max"),
SEND_ADDRESS_NEXT("send.address.next"),
SEND_ADDRESS_PASTE("send.address.paste"),
SEND_ADDRESS_REUSE("send.address.reuse"),
SEND_ADDRESS_BACK("send.address.back"),
SEND_ADDRESS_DONE_ADDRESS("send.address.done.address"),
SEND_ADDRESS_DONE_AMOUNT("send.address.done.amount"),
SEND_ADDRESS_SCAN("send.address.scan"),
SEND_CONFIRM_BACK("send.confirm.back"),
SEND_CONFIRM_NEXT("send.confirm.next"),
SEND_FINAL_EXIT("send.final.exit"),
SEND_FINAL_RETRY("send.final.retry"),
SEND_FINAL_CLOSE("send.final.close"),
SEND_MEMO_INCLUDE("send.memo.include"),
SEND_MEMO_EXCLUDE("send.memo.exclude"),
SEND_MEMO_NEXT("send.memo.next"),
SEND_MEMO_SKIP("send.memo.skip"),
SEND_MEMO_CLEAR("send.memo.clear"),
SEND_MEMO_BACK("send.memo.back"),
SEND_SUBMIT("send.submit"),
// General events
COPY_ADDRESS("copy.address"),
COPY_TRANSPARENT_ADDRESS("copy.address.transparent");
override val key = "tap.$id"
override fun toString() = "${key.replace('.', ' ')} button".replace("tap ", "tapped the ")
}
enum class NonUserAction(override val key: String, val description: String) : Feedback.Action {
FEEDBACK_STARTED("action.feedback.start", "feedback started"),
FEEDBACK_STOPPED("action.feedback.stop", "feedback stopped"),
SYNC_START("action.feedback.synchronizer.start", "sync started");
override fun toString(): String = description
}
enum class MetricType(override val key: String, val description: String) : Feedback.Action {
ENTROPY_CREATED("metric.entropy.created", "entropy created"),
SEED_CREATED("metric.seed.created", "seed created"),
SEED_IMPORTED("metric.seed.imported", "seed imported"),
SEED_PHRASE_CREATED("metric.seedphrase.created", "seed phrase created"),
SEED_PHRASE_LOADED("metric.seedphrase.loaded", "seed phrase loaded"),
WALLET_CREATED("metric.wallet.created", "wallet created"),
WALLET_IMPORTED("metric.wallet.imported", "wallet imported"),
ACCOUNT_CREATED("metric.account.created", "account created"),
// Transactions
TRANSACTION_INITIALIZED("metric.tx.initialized", "transaction initialized"),
TRANSACTION_CREATED("metric.tx.created", "transaction created successfully"),
TRANSACTION_SUBMITTED("metric.tx.submitted", "transaction submitted successfully"),
TRANSACTION_MINED("metric.tx.mined", "transaction mined")
}
}
/**
* Creates a metric with a start time of ZcashWalletApp.creationTime and an end time of when this
* instance was created. This can then be passed to [Feedback.report].
*/
class LaunchMetric private constructor(private val metric: Feedback.TimeMetric) :
Feedback.Metric by metric {
constructor() : this(
Feedback
.TimeMetric(
"metric.app.launch",
"app launched",
mutableListOf(ZcashWalletApp.instance.creationTime)
)
.markTime()
)
override fun toString(): String = metric.toString()
}
inline fun <T> Feedback.measure(type: Report.MetricType, block: () -> T): T =
this.measure(type.key, type.description, block)

6
app/src/main/java/cash/z/ecc/android/preference/PreferenceKeys.kt

@ -0,0 +1,6 @@
package cash.z.ecc.android.preference
internal object PreferenceKeys {
const val IS_AUTOSHIELDING_INFO_ACKNOWLEDGED = "is_autoshielding_info_acknowledged"
const val LAST_AUTOSHIELDING_PROMPT_EPOCH_MILLIS = "last_autoshielding_epoch_millis"
}

12
app/src/main/java/cash/z/ecc/android/preference/Preferences.kt

@ -0,0 +1,12 @@
package cash.z.ecc.android.preference
import cash.z.ecc.android.preference.model.BooleanDefaultValue
import cash.z.ecc.android.preference.model.LongDefaultValue
object Preferences {
val isAcknowledgedAutoshieldingInformationPrompt =
BooleanDefaultValue(PreferenceKeys.IS_AUTOSHIELDING_INFO_ACKNOWLEDGED, false)
val lastAutoshieldingEpochMillis =
LongDefaultValue(PreferenceKeys.LAST_AUTOSHIELDING_PROMPT_EPOCH_MILLIS, 0)
}

9
app/src/main/java/cash/z/ecc/android/preference/SharedPreferenceFactory.kt

@ -0,0 +1,9 @@
package cash.z.ecc.android.preference
import android.content.Context
object SharedPreferenceFactory {
private const val DEFAULT_SHARED_PREFERENCES = "cash.z.ecc.default"
fun getSharedPreferences(context: Context) = context.getSharedPreferences(DEFAULT_SHARED_PREFERENCES, Context.MODE_PRIVATE)
}

35
app/src/main/java/cash/z/ecc/android/preference/model/BooleanDefaultValue.kt

@ -0,0 +1,35 @@
package cash.z.ecc.android.preference.model
import android.content.Context
import android.content.SharedPreferences
import cash.z.ecc.android.preference.SharedPreferenceFactory
/**
* A default value represents a preference key, along with its default value. It does not, by itself,
* know how to read or write values from the preference repository.
*/
data class BooleanDefaultValue(override val key: String, internal val defaultValue: Boolean) :
DefaultValue<Boolean> {
init {
require(key.isNotEmpty())
}
}
fun BooleanDefaultValue.get(context: Context) = get(
SharedPreferenceFactory.getSharedPreferences(
context
)
)
internal fun BooleanDefaultValue.get(sharedPreferences: SharedPreferences) =
sharedPreferences.getBoolean(key, defaultValue)
fun BooleanDefaultValue.put(context: Context, newValue: Boolean) = put(
SharedPreferenceFactory.getSharedPreferences(
context
),
newValue
)
internal fun BooleanDefaultValue.put(sharedPreferences: SharedPreferences, newValue: Boolean) =
sharedPreferences.edit().putBoolean(key, newValue).apply()

23
app/src/main/java/cash/z/ecc/android/preference/model/DefaultValue.kt

@ -0,0 +1,23 @@
package cash.z.ecc.android.preference.model
/**
* A key and a default value for a key-value store of preferences.
*
* Use of this interface avoids duplication or accidental variation in default value, because key
* and default are defined together just once.
*
* Note that T is not fully generic and should be one of the supported types: Boolean, ... (other types to be added in the future)
*
* @see BooleanDefaultValue
*/
/*
* Although primitives would be nice, Objects don't increase memory usage much
* because of the autoboxing cache on the JVM. For example, Boolean's true/false values
* are cached.
*/
interface DefaultValue<T> {
// Note: the default value is not available through the public interface in order to prevent
// clients from accidentally using the default value instead of the stored value.
val key: String
}

33
app/src/main/java/cash/z/ecc/android/preference/model/LongDefaultValue.kt

@ -0,0 +1,33 @@
package cash.z.ecc.android.preference.model
import android.content.Context
import android.content.SharedPreferences
import cash.z.ecc.android.preference.SharedPreferenceFactory
/**
* A default value represents a preference key, along with its default value. It does not, by itself,
* know how to read or write values from the preference repository.
*/
data class LongDefaultValue(
override val key: String,
internal val defaultValue: Long
) : DefaultValue<Long> {
init {
require(key.isNotEmpty())
}
}
fun LongDefaultValue.get(context: Context) = get(
SharedPreferenceFactory.getSharedPreferences(context)
)
internal fun LongDefaultValue.get(sharedPreferences: SharedPreferences) =
sharedPreferences.getLong(key, defaultValue)
fun LongDefaultValue.put(context: Context, newValue: Long) = put(
SharedPreferenceFactory.getSharedPreferences(context),
newValue
)
internal fun LongDefaultValue.put(sharedPreferences: SharedPreferences, newValue: Long) =
sharedPreferences.edit().putLong(key, newValue).apply()

669
app/src/main/java/cash/z/ecc/android/ui/MainActivity.kt

@ -0,0 +1,669 @@
package cash.z.ecc.android.ui
import android.Manifest
import android.app.Dialog
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import android.media.MediaPlayer
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Vibrator
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
import android.widget.TextView
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.viewModels
import androidx.annotation.IdRes
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.BiometricManager.Authenticators.*
import androidx.biometric.BiometricPrompt
import androidx.biometric.BiometricPrompt.*
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import androidx.navigation.Navigator
import androidx.navigation.findNavController
import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.databinding.DialogFirstUseMessageBinding
import cash.z.ecc.android.di.DependenciesHolder
import cash.z.ecc.android.ext.*
import cash.z.ecc.android.feedback.Feedback
import cash.z.ecc.android.feedback.FeedbackCoordinator
import cash.z.ecc.android.feedback.LaunchMetric
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Error.NonFatal.Reorg
import cash.z.ecc.android.feedback.Report.NonUserAction.FEEDBACK_STOPPED
import cash.z.ecc.android.feedback.Report.NonUserAction.SYNC_START
import cash.z.ecc.android.feedback.Report.Tap.COPY_ADDRESS
import cash.z.ecc.android.feedback.Report.Tap.COPY_TRANSPARENT_ADDRESS
import cash.z.ecc.android.sdk.SdkSynchronizer
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException
import cash.z.ecc.android.sdk.ext.BatchMetrics
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.ext.toAbbreviatedAddress
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.ui.history.HistoryViewModel
import cash.z.ecc.android.ui.util.MemoUtil
import cash.z.ecc.android.util.twig
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity(R.layout.main_activity) {
val mainViewModel: MainViewModel by viewModels()
val feedback: Feedback = DependenciesHolder.feedback
val feedbackCoordinator: FeedbackCoordinator = DependenciesHolder.feedbackCoordinator
val clipboard: ClipboardManager = DependenciesHolder.clipboardManager
val historyViewModel: HistoryViewModel by viewModels()
private var syncStarted = false
private val mediaPlayer: MediaPlayer = MediaPlayer()
private var snackbar: Snackbar? = null
private var dialog: Dialog? = null
private var ignoreScanFailure: Boolean = false
var navController: NavController? = null
private val navInitListeners: MutableList<() -> Unit> = mutableListOf()
private val hasCameraPermission
get() = ContextCompat.checkSelfPermission(
this,
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
val latestHeight: BlockHeight?
get() = DependenciesHolder.synchronizer.latestHeight
override fun onCreate(savedInstanceState: Bundle?) {
lifecycleScope.launch {
feedback.start()
}
super.onCreate(savedInstanceState)
initNavigation()
initLoadScreen()
window.statusBarColor = Color.TRANSPARENT
window.navigationBarColor = Color.TRANSPARENT
window.setFlags(
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
)
setWindowFlag(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, false)
setWindowFlag(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, false)
}
override fun onResume() {
super.onResume()
// keep track of app launch metrics
// (how long does it take the app to open when it is not already in the foreground)
ZcashWalletApp.instance.let { app ->
if (!app.creationMeasured) {
app.creationMeasured = true
feedback.report(LaunchMetric())
}
}
}
override fun onDestroy() {
lifecycleScope.launch {
feedback.report(FEEDBACK_STOPPED)
feedback.stop()
}
super.onDestroy()
}
private fun setWindowFlag(bits: Int, on: Boolean) {
val win = window
val winParams = win.attributes
if (on) {
winParams.flags = winParams.flags or bits
} else {
winParams.flags = winParams.flags and bits.inv()
}
win.attributes = winParams
}
private fun initNavigation() {
navController = findNavController(R.id.nav_host_fragment)
navController!!.addOnDestinationChangedListener { _, _, _ ->
// hide the keyboard anytime we change destinations
getSystemService<InputMethodManager>()?.hideSoftInputFromWindow(
this@MainActivity.window.decorView.rootView.windowToken,
InputMethodManager.HIDE_NOT_ALWAYS
)
}
for (listener in navInitListeners) {
listener()
}
navInitListeners.clear()
}
private fun initLoadScreen() {
lifecycleScope.launchWhenResumed {
mainViewModel.loadingMessage.collect { message ->
onLoadingMessage(message)
}
}
}
private fun onLoadingMessage(message: String?) {
twig("Applying loading message: $message")
// TODO: replace with view binding
findViewById<View>(R.id.container_loading).goneIf(message == null)
findViewById<TextView>(R.id.text_message).text = message
}
fun popBackTo(@IdRes destination: Int, inclusive: Boolean = false) {
navController?.popBackStack(destination, inclusive)
}
fun safeNavigate(navDirections: NavDirections) =
safeNavigate(navDirections.actionId, navDirections.arguments, null)
fun safeNavigate(
@IdRes destination: Int,
args: Bundle? = null,
extras: Navigator.Extras? = null
) {
if (navController == null) {
navInitListeners.add {
try {
navController?.navigate(destination, args, null, extras)
} catch (t: Throwable) {
twig(
"WARNING: during callback, did not navigate to destination: R.id.${
resources.getResourceEntryName(
destination
)
} due to: $t"
)
}
}
} else {
try {
navController?.navigate(destination, args, null, extras)
} catch (t: Throwable) {
twig(
"WARNING: did not immediately navigate to destination: R.id.${
resources.getResourceEntryName(
destination
)
} due to: $t"
)
}
}
}
fun startSync(isRestart: Boolean = false) {
twig("MainActivity.startSync")
if (!syncStarted || isRestart) {
syncStarted = true
mainViewModel.setLoading(true)
feedback.report(SYNC_START)
DependenciesHolder.synchronizer.let { synchronizer ->
synchronizer.onProcessorErrorHandler = ::onProcessorError
synchronizer.onChainErrorHandler = ::onChainError
synchronizer.onCriticalErrorHandler = ::onCriticalError
(synchronizer as SdkSynchronizer).processor.onScanMetricCompleteListener =
::onScanMetricComplete
synchronizer.start(lifecycleScope)
mainViewModel.setSyncReady(true)
}
} else {
twig("Ignoring request to start sync because sync has already been started!")
}
mainViewModel.setLoading(false)
twig("MainActivity.startSync COMPLETE")
}
private fun onScanMetricComplete(batchMetrics: BatchMetrics, isComplete: Boolean) {
val reportingThreshold = 100
if (isComplete) {
if (batchMetrics.cumulativeItems > reportingThreshold) {
val network = DependenciesHolder.synchronizer.network.networkName
reportAction(
Report.Performance.ScanRate(
network,
batchMetrics.cumulativeItems.toInt(),
batchMetrics.cumulativeTime,
batchMetrics.cumulativeIps
)
)
}
}
}
private fun onCriticalError(error: Throwable?): Boolean {
val errorMessage = error?.message
?: error?.cause?.message
?: error?.toString()
?: "A critical error has occurred but no details were provided. Please report and consider submitting logs to help track this one down."
showCriticalMessage(
title = "Unrecoverable Error",
message = errorMessage,
) {
throw error ?: RuntimeException("A critical error occurred but it was null")
}
return false
}
fun reportScreen(screen: Report.Screen?) = reportAction(screen)
fun reportTap(tap: Report.Tap?) = reportAction(tap)
fun reportFunnel(step: Feedback.Funnel?) = reportAction(step)
private fun reportAction(action: Feedback.Action?) {
action?.let { feedback.report(it) }
}
fun setLoading(isLoading: Boolean, message: String? = null) {
mainViewModel.setLoading(isLoading, message)
}
fun authenticate(
description: String,
title: String = getString(R.string.biometric_prompt_title),
block: () -> Unit
) {
val callback = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
twig("Authentication success with type: ${if (result.authenticationType == AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL) "DEVICE_CREDENTIAL" else if (result.authenticationType == AUTHENTICATION_RESULT_TYPE_BIOMETRIC) "BIOMETRIC" else "UNKNOWN"} object: ${result.cryptoObject}")
block()
twig("Done authentication block")
// we probably only need to do this if the type is DEVICE_CREDENTIAL
// but it doesn't hurt to hide the keyboard every time
hideKeyboard()
}
override fun onAuthenticationFailed() {
twig("Authentication failed!!!!")
showMessage("Authentication failed :(")
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
twig("Authentication Error")
fun doNothing(message: String, interruptUser: Boolean = true) {
if (interruptUser) {
showSnackbar(message)
} else {
showMessage(message, true)
}
}
when (errorCode) {
ERROR_HW_NOT_PRESENT, ERROR_HW_UNAVAILABLE,
ERROR_NO_BIOMETRICS, ERROR_NO_DEVICE_CREDENTIAL -> {
twig("Warning: bypassing authentication because $errString [$errorCode]")
showMessage(
"Please enable screen lock on this device to add security here!",
true
)
block()
}
ERROR_LOCKOUT -> doNothing("Too many attempts. Try again in 30s.")
ERROR_LOCKOUT_PERMANENT -> doNothing("Whoa. Waaaay too many attempts!")
ERROR_CANCELED -> doNothing("I just can't right now. Please try again.")
ERROR_NEGATIVE_BUTTON -> doNothing("Authentication cancelled", false)
ERROR_USER_CANCELED -> doNothing("Cancelled", false)
ERROR_NO_SPACE -> doNothing("Not enough storage space!")
ERROR_TIMEOUT -> doNothing("Oops. It timed out.")
ERROR_UNABLE_TO_PROCESS -> doNothing(".")
ERROR_VENDOR -> doNothing("We got some weird error and you should report this.")
else -> {
twig("Warning: unrecognized authentication error $errorCode")
doNothing("Authentication failed with error code $errorCode")
}
}
}
}
BiometricPrompt(this, ContextCompat.getMainExecutor(this), callback).apply {
authenticate(
BiometricPrompt.PromptInfo.Builder()
.setTitle(title)
.setConfirmationRequired(false)
.setDescription(description)
.setAllowedAuthenticators(BIOMETRIC_STRONG or BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
.build()
)
}
}
fun playSound(fileName: String) {
mediaPlayer.apply {
if (isPlaying) stop()
try {
reset()
assets.openFd(fileName).let { afd ->
setDataSource(afd.fileDescriptor, afd.startOffset, afd.length)
}
prepare()
start()
} catch (t: Throwable) {
Log.e("SDK_ERROR", "ERROR: unable to play sound due to $t")
}
}
}
// TODO: spruce this up with API 26 stuff
fun vibrateSuccess() = vibrate(0, 200, 200, 100, 100, 800)
fun vibrate(initialDelay: Long, vararg durations: Long) {
val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
if (vibrator.hasVibrator()) {
vibrator.vibrate(longArrayOf(initialDelay, *durations), -1)
}
}
fun copyAddress(view: View? = null) {
reportTap(COPY_ADDRESS)
lifecycleScope.launch {
copyText(DependenciesHolder.synchronizer.getAddress(), "Address")
}
}
fun copyTransparentAddress(view: View? = null) {
reportTap(COPY_TRANSPARENT_ADDRESS)
lifecycleScope.launch {
copyText(DependenciesHolder.synchronizer.getTransparentAddress(), "T-Address")
}
}
fun copyText(textToCopy: String, label: String = "ECC Wallet Text") {
clipboard.setPrimaryClip(
ClipData.newPlainText(label, textToCopy)
)
showMessage("$label copied!")
vibrate(0, 50)
}
fun shareText(textToShare: String) {
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, textToShare)
}
val shareIntent = Intent.createChooser(sendIntent, null)
startActivity(shareIntent)
}
suspend fun isValidAddress(address: String): Boolean {
try {
return !DependenciesHolder.synchronizer.validateAddress(address).isNotValid
} catch (t: Throwable) {
}
return false
}
fun preventBackPress(fragment: Fragment) {
onFragmentBackPressed(fragment) {}
}
fun onFragmentBackPressed(fragment: Fragment, block: () -> Unit) {
onBackPressedDispatcher.addCallback(
fragment,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
block()
}
}
)
}
private fun showMessage(message: String, linger: Boolean = false) {
twig("toast: $message")
Toast.makeText(this, message, if (linger) Toast.LENGTH_LONG else Toast.LENGTH_SHORT).show()
}
fun showSnackbar(
message: String,
actionLabel: String = getString(android.R.string.ok),
action: () -> Unit = {}
): Snackbar {
return if (snackbar == null) {
val view = findViewById<View>(R.id.main_activity_container)
val snacks = Snackbar
.make(view, "$message", Snackbar.LENGTH_INDEFINITE)
.setAction(actionLabel) { action() }
val snackBarView = snacks.view as ViewGroup
val navigationBarHeight = resources.getDimensionPixelSize(
resources.getIdentifier(
"navigation_bar_height",
"dimen",
"android"
)
)
val params = snackBarView.getChildAt(0).layoutParams as ViewGroup.MarginLayoutParams
params.setMargins(
params.leftMargin,
params.topMargin,
params.rightMargin,
navigationBarHeight
)
snackBarView.getChildAt(0).setLayoutParams(params)
snacks
} else {
snackbar!!.setText(message).setAction(actionLabel) { action() }
}.also {
if (!it.isShownOrQueued) it.show()
}
}
fun showKeyboard(focusedView: View) {
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(focusedView, InputMethodManager.SHOW_FORCED)
}
fun hideKeyboard() {
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(findViewById<View>(android.R.id.content).windowToken, 0)
}
/**
* @param popUpToInclusive the destination to remove from the stack before opening the camera.
* This only takes effect in the common case where the permission is granted.
*/
fun maybeOpenScan(popUpToInclusive: Int? = null) {
if (hasCameraPermission) {
openCamera(popUpToInclusive)
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(arrayOf(Manifest.permission.CAMERA), 101)
} else {
onNoCamera()
}
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == 101) {
if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
openCamera()
} else {
onNoCamera()
}
}
}
private fun openCamera(popUpToInclusive: Int? = null) {
navController?.navigate(popUpToInclusive ?: R.id.action_global_nav_scan)
}
private fun onNoCamera() {
showSnackbar(getString(R.string.camera_permission_denied))
}
// TODO: clean up this error handling
private var ignoredErrors = 0
private fun onProcessorError(error: Throwable?): Boolean {
var notified = false
when (error) {
is CompactBlockProcessorException.Uninitialized -> {
if (dialog == null) {
notified = true
runOnUiThread {
dialog = showUninitializedError(error) {
dialog = null
}
}
}
}
is CompactBlockProcessorException.FailedScan -> {
if (dialog == null && !ignoreScanFailure) throttle("scanFailure", 20_000L) {
notified = true
runOnUiThread {
dialog = showScanFailure(
error,
onCancel = { dialog = null },
onDismiss = { dialog = null }
)
}
}
}
}
if (!notified) {
ignoredErrors++
if (ignoredErrors >= ZcashSdk.RETRIES) {
if (dialog == null) {
notified = true
runOnUiThread {
dialog = showCriticalProcessorError(error) {
dialog = null
}
}
}
}
}
twig("MainActivity has received an error${if (notified) " and notified the user" else ""} and reported it to bugsnag and mixpanel.")
feedback.report(error)
return true
}
private fun onChainError(errorHeight: BlockHeight, rewindHeight: BlockHeight) {
feedback.report(Reorg(errorHeight, rewindHeight))
}
// TODO: maybe move this quick helper code somewhere general or throttle the dialogs differently (like with a flow and stream operators, instead)
private val throttles = mutableMapOf<String, () -> Any>()
private val noWork = {}
private fun throttle(key: String, delay: Long, block: () -> Any) {
// if the key exists, just add the block to run later and exit
if (throttles.containsKey(key)) {
throttles[key] = block
return
}
block()
// after doing the work, check back in later and if another request came in, throttle it, otherwise exit
throttles[key] = noWork
findViewById<View>(android.R.id.content).postDelayed(
{
throttles[key]?.let { pendingWork ->
throttles.remove(key)
if (pendingWork !== noWork) throttle(key, delay, pendingWork)
}
},
delay
)
}
/* Memo functions that might possibly get moved to MemoUtils */
suspend fun getSender(transaction: ConfirmedTransaction?): String {
if (transaction == null) return getString(R.string.unknown)
return MemoUtil.findAddressInMemo(transaction, ::isValidAddress)?.toAbbreviatedAddress()
?: getString(R.string.unknown)
}
suspend fun String?.validateAddress(): String? {
if (this == null) return null
return if (isValidAddress(this)) this else null
}
fun showFirstUseWarning(
prefKey: String,
@StringRes titleResId: Int = R.string.blank,
@StringRes msgResId: Int = R.string.blank,
@StringRes positiveResId: Int = android.R.string.ok,
@StringRes negativeResId: Int = android.R.string.cancel,
action: MainActivity.() -> Unit = {}
) {
historyViewModel.prefs.getBoolean(prefKey).let { doNotWarnAgain ->
if (doNotWarnAgain) {
action()
return@showFirstUseWarning
}
}
val dialogViewBinding = DialogFirstUseMessageBinding.inflate(layoutInflater)
fun savePref() {
dialogViewBinding.dialogFirstUseCheckbox.isChecked.let { wasChecked ->
historyViewModel.prefs.setBoolean(prefKey, wasChecked)
}
}
dialogViewBinding.dialogMessage.setText(msgResId)
if (dialog != null) dialog?.dismiss()
// TODO: This should be moved to a DialogFragment, otherwise unmanaged dialogs go away during Activity configuration changes
dialog = MaterialAlertDialogBuilder(this)
.setTitle(titleResId)
.setView(dialogViewBinding.root)
.setCancelable(false)
.setPositiveButton(positiveResId) { d, _ ->
d.dismiss()
dialog = null
savePref()
action()
}
.setNegativeButton(negativeResId) { d, _ ->
d.dismiss()
dialog = null
savePref()
}
.show()
}
fun onLaunchUrl(url: String) {
try {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
} catch (t: Throwable) {
showMessage(getString(R.string.error_launch_url))
twig("Warning: failed to open browser due to $t")
}
}
}

36
app/src/main/java/cash/z/ecc/android/ui/MainViewModel.kt

@ -0,0 +1,36 @@
package cash.z.ecc.android.ui
import androidx.lifecycle.ViewModel
import cash.z.ecc.android.util.twig
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
class MainViewModel : ViewModel() {
private val _loadingMessage = MutableStateFlow<String?>("\u23F3 Loading...")
private val _syncReady = MutableStateFlow(false)
val loadingMessage: StateFlow<String?> get() = _loadingMessage
val isLoading get() = loadingMessage.value != null
/**
* A flow of booleans representing whether or not the synchronizer has been started. This is
* useful for views that want to monitor the status of the wallet but don't want to access the
* synchronizer before it is ready to be used. This is also helpful for race conditions where
* the status of the synchronizer is needed before it is created.
*/
val syncReady = _syncReady.asStateFlow()
fun setLoading(isLoading: Boolean = false, message: String? = null) {
twig("MainViewModel.setLoading: $isLoading")
_loadingMessage.value = if (!isLoading) {
null
} else {
message ?: "\u23F3 Loading..."
}
}
fun setSyncReady(isReady: Boolean) {
twig("MainViewModel.setSyncReady: $isReady")
_syncReady.value = isReady
}
}

95
app/src/main/java/cash/z/ecc/android/ui/base/BaseFragment.kt

@ -0,0 +1,95 @@
package cash.z.ecc.android.ui.base
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.NonNull
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.ui.MainActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
abstract class BaseFragment<T : ViewBinding> : Fragment() {
// Normally will be of type MainActivity, but will be null when run under automated tests.
// A future enhancement would be to move analytics. For example, refactor it out of the Activity
// so that we don't have to cast. Or at least put analytics into an interface, so that we're more
// explicitly casting to Analytics rather than MainActivity.
val mainActivity: MainActivity? get() = if (activity is MainActivity) {
activity as MainActivity
} else {
null
}
lateinit var binding: T
lateinit var resumedScope: CoroutineScope
open val screen: Report.Screen? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = inflate(inflater)
return binding.root
}
override fun onResume() {
super.onResume()
mainActivity?.reportScreen(screen)
resumedScope = lifecycleScope.coroutineContext.let {
CoroutineScope(Dispatchers.Main + SupervisorJob(it[Job]))
}
}
override fun onPause() {
super.onPause()
resumedScope.cancel()
}
// inflate is static in the ViewBinding class so we can't handle this ourselves
// each fragment must call FragmentMyLayoutBinding.inflate(inflater)
abstract fun inflate(@NonNull inflater: LayoutInflater): T
fun onBackPressNavTo(navResId: Int, block: (() -> Unit) = {}) {
mainActivity?.onFragmentBackPressed(this) {
block()
mainActivity?.safeNavigate(navResId)
}
}
fun tapped(tap: Report.Tap) {
mainActivity?.reportTap(tap)
}
/**
* Launch the given block once, within the 'resumedScope', once the Synchronizer is ready. This
* utility function helps solve the problem of taking action with the synchronizer before it
* is created. This surfaced while loading keys from secure storage: the HomeFragment would
* resume and start monitoring the synchronizer for changes BEFORE the onAttach function
* returned, meaning before the synchronizerComponent is created. So a state variable needed to
* exist with a longer lifecycle than the synchronizer. This function just takes care of all the
* boilerplate of monitoring that state variable until it returns true.
*/
fun launchWhenSyncReady(block: () -> Unit) {
resumedScope.launch {
mainActivity?.let {
it.mainViewModel.syncReady.filter { isReady -> isReady }.onEach {
block()
}.first()
}
}
}
}

103
app/src/main/java/cash/z/ecc/android/ui/history/HistoryFragment.kt

@ -0,0 +1,103 @@
package cash.z.ecc.android.ui.history
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.paging.PagedList
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentHistoryBinding
import cash.z.ecc.android.ext.*
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Tap.HISTORY_BACK
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.ext.collectWith
import cash.z.ecc.android.sdk.ext.toAbbreviatedAddress
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.util.twig
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch
class HistoryFragment : BaseFragment<FragmentHistoryBinding>() {
override val screen = Report.Screen.HISTORY
private val viewModel: HistoryViewModel by activityViewModels()
private lateinit var transactionAdapter: TransactionAdapter<ConfirmedTransaction>
override fun inflate(inflater: LayoutInflater): FragmentHistoryBinding =
FragmentHistoryBinding.inflate(inflater)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
twig("HistoryFragment.onViewCreated")
super.onViewCreated(view, savedInstanceState)
initTransactionUI()
binding.backButtonHitArea.onClickNavUp { tapped(HISTORY_BACK) }
lifecycleScope.launch {
binding.textAddress.text = viewModel.getAddress().toAbbreviatedAddress(10, 10)
}
}
override fun onResume() {
twig("HistoryFragment.onResume")
super.onResume()
viewModel.balance.filterNotNull().collectWith(resumedScope) {
onBalanceUpdated(it)
}
viewModel.transactions.collectWith(resumedScope) { onTransactionsUpdated(it) }
}
private fun onBalanceUpdated(balance: WalletBalance) {
if (balance.available.value < 0) {
binding.textBalanceAvailable.text = "Updating"
return
}
binding.textBalanceAvailable.text = WalletZecFormmatter.toZecStringShort(balance.available)
val change = balance.pending
binding.textBalanceDescription.apply {
goneIf(change.value <= 0L)
val changeString = WalletZecFormmatter.toZecStringFull(change)
val expecting = R.string.home_banner_expecting.toAppString(true)
val symbol = getString(R.string.symbol)
text = "($expecting +$changeString $symbol)".toColoredSpan(R.color.text_light, "+$changeString")
}
}
private fun initTransactionUI() {
twig("HistoryFragment.initTransactionUI")
transactionAdapter = TransactionAdapter()
transactionAdapter.stateRestorationPolicy =
RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY
binding.recyclerTransactions.apply {
layoutManager = LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
adapter = transactionAdapter
}
}
private fun onTransactionsUpdated(transactions: List<ConfirmedTransaction>) {
twig("HistoryFragment.onTransactionsUpdated")
transactions.size.let { newCount ->
twig("got a new paged list of transactions of length $newCount")
binding.groupEmptyViews.goneIf(newCount > 0)
// tricky: we handle two types of lists, empty and PagedLists. It's not easy to construct an empty PagedList so the SDK currently returns an emptyList() but that will not cast to a PagedList
if (newCount == 0) {
transactionAdapter.submitList(null)
} else {
// tricky: for now, explicitly fail (cast exception) if the transactions are not in a PagedList. Otherwise, this would silently fail to show items and be hard to debug if we're ever passed a non-empty list that isn't an instance of PagedList. This awkwardness will go away when we switch to Paging3
transactionAdapter.submitList(transactions as PagedList<ConfirmedTransaction>)
}
}
}
// TODO: maybe implement this for better fade behavior. Or do an actual scroll behavior instead, yeah do that. Or an item decoration.
fun onLastItemShown(item: ConfirmedTransaction, position: Int) {
binding.footerFade.alpha = position.toFloat() / (binding.recyclerTransactions.adapter?.itemCount ?: 1)
}
}

179
app/src/main/java/cash/z/ecc/android/ui/history/HistoryViewModel.kt

@ -0,0 +1,179 @@
package cash.z.ecc.android.ui.history
import android.text.format.DateUtils
import androidx.annotation.StringRes
import androidx.lifecycle.ViewModel
import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.di.DependenciesHolder
import cash.z.ecc.android.ext.WalletZecFormmatter
import cash.z.ecc.android.ext.toAppString
import cash.z.ecc.android.ext.toAppStringFormatted
import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.sdk.SdkSynchronizer
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.db.entity.valueInZatoshi
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.ui.util.MemoUtil
import cash.z.ecc.android.ui.util.toUtf8Memo
import cash.z.ecc.android.util.twig
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
class HistoryViewModel : ViewModel() {
private val synchronizer: Synchronizer = DependenciesHolder.synchronizer
val prefs: LockBox = DependenciesHolder.prefs
val selectedTransaction = MutableStateFlow<ConfirmedTransaction?>(null)
val uiModels = selectedTransaction.map { it.toUiModel() }
val transactions get() = synchronizer.clearedTransactions
val balance get() = synchronizer.saplingBalances
val latestHeight get() = synchronizer.latestHeight
suspend fun getAddress() = synchronizer.getAddress()
override fun onCleared() {
super.onCleared()
twig("HistoryViewModel cleared!")
}
//
// History Item UiModel
//
data class UiModel(
var topLabel: String = "",
var topValue: String = "",
var bottomLabel: String = "",
var bottomValue: String = "",
var minedHeight: String = "",
var timestamp: String = "",
var iconRotation: Float = -1f,
var fee: String? = null,
var source: String? = null,
var memo: String? = null,
var address: String? = null,
var isInbound: Boolean? = null,
var isMined: Boolean = false,
var confirmation: String? = null,
var txId: String? = null
)
private suspend fun ConfirmedTransaction?.toUiModel(latestHeight: Int? = null): UiModel =
UiModel().apply {
this@toUiModel.let { tx ->
txId = toTxId(tx?.rawTransactionId)
isInbound = when {
!(tx?.toAddress.isNullOrEmpty()) -> false
tx != null && tx.toAddress.isNullOrEmpty() && tx.value > 0L && tx.minedHeight > 0 -> true
else -> null
}
isMined =
tx?.minedHeight != null && tx.minedHeight > synchronizer.network.saplingActivationHeight.value
topValue =
if (tx == null) "" else "\$${WalletZecFormmatter.toZecStringFull(tx.valueInZatoshi)}"
minedHeight = String.format("%,d", tx?.minedHeight ?: 0)
val flags =
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_YEAR or DateUtils.FORMAT_ABBREV_MONTH
timestamp =
if (tx == null || tx.blockTimeInSeconds <= 0) getString(R.string.transaction_timestamp_unavailable) else DateUtils.getRelativeDateTimeString(
ZcashWalletApp.instance,
tx.blockTimeInSeconds * 1000,
DateUtils.SECOND_IN_MILLIS,
DateUtils.WEEK_IN_MILLIS,
flags
).toString()
// memo logic
val txMemo = tx?.memo.toUtf8Memo()
if (!txMemo.isEmpty()) {
memo = txMemo
}
// confirmation logic
// TODO: clean all of this up and remove/improve reliance on `isSufficientlyOld` function. Also, add a constant for the number of confirmations we expect.
tx?.let {
val isMined = it.blockTimeInSeconds != 0L
if (isMined) {
val hasLatestHeight =
latestHeight != null && latestHeight > synchronizer.network.saplingActivationHeight.value
if (it.minedHeight > 0 && hasLatestHeight) {
val confirmations = latestHeight!! - it.minedHeight + 1
confirmation =
if (confirmations >= 10) getString(R.string.transaction_status_confirmed) else "$confirmations ${
getString(
R.string.transaction_status_confirming
)
}"
} else {
if (!hasLatestHeight && isSufficientlyOld(tx)) {
twig("Warning: could not load latestheight from server to determine confirmations but this transaction is mined and old enough to be considered confirmed")
confirmation = getString(R.string.transaction_status_confirmed)
} else {
twig("Warning: could not determine confirmation text value so it will be left null!")
confirmation =
getString(R.string.transaction_confirmation_count_unavailable)
}
}
} else {
confirmation = getString(R.string.transaction_status_pending)
}
}
when (isInbound) {
true -> {
topLabel = getString(R.string.transaction_story_inbound)
bottomLabel = getString(R.string.transaction_story_inbound_total)
bottomValue = "\$${WalletZecFormmatter.toZecStringFull(tx?.valueInZatoshi)}"
iconRotation = 315f
source = getString(R.string.transaction_story_to_shielded)
address = MemoUtil.findAddressInMemo(
tx,
(synchronizer as SdkSynchronizer)::isValidAddress
)
}
false -> {
topLabel = getString(R.string.transaction_story_outbound)
bottomLabel = getString(R.string.transaction_story_outbound_total)
bottomValue =
"\$${WalletZecFormmatter.toZecStringFull(Zatoshi((tx?.valueInZatoshi?.value ?: 0) + ZcashSdk.MINERS_FEE.value))}"
iconRotation = 135f
fee = "+ 0.00001 network fee"
source = getString(R.string.transaction_story_from_shielded)
address = tx?.toAddress
}
null -> {
twig("Error: transaction appears to be invalid.")
}
}
}
}
private fun getString(@StringRes id: Int) = id.toAppString()
private fun getString(@StringRes id: Int, vararg args: Any) = id.toAppStringFormatted(args)
private fun toTxId(tx: ByteArray?): String? {
if (tx == null) return null
val sb = StringBuilder(tx.size * 2)
for (i in (tx.size - 1) downTo 0) {
sb.append(String.format("%02x", tx[i]))
}
return sb.toString()
}
// TODO: determine this in a more generic and technically correct way. For now, this is good enough.
// the goal is just to improve the edge cases where the latest height isn't known but other
// information suggests that the TX is confirmed. We can improve this, later.
private fun isSufficientlyOld(tx: ConfirmedTransaction): Boolean {
val threshold = 75 * 1000 * 25 // approx 25 blocks
val delta = System.currentTimeMillis() / 1000L - tx.blockTimeInSeconds
return tx.minedHeight > synchronizer.network.saplingActivationHeight.value &&
delta < threshold
}
}

46
app/src/main/java/cash/z/ecc/android/ui/history/TransactionAdapter.kt

@ -0,0 +1,46 @@
package cash.z.ecc.android.ui.history
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.DiffUtil
import cash.z.ecc.android.R
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
class TransactionAdapter<T : ConfirmedTransaction> :
PagedListAdapter<T, TransactionViewHolder<T>>(
object : DiffUtil.ItemCallback<T>() {
override fun areItemsTheSame(
oldItem: T,
newItem: T
) = oldItem.minedHeight == newItem.minedHeight && oldItem.noteId == newItem.noteId &&
// bugfix: distinguish between self-transactions so they don't overwrite each other in the UI // TODO confirm that this is working, as intended
((oldItem.raw == null && newItem.raw == null) || (oldItem.raw != null && newItem.raw != null && oldItem.raw!!.contentEquals(newItem.raw!!)))
override fun areContentsTheSame(
oldItem: T,
newItem: T
) = oldItem == newItem
}
) {
init {
setHasStableIds(true)
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
) = TransactionViewHolder<T>(
LayoutInflater.from(parent.context).inflate(R.layout.item_transaction, parent, false)
)
override fun onBindViewHolder(
holder: TransactionViewHolder<T>,
position: Int
) = holder.bindTo(getItem(position))
override fun getItemId(position: Int): Long {
return getItem(position)?.id ?: -1
}
}

221
app/src/main/java/cash/z/ecc/android/ui/history/TransactionFragment.kt

@ -0,0 +1,221 @@
package cash.z.ecc.android.ui.history
import android.content.res.ColorStateList
import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter
import android.os.Bundle
import android.text.method.ScrollingMovementMethod
import android.view.LayoutInflater
import android.view.View
import androidx.core.view.ViewCompat
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.transition.*
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentTransactionBinding
import cash.z.ecc.android.ext.*
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.sdk.ext.toAbbreviatedAddress
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.history.HistoryViewModel.UiModel
import cash.z.ecc.android.util.twig
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class TransactionFragment : BaseFragment<FragmentTransactionBinding>() {
override val screen = Report.Screen.TRANSACTION
private val viewModel: HistoryViewModel by activityViewModels()
var isMemoExpanded: Boolean = false
override fun inflate(inflater: LayoutInflater): FragmentTransactionBinding =
FragmentTransactionBinding.inflate(inflater)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// val transition = TransitionInflater.from(requireContext()).inflateTransition(android.R.transition.move)
// sharedElementEnterTransition = transition
// sharedElementReturnTransition = transition
// sharedElementEnterTransition = createSharedElementTransition()
// sharedElementReturnTransition = createSharedElementTransition()
// sharedElementEnterTransition = ChangeBounds().apply { duration = 1500 }
// sharedElementReturnTransition = ChangeBounds().apply { duration = 1500 }
// enterTransition = Fade().apply {
// duration = 1800
// // slideEdge = Gravity.END
// }
}
private fun createSharedElementTransition(duration: Long = 800L): Transition {
return TransitionSet().apply {
ordering = TransitionSet.ORDERING_TOGETHER
this.duration = duration
// interpolator = PathInterpolatorCompat.create(0.4f, 0f, 0.2f, 1f)
addTransition(ChangeBounds())
addTransition(ChangeClipBounds())
addTransition(ChangeTransform())
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.apply {
ViewCompat.setTransitionName(
topBoxValue,
"test_amount_anim_${viewModel.selectedTransaction.value?.id}"
)
ViewCompat.setTransitionName(
topBoxBackground,
"test_bg_anim_${viewModel.selectedTransaction.value?.id}"
)
backButtonHitArea.onClickNavBack { tapped(Report.Tap.TRANSACTION_BACK) }
lifecycleScope.launch {
viewModel.uiModels.stateIn(lifecycleScope).collect { uiModel ->
topBoxLabel.text = uiModel.topLabel
topBoxValue.text = uiModel.topValue
bottomBoxLabel.text = uiModel.bottomLabel
bottomBoxValue.text = uiModel.bottomValue
textBlockHeight.text = uiModel.minedHeight
textTimestamp.text = uiModel.timestamp
if (uiModel.iconRotation < 0) {
topBoxIcon.gone()
} else {
topBoxIcon.rotation = uiModel.iconRotation
topBoxIcon.visible()
}
if (!uiModel.isMined) {
textBlockHeight.invisible()
textBlockHeightPrefix.invisible()
}
val exploreOnClick = View.OnClickListener {
uiModel.txId?.let { txId ->
mainActivity?.showFirstUseWarning(
Const.Pref.FIRST_USE_VIEW_TX,
titleResId = R.string.dialog_first_use_view_tx_title,
msgResId = R.string.dialog_first_use_view_tx_message,
positiveResId = R.string.dialog_first_use_view_tx_positive,
negativeResId = R.string.dialog_first_use_view_tx_negative
) {
onLaunchUrl(txId.toTransactionUrl())
}
}
}
buttonExplore.setOnClickListener(exploreOnClick)
textBlockHeight.setOnClickListener(exploreOnClick)
uiModel.fee?.let {
subwaySpotFee.visible(); subwayLabelFee.visible(); subwayLabelFee.text = it
}
uiModel.source?.let {
subwaySpotSource.visible(); subwayLabelSource.visible(); subwayLabelSource.text =
it
}
uiModel.toAddressLabel()?.let {
subwaySpotAddress.visible(); subwayLabelAddress.visible(); subwayLabelAddress.text =
it
}
uiModel.toAddressClickListener()
?.let { subwayLabelAddress.setOnClickListener(it) }
// TODO: remove logic from sections below and add more fields or extension functions to UiModel
uiModel.confirmation?.let {
subwaySpotConfirmations.visible(); subwayLabelConfirmations.visible()
subwayLabelConfirmations.text = it
if (it.equals(getString(R.string.transaction_status_confirmed), true)) {
subwayLabelConfirmations.setTextColor(R.color.tx_primary.toAppColor())
} else {
subwayLabelConfirmations.setTextColor(R.color.tx_text_light_dimmed.toAppColor())
}
}
uiModel.memo?.let {
hitAreaMemoSubway.setOnClickListener { _ ->
onToggleMemo(
!isMemoExpanded,
it
)
}
hitAreaMemoIcon.setOnClickListener { _ ->
onToggleMemo(
!isMemoExpanded,
it
)
}
subwayLabelMemo.setOnClickListener { _ ->
onToggleMemo(
!isMemoExpanded,
it
)
}
subwayLabelMemo.setOnLongClickListener { _ ->
mainActivity?.copyText(it, "Memo")
true
}
subwayLabelMemo.movementMethod = ScrollingMovementMethod()
subwaySpotMemoContent.visible()
subwayLabelMemo.visible()
hitAreaMemoSubway.visible()
onToggleMemo(false)
}
}
}
}
}
val invertingMatrix = ColorMatrixColorFilter(ColorMatrix().apply { setSaturation(0f) })
private fun onToggleMemo(isExpanded: Boolean, memo: String = "") {
twig("onToggleMemo($isExpanded, $memo)")
if (isExpanded) {
twig("setting memo text to: $memo")
binding.subwayLabelMemo.setText(memo)
binding.subwayLabelMemo.invalidate()
// don't impede the ability to scroll
binding.groupMemoIcon.gone()
binding.subwayLabelMemo.backgroundTintList =
ColorStateList.valueOf(R.color.tx_text_light_dimmed.toAppColor())
binding.subwaySpotMemoContent.colorFilter = invertingMatrix
binding.subwaySpotMemoContent.rotation = 90.0f
} else {
binding.subwayLabelMemo.setText(getString(R.string.transaction_with_memo))
binding.subwayLabelMemo.scrollTo(0, 0)
binding.subwayLabelMemo.invalidate()
twig("setting memo text to: with a memo")
binding.groupMemoIcon.visible()
binding.subwayLabelMemo.backgroundTintList =
ColorStateList.valueOf(R.color.tx_primary.toAppColor())
binding.subwaySpotMemoContent.colorFilter = null
binding.subwaySpotMemoContent.rotation = 0.0f
}
isMemoExpanded = isExpanded
}
private fun String.toTransactionUrl(): String {
return getString(R.string.api_block_explorer, this)
}
private fun UiModel?.toAddressClickListener(): View.OnClickListener? {
return this?.address?.let { addr ->
View.OnClickListener { mainActivity?.copyText(addr, "Address") }
}
}
private fun UiModel?.toAddressLabel(): CharSequence? {
if (this == null || this.address == null || this.isInbound == null) return null
val prefix = getString(
if (isInbound == true) {
R.string.transaction_prefix_from
} else {
R.string.transaction_prefix_to
}
)
return "$prefix ${address?.toAbbreviatedAddress() ?: "Unknown"}".let {
it.toColoredSpan(R.color.tx_text_light_dimmed, if (address == null) it else prefix)
}
}
}

168
app/src/main/java/cash/z/ecc/android/ui/history/TransactionViewHolder.kt

@ -0,0 +1,168 @@
package cash.z.ecc.android.ui.history
import android.graphics.drawable.Drawable
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import cash.z.ecc.android.R
import cash.z.ecc.android.ext.WalletZecFormmatter
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ext.locale
import cash.z.ecc.android.ext.toAppColor
import cash.z.ecc.android.ext.toAppInt
import cash.z.ecc.android.ext.toColoredSpan
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.db.entity.valueInZatoshi
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.ext.isShielded
import cash.z.ecc.android.sdk.ext.toAbbreviatedAddress
import cash.z.ecc.android.ui.MainActivity
import cash.z.ecc.android.ui.util.toUtf8Memo
import cash.z.ecc.android.util.twig
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val indicator = itemView.findViewById<View>(R.id.indicator)
private val amountText = itemView.findViewById<TextView>(R.id.text_transaction_amount)
private val topText = itemView.findViewById<TextView>(R.id.text_transaction_top)
private val bottomText = itemView.findViewById<TextView>(R.id.text_transaction_bottom)
private val transactionArrow = itemView.findViewById<ImageView>(R.id.image_transaction_arrow)
private val formatter = SimpleDateFormat(itemView.context.getString(R.string.format_transaction_history_date_time), itemView.context.locale())
private val iconMemo = itemView.findViewById<ImageView>(R.id.image_memo)
fun bindTo(transaction: T?) {
val mainActivity = itemView.context as MainActivity
mainActivity.lifecycleScope.launch {
// update view
var lineOne: CharSequence = ""
var lineTwo = ""
var amountZec = ""
var amountDisplay = ""
var amountColor: Int = R.color.text_light
var lineOneColor: Int = R.color.text_light
var lineTwoColor: Int = R.color.text_light_dimmed
var indicatorBackground: Int = R.color.text_light_dimmed
var arrowRotation: Int = R.integer.transaction_arrow_rotation_send
var arrowBackgroundTint: Int = R.color.text_light
var isLineOneSpanned = false
try {
transaction?.apply {
itemView.setOnClickListener {
onTransactionClicked(this)
}
itemView.setOnLongClickListener {
onTransactionLongPressed(this)
true
}
amountZec = WalletZecFormmatter.toZecStringShort(valueInZatoshi)
// TODO: these might be good extension functions
val timestamp = formatter.format(blockTimeInSeconds * 1000L)
val isMined = blockTimeInSeconds != 0L
when {
!toAddress.isNullOrEmpty() -> {
indicatorBackground =
if (isMined) R.color.zcashRed else R.color.zcashGray
lineOne = "${
if (isMined) str(R.string.transaction_address_you_paid) else str(R.string.transaction_address_paying)
} ${toAddress?.toAbbreviatedAddress()}"
lineTwo =
if (isMined) "${str(R.string.transaction_status_sent)} $timestamp" else str(
R.string.transaction_status_pending
)
// TODO: this logic works but is sloppy. Find a more robust solution to displaying information about expiration (such as expires in 1 block, etc). Then if it is way beyond expired, remove it entirely. Perhaps give the user a button for that (swipe to dismiss?)
if (!isMined && (expiryHeight != null) && (expiryHeight!! < mainActivity.latestHeight?.value ?: -1)) lineTwo =
str(R.string.transaction_status_expired)
amountDisplay = "- $amountZec"
if (isMined) {
arrowRotation = R.integer.transaction_arrow_rotation_send
amountColor = R.color.transaction_sent
if (toAddress.isShielded()) {
lineOneColor = R.color.zcashYellow
} else {
toAddress?.toAbbreviatedAddress()?.let {
lineOne = lineOne.toColoredSpan(R.color.zcashBlueDark, it)
}
}
} else {
arrowRotation = R.integer.transaction_arrow_rotation_pending
}
}
toAddress.isNullOrEmpty() && value > 0L && minedHeight > 0 -> {
indicatorBackground = R.color.zcashGreen
val senderAddress = mainActivity.getSender(transaction)
lineOne = "${str(R.string.transaction_received_from)} $senderAddress"
lineTwo = "${str(R.string.transaction_received)} $timestamp"
amountDisplay = "+ $amountZec"
if (senderAddress.isShielded()) {
amountColor = R.color.zcashYellow
lineOneColor = R.color.zcashYellow
} else {
senderAddress.toAbbreviatedAddress().let {
lineOne =
if (senderAddress.equals(str(R.string.unknown), true)) {
lineOne.toColoredSpan(R.color.zcashYellow, it)
} else {
lineOne.toColoredSpan(R.color.zcashBlueDark, it)
}
}
}
arrowRotation = R.integer.transaction_arrow_rotation_received
}
else -> {
lineOne = str(R.string.unknown)
lineTwo = str(R.string.unknown)
amountDisplay = amountZec
amountColor = R.color.text_light
arrowRotation = R.integer.transaction_arrow_rotation_received
}
}
// sanitize amount
if (value < ZcashSdk.MINERS_FEE.value * 10) amountDisplay = "< 0.0001"
else if (amountZec.length > 10) { // 10 allows 3 digits to the left and 6 to the right of the decimal
amountDisplay = str(R.string.transaction_instruction_tap)
}
}
topText.text = lineOne
bottomText.text = lineTwo
amountText.text = amountDisplay
amountText.setTextColor(amountColor.toAppColor())
if (!isLineOneSpanned) {
topText.setTextColor(lineOneColor.toAppColor())
}
bottomText.setTextColor(lineTwoColor.toAppColor())
indicator.setBackgroundColor(indicatorBackground.toAppColor())
transactionArrow.setColorFilter(arrowBackgroundTint.toAppColor())
transactionArrow.rotation = arrowRotation.toAppInt().toFloat()
var bottomTextRightDrawable: Drawable? = null
iconMemo.goneIf(!transaction?.memo.toUtf8Memo().isNotEmpty())
bottomText.setCompoundDrawablesWithIntrinsicBounds(null, null, bottomTextRightDrawable, null)
} catch (t: Throwable) {
twig("Failed to parse the transaction due to $t")
}
}
}
private fun onTransactionClicked(transaction: ConfirmedTransaction) {
(itemView.context as MainActivity).apply {
historyViewModel.selectedTransaction.value = transaction
safeNavigate(R.id.action_nav_history_to_nav_transaction)
}
}
private fun onTransactionLongPressed(transaction: ConfirmedTransaction) {
val mainActivity = itemView.context as MainActivity
transaction.toAddress?.let {
mainActivity.copyText(it, "Transaction Address")
}
}
private inline fun str(@StringRes resourceId: Int) = itemView.context.getString(resourceId)
}

52
app/src/main/java/cash/z/ecc/android/ui/history/TransactionsDrawableFooter.kt

@ -0,0 +1,52 @@
package cash.z.ecc.android.ui.history
//
// import android.content.Context
// import android.graphics.Canvas
// import android.graphics.Rect
// import android.view.LayoutInflater
// import android.view.View
// import androidx.recyclerview.widget.RecyclerView
// import cash.z.ecc.android.R
//
//
// class TransactionsDrawableFooter(context: Context) : RecyclerView.ItemDecoration() {
//
// private var footer: View =
// LayoutInflater.from(context).inflate(R.layout.footer_transactions, null, false)
//
// override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
// super.onDraw(c, parent, state!!)
// footer.measure(
// View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.AT_MOST),
// View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
// )
// // layout basically just gets drawn on the reserved space on top of the first view
// footer.layout(parent.left, 0, parent.right, footer.measuredHeight)
// for (i in 0 until parent.childCount) {
// val view: View = parent.getChildAt(i)
// if (parent.getChildAdapterPosition(view) == parent.adapter!!.itemCount - 1) {
// c.save()
// val height: Int = footer.measuredHeight
// val top: Int = view.top - height
// c.translate(0.0f, top.toFloat())
// footer.draw(c)
// c.restore()
// break
// }
// }
// }
//
// override fun getItemOffsets(
// outRect: Rect,
// view: View,
// parent: RecyclerView,
// state: RecyclerView.State
// ) {
// super.getItemOffsets(outRect, view, parent, state)
// if (parent.getChildAdapterPosition(view) == parent.adapter!!.itemCount - 1) {
// outRect.set(0, 0, 0, 150)
// } else {
// outRect.setEmpty()
// }
// }
// }

48
app/src/main/java/cash/z/ecc/android/ui/history/TransactionsFooter.kt

@ -0,0 +1,48 @@
package cash.z.ecc.android.ui.history
import android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import cash.z.ecc.android.R
class TransactionsFooter(context: Context) : RecyclerView.ItemDecoration() {
private var footer: Drawable = context.resources.getDrawable(R.drawable.background_footer)
val bounds = Rect()
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
c.save()
val left: Int = 0
val right: Int = parent.width
val childCount = parent.childCount
val adapterItemCount = parent.adapter!!.itemCount
for (i in 0 until childCount) {
val child = parent.getChildAt(i)
if (parent.getChildAdapterPosition(child) == adapterItemCount - 1) {
parent.getDecoratedBoundsWithMargins(child, bounds)
val bottom: Int = bounds.bottom + Math.round(child.translationY)
val top: Int = bottom - footer.intrinsicHeight
footer.setBounds(left, top, right, bottom)
footer.draw(c)
}
}
c.restore()
}
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
if (parent.getChildAdapterPosition(view) == parent.adapter!!.itemCount - 1) {
outRect.set(0, 0, 0, footer.intrinsicHeight)
} else {
outRect.setEmpty()
}
}
}

69
app/src/main/java/cash/z/ecc/android/ui/home/AutoshieldingInformationFragment.kt

@ -0,0 +1,69 @@
package cash.z.ecc.android.ui.home
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import cash.z.ecc.android.databinding.FragmentAutoShieldInformationBinding
import cash.z.ecc.android.ext.requireApplicationContext
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.preference.Preferences
import cash.z.ecc.android.preference.model.put
import cash.z.ecc.android.ui.base.BaseFragment
class AutoshieldingInformationFragment : BaseFragment<FragmentAutoShieldInformationBinding>() {
override val screen = Report.Screen.AUTO_SHIELD_INFORMATION
private val args: AutoshieldingInformationFragmentArgs by navArgs()
override fun inflate(inflater: LayoutInflater): FragmentAutoShieldInformationBinding =
FragmentAutoShieldInformationBinding.inflate(inflater)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
/*
* Once the fragment is displayed, acknowledge it was presented to the user. While it might
* be better to have explicit user interaction (positive/negative button or back),
* this implementation is simpler. Hooking into the positive/negative button is easy, but
* hooking into the back button from a Fragment ends up being gross.
*
* Always acknowledging is necessary, because the HomeFragment will otherwise almost immediately
* re-launch this Fragment when it refreshes the UI (and therefore re-runs the
* check as to whether the preference to display this fragment has been set).
*/
acknowledge()
binding.buttonAutoshieldDismiss.setOnClickListener {
if (args.isStartAutoshield) {
findNavController().navigate(AutoshieldingInformationFragmentDirections.actionNavAutoshieldingInfoToAutoshield())
} else {
findNavController().navigate(AutoshieldingInformationFragmentDirections.actionNavAutoshieldingInfoToHome())
}
}
binding.buttonAutoshieldMoreInfo.setOnClickListener {
try {
findNavController().navigate(AutoshieldingInformationFragmentDirections.actionNavAutoshieldingInfoToBrowser())
} catch (e: Exception) {
// ActivityNotFoundException could happen on certain devices, like Android TV, Android Things, etc.
// SecurityException shouldn't occur, but just in case we catch all exceptions to
// prevent another package on the device from crashing us if that package tries to be malicious
// by adding permissions or changing export status dynamically.
// In the future, it might also be desirable to display a Toast or Snackbar indicating
// that the browser couldn't be launched
findNavController().navigate(AutoshieldingInformationFragmentDirections.actionNavAutoshieldingInfoToHome())
}
}
}
private fun acknowledge() {
Preferences.isAcknowledgedAutoshieldingInformationPrompt.put(
requireApplicationContext(),
true
)
}
}

158
app/src/main/java/cash/z/ecc/android/ui/home/BalanceDetailFragment.kt

@ -0,0 +1,158 @@
package cash.z.ecc.android.ui.home
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.Toast
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.databinding.FragmentBalanceDetailBinding
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ext.onClickNavBack
import cash.z.ecc.android.ext.toAppColor
import cash.z.ecc.android.ext.toSplitColorSpan
import cash.z.ecc.android.feedback.Report.Tap.RECEIVE_BACK
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.home.BalanceDetailViewModel.StatusModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
class BalanceDetailFragment : BaseFragment<FragmentBalanceDetailBinding>() {
private val viewModel: BalanceDetailViewModel by viewModels()
private var lastSignal: BlockHeight? = null
override fun inflate(inflater: LayoutInflater): FragmentBalanceDetailBinding =
FragmentBalanceDetailBinding.inflate(inflater)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.balances.onEach { onBalanceUpdated(it) }.launchIn(this)
viewModel.statuses.onEach { onStatusUpdated(it) }.launchIn(this)
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.hitAreaExit.onClickNavBack() { tapped(RECEIVE_BACK) }
binding.textShieldedHushTitle.text = "SHIELDED ${getString(R.string.symbol)}"
}
private fun onBalanceUpdated(balanceModel: BalanceDetailViewModel.BalanceModel) {
balanceModel.apply {
if (balanceModel.hasData()) {
setBalances(paddedShielded, paddedTransparent, paddedTotal)
} else {
setBalances(" --", " --", " --")
}
}
}
private fun onStatusUpdated(status: StatusModel) {
binding.textStatus.text = status.toStatus()
if (status.missingBlocks > 100) {
binding.textBlockHeightPrefix.text = "Processing "
binding.textBlockHeight.text = String.format(
"%,d",
status.info.lastScannedHeight?.value ?: 0
) + " of " + String.format("%,d", status.info.networkBlockHeight?.value ?: 0)
} else {
status.info.lastScannedHeight.let { height ->
if (height == null) {
binding.textBlockHeightPrefix.text = "Processing..."
binding.textBlockHeight.text = ""
} else {
binding.textBlockHeightPrefix.text = "Balances as of block "
binding.textBlockHeight.text =
String.format("%,d", status.info.lastScannedHeight?.value ?: 0)
sendNewBlockSignal(status.info.lastScannedHeight)
}
}
}
}
private fun sendNewBlockSignal(currentHeight: BlockHeight?) {
// prevent a flood of signals while scanning blocks
if (lastSignal != null && (currentHeight?.value ?: 0) > lastSignal!!.value) {
mainActivity?.vibrate(0, 100, 100, 300)
Toast.makeText(mainActivity, "New block!", Toast.LENGTH_SHORT).show()
}
lastSignal = currentHeight
}
fun setBalances(shielded: String, transparent: String, total: String) {
binding.textShieldAmount.text = shielded.colorize()
}
private fun String.colorize(): CharSequence {
val dotIndex = indexOf('.')
return if (dotIndex < 0 || length < (dotIndex + 4)) {
this
} else {
toSplitColorSpan(R.color.text_light, R.color.zcashWhite_24, indexOf('.') + 4)
}
}
private fun StatusModel.toStatus(): String {
fun String.plural(count: Int) = if (count > 1) "${this}s" else this
if (viewModel.latestBalance?.hasData() == false) {
return "Balance info is not yet available"
}
var status = ""
if (hasUnmined) {
val count = pendingUnmined.count()
status += "Balance excludes $count unconfirmed ${"transaction".plural(count)}. "
}
status += when {
hasPendingTransparentBalance && hasPendingShieldedBalance -> {
"Awaiting ${pendingShieldedBalance.convertZatoshiToZecString(8)} ${
ZcashWalletApp.instance.getString(
R.string.symbol
)
} in shielded funds and {pendingTransparentBalance.convertZatoshiToZecString(8)} ${
ZcashWalletApp.instance.getString(
R.string.symbol
)
} in transparent funds"
}
hasPendingShieldedBalance -> {
"Awaiting ${pendingShieldedBalance.convertZatoshiToZecString(8)} ${
ZcashWalletApp.instance.getString(
R.string.symbol
)
} in shielded funds"
}
hasPendingTransparentBalance -> {
"Awaiting ${pendingTransparentBalance.convertZatoshiToZecString(8)} ${
ZcashWalletApp.instance.getString(
R.string.symbol
)
} in transparent funds"
}
else -> ""
}
pendingUnconfirmed.count().takeUnless { it == 0 }?.let { count ->
if (status.contains("Awaiting")) status += " and "
status += "$count outbound ${"transaction".plural(count)}"
remainingConfirmations().firstOrNull()?.let { remaining ->
status += " with $remaining ${"confirmation".plural(remaining.toInt())} remaining"
}
}
return if (status.isEmpty()) "All funds are available!" else status
}
}

147
app/src/main/java/cash/z/ecc/android/ui/home/BalanceDetailViewModel.kt

@ -0,0 +1,147 @@
package cash.z.ecc.android.ui.home
import androidx.lifecycle.ViewModel
import cash.z.ecc.android.di.DependenciesHolder
import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
import cash.z.ecc.android.sdk.db.entity.PendingTransaction
import cash.z.ecc.android.sdk.db.entity.isMined
import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combineTransform
class BalanceDetailViewModel : ViewModel() {
private val synchronizer: Synchronizer = DependenciesHolder.synchronizer
private val lockBox: LockBox = DependenciesHolder.lockBox
var showAvailable: Boolean = true
set(value) {
field = value
latestBalance?.showAvailable = value
}
var latestBalance: BalanceModel? = null
val balances: Flow<BalanceModel>
get() = combineTransform(
synchronizer.saplingBalances,
synchronizer.transparentBalances
) { saplingBalance, transparentBalance ->
BalanceModel(saplingBalance, transparentBalance, showAvailable).let {
latestBalance = it
emit(it)
}
}
val statuses: Flow<StatusModel>
get() = combineTransform(
balances,
synchronizer.pendingTransactions,
synchronizer.processorInfo
) { balances, pending, info ->
emit(StatusModel(balances, pending, info))
}
data class BalanceModel(
val shieldedBalance: WalletBalance?,
val transparentBalance: WalletBalance?,
var showAvailable: Boolean = false
) {
/** Whether to make calculations based on total or available zatoshi */
val canAutoShield: Boolean =
(transparentBalance?.available?.value ?: 0L) > ZcashSdk.MINERS_FEE.value
val balanceShielded: String
get() {
return if (showAvailable) shieldedBalance?.available.toDisplay()
else shieldedBalance?.total.toDisplay()
}
val balanceTransparent: String
get() {
return if (showAvailable) transparentBalance?.available.toDisplay()
else transparentBalance?.total.toDisplay()
}
val balanceTotal: String
get() {
return if (showAvailable) ((shieldedBalance?.available
?: Zatoshi(0)) + (transparentBalance?.available ?: Zatoshi(0))).toDisplay()
else ((shieldedBalance?.total ?: Zatoshi(0)) + (transparentBalance?.total
?: Zatoshi(0))).toDisplay()
}
val paddedShielded get() = pad(balanceShielded)
val paddedTransparent get() = pad(balanceTransparent)
val paddedTotal get() = pad(balanceTotal)
val maxLength
get() = maxOf(
balanceShielded.length,
balanceTransparent.length,
balanceTotal.length
)
val hasPending =
(null != shieldedBalance && shieldedBalance.available != shieldedBalance.total) ||
(null != transparentBalance && transparentBalance.available != transparentBalance.total)
private fun Zatoshi?.toDisplay(): String {
return this?.convertZatoshiToZecString(8, 8) ?: "0"
}
private fun pad(balance: String): String {
var diffLength = maxLength - balance.length
return buildString {
repeat(diffLength) {
append(' ')
}
append(balance)
}
}
fun hasData(): Boolean {
return shieldedBalance != null || transparentBalance != null
}
}
data class StatusModel(
val balances: BalanceModel,
val pending: List<PendingTransaction>,
val info: CompactBlockProcessor.ProcessorInfo,
) {
val pendingUnconfirmed =
pending.filter { it.isSubmitSuccess() && it.isMined() && !it.isConfirmed(info.lastScannedHeight) }
val pendingUnmined = pending.filter { it.isSubmitSuccess() && !it.isMined() }
val pendingShieldedBalance = balances.shieldedBalance?.pending
val pendingTransparentBalance = balances.transparentBalance?.pending
val hasUnconfirmed = pendingUnconfirmed.isNotEmpty()
val hasUnmined = pendingUnmined.isNotEmpty()
val hasPendingShieldedBalance = (pendingShieldedBalance?.value ?: 0L) > 0L
val hasPendingTransparentBalance = (pendingTransparentBalance?.value ?: 0L) > 0L
val missingBlocks = ((info.networkBlockHeight?.value ?: 0) - (info.lastScannedHeight?.value
?: 0)).coerceAtLeast(0)
private fun PendingTransaction.isConfirmed(networkBlockHeight: BlockHeight?): Boolean {
return networkBlockHeight?.let {
isMined() && (it.value - minedHeight + 1) > 10 // fix: plus 1 because the mined block counts as the FIRST confirmation
} ?: false
}
fun remainingConfirmations(confirmationsRequired: Int = 10) =
pendingUnconfirmed
.map {
confirmationsRequired - ((info.lastScannedHeight?.value
?: -1) - it.minedHeight + 1)
} // fix: plus 1 because the mined block counts as the FIRST confirmation
.filter { it > 0 }
.sortedDescending()
}
}

596
app/src/main/java/cash/z/ecc/android/ui/home/HomeFragment.kt

@ -0,0 +1,596 @@
package cash.z.ecc.android.ui.home
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.content.res.AppCompatResources
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.databinding.DialogSolicitFeedbackRatingBinding
import cash.z.ecc.android.databinding.FragmentHomeBinding
import cash.z.ecc.android.ext.*
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Tap.*
import cash.z.ecc.android.preference.Preferences
import cash.z.ecc.android.preference.model.get
import cash.z.ecc.android.sdk.Synchronizer.Status.*
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
import cash.z.ecc.android.sdk.ext.convertZecToZatoshi
import cash.z.ecc.android.sdk.ext.onFirstWith
import cash.z.ecc.android.sdk.ext.safelyConvertToBigDecimal
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.home.HomeFragment.BannerAction.*
import cash.z.ecc.android.ui.send.AutoShieldFragment
import cash.z.ecc.android.ui.send.SendViewModel
import cash.z.ecc.android.ui.setup.WalletSetupViewModel
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.NO_SEED
import cash.z.ecc.android.util.twig
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.runningReduce
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
// There are deprecations with the use of BroadcastChannel
@kotlinx.coroutines.ObsoleteCoroutinesApi
class HomeFragment : BaseFragment<FragmentHomeBinding>() {
override val screen = Report.Screen.HOME
private val walletSetup: WalletSetupViewModel by activityViewModels()
private val sendViewModel: SendViewModel by activityViewModels()
private val viewModel: HomeViewModel by viewModels()
private lateinit var numberPad: List<TextView>
private lateinit var uiModel: HomeViewModel.UiModel
lateinit var snake: MagicSnakeLoader
override fun inflate(inflater: LayoutInflater): FragmentHomeBinding =
FragmentHomeBinding.inflate(inflater)
//
// LifeCycle
//
override fun onAttach(context: Context) {
twig("HomeFragment.onAttach")
twig("ZZZ")
twig("ZZZ")
twig("ZZZ")
twig("ZZZ ===================== HOME FRAGMENT CREATED ==================================")
super.onAttach(context)
walletSetup.checkSeed().onFirstWith(lifecycleScope) {
if (it == NO_SEED) {
// interact with user to create, backup and verify seed
// leads to a call to startSync(), later (after accounts are created from seed)
twig("Previous wallet not found, therefore, launching seed creation flow")
mainActivity?.setLoading(false)
mainActivity?.safeNavigate(R.id.action_nav_home_to_create_wallet)
} else {
twig("Previous wallet found. Re-opening it.")
mainActivity?.setLoading(true)
try {
walletSetup.openStoredWallet()
mainActivity?.startSync()
} catch (e: UnsatisfiedLinkError) {
mainActivity?.showSharedLibraryCriticalError(e)
}
twig("Done reopening wallet.")
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
twig("HomeFragment.onViewCreated uiModel: ${::uiModel.isInitialized} saved: ${savedInstanceState != null}")
with(binding) {
numberPad = arrayListOf(
buttonNumberPad0.asKey(),
buttonNumberPad1.asKey(),
buttonNumberPad2.asKey(),
buttonNumberPad3.asKey(),
buttonNumberPad4.asKey(),
buttonNumberPad5.asKey(),
buttonNumberPad6.asKey(),
buttonNumberPad7.asKey(),
buttonNumberPad8.asKey(),
buttonNumberPad9.asKey(),
buttonNumberPadDecimal.asKey(),
buttonNumberPadBack.asKey()
)
hitAreaProfile.onClickNavTo(R.id.action_nav_home_to_nav_profile) { tapped(HOME_PROFILE) }
textHistory.onClickNavTo(R.id.action_nav_home_to_nav_history) { tapped(HOME_HISTORY) }
textSendAmount.onClickNavTo(R.id.action_nav_home_to_nav_balance_detail) {
tapped(
HOME_BALANCE_DETAIL
)
}
hitAreaBalance.onClickNavTo(R.id.action_nav_home_to_nav_balance_detail) {
tapped(
HOME_BALANCE_DETAIL
)
}
hitAreaReceive.onClickNavTo(R.id.action_nav_home_to_nav_receive) { tapped(HOME_RECEIVE) }
textBannerAction.setOnClickListener {
onBannerAction(BannerAction.from((it as? TextView)?.text?.toString()))
}
buttonSendAmount.setOnClickListener {
onSend().also { tapped(HOME_SEND) }
}
setSendAmount("0", false)
snake = MagicSnakeLoader(binding.lottieButtonLoading)
// fix: don't start up with just a black screen
buttonSendAmount.text = getString(R.string.home_button_send_disconnected)
buttonSendAmount.setTextColor(R.color.text_light.toAppColor())
}
binding.buttonNumberPadBack.setOnLongClickListener {
onClearAmount().also { tapped(HOME_CLEAR_AMOUNT) }
true
}
if (::uiModel.isInitialized) {
twig("uiModel exists! it has pendingSend=${uiModel.pendingSend} ZEC while the sendViewModel=${sendViewModel.zatoshiAmount} zats")
// if the model already existed, cool but let the sendViewModel be the source of truth for the amount
onModelUpdated(
null,
uiModel.copy(
pendingSend = WalletZecFormmatter.toZecStringFull(
sendViewModel.zatoshiAmount ?: Zatoshi(0L)
)
)
)
}
}
private fun onClearAmount() {
twig("onClearAmount()")
if (::uiModel.isInitialized) {
resumedScope.launch {
binding.textSendAmount.text.apply {
while (uiModel.pendingSend != "0") {
viewModel.onChar('<')
delay(5)
}
}
}
}
}
override fun onResume() {
super.onResume()
twig("HomeFragment.onResume resumeScope.isActive: ${resumedScope.isActive} $resumedScope")
launchWhenSyncReady(::onSyncReady)
}
private fun onSyncReady() {
twig("Sync ready! Monitoring synchronizer state...")
monitorUiModelChanges()
twig("HomeFragment.onSyncReady COMPLETE")
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
// if (::uiModel.isInitialized) {
// outState.putParcelable("uiModel", uiModel)
// }
}
override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(savedInstanceState)
savedInstanceState?.let { inState ->
// onModelUpdated(HomeViewModel.UiModel(), inState.getParcelable("uiModel")!!)
}
}
//
// Public UI API
//
var isSendEnabled = false
fun setSendEnabled(enabled: Boolean, isSynced: Boolean) {
isSendEnabled = enabled
binding.buttonSendAmount.apply {
if (enabled || !isSynced) {
isEnabled = true
isClickable = isSynced
binding.lottieButtonLoading.alpha = 1.0f
} else {
isEnabled = false
isClickable = false
binding.lottieButtonLoading.alpha = 0.32f
}
}
}
fun setProgress(uiModel: HomeViewModel.UiModel) {
if (!uiModel.processorInfo.hasData && !uiModel.isDisconnected) {
twig("Warning: ignoring progress update because the processor is still starting.")
return
}
snake.isSynced = uiModel.isSynced
if (!uiModel.isSynced) {
snake.downloadProgress = uiModel.downloadProgress
snake.scanProgress = uiModel.scanProgress
}
val sendText = when {
uiModel.status == DISCONNECTED -> getString(R.string.home_button_send_disconnected)
uiModel.isSynced -> if (uiModel.hasFunds) getString(R.string.home_button_send_has_funds) else getString(
R.string.home_button_send_no_funds
)
uiModel.status == STOPPED -> getString(R.string.home_button_send_idle)
uiModel.isDownloading -> {
when (snake.downloadProgress) {
0 -> "Preparing to download..."
else -> getString(R.string.home_button_send_downloading, snake.downloadProgress)
}
}
uiModel.isValidating -> getString(R.string.home_button_send_validating)
uiModel.isScanning -> {
when (snake.scanProgress) {
0 -> "Preparing to scan..."
100 -> "Finalizing..."
else -> getString(R.string.home_button_send_scanning, snake.scanProgress)
}
}
else -> getString(R.string.home_button_send_updating)
}
binding.buttonSendAmount.text = sendText
twig("Send button set to: $sendText")
val resId =
if (uiModel.isSynced) R.color.selector_button_text_dark else R.color.selector_button_text_light
context?.let {
binding.buttonSendAmount.setTextColor(
AppCompatResources.getColorStateList(
it,
resId
)
)
}
binding.lottieButtonLoading.invisibleIf(uiModel.isDisconnected)
}
/**
* @param amount the amount to send represented as ZEC, without the dollar sign.
*/
fun setSendAmount(amount: String, updateModel: Boolean = true) {
twig("setSendAmount($amount, $updateModel)")
binding.textSendAmount.text = "\$$amount".toColoredSpan(R.color.text_light_dimmed, "$")
if (updateModel) {
sendViewModel.zatoshiAmount = amount.safelyConvertToBigDecimal().convertZecToZatoshi()
twig(
"dBUG: updating model. converting: $amount\tresult: ${sendViewModel.zatoshiAmount}\tprint: ${
WalletZecFormmatter.toZecStringFull(
sendViewModel.zatoshiAmount
)
}"
)
}
binding.buttonSendAmount.disabledIf(amount == "0")
}
fun setAvailable(
availableBalance: Zatoshi?,
totalBalance: Zatoshi?,
availableTransparentBalance: Zatoshi?,
unminedCount: Int = 0
) {
val missingBalance = availableBalance == null
val availableString =
if (missingBalance) getString(R.string.home_button_send_updating) else WalletZecFormmatter.toZecStringFull(
availableBalance
)
binding.textBalanceAvailable.text = availableString
binding.textBalanceAvailable.transparentIf(missingBalance)
binding.labelBalance.transparentIf(missingBalance)
binding.textBalanceDescription.apply {
goneIf(missingBalance)
text = when {
unminedCount > 0 -> "(excludes $unminedCount unconfirmed ${if (unminedCount > 1) "transactions" else "transaction"})"
availableBalance != null && totalBalance != null && (availableBalance.value < totalBalance.value) -> {
val change =
WalletZecFormmatter.toZecStringFull(totalBalance - availableBalance)
val symbol = getString(R.string.symbol)
"(${getString(R.string.home_banner_expecting)} +$change $symbol)".toColoredSpan(
R.color.text_light,
"+$change"
)
}
else -> getString(R.string.home_instruction_enter_amount)
}
}
}
fun setBanner(message: String = "", action: BannerAction = CLEAR) {
with(binding) {
val hasMessage = !message.isEmpty() || action != CLEAR
groupBalance.goneIf(hasMessage)
groupBanner.goneIf(!hasMessage)
//layerLock.goneIf(!hasMessage)
textBannerMessage.text = message
textBannerAction.text = action.action
}
}
//
// Private UI Events
//
private fun onModelUpdated(old: HomeViewModel.UiModel?, new: HomeViewModel.UiModel) {
logUpdate(old, new)
uiModel = new
if (old?.pendingSend != new.pendingSend) {
setSendAmount(new.pendingSend)
}
setProgress(new) // TODO: we may not need to separate anymore
// if (new.status = SYNCING) onSyncing(new) else onSynced(new)
if (new.status == SYNCED) onSynced(new) else onSyncing(new)
setSendEnabled(new.isSendEnabled, new.status == SYNCED)
}
private fun logUpdate(old: HomeViewModel.UiModel?, new: HomeViewModel.UiModel) {
var message = ""
fun maybeComma() = if (message.length > "UiModel(".length) ", " else ""
message = when {
old == null -> "$new"
new == null -> "null"
else -> {
buildString {
append("UiModel(")
if (old.status != new.status) append("status=${new.status}")
if (old.processorInfo != new.processorInfo) {
append("${maybeComma()}processorInfo=ProcessorInfo(")
val startLength = length
fun innerComma() = if (length > startLength) ", " else ""
if (old.processorInfo.networkBlockHeight != new.processorInfo.networkBlockHeight) append(
"networkBlockHeight=${new.processorInfo.networkBlockHeight}"
)
if (old.processorInfo.lastScannedHeight != new.processorInfo.lastScannedHeight) append(
"${innerComma()}lastScannedHeight=${new.processorInfo.lastScannedHeight}"
)
if (old.processorInfo.lastDownloadedHeight != new.processorInfo.lastDownloadedHeight) append(
"${innerComma()}lastDownloadedHeight=${new.processorInfo.lastDownloadedHeight}"
)
if (old.processorInfo.lastDownloadRange != new.processorInfo.lastDownloadRange) append(
"${innerComma()}lastDownloadRange=${new.processorInfo.lastDownloadRange}"
)
if (old.processorInfo.lastScanRange != new.processorInfo.lastScanRange) append(
"${innerComma()}lastScanRange=${new.processorInfo.lastScanRange}"
)
append(")")
}
if (old.saplingBalance?.available != new.saplingBalance?.available) append("${maybeComma()}availableBalance=${new.saplingBalance?.available}")
if (old.saplingBalance?.total != new.saplingBalance?.total) append("${maybeComma()}totalBalance=${new.saplingBalance?.total}")
if (old.pendingSend != new.pendingSend) append("${maybeComma()}pendingSend=${new.pendingSend}")
append(")")
}
}
}
twig("onModelUpdated: $message")
}
private fun onSyncing(uiModel: HomeViewModel.UiModel) {
setAvailable(null, null, null)
}
private fun onSynced(uiModel: HomeViewModel.UiModel) {
snake.isSynced = true
if (!uiModel.hasSaplingBalance) {
onNoFunds()
} else {
setBanner("")
setAvailable(
uiModel.saplingBalance?.available,
uiModel.saplingBalance?.total,
uiModel.transparentBalance?.available,
uiModel.unminedCount
)
}
autoShield(uiModel)
}
private fun autoShield(uiModel: HomeViewModel.UiModel) {
// TODO: Move the preference read to a suspending function
// First time SharedPreferences are hit, it'll perform disk IO
val isAutoshieldingAcknowledged =
Preferences.isAcknowledgedAutoshieldingInformationPrompt.get(requireApplicationContext())
val canAutoshield = AutoShieldFragment.canAutoshield(requireApplicationContext())
if (uiModel.hasAutoshieldFunds && canAutoshield) {
if (!isAutoshieldingAcknowledged) {
mainActivity?.safeNavigate(
HomeFragmentDirections.actionNavHomeToAutoshieldingInfo(
true
)
)
} else {
twig("Autoshielding is available! Let's do this!!!")
mainActivity?.safeNavigate(HomeFragmentDirections.actionNavHomeToNavFundsAvailable())
}
} else {
if (!isAutoshieldingAcknowledged) {
mainActivity?.safeNavigate(
HomeFragmentDirections.actionNavHomeToAutoshieldingInfo(
false
)
)
}
// troubleshooting logs
if ((uiModel.transparentBalance?.available?.value ?: 0) > 0) {
twig(
"Transparent funds are available but not enough to autoshield. Available: ${
uiModel.transparentBalance?.available.convertZatoshiToZecString(
10
)
} Required: ${
Zatoshi(ZcashWalletApp.instance.autoshieldThreshold).convertZatoshiToZecString(
8
)
}"
)
} else if ((uiModel.transparentBalance?.total?.value ?: 0) > 0) {
twig("Transparent funds have been received but they require 10 confirmations for autoshielding.")
} else if (!canAutoshield) {
twig("Could not autoshield probably because the last one occurred too recently")
}
}
}
private fun onSend() {
if (isSendEnabled) mainActivity?.safeNavigate(R.id.action_nav_home_to_send)
}
private fun onBannerAction(action: BannerAction) {
when (action) {
FUND_NOW -> {
MaterialAlertDialogBuilder(requireContext())
.setMessage(R.string.home_dialog_no_balance_message)
.setTitle(R.string.home_dialog_no_balance_title)
.setCancelable(true)
.setPositiveButton(R.string.home_dialog_no_balance_button_positive) { dialog, _ ->
tapped(HOME_FUND_NOW)
dialog.dismiss()
mainActivity?.safeNavigate(R.id.action_nav_home_to_nav_receive)
}
.show()
// MaterialAlertDialogBuilder(activity)
// .setMessage("To make full use of this wallet, deposit funds to your address or tap the faucet to trigger a tiny automatic deposit.\n\nFaucet funds are made available for the community by the community for testing. So please be kind enough to return what you borrow!")
// .setTitle("No Balance")
// .setCancelable(true)
// .setPositiveButton("Tap Faucet") { dialog, _ ->
// dialog.dismiss()
// setBanner("Tapping faucet...", CANCEL)
// }
// .setNegativeButton("View Address") { dialog, _ ->
// dialog.dismiss()
// mainActivity?.safeNavigate(R.id.action_nav_home_to_nav_receive)
// }
// .show()
}
CANCEL -> {
// TODO: trigger banner / balance update
onNoFunds()
}
BannerAction.NONE -> TODO()
CLEAR -> TODO()
}
}
private fun onNoFunds() {
setBanner(getString(R.string.home_no_balance), FUND_NOW)
}
private fun monitorUiModelChanges() {
val existingAmount = sendViewModel.zatoshiAmount ?: Zatoshi(0)
viewModel.initializeMaybe(WalletZecFormmatter.toZecStringFull(existingAmount))
if (existingAmount.value == 0L) onClearAmount()
viewModel.uiModels.runningReduce { old, new ->
onModelUpdated(old, new)
new
}.onCompletion {
twig("uiModel.scanReduce completed.")
}.catch { e ->
twig("exception while processing uiModels $e")
throw e
}.launchIn(resumedScope)
}
//
// Inner classes and extensions
//
enum class BannerAction(val action: String) {
FUND_NOW(""),
CANCEL("Cancel"),
NONE(""),
CLEAR("clear");
companion object {
fun from(action: String?): BannerAction {
values().forEach {
if (it.action == action) return it
}
throw IllegalArgumentException("Invalid BannerAction: $action")
}
}
}
private fun TextView.asKey(): TextView {
val c = text[0]
setOnClickListener {
lifecycleScope.launch {
viewModel.onChar(c)
}
}
return this
}
//
// User Interruptions
//
// TODO: Expand this placeholder logic around when to interrupt the user.
// For now, we just need to get this in the app so that we can BEGIN capturing ECC feedback.
var hasInterrupted = false
private fun canInterruptUser(): Boolean {
// requirements:
// - we want occasional random feedback that does not occur too often
return !hasInterrupted && Math.random() < 0.01
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
}
override fun onStart() {
super.onStart()
twig("HomeFragment.onStart")
}
override fun onPause() {
super.onPause()
}
override fun onStop() {
super.onStop()
}
override fun onDestroyView() {
super.onDestroyView()
}
override fun onDestroy() {
super.onDestroy()
}
override fun onDetach() {
super.onDetach()
}
}

158
app/src/main/java/cash/z/ecc/android/ui/home/HomeViewModel.kt

@ -0,0 +1,158 @@
package cash.z.ecc.android.ui.home
import androidx.lifecycle.ViewModel
import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.di.DependenciesHolder
import cash.z.ecc.android.ext.toAppString
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.Synchronizer.Status.*
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
import cash.z.ecc.android.sdk.db.entity.PendingTransaction
import cash.z.ecc.android.sdk.db.entity.isMined
import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
import cash.z.ecc.android.sdk.ext.ZcashSdk.MINERS_FEE
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.util.twig
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.flow.*
import kotlin.math.roundToInt
// There are deprecations with the use of BroadcastChannel
@kotlinx.coroutines.ObsoleteCoroutinesApi
class HomeViewModel : ViewModel() {
lateinit var uiModels: Flow<UiModel>
lateinit var _typedChars: ConflatedBroadcastChannel<Char>
var initialized = false
fun initializeMaybe(preTypedChars: String = "0") {
twig("init called")
if (initialized) {
twig("Warning already initialized HomeViewModel. Ignoring call to initialize.")
return
}
if (::_typedChars.isInitialized) {
_typedChars.close()
}
_typedChars = ConflatedBroadcastChannel()
val typedChars = _typedChars.asFlow()
val decimal = '.' // R.string.key_decimal.toAppString()[0]
val backspace = R.string.key_backspace.toAppString()[0]
val zec = typedChars.scan(preTypedChars) { acc, c ->
when {
// no-op cases
acc == "0" && c == '0' ||
(c == backspace && acc == "0")
|| (c == decimal && acc.contains(decimal)) -> {
acc
}
c == backspace && acc.length <= 1 -> {
"0"
}
c == backspace -> {
acc.substring(0, acc.length - 1)
}
acc == "0" && c != decimal -> {
c.toString()
}
acc.contains(decimal) && acc.length - acc.indexOf(decimal) > 8 -> {
acc
}
else -> {
"$acc$c"
}
}
}
twig("initializing view models stream")
uiModels = DependenciesHolder.synchronizer.run {
combine(
status,
processorInfo,
orchardBalances,
saplingBalances,
transparentBalances,
zec,
pendingTransactions.distinctUntilChanged()
// unfortunately we have to use an untyped array here rather than typed parameters because combine only supports up to 5 typed params
) { flows ->
val unminedCount = (flows[6] as List<PendingTransaction>).count {
it.isSubmitSuccess() && !it.isMined()
}
UiModel(
status = flows[0] as Synchronizer.Status,
processorInfo = flows[1] as CompactBlockProcessor.ProcessorInfo,
orchardBalance = flows[2] as WalletBalance?,
saplingBalance = flows[3] as WalletBalance?,
transparentBalance = flows[4] as WalletBalance?,
pendingSend = flows[5] as String,
unminedCount = unminedCount
)
}.onStart { emit(UiModel(orchardBalance = null, saplingBalance = null, transparentBalance = null)) }
}.conflate()
}
override fun onCleared() {
super.onCleared()
twig("HomeViewModel cleared!")
}
suspend fun onChar(c: Char) {
_typedChars.send(c)
}
data class UiModel(
val status: Synchronizer.Status = DISCONNECTED,
val processorInfo: CompactBlockProcessor.ProcessorInfo = CompactBlockProcessor.ProcessorInfo(null, null, null, null, null),
val orchardBalance: WalletBalance?,
val saplingBalance: WalletBalance?,
val transparentBalance: WalletBalance?,
val pendingSend: String = "0",
val unminedCount: Int = 0
) {
// Note: the wallet is effectively empty if it cannot cover the miner's fee
val hasFunds: Boolean get() = (saplingBalance?.available?.value ?: 0) > (MINERS_FEE.value.toDouble() / Zatoshi.ZATOSHI_PER_ZEC) // 0.00001
val hasSaplingBalance: Boolean get() = (saplingBalance?.total?.value ?: 0) > 0L
val hasAutoshieldFunds: Boolean get() = (transparentBalance?.available?.value ?: 0) >= ZcashWalletApp.instance.autoshieldThreshold
val isSynced: Boolean get() = status == SYNCED
val isSendEnabled: Boolean get() = isSynced && hasFunds
// Processor Info
val isDownloading = status == DOWNLOADING
val isScanning = status == SCANNING
val isValidating = status == VALIDATING
val isDisconnected = status == DISCONNECTED
val downloadProgress: Int get() {
return processorInfo.run {
if (lastDownloadRange?.isEmpty() == true) {
100
} else {
val progress =
((((lastDownloadedHeight?.value ?: 0) - (lastDownloadRange?.start?.value ?: 0) + 1).coerceAtLeast(0).toFloat() / ((lastDownloadRange?.endInclusive?.value ?: 0) - (lastDownloadRange?.start?.value ?: 0) + 1)) * 100.0f).coerceAtMost(
100.0f
).roundToInt()
progress
}
}
}
val scanProgress: Int get() {
return processorInfo.run {
if (lastScanRange?.isEmpty() == true) {
100
} else {
val progress = ((((lastScannedHeight?.value ?: 0) - (lastScanRange?.start?.value ?: 0) + 1).coerceAtLeast(0).toFloat() / ((lastScanRange?.endInclusive?.value ?: 0) - (lastScanRange?.start?.value ?: 0) + 1)) * 100.0f).coerceAtMost(100.0f).roundToInt()
progress
}
}
}
val totalProgress: Float get() {
val downloadWeighted = 0.40f * (downloadProgress.toFloat() / 100.0f).coerceAtMost(1.0f)
val scanWeighted = 0.60f * (scanProgress.toFloat() / 100.0f).coerceAtMost(1.0f)
return downloadWeighted.coerceAtLeast(0.0f) + scanWeighted.coerceAtLeast(0.0f)
}
}
}

155
app/src/main/java/cash/z/ecc/android/ui/home/MagicSnakeLoader.kt

@ -0,0 +1,155 @@
package cash.z.ecc.android.ui.home
import android.animation.ValueAnimator
import com.airbnb.lottie.LottieAnimationView
class MagicSnakeLoader(
val lottie: LottieAnimationView,
private val scanningStartFrame: Int = 100,
private val scanningEndFrame: Int = 187,
val totalFrames: Int = 200
) : ValueAnimator.AnimatorUpdateListener {
private var isPaused: Boolean = true
private var isStarted: Boolean = false
var isSynced: Boolean = false
set(value) {
if (value && !isStarted) {
lottie.progress = 1.0f
field = value
return
}
// it is started but it hadn't reached the synced state yet
if (value && !field) {
field = value
playToCompletion()
} else {
field = value
}
}
var scanProgress: Int = 0
set(value) {
field = value
if (value > 0) {
startMaybe()
onScanUpdated()
}
}
var downloadProgress: Int = 0
set(value) {
field = value
if (value > 0) {
startMaybe()
} else {
// if (!isSynced) {
// lottie.progress = 0.0f
// if(!isStarted) startMaybe()
// }
}
}
private fun startMaybe() {
if (!isSynced && !isStarted) lottie.postDelayed(
{
// after some delay, if we're still not synced then we better start animating (unless we already are)!
if (!isSynced && isPaused) {
lottie.resumeAnimation()
isPaused = false
isStarted = true
}
},
200L
)
}
private val isDownloading get() = downloadProgress in 1..99
private val isScanning get() = scanProgress in 1..99
init {
lottie.addAnimatorUpdateListener(this)
}
override fun onAnimationUpdate(animation: ValueAnimator) {
if (isSynced || isPaused) {
// playToCompletion()
return
}
// if we are scanning, then set the animation progress, based on the scan progress
// if we're not scanning, then we're looping
animation.currentFrame().let { frame ->
if (isDownloading) allowLoop(frame) else applyScanProgress(frame)
}
}
private val acceptablePauseFrames = arrayOf(33, 34, 67, 68, 99)
private fun applyScanProgress(frame: Int) {
// don't hardcode the progress until the loop animation has completed, cleanly
if (isPaused) {
onScanUpdated()
} else {
// once we're ready to show scan progress, do it! Don't do extra loops.
if (frame >= scanningStartFrame || frame in acceptablePauseFrames) {
pause()
}
}
}
private fun onScanUpdated() {
if (isSynced) {
// playToCompletion()
return
}
if (isPaused && isStarted) {
// move forward within the scan range, proportionate to how much scanning is complete
val scanRange = scanningEndFrame - scanningStartFrame
val scanRangeProgress = scanProgress.toFloat() / 100.0f * scanRange.toFloat()
lottie.progress = (scanningStartFrame.toFloat() + scanRangeProgress) / totalFrames
}
}
private fun playToCompletion() {
removeLoops()
unpause()
}
private fun removeLoops() {
lottie.frame.let { frame ->
if (frame in 33..67) {
lottie.frame = frame + 34
} else if (frame in 0..33) {
lottie.frame = frame + 67
}
}
}
private fun allowLoop(frame: Int) {
unpause()
if (frame >= scanningStartFrame) {
lottie.progress = 0f
}
}
fun unpause() {
if (isPaused) {
lottie.resumeAnimation()
isPaused = false
}
}
fun pause() {
if (!isPaused) {
lottie.pauseAnimation()
isPaused = true
}
}
private fun ValueAnimator.currentFrame(): Int {
return ((animatedValue as Float) * totalFrames).toInt()
}
}

288
app/src/main/java/cash/z/ecc/android/ui/profile/AwesomeFragment.kt

@ -0,0 +1,288 @@
package cash.z.ecc.android.ui.profile
import android.annotation.SuppressLint
import android.os.Bundle
import android.text.SpannableString
import android.text.Spanned
import android.view.LayoutInflater
import android.view.View
import android.widget.Toast
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentAwesomeBinding
import cash.z.ecc.android.ext.distribute
import cash.z.ecc.android.ext.invisibleIf
import cash.z.ecc.android.ext.onClickNavBack
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Tap.*
import cash.z.ecc.android.sdk.db.entity.*
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.util.AddressPartNumberSpan
import cash.z.ecc.android.util.twig
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
class AwesomeFragment : BaseFragment<FragmentAwesomeBinding>() {
override val screen = Report.Screen.AWESOME
private val viewModel: ProfileViewModel by viewModels()
private var lastBalance: WalletBalance? = null
private var initialized: Boolean = false
override fun inflate(inflater: LayoutInflater): FragmentAwesomeBinding =
FragmentAwesomeBinding.inflate(inflater)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.hitAreaExit.onClickNavBack() { tapped(AWESOME_CLOSE) }
binding.hitAreaAddress.setOnClickListener {
tapped(COPY_TRANSPARENT_ADDRESS)
onCopyTransparentAddress()
}
binding.buttonAction.setOnClickListener {
onShieldFundsAction()
}
binding.lottieShielding.visibility = View.GONE
setStatus("Checking balance...")
}
private fun onCopyTransparentAddress() {
resumedScope.launch {
mainActivity?.copyText(viewModel.getTransparentAddress(), "T-Address")
}
}
override fun onResume() {
super.onResume()
if (!initialized) {
resumedScope.launch {
onAddressLoaded(viewModel.getTransparentAddress())
updateBalance()
}
initialized = true
}
}
private fun setStatus(status: String) {
binding.textStatus.text = status
}
@SuppressLint("SetTextI18n")
private fun appendStatus(status: String) {
binding.textStatus.text = "${binding.textStatus.text}$status"
}
private suspend fun updateBalance() {
val utxoCount = viewModel.fetchUtxos()
viewModel.getTransparentBalance().let { balance ->
onBalanceUpdated(balance, utxoCount)
}
}
private fun onAddressLoaded(address: String) {
twig("t-address loaded: $address length: ${address.length}")
// qrecycler.load(address)
// .withQuietZoneSize(3)
// .withCorrectionLevel(QRecycler.CorrectionLevel.MEDIUM)
// .into(binding.receiveQrCode)
address.distribute(2) { i, part ->
setAddressPart(i, part)
}
}
private fun setAddressPart(index: Int, addressPart: String) {
twig("setting t-address for part $index) $addressPart")
val address = when (index) {
0 -> binding.textAddressPart1
1 -> binding.textAddressPart2
else -> throw IllegalArgumentException(
"Unexpected address index $index. Unable to split the t-addr into two parts." +
" Ensure that the address is valid."
)
}
val thinSpace = "\u2005" // 0.25 em space
val textSpan = SpannableString("${index + 1}$thinSpace$addressPart")
textSpan.setSpan(AddressPartNumberSpan(), 0, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
address.text = textSpan
}
private fun onShieldFundsAction() {
if (binding.buttonAction.isActivated) {
tapped(AWESOME_SHIELD)
mainActivity?.let { main ->
main.authenticate(
"Shield transparent funds",
getString(R.string.biometric_backup_phrase_title)
) {
onShieldFunds()
}
}
} else {
Toast.makeText(requireContext(), "No balance to shield!", Toast.LENGTH_SHORT).show()
}
}
private fun onDoneAction() {
viewModel.setEasterEggTriggered()
mainActivity?.safeNavigate(R.id.action_nav_awesome_to_nav_history)
}
private fun onShieldFunds() {
twig("onShieldFunds")
lifecycleScope.launchWhenResumed {
twig("launching shield funds job")
viewModel.shieldFunds().onEach {
onPendingTxUpdated(it)
}.launchIn(lifecycleScope)
}
}
private fun onPendingTxUpdated(tx: PendingTransaction) {
twig("shielding transaction updated: $tx")
if (tx == null) return // TODO: maybe log this
try {
tx.toUiModel().let { model ->
binding.apply {
lottieShielding.invisibleIf(!model.showProgress)
buttonAction.isActivated = !model.showProgress || model.canCancel
buttonAction.isEnabled = true
buttonAction.refreshDrawableState()
setStatus(model.status)
appendStatus(model.details.joinToString("\n", "\n\n"))
buttonAction.apply {
text = model.primaryButtonText
setOnClickListener { model.primaryAction() }
}
}
if (model.updateBalance) {
resumedScope.launch {
delay(1000L)
updateBalance()
}
}
}
} catch (t: Throwable) {
val message = "ERROR: error while handling pending transaction update! $t"
twig(message)
mainActivity?.feedback?.report(Report.Error.NonFatal.TxUpdateFailed(t))
mainActivity?.feedback?.report(t)
}
}
private fun onShieldComplete(isSuccess: Boolean) {
binding.lottieShielding.visibility = View.GONE
if (isSuccess) {
Toast.makeText(mainActivity, "Funds shielded successfully!", Toast.LENGTH_SHORT).show()
binding.buttonAction.isEnabled = true
binding.buttonAction.isActivated = true
binding.buttonAction.text = "See Details"
binding.textStatus.text = "Success!\n\nIt may take a while to show up."
binding.buttonAction.setOnClickListener {
mainActivity?.popBackTo(R.id.nav_home)
}
} else {
Toast.makeText(mainActivity, "Failed to shield funds :(", Toast.LENGTH_SHORT).show()
binding.buttonAction.isEnabled = true
binding.buttonAction.text = "Shield Transparent Funds"
binding.textStatus.text = "Failed!"
binding.buttonAction.visibility = View.GONE
}
}
private fun onBalanceUpdated(
balance: WalletBalance = WalletBalance(Zatoshi(0), Zatoshi(0)),
utxoCount: Int = 0
) {
lastBalance = balance
twig("TRANSPARENT BALANCE: ${balance.available} / ${balance.total}")
binding.textStatus.text = if (balance.available.value > 0L) {
binding.buttonAction.isActivated = true
binding.buttonAction.isEnabled = true
"Balance: ᙇ${balance.available.convertZatoshiToZecString(8)}"
} else {
binding.buttonAction.isActivated = false
binding.buttonAction.isEnabled = true
"No available balance found"
}
if (utxoCount > 0) {
appendStatus("\n\nDownloaded $utxoCount ")
appendStatus(if (utxoCount == 1) "transaction!" else "transactions!")
}
balance.pending.takeIf { it.value > 0 }?.let {
appendStatus("\n\n(ᙇ${it.convertZatoshiToZecString()} pending confirmation)")
}
}
private fun PendingTransaction.toUiModel() = UiModel().also { model ->
when {
isCancelled() -> {
model.status = "Shielding Cancelled!"
model.updateBalance = true
model.primaryAction = { onShieldFundsAction() }
model.details.add("Cancelled!")
}
isSubmitSuccess() -> {
model.status = "Shielding Success!"
model.primaryButtonText = "Done"
model.primaryAction = { onDoneAction() }
}
isFailure() -> {
model.status = if (isFailedEncoding()) {
"${getString(R.string.send_final_error_encoding)}\n\nPlease note:\nShielding requires funds\nto have 10 confirmations."
} else {
"${getString(R.string.send_final_error_submitting)}\n\n${this.errorMessage}"
}
model.primaryAction = { onShieldFundsAction() }
}
else -> {
model.status = "Shielding ᙇ${lastBalance?.available.convertZatoshiToZecString()}\n\nPlease do not exit this screen!"
model.showProgress = true
if (isCreating()) {
model.canCancel = true
model.details.add("Creating transaction...")
model.primaryButtonText = getString(R.string.send_final_button_primary_cancel)
model.primaryAction = { onCancel(this) }
} else {
model.primaryButtonText = "Shielding Funds..."
if (isCreated()) model.details.add("Submitting transaction...")
}
}
}
}
private fun onCancel(tx: PendingTransaction) {
resumedScope.launch {
viewModel.cancel(tx.id)
}
}
// fields are ordered, as they appear, top-to-bottom in the UI because that makes it easier to reason about each screen state
data class UiModel(
var status: String = "",
val details: MutableSet<String> = linkedSetOf(),
var showProgress: Boolean = false,
var primaryButtonText: String = "Shield Transparent Funds",
var primaryAction: () -> Unit = {},
var canCancel: Boolean = false,
var updateBalance: Boolean = false,
)
}

208
app/src/main/java/cash/z/ecc/android/ui/profile/ProfileFragment.kt

@ -0,0 +1,208 @@
package cash.z.ecc.android.ui.profile
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.Toast
import androidx.core.content.FileProvider.getUriForFile
import androidx.fragment.app.viewModels
import androidx.lifecycle.viewModelScope
import cash.z.ecc.android.BuildConfig
import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.databinding.FragmentProfileBinding
import cash.z.ecc.android.ext.*
import cash.z.ecc.android.feedback.FeedbackFile
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Funnel.UserFeedback
import cash.z.ecc.android.feedback.Report.Tap.*
import cash.z.ecc.android.sdk.SdkSynchronizer
import cash.z.ecc.android.sdk.ext.toAbbreviatedAddress
import cash.z.ecc.android.ui.MainActivity
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.util.DebugFileTwig
import cash.z.ecc.android.util.Bush
import cash.z.ecc.android.util.twig
import kotlinx.coroutines.launch
import java.io.File
class ProfileFragment : BaseFragment<FragmentProfileBinding>() {
override val screen = Report.Screen.PROFILE
private val viewModel: ProfileViewModel by viewModels()
override fun inflate(inflater: LayoutInflater): FragmentProfileBinding =
FragmentProfileBinding.inflate(inflater)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.hitAreaSettings.onClickNavTo(R.id.action_nav_profile_to_nav_settings)
binding.hitAreaExit.onClickNavBack() { tapped(PROFILE_CLOSE) }
binding.buttonBackup.setOnClickListener {
tapped(PROFILE_BACKUP)
mainActivity?.let { main ->
main.authenticate(
getString(R.string.biometric_backup_phrase_description),
getString(R.string.biometric_backup_phrase_title)
) {
main.safeNavigate(R.id.action_nav_profile_to_nav_backup)
}
}
}
binding.buttonRescan.setOnClickListener {
tapped(PROFILE_RESCAN)
onRescanWallet()
}
binding.textVersion.text = BuildConfig.VERSION_NAME
onClick(binding.buttonLogs) {
tapped(PROFILE_VIEW_USER_LOGS)
onViewLogs()
}
binding.buttonLogs.setOnLongClickListener {
tapped(PROFILE_VIEW_DEV_LOGS)
onViewDevLogs()
true
}
binding.iconProfile.setOnLongClickListener {
tapped(AWESOME_OPEN)
onEnterAwesomeMode()
true
}
binding.textBannerMessage.setOnClickListener {
openPlayStoreLink()
}
if (viewModel.isEasterEggTriggered()) {
binding.iconProfile.setImageResource(R.drawable.ic_profile_zebra_02)
}
}
private fun openPlayStoreLink() {
getString(R.string.play_store_url).takeUnless { it.isBlank() }?.let { url ->
mainActivity?.onLaunchUrl(url)
}
}
private fun onEnterAwesomeMode() {
(context as? MainActivity)?.safeNavigate(R.id.action_nav_profile_to_nav_awesome)
?: throw IllegalStateException(
"Cannot navigate from this activity. " +
"Expected MainActivity but found ${context?.javaClass?.simpleName}"
)
}
override fun onResume() {
super.onResume()
resumedScope.launch {
binding.textAddress.text = viewModel.getShieldedAddress().toAbbreviatedAddress(12, 12)
}
}
// TODO: reduce these to one function
private fun onFullRescan() {
twig("TMP: onFullRescan: CALLED")
(viewModel.synchronizer as SdkSynchronizer).coroutineScope.launch {
try {
twig("TMP: onFullRescan: START")
viewModel.fullRescan()
Toast.makeText(ZcashWalletApp.instance, "Performing full rescan!", Toast.LENGTH_LONG).show()
mainActivity?.navController?.popBackStack()
} catch (t: Throwable) {
mainActivity?.showCriticalMessage(
"Full Rescan Failed",
"Unable to perform full rescan due to error:\n\n${t.message}"
)
}
}
}
private fun onQuickRescan() {
twig("TMP: onQuickRescan: CALLED")
viewModel.viewModelScope.launch {
try {
twig("TMP: onQuickRescan: START")
viewModel.quickRescan()
Toast.makeText(ZcashWalletApp.instance, "Performing quick rescan!", Toast.LENGTH_LONG).show()
mainActivity?.navController?.popBackStack()
} catch (t: Throwable) {
mainActivity?.showCriticalMessage("Quick Rescan Failed", "Unable to perform quick rescan due to error:\n\n${t.message}")
}
}
}
private fun onWipe() {
mainActivity?.showConfirmation(
"Are you sure?",
"Wiping your data will close the app. Since your seed is preserved, " +
"this operation is probably safe but please backup your seed anyway." +
"\n\nContinue?",
"Wipe"
) {
viewModel.wipe()
mainActivity?.finish()
}
}
private fun onRescanWallet() {
val quickDistance = viewModel.quickScanDistance()
val fullDistance = viewModel.fullScanDistance()
mainActivity?.showRescanWalletDialog(
String.format("%,d", quickDistance),
viewModel.blocksToMinutesString(quickDistance),
String.format("%,d", fullDistance),
viewModel.blocksToMinutesString(fullDistance),
onFullRescan = ::onFullRescan,
onQuickRescan = ::onQuickRescan,
onWipe = ::onWipe
)
}
private fun onViewLogs() {
shareFile(userLogFile())
}
private fun onViewDevLogs() {
developerLogFile().let {
if (it == null) {
mainActivity?.showSnackbar("Error: No developer log found!")
} else {
shareFile(it)
}
}
}
private fun shareFiles(vararg files: File?) {
val uris = arrayListOf<Uri>().apply {
files.filterNotNull().mapNotNull {
getUriForFile(ZcashWalletApp.instance, "${BuildConfig.APPLICATION_ID}.fileprovider", it)
}.forEach {
add(it)
}
}
val intent = Intent(Intent.ACTION_SEND_MULTIPLE).apply {
putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris)
type = "text/*"
}
startActivity(Intent.createChooser(intent, getString(R.string.profile_share_log_title)))
}
fun shareFile(file: File?) {
file ?: return
val uri = getUriForFile(ZcashWalletApp.instance, "${BuildConfig.APPLICATION_ID}.fileprovider", file)
val intent = Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_STREAM, uri)
type = "text/plain"
}
startActivity(Intent.createChooser(intent, getString(R.string.profile_share_log_title)))
}
private fun userLogFile(): File? {
return mainActivity?.feedbackCoordinator?.findObserver<FeedbackFile>()?.file
}
private fun developerLogFile(): File? {
return Bush.trunk.find<DebugFileTwig>()?.file
}
}

164
app/src/main/java/cash/z/ecc/android/ui/profile/ProfileViewModel.kt

@ -0,0 +1,164 @@
package cash.z.ecc.android.ui.profile
import android.widget.Toast
import androidx.lifecycle.ViewModel
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.di.DependenciesHolder
import cash.z.ecc.android.ext.Const
import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.db.entity.PendingTransaction
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.util.twig
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import kotlin.time.DurationUnit
import kotlin.time.toDuration
class ProfileViewModel : ViewModel() {
val synchronizer: Synchronizer = DependenciesHolder.synchronizer
private val lockBox: LockBox = DependenciesHolder.lockBox
private val prefs: LockBox = DependenciesHolder.prefs
// TODO: track this in the app and then fetch. For now, just estimate the blocks per second.
val bps = 40
suspend fun getShieldedAddress(): String = synchronizer.getAddress()
suspend fun getTransparentAddress(): String {
return synchronizer.getTransparentAddress()
}
override fun onCleared() {
super.onCleared()
twig("ProfileViewModel cleared!")
}
suspend fun fetchUtxos(): Int {
val address = getTransparentAddress()
val height: Long = lockBox[Const.Backup.BIRTHDAY_HEIGHT]
?: synchronizer.network.saplingActivationHeight.value
return synchronizer.refreshUtxos(address, BlockHeight.new(synchronizer.network, height))
?: 0
}
suspend fun getTransparentBalance(): WalletBalance {
val address = getTransparentAddress()
return synchronizer.getTransparentBalance(address)
}
fun shieldFunds(): Flow<PendingTransaction> {
return lockBox.getBytes(Const.Backup.SEED)?.let {
val sk = runBlocking { DerivationTool.deriveSpendingKeys(it, synchronizer.network)[0] }
val tsk =
runBlocking { DerivationTool.deriveTransparentSecretKey(it, synchronizer.network) }
val addr = runBlocking {
DerivationTool.deriveTransparentAddressFromPrivateKey(
tsk,
synchronizer.network
)
}
synchronizer.shieldFunds(
sk,
tsk,
"${ZcashSdk.DEFAULT_SHIELD_FUNDS_MEMO_PREFIX}\nAll UTXOs from $addr"
).onEach {
twig("Received shielding txUpdate: ${it?.toString()}")
// updateMetrics(it)
// reportFailures(it)
}
} ?: throw IllegalStateException("Seed was expected but it was not found!")
}
fun setEasterEggTriggered() {
lockBox.setBoolean(Const.Pref.EASTER_EGG_TRIGGERED_SHIELDING, true)
}
fun isEasterEggTriggered(): Boolean {
return lockBox.getBoolean(Const.Pref.EASTER_EGG_TRIGGERED_SHIELDING)
}
suspend fun cancel(id: Long) {
synchronizer.cancelSpend(id)
}
fun wipe() {
synchronizer.stop()
Toast.makeText(
ZcashWalletApp.instance,
"SUCCESS! Wallet data cleared. Please relaunch to rescan!",
Toast.LENGTH_LONG
).show()
runBlocking {
Initializer.erase(
ZcashWalletApp.instance,
ZcashWalletApp.instance.defaultNetwork
)
}
}
suspend fun fullRescan() {
synchronizer.latestBirthdayHeight?.let {
rewindTo(it)
}
}
suspend fun quickRescan() {
synchronizer.latestHeight?.let {
val newHeightValue =
(it.value - 8064L).coerceAtLeast(synchronizer.network.saplingActivationHeight.value)
rewindTo(BlockHeight.new(synchronizer.network, newHeightValue))
}
}
private suspend fun rewindTo(targetHeight: BlockHeight) {
twig("TMP: rewinding to targetHeight $targetHeight")
synchronizer.rewindToNearestHeight(targetHeight, true)
}
fun fullScanDistance(): Long {
synchronizer.latestHeight?.let { latestHeight ->
synchronizer.latestBirthdayHeight?.let { latestBirthdayHeight ->
return (latestHeight.value - latestBirthdayHeight.value).coerceAtLeast(0)
}
}
return 0
}
fun quickScanDistance(): Int {
val latest = synchronizer.latestHeight
val oneWeek = 60 * 60 * 24 / 75 * 7 // a week's worth of blocks
val height = BlockHeight.new(
synchronizer.network,
((latest?.value ?: synchronizer.network.saplingActivationHeight.value) - oneWeek)
.coerceAtLeast(synchronizer.network.saplingActivationHeight.value)
)
val foo = runBlocking {
synchronizer.getNearestRewindHeight(height)
}
return ((latest?.value ?: 0) - foo.value).toInt().coerceAtLeast(0)
}
fun blocksToMinutesString(blocks: BlockHeight): String {
val duration = (blocks.value / bps.toDouble()).toDuration(DurationUnit.SECONDS)
return duration.toString(DurationUnit.MINUTES).replace("m", " minutes")
}
fun blocksToMinutesString(blocks: Int): String {
val duration = (blocks / bps.toDouble()).toDuration(DurationUnit.SECONDS)
return duration.toString(DurationUnit.MINUTES).replace("m", " minutes")
}
fun blocksToMinutesString(blocks: Long): String {
val duration = (blocks / bps.toDouble()).toDuration(DurationUnit.SECONDS)
return duration.toString(DurationUnit.MINUTES).replace("m", " minutes")
}
}

88
app/src/main/java/cash/z/ecc/android/ui/receive/ReceiveTabFragment.kt

@ -0,0 +1,88 @@
package cash.z.ecc.android.ui.receive
import android.content.Context
import android.os.Bundle
import android.text.SpannableString
import android.text.Spanned
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import androidx.fragment.app.viewModels
import cash.z.android.qrecycler.QRecycler
import cash.z.ecc.android.BuildConfig
import cash.z.ecc.android.databinding.FragmentTabReceiveShieldedBinding
import cash.z.ecc.android.ext.distribute
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.util.AddressPartNumberSpan
import cash.z.ecc.android.util.twig
import kotlinx.coroutines.launch
class ReceiveTabFragment :
BaseFragment<FragmentTabReceiveShieldedBinding>() {
override val screen = Report.Screen.RECEIVE
private val viewModel: ReceiveViewModel by viewModels()
lateinit var qrecycler: QRecycler
lateinit var addressParts: Array<TextView>
override fun inflate(inflater: LayoutInflater): FragmentTabReceiveShieldedBinding =
FragmentTabReceiveShieldedBinding.inflate(inflater)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
addressParts = arrayOf(
binding.textAddressPart1,
binding.textAddressPart2,
binding.textAddressPart3,
binding.textAddressPart4,
binding.textAddressPart5,
binding.textAddressPart6,
binding.textAddressPart7,
binding.textAddressPart8
)
binding.iconQrLogo.setOnLongClickListener {
mainActivity?.takeIf { BuildConfig.FLAVOR.lowercase().contains("testnet") }?.let {
it.copyAddress(null)
it.onLaunchUrl("https://faucet.testnet.z.cash/")
true
} ?: false
}
}
override fun onAttach(context: Context) {
qrecycler = QRecycler() // inject! :)
super.onAttach(context)
}
override fun onResume() {
super.onResume()
resumedScope.launch {
onAddressLoaded(viewModel.getAddress())
}
}
private fun onAddressLoaded(address: String) {
twig("address loaded: $address length: ${address.length}")
qrecycler.load(address)
.withQuietZoneSize(3)
.withCorrectionLevel(QRecycler.CorrectionLevel.MEDIUM)
.into(binding.receiveQrCode)
address.distribute(8) { i, part ->
setAddressPart(i, part)
}
}
private fun setAddressPart(index: Int, addressPart: String) {
twig("setting address for part $index) $addressPart")
val thinSpace = "\u2005" // 0.25 em space
val textSpan = SpannableString("${index + 1}$thinSpace$addressPart")
textSpan.setSpan(AddressPartNumberSpan(), 0, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
addressParts[index].text = textSpan
}
}

18
app/src/main/java/cash/z/ecc/android/ui/receive/ReceiveViewModel.kt

@ -0,0 +1,18 @@
package cash.z.ecc.android.ui.receive
import androidx.lifecycle.ViewModel
import cash.z.ecc.android.di.DependenciesHolder
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.util.twig
class ReceiveViewModel : ViewModel() {
private val synchronizer: Synchronizer = DependenciesHolder.synchronizer
suspend fun getAddress(): String = synchronizer.getAddress()
override fun onCleared() {
super.onCleared()
twig("ReceiveViewModel cleared!")
}
}

66
app/src/main/java/cash/z/ecc/android/ui/scan/QrAnalyzer.kt

@ -0,0 +1,66 @@
package cash.z.ecc.android.ui.scan
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import cash.z.ecc.android.util.twig
import com.google.zxing.BinaryBitmap
import com.google.zxing.NotFoundException
import com.google.zxing.PlanarYUVLuminanceSource
import com.google.zxing.Reader
import com.google.zxing.common.HybridBinarizer
import com.google.zxing.qrcode.QRCodeReader
class QrAnalyzer(val scanCallback: (qrContent: String, image: ImageProxy) -> Unit) :
ImageAnalysis.Analyzer {
private val reader = QRCodeReader()
override fun analyze(image: ImageProxy) {
image.toBinaryBitmap().let { bitmap ->
val qrContent = bitmap.decodeWith(reader) ?: bitmap.flip().decodeWith(reader)
if (qrContent == null) {
image.close()
} else {
onImageScan(qrContent, image)
}
}
}
private fun ImageProxy.toBinaryBitmap(): BinaryBitmap {
return planes[0].buffer.let { buffer ->
ByteArray(buffer.remaining()).also { buffer.get(it) }
}.let { bytes ->
PlanarYUVLuminanceSource(bytes, width, height, 0, 0, width, height, false)
}.let { source ->
BinaryBitmap(HybridBinarizer(source))
}
}
private fun BinaryBitmap.decodeWith(reader: Reader): String? {
return try {
reader.decode(this).toString()
} catch (e: NotFoundException) {
// these happen frequently. Whenever no QR code is found in the frame. No need to log.
null
} catch (e: Throwable) {
twig("Error while scanning QR: $e")
twig(e)
null
}
}
private fun BinaryBitmap.flip(): BinaryBitmap {
blackMatrix.apply {
repeat(width) { w ->
repeat(height) { h ->
flip(w, h)
}
}
}
return this
}
private fun onImageScan(result: String, image: ImageProxy) {
scanCallback(result, image)
}
}

216
app/src/main/java/cash/z/ecc/android/ui/scan/ScanFragment.kt

@ -0,0 +1,216 @@
package cash.z.ecc.android.ui.scan
import android.content.Context
import android.content.pm.PackageManager
import android.os.Bundle
import android.util.DisplayMetrics
import android.view.LayoutInflater
import android.view.View
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentScanBinding
import cash.z.ecc.android.ext.onClickNavBack
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Tap.SCAN_BACK
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.send.SendViewModel
import cash.z.ecc.android.util.twig
import com.google.common.util.concurrent.ListenableFuture
import kotlinx.coroutines.launch
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
class ScanFragment : BaseFragment<FragmentScanBinding>() {
override val screen = Report.Screen.SCAN
private val viewModel: ScanViewModel by viewModels()
private val sendViewModel: SendViewModel by activityViewModels()
private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>
private var cameraExecutor: ExecutorService? = null
override fun inflate(inflater: LayoutInflater): FragmentScanBinding =
FragmentScanBinding.inflate(inflater)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (cameraExecutor != null) cameraExecutor?.shutdown()
cameraExecutor = Executors.newSingleThreadExecutor()
binding.backButtonHitArea.onClickNavBack() { tapped(SCAN_BACK) }
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (!allPermissionsGranted()) getRuntimePermissions()
}
override fun onAttach(context: Context) {
super.onAttach(context)
cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener(
Runnable {
bindPreview(cameraProviderFuture.get())
},
ContextCompat.getMainExecutor(context)
)
}
override fun onDestroyView() {
super.onDestroyView()
cameraExecutor?.shutdown()
cameraExecutor = null
}
private fun bindPreview(cameraProvider: ProcessCameraProvider) {
// Most of the code here is adapted from: https://github.com/android/camera-samples/blob/master/CameraXBasic/app/src/main/java/com/android/example/cameraxbasic/fragments/CameraFragment.kt
// it's worth keeping tabs on that implementation because they keep making breaking changes to these APIs!
// Get screen metrics used to setup camera for full screen resolution
val metrics = DisplayMetrics().also { binding.preview.display.getRealMetrics(it) }
val screenAspectRatio = aspectRatio(metrics.widthPixels, metrics.heightPixels)
val rotation = binding.preview.display.rotation
val preview =
Preview.Builder().setTargetName("Preview").setTargetAspectRatio(screenAspectRatio)
.setTargetRotation(rotation).build()
val cameraSelector = CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build()
val imageAnalysis = ImageAnalysis.Builder().setTargetAspectRatio(screenAspectRatio)
.setTargetRotation(rotation)
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
imageAnalysis.setAnalyzer(
cameraExecutor!!,
QrAnalyzer { q, i ->
onQrScanned(q, i)
}
)
// Must unbind the use-cases before rebinding them
cameraProvider.unbindAll()
try {
cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageAnalysis)
preview.setSurfaceProvider(binding.preview.surfaceProvider)
} catch (t: Throwable) {
// TODO: consider bubbling this up to the user
mainActivity?.feedback?.report(t)
twig("Error while opening the camera: $t")
}
}
/**
* Adapted from: https://github.com/android/camera-samples/blob/master/CameraXBasic/app/src/main/java/com/android/example/cameraxbasic/fragments/CameraFragment.kt#L350
*/
private fun aspectRatio(width: Int, height: Int): Int {
val previewRatio = kotlin.math.max(width, height).toDouble() / kotlin.math.min(
width,
height
)
if (kotlin.math.abs(previewRatio - (4.0 / 3.0))
<= kotlin.math.abs(previewRatio - (16.0 / 9.0))
) {
return AspectRatio.RATIO_4_3
}
return AspectRatio.RATIO_16_9
}
private fun onQrScanned(qrContent: String, image: ImageProxy) {
resumedScope.launch {
val parsed = viewModel.parse(qrContent)
if (parsed == null) {
val network = viewModel.networkName
binding.textScanError.text = getString(R.string.scan_invalid_address, network, qrContent)
image.close()
} else { /* continue scanning*/
binding.textScanError.text = ""
sendViewModel.toAddress = parsed
mainActivity?.safeNavigate(R.id.action_nav_scan_to_nav_send)
}
}
}
// private fun updateOverlay(detectedObjects: DetectedObjects) {
// if (detectedObjects.objects.isEmpty()) {
// return
// }
//
// overlay.setSize(detectedObjects.imageWidth, detectedObjects.imageHeight)
// val list = mutableListOf<BoxData>()
// for (obj in detectedObjects.objects) {
// val box = obj.boundingBox
// val name = "${categoryNames[obj.classificationCategory]}"
// val confidence =
// if (obj.classificationCategory != FirebaseVisionObject.CATEGORY_UNKNOWN) {
// val confidence: Int = obj.classificationConfidence!!.times(100).toInt()
// "$confidence%"
// } else {
// ""
// }
// list.add(BoxData("$name $confidence", box))
// }
// overlay.set(list)
// }
//
// Permissions
//
private val requiredPermissions: Array<String?>
get() {
return try {
val info = mainActivity?.packageManager
?.getPackageInfo(mainActivity?.packageName ?: "", PackageManager.GET_PERMISSIONS)
val ps = info?.requestedPermissions
if (ps != null && ps.isNotEmpty()) {
ps
} else {
arrayOfNulls(0)
}
} catch (e: Exception) {
arrayOfNulls(0)
}
}
private fun allPermissionsGranted(): Boolean {
for (permission in requiredPermissions) {
if (!isPermissionGranted(mainActivity!!, permission!!)) {
return false
}
}
return true
}
private fun getRuntimePermissions() {
val allNeededPermissions = arrayListOf<String>()
for (permission in requiredPermissions) {
if (!isPermissionGranted(mainActivity!!, permission!!)) {
allNeededPermissions.add(permission)
}
}
if (allNeededPermissions.isNotEmpty()) {
requestPermissions(allNeededPermissions.toTypedArray(), CAMERA_PERMISSION_REQUEST)
}
}
companion object {
private const val CAMERA_PERMISSION_REQUEST = 1002
private fun isPermissionGranted(context: Context, permission: String): Boolean {
return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
}
}
}

29
app/src/main/java/cash/z/ecc/android/ui/scan/ScanViewModel.kt

@ -0,0 +1,29 @@
package cash.z.ecc.android.ui.scan
import androidx.lifecycle.ViewModel
import cash.z.ecc.android.di.DependenciesHolder
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.util.twig
class ScanViewModel : ViewModel() {
private val synchronizer: Synchronizer = DependenciesHolder.synchronizer
val networkName get() = synchronizer.network.networkName
suspend fun parse(qrCode: String): String? {
// temporary parse code to allow both plain addresses and those that start with zcash:
// TODO: replace with more robust ZIP-321 handling of QR codes
val address = if (qrCode.startsWith("zcash:")) {
qrCode.substring(6, qrCode.indexOf("?").takeUnless { it == -1 } ?: qrCode.length)
} else {
qrCode
}
return if (synchronizer.validateAddress(address).isNotValid) null else address
}
override fun onCleared() {
super.onCleared()
twig("${javaClass.simpleName} cleared!")
}
}

236
app/src/main/java/cash/z/ecc/android/ui/send/AutoShieldFragment.kt

@ -0,0 +1,236 @@
package cash.z.ecc.android.ui.send
import android.content.Context
import android.os.Bundle
import android.text.format.DateUtils
import android.view.LayoutInflater
import android.view.View
import android.widget.Toast
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentAutoShieldBinding
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ext.invisibleIf
import cash.z.ecc.android.ext.requireApplicationContext
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.preference.Preferences
import cash.z.ecc.android.preference.model.get
import cash.z.ecc.android.preference.model.put
import cash.z.ecc.android.sdk.db.entity.*
import cash.z.ecc.android.sdk.ext.collectWith
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.util.twig
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import java.time.Clock
class AutoShieldFragment : BaseFragment<FragmentAutoShieldBinding>() {
override val screen = Report.Screen.AUTO_SHIELD_FINAL
private val viewModel: AutoShieldViewModel by viewModels()
private val uiModels = MutableStateFlow(UiModel())
override fun inflate(inflater: LayoutInflater): FragmentAutoShieldBinding =
FragmentAutoShieldBinding.inflate(inflater)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (null == savedInstanceState) {
setAutoshield(requireApplicationContext())
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.backButtonHitArea.setOnClickListener {
onExit().also { tapped(Report.Tap.AUTO_SHIELD_FINAL_CLOSE) }
}
mainActivity?.preventBackPress(this)
uiModels.collectWith(lifecycleScope, ::updateUi)
}
override fun onAttach(context: Context) {
super.onAttach(context)
mainActivity?.apply {
viewModel.shieldFunds().onEach { p: PendingTransaction ->
try {
uiModels.value = p.toUiModel()
} catch (t: Throwable) {
val message = "ERROR: error while handling pending transaction update! $t"
twig(message)
mainActivity?.feedback?.report(Report.Error.NonFatal.TxUpdateFailed(t))
mainActivity?.feedback?.report(t)
}
}.launchIn(lifecycleScope)
}
}
private fun updateUi(uiModel: UiModel) = uiModel.apply {
if (isResumed) {
// if this is the first success
if (!binding.lottieSuccess.isVisible && showSuccess) {
mainActivity?.vibrateSuccess()
}
binding.backButton.goneIf(!showCloseIcon)
binding.textTitle.text = title
binding.lottieShielding.invisibleIf(!showShielding)
if (pauseShielding) binding.lottieShielding.pauseAnimation()
binding.lottieSuccess.invisibleIf(!showSuccess)
binding.imageFailed.invisibleIf(!isFailure)
binding.textStatus.text = statusMessage
binding.textStatus.text = when {
showStatusDetails && showStatusMessage -> statusDetails
showStatusDetails -> statusDetails
showStatusMessage -> statusMessage
else -> ""
}
binding.buttonPrimary.text = primaryButtonText
binding.buttonPrimary.setOnClickListener { primaryAction() }
binding.buttonMoreInfo.text = moreInfoButtonText
binding.buttonMoreInfo.goneIf(!showMoreInfoButton)
binding.buttonMoreInfo.setOnClickListener { moreInfoAction() }
if (showSuccess) {
if (viewModel.updateAutoshieldAchievement()) {
mainActivity?.showSnackbar("Achievement unlocked! Golden Zebra.", "View") {
mainActivity?.safeNavigate(R.id.action_nav_shield_final_to_profile)
Toast.makeText(mainActivity, "Your Zebra is now yellow because you are great", Toast.LENGTH_LONG).show()
}
}
}
}
}
private fun onExit() {
mainActivity?.safeNavigate(R.id.action_nav_shield_final_to_nav_home)
}
private fun onCancel(tx: PendingTransaction) {
viewModel.cancel(tx.id)
}
private fun onSeeDetails() {
mainActivity?.safeNavigate(R.id.action_nav_shield_final_to_nav_history)
}
private fun PendingTransaction.toUiModel() = UiModel().also { model ->
when {
isCancelled() -> {
model.title = getString(R.string.send_final_result_cancelled)
model.pauseShielding = true
model.primaryButtonText = getString(R.string.send_final_button_primary_back)
model.primaryAction = { mainActivity?.navController?.popBackStack() }
}
isSubmitSuccess() -> {
model.showCloseIcon = true
model.title = getString(R.string.send_final_button_primary_sent)
model.showShielding = false
model.showSuccess = true
model.primaryButtonText = getString(R.string.done)
model.primaryAction = ::onExit
model.showMoreInfoButton = true
model.moreInfoButtonText = getString(R.string.send_final_button_primary_details)
model.moreInfoAction = ::onSeeDetails
}
isFailure() -> {
model.showCloseIcon = true
model.title =
if (isFailedEncoding()) getString(R.string.send_final_error_encoding) else getString(
R.string.send_final_error_submitting
)
model.showShielding = false
model.showSuccess = false
model.isFailure = true
model.showStatusDetails = false
model.primaryButtonText = getString(R.string.translated_button_back)
model.primaryAction = { mainActivity?.navController?.popBackStack() }
model.showMoreInfoButton = errorMessage != null
model.moreInfoButtonText = getString(R.string.send_more_info)
model.moreInfoAction = {
showMoreInfo(errorMessage ?: "No details available")
}
}
isCreating() -> {
model.showCloseIcon = false
model.primaryButtonText = getString(R.string.send_final_button_primary_cancel)
model.showStatusMessage = true
model.statusMessage = "Creating transaction..."
model.primaryAction = { onCancel(this) }
}
isCreated() -> {
model.showStatusMessage = true
model.statusMessage = "Submitting transaction..."
model.primaryButtonText = getString(R.string.translated_button_back)
model.primaryAction = { mainActivity?.navController?.popBackStack() }
}
else -> {
model.primaryButtonText = getString(R.string.translated_button_back)
model.primaryAction = { mainActivity?.navController?.popBackStack() }
}
}
}
private fun showMoreInfo(info: String) {
val current = uiModels.value
uiModels.value = current.copy(
showMoreInfoButton = true,
moreInfoButtonText = getString(R.string.done),
moreInfoAction = ::onExit,
showStatusMessage = false,
showStatusDetails = true,
statusDetails = info
)
}
// fields are ordered, as they appear, top-to-bottom in the UI because that makes it easier to reason about each screen state
data class UiModel(
var showCloseIcon: Boolean = false,
var title: String = "Shielding Now!",
var showShielding: Boolean = true,
var pauseShielding: Boolean = false,
var showSuccess: Boolean = false,
var isFailure: Boolean = false,
var statusMessage: String = "",
var statusDetails: String = "",
var showStatusDetails: Boolean = false,
var showStatusMessage: Boolean = true,
var primaryButtonText: String = "Cancel",
var primaryAction: () -> Unit = {},
var moreInfoButtonText: String = "",
var showMoreInfoButton: Boolean = false,
var moreInfoAction: () -> Unit = {},
)
companion object {
private const val maxAutoshieldFrequency: Long = 30 * DateUtils.MINUTE_IN_MILLIS
/**
* @param clock Optionally allows injecting a clock, in order to make this testable.
*/
fun canAutoshield(context: Context, clock: Clock = Clock.systemUTC()): Boolean {
val currentEpochMillis = clock.millis()
val lastAutoshieldEpochMillis = Preferences.lastAutoshieldingEpochMillis.get(context)
val isLastAutoshieldOld = (currentEpochMillis - lastAutoshieldEpochMillis) > maxAutoshieldFrequency
// Prevent a corner case where a user with a clock in the future during one autoshielding prompt
// could prevent all subsequent autoshielding prompts.
val isTimeTraveling = lastAutoshieldEpochMillis > currentEpochMillis
return isLastAutoshieldOld || isTimeTraveling
}
/**
* @param clock Optionally allows injecting a clock, in order to make this testable.
*/
private fun setAutoshield(context: Context, clock: Clock = Clock.systemUTC()) =
Preferences.lastAutoshieldingEpochMillis.put(context, clock.millis())
}
}

158
app/src/main/java/cash/z/ecc/android/ui/send/AutoShieldViewModel.kt

@ -0,0 +1,158 @@
package cash.z.ecc.android.ui.send
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import cash.z.ecc.android.di.DependenciesHolder
import cash.z.ecc.android.ext.Const
import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.db.entity.PendingTransaction
import cash.z.ecc.android.sdk.db.entity.isMined
import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.util.twig
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combineTransform
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
class AutoShieldViewModel : ViewModel() {
private val synchronizer: Synchronizer = DependenciesHolder.synchronizer
private val lockBox: LockBox = DependenciesHolder.lockBox
var latestBalance: BalanceModel? = null
val balances
get() = combineTransform(
synchronizer.orchardBalances,
synchronizer.saplingBalances,
synchronizer.transparentBalances,
) { o, s, t ->
BalanceModel(o, s, t).let {
latestBalance = it
emit(it)
}
}
val statuses
get() = combineTransform(
synchronizer.saplingBalances,
synchronizer.pendingTransactions,
synchronizer.processorInfo
) { balance, pending, info ->
val unconfirmed = pending.filter { !it.isConfirmed(info.networkBlockHeight) }
val unmined = pending.filter { it.isSubmitSuccess() && !it.isMined() }
val pending = balance?.pending?.value ?: 0
emit(StatusModel(unmined, unconfirmed, pending, info.networkBlockHeight))
}
private fun PendingTransaction.isConfirmed(networkBlockHeight: BlockHeight?): Boolean {
return networkBlockHeight?.let { height ->
isMined() && (height.value - minedHeight + 1) > 10
} ?: false
}
fun cancel(id: Long) {
viewModelScope.launch {
synchronizer.cancelSpend(id)
}
}
/**
* Update the autoshielding achievement and return true if this is the first time.
*/
fun updateAutoshieldAchievement(): Boolean {
val existingValue = lockBox.getBoolean(Const.Pref.EASTER_EGG_TRIGGERED_SHIELDING)
return if (!existingValue) {
lockBox.setBoolean(Const.Pref.EASTER_EGG_TRIGGERED_SHIELDING, true)
true
} else {
false
}
}
fun shieldFunds(): Flow<PendingTransaction> {
return lockBox.getBytes(Const.Backup.SEED)?.let {
val sk = runBlocking { DerivationTool.deriveSpendingKeys(it, synchronizer.network)[0] }
val tsk = runBlocking {
DerivationTool.deriveTransparentSecretKey(
it,
synchronizer.network
)
}
val addr = runBlocking {
DerivationTool.deriveTransparentAddressFromPrivateKey(
tsk,
synchronizer.network
)
}
synchronizer.shieldFunds(
sk,
tsk,
"${ZcashSdk.DEFAULT_SHIELD_FUNDS_MEMO_PREFIX}\nAll UTXOs from $addr"
).onEach { tx ->
twig("Received shielding txUpdate: ${tx?.toString()}")
// updateMetrics(it)
// reportFailures(it)
}
} ?: throw IllegalStateException("Seed was expected but it was not found!")
}
data class BalanceModel(
val orchardBalance: WalletBalance?,
val saplingBalance: WalletBalance?,
val transparentBalance: WalletBalance?,
) {
val balanceShielded: String = saplingBalance?.available.toDisplay()
val balanceTransparent: String = transparentBalance?.available.toDisplay()
val balanceTotal: String =
((saplingBalance?.available ?: Zatoshi(0)) + (transparentBalance?.available
?: Zatoshi(0))).toDisplay()
val canAutoShield: Boolean = (transparentBalance?.available?.value ?: 0) > 0L
val maxLength =
maxOf(balanceShielded.length, balanceTransparent.length, balanceTotal.length)
val paddedShielded = pad(balanceShielded)
val paddedTransparent = pad(balanceTransparent)
val paddedTotal = pad(balanceTotal)
private fun Zatoshi?.toDisplay(): String {
return convertZatoshiToZecString(8, 8)
}
private fun pad(balance: String): String {
var diffLength = maxLength - balance.length
return buildString {
repeat(diffLength) {
append(' ')
}
append(balance)
}
}
}
data class StatusModel(
val pendingUnconfirmed: List<PendingTransaction> = listOf(),
val pendingUnmined: List<PendingTransaction> = listOf(),
val pendingBalance: Long = 0L,
val latestHeight: BlockHeight? = null,
) {
val hasUnconfirmed = pendingUnconfirmed.isNotEmpty()
val hasUnmined = pendingUnmined.isNotEmpty()
val hasPendingBalance = pendingBalance > 0L
fun remainingConfirmations(latestHeight: Int, confirmationsRequired: Int = 10) =
pendingUnconfirmed
.map { confirmationsRequired - (latestHeight - it.minedHeight + 1) }
.filter { it > 0 }
.sortedDescending()
}
}

37
app/src/main/java/cash/z/ecc/android/ui/send/FundsAvailableFragment.kt

@ -0,0 +1,37 @@
package cash.z.ecc.android.ui.send
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentFundsAvailableBinding
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.ui.base.BaseFragment
class FundsAvailableFragment : BaseFragment<FragmentFundsAvailableBinding>() {
override val screen = Report.Screen.AUTO_SHIELD_AVAILABLE
override fun inflate(inflater: LayoutInflater): FragmentFundsAvailableBinding =
FragmentFundsAvailableBinding.inflate(inflater)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.buttonAction.setOnClickListener {
onProceedWithAutoshielding()
}
}
/**
* This function probably serves no purpose other than to click through to the next screen
*/
private fun onProceedWithAutoshielding() {
mainActivity?.let { main ->
main.authenticate(
"Shield transparent funds",
getString(R.string.biometric_backup_phrase_title)
) {
main.safeNavigate(R.id.action_nav_funds_available_to_nav_shield_final)
}
}
}
}

169
app/src/main/java/cash/z/ecc/android/ui/send/SendFinalFragment.kt

@ -0,0 +1,169 @@
package cash.z.ecc.android.ui.send
import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import androidx.fragment.app.activityViewModels
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentSendFinalBinding
import cash.z.ecc.android.ext.WalletZecFormmatter
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Tap.SEND_FINAL_CLOSE
import cash.z.ecc.android.sdk.SdkSynchronizer
import cash.z.ecc.android.sdk.db.entity.*
import cash.z.ecc.android.sdk.ext.toAbbreviatedAddress
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.util.twig
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
class SendFinalFragment : BaseFragment<FragmentSendFinalBinding>() {
override val screen = Report.Screen.SEND_FINAL
private val sendViewModel: SendViewModel by activityViewModels()
override fun inflate(inflater: LayoutInflater): FragmentSendFinalBinding =
FragmentSendFinalBinding.inflate(inflater)
@SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.buttonPrimary.setOnClickListener {
onReturnToSend()
}
binding.backButtonHitArea.setOnClickListener {
onExit().also { tapped(SEND_FINAL_CLOSE) }
}
binding.textConfirmation.text =
"${getString(R.string.send_final_sending)} ${WalletZecFormmatter.toZecStringFull(sendViewModel.zatoshiAmount)} ${getString(R.string.symbol)}\n${getString(R.string.send_final_to)}\n${sendViewModel.toAddress.toAbbreviatedAddress()}"
mainActivity?.preventBackPress(this)
}
override fun onAttach(context: Context) {
super.onAttach(context)
mainActivity?.apply {
sendViewModel.send().onEach {
onPendingTxUpdated(it)
}.launchIn((sendViewModel.synchronizer as SdkSynchronizer).coroutineScope)
}
}
private fun onPendingTxUpdated(tx: PendingTransaction?) {
if (tx == null || !isResumed) return // TODO: maybe log this
try {
tx.toUiModel().let { model ->
updateUi(model)
}
// only hold onto the view model if the transaction failed so that the user can retry
if (tx.isSubmitSuccess()) {
sendViewModel.reset()
// celebrate
mainActivity?.vibrate(0, 100, 100, 200, 200, 400)
}
} catch (t: Throwable) {
val message = "ERROR: error while handling pending transaction update! $t"
twig(message)
mainActivity?.feedback?.report(Report.Error.NonFatal.TxUpdateFailed(t))
mainActivity?.feedback?.report(t)
}
}
private fun onExit() {
sendViewModel.reset()
mainActivity?.safeNavigate(R.id.action_nav_send_final_to_nav_home)
}
private fun onCancel(tx: PendingTransaction) {
sendViewModel.cancel(tx.id)
}
private fun onReturnToSend() {
mainActivity?.safeNavigate(R.id.action_nav_send_final_to_nav_send)
}
private fun onSeeDetails() {
sendViewModel.reset()
mainActivity?.safeNavigate(R.id.action_nav_send_final_to_nav_history)
}
private fun updateUi(model: UiModel) {
binding.apply {
backButton.goneIf(!model.showCloseIcon)
backButtonHitArea.goneIf(!model.showCloseIcon)
textConfirmation.text = model.title
lottieSending.goneIf(!model.showProgress)
if (!model.showProgress) lottieSending.pauseAnimation() else lottieSending.playAnimation()
errorMessage.text = model.errorMessage
buttonPrimary.apply {
text = model.primaryButtonText
setOnClickListener { model.primaryAction() }
}
buttonMoreInfo.apply {
goneIf(!model.showSecondaryButton)
text = getString(R.string.send_more_info)
setOnClickListener {
binding.textMoreInfo.text = model.errorDescription
text = getString(R.string.done)
setOnClickListener { onExit() }
}
}
}
}
private fun PendingTransaction.toUiModel() = UiModel().also { model ->
when {
isCancelled() -> {
model.title = getString(R.string.send_final_result_cancelled)
model.primaryButtonText = getString(R.string.send_final_button_primary_back)
model.primaryAction = { onReturnToSend() }
}
isSubmitSuccess() -> {
model.title = getString(R.string.send_final_button_primary_sent)
model.primaryButtonText = getString(R.string.send_final_button_primary_details)
model.primaryAction = { onSeeDetails() }
}
isFailure() -> {
model.title = getString(R.string.send_final_button_primary_failed)
model.errorMessage = if (isFailedEncoding()) getString(R.string.send_final_error_encoding) else getString(
R.string.send_final_error_submitting
)
model.errorDescription = errorMessage.toString()
model.primaryButtonText = getString(R.string.send_final_button_primary_retry)
model.primaryAction = { onReturnToSend() }
model.showSecondaryButton = true
}
else -> {
model.title = "${getString(R.string.send_final_sending)} ${WalletZecFormmatter.toZecStringFull(
Zatoshi(value))} ${getString(R.string.symbol)} ${getString(R.string.send_final_to)}\n${toAddress.toAbbreviatedAddress()}"
model.showProgress = true
if (isCreating()) {
model.showCloseIcon = false
model.primaryButtonText = getString(R.string.send_final_button_primary_cancel)
model.primaryAction = { onCancel(this) }
} else {
model.primaryButtonText = getString(R.string.send_final_button_primary_details)
model.primaryAction = { onSeeDetails() }
}
}
}
}
// fields are ordered, as they appear, top-to-bottom in the UI because that makes it easier to reason about each screen state
data class UiModel(
var showCloseIcon: Boolean = true,
var title: String = "",
var errorDescription: String = "",
var showProgress: Boolean = false,
var errorMessage: String = "",
var primaryButtonText: String = "See Details",
var primaryAction: () -> Unit = {},
var showSecondaryButton: Boolean = false,
)
}

373
app/src/main/java/cash/z/ecc/android/ui/send/SendFragment.kt

@ -0,0 +1,373 @@
package cash.z.ecc.android.ui.send
import android.content.ClipboardManager
import android.content.Context
import android.content.res.ColorStateList
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import androidx.constraintlayout.widget.Group
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.widget.ImageViewCompat
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewModelScope
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentSendBinding
import cash.z.ecc.android.ext.*
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Tap.*
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.ext.collectWith
import cash.z.ecc.android.sdk.ext.onFirstWith
import cash.z.ecc.android.sdk.ext.toAbbreviatedAddress
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.type.AddressType
import cash.z.ecc.android.ui.base.BaseFragment
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
class SendFragment :
BaseFragment<FragmentSendBinding>(),
ClipboardManager.OnPrimaryClipChangedListener {
override val screen = Report.Screen.SEND_ADDRESS
private var maxZatoshi: Long? = null
private var availableZatoshi: Long? = null
val sendViewModel: SendViewModel by activityViewModels()
override fun inflate(inflater: LayoutInflater): FragmentSendBinding =
FragmentSendBinding.inflate(inflater)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Apply View Model
applyViewModel(sendViewModel)
updateAddressUi(false)
// Apply behaviors
binding.buttonSend.setOnClickListener {
onSubmit().also { tapped(SEND_SUBMIT) }
}
binding.checkIncludeAddress.setOnCheckedChangeListener { _, _ ->
onIncludeMemo(binding.checkIncludeAddress.isChecked)
}
binding.inputZcashAddress.apply {
doAfterTextChanged {
val textStr = text.toString()
val trim = textStr.trim()
// bugfix: prevent cursor from moving while backspacing and deleting whitespace
if (text.toString() != trim) {
setText(trim)
setSelection(selectionEnd - (textStr.length - trim.length))
}
onAddressChanged(trim)
}
}
binding.backButtonHitArea.onClickNavUp { tapped(SEND_ADDRESS_BACK) }
//
// binding.clearMemo.setOnClickListener {
// onClearMemo().also { tapped(SEND_MEMO_CLEAR) }
// }
binding.inputZcashMemo.doAfterTextChanged {
sendViewModel.memo = binding.inputZcashMemo.text?.toString() ?: ""
onMemoUpdated()
}
binding.textLayoutAddress.setEndIconOnClickListener {
mainActivity?.maybeOpenScan().also { tapped(SEND_ADDRESS_SCAN) }
}
// banners
binding.backgroundClipboard.setOnClickListener {
onPaste().also { tapped(SEND_ADDRESS_PASTE) }
}
binding.containerClipboard.setOnClickListener {
onPaste().also { tapped(SEND_ADDRESS_PASTE) }
}
binding.backgroundLastUsed.setOnClickListener {
onReuse().also { tapped(SEND_ADDRESS_REUSE) }
}
binding.containerLastUsed.setOnClickListener {
onReuse().also { tapped(SEND_ADDRESS_REUSE) }
}
}
private fun applyViewModel(model: SendViewModel) {
// apply amount
val roundedAmount =
WalletZecFormmatter.toZecStringFull(model.zatoshiAmount)
binding.textSendAmount.text = "\$$roundedAmount"
// apply address
binding.inputZcashAddress.setText(model.toAddress)
// apply memo
binding.inputZcashMemo.setText(model.memo)
binding.checkIncludeAddress.isChecked = model.includeFromAddress
onMemoUpdated()
}
private fun onMemoUpdated() {
val totalLength = sendViewModel.createMemoToSend().length
binding.textLayoutMemo.helperText =
"$totalLength/${ZcashSdk.MAX_MEMO_SIZE} ${getString(R.string.send_memo_chars_abbreviation)}"
val color =
if (totalLength > ZcashSdk.MAX_MEMO_SIZE) R.color.zcashRed else R.color.text_light_dimmed
binding.textLayoutMemo.setHelperTextColor(ColorStateList.valueOf(color.toAppColor()))
}
private fun onClearMemo() {
binding.inputZcashMemo.setText("")
}
private fun onIncludeMemo(checked: Boolean) {
sendViewModel.afterInitFromAddress {
sendViewModel.includeFromAddress = checked
onMemoUpdated()
tapped(if (checked) SEND_MEMO_INCLUDE else SEND_MEMO_EXCLUDE)
}
}
private fun onAddressChanged(address: String) {
lifecycleScope.launchWhenResumed {
val validation = sendViewModel.validateAddress(address)
binding.buttonSend.isActivated = !validation.isNotValid
var type = when (validation) {
is AddressType.Transparent -> R.string.send_validation_address_valid_taddr to R.color.zcashGreen
is AddressType.Shielded -> R.string.send_validation_address_valid_zaddr to R.color.zcashGreen
else -> R.string.send_validation_address_invalid to R.color.zcashRed
}
updateAddressUi(validation is AddressType.Transparent)
if (address == sendViewModel.synchronizer.getAddress() || address == sendViewModel.synchronizer.getTransparentAddress()) {
type = R.string.send_validation_address_self to R.color.zcashRed
}
binding.textLayoutAddress.helperText = getString(type.first)
binding.textLayoutAddress.setHelperTextColor(ColorStateList.valueOf(type.second.toAppColor()))
// if we have the clipboard address but we're changing it, then clear the selection
if (binding.imageClipboardAddressSelected.isVisible) {
loadAddressFromClipboard().let { clipboardAddress ->
if (address != clipboardAddress) {
updateClipboardBanner(clipboardAddress, false)
}
}
}
// if we have the last used address but we're changing it, then clear the selection
if (binding.imageLastUsedAddressSelected.isVisible) {
loadLastUsedAddress().let { lastAddress ->
if (address != lastAddress) {
updateLastUsedBanner(lastAddress, false)
}
}
}
}
}
/**
* To hide input Memo and reply-to option for T type address and show a info message about memo option availability */
private fun updateAddressUi(isMemoHidden: Boolean) {
if (isMemoHidden) {
binding.textLayoutMemo.gone()
binding.checkIncludeAddress.gone()
binding.textNoZAddress.visible()
} else {
binding.textLayoutMemo.visible()
binding.checkIncludeAddress.visible()
binding.textNoZAddress.gone()
}
}
private fun onSubmit(unused: EditText? = null) {
sendViewModel.toAddress = binding.inputZcashAddress.text.toString()
sendViewModel.validate(requireContext(), availableZatoshi, maxZatoshi)
.onFirstWith(resumedScope) { errorMessage ->
if (errorMessage == null) {
val symbol = getString(R.string.symbol)
mainActivity?.authenticate(
"${getString(R.string.send_confirmation_prompt)}\n${
WalletZecFormmatter.toZecStringFull(
sendViewModel.zatoshiAmount
)
} $symbol ${getString(R.string.send_final_to)}\n${sendViewModel.toAddress.toAbbreviatedAddress()}"
) {
// sendViewModel.funnel(Send.AddressPageComplete)
mainActivity?.safeNavigate(R.id.action_nav_send_to_nav_send_final)
}
} else {
resumedScope.launch {
binding.textAddressError.text = errorMessage
delay(2500L)
binding.textAddressError.text = ""
}
}
}
}
private fun onMax() {
if (maxZatoshi != null) {
// binding.inputZcashAmount.apply {
// setText(WalletZecFormmatter.toZecStringFull(maxZatoshi))
// postDelayed({
// requestFocus()
// setSelection(text?.length ?: 0)
// }, 10L)
// }
}
}
override fun onAttach(context: Context) {
super.onAttach(context)
mainActivity?.clipboard?.addPrimaryClipChangedListener(this)
}
override fun onDetach() {
super.onDetach()
mainActivity?.clipboard?.removePrimaryClipChangedListener(this)
}
override fun onResume() {
super.onResume()
onPrimaryClipChanged()
sendViewModel.synchronizer.saplingBalances.filterNotNull().collectWith(resumedScope) {
onBalanceUpdated(it)
}
binding.inputZcashAddress.text.toString().let {
if (!it.isNullOrEmpty()) onAddressChanged(it)
}
}
private fun onBalanceUpdated(balance: WalletBalance) {
// binding.textLayoutAmount.helperText =
// "You have ${WalletZecFormmatter.toZecStringFull(balance.availableZatoshi.coerceAtLeast(0L))} available"
maxZatoshi = (balance.available - ZcashSdk.MINERS_FEE).value
availableZatoshi = balance.available.value
}
override fun onPrimaryClipChanged() {
resumedScope.launch {
updateClipboardBanner(loadAddressFromClipboard())
updateLastUsedBanner(loadLastUsedAddress())
}
}
private fun updateClipboardBanner(address: String?, selected: Boolean = false) {
binding.apply {
updateAddressBanner(
groupClipboard,
clipboardAddress,
imageClipboardAddressSelected,
imageShield,
clipboardAddressLabel,
selected,
address
)
}
}
private suspend fun updateLastUsedBanner(
address: String? = null,
selected: Boolean = false
) {
val isBoth = address == loadAddressFromClipboard()
binding.apply {
updateAddressBanner(
groupLastUsed,
lastUsedAddress,
imageLastUsedAddressSelected,
imageLastUsedShield,
lastUsedAddressLabel,
selected,
address.takeUnless { isBoth }
)
}
binding.dividerClipboard.setText(if (isBoth) R.string.send_history_last_and_clipboard else R.string.send_history_clipboard)
}
private fun updateAddressBanner(
group: Group,
addressTextView: TextView,
checkIcon: ImageView,
shieldIcon: ImageView,
addressLabel: TextView,
selected: Boolean = false,
address: String? = null
) {
resumedScope.launch {
if (address == null) {
group.gone()
} else {
val userShieldedAddr = sendViewModel.synchronizer.getAddress()
val userTransparentAddr = sendViewModel.synchronizer.getTransparentAddress()
group.visible()
addressTextView.text = address.toAbbreviatedAddress(16, 16)
checkIcon.goneIf(!selected)
ImageViewCompat.setImageTintList(
shieldIcon,
ColorStateList.valueOf(if (selected) R.color.colorPrimary.toAppColor() else R.color.zcashWhite_12.toAppColor())
)
addressLabel.setText(if (address == userShieldedAddr) R.string.send_banner_address_user else R.string.send_banner_address_unknown)
if (address == userTransparentAddr) addressLabel.setText("Your Auto-Shielding Address")
addressLabel.setTextColor(if (selected) R.color.colorPrimary.toAppColor() else R.color.text_light.toAppColor())
addressTextView.setTextColor(if (selected) R.color.text_light.toAppColor() else R.color.text_light_dimmed.toAppColor())
}
}
}
private fun onPaste() {
mainActivity?.clipboard?.let { clipboard ->
if (clipboard.hasPrimaryClip()) {
val address = clipboard.text().toString()
val applyValue = binding.imageClipboardAddressSelected.isGone
updateClipboardBanner(address, applyValue)
binding.inputZcashAddress.setText(address.takeUnless { !applyValue })
}
}
}
private fun onReuse() {
sendViewModel.viewModelScope.launch {
val address = loadLastUsedAddress()
val applyValue = binding.imageLastUsedAddressSelected.isGone
updateLastUsedBanner(address, applyValue)
binding.inputZcashAddress.setText(address.takeUnless { !applyValue })
}
}
private suspend fun loadAddressFromClipboard(): String? {
mainActivity?.clipboard?.apply {
if (hasPrimaryClip()) {
text().toString().let { text ->
if (sendViewModel.isValidAddress(text)) return@loadAddressFromClipboard text
}
}
}
return null
}
private var lastUsedAddress: String? = null
private suspend fun loadLastUsedAddress(): String? {
if (lastUsedAddress == null) {
lastUsedAddress = sendViewModel.synchronizer.sentTransactions.first()
.firstOrNull { !it.toAddress.isNullOrEmpty() }?.toAddress
updateLastUsedBanner(lastUsedAddress, binding.imageLastUsedAddressSelected.isVisible)
}
return lastUsedAddress
}
private fun ClipboardManager.text(): CharSequence =
primaryClip!!.getItemAt(0).coerceToText(mainActivity)
}

121
app/src/main/java/cash/z/ecc/android/ui/send/SendMemoFragment.kt

@ -0,0 +1,121 @@
package cash.z.ecc.android.ui.send
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.activityViewModels
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentSendMemoBinding
import cash.z.ecc.android.ext.gone
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ext.onEditorActionDone
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Funnel.Send
import cash.z.ecc.android.feedback.Report.Tap.*
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.util.INCLUDE_MEMO_PREFIX_STANDARD
class SendMemoFragment : BaseFragment<FragmentSendMemoBinding>() {
override val screen = Report.Screen.SEND_MEMO
val sendViewModel: SendViewModel by activityViewModels()
override fun inflate(inflater: LayoutInflater): FragmentSendMemoBinding =
FragmentSendMemoBinding.inflate(inflater)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.buttonNext.setOnClickListener {
onTopButton().also { tapped(SEND_MEMO_NEXT) }
}
binding.buttonSkip.setOnClickListener {
onBottomButton().also { tapped(SEND_MEMO_SKIP) }
}
binding.clearMemo.setOnClickListener {
onClearMemo().also { tapped(SEND_MEMO_CLEAR) }
}
// R.id.action_nav_send_memo_to_nav_send_address.let {
// binding.backButtonHitArea.onClickNavTo(it) { tapped(SEND_MEMO_BACK) }
// onBackPressNavTo(it) { tapped(SEND_MEMO_BACK) }
// }
binding.checkIncludeAddress.setOnCheckedChangeListener { _, _ ->
onIncludeMemo(binding.checkIncludeAddress.isChecked)
}
binding.inputMemo.let { memo ->
memo.onEditorActionDone {
onTopButton().also { tapped(SEND_MEMO_NEXT) }
}
memo.doAfterTextChanged {
binding.clearMemo.goneIf(memo.text.isEmpty())
}
}
sendViewModel.afterInitFromAddress {
binding.textIncludedAddress.text =
"$INCLUDE_MEMO_PREFIX_STANDARD ${sendViewModel.fromAddress}"
}
binding.textIncludedAddress.gone()
applyModel()
}
private fun onClearMemo() {
binding.inputMemo.setText("")
}
private fun applyModel() {
sendViewModel.isShielded.let { isShielded ->
binding.groupShielded.goneIf(!isShielded)
binding.groupTransparent.goneIf(isShielded)
binding.textIncludedAddress.goneIf(!sendViewModel.includeFromAddress)
if (isShielded) {
binding.inputMemo.setText(sendViewModel.memo)
binding.checkIncludeAddress.isChecked = sendViewModel.includeFromAddress
binding.buttonNext.text = "ADD MEMO"
binding.buttonSkip.text = "OMIT MEMO"
} else {
binding.buttonNext.text = "GO BACK"
binding.buttonSkip.text = "PROCEED"
}
}
}
private fun onIncludeMemo(checked: Boolean) {
binding.textIncludedAddress.goneIf(!checked)
sendViewModel.includeFromAddress = checked
binding.textInfoShielded.text = if (checked) {
tapped(SEND_MEMO_INCLUDE)
getString(R.string.send_memo_included_message)
} else {
tapped(SEND_MEMO_EXCLUDE)
getString(R.string.send_memo_excluded_message)
}
}
private fun onTopButton() {
if (sendViewModel.isShielded) {
sendViewModel.memo = binding.inputMemo.text.toString()
onNext()
} else {
// mainActivity?.safeNavigate(R.id.action_nav_send_memo_to_nav_send_address)
}
}
private fun onBottomButton() {
binding.inputMemo.setText("")
sendViewModel.memo = ""
sendViewModel.includeFromAddress = false
onNext()
}
private fun onNext() {
sendViewModel.funnel(Send.MemoPageComplete)
// mainActivity?.safeNavigate(R.id.action_nav_send_memo_to_send_confirm)
}
}

274
app/src/main/java/cash/z/ecc/android/ui/send/SendViewModel.kt

@ -0,0 +1,274 @@
package cash.z.ecc.android.ui.send
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.di.DependenciesHolder
import cash.z.ecc.android.ext.Const
import cash.z.ecc.android.ext.WalletZecFormmatter
import cash.z.ecc.android.feedback.Feedback
import cash.z.ecc.android.feedback.Feedback.Keyed
import cash.z.ecc.android.feedback.Feedback.TimeMetric
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Funnel.Send
import cash.z.ecc.android.feedback.Report.Funnel.Send.SendSelected
import cash.z.ecc.android.feedback.Report.Funnel.Send.SpendingKeyFound
import cash.z.ecc.android.feedback.Report.Issue
import cash.z.ecc.android.feedback.Report.MetricType
import cash.z.ecc.android.feedback.Report.MetricType.*
import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.db.entity.*
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.AddressType
import cash.z.ecc.android.ui.util.INCLUDE_MEMO_PREFIX_STANDARD
import cash.z.ecc.android.util.twig
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
class SendViewModel : ViewModel() {
// note used in testing
val metrics = mutableMapOf<String, TimeMetric>()
private val lockBox: LockBox = DependenciesHolder.lockBox
val synchronizer: Synchronizer = DependenciesHolder.synchronizer
private val feedback: Feedback = DependenciesHolder.feedback
var fromAddress: String = ""
var toAddress: String = ""
var memo: String = ""
var zatoshiAmount: Zatoshi? = null
var includeFromAddress: Boolean = false
set(value) {
require(!value || (value && !fromAddress.isNullOrEmpty())) {
"Error: fromAddress was empty while attempting to include it in the memo. Verify" +
" that initFromAddress() has previously been called on this viewmodel."
}
field = value
}
val isShielded get() = toAddress.startsWith("z")
fun send(): Flow<PendingTransaction> {
funnel(SendSelected)
val memoToSend = createMemoToSend()
val keys = runBlocking {
DerivationTool.deriveSpendingKeys(
lockBox.getBytes(Const.Backup.SEED)!!,
synchronizer.network
)
}
funnel(SpendingKeyFound)
reportUserInputIssues(memoToSend)
return synchronizer.sendToAddress(
keys[0],
zatoshiAmount!!,
toAddress,
memoToSend.chunked(ZcashSdk.MAX_MEMO_SIZE).firstOrNull() ?: ""
).onEach {
twig("Received pending txUpdate: ${it?.toString()}")
updateMetrics(it)
reportFailures(it)
}
}
fun cancel(pendingId: Long) {
viewModelScope.launch {
synchronizer.cancelSpend(pendingId)
}
}
fun createMemoToSend() =
if (includeFromAddress) "$memo\n$INCLUDE_MEMO_PREFIX_STANDARD\n$fromAddress" else memo
suspend fun validateAddress(address: String): AddressType =
synchronizer.validateAddress(address)
suspend fun isValidAddress(address: String): Boolean = when (validateAddress(address)) {
is AddressType.Shielded, is AddressType.Transparent -> true
else -> false
}
fun validate(context: Context, availableZatoshi: Long?, maxZatoshi: Long?) = flow<String?> {
when {
synchronizer.validateAddress(toAddress).isNotValid -> {
emit(context.getString(R.string.send_validation_error_address_invalid))
}
zatoshiAmount?.let { it.value < 1L } ?: false -> {
emit(context.getString(R.string.send_validation_error_amount_minimum))
}
availableZatoshi == null -> {
emit(context.getString(R.string.send_validation_error_unknown_funds))
}
availableZatoshi == 0L -> {
emit(context.getString(R.string.send_validation_error_no_available_funds))
}
availableZatoshi > 0 && availableZatoshi.let { it < ZcashSdk.MINERS_FEE.value } ?: false -> {
emit(context.getString(R.string.send_validation_error_dust))
}
maxZatoshi != null && zatoshiAmount?.let { it.value > maxZatoshi } ?: false -> {
emit(
context.getString(
R.string.send_validation_error_too_much,
WalletZecFormmatter.toZecStringFull(Zatoshi((maxZatoshi))),
ZcashWalletApp.instance.getString(R.string.symbol)
)
)
}
createMemoToSend().length > ZcashSdk.MAX_MEMO_SIZE -> {
emit(
context.getString(
R.string.send_validation_error_memo_length,
ZcashSdk.MAX_MEMO_SIZE
)
)
}
else -> emit(null)
}
}
fun afterInitFromAddress(block: () -> Unit) {
viewModelScope.launch {
fromAddress = synchronizer.getAddress()
block()
}
}
fun reset() {
fromAddress = ""
toAddress = ""
memo = ""
zatoshiAmount = null
includeFromAddress = false
}
//
// Analytics
//
private fun reportFailures(tx: PendingTransaction?) {
if (tx == null) {
// put a stack trace in the logs
twig(IllegalArgumentException("Warning: Could not report failures because tx was null"))
return
}
when {
tx.isCancelled() -> funnel(Send.Cancelled)
tx.isFailedEncoding() -> {
// report that the funnel leaked and also capture a non-fatal app error
funnel(Send.ErrorEncoding(tx.errorCode, tx.errorMessage))
feedback.report(Report.Error.NonFatal.TxEncodeError(tx.errorCode, tx.errorMessage))
}
tx.isFailedSubmit() -> {
// report that the funnel leaked and also capture a non-fatal app error
funnel(Send.ErrorSubmitting(tx.errorCode, tx.errorMessage))
feedback.report(Report.Error.NonFatal.TxSubmitError(tx.errorCode, tx.errorMessage))
}
}
}
private fun reportUserInputIssues(memoToSend: String) {
if (toAddress == fromAddress) feedback.report(Issue.SelfSend)
when {
(zatoshiAmount?.value
?: 0L) < ZcashSdk.MINERS_FEE.value -> feedback.report(Issue.TinyAmount)
(zatoshiAmount?.value ?: 0L) < 100L -> feedback.report(Issue.MicroAmount)
(zatoshiAmount ?: 0L) == 1L -> feedback.report(Issue.MinimumAmount)
}
memoToSend.length.also {
when {
it > ZcashSdk.MAX_MEMO_SIZE -> feedback.report(Issue.TruncatedMemo(it))
it > (ZcashSdk.MAX_MEMO_SIZE * 0.96) -> feedback.report(Issue.LargeMemo(it))
}
}
}
fun updateMetrics(tx: PendingTransaction?) {
if (tx == null) {
// put a stack trace in the logs
twig(IllegalArgumentException("Warning: Could not update metrics because tx was null"))
return
}
try {
when {
tx.isMined() -> TRANSACTION_SUBMITTED to TRANSACTION_MINED by tx.id
tx.isSubmitSuccess() -> TRANSACTION_CREATED to TRANSACTION_SUBMITTED by tx.id
tx.isCreated() -> TRANSACTION_INITIALIZED to TRANSACTION_CREATED by tx.id
tx.isCreating() -> +TRANSACTION_INITIALIZED by tx.id
else -> null
}?.let { metricId ->
report(metricId)
}
} catch (t: Throwable) {
feedback.report(t)
}
}
fun report(metricId: String?) {
metrics[metricId]?.let { metric ->
metric.takeUnless { (it.elapsedTime ?: 0) <= 0L }?.let {
viewModelScope.launch {
withContext(IO) {
feedback.report(metric)
// does this metric complete another metric?
metricId!!.toRelatedMetricId().let { relatedId ->
metrics[relatedId]?.let { relatedMetric ->
// then remove the related metric, itself. And the relation.
metrics.remove(relatedMetric.toMetricIdFor(metricId!!.toTxId()))
metrics.remove(relatedId)
}
}
// remove all top-level metrics
if (metric.key == Report.MetricType.TRANSACTION_MINED.key) metrics.remove(
metricId
)
}
}
}
}
}
fun funnel(step: Send?) {
step ?: return
feedback.report(step)
}
private operator fun MetricType.unaryPlus(): TimeMetric =
TimeMetric(key, description).markTime()
private infix fun TimeMetric.by(txId: Long) =
this.toMetricIdFor(txId).also { metrics[it] = this }
private infix fun Pair<MetricType, MetricType>.by(txId: Long): String? {
val startMetric = first.toMetricIdFor(txId).let { metricId ->
metrics[metricId].also { if (it == null) println("Warning no start metric for id: $metricId") }
}
return startMetric?.endTime?.let { startMetricEndTime ->
TimeMetric(second.key, second.description, mutableListOf(startMetricEndTime))
.markTime().let { endMetric ->
endMetric.toMetricIdFor(txId).also { metricId ->
metrics[metricId] = endMetric
metrics[metricId.toRelatedMetricId()] = startMetric
}
}
}
}
private fun Keyed<String>.toMetricIdFor(id: Long): String = "$id.$key"
private fun String.toRelatedMetricId(): String = "$this.related"
private fun String.toTxId(): Long = split('.').first().toLong()
}

151
app/src/main/java/cash/z/ecc/android/ui/settings/SettingsFragment.kt

@ -0,0 +1,151 @@
package cash.z.ecc.android.ui.settings
import android.content.res.ColorStateList
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.Toast
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.viewModels
import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.databinding.FragmentSettingsBinding
import cash.z.ecc.android.ext.*
import cash.z.ecc.android.sdk.exception.LightWalletException
import cash.z.ecc.android.sdk.ext.collectWith
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.util.twig
import kotlinx.coroutines.launch
class SettingsFragment : BaseFragment<FragmentSettingsBinding>() {
private val viewModel: SettingsViewModel by viewModels()
override fun inflate(inflater: LayoutInflater): FragmentSettingsBinding =
FragmentSettingsBinding.inflate(inflater)
//
// Lifecycle
//
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mainActivity?.preventBackPress(this)
viewModel.init()
binding.apply {
groupLoading.gone()
hitAreaExit.onClickNavBack()
buttonReset.setOnClickListener(::onResetClicked)
buttonUpdate.setOnClickListener(::onUpdateClicked)
buttonUpdate.isActivated = true
buttonReset.isActivated = true
inputHost.doAfterTextChanged {
viewModel.pendingHost = it.toString()
}
inputPort.doAfterTextChanged {
viewModel.pendingPortText = it.toString()
}
}
}
override fun onResume() {
super.onResume()
viewModel.uiModels.collectWith(resumedScope, ::onUiModelUpdated)
}
//
// Event handlers
//
private fun onResetClicked(unused: View?) {
mainActivity?.hideKeyboard()
context?.showUpdateServerDialog(R.string.settings_buttons_restore) {
resumedScope.launch {
binding.groupLoading.visible()
binding.loadingView.requestFocus()
viewModel.resetServer()
}
}
}
private fun onUpdateClicked(unused: View?) {
mainActivity?.hideKeyboard()
context?.showUpdateServerDialog {
resumedScope.launch {
binding.groupLoading.visible()
binding.loadingView.requestFocus()
viewModel.submit()
}
}
}
private fun onUiModelUpdated(uiModel: SettingsViewModel.UiModel) {
twig("onUiModelUpdated:::::$uiModel")
binding.apply {
if (handleCompletion(uiModel)) return@onUiModelUpdated
// avoid moving the cursor on instances where the change originated from the UI
if (inputHost.text.toString() != uiModel.host) inputHost.setText(uiModel.host)
if (inputPort.text.toString() != uiModel.portText) inputPort.setText(uiModel.portText)
buttonReset.isEnabled = uiModel.submitEnabled
buttonUpdate.isEnabled = uiModel.submitEnabled && !uiModel.hasError
uiModel.hostErrorMessage.let { it ->
textInputLayoutHost.helperText = it
?: R.string.settings_host_helper_text.toAppString()
textInputLayoutHost.setHelperTextColor(it.toHelperTextColor())
}
uiModel.portErrorMessage.let { it ->
textInputLayoutPort.helperText = it
?: R.string.settings_port_helper_text.toAppString()
textInputLayoutPort.setHelperTextColor(it.toHelperTextColor())
}
}
}
/**
* Handle the exit conditions and return true if we're done here.
*/
private fun handleCompletion(uiModel: SettingsViewModel.UiModel): Boolean {
return if (uiModel.changeError != null) {
binding.groupLoading.gone()
onCriticalError(uiModel.changeError)
true
} else {
if (uiModel.complete) {
binding.groupLoading.gone()
mainActivity?.safeNavigate(R.id.nav_home)
Toast.makeText(ZcashWalletApp.instance, getString(R.string.settings_toast_change_server_success), Toast.LENGTH_SHORT).show()
true
}
false
}
}
private fun onCriticalError(error: Throwable) {
val details = if (error is LightWalletException.ChangeServerException.StatusException) {
error.status.description
} else {
error.javaClass.simpleName
}
val message = "An error occured while changing servers. Please verify the info" +
" and try again.\n\nError: $details"
twig(message)
Toast.makeText(ZcashWalletApp.instance, getString(R.string.settings_toast_change_server_failure), Toast.LENGTH_SHORT).show()
context?.showUpdateServerCriticalError(message)
}
//
// Utilities
//
private fun String?.toHelperTextColor(): ColorStateList {
val color = if (this == null) {
R.color.text_light_dimmed
} else {
R.color.zcashRed
}
return ColorStateList.valueOf(color.toAppColor())
}
}

88
app/src/main/java/cash/z/ecc/android/ui/settings/SettingsViewModel.kt

@ -0,0 +1,88 @@
package cash.z.ecc.android.ui.settings
import androidx.lifecycle.ViewModel
import cash.z.ecc.android.di.DependenciesHolder
import cash.z.ecc.android.ext.Const
import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.util.twig
import kotlinx.coroutines.flow.MutableStateFlow
import kotlin.properties.Delegates.observable
import kotlin.reflect.KProperty
class SettingsViewModel : ViewModel() {
private val synchronizer: Synchronizer = DependenciesHolder.synchronizer
private val prefs: LockBox = DependenciesHolder.prefs
lateinit var uiModels: MutableStateFlow<UiModel>
private lateinit var initialServer: UiModel
var pendingHost by observable("", ::onUpdateModel)
var pendingPortText by observable("", ::onUpdateModel)
private fun getHost(): String {
return prefs[Const.Pref.SERVER_HOST] ?: Const.Default.Server.HOST
}
private fun getPort(): Int {
return prefs[Const.Pref.SERVER_PORT] ?: Const.Default.Server.PORT
}
fun init() {
initialServer = UiModel(getHost(), getPort().toString())
uiModels = MutableStateFlow(initialServer)
}
suspend fun resetServer() {
UiModel(
Const.Default.Server.HOST,
Const.Default.Server.PORT.toString()
).let { default ->
uiModels.value = default
submit()
}
}
suspend fun submit() {
// Note: this only takes effect after the app is relaunched
val host = uiModels.value.host
val port = uiModels.value.portInt
prefs[Const.Pref.SERVER_HOST] = host
prefs[Const.Pref.SERVER_PORT] = port
uiModels.value = uiModels.value.copy(changeError = null, complete = true)
}
private fun onUpdateModel(kProperty: KProperty<*>, old: String, new: String) {
val pendingPort = pendingPortText.toIntOrNull() ?: -1
uiModels.value = UiModel(
pendingHost,
pendingPortText,
pendingHost != initialServer.host || pendingPortText != initialServer.portText,
if (!pendingHost.isValidHost()) "Please enter a valid host name or IP" else null,
if (pendingPort >= 65535) "Please enter a valid port number below 65535" else null
).also {
twig("updated model with $it")
}
}
data class UiModel(
val host: String = "",
val portText: String = "",
val submitEnabled: Boolean = false,
val hostErrorMessage: String? = null,
val portErrorMessage: String? = null,
val changeError: Throwable? = null,
val complete: Boolean = false
) {
val portInt get() = portText.toIntOrNull() ?: -1
val hasError get() = hostErrorMessage != null || portErrorMessage != null
}
// we can beef this up later if we want to but this is enough for now
private fun String.isValidHost(): Boolean {
return !contains("://")
}
}

158
app/src/main/java/cash/z/ecc/android/ui/setup/BackupFragment.kt

@ -0,0 +1,158 @@
package cash.z.ecc.android.ui.setup
import android.content.Context
import android.os.Bundle
import android.text.SpannableString
import android.text.Spanned
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import android.widget.Toast
import androidx.activity.addCallback
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.databinding.FragmentBackupBinding
import cash.z.ecc.android.di.DependenciesHolder
import cash.z.ecc.android.ext.Const
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.MetricType.SEED_PHRASE_LOADED
import cash.z.ecc.android.feedback.Report.Tap.BACKUP_DONE
import cash.z.ecc.android.feedback.Report.Tap.BACKUP_VERIFY
import cash.z.ecc.android.feedback.measure
import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.SEED_WITH_BACKUP
import cash.z.ecc.android.ui.util.AddressPartNumberSpan
import cash.z.ecc.android.util.twig
import cash.z.ecc.kotlin.mnemonic.Mnemonics
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class BackupFragment : BaseFragment<FragmentBackupBinding>() {
override val screen = Report.Screen.BACKUP
private val walletSetup: WalletSetupViewModel by activityViewModels()
private var hasBackUp: Boolean = true // TODO: implement backup and then check for it here-ish
override fun inflate(inflater: LayoutInflater): FragmentBackupBinding =
FragmentBackupBinding.inflate(inflater)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(binding) {
applySpan(
textAddressPart1, textAddressPart2, textAddressPart3,
textAddressPart4, textAddressPart5, textAddressPart6,
textAddressPart7, textAddressPart8, textAddressPart9,
textAddressPart10, textAddressPart11, textAddressPart12,
textAddressPart13, textAddressPart14, textAddressPart15,
textAddressPart16, textAddressPart17, textAddressPart18,
textAddressPart19, textAddressPart20, textAddressPart21,
textAddressPart22, textAddressPart23, textAddressPart24
)
}
binding.buttonPositive.setOnClickListener {
onEnterWallet().also { if (hasBackUp) tapped(BACKUP_DONE) else tapped(BACKUP_VERIFY) }
}
if (hasBackUp) {
binding.buttonPositive.text = getString(R.string.backup_button_done)
}
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
mainActivity?.onBackPressedDispatcher?.addCallback(this) {
onEnterWallet(false)
}
}
override fun onAttach(context: Context) {
super.onAttach(context)
walletSetup.checkSeed().onEach {
hasBackUp = when (it) {
SEED_WITH_BACKUP -> true
else -> false
}
}.launchIn(lifecycleScope)
}
override fun onResume() {
super.onResume()
resumedScope.launch {
binding.textBirtdate.text =
getString(R.string.backup_format_birthday_height, calculateBirthday().value)
}
}
// TODO: move this into the SDK
private suspend fun calculateBirthday(): BlockHeight {
var storedBirthday: BlockHeight? = null
var oldestTransactionHeight: BlockHeight? = null
var activationHeight: BlockHeight? = null
try {
activationHeight = DependenciesHolder.synchronizer.network.saplingActivationHeight
storedBirthday = walletSetup.loadBirthdayHeight()
oldestTransactionHeight = DependenciesHolder.synchronizer.receivedTransactions.first()
?.last()?.minedHeight?.let {
BlockHeight.new(ZcashWalletApp.instance.defaultNetwork, it)
}
// to be safe adjust for reorgs (and generally a little cushion is good for privacy)
// so we round down to the nearest 100 and then subtract 100 to ensure that the result is always at least 100 blocks away
oldestTransactionHeight = ZcashSdk.MAX_REORG_SIZE.let { boundary ->
oldestTransactionHeight?.let { it.value - it.value.rem(boundary) - boundary }
?.let { BlockHeight.new(ZcashWalletApp.instance.defaultNetwork, it) }
}
} catch (t: Throwable) {
twig("failed to calculate birthday due to: $t")
}
return listOfNotNull(
storedBirthday,
oldestTransactionHeight,
activationHeight
).maxBy { it.value }
}
private fun onEnterWallet(showMessage: Boolean = !this.hasBackUp) {
if (showMessage) {
Toast.makeText(
activity,
R.string.backup_verification_not_implemented,
Toast.LENGTH_LONG
).show()
}
mainActivity?.navController?.popBackStack()
}
private fun applySpan(vararg textViews: TextView) = lifecycleScope.launch {
val words = loadSeedWords()
val thinSpace = "\u2005" // 0.25 em space
textViews.forEachIndexed { index, textView ->
val numLength = "$index".length
val word = words[index]
// TODO: work with a charsequence here, rather than constructing a String
textView.text = SpannableString("${index + 1}$thinSpace${String(word)}").apply {
setSpan(AddressPartNumberSpan(), 0, 1 + numLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
}
private suspend fun loadSeedWords(): List<CharArray> = withContext(Dispatchers.IO) {
mainActivity!!.feedback.measure(SEED_PHRASE_LOADED) {
val lockBox = LockBox(ZcashWalletApp.instance)
val mnemonics = Mnemonics()
val seedPhrase = lockBox.getCharsUtf8(Const.Backup.SEED_PHRASE)
?: throw RuntimeException("Seed Phrase expected but not found in storage!!")
val result = mnemonics.toWordList(seedPhrase)
result
}
}
}

216
app/src/main/java/cash/z/ecc/android/ui/setup/LandingFragment.kt

@ -0,0 +1,216 @@
package cash.z.ecc.android.ui.setup
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.Toast
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.databinding.FragmentLandingBinding
import cash.z.ecc.android.ext.locale
import cash.z.ecc.android.ext.showSharedLibraryCriticalError
import cash.z.ecc.android.ext.toAppString
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Funnel.Restore
import cash.z.ecc.android.feedback.Report.Tap.*
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.SEED_WITHOUT_BACKUP
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.SEED_WITH_BACKUP
import cash.z.ecc.android.util.twig
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
class LandingFragment : BaseFragment<FragmentLandingBinding>() {
override val screen = Report.Screen.LANDING
private val walletSetup: WalletSetupViewModel by activityViewModels()
private var skipCount: Int = 0
override fun inflate(inflater: LayoutInflater): FragmentLandingBinding =
FragmentLandingBinding.inflate(inflater)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.buttonPositive.setOnClickListener {
when (binding.buttonPositive.text.toString().toLowerCase(locale())) {
R.string.landing_button_primary.toAppString(true) -> onNewWallet().also {
tapped(
LANDING_NEW
)
}
R.string.landing_button_primary_create_success.toAppString(true) -> onBackupWallet().also {
tapped(
LANDING_BACKUP
)
}
}
}
binding.buttonNegative.setOnLongClickListener {
tapped(DEVELOPER_WALLET_PROMPT)
if (binding.buttonNegative.text.toString().toLowerCase(locale()) == "restore") {
MaterialAlertDialogBuilder(requireContext())
.setMessage("Would you like to import the dev wallet?\n\nIf so, please only send 1000 zatoshis at a time and return some later so that the account remains funded.")
.setTitle("Import Dev Wallet?")
.setCancelable(true)
.setPositiveButton("Import") { dialog, _ ->
tapped(DEVELOPER_WALLET_IMPORT)
dialog.dismiss()
onUseDevWallet()
}
.setNegativeButton("Cancel") { dialog, _ ->
tapped(DEVELOPER_WALLET_CANCEL)
dialog.dismiss()
}
.show()
true
} else {
false
}
}
binding.buttonNegative.setOnClickListener {
when (binding.buttonNegative.text.toString().toLowerCase(locale())) {
R.string.landing_button_secondary.toAppString(true) -> onRestoreWallet().also {
mainActivity?.reportFunnel(Restore.Initiated)
tapped(LANDING_RESTORE)
}
else -> onSkip(++skipCount)
}
}
}
override fun onAttach(context: Context) {
super.onAttach(context)
walletSetup.checkSeed().onEach {
when (it) {
SEED_WITHOUT_BACKUP, SEED_WITH_BACKUP -> {
mainActivity?.safeNavigate(R.id.nav_backup)
}
else -> {}
}
}.launchIn(lifecycleScope)
}
override fun onResume() {
super.onResume()
view?.postDelayed(
{
mainActivity?.hideKeyboard()
},
25L
)
}
private fun onSkip(count: Int) {
when (count) {
1 -> {
tapped(LANDING_BACKUP_SKIPPED_1)
binding.textMessage.setText(R.string.landing_backup_skipped_message_1)
binding.buttonNegative.setText(R.string.landing_button_backup_skipped_1)
}
2 -> {
tapped(LANDING_BACKUP_SKIPPED_2)
binding.textMessage.setText(R.string.landing_backup_skipped_message_2)
binding.buttonNegative.setText(R.string.landing_button_backup_skipped_2)
}
else -> {
tapped(LANDING_BACKUP_SKIPPED_3)
onEnterWallet()
}
}
}
private fun onRestoreWallet() {
mainActivity?.safeNavigate(R.id.action_nav_landing_to_nav_restore)
}
// AKA import wallet
private fun onUseDevWallet() {
val seedPhrase: String
val birthday: BlockHeight
// new testnet dev wallet
when (ZcashWalletApp.instance.defaultNetwork) {
ZcashNetwork.Mainnet -> {
seedPhrase =
"still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"
birthday = BlockHeight.new(ZcashNetwork.Mainnet, 991645) // 663174
}
ZcashNetwork.Testnet -> {
seedPhrase =
"quantum whisper lion route fury lunar pelican image job client hundred sauce chimney barely life cliff spirit admit weekend message recipe trumpet impact kitten"
birthday = BlockHeight.new(ZcashNetwork.Testnet, 1330190)
}
else -> throw RuntimeException("No developer wallet exists for network ${ZcashWalletApp.instance.defaultNetwork}")
}
mainActivity?.apply {
lifecycleScope.launch {
try {
walletSetup.importWallet(seedPhrase, birthday)
mainActivity?.startSync()
binding.buttonPositive.isEnabled = true
binding.textMessage.setText(R.string.landing_import_success_message)
binding.buttonNegative.setText(R.string.landing_button_secondary_import_success)
binding.buttonPositive.setText(R.string.landing_import_success_primary_button)
playSound("sound_receive_small.mp3")
vibrateSuccess()
} catch (e: UnsatisfiedLinkError) {
mainActivity?.showSharedLibraryCriticalError(e)
}
}
}
}
private fun onNewWallet() {
lifecycleScope.launch {
binding.buttonPositive.setText(R.string.landing_button_progress_create)
binding.buttonPositive.isEnabled = false
try {
walletSetup.newWallet()
mainActivity?.startSync()
binding.buttonPositive.isEnabled = true
binding.textMessage.setText(R.string.landing_create_success_message)
binding.buttonNegative.setText(R.string.landing_button_secondary_create_success)
binding.buttonPositive.setText(R.string.landing_button_primary_create_success)
mainActivity?.playSound("sound_receive_small.mp3")
mainActivity?.vibrateSuccess()
} catch (e: UnsatisfiedLinkError) {
// For developer sanity:
// show a nice dialog, rather than a toast, when the rust didn't get compile
// which can happen often when working from a local SDK build
mainActivity?.showSharedLibraryCriticalError(e)
} catch (t: Throwable) {
twig("Failed to create wallet due to: $t")
mainActivity?.feedback?.report(t)
binding.buttonPositive.isEnabled = true
binding.buttonPositive.setText(R.string.landing_button_primary)
Toast.makeText(
context,
"Failed to create wallet. See logs for details. Try restarting the app.\n\nMessage: \n${t.message}",
Toast.LENGTH_LONG
).show()
}
}
}
private fun onBackupWallet() {
skipCount = 0
mainActivity?.safeNavigate(R.id.action_nav_landing_to_nav_backup)
}
private fun onEnterWallet() {
skipCount = 0
mainActivity?.navController?.popBackStack()
}
}

246
app/src/main/java/cash/z/ecc/android/ui/setup/RestoreFragment.kt

@ -0,0 +1,246 @@
package cash.z.ecc.android.ui.setup
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.os.SystemClock
import android.text.InputType
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.MotionEvent.ACTION_DOWN
import android.view.MotionEvent.ACTION_UP
import android.view.View
import android.widget.TextView
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.databinding.FragmentRestoreBinding
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ext.showConfirmation
import cash.z.ecc.android.ext.showInvalidSeedPhraseError
import cash.z.ecc.android.ext.showSharedLibraryCriticalError
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Funnel.Restore
import cash.z.ecc.android.feedback.Report.Tap.*
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.ui.base.BaseFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tylersuehr.chips.Chip
import com.tylersuehr.chips.ChipsAdapter
import com.tylersuehr.chips.SeedWordAdapter
import kotlinx.coroutines.launch
class RestoreFragment : BaseFragment<FragmentRestoreBinding>(), View.OnKeyListener {
override val screen = Report.Screen.RESTORE
private val walletSetup: WalletSetupViewModel by activityViewModels()
private lateinit var seedWordRecycler: RecyclerView
private var seedWordAdapter: SeedWordAdapter? = null
override fun inflate(inflater: LayoutInflater): FragmentRestoreBinding =
FragmentRestoreBinding.inflate(inflater)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
seedWordRecycler = binding.chipsInput.findViewById<RecyclerView>(R.id.chips_recycler)
seedWordAdapter = SeedWordAdapter(seedWordRecycler.adapter as ChipsAdapter).onDataSetChanged {
onChipsModified()
}.also { onChipsModified() }
seedWordRecycler.adapter = seedWordAdapter
binding.chipsInput.apply {
setFilterableChipList(getChips())
setDelimiter("[ ;,]", true)
}
binding.buttonDone.setOnClickListener {
onDone().also { tapped(RESTORE_DONE) }
}
binding.buttonSuccess.setOnClickListener {
onEnterWallet().also { tapped(RESTORE_SUCCESS) }
}
binding.buttonClear.setOnClickListener {
onClearSeedWords().also { tapped(RESTORE_CLEAR) }
}
}
private fun onClearSeedWords() {
mainActivity?.showConfirmation(
"Clear All Words",
"Are you sure you would like to clear all the seed words and type them again?",
"Clear",
onPositive = {
binding.chipsInput.clearSelectedChips()
}
)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
mainActivity?.onFragmentBackPressed(this) {
tapped(RESTORE_BACK)
if (seedWordAdapter == null || seedWordAdapter?.itemCount == 1) {
onExit()
} else {
MaterialAlertDialogBuilder(requireContext())
.setMessage("Are you sure? For security, the words that you have entered will be cleared!")
.setTitle("Abort?")
.setPositiveButton("Stay") { dialog, _ ->
mainActivity?.reportFunnel(Restore.Stay)
dialog.dismiss()
}
.setNegativeButton("Exit") { dialog, _ ->
dialog.dismiss()
onExit()
}
.show()
}
}
}
override fun onResume() {
super.onResume()
// Require one less tap to enter the seed words
touchScreenForUser()
}
private fun onExit() {
mainActivity?.reportFunnel(Restore.Exit)
hideAutoCompleteWords()
mainActivity?.hideKeyboard()
mainActivity?.navController?.popBackStack()
}
private fun onEnterWallet() {
mainActivity?.reportFunnel(Restore.Success)
mainActivity?.safeNavigate(R.id.action_nav_restore_to_nav_home)
}
private fun onDone() {
mainActivity?.reportFunnel(Restore.Done)
mainActivity?.hideKeyboard()
val activation = ZcashWalletApp.instance.defaultNetwork.saplingActivationHeight
val seedPhrase = binding.chipsInput.selectedChips.joinToString(" ") {
it.title
}
var birthday = binding.root.findViewById<TextView>(R.id.input_birthdate).text.toString()
.let { birthdateString ->
if (birthdateString.isNullOrEmpty()) activation.value else birthdateString.toLong()
}.coerceAtLeast(activation.value)
try {
walletSetup.validatePhrase(seedPhrase)
importWallet(seedPhrase, BlockHeight.new(ZcashWalletApp.instance.defaultNetwork, birthday))
} catch (t: Throwable) {
mainActivity?.showInvalidSeedPhraseError(t)
}
}
private fun importWallet(seedPhrase: String, birthday: BlockHeight?) {
mainActivity?.reportFunnel(Restore.ImportStarted)
mainActivity?.hideKeyboard()
mainActivity?.apply {
lifecycleScope.launch {
try {
walletSetup.importWallet(seedPhrase, birthday)
mainActivity?.startSync()
// bugfix: if the user proceeds before the synchronizer is created the app will crash!
binding.buttonSuccess.isEnabled = true
mainActivity?.reportFunnel(Restore.ImportCompleted)
playSound("sound_receive_small.mp3")
vibrateSuccess()
} catch (e: UnsatisfiedLinkError) {
mainActivity?.showSharedLibraryCriticalError(e)
}
}
}
binding.groupDone.visibility = View.GONE
binding.groupStart.visibility = View.GONE
binding.groupSuccess.visibility = View.VISIBLE
binding.buttonSuccess.isEnabled = false
}
private fun onChipsModified() {
updateDoneViews()
forceShowKeyboard()
}
private fun updateDoneViews(): Boolean {
val count = seedWordAdapter?.itemCount ?: 0
reportWords(count - 1) // subtract 1 for the editText
val isDone = count > 24
binding.groupDone.goneIf(!isDone)
return !isDone
}
// forcefully show the keyboard as a hack to fix odd behavior where the keyboard
// sometimes closes randomly and inexplicably in between seed word entries
private fun forceShowKeyboard() {
requireView().postDelayed(
{
val isDone = (seedWordAdapter?.itemCount ?: 0) > 24
val focusedView = if (isDone) binding.inputBirthdate else seedWordAdapter!!.editText
mainActivity!!.showKeyboard(focusedView)
focusedView.requestFocus()
},
500L
)
}
private fun reportWords(count: Int) {
mainActivity?.run {
// reportFunnel(Restore.SeedWordCount(count))
if (count == 1) {
reportFunnel(Restore.SeedWordsStarted)
} else if (count == 24) {
reportFunnel(Restore.SeedWordsCompleted)
}
}
}
private fun hideAutoCompleteWords() {
seedWordAdapter?.editText?.setText("")
}
private fun getChips(): List<Chip> {
return resources.getStringArray(R.array.word_list).map {
SeedWordChip(it)
}
}
private fun touchScreenForUser() {
seedWordAdapter?.editText?.apply {
postDelayed(
{
seedWordAdapter?.editText?.inputType = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
dispatchTouchEvent(motionEvent(ACTION_DOWN))
dispatchTouchEvent(motionEvent(ACTION_UP))
},
100L
)
}
}
private fun motionEvent(action: Int) = SystemClock.uptimeMillis().let { now ->
MotionEvent.obtain(now, now, action, 0f, 0f, 0)
}
override fun onKey(v: View?, keyCode: Int, event: KeyEvent?): Boolean {
return false
}
}
class SeedWordChip(val word: String, var index: Int = -1) : Chip() {
override fun getSubtitle(): String? = null // "subtitle for $word"
override fun getAvatarDrawable(): Drawable? = null
override fun getId() = index
override fun getTitle() = word
override fun getAvatarUri() = null
}

103
app/src/main/java/cash/z/ecc/android/ui/setup/SeedWordAdapter.kt

@ -0,0 +1,103 @@
package com.tylersuehr.chips
import android.content.Context
import android.text.TextUtils
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import cash.z.ecc.android.R
import cash.z.ecc.android.ext.toAppColor
import cash.z.ecc.android.ui.setup.SeedWordChip
class SeedWordAdapter : ChipsAdapter {
constructor(existingAdapter: ChipsAdapter) : super(existingAdapter.mDataSource, existingAdapter.mEditText, existingAdapter.mOptions)
val editText = mEditText
private var onDataSetChangedListener: (() -> Unit)? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return if (viewType == CHIP) SeedWordHolder(SeedWordChipView(parent.context))
else object : RecyclerView.ViewHolder(mEditText) {}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (getItemViewType(position) == CHIP) { // Chips
// Display the chip information on the chip view
(holder as SeedWordHolder).seedChipView.bind(mDataSource.getSelectedChip(position), position)
} else {
val size = mDataSource.selectedChips.size
// tricky bugfix:
// keep this always enabled otherwise older versions of android crash when this
// view is given focus. As a work around, just hide the cursor when the user is done
// editing. This is not ideal but it's better than a crash during wallet restore!
mEditText.isEnabled = true
mEditText.hint = if (size < 3) {
mEditText.isCursorVisible = true
mEditText.setHintTextColor(R.color.text_light_dimmed.toAppColor())
val ordinal = when (size) { 2 -> "3rd"; 1 -> "2nd"; else -> "1st" }
"Enter $ordinal seed word"
} else if (size >= 24) {
mEditText.setHintTextColor(R.color.zcashGreen.toAppColor())
mEditText.isCursorVisible = false
"done"
} else {
mEditText.isCursorVisible = true
mEditText.setHintTextColor(R.color.zcashYellow.toAppColor())
"${size + 1}"
}
}
}
override fun onChipDataSourceChanged() {
super.onChipDataSourceChanged()
onDataSetChangedListener?.invoke()
}
fun onDataSetChanged(block: () -> Unit): SeedWordAdapter {
onDataSetChangedListener = block
return this
}
override fun onKeyboardActionDone(text: String?) {
if (TextUtils.isEmpty(text)) return
if (mDataSource.originalChips.firstOrNull { it.title == text } != null) {
mDataSource.addSelectedChip(DefaultCustomChip(text))
mEditText.apply {
postDelayed(
{
setText("")
requestFocus()
},
50L
)
}
}
}
// this function is called with the contents of the field, split by the delimiter
override fun onKeyboardDelimiter(text: String) {
val firstMatchingWord = (mDataSource.filteredChips.firstOrNull() as? SeedWordChip)?.word?.takeUnless {
!it.startsWith(text)
}
if (firstMatchingWord != null) {
onKeyboardActionDone(firstMatchingWord)
} else {
onKeyboardActionDone(text)
}
}
private inner class SeedWordHolder(chipView: SeedWordChipView) : ChipsAdapter.ChipHolder(chipView) {
val seedChipView = super.chipView as SeedWordChipView
}
private inner class SeedWordChipView(context: Context) : ChipView(context) {
private val indexView: TextView = findViewById(R.id.chip_index)
fun bind(chip: Chip, index: Int) {
super.inflateFromChip(chip)
indexView.text = (index + 1).toString()
}
}
}

233
app/src/main/java/cash/z/ecc/android/ui/setup/WalletSetupViewModel.kt

@ -0,0 +1,233 @@
package cash.z.ecc.android.ui.setup
import android.content.Context
import androidx.lifecycle.ViewModel
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.di.DependenciesHolder
import cash.z.ecc.android.ext.Const
import cash.z.ecc.android.ext.failWith
import cash.z.ecc.android.feedback.Feedback
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.exception.InitializerException
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.UnifiedViewingKey
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.*
import cash.z.ecc.android.util.twig
import cash.z.ecc.kotlin.mnemonic.Mnemonics
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext
class WalletSetupViewModel : ViewModel() {
private val mnemonics: Mnemonics = DependenciesHolder.mnemonics
private val lockBox: LockBox = DependenciesHolder.lockBox
private val prefs: LockBox = DependenciesHolder.prefs
private val feedback: Feedback = DependenciesHolder.feedback
enum class WalletSetupState {
SEED_WITH_BACKUP, SEED_WITHOUT_BACKUP, NO_SEED
}
fun checkSeed(): Flow<WalletSetupState> = flow {
when {
lockBox.getBoolean(Const.Backup.HAS_BACKUP) -> emit(SEED_WITH_BACKUP)
lockBox.getBoolean(Const.Backup.HAS_SEED) -> emit(SEED_WITHOUT_BACKUP)
else -> emit(NO_SEED)
}
}
/**
* Throw an exception if the seed phrase is bad.
*/
fun validatePhrase(seedPhrase: String) {
mnemonics.validate(seedPhrase.toCharArray())
}
fun loadBirthdayHeight(): BlockHeight? {
val h: Int? = lockBox[Const.Backup.BIRTHDAY_HEIGHT]
twig("Loaded birthday with key ${Const.Backup.BIRTHDAY_HEIGHT} and found $h")
h?.let {
return BlockHeight.new(ZcashWalletApp.instance.defaultNetwork, it.toLong())
}
return null
}
suspend fun newWallet() {
val network = ZcashWalletApp.instance.defaultNetwork
twig("Initializing new ${network.networkName} wallet")
with(mnemonics) {
storeWallet(nextMnemonic(nextEntropy()), network, loadNearestBirthday(network))
}
openStoredWallet()
}
suspend fun importWallet(seedPhrase: String, birthdayHeight: BlockHeight?) {
val network = ZcashWalletApp.instance.defaultNetwork
twig("Importing ${network.networkName} wallet. Requested birthday: $birthdayHeight")
storeWallet(
seedPhrase.toCharArray(),
network,
birthdayHeight ?: loadNearestBirthday(network)
)
openStoredWallet()
}
suspend fun openStoredWallet() {
DependenciesHolder.initializerComponent.createInitializer(loadConfig())
}
/**
* Build a config object by loading in the viewingKey, birthday and server info which is already
* known by this point.
*/
private suspend fun loadConfig(): Initializer.Config {
twig("Loading config variables")
var overwriteVks = false
val network = ZcashWalletApp.instance.defaultNetwork
val vk =
loadUnifiedViewingKey() ?: onMissingViewingKey(network).also { overwriteVks = true }
val birthdayHeight = loadBirthdayHeight() ?: onMissingBirthday(network)
val host = prefs[Const.Pref.SERVER_HOST] ?: Const.Default.Server.HOST
val port = prefs[Const.Pref.SERVER_PORT] ?: Const.Default.Server.PORT
twig("Done loading config variables")
return Initializer.Config {
it.importWallet(vk, birthdayHeight, network, LightWalletEndpoint(host, port, true))
it.setOverwriteKeys(overwriteVks)
}
}
private fun loadUnifiedViewingKey(): UnifiedViewingKey? {
val extfvk = lockBox.getCharsUtf8(Const.Backup.VIEWING_KEY)
val extpub = lockBox.getCharsUtf8(Const.Backup.PUBLIC_KEY)
return when {
extfvk == null || extpub == null -> {
if (extfvk == null) {
twig("Warning: Shielded key was missing")
}
if (extpub == null) {
twig("Warning: Transparent key was missing")
}
null
}
else -> UnifiedViewingKey(extfvk = String(extfvk), extpub = String(extpub))
}
}
private suspend fun onMissingViewingKey(network: ZcashNetwork): UnifiedViewingKey {
twig("Recover VK: Viewing key was missing")
// add some temporary logic to help us troubleshoot this problem.
ZcashWalletApp.instance.getSharedPreferences("SecurePreferences", Context.MODE_PRIVATE)
.all.map { it.key }.joinToString().let { keyNames ->
"${Const.Backup.VIEWING_KEY}, ${Const.Backup.PUBLIC_KEY}".let { missingKeys ->
// is there a typo or change in how the value is labelled?
// for troubleshooting purposes, let's see if we CAN derive the vk from the seed in these situations
var recoveryViewingKey: UnifiedViewingKey? = null
var ableToLoadSeed = false
try {
val seed = lockBox.getBytes(Const.Backup.SEED)!!
ableToLoadSeed = true
twig("Recover UVK: Seed found")
recoveryViewingKey =
DerivationTool.deriveUnifiedViewingKeys(seed, network)[0]
twig("Recover UVK: successfully derived UVK from seed")
} catch (t: Throwable) {
twig("Failed while trying to recover UVK due to: $t")
}
// this will happen during rare upgrade scenarios when the user migrates from a seed-only wallet to this vk-based version
// or during more common scenarios where the user migrates from a vk only wallet to a unified vk wallet
if (recoveryViewingKey != null) {
storeUnifiedViewingKey(recoveryViewingKey)
return recoveryViewingKey
} else {
feedback.report(
Report.Issue.MissingViewkey(
ableToLoadSeed,
missingKeys,
keyNames,
lockBox.getCharsUtf8(Const.Backup.VIEWING_KEY) != null
)
)
}
throw InitializerException.MissingViewingKeyException
}
}
}
private suspend fun onMissingBirthday(network: ZcashNetwork): BlockHeight =
failWith(InitializerException.MissingBirthdayException) {
twig("Recover Birthday: falling back to sapling birthday")
loadNearestBirthday(network)
}
private suspend fun loadNearestBirthday(network: ZcashNetwork) =
BlockHeight.ofLatestCheckpoint(
ZcashWalletApp.instance,
network,
)
//
// Storage Helpers
//
/**
* Entry point for all storage. Takes a seed phrase and stores all the parts so that we can
* selectively use them, the next time the app is opened. Although we store everything, we
* primarily only work with the viewing key and spending key. The seed is only accessed when
* presenting backup information to the user.
*/
private suspend fun storeWallet(
seedPhraseChars: CharArray,
network: ZcashNetwork,
birthday: BlockHeight
) {
check(!lockBox.getBoolean(Const.Backup.HAS_SEED)) {
"Error! Cannot store a seed when one already exists! This would overwrite the" +
" existing seed and could lead to a loss of funds if the user has no backup!"
}
storeBirthday(birthday)
mnemonics.toSeed(seedPhraseChars).let { bip39Seed ->
DerivationTool.deriveUnifiedViewingKeys(bip39Seed, network)[0].let { viewingKey ->
storeSeedPhrase(seedPhraseChars)
storeSeed(bip39Seed)
storeUnifiedViewingKey(viewingKey)
}
}
}
private suspend fun storeBirthday(birthday: BlockHeight) = withContext(IO) {
twig("Storing birthday ${birthday.value} with and key ${Const.Backup.BIRTHDAY_HEIGHT}")
lockBox[Const.Backup.BIRTHDAY_HEIGHT] = birthday.value
}
private suspend fun storeSeedPhrase(seedPhrase: CharArray) = withContext(IO) {
twig("Storing seedphrase: ${seedPhrase.size}")
lockBox[Const.Backup.SEED_PHRASE] = seedPhrase
lockBox[Const.Backup.HAS_SEED_PHRASE] = true
}
private suspend fun storeSeed(bip39Seed: ByteArray) = withContext(IO) {
twig("Storing seed: ${bip39Seed.size}")
lockBox.setBytes(Const.Backup.SEED, bip39Seed)
lockBox[Const.Backup.HAS_SEED] = true
}
private suspend fun storeUnifiedViewingKey(vk: UnifiedViewingKey) = withContext(IO) {
twig("storeViewingKey vk: ${vk.extfvk.length}")
lockBox[Const.Backup.VIEWING_KEY] = vk.extfvk
lockBox[Const.Backup.PUBLIC_KEY] = vk.extpub
}
}

144
app/src/main/java/cash/z/ecc/android/ui/tab_layout/TabLayoutFragment.kt

@ -0,0 +1,144 @@
package cash.z.ecc.android.ui.tab_layout
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.widget.ViewPager2
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentTabLayoutBinding
import cash.z.ecc.android.ext.onClickNavBack
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.receive.ReceiveTabFragment
import cash.z.ecc.android.ui.receive.ReceiveViewModel
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.coroutines.launch
class TabLayoutFragment :
BaseFragment<FragmentTabLayoutBinding>(),
FragmentCreator,
TabLayout.OnTabSelectedListener {
private val viewModel: ReceiveViewModel by viewModels()
override fun inflate(inflater: LayoutInflater): FragmentTabLayoutBinding =
FragmentTabLayoutBinding.inflate(inflater)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.hitAreaExit.onClickNavBack { tapped(Report.Tap.RECEIVE_BACK) }
binding.textTitle.text = "Receive ${getString(R.string.symbol)}"
binding.viewPager.adapter = ViewPagerAdapter(this, this)
binding.viewPager.setPageTransformer(ZoomOutPageTransformer())
binding.tabLayout.addOnTabSelectedListener(this)
/*
TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, position ->
tab.text = if (position == 0) "Shielded" else "Transparent"
}.attach()
*/
binding.buttonShareAddress.setOnClickListener {
shareActiveAddress()
}
}
private fun shareActiveAddress() {
mainActivity?.apply {
lifecycleScope.launch {
val address =
viewModel.getAddress()
shareText(address)
}
}
}
//
// TabLayout.OnTabSelectedListener implementation
//
override fun onTabSelected(tab: TabLayout.Tab) {
when (tab.position) {
0 -> setSelectedTab(R.color.zcashYellow)
}
}
override fun onTabUnselected(tab: TabLayout.Tab) {}
override fun onTabReselected(tab: TabLayout.Tab) {}
private fun setSelectedTab(@ColorRes color: Int) {
binding.tabLayout.setSelectedTabIndicatorColor(
ContextCompat.getColor(requireContext(), color)
)
binding.tabLayout.setTabTextColors(
ContextCompat.getColor(requireContext(), R.color.unselected_tab_grey),
ContextCompat.getColor(requireContext(), color)
)
}
//
// FragmentCreator implementation
//
override fun createFragment(position: Int): Fragment {
return when (position) {
0 -> ReceiveTabFragment()
else -> throw IndexOutOfBoundsException("Cannot create a fragment for index $position")
}
}
override fun getItemCount() = 2
interface AddressFragment {
suspend fun getAddress(): String
}
}
private const val MIN_SCALE = 0.8f
private const val MIN_ALPHA = 0.1f
class ZoomOutPageTransformer : ViewPager2.PageTransformer {
override fun transformPage(view: View, position: Float) {
view.apply {
val pageWidth = width
val pageHeight = height
when {
position < -1 -> { // [-Infinity,-1)
// This page is way off-screen to the left.
alpha = 0f
}
position <= 1 -> { // [-1,1]
// Modify the default slide transition to shrink the page as well
val scaleFactor = Math.max(MIN_SCALE, 1 - Math.abs(position))
val vertMargin = pageHeight * (1 - scaleFactor) / 2
val horzMargin = pageWidth * (1 - scaleFactor) / 2
translationX = if (position < 0) {
horzMargin - vertMargin / 2
} else {
horzMargin + vertMargin / 2
}
// Scale the page down (between MIN_SCALE and 1)
scaleX = scaleFactor
scaleY = scaleFactor
// Fade the page relative to its size.
alpha = (
MIN_ALPHA +
(((scaleFactor - MIN_SCALE) / (1 - MIN_SCALE)) * (1 - MIN_ALPHA))
)
}
else -> { // (1,+Infinity]
// This page is way off-screen to the right.
alpha = 0f
}
}
}
}
}

13
app/src/main/java/cash/z/ecc/android/ui/tab_layout/ViewPagerAdapter.kt

@ -0,0 +1,13 @@
package cash.z.ecc.android.ui.tab_layout
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
class ViewPagerAdapter(parent: Fragment, creator: FragmentCreator) :
FragmentStateAdapter(parent),
FragmentCreator by creator
interface FragmentCreator {
fun createFragment(position: Int): Fragment
fun getItemCount(): Int
}

28
app/src/main/java/cash/z/ecc/android/ui/util/AddressPartNumberSpan.kt

@ -0,0 +1,28 @@
package cash.z.ecc.android.ui.util
import android.text.TextPaint
import android.text.style.MetricAffectingSpan
import androidx.core.content.ContextCompat
import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
/**
* A span used for numbering the parts of an address. It combines a [android.text.style.RelativeSizeSpan],
* [android.text.style.SuperscriptSpan], and a [android.text.style.ForegroundColorSpan] into one class for efficiency.
*/
class AddressPartNumberSpan(
val proportion: Float = 0.5f,
val color: Int = ContextCompat.getColor(ZcashWalletApp.instance, R.color.colorPrimary)
) : MetricAffectingSpan() {
override fun updateMeasureState(textPaint: TextPaint) {
textPaint.baselineShift += (textPaint.ascent() / 2).toInt() // from SuperscriptSpan
textPaint.textSize = textPaint.textSize * proportion // from RelativeSizeSpan
}
override fun updateDrawState(textPaint: TextPaint) {
textPaint.baselineShift += (textPaint.ascent() / 2).toInt() // from SuperscriptSpan (baseline must shift before resizing or else it will not properly align to the top of the text)
textPaint.textSize = textPaint.textSize * proportion // from RelativeSizeSpan
textPaint.color = color // from ForegroundColorSpan
}
}

22
app/src/main/java/cash/z/ecc/android/ui/util/DebugFileTwig.kt

@ -0,0 +1,22 @@
package cash.z.ecc.android.ui.util
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.util.TroubleshootingTwig
import okio.appendingSink
import okio.buffer
import java.io.File
class DebugFileTwig(fileName: String = "developer_log.txt") : TroubleshootingTwig(formatter = spiffy(6)) {
val file = File("${ZcashWalletApp.instance.filesDir}/logs", fileName)
override fun twig(logMessage: String, priority: Int) {
super.twig(logMessage, priority)
appendToFile(formatter(logMessage))
}
private fun appendToFile(message: String) {
file.appendingSink().buffer().use {
it.writeUtf8("$message\n")
}
}
}

54
app/src/main/java/cash/z/ecc/android/ui/util/MemoUtil.kt

@ -0,0 +1,54 @@
package cash.z.ecc.android.ui.util
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import java.nio.charset.StandardCharsets
/**
* The prefix that this wallet uses whenever the user chooses to include their address in the memo.
* This is the one we standardize around.
*/
const val INCLUDE_MEMO_PREFIX_STANDARD = "Reply-To:"
/**
* The non-standard prefixes that we will parse if other wallets send them our way.
*/
val INCLUDE_MEMO_PREFIXES_RECOGNIZED = arrayOf(
INCLUDE_MEMO_PREFIX_STANDARD, // standard
"reply-to", // standard w/o colon
"reply to:", // space instead of dash
"reply to", // space instead of dash w/o colon
"sent from:", // previous standard
"sent from" // previous standard w/o colon
)
// TODO: move this to the SDK
inline fun ByteArray?.toUtf8Memo(): String {
// TODO: make this more official but for now, this will do
return if (this == null || this.isEmpty() || this[0] >= 0xF5) "" else try {
// trim empty and "replacement characters" for codes that can't be represented in unicode
String(this, StandardCharsets.UTF_8).trim('\u0000', '\uFFFD')
} catch (t: Throwable) {
"Unable to parse memo."
}
}
object MemoUtil {
suspend fun findAddressInMemo(tx: ConfirmedTransaction?, addressValidator: suspend (String) -> Boolean): String? {
// note: t-addr min length is 35, plus we're expecting prefixes
return tx?.memo?.toUtf8Memo()?.takeUnless { it.length < 35 }?.let { memo ->
// start with what we accept as prefixes
INCLUDE_MEMO_PREFIXES_RECOGNIZED.mapNotNull {
val maybeMemo = memo.substringAfterLast(it)
if (addressValidator(maybeMemo)) maybeMemo else null
}.firstOrNull { !it.isNullOrBlank() }
}
}
// note: cannot use substringAfterLast, directly because we want to ignore case. perhaps submit a feature request to kotlin for adding `ignoreCase`
private fun String.substringAfterLast(prefix: String): String {
return lastIndexOf(prefix, ignoreCase = true).takeUnless { it == -1 }?.let { i ->
substring(i + prefix.length).trimStart()
} ?: ""
}
}

47
app/src/main/java/cash/z/ecc/android/ui/util/PermissionFragment.kt

@ -0,0 +1,47 @@
package cash.z.ecc.android.ui.util
//
// import android.Manifest
// import android.content.Context
// import android.content.pm.PackageManager
// import android.os.Bundle
// import android.widget.Toast
// import androidx.core.content.ContextCompat
// import androidx.fragment.app.Fragment
// import cash.z.ecc.android.ui.MainActivity
//
// class PermissionFragment : Fragment() {
//
// val activity get() = context as MainActivity
//
// override fun onCreate(savedInstanceState: Bundle?) {
// super.onCreate(savedInstanceState)
// if (!hasPermissions(activity)) {
// requestPermissions(PERMISSIONS, REQUEST_CODE)
// } else {
// activity.openCamera()
// }
// }
//
// override fun onRequestPermissionsResult(
// requestCode: Int, permissions: Array<String>, grantResults: IntArray
// ) {
// super.onRequestPermissionsResult(requestCode, permissions, grantResults)
//
// if (requestCode == REQUEST_CODE) {
// if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
// activity.openCamera()
// } else {
// Toast.makeText(context, "Camera request denied", Toast.LENGTH_LONG).show()
// }
// }
// }
//
// companion object {
// private const val REQUEST_CODE = 101
// private val PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
//
// fun hasPermissions(context: Context) = PERMISSIONS.all {
// ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
// }
// }
// }

200
app/src/main/java/cash/z/ecc/android/util/Twig.kt

@ -0,0 +1,200 @@
@file:Suppress("NOTHING_TO_INLINE")
package cash.z.ecc.android.util
import java.util.concurrent.CopyOnWriteArraySet
import kotlin.math.roundToLong
internal typealias Leaf = String
/**
* A tiny log.
*/
interface Twig {
/**
* Log the message. Simple.
*/
fun twig(logMessage: String = "", priority: Int = 0)
/**
* Bundles twigs together.
*/
operator fun plus(twig: Twig): Twig {
// if the other twig is a composite twig, let it handle the addition
return if (twig is CompositeTwig) twig.plus(this) else CompositeTwig(mutableListOf(this, twig))
}
companion object {
/**
* Access the trunk corresponding to this twig.
*/
val trunk get() = Bush.trunk
/**
* Convenience function to just turn this thing on. Twigs are silent by default so this is
* most useful to enable developer logging at the right time.
*/
fun enabled(isEnabled: Boolean) {
if (isEnabled) plant(TroubleshootingTwig()) else plant(SilentTwig())
}
/**
* Plants the twig, making it the one and only bush. Twigs can be bundled together to create
* the appearance of multiple bushes (i.e `Twig.plant(twigA + twigB + twigC)`) even though
* there's only ever one bush.
*/
fun plant(rootTwig: Twig) {
Bush.trunk = rootTwig
}
/**
* Generate a leaf on the bush. Leaves show up in every log message as tags until they are
* clipped.
*/
fun sprout(leaf: Leaf) = Bush.leaves.add(leaf)
/**
* Clip a leaf from the bush. Clipped leaves no longer appear in logs.
*/
fun clip(leaf: Leaf) = Bush.leaves.remove(leaf)
/**
* Clip all leaves from the bush.
*/
fun prune() = Bush.leaves.clear()
}
}
/**
* A collection of tiny logs (twigs) consisting of one trunk and maybe some leaves. There can only
* ever be one trunk. Trunks are created by planting a twig. Whenever a leaf sprouts, it will appear
* as a tag on every log message until clipped.
*
* @see [Twig.plant]
* @see [Twig.sprout]
* @see [Twig.clip]
*/
object Bush {
var trunk: Twig = SilentTwig()
val leaves: MutableSet<Leaf> = CopyOnWriteArraySet<Leaf>()
}
/**
* Makes a tiny log.
*/
inline fun twig(message: String, priority: Int = 0) = Bush.trunk.twig(message, priority)
/**
* Makes an exception.
*/
inline fun twig(t: Throwable) = t.stackTraceToString().lines().forEach {
twig(it)
}
/**
* Times a tiny log.
*/
inline fun <R> twig(logMessage: String, priority: Int = 0, block: () -> R): R = Bush.trunk.twig(logMessage, priority, block)
/**
* Meticulously times a tiny task.
*/
inline fun <R> twigTask(logMessage: String, priority: Int = 0, block: () -> R): R = Bush.trunk.twigTask(logMessage, priority, block)
/**
* A tiny log that does nothing. No one hears this twig fall in the woods.
*/
class SilentTwig : Twig {
/**
* Shh.
*/
override fun twig(logMessage: String, priority: Int) {
// shh
}
}
/**
* A tiny log for detecting troubles. Aim at your troubles and pull the twigger.
*
* @param formatter a formatter for the twigs. The default one is pretty spiffy.
* @param printer a printer for the twigs. The default is System.err.println.
*/
open class TroubleshootingTwig(
val formatter: (String) -> String = spiffy(6),
val printer: (String) -> Any = System.err::println,
val minPriority: Int = 0
) : Twig {
/**
* Actually print and format the log message, unlike the SilentTwig, which does nothing.
*/
override fun twig(logMessage: String, priority: Int) {
if (priority >= minPriority) printer(formatter(logMessage))
}
companion object {
/**
* A tiny log formatter that makes twigs pretty spiffy.
*
* @param stackFrame the stack frame from which we try to derive the class. This can vary depending
* on how the code is called so we expose it for flexibility. Jiggle the handle on this whenever the
* line numbers appear incorrect.
*/
fun spiffy(stackFrame: Int = 4, tag: String = "@TWIG"): (String) -> String = { logMessage: String ->
val stack = Thread.currentThread().stackTrace[stackFrame]
val time = String.format("$tag %1\$tD %1\$tI:%1\$tM:%1\$tS.%1\$tN", System.currentTimeMillis())
val className = stack.className.split(".").lastOrNull()?.split("\$")?.firstOrNull()
val tags = Bush.leaves.joinToString(" #", "#")
"$time[$className:${stack.lineNumber}]($tags) $logMessage"
}
}
}
/**
* Since there can only ever be one trunk on the bush of twigs, this class lets
* you cheat and make that trunk be a bundle of twigs.
*/
open class CompositeTwig(open val twigBundle: MutableList<Twig>) :
Twig {
override operator fun plus(twig: Twig): Twig {
if (twig is CompositeTwig) twigBundle.addAll(twig.twigBundle) else twigBundle.add(twig)
return this
}
override fun twig(logMessage: String, priority: Int) {
for (twig in twigBundle) {
twig.twig(logMessage, priority)
}
}
}
/**
* Times a tiny log. Execute the block of code on the clock.
*/
inline fun <R> Twig.twig(logMessage: String, priority: Int = 0, block: () -> R): R {
val start = System.currentTimeMillis()
val result = block()
val elapsed = (System.currentTimeMillis() - start)
twig("$logMessage | ${elapsed}ms", priority)
return result
}
/**
* A tiny log task. Execute the block of code with some twigging around the outside. For silent
* twigs, this adds a small amount of overhead at the call site but still avoids logging.
*
* note: being an extension function (i.e. static rather than a member of the Twig interface) allows
* this function to be inlined and simplifies its use with suspend functions
* (otherwise the function and its "block" param would have to suspend)
*/
inline fun <R> Twig.twigTask(logMessage: String, priority: Int = 0, block: () -> R): R {
twig("$logMessage - started | on thread ${Thread.currentThread().name}", priority)
val start = System.nanoTime()
val result = block()
val elapsed = ((System.nanoTime() - start) / 1e5).roundToLong() / 10L
twig("$logMessage - completed | in $elapsed ms" + " on thread ${Thread.currentThread().name}", priority)
return result
}

9
app/src/main/res/anim/anim_enter_from_bottom.xml

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/decelerate_interpolator"
android:fillAfter="true">
<translate
android:fromXDelta="0%" android:toXDelta="0%"
android:fromYDelta="100%" android:toYDelta="0%"
android:duration="300" />
</set>

9
app/src/main/res/anim/anim_enter_from_left.xml

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/decelerate_interpolator"
android:fillAfter="true">
<translate
android:fromXDelta="-100%" android:toXDelta="0%"
android:fromYDelta="0%" android:toYDelta="0%"
android:duration="300" />
</set>

9
app/src/main/res/anim/anim_enter_from_right.xml

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/decelerate_interpolator"
android:fillAfter="true">
<translate
android:fromXDelta="100%" android:toXDelta="0%"
android:fromYDelta="0%" android:toYDelta="0%"
android:duration="300" />
</set>

10
app/src/main/res/anim/anim_exit_to_left.xml

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/decelerate_interpolator"
android:fillAfter="true">
<translate
android:interpolator="@android:interpolator/decelerate_cubic"
android:fromXDelta="0%" android:toXDelta="-100%"
android:fromYDelta="0%" android:toYDelta="0%"
android:duration="300"/>
</set>

10
app/src/main/res/anim/anim_exit_to_right.xml

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/decelerate_interpolator"
android:fillAfter="true">
<translate
android:interpolator="@android:interpolator/decelerate_cubic"
android:fromXDelta="0%" android:toXDelta="100%"
android:fromYDelta="0%" android:toYDelta="0%"
android:duration="300"/>
</set>

6
app/src/main/res/anim/anim_fade_in.xml

@ -0,0 +1,6 @@
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/accelerate_interpolator"
android:fromAlpha="0.0"
android:toAlpha="1.0"
android:duration="200"
/>

6
app/src/main/res/anim/anim_fade_in_scanner.xml

@ -0,0 +1,6 @@
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/accelerate_interpolator"
android:fromAlpha="0.0"
android:toAlpha="1.0"
android:duration="700"
/>

6
app/src/main/res/anim/anim_fade_out.xml

@ -0,0 +1,6 @@
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/accelerate_interpolator"
android:fromAlpha="1.0"
android:toAlpha="0.0"
android:duration="200"
/>

6
app/src/main/res/anim/anim_fade_out_address.xml

@ -0,0 +1,6 @@
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/accelerate_interpolator"
android:fromAlpha="1.0"
android:toAlpha="0.0"
android:duration="250"
/>

6
app/src/main/res/anim/anim_fade_out_medium.xml

@ -0,0 +1,6 @@
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/accelerate_interpolator"
android:fromAlpha="1.0"
android:toAlpha="0.0"
android:duration="300"
/>

5
app/src/main/res/color/selector_button_text_dark.xml

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="false" android:color="@color/text_dark"/>
<item android:state_pressed="true" android:color="@color/text_light" />
</selector>

5
app/src/main/res/color/selector_button_text_light.xml

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="false" android:color="@color/text_light"/>
<item android:state_pressed="true" android:color="@color/text_dark" />
</selector>

5
app/src/main/res/color/selector_button_text_light_dimmed.xml

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="false" android:color="@color/text_light_dimmed"/>
<item android:state_pressed="true" android:color="@color/text_dark" />
</selector>

5
app/src/main/res/color/selector_button_text_light_to_dimmed.xml

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="false" android:color="@color/text_light"/>
<item android:state_pressed="true" android:color="@color/text_light_dimmed" />
</selector>

6
app/src/main/res/color/selector_feedback_button.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_activated="false" android:color="@color/text_light"/>
<item android:state_activated="true" android:color="@color/colorPrimary" />
</selector>

8
app/src/main/res/color/selector_primary_button_activatable.xml

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="false" android:color="#7C5A00" />
<item android:state_activated="false" android:color="#7C5A00"/>
<item android:state_activated="true" android:color="@color/colorPrimary" />
</selector>

8
app/src/main/res/color/selector_secondary_button_activatable.xml

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="false" android:color="@color/text_light_dimmed" />
<item android:state_activated="false" android:color="@color/text_light_dimmed"/>
<item android:state_activated="true" android:color="@color/text_light" />
</selector>

10
app/src/main/res/drawable-anydpi/ic_settings.xml

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/zcashWhite_50">
<path
android:fillColor="@android:color/white"
android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
</vector>

23
app/src/main/res/drawable-v24/ic_launcher_background.xml

@ -0,0 +1,23 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:pathData="M0,0h108v108h-108z"
android:strokeWidth="1"
android:fillType="evenOdd"
android:strokeColor="#00000000">
<aapt:attr name="android:fillColor">
<gradient
android:gradientRadius="92.96752"
android:centerX="54"
android:centerY="36.01165"
android:type="radial">
<item android:offset="0" android:color="@color/app_icon_background_0"/>
<item android:offset="1" android:color="@color/app_icon_background_1"/>
</gradient>
</aapt:attr>
</path>
</vector>

12
app/src/main/res/drawable-v24/ic_launcher_foreground.xml

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
android:viewportWidth="350"
android:viewportHeight="350"
android:width="350dp"
android:height="350dp">
<path
android:pathData="M270.8 175c-26 -32.3 5 -60 37.2 -64.1c-32 -5.7 -39.1 -2.3 -58.9 13.6c-4.8 -11.2 52.9 -98.5 52.9 -98.5s-82.9 84.7 -101.9 82.9c-24 -2.3 -26.9 -71.6 -27.2 -88.1c-0.3 16.4 -3.2 85.8 -27.2 88.1C126.7 110.8 43.8 26 43.8 26s57.8 87.2 52.9 98.5c-19.8 -15.9 -26.9 -19.3 -58.9 -13.6C70.1 115 101 142.7 75 175c61.7 -8 6.8 52.3 -13.6 63c80.2 -6.9 81.4 48.8 77.5 71.6c28.5 -1 34 29.7 34 29.7s5.6 -30.7 34 -29.7c-3.9 -22.8 -2.7 -78.5 77.5 -71.6C264 227.3 209.1 167 270.8 175zM164.9 115.5c0 -3.7 3 -6.7 6.7 -6.7c0 0 0 0 0 0c3.7 0 6.7 3 6.7 6.7v22.7c0 0 -1.7 -4 -7 -4c-4.6 0 -6.4 4 -6.4 4V115.5zM164.9 154.7c0 -3.7 3 -6.7 6.7 -6.7c0 0 0 0 0 0c3.7 0 6.7 3 6.7 6.7v26.5c0 2.1 -1.3 2.5 -2.3 2.5c-1 0 -1.2 -0.1 -4.1 -3.3c-2.9 -3.2 -7 -8.1 -7 -8.1V154.7zM166.1 196.4c1.5 -0.6 2.4 0.5 3.6 2c1.2 1.4 1.6 1.8 1.6 1.8c2.9 3.2 7 8.1 7 8.1v12.1c0 3.7 -3 6.7 -6.7 6.7c-3.7 0 -6.7 -3 -6.7 -6.7v-21.1C164.9 198.3 164.6 197 166.1 196.4zM138 123.4c0 -3.7 3 -6.7 6.7 -6.7s6.7 3 6.7 6.7v55.8c0 3.7 -3 6.7 -6.7 6.7s-6.7 -3 -6.7 -6.7V123.4zM125.6 232.9c0 3.7 -3 6.7 -6.6 6.7h-0.2c-3.7 0 -6.6 -3 -6.6 -6.7v-94.8c0 -3.7 3 -6.7 6.6 -6.7h0.2c3.7 0 6.6 3 6.6 6.7V232.9zM138 246.7v-41c0 -3.7 3 -6.7 6.8 -6.7c3.7 0 6.8 3 6.8 6.7v41c0 3.7 -3 6.7 -6.8 6.7C141 253.3 138 250.4 138 246.7zM162.2 302.3c-2.7 1.9 -7.5 -0.5 -10.7 -5.2c-3.2 -4.7 -3.6 -10.1 -0.8 -11.9c2.7 -1.9 7.5 0.5 10.7 5.2C164.6 295.1 164.9 300.4 162.2 302.3zM178.4 261c0 3.7 -3 6.7 -6.8 6.7c-3.7 0 -6.8 -3 -6.8 -6.7v-23.1c0 0 2 4 6.8 4c5.1 0 6.8 -4 6.8 -4V261zM194.4 297.1c-3.2 4.7 -8 7 -10.7 5.2c-2.7 -1.9 -2.4 -7.2 0.8 -11.9c3.2 -4.7 8 -7 10.7 -5.2C197.9 287 197.6 292.4 194.4 297.1zM206 246.7c0 3.7 -2.9 6.7 -6.5 6.7H199c-3.6 0 -6.5 -3 -6.5 -6.7v-60.9c0 -3.7 2.9 -6.7 6.5 -6.7h0.5c3.6 0 6.5 3 6.5 6.7V246.7zM206 158.9c0 3.8 -3 6.9 -6.8 6.9c-3.8 0 -6.8 -3.1 -6.8 -6.9v-35.3c0 -3.8 3 -6.9 6.8 -6.9c3.8 0 6.8 3.1 6.8 6.9V158.9zM233.7 138.1l0 94.8c0 3.7 -3 6.7 -6.6 6.7h-0.2c-3.7 0 -6.6 -3 -6.6 -6.7v-94.8c0 -3.7 3 -6.7 6.6 -6.7h0.2C230.7 131.4 233.7 134.4 233.7 138.1L233.7 138.1z"
android:fillColor="#FFFFFF" />
<path
android:pathData="M188.7 55.2c0.4 3.1 0.9 6.3 1.4 9.4c0.1 0.6 0.2 1.2 0.3 1.8c12.6 2.1 24.6 6.3 35.4 12.2c2.7 -2.2 5.6 -4.6 8.9 -7.4C220.8 63.1 205.3 57.5 188.7 55.2zM287.1 132.2c-3.5 2.3 -6.4 4.9 -8.6 7.7c4.4 12.1 6.8 25.1 6.8 38.6c0 13.5 -2.4 26.5 -6.7 38.5c3.3 3 6.4 5.4 8.9 7.2c5.6 -14.2 8.7 -29.6 8.7 -45.7C296 162.2 292.9 146.5 287.1 132.2zM125.5 282.4c-16.1 -7.2 -30.2 -17.9 -41.3 -31.3c-3.1 -0.3 -6.3 -0.4 -9.7 -0.4c-1.4 0 -2.8 0 -4.2 0.1c14.1 19.7 33.7 35.1 56.6 43.9C126.8 290.9 126.4 286.7 125.5 282.4zM56.6 130.9c-6.1 14.7 -9.5 30.8 -9.5 47.7c0 16.7 3.3 32.6 9.3 47.1c2.3 -1.4 5.5 -3.7 8.9 -6.8c-4.8 -12.5 -7.4 -26.1 -7.4 -40.3c0 -14.4 2.7 -28.1 7.6 -40.8C63.1 135.2 60.1 132.9 56.6 130.9zM271.4 250.7c-4.5 0 -8.7 0.2 -12.7 0.8c-10.4 12.4 -23.3 22.5 -38 29.6c-1.1 4.3 -1.6 8.7 -1.7 12.6c21.7 -9 40.4 -24 53.9 -42.9C272.4 250.7 271.9 250.7 271.4 250.7zM110.1 70.4c3.2 2.8 6.2 5.3 8.9 7.5c11.2 -5.9 23.5 -10 36.6 -11.8c0.1 -0.4 0.2 -0.9 0.2 -1.3c0.6 -3.3 1.1 -6.6 1.5 -9.8C140.2 56.8 124.3 62.2 110.1 70.4z"
android:fillColor="#FFFFFF" />
</vector>

6
app/src/main/res/drawable/background_balance_detail_amounts_container.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="10dp" />
<solid android:color="@color/background_banner"/>
</shape>

5
app/src/main/res/drawable/background_balance_details_total.xml

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#CF000000"/>
<stroke android:color="#60000000" android:width="4dp" />
</shape>

5
app/src/main/res/drawable/background_balance_details_transparent.xml

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#CF868686"/>
<stroke android:color="#60000000" android:width="4dp" />
</shape>

7
app/src/main/res/drawable/background_banner.xml

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="10dp" />
<stroke android:width="1dp" android:color="@color/background_banner_stroke"/>
<solid android:color="@color/background_banner"/>
</shape>

7
app/src/main/res/drawable/background_banner_large.xml

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="16dp" />
<stroke android:width="1dp" android:color="#282828"/>
<solid android:color="@color/background_banner"/>
</shape>

10
app/src/main/res/drawable/background_button_rounded.xml

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="34dp" />
<gradient
android:startColor="@color/zcashYellow_dark"
android:endColor="@color/zcashYellow"
android:angle="180"
android:type="linear" />
</shape>

9
app/src/main/res/drawable/background_circle.xml

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<size
android:width="48dp"
android:height="48dp"/>
<solid
android:color="@color/zcashWhite_60" />
</shape>

9
app/src/main/res/drawable/background_circle_solid.xml

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<size
android:width="48dp"
android:height="48dp"/>
<solid
android:color="#fff" />
</shape>

29
app/src/main/res/drawable/background_footer.xml

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list
xmlns:android="http://schemas.android.com/apk/res/android">
<item android:height="16dp">
<shape>
<corners
android:bottomLeftRadius="10dp"
android:bottomRightRadius="10dp"
android:topLeftRadius="0dp"
android:topRightRadius="0dp" />
<solid android:color="@color/text_light_dimmed" />
</shape>
</item>
<item
android:end="1dp"
android:start="1dp"
android:left="1dp"
android:right="1dp"
android:bottom="1dp"
android:height="15dp">
<shape>
<corners
android:bottomLeftRadius="10dp"
android:bottomRightRadius="10dp"
android:topLeftRadius="0dp"
android:topRightRadius="0dp" />
<solid android:color="@color/background_banner" />
</shape>
</item></layer-list>

8
app/src/main/res/drawable/background_gradient_balance_details.xml

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="8dp" />
<gradient
android:angle="90"
android:endColor="#2d2d33"
android:startColor="#2a2933" />
</shape>

7
app/src/main/res/drawable/background_gradient_bottom.xml

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:angle="270"
android:endColor="@color/background_banner"
android:startColor="@android:color/transparent" />
</shape>

27
app/src/main/res/drawable/background_header.xml

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list
xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape
xmlns:android="http://schemas.android.com/apk/res/android">
<corners
android:bottomLeftRadius="0dp"
android:bottomRightRadius="0dp"
android:topLeftRadius="10dp"
android:topRightRadius="10dp" />
<solid android:color="@color/text_light_dimmed" />
</shape>
</item>
<item
android:end="1dp"
android:start="1dp"
android:top="1dp">
<shape>
<corners
android:bottomLeftRadius="0dp"
android:bottomRightRadius="0dp"
android:topLeftRadius="10dp"
android:topRightRadius="10dp" />
<solid android:color="@color/background_banner" />
</shape>
</item></layer-list>

11
app/src/main/res/drawable/background_home.xml

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:endColor="#000000"
android:startColor="#28282C"
android:type="radial"
android:centerY="0.36"
android:centerX="0.50"
android:gradientRadius="640dp"/>
</shape>

6
app/src/main/res/drawable/background_indicator_failed.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="4dp" />
<gradient android:startColor="@color/colorPrimary" android:endColor="@color/colorPrimaryDark" />
</shape>

10
app/src/main/res/drawable/background_indicator_inbound.xml

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="4dp" />
<gradient
android:angle="270"
android:centerColor="@color/zcashGreen"
android:endColor="@color/zcashBlue"
android:startColor="@color/zcashGreen" />
</shape>

9
app/src/main/res/drawable/background_indicator_outbound.xml

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="4dp" />
<gradient
android:angle="270"
android:endColor="@color/colorPrimaryMedium"
android:startColor="@color/colorPrimary" />
</shape>

9
app/src/main/res/drawable/background_indicator_unknown.xml

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="4dp" />
<gradient
android:angle="270"
android:endColor="@color/zcashWhite_12"
android:startColor="@color/zcashWhite_60" />
</shape>

11
app/src/main/res/drawable/background_send_final.xml

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:endColor="@color/colorPrimaryDark"
android:startColor="@color/colorPrimary"
android:type="radial"
android:centerY="0.36"
android:centerX="0.50"
android:gradientRadius="640dp"/>
</shape>

7
app/src/main/res/drawable/background_title_primary.xml

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="10dp" />
<stroke android:width="1dp" android:color="#282828"/>
<solid android:color="#171717"/>
</shape>

7
app/src/main/res/drawable/bg_chip_view.xml

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="3dp" />
<stroke android:width="1dp" android:color="#282828"/>
<solid android:color="@color/background_banner"/>
</shape>

7
app/src/main/res/drawable/chip_details_background.xml

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/zcashGray"/>
<corners android:radius="2dp"/>
</shape>

5
app/src/main/res/drawable/ic_account_circle.xml

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,5c1.66,0 3,1.34 3,3s-1.34,3 -3,3 -3,-1.34 -3,-3 1.34,-3 3,-3zM12,19.2c-2.5,0 -4.71,-1.28 -6,-3.22 0.03,-1.99 4,-3.08 6,-3.08 1.99,0 5.97,1.09 6,3.08 -1.29,1.94 -3.5,3.22 -6,3.22z"/>
</vector>

138
app/src/main/res/drawable/ic_address_qr.xml

@ -0,0 +1,138 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="42dp"
android:height="42dp"
android:viewportWidth="42"
android:viewportHeight="42">
<path
android:pathData="M14.5052,41.6579L0,41.6579L0,26.9363C0.3031,26.9363 0.6452,26.8627 0.9872,26.8627C5.1353,26.8627 9.2833,26.8627 13.4313,26.8367C14.319,26.8367 14.6134,27.0705 14.6047,27.9841C14.5614,32.2058 14.6047,36.4274 14.6047,40.6491C14.5961,40.9781 14.5355,41.3202 14.5052,41.6579ZM7.2569,38.7526C8.4823,38.7526 9.7076,38.7526 10.933,38.7526C11.5738,38.7526 11.8423,38.5447 11.8336,37.8866C11.8336,35.4402 11.8336,32.9895 11.8336,30.5258C11.8336,29.9109 11.6647,29.5862 10.9676,29.5905C8.4779,29.5905 5.9926,29.5905 3.5072,29.5905C2.9184,29.5905 2.6412,29.807 2.6629,30.4132C2.6629,32.8986 2.6629,35.3882 2.6629,37.8736C2.6629,38.5404 2.9357,38.7396 3.5678,38.7396C4.8062,38.7309 6.0315,38.7526 7.2569,38.7526Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M14.5181,0.0909C14.5441,0.4546 14.5918,0.7707 14.5918,1.0825C14.5918,5.2652 14.5918,9.4478 14.5918,13.6392C14.6047,14.2887 14.5485,14.7216 13.6998,14.7216C9.3353,14.6913 4.9751,14.7216 0.6105,14.7216C0.4044,14.6957 0.2004,14.6552 0,14.6004L0,0.0909L14.5181,0.0909ZM7.2396,11.9462C8.426,11.9462 9.6167,11.9202 10.8031,11.9462C11.5175,11.9722 11.8553,11.7643 11.8423,10.9806C11.8076,8.6035 11.812,6.2177 11.8423,3.8449C11.8423,3.1565 11.6041,2.888 10.8984,2.8923C8.488,2.9184 6.0748,2.9184 3.6588,2.8923C2.992,2.8923 2.6672,3.0742 2.6759,3.806C2.7019,6.2221 2.6759,8.6338 2.6759,11.0499C2.6759,11.6951 2.914,11.9548 3.5635,11.9419C4.7889,11.9245 6.0142,11.9462 7.2396,11.9462Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M26.8454,0.0476L41.3505,0.0476C41.3852,0.407 41.4414,0.7491 41.4414,1.0911C41.4414,5.2435 41.4414,9.3915 41.4631,13.5396C41.4631,14.4056 41.2293,14.7303 40.3157,14.7173C36.1287,14.674 31.946,14.687 27.759,14.7173C27.0575,14.7173 26.7198,14.5701 26.7285,13.7647C26.7588,9.4348 26.7285,5.1049 26.7501,0.7751C26.7674,0.5309 26.7992,0.288 26.8454,0.0476ZM29.5342,7.3262C29.5342,8.5862 29.5342,9.8505 29.5342,11.1105C29.5342,11.7037 29.7507,11.9548 30.3569,11.9506C32.8466,11.9506 35.332,11.9289 37.8173,11.9506C38.4841,11.9506 38.6833,11.6864 38.6833,11.0499C38.6631,8.6338 38.6631,6.2192 38.6833,3.806C38.6833,3.1652 38.4668,2.901 37.8173,2.9053C35.3666,2.9053 32.9159,2.9313 30.4565,2.9053C29.7377,2.9053 29.5039,3.1868 29.5256,3.8709C29.5429,5.0184 29.5212,6.1744 29.5212,7.3262L29.5342,7.3262Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M41.4371,23.6326C40.9002,23.6586 40.5452,23.6802 40.1901,23.6889C38.4885,23.7322 38.4581,23.3165 38.4581,25.4641C38.4581,26.5466 38.4581,27.6291 38.4581,28.7072C38.4581,29.3264 38.272,29.5992 37.5922,29.6252C35.4272,29.7118 35.4272,29.7377 35.5008,31.972C35.5008,32.1798 35.5441,32.4049 35.5831,32.76C36.0849,32.7904 36.5881,32.7904 37.0899,32.76C38.2157,32.6128 38.601,33.1194 38.4538,34.1802C38.4343,34.3948 38.4343,34.6108 38.4538,34.8254C38.4885,35.4315 38.2113,35.6524 37.6138,35.6394C36.2412,35.6091 34.873,35.6047 33.5047,35.6394C32.8293,35.6394 32.6388,35.3796 32.6388,34.7474C32.6691,33.4485 32.5825,32.1495 32.6691,30.8505C32.7383,29.8113 32.327,29.5515 31.3701,29.5992C29.8676,29.6641 28.3652,29.5992 26.7934,29.5992L26.7934,26.9146C27.1961,26.9146 27.5771,26.8497 27.9625,26.8497C30.1967,26.8497 32.4309,26.8237 34.6652,26.8497C35.4229,26.8497 35.674,26.5856 35.6524,25.8408C35.6047,24.5072 35.687,23.1736 35.6221,21.84C35.5831,21.0043 35.9035,20.8052 36.6829,20.8095C38.5924,20.8095 38.6313,20.8095 38.627,18.8784C38.627,18.0946 38.9258,17.8522 39.6445,17.9171C40.039,17.8943 40.4348,17.9088 40.8266,17.9604C41.0604,18.0254 41.3852,18.3328 41.3938,18.5449C41.4544,20.2033 41.4371,21.8573 41.4371,23.6326Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M20.5367,17.601C17.7829,17.9431 17.7829,17.9431 17.7829,15.3278C17.7829,14.5008 17.7829,13.6695 17.7829,12.8381C17.7829,12.2016 18.0124,11.9722 18.6489,11.9332C20.8398,11.8466 20.8138,11.8206 20.7359,9.6773C20.7099,9.0148 20.4674,8.8113 19.8353,8.7897C17.7656,8.7074 17.7656,8.6858 17.7656,6.5988C17.7656,4.7586 17.7915,2.9184 17.7656,1.0781C17.7656,0.3247 17.9388,-0.0346 18.7918,0.0043C20.3332,0.0693 21.879,0.0043 23.5546,0.0043C23.5546,0.8703 23.5893,1.6021 23.5287,2.3425C23.5287,2.533 23.2082,2.7278 23.0004,2.8707C22.9008,2.94 22.719,2.8707 22.5674,2.8707C20.5497,2.9313 20.1427,3.4379 20.606,5.4297C20.7495,5.7166 21.0214,5.9177 21.3377,5.9709C22.0583,6.02 22.7811,6.0286 23.5027,5.9969C23.5287,6.6334 23.572,7.1616 23.5763,7.6899C23.5763,9.712 23.5763,11.734 23.5763,13.7518C23.5763,14.3753 23.4074,14.648 22.7103,14.674C20.619,14.752 20.619,14.7823 20.5454,16.8779C20.593,17.1247 20.5627,17.3196 20.5367,17.601Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M20.6146,20.7402C20.6146,22.3942 20.5757,24.0526 20.6146,25.7066C20.6146,26.434 20.3592,26.6895 19.6751,26.6332C19.387,26.6072 19.0971,26.6072 18.8091,26.6332C18.0903,26.6895 17.7872,26.421 17.7699,25.6546C17.7223,23.6889 17.6876,23.6889 15.7219,23.6889C13.847,23.6889 11.9722,23.6456 10.093,23.6889C9.1491,23.7192 8.7464,23.5027 8.794,22.4462C8.8676,20.4111 8.9975,20.6363 7.0144,20.7142C5.9493,20.7489 5.854,20.2336 5.8757,19.4153C5.8973,18.5969 5.7891,17.8132 7.0534,17.9128C8.2417,17.9821 9.433,17.9821 10.6212,17.9128C11.3746,17.8868 11.6128,18.1856 11.6258,18.926C11.6604,20.8311 11.6907,20.8398 13.6348,20.8311C15.973,20.8311 18.3111,20.7922 20.6493,20.7705L20.6146,20.7402Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M20.8138,41.6276L20.8138,38.7526C23.4421,38.7526 26.0097,38.7223 28.547,38.7526C29.413,38.7526 29.7551,38.5231 29.7421,37.6052C29.7161,35.5485 29.3913,35.752 31.6645,35.7433C31.907,35.7433 32.1538,35.7866 32.4612,35.8126L32.4612,41.6276L20.8138,41.6276Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M20.6017,32.7817C21.2463,32.8168 21.8924,32.8168 22.5371,32.7817C23.4377,32.6777 23.6975,33.0718 23.6023,33.8944C23.6259,34.2512 23.6142,34.6094 23.5676,34.9639C23.481,35.2237 23.1736,35.4229 22.9441,35.6221C22.8792,35.6784 22.732,35.6221 22.6237,35.6221C20.6579,35.674 20.6233,35.674 20.619,37.6008C20.619,38.3802 20.3245,38.5794 19.6058,38.5924C17.7872,38.6227 17.7829,38.6487 17.7829,36.8604C17.7829,34.8773 17.8089,32.8986 17.7829,30.9198C17.7829,30.0885 18.0427,29.7031 18.9,29.807C19.2241,29.8352 19.5501,29.8352 19.8742,29.807C20.4241,29.781 20.6493,30.0235 20.6233,30.5821C20.5757,31.2705 20.6017,31.9503 20.6017,32.7817Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M32.5262,23.6759C31.7872,23.7053 31.0472,23.6952 30.3093,23.6456C30.1014,23.6456 29.8157,23.2775 29.7637,23.0351C29.6078,22.2903 29.7984,21.2555 29.3697,20.8441C28.941,20.4328 27.9365,20.7099 27.1831,20.6753C27.0354,20.6404 26.8906,20.5941 26.7501,20.5367C26.7105,19.909 26.7206,19.2792 26.7804,18.6532C26.8739,18.3263 27.1265,18.0688 27.4515,17.9691C28.9237,17.9041 30.4045,17.9085 31.881,17.9691C32.18,18.0529 32.4142,18.2856 32.5002,18.5839C32.5522,20.2293 32.5262,21.879 32.5262,23.6759Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M23.6196,29.5386L20.8052,29.5386L20.8052,26.8194C21.7188,26.88 22.8532,26.6375 23.3685,27.0922C23.8144,27.4862 23.5633,28.6726 23.6196,29.5386Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M35.713,41.8398L35.713,38.692C36.6353,38.7829 37.7437,38.5751 38.2763,39.034C38.7093,39.3934 38.7093,40.9089 38.3153,41.3159C37.8303,41.8095 36.7089,41.6753 35.713,41.8398Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M20.6493,20.7835C20.6796,20.1037 20.7445,19.4282 20.7315,18.7528C20.7186,18.0773 20.974,17.8868 21.5586,17.9388C21.5946,17.9409 21.6308,17.9409 21.6668,17.9388C23.6239,17.8565 23.8318,18.0687 23.5633,20.0128C23.4554,20.3339 23.1936,20.5795 22.8662,20.6666C22.118,20.7396 21.3661,20.7671 20.6146,20.7489L20.6493,20.7835Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M5.9103,20.7835C5.6635,22.0089 5.7371,23.0567 5.2522,23.5849C4.9231,23.9357 3.412,23.8274 3.1262,23.4247C2.7408,22.8878 2.6932,21.4936 3.1002,21.1515C3.6371,20.6796 4.7326,20.8788 5.9103,20.7835Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M29.6295,35.5614L26.6808,35.5614C26.7761,34.6392 26.6245,33.6 27.0532,32.9635C27.2957,32.5998 28.9194,32.5695 29.1922,32.9375C29.6425,33.5177 29.5039,34.5612 29.6295,35.5614Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M0,18.073C3.1132,17.7266 2.7625,17.7396 2.5979,20.6017L0,20.6017L0,18.073Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M23.7495,23.8404C24.2687,23.8695 24.7891,23.8695 25.3082,23.8404C26.2305,23.7278 26.6072,24.0309 26.5509,25.0181C26.486,26.5379 25.62,27.1831 24.1392,26.5856C23.937,26.4736 23.7991,26.2732 23.7668,26.0443C23.7235,25.3602 23.7495,24.6804 23.7495,23.8404Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M23.7408,32.6128C23.7719,32.085 23.7719,31.5558 23.7408,31.028C23.6153,30.0755 23.9746,29.7637 24.9359,29.8027C26.4167,29.8676 27.0705,30.8072 26.4816,32.2447C26.3607,32.4434 26.1543,32.5746 25.9231,32.5998C25.239,32.6431 24.5635,32.6128 23.7408,32.6128Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M41.6017,32.5565L38.64,32.5565C38.6963,31.6905 38.4971,30.6643 38.9085,30.0365C39.1509,29.6641 40.7183,29.6035 41.0431,29.9629C41.5194,30.5041 41.4241,31.5563 41.6017,32.5565Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M38.5361,35.8082L41.4891,35.8082C41.4068,36.7262 41.5887,37.761 41.16,38.3672C40.8959,38.7439 39.3241,38.8002 39.0254,38.4148C38.5621,37.8433 38.6833,36.8041 38.5361,35.8082Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M8.6598,34.4227C8.8893,35.3623 8.439,35.7216 7.3305,35.6437C5.8757,35.5528 5.8713,35.6437 5.8713,34.2581C5.8713,32.8076 5.8713,32.7946 7.2786,32.7903C8.6858,32.786 8.6598,32.786 8.6598,34.4227Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M7.5427,8.7984C5.893,8.7984 5.893,8.7984 5.8887,7.4474C5.8843,6.0965 5.8887,6.1138 7.3219,6.0186C8.413,5.9493 8.7637,6.287 8.7074,7.3911C8.6598,8.361 8.6295,9.0928 7.5427,8.7984Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M34.1672,6.0619C35.5052,6.0619 35.5052,6.0619 35.5052,7.4864C35.5052,8.7854 35.5052,8.7854 34.0113,8.7854C32.6431,8.7854 32.6431,8.7854 32.6474,7.3348C32.6691,6.0359 32.6691,6.0359 34.1672,6.0619Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
</vector>

9
app/src/main/res/drawable/ic_arrow_back_black_24dp.xml

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
</vector>

12
app/src/main/res/drawable/ic_background_qr.xml

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="286dp"
android:height="285dp"
android:viewportWidth="286"
android:viewportHeight="285">
<path
android:pathData="M13.406,0L272.594,0L286,13.364L286,270.914L272.594,285L13.406,285L0,271.641L0,13.364L13.406,0Z"
android:strokeWidth="1"
android:fillColor="#2C2C2F"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
</vector>

5
app/src/main/res/drawable/ic_baseline_done_24.xml

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"/>
</vector>

5
app/src/main/res/drawable/ic_baseline_keyboard_arrow_down_24.xml

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M7.41,8.59L12,13.17l4.59,-4.58L18,10l-6,6 -6,-6 1.41,-1.41z"/>
</vector>

5
app/src/main/res/drawable/ic_baseline_launch_24.xml

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19,19H5V5h7V3H5c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2v-7h-2v7zM14,3v2h3.59l-9.83,9.83 1.41,1.41L19,6.41V10h2V3h-7z"/>
</vector>

5
app/src/main/res/drawable/ic_cancel.xml

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2zM17,15.59L15.59,17 12,13.41 8.41,17 7,15.59 10.59,12 7,8.41 8.41,7 12,10.59 15.59,7 17,8.41 13.41,12 17,15.59z"/>
</vector>

14
app/src/main/res/drawable/ic_check_shield.xml

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="17dp"
android:height="21dp"
android:viewportWidth="17"
android:viewportHeight="21">
<path
android:pathData="M17,3.3876L17,11.6833C17,14.6427 15.3708,17.348 12.75,18.7407L8.5,21L4.25,18.7407C1.6292,17.348 0,14.6427 0,11.6833L0,3.3876L8.5,0L17,3.3876ZM8.0121,10.8519L11.9036,7L14,9.0741L8.0121,15L4,11.0298L6.0964,8.9558L8.0121,10.8519Z"
android:strokeAlpha="0.4079706"
android:strokeWidth="1"
android:fillColor="#FFFFFF"
android:fillType="evenOdd"
android:strokeColor="#00000000"
android:fillAlpha="0.4079706"/>
</vector>

12
app/src/main/res/drawable/ic_check_shielded.xml

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="42dp"
android:height="33dp"
android:viewportWidth="42"
android:viewportHeight="33">
<path
android:pathData="M16.8506,15.8889l16.3444,-15.8889l8.805,8.5555l-25.1494,24.4445l-16.8506,-16.377l8.805,-8.5555z"
android:strokeWidth="1"
android:fillColor="#666666"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
</vector>

9
app/src/main/res/drawable/ic_close_black_24dp.xml

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>

5
app/src/main/res/drawable/ic_content_copy.xml

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"/>
</vector>

5
app/src/main/res/drawable/ic_done_24dp.xml

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"/>
</vector>

24
app/src/main/res/drawable/ic_expand_memo_enabled.xml

@ -0,0 +1,24 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<path
android:pathData="M11,11m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0"
android:strokeWidth="1"
android:fillColor="#1F1F22"
android:strokeColor="@color/tx_primary"/>
<path
android:pathData="M8.575,14.125l2.3,2.3311l2.3,-2.3311l0.7,0.7095l-3,3.0405l-3,-3.0405l0.7,-0.7095z"
android:strokeWidth="1"
android:fillColor="@color/tx_primary"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M13.175,7.875l-2.3,-2.3311l-2.3,2.3311l-0.7,-0.7095l3,-3.0405l3,3.0405l-0.7,0.7095z"
android:strokeWidth="1"
android:fillColor="@color/tx_primary"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
</vector>

5
app/src/main/res/drawable/ic_info_24dp.xml

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z"/>
</vector>

48
app/src/main/res/drawable/ic_logo_landing.xml

@ -0,0 +1,48 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
android:viewportWidth="345.8"
android:viewportHeight="402.5"
android:width="345.8dp"
android:height="402.5dp">
<path
android:pathData="M270.8 175c-26 -32.3 5 -60 37.2 -64.1c-32 -5.7 -39.1 -2.3 -58.9 13.6c-4.8 -11.2 52.9 -98.5 52.9 -98.5s-82.9 84.7 -101.9 82.9c-24 -2.3 -26.9 -71.6 -27.2 -88.1c-0.3 16.4 -3.2 85.8 -27.2 88.1C126.7 110.8 43.8 26 43.8 26s57.8 87.2 52.9 98.5c-19.8 -15.9 -26.9 -19.3 -58.9 -13.6C70.1 115 101 142.7 75 175c61.7 -8 6.8 52.3 -13.6 63c80.2 -6.9 81.4 48.8 77.5 71.6c28.5 -1 34 29.7 34 29.7s5.6 -30.7 34 -29.7c-3.9 -22.8 -2.7 -78.5 77.5 -71.6C264 227.3 209.1 167 270.8 175zM164.9 115.5c0 -3.7 3 -6.7 6.7 -6.7c0 0 0 0 0 0c3.7 0 6.7 3 6.7 6.7v22.7c0 0 -1.7 -4 -7 -4c-4.6 0 -6.4 4 -6.4 4V115.5zM164.9 154.7c0 -3.7 3 -6.7 6.7 -6.7c0 0 0 0 0 0c3.7 0 6.7 3 6.7 6.7v26.5c0 2.1 -1.3 2.5 -2.3 2.5c-1 0 -1.2 -0.1 -4.1 -3.3c-2.9 -3.2 -7 -8.1 -7 -8.1V154.7zM166.1 196.4c1.5 -0.6 2.4 0.5 3.6 2c1.2 1.4 1.6 1.8 1.6 1.8c2.9 3.2 7 8.1 7 8.1v12.1c0 3.7 -3 6.7 -6.7 6.7c-3.7 0 -6.7 -3 -6.7 -6.7v-21.1C164.9 198.3 164.6 197 166.1 196.4zM138 123.4c0 -3.7 3 -6.7 6.7 -6.7s6.7 3 6.7 6.7v55.8c0 3.7 -3 6.7 -6.7 6.7s-6.7 -3 -6.7 -6.7V123.4zM125.6 232.9c0 3.7 -3 6.7 -6.6 6.7h-0.2c-3.7 0 -6.6 -3 -6.6 -6.7v-94.8c0 -3.7 3 -6.7 6.6 -6.7h0.2c3.7 0 6.6 3 6.6 6.7V232.9zM138 246.7v-41c0 -3.7 3 -6.7 6.8 -6.7c3.7 0 6.8 3 6.8 6.7v41c0 3.7 -3 6.7 -6.8 6.7C141 253.3 138 250.4 138 246.7zM162.2 302.3c-2.7 1.9 -7.5 -0.5 -10.7 -5.2c-3.2 -4.7 -3.6 -10.1 -0.8 -11.9c2.7 -1.9 7.5 0.5 10.7 5.2C164.6 295.1 164.9 300.4 162.2 302.3zM178.4 261c0 3.7 -3 6.7 -6.8 6.7c-3.7 0 -6.8 -3 -6.8 -6.7v-23.1c0 0 2 4 6.8 4c5.1 0 6.8 -4 6.8 -4V261zM194.4 297.1c-3.2 4.7 -8 7 -10.7 5.2c-2.7 -1.9 -2.4 -7.2 0.8 -11.9c3.2 -4.7 8 -7 10.7 -5.2C197.9 287 197.6 292.4 194.4 297.1zM206 246.7c0 3.7 -2.9 6.7 -6.5 6.7H199c-3.6 0 -6.5 -3 -6.5 -6.7v-60.9c0 -3.7 2.9 -6.7 6.5 -6.7h0.5c3.6 0 6.5 3 6.5 6.7V246.7zM206 158.9c0 3.8 -3 6.9 -6.8 6.9c-3.8 0 -6.8 -3.1 -6.8 -6.9v-35.3c0 -3.8 3 -6.9 6.8 -6.9c3.8 0 6.8 3.1 6.8 6.9V158.9zM233.7 138.1l0 94.8c0 3.7 -3 6.7 -6.6 6.7h-0.2c-3.7 0 -6.6 -3 -6.6 -6.7v-94.8c0 -3.7 3 -6.7 6.6 -6.7h0.2C230.7 131.4 233.7 134.4 233.7 138.1L233.7 138.1z"
android:fillColor="#FFFFFF" />
<path
android:pathData="M188.7 55.2c0.4 3.1 0.9 6.3 1.4 9.4c0.1 0.6 0.2 1.2 0.3 1.8c12.6 2.1 24.6 6.3 35.4 12.2c2.7 -2.2 5.6 -4.6 8.9 -7.4C220.8 63.1 205.3 57.5 188.7 55.2zM287.1 132.2c-3.5 2.3 -6.4 4.9 -8.6 7.7c4.4 12.1 6.8 25.1 6.8 38.6c0 13.5 -2.4 26.5 -6.7 38.5c3.3 3 6.4 5.4 8.9 7.2c5.6 -14.2 8.7 -29.6 8.7 -45.7C296 162.2 292.9 146.5 287.1 132.2zM125.5 282.4c-16.1 -7.2 -30.2 -17.9 -41.3 -31.3c-3.1 -0.3 -6.3 -0.4 -9.7 -0.4c-1.4 0 -2.8 0 -4.2 0.1c14.1 19.7 33.7 35.1 56.6 43.9C126.8 290.9 126.4 286.7 125.5 282.4zM56.6 130.9c-6.1 14.7 -9.5 30.8 -9.5 47.7c0 16.7 3.3 32.6 9.3 47.1c2.3 -1.4 5.5 -3.7 8.9 -6.8c-4.8 -12.5 -7.4 -26.1 -7.4 -40.3c0 -14.4 2.7 -28.1 7.6 -40.8C63.1 135.2 60.1 132.9 56.6 130.9zM271.4 250.7c-4.5 0 -8.7 0.2 -12.7 0.8c-10.4 12.4 -23.3 22.5 -38 29.6c-1.1 4.3 -1.6 8.7 -1.7 12.6c21.7 -9 40.4 -24 53.9 -42.9C272.4 250.7 271.9 250.7 271.4 250.7zM110.1 70.4c3.2 2.8 6.2 5.3 8.9 7.5c11.2 -5.9 23.5 -10 36.6 -11.8c0.1 -0.4 0.2 -0.9 0.2 -1.3c0.6 -3.3 1.1 -6.6 1.5 -9.8C140.2 56.8 124.3 62.2 110.1 70.4z"
android:fillColor="#FFFFFF" />
<path
android:pathData="M50.8 373.8c-6.1 0 -10.8 -3 -11 -8.7h7.3c0.2 2.2 1.6 3.2 3.5 3.2c2 0 3.3 -1 3.3 -2.7c0 -5.2 -14.1 -2.4 -14 -12.4c0 -5.3 4.4 -8.3 10.3 -8.3c6.1 0 10.3 3.1 10.6 8.4h-7.4c-0.1 -1.8 -1.4 -2.9 -3.3 -2.9c-1.6 0 -2.9 0.8 -2.9 2.6c0 4.9 13.9 2.7 13.9 12.1C61.1 369.8 57.4 373.8 50.8 373.8z"
android:fillColor="#FFFFFF" />
<path
android:pathData="M64.7 345c0 -2.1 1.6 -3.7 4.1 -3.7c2.4 0 4.1 1.6 4.1 3.7c0 2 -1.6 3.7 -4.1 3.7C66.3 348.7 64.7 347.1 64.7 345zM65.3 351.1h6.9v22.4h-6.9V351.1z"
android:fillColor="#FFFFFF" />
<path
android:pathData="M77.2 343.8h6.9v29.7h-6.9V343.8z"
android:fillColor="#FFFFFF" />
<path
android:pathData="M99 373.8c-6.6 0 -11.3 -4.4 -11.3 -11.5c0 -7.1 4.7 -11.5 11.3 -11.5c6.5 0 11.2 4.3 11.2 11.2c0 0.6 0 1.3 -0.1 2H94.5c0.2 2.9 2 4.3 4.3 4.3c1.9 0 3 -1 3.6 -2.2h7.3C108.6 370.4 104.6 373.8 99 373.8zM94.5 360.1h8.6c0 -2.5 -1.9 -3.9 -4.2 -3.9C96.7 356.2 94.9 357.6 94.5 360.1z"
android:fillColor="#FFFFFF" />
<path
android:pathData="M129.3 361.3c0 -3.1 -1.7 -4.7 -4.3 -4.7c-2.6 0 -4.3 1.7 -4.3 4.7v12.2h-6.9v-22.4h6.9v3c1.4 -1.9 3.8 -3.2 6.8 -3.2c5.2 0 8.6 3.5 8.6 9.6v13.1h-6.8V361.3z"
android:fillColor="#FFFFFF" />
<path
android:pathData="M141.9 356.8h-2.7v-5.7h2.7v-5.5h6.9v5.5h4.5v5.7h-4.5v9c0 1.3 0.6 1.9 2.1 1.9h2.5v5.8h-3.5c-4.7 0 -7.9 -2 -7.9 -7.8V356.8z"
android:fillColor="#FFFFFF" />
<path
android:pathData="M182.5 359.4c0 8.4 -5.8 14.1 -14.8 14.1h-10.6v-28.2h10.6C176.7 345.3 182.5 351 182.5 359.4zM167.3 367.5c5.2 0 8.2 -3 8.2 -8.2c0 -5.2 -3.1 -8.2 -8.2 -8.2H164v16.4H167.3z"
android:fillColor="#FFFFFF" />
<path
android:pathData="M193.2 373.5h-6.9v-22.4h6.9v3.7c1.6 -2.4 4 -4 7 -4v7.3h-1.9c-3.3 0 -5.1 1.1 -5.1 5V373.5z"
android:fillColor="#FFFFFF" />
<path
android:pathData="M212.1 350.7c3.3 0 5.7 1.5 6.9 3.5v-3.2h6.9v22.4H219v-3.2c-1.2 2 -3.6 3.5 -6.9 3.5c-5.5 0 -9.8 -4.5 -9.8 -11.6C202.2 355.2 206.6 350.7 212.1 350.7zM214.1 356.7c-2.6 0 -4.9 1.9 -4.9 5.5c0 3.6 2.3 5.6 4.9 5.6c2.6 0 4.9 -2 4.9 -5.5C219 358.7 216.7 356.7 214.1 356.7z"
android:fillColor="#FFFFFF" />
<path
android:pathData="M239.4 350.7c3.3 0 5.7 1.5 6.9 3.5v-3.2h6.9v22.4c0 6 -3.5 11.1 -11.2 11.1c-6.7 0 -11 -3.4 -11.6 -8.7h6.8c0.4 1.7 2.1 2.8 4.5 2.8c2.6 0 4.7 -1.4 4.7 -5.1v-3.2c-1.2 2 -3.6 3.5 -6.9 3.5c-5.5 0 -9.9 -4.5 -9.9 -11.6C229.5 355.2 233.8 350.7 239.4 350.7zM241.4 356.7c-2.6 0 -4.9 1.9 -4.9 5.5c0 3.6 2.3 5.6 4.9 5.6c2.6 0 4.9 -2 4.9 -5.5C246.3 358.7 244 356.7 241.4 356.7z"
android:fillColor="#FFFFFF" />
<path
android:pathData="M268.3 373.8c-6.6 0 -11.6 -4.4 -11.6 -11.5c0 -7.1 5.1 -11.5 11.6 -11.5c6.6 0 11.6 4.4 11.6 11.5C280 369.4 274.9 373.8 268.3 373.8zM268.3 367.9c2.5 0 4.7 -1.8 4.7 -5.6c0 -3.8 -2.2 -5.6 -4.7 -5.6c-2.5 0 -4.7 1.8 -4.7 5.6C263.7 366 265.8 367.9 268.3 367.9z"
android:fillColor="#FFFFFF" />
<path
android:pathData="M299.2 361.3c0 -3.1 -1.7 -4.7 -4.3 -4.7c-2.6 0 -4.3 1.7 -4.3 4.7v12.2h-6.9v-22.4h6.9v3c1.4 -1.9 3.8 -3.2 6.8 -3.2c5.2 0 8.6 3.5 8.6 9.6v13.1h-6.8V361.3z"
android:fillColor="#FFFFFF" />
</vector>

12
app/src/main/res/drawable/ic_memo.xml

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="42"
android:viewportHeight="42">
<path
android:pathData="M37.8,0L4.2,0C1.89,0 0.021,1.89 0.021,4.2L0,42L8.4,33.6L37.8,33.6C40.11,33.6 42,31.71 42,29.4L42,4.2C42,1.89 40.11,0 37.8,0ZM12,24L9,24L9,21L12,21L12,24ZM12,18L9,18L9,15L12,15L12,18ZM12,12L9,12L9,9L12,9L12,12ZM27,24L15,24L15,21L27,21L27,24ZM33,18L15,18L15,15L33,15L33,18ZM33,12L15,12L15,9L33,9L33,12Z"
android:strokeWidth="1"
android:fillColor="#4A90E2"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
</vector>

1252
app/src/main/res/drawable/ic_profile_zebra_01.xml

File diff suppressed because it is too large

1252
app/src/main/res/drawable/ic_profile_zebra_02.xml

File diff suppressed because it is too large

163
app/src/main/res/drawable/ic_qr_scan.xml

@ -0,0 +1,163 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="42"
android:viewportHeight="42">
<path
android:pathData="M16.4055,35.5685L6.1345,35.5685L6.1345,25.1279C6.3509,25.1279 6.5927,25.0727 6.8345,25.0727C9.7745,25.0727 12.7145,25.0727 15.6545,25.0727C16.2824,25.0727 16.503,25.2382 16.503,25.883C16.4733,28.8739 16.503,31.8648 16.503,34.86C16.4691,35.0891 16.4267,35.3267 16.4055,35.5685ZM11.2721,33.5152C12.1206,33.5152 13.0073,33.5152 13.877,33.5152C14.3267,33.5152 14.5176,33.3667 14.5133,32.8958C14.5133,31.1592 14.5133,29.4226 14.5133,27.6861C14.5133,27.2618 14.3945,27.02 13.9109,27.0242C12.1503,27.0242 10.3855,27.0242 8.6248,27.0242C8.2006,27.0242 8.0224,27.1812 8.0224,27.6097C8.0224,29.3745 8.0224,31.1352 8.0224,32.8958C8.0224,33.3709 8.2133,33.5236 8.663,33.5152C9.5327,33.5152 10.4024,33.5152 11.2721,33.5152Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M16.4139,6.1048C16.4352,6.3636 16.4691,6.5885 16.4691,6.8091C16.4691,9.7788 16.4691,12.7485 16.4691,15.7182C16.4691,16.1848 16.4267,16.4903 15.8285,16.4861C12.7358,16.4606 9.643,16.4861 6.5503,16.4861C6.4075,16.4755 6.2657,16.4542 6.1261,16.4224L6.1261,6.1048L16.4139,6.1048ZM11.2552,14.5091C12.1036,14.5091 12.9521,14.5091 13.8006,14.5091C14.3055,14.5091 14.543,14.3776 14.5345,13.8218C14.5091,12.1248 14.5345,10.4533 14.5345,8.7648C14.5345,8.277 14.3648,8.0861 13.8642,8.0903C12.1673,8.1115 10.4448,8.0903 8.7309,8.0903C8.26,8.0903 8.0309,8.2218 8.0351,8.7394C8.0564,10.4364 8.0351,12.163 8.0351,13.8727C8.0351,14.3309 8.2048,14.5176 8.663,14.5048C9.52,14.4964 10.3897,14.5091 11.2552,14.5091Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M25.1448,6.0752L35.437,6.0752C35.437,6.3297 35.4964,6.5715 35.4964,6.8176C35.4964,9.7576 35.4964,12.6976 35.4964,15.6376C35.4964,16.2612 35.3309,16.4861 34.6818,16.4861C31.7121,16.4564 28.7424,16.4861 25.7727,16.4861C25.2764,16.4861 25.0345,16.38 25.0387,15.8115C25.0642,12.74 25.0387,9.6727 25.0387,6.6055C25.0646,6.4269 25.1,6.2499 25.1448,6.0752L25.1448,6.0752ZM27.0497,11.2339C27.0497,12.1291 27.0497,13.0242 27.0497,13.9152C27.0497,14.3394 27.2067,14.5133 27.6352,14.5133C29.3958,14.5133 31.1606,14.5133 32.9212,14.5133C33.3964,14.5133 33.5448,14.3267 33.5406,13.8727C33.5236,12.1758 33.5236,10.4646 33.5406,8.7394C33.5406,8.2855 33.3836,8.0988 32.917,8.103C31.1818,8.103 29.4424,8.103 27.7073,8.103C27.1982,8.103 27.0327,8.3024 27.0455,8.7861C27.0667,9.6006 27.0497,10.4152 27.0497,11.2339L27.0497,11.2339Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M35.4964,22.7903L34.6139,22.8327C33.4049,22.8624 33.3964,22.5655 33.3964,24.1055C33.3964,24.8691 33.3964,25.637 33.3964,26.4006C33.3964,26.8248 33.2606,27.0327 32.7812,27.0539C31.2455,27.1133 31.2455,27.1303 31.3048,28.717C31.3048,28.8655 31.3345,29.0097 31.36,29.2727C31.716,29.294 32.073,29.294 32.4291,29.2727C33.2267,29.1709 33.5024,29.5273 33.3964,30.2782C33.3815,30.4305 33.3815,30.584 33.3964,30.7364C33.3964,31.1606 33.2224,31.3218 32.7982,31.3133C31.8267,31.3133 30.8552,31.2879 29.8836,31.3133C29.4042,31.3133 29.26,31.1309 29.2727,30.6812C29.2727,29.7606 29.2303,28.8358 29.2727,27.9236C29.3194,27.1855 29.0309,27.003 28.3521,27.0327C27.2873,27.0794 26.2309,27.0327 25.1067,27.0327L25.1067,25.1236C25.3952,25.1236 25.6624,25.077 25.9552,25.077C27.5418,25.077 29.1242,25.0558 30.7067,25.077C31.2455,25.077 31.4236,24.8903 31.4067,24.3642C31.377,23.4182 31.4321,22.4721 31.4067,21.5303C31.3812,20.9364 31.6061,20.7921 32.1618,20.7964C33.5109,20.7964 33.5406,20.7964 33.5364,19.4303C33.5364,18.8703 33.7485,18.7006 34.2576,18.7473C34.5407,18.7309 34.8248,18.7408 35.1061,18.777C35.2715,18.8236 35.5006,19.0442 35.5048,19.2012C35.5091,20.3636 35.4964,21.5345 35.4964,22.7903Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M20.6818,18.5182C18.7303,18.76 18.7303,18.76 18.7303,16.9061C18.7303,16.3164 18.7303,15.7309 18.7303,15.1412C18.7303,14.6915 18.8958,14.5218 19.3582,14.5006C20.9109,14.437 20.8939,14.42 20.8388,12.9012C20.8388,12.4303 20.6479,12.2903 20.1982,12.2733C18.7345,12.2139 18.7345,12.197 18.7345,10.7206C18.7345,9.4139 18.7345,8.1115 18.7345,6.8048C18.7345,6.2703 18.8533,6.02 19.46,6.0455C20.5503,6.0879 21.6491,6.0455 22.8539,6.0455C22.8539,6.6436 22.8794,7.1739 22.8539,7.7C22.8539,7.8358 22.6248,7.9758 22.4764,8.0776C22.4085,8.1242 22.2812,8.0776 22.1752,8.0776C20.7455,8.1242 20.457,8.4806 20.7879,9.8933C20.8888,10.0966 21.0815,10.2387 21.3055,10.2752C21.8113,10.3032 22.3184,10.3032 22.8242,10.2752C22.8455,10.7291 22.8752,11.1024 22.8794,11.48C22.8794,12.9097 22.8794,14.3436 22.8794,15.7733C22.8794,16.1976 22.7564,16.4097 22.2727,16.4267C20.7879,16.5455 20.7879,16.5455 20.7242,18.0303C20.72,18.1788 20.6988,18.3273 20.6818,18.5182Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M20.737,20.7412C20.737,21.9164 20.7073,23.0915 20.737,24.2624C20.737,24.7758 20.5545,24.9582 20.0709,24.9158C19.8676,24.8988 19.6633,24.8988 19.46,24.9158C18.9509,24.9582 18.7345,24.7715 18.7218,24.2242C18.6879,22.8285 18.6624,22.8327 17.2667,22.8327C15.9388,22.8327 14.6109,22.803 13.2873,22.8327C12.617,22.8327 12.3327,22.7012 12.3582,21.9503C12.4091,20.5079 12.5024,20.6776 11.0855,20.7115C10.3303,20.737 10.2624,20.3764 10.2794,19.7867C10.2964,19.197 10.2158,18.6497 11.1279,18.7218C11.9686,18.7706 12.8114,18.7706 13.6521,18.7218C14.1867,18.7218 14.3564,18.9127 14.3648,19.4345C14.3903,20.7879 14.4115,20.7964 15.7903,20.7879C17.4448,20.7879 19.0909,20.7879 20.7624,20.7879L20.737,20.7412Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M20.877,35.543L20.877,33.5152C22.7436,33.5152 24.5509,33.5152 26.3624,33.5152C26.9818,33.5152 27.2109,33.3539 27.2109,32.7048C27.2109,31.2455 26.9606,31.3897 28.5727,31.3812C28.7467,31.3812 28.9206,31.4152 29.137,31.4364L29.137,35.5388L20.877,35.543Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M20.7285,29.2727C21.1849,29.2986 21.6424,29.2986 22.0988,29.2727C22.7394,29.1964 22.9218,29.4764 22.8582,30.0576C22.8721,30.3111 22.8622,30.5653 22.8285,30.817C22.7691,31.0036 22.5527,31.1436 22.4042,31.2836C22.3576,31.3218 22.2558,31.2836 22.1794,31.2836C20.7836,31.3218 20.7582,31.3218 20.7582,32.6879C20.7582,33.2394 20.5503,33.3836 20.0412,33.3921C18.7473,33.3921 18.7473,33.4303 18.7473,32.1703C18.7473,30.7703 18.7473,29.3661 18.7473,27.9618C18.7473,27.3721 18.9297,27.1133 19.5406,27.1727C19.7693,27.1918 19.9992,27.1918 20.2279,27.1727C20.6182,27.1727 20.7752,27.3255 20.7582,27.72C20.7115,28.2079 20.7285,28.6873 20.7285,29.2727Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M29.1794,22.8242C28.6537,22.8524 28.1269,22.8524 27.6012,22.8242C27.4527,22.8242 27.2533,22.5612 27.2152,22.4C27.1048,21.8697 27.2406,21.1273 26.9352,20.8473C26.6297,20.5673 25.9212,20.7497 25.3867,20.7242C25.2819,20.702 25.1795,20.6693 25.0812,20.6267C25.0537,20.1827 25.0622,19.7372 25.1067,19.2945C25.172,19.0633 25.3488,18.8801 25.5776,18.8067C26.6255,18.76 27.6733,18.7642 28.717,18.8067C28.9208,18.8677 29.0802,19.0271 29.1412,19.2309C29.1964,20.3636 29.1794,21.5473 29.1794,22.8242Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M22.8667,26.9776L20.8727,26.9776L20.8727,25.0515C21.5218,25.0939 22.3236,24.92 22.6885,25.2424C23.0533,25.5648 22.8285,26.3624 22.8667,26.9776Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M31.4364,35.6958L31.4364,33.4685C32.0939,33.5279 32.8788,33.3836 33.2564,33.7103C33.5491,33.9606 33.5703,35.0382 33.2818,35.3267C32.9933,35.6152 32.1448,35.5812 31.4364,35.6958Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M20.7624,20.7879C20.7836,20.3085 20.8303,19.8248 20.8176,19.3455C20.8048,18.8661 20.9915,18.7388 21.4073,18.7727L21.4836,18.7727C22.8709,18.7133 23.0236,18.8618 22.8285,20.2406C22.7503,20.4686 22.5645,20.6432 22.3321,20.7073C21.8019,20.7555 21.2693,20.7739 20.737,20.7624L20.7624,20.7879Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M10.3133,20.7624C10.1394,21.6321 10.1945,22.3745 9.8467,22.7436C9.6176,22.9939 8.5442,22.9176 8.3406,22.6333C8.0691,22.2515 8.0436,21.263 8.3406,21.0212C8.7055,20.6988 9.4818,20.843 10.3133,20.7624Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M27.1261,31.2497L25.0303,31.2497C25.0982,30.5921 24.9879,29.8582 25.2933,29.3915C25.463,29.137 26.6127,29.1158 26.8079,29.3915C27.1515,29.7988 27.037,30.5455 27.1261,31.2497Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M6.1261,18.8491C8.3321,18.6073 8.0861,18.6158 7.9545,20.6436L6.1261,20.6436L6.1261,18.8491Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M22.96,22.9388C23.3274,22.9607 23.6957,22.9607 24.063,22.9388C24.7164,22.8582 24.9752,23.0745 24.9455,23.7873C24.8988,24.8606 24.2879,25.3061 23.2485,24.8988C23.1049,24.8187 23.0074,24.6757 22.9855,24.5127C22.9388,24.0164 22.96,23.537 22.96,22.9388Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M22.9558,29.1582C22.9777,28.7838 22.9777,28.4084 22.9558,28.0339C22.8667,27.3594 23.1212,27.1388 23.8042,27.1855C24.8564,27.2321 25.3188,27.8982 24.903,28.9206C24.8155,29.0581 24.6704,29.1485 24.5085,29.1667C24.0164,29.1752 23.537,29.1582 22.9558,29.1582Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M35.6109,29.12L33.5152,29.12C33.5533,28.5048 33.4133,27.7752 33.7018,27.3297C33.8758,27.0667 34.9745,27.02 35.2164,27.2788C35.5558,27.6648 35.4836,28.4242 35.6109,29.12Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M33.4388,31.4236L35.5303,31.4236C35.4752,32.0727 35.6024,32.8067 35.3012,33.2352C35.1103,33.5024 33.9988,33.5279 33.7867,33.2691C33.4558,32.8661 33.5448,32.1236 33.4388,31.4236Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M12.2776,30.4394C12.4388,31.1055 12.1206,31.3558 11.3358,31.2879C10.3006,31.2242 10.3006,31.2879 10.3006,30.3036C10.3006,29.3194 10.3091,29.2727 11.2976,29.2727C12.2861,29.2727 12.2776,29.2727 12.2776,30.4394Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M11.4545,12.2776C10.2836,12.2776 10.2836,12.2776 10.2836,11.323C10.2836,10.3685 10.2836,10.3727 11.2976,10.3091C12.0697,10.2582 12.32,10.4958 12.2818,11.2806C12.2648,11.9679 12.2436,12.4812 11.4545,12.2776Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M30.3418,10.3218C31.3048,10.3218 31.3048,10.3218 31.3048,11.3485C31.3048,12.2818 31.3048,12.2818 30.2442,12.2776C29.277,12.2776 29.277,12.2776 29.277,11.2509C29.2727,10.3218 29.2727,10.3218 30.3418,10.3218Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M11.9679,0l-9.2061,0l-2.7618,0l0,2.7618l0,9.2061l2.7618,0l0,-9.2061l9.2061,0z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M41.737,11.9679l0,-9.2061l0,-2.7618l-2.7618,0l-9.2061,0l0,2.7618l9.2061,0l0,9.2061z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M29.7691,41.737l9.2061,0l2.7618,0l0,-2.7618l0,-9.2061l-2.7618,0l0,9.2061l-9.2061,0z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M0,29.7691l0,9.2061l0,2.7618l2.7618,0l9.2061,0l0,-2.7618l-9.2061,0l0,-9.2061z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
</vector>

10
app/src/main/res/drawable/ic_qrcode_24dp.xml

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@android:color/black"
android:pathData="M3,11H5V13H3V11M11,5H13V9H11V5M9,11H13V15H11V13H9V11M15,11H17V13H19V11H21V13H19V15H21V19H19V21H17V19H13V21H11V17H15V15H17V13H15V11M19,19V15H17V19H19M15,3H21V9H15V3M17,5V7H19V5H17M3,3H9V9H3V3M5,5V7H7V5H5M3,15H9V21H3V15M5,17V19H7V17H5Z"/>
</vector>

9
app/src/main/res/drawable/ic_receipt_24dp.xml

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@android:color/black"
android:pathData="M18,17L6,17v-2h12v2zM18,13L6,13v-2h12v2zM18,9L6,9L6,7h12v2zM3,22l1.5,-1.5L6,22l1.5,-1.5L9,22l1.5,-1.5L12,22l1.5,-1.5L15,22l1.5,-1.5L18,22l1.5,-1.5L21,22L21,2l-1.5,1.5L18,2l-1.5,1.5L15,2l-1.5,1.5L12,2l-1.5,1.5L9,2 7.5,3.5 6,2 4.5,3.5 3,2v20z"/>
</vector>

5
app/src/main/res/drawable/ic_receive.xml

@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@color/zcashGreen" android:pathData="M20,5.41L18.59,4 7,15.59V9H5v10h10v-2H8.41z"/>
</vector>

12
app/src/main/res/drawable/ic_receive_funds.xml

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="23dp"
android:height="28dp"
android:viewportWidth="23"
android:viewportHeight="28">
<path
android:pathData="M23,4.5167L23,15.5777C23,19.5236 20.7958,23.1307 17.25,24.9876L11.5,28L5.75,24.9876C2.2042,23.1307 0,19.5236 0,15.5777L0,4.5167L11.5,0L23,4.5167ZM11.2157,14.2963L16.2746,9L19,11.8519L11.2157,20L6,14.541L8.7254,11.6892L11.2157,14.2963Z"
android:strokeWidth="1"
android:fillColor="#666666"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
</vector>

496
app/src/main/res/drawable/ic_sadzebra.xml

File diff suppressed because one or more lines are too long

12
app/src/main/res/drawable/ic_scan_corner.xml

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="52dp"
android:height="53dp"
android:viewportWidth="52"
android:viewportHeight="53">
<path
android:pathData="M0.0488,0l0,33.502l19.0122,18.7646l32.4937,0l0,-11.9263l-27.7486,0l-11.1138,-11.5889l0,-28.7515z"
android:strokeWidth="1"
android:fillColor="#FFFFFF"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
</vector>

36
app/src/main/res/drawable/ic_scan_frame.xml

@ -0,0 +1,36 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="279dp"
android:height="280dp"
android:viewportWidth="279"
android:viewportHeight="280">
<path
android:pathData="M18.977,6L7,17.981L7,260.023L18.977,272L261.008,272L273,259.355L273,17.981L261.023,6L18.977,6Z"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:fillType="evenOdd"/>
<path
android:pathData="M1,227l0,33.502l19.012,18.765l32.494,0l0,-11.926l-27.749,0l-11.114,-11.589l0,-28.751z"
android:strokeWidth="1"
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M226.62,278.886l33.502,-0l18.765,-19.012l0,-32.494l-11.926,-0l0,27.749l-11.589,11.114l-28.751,-0z"
android:strokeWidth="1"
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M278.506,52.267l-0,-33.502l-19.012,-18.765l-32.494,-0l-0,11.926l27.749,-0l11.114,11.589l-0,28.751z"
android:strokeWidth="1"
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M52.886,0.38l-33.502,0l-18.765,19.012l-0,32.494l11.926,0l-0,-27.749l11.589,-11.114l28.751,0z"
android:strokeWidth="1"
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
</vector>

48
app/src/main/res/drawable/ic_scan_overlay.xml

@ -0,0 +1,48 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:height="726dp"
android:viewportHeight="726"
android:viewportWidth="412"
android:width="412dp">
<path
android:fillAlpha="0.6"
android:fillColor="#000000"
android:fillType="evenOdd"
android:pathData="M412,0L412,732L0,732L0,0L412,0ZM351,135L61,135L46,150.005L46,440L61,455L351,455L366,439.184L366,150.005L351,135Z"
android:strokeAlpha="0.5108352"
android:strokeColor="#00000000"
android:strokeWidth="1" />
<group android:scaleX="0.5" android:scaleY="0.5">
<path
android:fillColor="#00000000"
android:fillType="evenOdd"
android:pathData="M61.414,136L47,150.419L47,439.586L61.414,454L350.57,454L365,438.785L365,150.419L350.586,136L61.414,136Z"
android:strokeColor="#FFFFFF"
android:strokeWidth="2" />
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M41.049,407l0,33.502l19.012,18.765l32.494,0l0,-11.926l-27.749,0l-11.114,-11.589l0,-28.751z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M319,460.451l33.502,-0l18.765,-19.012l0,-32.494l-11.926,-0l0,27.749l-11.589,11.114l-28.751,-0z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M371.218,182.451l-0,-33.502l-19.012,-18.765l-32.494,-0l-0,11.926l27.749,-0l11.114,11.589l-0,28.751z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M93.267,129l-33.502,0l-18.765,19.012l-0,32.494l11.926,0l-0,-27.749l11.589,-11.114l28.751,0z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</group>
</vector>

44
app/src/main/res/drawable/ic_scan_overlay_edited.xml

@ -0,0 +1,44 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="412dp"
android:height="726dp"
android:viewportWidth="412"
android:viewportHeight="726">
<path
android:pathData="M412,0L412,732L0,732L0,0L412,0ZM327.484,157L85.516,157L73,169.473L73,410.531L85.516,423L327.484,423L340,409.853L340,169.473L327.484,157Z"
android:strokeAlpha="0.5108352"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="evenOdd"
android:strokeColor="#00000000"
android:fillAlpha="0.6"/>
<path
android:pathData="M84.977,157L73,168.981L73,411.023L84.977,423L327.008,423L339,410.355L339,168.981L327.023,157L84.977,157Z"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:fillType="evenOdd"/>
<path
android:pathData="M67,378l0,33.502l19.012,18.765l32.494,0l0,-11.926l-27.749,0l-11.114,-11.589l0,-28.751z"
android:strokeWidth="1"
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M292.62,429.886l33.502,-0l18.765,-19.012l0,-32.494l-11.926,-0l0,27.749l-11.589,11.114l-28.751,-0z"
android:strokeWidth="1"
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M344.506,203.267l-0,-33.502l-19.012,-18.765l-32.494,-0l-0,11.926l27.749,-0l11.114,11.589l-0,28.751z"
android:strokeWidth="1"
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M118.886,151.38l-33.502,0l-18.765,19.012l-0,32.494l11.926,0l-0,-27.749l11.589,-11.114l28.751,0z"
android:strokeWidth="1"
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
</vector>

14
app/src/main/res/drawable/ic_shield.xml

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="358dp"
android:height="418dp"
android:viewportWidth="358"
android:viewportHeight="418">
<path
android:pathData="M179,0C207.069,11.901 236.599,23.2 267.591,33.897C298.582,44.595 328.719,53.128 358,59.497C354.385,162.895 337.225,240.396 306.519,292C275.814,343.603 233.307,385.603 179,418C124.693,385.603 82.186,343.603 51.481,292C20.775,240.396 3.615,162.895 0,59.497C29.281,53.128 59.418,44.595 90.409,33.897C121.401,23.2 150.931,11.901 179,0Z"
android:strokeAlpha="0.2"
android:strokeWidth="1"
android:fillColor="#A1A1A1"
android:fillType="nonZero"
android:strokeColor="#00000000"
android:fillAlpha="1.0"/>
</vector>

166
app/src/main/res/drawable/ic_shield_address.xml

@ -0,0 +1,166 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
android:viewportWidth="161"
android:viewportHeight="200"
android:width="161dp"
android:height="200dp">
<path
android:pathData="M77.5 2Q85.4 1.1 87 6.5L86.5 36Q84.2 32.2 77.5 33L71.5 37L71 8.5L75.5 3L77.5 2Z"
android:fillColor="#0E0E0E"
android:strokeColor="#0E0E0E"
android:strokeWidth="1" />
<path
android:pathData="M43.5 12Q52.1 10.9 54 16.5L54 91.5L50.5 96L44.5 97L39 92.5L38 90.5L38 17.5L41.5 13L43.5 12Z"
android:fillColor="#0E0E0E"
android:strokeColor="#0E0E0E"
android:strokeWidth="1" />
<path
android:pathData="M110.5 12Q119.1 10.9 121 16.5L121 67.5L117.5 71L110.5 72L107 70L105 65.5L105 18.5L108.5 13L110.5 12Z"
android:fillColor="#0E0E0E"
android:strokeColor="#0E0E0E"
android:strokeWidth="1" />
<path
android:pathData="M12.5 30Q20.4 29.1 22 34.5L22 158.5L18.5 162L11.5 163L8 160.5L6 156.5L6 36.5L8 32L12.5 30Z"
android:fillColor="#0E0E0E"
android:strokeColor="#0E0E0E"
android:strokeWidth="1" />
<path
android:pathData="M145.5 30Q153.4 29.1 155 34.5L156 37.5L156 155.5L151.5 162L145.5 163L140 158.5L139 155.5L139 37.5L143.5 31L145.5 30Z"
android:fillColor="#0E0E0E"
android:strokeColor="#0E0E0E"
android:strokeWidth="1" />
<path
android:pathData="M79.5 50L86 53.5L87 55.5L87 94L83.5 94L78 88.5L71 80.5L71 56.5L75.5 51L79.5 50Z"
android:fillColor="#0E0E0E"
android:strokeColor="#0E0E0E"
android:strokeWidth="1" />
<path
android:pathData="M110.5 89Q117.8 87.8 120 91.5L121 93.5L121 175.5Q119.1 181.1 110.5 180L107 177.5L105 173.5L105 94.5L108.5 90L110.5 89Z"
android:fillColor="#0E0E0E"
android:strokeColor="#0E0E0E"
android:strokeWidth="1" />
<path
android:pathData="M72.5 110L74.5 110L80 115.5L87 124.5L87 142.5L84.5 146L75.5 147L71 141.5L71 111.5L72.5 110Z"
android:fillColor="#0E0E0E"
android:strokeColor="#0E0E0E"
android:strokeWidth="1" />
<path
android:pathData="M44.5 113Q51.1 112.4 53 116.5L54 118.5L54 175.5Q52.1 181.1 43.5 180L39 176.5L38 174.5L38 119.5L42.5 114L44.5 113Z"
android:fillColor="#0E0E0E"
android:strokeColor="#0E0E0E"
android:strokeWidth="1" />
<path
android:pathData="M71.5 162Q73.7 167.3 82.5 166L86.5 163L87 192.5L83.5 197L78.5 198L72 193.5L71 191.5L71.5 162Z"
android:fillColor="#0E0E0E"
android:strokeColor="#0E0E0E"
android:strokeWidth="1" />
<path
android:pathData="M77.5 1Q85.6 0.9 88 6.5L87.5 39Q84.8 33.8 77 33.5L81.5 33L86.5 36L87 6.5Q85.4 1.1 77.5 2L72 6.5L71 8.5L71 36.5L74.5 34L76 34.5L70.5 39L70 8.5L75.5 2L77.5 1Z"
android:fillColor="#FEFEFE"
android:strokeColor="#FEFEFE"
android:strokeWidth="1" />
<path
android:pathData="M44.5 11Q52.6 10.9 55 16.5L55 91.5L50.5 97L44.5 98L39 95L37 90.5L37 18.5L38 18.5L38 90.5L42.5 96L47.5 97L53 93.5L54 91.5L54 16.5L50.5 13L44.5 12L44.5 11Z"
android:fillColor="#FEFEFE"
android:strokeColor="#FEFEFE"
android:strokeWidth="1" />
<path
android:pathData="M111.5 11L115.5 11L121 14L122 16.5L122 67.5L115.5 73L110.5 73L106 69.5L104 65.5L104 18.5L107.5 13L107 14.5L105 18.5L105 65.5L107 70L110.5 72L115.5 72L121 67.5L121 16.5L117.5 13L111.5 12L111.5 11Z"
android:fillColor="#FEFEFE"
android:strokeColor="#FEFEFE"
android:strokeWidth="1" />
<path
android:pathData="M41.5 12L43 12.5L39 15.5L38.5 17L38 15.5L41.5 12Z"
android:fillColor="#FEFEFE"
android:strokeColor="#FEFEFE"
android:strokeWidth="1" />
<path
android:pathData="M12.5 29L16 29.5L8 32L6 36.5L6 156.5L9.5 162L11 162.5L9.5 163L5 156.5L5 36.5L8.5 31L12.5 29Z"
android:fillColor="#FEFEFE"
android:strokeColor="#FEFEFE"
android:strokeWidth="1" />
<path
android:pathData="M145.5 29Q152.8 28.2 155 32.5L156 36.5L155 36.5Q155.5 31.5 151.5 31L145.5 30L140 34.5L139.5 37L139 34.5L141.5 31L145.5 29Z"
android:fillColor="#FEFEFE"
android:strokeColor="#FEFEFE"
android:strokeWidth="1" />
<path
android:pathData="M17.5 30L23 34.5L23 158.5L18.5 163L12.5 164L12.5 163Q20.4 163.9 22 158.5L22 34.5L18.5 31L17.5 30Z"
android:fillColor="#FEFEFE"
android:strokeColor="#FEFEFE"
android:strokeWidth="1" />
<path
android:pathData="M76.5 50L79 50.5L76.5 51L76.5 50Z"
android:fillColor="#FEFEFE"
android:strokeColor="#FEFEFE"
android:strokeWidth="1" />
<path
android:pathData="M80.5 50Q85.3 50.3 87 53.5L88 55.5L88 93.5L87 93.5L87 55.5L84.5 52L80.5 51L80.5 50Z"
android:fillColor="#FEFEFE"
android:strokeColor="#FEFEFE"
android:strokeWidth="1" />
<path
android:pathData="M74.5 51L72 54.5L71.5 56L71 54.5L74.5 51Z"
android:fillColor="#FEFEFE"
android:strokeColor="#FEFEFE"
android:strokeWidth="1" />
<path
android:pathData="M70.5 57L71 80.5L83.5 94L86 94.5L83.5 95L70 80.5L70.5 57Z"
android:fillColor="#FEFEFE"
android:strokeColor="#FEFEFE"
android:strokeWidth="1" />
<path
android:pathData="M110.5 88Q118.8 86.7 121 91.5L122 93.5L122 175.5L119.5 179Q117.2 181.7 111.5 181L111.5 180Q119.4 180.9 121 175.5L121 93.5L118.5 90L110.5 89L106.5 92L108.5 89L110.5 88Z"
android:fillColor="#FEFEFE"
android:strokeColor="#FEFEFE"
android:strokeWidth="1" />
<path
android:pathData="M105.5 93L105 94.5L105 173.5L108.5 179L110 179.5L108.5 180L104 173.5L104 94.5L105.5 93Z"
android:fillColor="#FEFEFE"
android:strokeColor="#FEFEFE"
android:strokeWidth="1" />
<path
android:pathData="M72.5 109L74.5 109L82 116.5L88 123.5L88 142.5L84.5 147L75.5 148L71 142.5L72 142.5L75.5 147Q83.5 148.5 86 144.5L87 142.5L87 124.5L74.5 110L71 111.5L70.5 141L70 111.5L72.5 109Z"
android:fillColor="#FEFEFE"
android:strokeColor="#FEFEFE"
android:strokeWidth="1" />
<path
android:pathData="M42.5 113L44 113.5L39 117.5L38 119.5L37.5 174L37 119.5L40.5 114L42.5 113Z"
android:fillColor="#FEFEFE"
android:strokeColor="#FEFEFE"
android:strokeWidth="1" />
<path
android:pathData="M48.5 113Q53.5 112.5 54 116.5L55 118.5L55 175.5L49.5 180L49.5 179L54 175.5L54 118.5L51.5 115Q47.7 114.7 48.5 113Z"
android:fillColor="#FEFEFE"
android:strokeColor="#FEFEFE"
android:strokeWidth="1" />
<path
android:pathData="M139.5 156L143.5 162L149.5 163L155 158.5L155.5 156L156 158.5L153.5 162Q151.2 164.7 145.5 164L141 162L139 158.5L139.5 156Z"
android:fillColor="#FEFEFE"
android:strokeColor="#FEFEFE"
android:strokeWidth="1" />
<path
android:pathData="M70 161L75 164.5L71 162.5L71 191.5L75.5 197L78 197.5L75.5 198L72 196L70 191.5L70 161Z"
android:fillColor="#FEFEFE"
android:strokeColor="#FEFEFE"
android:strokeWidth="1" />
<path
android:pathData="M87.5 161L88 193.5L84 196.5L87 192.5L87 163.5L84.5 165L76.5 166L76.5 165Q85.3 166.3 87.5 161Z"
android:fillColor="#FEFEFE"
android:strokeColor="#FEFEFE"
android:strokeWidth="1" />
<path
android:pathData="M38.5 175L41.5 179L43 179.5L41.5 180L38 176.5L38.5 175Z"
android:fillColor="#FEFEFE"
android:strokeColor="#FEFEFE"
android:strokeWidth="1" />
<path
android:pathData="M44.5 180L48 180.5L44.5 181L44.5 180Z"
android:fillColor="#FEFEFE"
android:strokeColor="#FEFEFE"
android:strokeWidth="1" />
<path
android:pathData="M0 0L161 0L161 200L0 200L0 0ZM78 1L76 2L70 9L71 39L77 34Q86 33 88 39L88 7Q86 1 78 1ZM45 11L42 12L37 19L37 91L39 95L45 98L51 97L55 92L55 17Q53 11 45 11ZM112 11L108 13L104 19L104 66L106 70L111 73L116 73L122 68L122 17L121 14L116 11L112 11ZM13 29L9 31L5 37L5 157L6 160L13 164L19 163L23 159L23 35Q21 29 13 29ZM146 29L142 31L139 35L139 159L141 162L146 164Q151 165 154 162L156 159L156 35L155 33Q153 28 146 29ZM77 50L75 51L70 58L70 81L84 95L88 94L88 56L87 54Q85 49 77 50ZM111 88L109 89L104 95L104 174L106 178L112 181Q117 182 120 179L122 176L122 94L121 92Q119 87 111 88ZM73 109L70 112L70 141L76 148L85 147L88 143L88 124L82 117L75 109L73 109ZM46 112L41 114L37 120L37 174L38 177L45 181Q53 181 55 176L55 119L54 117L51 113L46 112ZM70 161L70 192L72 196L76 198Q86 200 88 194L88 161L83 165L76 165L70 161Z"
android:fillColor="#FFFFFF"
android:strokeColor="#FFFFFF"
android:strokeWidth="1" />
</vector>

12
app/src/main/res/drawable/ic_shielded.xml

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="33dp"
android:height="42dp"
android:viewportWidth="33"
android:viewportHeight="42">
<path
android:pathData="M33,6.7751L33,23.3666C33,29.2854 29.8375,34.696 24.75,37.4813L16.5,42L8.25,37.4813C3.1625,34.696 0,29.2854 0,23.3666L0,6.7751L16.5,0L33,6.7751Z"
android:strokeWidth="1"
android:fillColor="#FFB900"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
</vector>

5
app/src/main/res/drawable/ic_warning_24dp.xml

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z"/>
</vector>

1252
app/src/main/res/drawable/ic_zcash_primary.xml

File diff suppressed because it is too large

1252
app/src/main/res/drawable/ic_zcash_white.xml

File diff suppressed because it is too large

1252
app/src/main/res/drawable/ic_zcashlogo.xml

File diff suppressed because it is too large

1252
app/src/main/res/drawable/ic_zec_symbol.xml

File diff suppressed because it is too large

1252
app/src/main/res/drawable/ic_zec_symbol_right.xml

File diff suppressed because it is too large

8
app/src/main/res/drawable/ripple_button_circle.xml

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple
xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@android:color/white">
<item android:drawable="@drawable/background_circle" />
</ripple>

6
app/src/main/res/drawable/selector_pressed_ripple_circle.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/transparent" android:state_pressed="false" />
<item android:drawable="@drawable/ripple_button_circle" android:state_pressed="true" />
</selector>

BIN
app/src/main/res/font/inconsolata.ttf

Binary file not shown.

BIN
app/src/main/res/font/zboto.otf

Binary file not shown.

65
app/src/main/res/layout/chip_view.xml

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="wrap_content"
android:layout_height="@dimen/chip_height"
android:background="@drawable/bg_chip_view"
android:clickable="true">
<com.tylersuehr.chips.CircleImageView
android:id="@+id/avatar"
android:layout_width="@dimen/chip_height"
android:layout_height="@dimen/chip_height"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/avatar" />
<TextView
android:id="@+id/chip_index"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginTop="2dp"
android:text="12"
android:textSize="12dp"
android:fontFamily="@font/inconsolata"
android:textColor="@color/text_light_dimmed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="4dp"
android:layout_marginStart="8dp"
android:textColor="@color/text_light_dimmed"
android:textSize="@dimen/chip_label_text_size"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@+id/chip_index"
app:layout_constraintRight_toLeftOf="@+id/button_delete"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="artwork" />
<ImageButton
android:id="@+id/button_delete"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="4dp"
android:layout_marginRight="4dp"
android:alpha=".54"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/chip_delete_icon_20dp"
app:tint="@color/zcashWhite_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

50
app/src/main/res/layout/chip_view_filterable.xml

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:clickable="true">
<com.tylersuehr.chips.CircleImageView
android:id="@+id/image"
android:layout_width="40dp"
android:layout_height="40dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:visibility="gone"
tools:src="@drawable/avatar"/>
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:paddingRight="16dp"
android:paddingLeft="16dp"
android:textSize="18dp"
android:includeFontPadding="false"
android:textColor="@color/selector_button_text_light_to_dimmed"
app:layout_constraintBottom_toTopOf="@+id/subtitle"
app:layout_constraintLeft_toRightOf="@+id/image"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Title"/>
<TextView
android:id="@+id/subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:alpha=".56"
android:textSize="@dimen/chip_label_text_size"
android:textColor="#212121"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@+id/title"
app:layout_constraintLeft_toRightOf="@+id/image"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
tools:text="Subtitle"/>
</androidx.constraintlayout.widget.ConstraintLayout>

28
app/src/main/res/layout/dialog_first_use_message.xml

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:paddingStart="?attr/dialogPreferredPadding"
android:paddingEnd="?attr/dialogPreferredPadding">
<TextView
android:id="@+id/dialog_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="some text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<CheckBox
android:id="@+id/dialog_first_use_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/dialog_not_again"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/dialog_message" />
</androidx.constraintlayout.widget.ConstraintLayout>

132
app/src/main/res/layout/dialog_solicit_feedback_rating.xml

@ -0,0 +1,132 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="16dp"
android:paddingTop="16dp">
<TextView
android:id="@+id/dialog_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Rate your experience!"
android:textSize="22sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- Ratings -->
<TextView
android:id="@+id/feedback_exp_1"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginStart="?attr/dialogPreferredPadding"
android:layout_marginTop="30dp"
android:background="@drawable/background_circle"
android:backgroundTint="@color/selector_feedback_button"
android:gravity="center"
android:text="1"
android:textColor="@color/text_dark"
android:textSize="30dp"
app:layout_constraintEnd_toStartOf="@id/feedback_exp_2"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/dialog_title" />
<TextView
android:id="@+id/feedback_exp_2"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@drawable/background_circle"
android:backgroundTint="@color/selector_feedback_button"
android:gravity="center"
android:text="2"
android:textColor="@color/text_dark"
android:textSize="30dp"
app:layout_constraintBottom_toBottomOf="@id/feedback_exp_1"
app:layout_constraintEnd_toStartOf="@id/feedback_exp_3"
app:layout_constraintStart_toEndOf="@id/feedback_exp_1"
app:layout_constraintTop_toTopOf="@id/feedback_exp_1" />
<TextView
android:id="@+id/feedback_exp_3"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@drawable/background_circle"
android:backgroundTint="@color/selector_feedback_button"
android:gravity="center"
android:text="3"
android:textColor="@color/text_dark"
android:textSize="30dp"
app:layout_constraintBottom_toBottomOf="@id/feedback_exp_1"
app:layout_constraintEnd_toStartOf="@id/feedback_exp_4"
app:layout_constraintStart_toEndOf="@id/feedback_exp_2"
app:layout_constraintTop_toTopOf="@id/feedback_exp_1" />
<TextView
android:id="@+id/feedback_exp_4"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@drawable/background_circle"
android:backgroundTint="@color/selector_feedback_button"
android:gravity="center"
android:text="4"
android:textColor="@color/text_dark"
android:textSize="30dp"
app:layout_constraintBottom_toBottomOf="@id/feedback_exp_1"
app:layout_constraintEnd_toStartOf="@id/feedback_exp_5"
app:layout_constraintStart_toEndOf="@id/feedback_exp_3"
app:layout_constraintTop_toTopOf="@id/feedback_exp_1" />
<TextView
android:id="@+id/feedback_exp_5"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginEnd="?attr/dialogPreferredPadding"
android:background="@drawable/background_circle"
android:backgroundTint="@color/selector_feedback_button"
android:gravity="center"
android:text="5"
android:textColor="@color/text_dark"
android:textSize="30dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/feedback_exp_4"
app:layout_constraintTop_toTopOf="@id/feedback_exp_1" />
<TextView
android:id="@+id/label_rating_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="Very Bad"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="@id/feedback_exp_1"
app:layout_constraintStart_toStartOf="@id/feedback_exp_1"
app:layout_constraintTop_toBottomOf="@id/feedback_exp_1" />
<TextView
android:id="@+id/label_rating_5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="Very Good"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="@id/feedback_exp_5"
app:layout_constraintStart_toStartOf="@id/feedback_exp_5"
app:layout_constraintTop_toBottomOf="@id/feedback_exp_5" />
<TextView
android:id="@+id/button_ask_later"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ask Me Later"
android:paddingTop="32dp"
android:paddingStart="20dp"
android:textAllCaps="true"
android:textColor="@color/colorPrimary"
android:textSize="12sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/label_rating_1" />
</androidx.constraintlayout.widget.ConstraintLayout>

6
app/src/main/res/layout/footer_transactions.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<View
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="32dp"
android:background="@drawable/background_footer" />

19
app/src/main/res/layout/fragment_address.xml

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/background_home"
xmlns:tools="http://schemas.android.com/tools">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/text_light"
android:text="send address"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

168
app/src/main/res/layout/fragment_auto_shield.xml

@ -0,0 +1,168 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
android:background="@drawable/background_home">
<View
android:id="@+id/guide_keys"
android:layout_width="0dp"
android:layout_height="0dp"
android:alpha="0.3"
android:background="@android:color/transparent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.38196601125"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.58"
app:layout_constraintWidth_percent="0.7475728155" />
<!-- Back Button -->
<ImageView
android:id="@+id/back_button"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@string/content_description_send_final_back"
app:tint="@color/zcashWhite_87"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.088"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.065"
app:layout_constraintWidth_percent="0.08"
app:srcCompat="@drawable/ic_cancel"
tools:visibility="visible" />
<View
android:id="@+id/back_button_hit_area"
android:layout_width="68dp"
android:layout_height="68dp"
android:layout_marginStart="24dp"
android:clickable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.045"
android:focusable="true" />
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lottie_shielding"
android:layout_width="0dp"
android:layout_height="0dp"
android:elevation="6dp"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_bias="0.212"
app:layout_constraintWidth_percent="0.46"
app:lottie_autoPlay="true"
app:lottie_loop="true"
app:lottie_rawRes="@raw/lottie_shielding" />
<ImageView
android:id="@+id/image_failed"
android:layout_width="0dp"
android:layout_height="0dp"
android:elevation="6dp"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_bias="0.212"
app:layout_constraintWidth_percent="0.46"
android:visibility="invisible"
android:src="@drawable/ic_sadzebra"/>
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lottie_success"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="@id/lottie_shielding"
app:layout_constraintStart_toStartOf="@id/lottie_shielding"
app:layout_constraintEnd_toEndOf="@id/lottie_shielding"
app:layout_constraintBottom_toBottomOf="@id/lottie_shielding"
android:visibility="invisible"
app:lottie_autoPlay="true"
app:lottie_loop="false"
app:lottie_rawRes="@raw/lottie_success" />
<TextView
android:id="@+id/text_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/TextAppearance.MaterialComponents.Headline5"
app:autoSizeTextType="uniform"
android:gravity="center"
android:maxLines="3"
android:paddingEnd="16dp"
android:paddingStart="16dp"
android:textColor="@color/text_light"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/lottie_shielding"
tools:text="Shielding Now!" />
<ScrollView
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="12dp"
android:layout_marginTop="16dp"
app:layout_constraintBottom_toTopOf="@id/button_primary"
app:layout_constraintEnd_toEndOf="@id/button_primary"
app:layout_constraintStart_toStartOf="@id/button_primary"
app:layout_constraintTop_toBottomOf="@id/text_title">
<TextView
android:id="@+id/text_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/text_light" />
</ScrollView>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_primary"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/Zcash.Button.OutlinedButton"
android:padding="20dp"
android:text="@string/cancel"
android:textColor="@color/text_light"
android:translationY="-6dp"
app:layout_constraintBottom_toTopOf="@id/button_more_info"
app:layout_constraintEnd_toEndOf="@id/guide_keys"
app:layout_constraintStart_toStartOf="@id/guide_keys"
app:layout_constraintTop_toBottomOf="@id/guide_keys"
app:layout_constraintVertical_bias="0.2"
app:layout_constraintVertical_chainStyle="packed"
app:strokeColor="@color/text_light" />
<TextView
android:id="@+id/button_more_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:gravity="center"
android:text="@string/send_more_info"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/text_light"
app:layout_constraintEnd_toEndOf="@+id/button_primary"
app:layout_constraintStart_toStartOf="@+id/button_primary"
app:layout_constraintTop_toBottomOf="@id/button_primary" />
</androidx.constraintlayout.widget.ConstraintLayout>

108
app/src/main/res/layout/fragment_auto_shield_information.xml

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/background_home">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_hit_area_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.04" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_send_amount_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.13" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_content_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.15" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_content_end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.85" />
<!-- TODO: the color isn't exactly right -->
<ImageView
android:id="@+id/icon_autoshielding"
android:layout_width="0dp"
android:layout_height="0dp"
android:elevation="6dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.212"
app:layout_constraintWidth_percent="0.4"
android:src="@drawable/ic_check_shield"
app:tint="@color/colorPrimary" />
<TextView
android:id="@+id/text_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
style="@style/TextAppearance.AppCompat.Body1"
android:text="@string/autoshielding_title_text"
android:textSize="20sp"
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
app:layout_constraintTop_toBottomOf="@id/icon_autoshielding" />
<TextView
android:id="@+id/text_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
style="@style/TextAppearance.AppCompat.Body1"
android:text="@string/autoshielding_body_text"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
app:layout_constraintTop_toBottomOf="@id/text_title" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_autoshield_dismiss"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/Zcash.Button"
android:gravity="center"
android:padding="12dp"
android:text="@string/autoshielding_button_positive"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="#000000"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
app:layout_constraintTop_toBottomOf="@id/text_description"
app:layout_constraintVertical_bias="0.1" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_autoshield_more_info"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/Zcash.Button.OutlinedButton"
android:gravity="center"
android:padding="12dp"
android:text="@string/autoshielding_button_neutral"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/text_light"
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
app:layout_constraintTop_toBottomOf="@id/button_autoshield_dismiss" />
</androidx.constraintlayout.widget.ConstraintLayout>

168
app/src/main/res/layout/fragment_awesome.xml

@ -0,0 +1,168 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/background_home">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_hit_area_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.04" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_content_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.14" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_content_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.15" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_content_end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.85" />
<View
android:id="@+id/hit_area_exit"
android:layout_width="68dp"
android:layout_height="68dp"
android:layout_marginEnd="24dp"
android:layout_marginStart="24dp"
android:alpha="0.3"
android:background="@android:color/transparent"
android:elevation="6dp"
app:layout_constraintStart_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/guideline_hit_area_top" />
<ImageView
android:id="@+id/icon_exit"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@string/content_description_profile_back"
android:elevation="6dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.088"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.065"
app:layout_constraintWidth_percent="0.08"
app:srcCompat="@drawable/ic_cancel"
app:tint="@color/text_light_dimmed" />
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lottie_shielding"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@string/content_description_profile_zebra"
android:elevation="6dp"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_address_part_2"
app:layout_constraintVertical_bias="0.212"
app:layout_constraintWidth_percent="0.46"
app:lottie_autoPlay="true"
app:lottie_loop="true"
app:lottie_rawRes="@raw/lottie_shielding" />
<TextView
android:id="@+id/label_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/TextAppearance.AppCompat.Body1"
android:drawableEnd="@drawable/ic_content_copy"
android:drawablePadding="16dp"
android:text="Your Transparent Address"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/guideline_content_top"
app:layout_constraintVertical_bias="0.01" />
<TextView
android:id="@+id/text_address_part_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Zcash.TextAppearance.AddressPart"
android:textSize="20dp"
android:maxLines="1"
android:layout_marginTop="16dp"
app:layout_constraintBottom_toTopOf="@id/text_address_part_2"
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
app:layout_constraintTop_toBottomOf="@id/label_address"
app:layout_constraintVertical_bias="0.2"
app:layout_constraintVertical_chainStyle="packed"
tools:text=" ztestsaplin" />
<TextView
android:id="@+id/text_address_part_2"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/Zcash.TextAppearance.AddressPart"
android:textSize="20dp"
android:maxLines="1"
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
app:layout_constraintEnd_toEndOf="@id/text_address_part_1"
app:layout_constraintTop_toBottomOf="@id/text_address_part_1"
tools:text=" jceuu9s2p6t" />
<View
android:id="@+id/hit_area_address"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="@id/text_address_part_2"
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
app:layout_constraintTop_toTopOf="@id/label_address"
tools:background="@color/spacer" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_action"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/Zcash.Button"
android:backgroundTint="@color/selector_primary_button_activatable"
android:gravity="center"
android:padding="12dp"
android:text="Shield Transparent Funds"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="#000000"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.9" />
<TextView
android:id="@+id/text_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/TextAppearance.AppCompat.Body1"
android:text="Balance: 0.00000000"
android:gravity="center"
android:textSize="18sp"
app:layout_constraintBottom_toTopOf="@id/button_action"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_address_part_2"
app:layout_constraintVertical_bias="0.65"/>
</androidx.constraintlayout.widget.ConstraintLayout>

390
app/src/main/res/layout/fragment_backup.xml

@ -0,0 +1,390 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
android:background="@drawable/background_home">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_buttons"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.667" />
<!-- Address parts -->
<!-- Someday, there will be an advanced VirtualLayout that helps us do this without nesting but for now, this seems to be the only clean way to center all the fields -->
<!-- its tempting to do this programmatically but for now, it's always 24 words so I'll do it statically. If this ever changes, we'll probably be using Jetpack Compose by then so it will be easier to do in code -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/receive_address_parts"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:foregroundGravity="center"
android:padding="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/text_message"
>
<!-- -->
<!-- Column 1 -->
<!-- -->
<TextView
android:id="@+id/text_address_part_1"
style="@style/Zcash.TextAppearance.AddressPart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="drum"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_4"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/text_address_part_4"
style="@style/Zcash.TextAppearance.AddressPart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
tools:text="word"
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
app:layout_constraintTop_toBottomOf="@id/text_address_part_1"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_7" />
<TextView
android:id="@+id/text_address_part_7"
style="@style/Zcash.TextAppearance.AddressPart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
tools:text="word"
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
app:layout_constraintTop_toBottomOf="@id/text_address_part_4"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_10" />
<TextView
android:id="@+id/text_address_part_10"
style="@style/Zcash.TextAppearance.AddressPart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
tools:text="word"
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
app:layout_constraintTop_toBottomOf="@id/text_address_part_7"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_13" />
<TextView
android:id="@+id/text_address_part_13"
style="@style/Zcash.TextAppearance.AddressPart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
tools:text="word"
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
app:layout_constraintTop_toBottomOf="@id/text_address_part_10"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_16" />
<TextView
android:id="@+id/text_address_part_16"
style="@style/Zcash.TextAppearance.AddressPart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
tools:text="word"
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
app:layout_constraintTop_toBottomOf="@id/text_address_part_13"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_19" />
<TextView
android:id="@+id/text_address_part_19"
style="@style/Zcash.TextAppearance.AddressPart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
tools:text="word"
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
app:layout_constraintTop_toBottomOf="@id/text_address_part_16"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_22" />
<TextView
android:id="@+id/text_address_part_22"
style="@style/Zcash.TextAppearance.AddressPart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
tools:text="word"
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
app:layout_constraintTop_toBottomOf="@id/text_address_part_19" />
<!-- -->
<!-- Column 2 -->
<!-- -->
<TextView
android:id="@+id/text_address_part_2"
style="@style/Zcash.TextAppearance.AddressPart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
tools:text="fitness"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_5"
app:layout_constraintStart_toEndOf="@id/barrier_left_address_column_1"
app:layout_constraintTop_toTopOf="@id/text_address_part_1" />
<TextView
android:id="@+id/text_address_part_5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
style="@style/Zcash.TextAppearance.AddressPart"
tools:text="word"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_8"
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
app:layout_constraintTop_toBottomOf="@id/text_address_part_2" />
<TextView
android:id="@+id/text_address_part_8"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
style="@style/Zcash.TextAppearance.AddressPart"
tools:text="word"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_11"
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
app:layout_constraintTop_toBottomOf="@id/text_address_part_5" />
<TextView
android:id="@+id/text_address_part_11"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
style="@style/Zcash.TextAppearance.AddressPart"
tools:text="word"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_14"
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
app:layout_constraintTop_toBottomOf="@id/text_address_part_8" />
<TextView
android:id="@+id/text_address_part_14"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
style="@style/Zcash.TextAppearance.AddressPart"
tools:text="word"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_17"
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
app:layout_constraintTop_toBottomOf="@id/text_address_part_11" />
<TextView
android:id="@+id/text_address_part_17"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
style="@style/Zcash.TextAppearance.AddressPart"
tools:text="word"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_20"
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
app:layout_constraintTop_toBottomOf="@id/text_address_part_14" />
<TextView
android:id="@+id/text_address_part_20"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
style="@style/Zcash.TextAppearance.AddressPart"
tools:text="word"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_23"
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
app:layout_constraintTop_toBottomOf="@id/text_address_part_17" />
<TextView
android:id="@+id/text_address_part_23"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
style="@style/Zcash.TextAppearance.AddressPart"
tools:text="word"
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
app:layout_constraintTop_toBottomOf="@id/text_address_part_20" />
<!-- -->
<!-- Column 3 -->
<!-- -->
<TextView
android:id="@+id/text_address_part_3"
style="@style/Zcash.TextAppearance.AddressPart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
tools:text="goals"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_6"
app:layout_constraintStart_toEndOf="@id/barrier_left_address_column_2"
app:layout_constraintTop_toTopOf="@id/text_address_part_1" />
<TextView
android:id="@+id/text_address_part_6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
style="@style/Zcash.TextAppearance.AddressPart"
tools:text="word"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_9"
app:layout_constraintStart_toStartOf="@id/text_address_part_3"
app:layout_constraintTop_toBottomOf="@id/text_address_part_3" />
<TextView
android:id="@+id/text_address_part_9"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
style="@style/Zcash.TextAppearance.AddressPart"
tools:text="word"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_12"
app:layout_constraintStart_toStartOf="@id/text_address_part_3"
app:layout_constraintTop_toBottomOf="@id/text_address_part_6" />
<TextView
android:id="@+id/text_address_part_12"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
style="@style/Zcash.TextAppearance.AddressPart"
tools:text="word"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_15"
app:layout_constraintStart_toStartOf="@id/text_address_part_3"
app:layout_constraintTop_toBottomOf="@id/text_address_part_9" />
<TextView
android:id="@+id/text_address_part_15"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
style="@style/Zcash.TextAppearance.AddressPart"
tools:text="word"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_18"
app:layout_constraintStart_toStartOf="@id/text_address_part_3"
app:layout_constraintTop_toBottomOf="@id/text_address_part_12" />
<TextView
android:id="@+id/text_address_part_18"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
style="@style/Zcash.TextAppearance.AddressPart"
tools:text="word"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_21"
app:layout_constraintStart_toStartOf="@id/text_address_part_3"
app:layout_constraintTop_toBottomOf="@id/text_address_part_15" />
<TextView
android:id="@+id/text_address_part_21"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
style="@style/Zcash.TextAppearance.AddressPart"
tools:text="word"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_24"
app:layout_constraintStart_toStartOf="@id/text_address_part_3"
app:layout_constraintTop_toBottomOf="@id/text_address_part_18" />
<TextView
android:id="@+id/text_address_part_24"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
style="@style/Zcash.TextAppearance.AddressPart"
tools:text="word"
app:layout_constraintStart_toStartOf="@id/text_address_part_3"
app:layout_constraintTop_toBottomOf="@id/text_address_part_21" />
<!--
text_address_part_3, text_address_part_6, text_address_part_9, text_address_part_12, text_address_part_15, text_address_part_18, text_address_part_21, text_address_part_24
-->
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_left_address_column_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="end"
app:constraint_referenced_ids="text_address_part_1, text_address_part_4, text_address_part_7, text_address_part_10, text_address_part_13, text_address_part_16, text_address_part_19, text_address_part_22" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_left_address_column_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="end"
app:constraint_referenced_ids="text_address_part_2, text_address_part_5, text_address_part_8, text_address_part_11, text_address_part_14, text_address_part_17, text_address_part_20, text_address_part_23" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/text_birtdate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Birthday Height: 510,123"
android:textSize="20sp"
android:fontFamily="@font/inconsolata"
app:layout_constraintTop_toBottomOf="@id/receive_address_parts"
app:layout_constraintBottom_toTopOf="@id/text_message"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/TextAppearance.MaterialComponents.Body1"
android:gravity="center"
android:text=" "
android:textColor="@color/zcashWhite_50"
android:textSize="56dp"
android:alpha="0.03"
app:layout_constraintBottom_toBottomOf="@id/icon_logo"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/icon_logo"
tools:ignore="SpUsage" />
<ImageView
android:id="@+id/icon_logo"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="W,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.33333"
app:layout_constraintWidth_percent="0.4053398058"
android:visibility="invisible"
app:srcCompat="@drawable/ic_logo_landing"
android:contentDescription="@string/content_description_backup_zcash_logo" />
<!-- Choose release names from here https://en.wikipedia.org/wiki/List_of_woods -->
<TextView
android:id="@+id/text_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/TextAppearance.MaterialComponents.Headline6"
android:gravity="center"
android:paddingStart="32dp"
android:paddingEnd="32dp"
android:text="@string/backup_instruction_store_words"
android:textColor="@color/zcashWhite"
app:layout_constraintBottom_toTopOf="@id/guideline_buttons"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/icon_logo" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_positive"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/backup_button_primary"
android:textColor="@color/text_dark"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/guideline_buttons" />
</androidx.constraintlayout.widget.ConstraintLayout>

226
app/src/main/res/layout/fragment_balance_detail.xml

@ -0,0 +1,226 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/background_home">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_hit_area_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.04" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_hit_area_bottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.96" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_content_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.15" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_content_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.10" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_content_end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.95" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_hit_area_topp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.04" />
<View
android:id="@+id/hit_area_exit"
android:layout_width="68dp"
android:layout_height="68dp"
android:layout_marginStart="24dp"
android:alpha="0.3"
android:background="@android:color/transparent"
app:layout_constraintStart_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/guideline_hit_area_topp" />
<TextView
android:id="@+id/text_block_height_prefix"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:text="as of block "
android:textColor="@color/tx_text_light_dimmed"
app:layout_constraintEnd_toStartOf="@id/text_block_height"
app:layout_constraintTop_toBottomOf="@id/icon_exit" />
<TextView
android:id="@+id/text_block_height"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/tx_text_light_dimmed_less"
app:layout_constraintBaseline_toBaselineOf="@id/text_block_height_prefix"
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
tools:text="796,798/123,123" />
<View
android:id="@+id/background_group_outter"
android:layout_width="0dp"
android:layout_height="0dp"
android:alpha="0.5"
android:background="@drawable/background_gradient_balance_details"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.20"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/guideline_content_top"
app:layout_constraintWidth_percent="0.9" />
<View
android:id="@+id/background_group_inner"
android:layout_width="0dp"
android:layout_height="0dp"
android:alpha="0.5"
app:layout_constraintBottom_toBottomOf="@id/background_group_outter"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/background_group_outter"
app:layout_constraintWidth_percent="0.8382" />
<View
android:id="@+id/background_top"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="#25242B"
app:layout_constraintBottom_toTopOf="@id/background_bottom"
app:layout_constraintEnd_toEndOf="@id/background_group_inner"
app:layout_constraintHeight_percent="0.1"
app:layout_constraintStart_toStartOf="@id/background_group_inner"
app:layout_constraintTop_toTopOf="@id/background_group_inner"
app:layout_constraintVertical_chainStyle="spread_inside" />
<View
android:id="@+id/background_bottom"
android:layout_width="0dp"
android:layout_height="0dp"
android:alpha="0.1"
android:background="@drawable/background_balance_detail_amounts_container"
app:layout_constraintBottom_toBottomOf="@id/background_group_inner"
app:layout_constraintEnd_toEndOf="@id/background_top"
app:layout_constraintHeight_percent="0"
app:layout_constraintStart_toStartOf="@id/background_top"
app:layout_constraintTop_toBottomOf="@id/background_top" />
<ImageView
android:id="@+id/icon_exit"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@string/content_description_settings_back"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.088"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.065"
app:layout_constraintWidth_percent="0.08"
app:srcCompat="@drawable/ic_cancel"
app:tint="@color/text_light_dimmed" />
<TextView
android:id="@+id/text_shielded_hush_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="16dp"
android:fontFamily="@font/inconsolata"
android:textAllCaps="true"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
android:textColor="@color/zcashYellow"
app:layout_constraintTop_toTopOf="@id/background_top"
app:layout_constraintEnd_toStartOf="@+id/guideline_content_end"
tools:text="Shielded HUSH" />
<TextView
android:id="@+id/text_shield_amount"
android:layout_width="0dp"
android:layout_height="0dp"
android:fontFamily="@font/inconsolata"
android:includeFontPadding="false"
android:maxLines="1"
android:textAppearance="@style/Zcash.TextAppearance.Zec"
android:textSize="32sp"
app:autoSizeTextType="uniform"
app:layout_constraintBottom_toBottomOf="@id/background_top"
app:layout_constraintEnd_toEndOf="@+id/text_shielded_hush_title"
app:layout_constraintStart_toEndOf="@id/zec_sign_total"
app:layout_constraintTop_toBottomOf="@+id/text_shielded_hush_title"
tools:text="1.11111111" />
<!-- Z signs (possibly temporary) -->
<ImageView
android:id="@+id/zec_sign_shielded"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="2dp"
android:layout_marginTop="8dp"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
app:layout_constraintTop_toTopOf="@id/text_shield_amount"
app:layout_constraintWidth_percent="0.042"
app:srcCompat="@drawable/ic_zec_symbol_right" />
<ImageView
android:id="@+id/zec_sign_total"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="2dp"
android:layout_marginTop="8dp"
android:visibility="invisible"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
app:layout_constraintTop_toTopOf="@id/text_shield_amount"
app:layout_constraintWidth_percent="0.042"
app:srcCompat="@drawable/ic_zec_symbol_right" />
<TextView
android:id="@+id/text_status"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="32dp"
android:fontFamily="@font/inconsolata"
android:gravity="center"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/text_light_dimmed"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.088"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.65"
app:layout_constraintWidth_percent="0.7475728155"
tools:text="(2.427893 HUSH pending)\nadsf\nasdfasf\nasdfasdf\nadsfas\nasdf\nasdfas\nasdfas \nasdfas\nasdfas\nasdfas\nasdfas\nasdfas\nasdfas\nasdfas\nasdfas\nasdfas\nasdfas\nasdfas" />
</androidx.constraintlayout.widget.ConstraintLayout>

19
app/src/main/res/layout/fragment_confirm.xml

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/background_home"
xmlns:tools="http://schemas.android.com/tools">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/text_light"
android:text="send confirm"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

100
app/src/main/res/layout/fragment_funds_available.xml

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
android:background="@drawable/background_home">
<androidx.cardview.widget.CardView
android:id="@+id/card_background"
android:layout_width="0dp"
android:layout_height="0dp"
app:cardBackgroundColor="#292929"
app:cardCornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.33"
app:layout_constraintWidth_percent="0.8413" />
<ImageView
android:id="@+id/image_shield"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@string/content_description_send_shield"
android:elevation="8dp"
android:src="@drawable/ic_shielded"
app:layout_constraintBottom_toTopOf="@id/card_background"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.15625"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/card_background"
app:tint="#F5BA41" />
<TextView
android:id="@+id/text_exclamation"
android:layout_width="0dp"
android:layout_height="0dp"
android:elevation="8dp"
android:gravity="center"
android:paddingBottom="8dp"
android:text="!"
android:textColor="@android:color/black"
android:textSize="94dp"
app:layout_constraintBottom_toBottomOf="@id/image_shield"
app:layout_constraintEnd_toEndOf="@id/image_shield"
app:layout_constraintStart_toStartOf="@id/image_shield"
app:layout_constraintTop_toTopOf="@id/image_shield" />
<TextView
android:id="@+id/text_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:elevation="8dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
android:textSize="32dp"
android:text="Shielding!"
app:layout_constraintVertical_bias="0.24"
app:layout_constraintTop_toTopOf="@id/card_background"
app:layout_constraintBottom_toBottomOf="@id/card_background"
app:layout_constraintEnd_toEndOf="@id/card_background"
app:layout_constraintStart_toStartOf="@id/card_background" />
<TextView
android:id="@+id/text_body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:elevation="8dp"
android:textSize="20dp"
android:gravity="center"
android:text="You should never receive HUSH in a transparent address."
app:layout_constraintVertical_bias="0.24"
app:layout_constraintTop_toBottomOf="@id/text_title"
app:layout_constraintBottom_toBottomOf="@id/card_background"
app:layout_constraintEnd_toEndOf="@id/card_background"
app:layout_constraintStart_toStartOf="@id/card_background" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_action"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:layout_marginEnd="24dp"
android:layout_marginStart="24dp"
style="@style/Zcash.Button"
android:backgroundTint="@color/zcashWhite"
android:elevation="8dp"
android:gravity="center"
android:padding="16dp"
android:text="Got it"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="#000000"
app:layout_constraintBottom_toBottomOf="@id/card_background"
app:layout_constraintEnd_toEndOf="@id/card_background"
app:layout_constraintStart_toStartOf="@id/card_background" />
</androidx.constraintlayout.widget.ConstraintLayout>

224
app/src/main/res/layout/fragment_history.xml

@ -0,0 +1,224 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/background_home">
<!-- -->
<!-- Guidelines -->
<!-- -->
<!-- TODO: redo these keylines to match the designs, exactly -->
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_bottom_buttons"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.7017784" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_keyline_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.054" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_keyline_end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.946" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_keyline_bottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="1.0" />
<!-- Back Button -->
<ImageView
android:id="@+id/back_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:tint="@color/text_light"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.05"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.065"
app:srcCompat="@drawable/ic_arrow_back_black_24dp"
android:contentDescription="@string/content_description_history_back" />
<View
android:id="@+id/back_button_hit_area"
android:layout_width="56dp"
android:layout_height="56dp"
android:clickable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.01"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.045"
android:focusable="true" />
<TextView
android:id="@+id/text_balance_available"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/history_balance_updating"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/text_light"
android:visibility="visible"
app:layout_constraintBottom_toTopOf="@id/text_balance_description"
app:layout_constraintEnd_toStartOf="@id/label_balance"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/back_button" />
<TextView
android:id="@+id/label_balance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="@string/history_balance_available"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/colorPrimary"
app:layout_constraintBaseline_toBaselineOf="@id/text_balance_available"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/text_balance_available" />
<TextView
android:id="@+id/text_balance_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/history_instruction_enter_amount"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
android:textColor="@color/text_light_dimmed"
app:layout_constraintBottom_toBottomOf="@id/back_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_balance_available" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/header"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@drawable/background_header"
android:onClick="copyAddress"
android:paddingBottom="24dp"
android:paddingTop="24dp"
app:layout_constraintEnd_toEndOf="@id/guideline_keyline_end"
app:layout_constraintStart_toStartOf="@id/guideline_keyline_start"
app:layout_constraintTop_toBottomOf="@id/back_button_hit_area">
<TextView
android:id="@+id/text_header_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="@string/history_header_transactions"
android:textColor="@color/text_light"
android:textSize="22sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/label_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:paddingEnd="8dp"
android:text="@string/history_address_label"
android:textColor="@color/text_light_dimmed"
app:layout_constraintStart_toStartOf="@id/text_header_title"
app:layout_constraintTop_toBottomOf="@+id/text_header_title" />
<TextView
android:id="@+id/text_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="@color/colorPrimary"
app:layout_constraintStart_toEndOf="@id/label_address"
app:layout_constraintTop_toBottomOf="@+id/text_header_title"
tools:text="zs1g7cqw...9qmvyzgm" />
<ImageView
android:id="@+id/image_copy"
android:layout_width="0dp"
android:layout_height="0dp"
android:src="@drawable/ic_content_copy"
app:layout_constraintBottom_toBottomOf="@id/label_address"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.15"
app:layout_constraintStart_toEndOf="@id/text_address"
app:layout_constraintTop_toTopOf="@id/label_address"
android:contentDescription="@string/content_description_history_copy" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:id="@+id/empty_footer"
android:layout_width="0dp"
android:layout_height="16dp"
android:background="@drawable/background_footer"
app:layout_constraintEnd_toEndOf="@id/guideline_keyline_end"
app:layout_constraintStart_toStartOf="@id/guideline_keyline_start"
app:layout_constraintTop_toBottomOf="@id/header" />
<TextView
android:id="@+id/empty_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/history_empty_text"
android:textColor="@color/text_light"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_transactions"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:paddingBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/guideline_keyline_end"
app:layout_constraintStart_toStartOf="@id/guideline_keyline_start"
app:layout_constraintTop_toBottomOf="@id/empty_footer"
tools:itemCount="15"
tools:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_transaction"
tools:orientation="vertical"
tools:visibility="gone" />
<View
android:id="@+id/footer_fade"
android:layout_width="0dp"
android:layout_height="56dp"
android:layout_marginEnd="2dp"
android:layout_marginStart="2dp"
android:alpha="0.8"
android:visibility="gone"
android:background="@drawable/background_gradient_bottom"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/guideline_keyline_end"
app:layout_constraintStart_toStartOf="@id/guideline_keyline_start" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_empty_views"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="empty_message" />
</androidx.constraintlayout.widget.ConstraintLayout>

470
app/src/main/res/layout/fragment_home.xml

@ -0,0 +1,470 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
android:background="@drawable/background_home">
<View
android:id="@+id/guide_keys"
android:layout_width="0dp"
android:layout_height="0dp"
android:alpha="0.3"
android:background="@android:color/transparent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.38196601125"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.58"
app:layout_constraintWidth_percent="0.7475728155" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_hit_area_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.04" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_send_amount_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.13" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_send_amount_bottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.23" />
<TextView
android:id="@+id/text_balance_available"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/home_balance_updating"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/text_light"
android:visibility="visible"
app:layout_constraintEnd_toStartOf="@id/label_balance"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_send_amount" />
<TextView
android:id="@+id/label_balance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="@string/home_balance_available"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/colorPrimary"
android:visibility="gone"
app:layout_constraintBaseline_toBaselineOf="@id/text_balance_available"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/text_balance_available" />
<TextView
android:id="@+id/text_balance_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/home_instruction_enter_amount"
android:visibility="gone"
tools:visibility="visible"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
android:textColor="@color/text_light_dimmed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_balance_available" />
<!-- -->
<!-- Number Pad -->
<!-- -->
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/button_number_pad_1"
android:layout_width="0dp"
android:layout_height="0dp"
style="@style/Zcash.TextView.NumberPad"
android:text="1"
app:layout_constraintBottom_toTopOf="@id/button_number_pad_4"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintStart_toStartOf="@id/guide_keys"
app:layout_constraintTop_toTopOf="@id/guide_keys"
app:layout_constraintVertical_chainStyle="spread_inside"
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent"
tools:ignore="HardcodedText" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/button_number_pad_2"
android:layout_width="0dp"
android:layout_height="0dp"
style="@style/Zcash.TextView.NumberPad"
android:text="2"
app:layout_constraintBottom_toTopOf="@id/button_number_pad_5"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="@id/guide_keys"
app:layout_constraintStart_toStartOf="@id/guide_keys"
app:layout_constraintTop_toTopOf="@id/guide_keys"
app:layout_constraintVertical_chainStyle="spread_inside"
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent"
tools:ignore="HardcodedText" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/button_number_pad_3"
android:layout_width="0dp"
android:layout_height="0dp"
style="@style/Zcash.TextView.NumberPad"
android:text="3"
app:layout_constraintBottom_toTopOf="@id/button_number_pad_6"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="@id/guide_keys"
app:layout_constraintTop_toTopOf="@id/guide_keys"
app:layout_constraintVertical_chainStyle="spread_inside"
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent"
tools:ignore="HardcodedText" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/button_number_pad_4"
android:layout_width="0dp"
android:layout_height="0dp"
style="@style/Zcash.TextView.NumberPad"
android:text="4"
app:layout_constraintBottom_toTopOf="@id/button_number_pad_7"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="@id/guide_keys"
app:layout_constraintTop_toBottomOf="@id/button_number_pad_1"
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent"
tools:ignore="HardcodedText" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/button_number_pad_5"
android:layout_width="0dp"
android:layout_height="0dp"
style="@style/Zcash.TextView.NumberPad"
android:text="5"
app:layout_constraintBottom_toTopOf="@id/button_number_pad_8"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="@id/guide_keys"
app:layout_constraintStart_toStartOf="@id/guide_keys"
app:layout_constraintTop_toBottomOf="@id/button_number_pad_2"
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent"
tools:ignore="HardcodedText" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/button_number_pad_6"
android:layout_width="0dp"
android:layout_height="0dp"
style="@style/Zcash.TextView.NumberPad"
android:text="6"
app:layout_constraintBottom_toTopOf="@id/button_number_pad_9"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="@id/guide_keys"
app:layout_constraintTop_toBottomOf="@id/button_number_pad_3"
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent"
tools:ignore="HardcodedText" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/button_number_pad_7"
android:layout_width="0dp"
android:layout_height="0dp"
style="@style/Zcash.TextView.NumberPad"
android:text="7"
app:layout_constraintBottom_toTopOf="@id/button_number_pad_decimal"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="@id/guide_keys"
app:layout_constraintTop_toBottomOf="@id/button_number_pad_4"
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent"
tools:ignore="HardcodedText" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/button_number_pad_8"
android:layout_width="0dp"
android:layout_height="0dp"
style="@style/Zcash.TextView.NumberPad"
android:text="8"
app:layout_constraintBottom_toTopOf="@id/button_number_pad_0"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="@id/guide_keys"
app:layout_constraintStart_toStartOf="@id/guide_keys"
app:layout_constraintTop_toBottomOf="@id/button_number_pad_5"
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent"
tools:ignore="HardcodedText" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/button_number_pad_9"
android:layout_width="0dp"
android:layout_height="0dp"
style="@style/Zcash.TextView.NumberPad"
android:text="9"
app:layout_constraintBottom_toTopOf="@id/button_number_pad_back"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="@id/guide_keys"
app:layout_constraintTop_toBottomOf="@id/button_number_pad_6"
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent"
tools:ignore="HardcodedText" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/button_number_pad_decimal"
android:layout_width="0dp"
android:layout_height="0dp"
style="@style/Zcash.TextView.NumberPad"
android:paddingBottom="2dp"
android:text="@string/key_decimal"
app:layout_constraintBottom_toBottomOf="@id/guide_keys"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="@id/guide_keys"
app:layout_constraintTop_toBottomOf="@id/button_number_pad_7"
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/button_number_pad_0"
android:layout_width="0dp"
android:layout_height="0dp"
style="@style/Zcash.TextView.NumberPad"
android:text="0"
app:layout_constraintBottom_toBottomOf="@id/guide_keys"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="@id/guide_keys"
app:layout_constraintStart_toStartOf="@id/guide_keys"
app:layout_constraintTop_toBottomOf="@id/button_number_pad_8"
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent"
tools:ignore="HardcodedText" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/button_number_pad_back"
android:layout_width="0dp"
android:layout_height="0dp"
style="@style/Zcash.TextView.NumberPad"
android:text="@string/key_backspace"
app:layout_constraintBottom_toBottomOf="@id/guide_keys"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="@id/guide_keys"
app:layout_constraintTop_toBottomOf="@id/button_number_pad_9"
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent" />
<!--
<View
android:id="@+id/layer_lock"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
android:background="#D0000000"
tools:visibility="gone"
android:elevation="5dp"
android:focusable="true" />
-->
<!-- -->
<!-- Upper Layer -->
<!-- -->
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lottie_button_loading"
android:layout_width="0dp"
android:layout_height="0dp"
android:elevation="6dp"
app:layout_constraintHeight_percent="0.075"
app:layout_constraintWidth_percent="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/guide_keys"
app:layout_constraintBottom_toTopOf="@id/text_history"
app:layout_constraintVertical_bias="0.2"
app:layout_constraintVertical_chainStyle="packed"
android:visibility="invisible"
app:lottie_autoPlay="false"
app:lottie_loop="false"
app:lottie_rawRes="@raw/lottie_button_loading_new" />
<TextView
android:id="@+id/button_send_amount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Downloading...5%"
android:elevation="6dp"
android:paddingTop="24dp"
android:paddingBottom="24dp"
android:paddingStart="56dp"
android:paddingEnd="56dp"
android:textColor="@color/selector_button_text_dark"
android:textSize="16sp"
app:layout_constraintTop_toTopOf="@id/lottie_button_loading"
app:layout_constraintBottom_toBottomOf="@id/lottie_button_loading"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<ImageView
android:id="@+id/icon_scan"
android:layout_width="0dp"
android:layout_height="0dp"
android:padding="2dp"
android:elevation="6dp"
app:tint="@color/colorAccent"
app:layout_constraintBottom_toBottomOf="@id/icon_profile"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.038"
app:layout_constraintHorizontal_bias="0.088"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/icon_profile"
app:layout_constraintWidth_percent="0.0887"
app:srcCompat="@drawable/ic_address_qr"
android:contentDescription="@string/scan_address_title" />
<ImageView
android:id="@+id/icon_profile"
android:layout_width="0dp"
android:layout_height="0dp"
android:elevation="6dp"
app:tint="@color/colorAccent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintHorizontal_bias="0.912"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.064"
app:layout_constraintWidth_percent="0.08"
app:srcCompat="@drawable/ic_account_circle"
android:contentDescription="@string/content_description_home_icon_profile" />
<View
android:id="@+id/hit_area_receive"
android:layout_width="68dp"
android:layout_height="68dp"
android:layout_marginStart="24dp"
android:alpha="0.3"
android:background="@android:color/transparent"
android:elevation="6dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/guideline_hit_area_top" />
<View
android:id="@+id/hit_area_profile"
android:layout_width="68dp"
android:layout_height="68dp"
android:layout_marginEnd="24dp"
android:background="@android:color/transparent"
android:elevation="6dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/guideline_hit_area_top" />
<TextView
android:id="@+id/text_title"
android:layout_width="0dp"
android:layout_height="0dp"
android:text="@string/home_title"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/text_light"
android:gravity="center"
android:maxLines="1"
app:autoSizeMaxTextSize="16sp"
app:autoSizeMinTextSize="2sp"
app:autoSizeTextType="uniform"
app:layout_constraintTop_toTopOf="@id/icon_profile"
app:layout_constraintBottom_toBottomOf="@id/icon_profile"
app:layout_constraintStart_toEndOf="@id/hit_area_receive"
app:layout_constraintEnd_toStartOf="@id/hit_area_profile" />
<TextView
android:id="@+id/text_history"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="12dp"
android:elevation="6dp"
android:layout_marginTop="12dp"
android:text="@string/home_history_button_text"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/colorAccent"
app:tint="@color/colorAccent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/lottie_button_loading" />
<TextView
android:id="@+id/text_send_amount"
android:elevation="6dp"
android:layout_width="match_parent"
android:layout_height="0dp"
android:gravity="center"
android:includeFontPadding="false"
tools:text="0.00000000"
android:textAppearance="@style/Zcash.TextAppearance.Zec"
android:textSize="72sp"
android:maxLines="1"
android:paddingStart="16dp"
android:paddingEnd="16dp"
app:autoSizeTextType="uniform"
app:layout_constraintBottom_toTopOf="@id/guideline_send_amount_bottom"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/guideline_send_amount_top" />
<!-- -->
<!-- Banner -->
<!-- -->
<TextView
android:id="@+id/text_banner_message"
android:elevation="6dp"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@drawable/background_banner"
android:paddingBottom="8dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="8dp"
android:text="@string/home_no_balance"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/text_light"
app:layout_constraintEnd_toEndOf="@id/icon_profile"
app:layout_constraintStart_toStartOf="@id/icon_scan"
app:layout_constraintTop_toBottomOf="@id/text_send_amount" />
<TextView
android:id="@+id/text_banner_action"
android:elevation="6dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/home_instruction_fund_now"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/colorPrimary"
app:layout_constraintBaseline_toBaselineOf="@id/text_banner_message"
app:layout_constraintEnd_toEndOf="@id/text_banner_message" />
<View
android:id="@+id/hit_area_balance"
android:layout_width="0dp"
android:layout_height="0dp"
app:elevation="8dp"
app:layout_constraintStart_toEndOf="@id/hit_area_receive"
app:layout_constraintEnd_toStartOf="@id/hit_area_profile"
app:layout_constraintTop_toTopOf="@id/text_title"
app:layout_constraintBottom_toBottomOf="@id/text_balance_description" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_balance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="text_balance_available,label_balance" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_banner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="text_banner_message, text_banner_action" />
</androidx.constraintlayout.widget.ConstraintLayout>

84
app/src/main/res/layout/fragment_landing.xml

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/background_home">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_buttons"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.667" />
<TextView
android:id="@+id/textView"
style="@style/TextAppearance.MaterialComponents.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:alpha="0.05"
android:gravity="center"
android:textColor="@color/zcashWhite_50"
android:textSize="56dp"
app:layout_constraintBottom_toBottomOf="@id/icon_logo"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/icon_logo" />
<!-- Choose release names from here https://en.wikipedia.org/wiki/List_of_woods -->
<ImageView
android:id="@+id/icon_logo"
android:layout_width="250dp"
android:layout_height="250dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="W,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.25"
app:layout_constraintWidth_percent="0.5"
app:srcCompat="@drawable/ic_logo_landing" />
<TextView
android:id="@+id/text_message"
style="@style/TextAppearance.MaterialComponents.Headline6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingStart="32dp"
android:paddingEnd="32dp"
android:textColor="@color/zcashWhite"
app:layout_constraintBottom_toTopOf="@id/guideline_buttons"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/icon_logo" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_negative"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Zcash.Button.OutlinedButton"
android:text="@string/landing_button_secondary"
android:textColor="@color/text_light"
app:layout_constraintEnd_toStartOf="@id/button_positive"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/guideline_buttons" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_positive"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="@string/landing_button_primary"
android:textColor="@color/text_dark"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/button_negative"
app:layout_constraintTop_toTopOf="@id/guideline_buttons" />
</androidx.constraintlayout.widget.ConstraintLayout>

19
app/src/main/res/layout/fragment_memo.xml

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/background_home"
xmlns:tools="http://schemas.android.com/tools">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/text_light"
android:text="send memo"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

252
app/src/main/res/layout/fragment_profile.xml

@ -0,0 +1,252 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/background_home">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_hit_area_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.04" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_send_amount_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.13" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_content_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.15" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_content_end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.85" />
<View
android:id="@+id/hit_area_exit"
android:layout_width="68dp"
android:layout_height="68dp"
android:layout_marginEnd="24dp"
android:layout_marginStart="24dp"
android:alpha="0.3"
android:background="@android:color/transparent"
android:elevation="6dp"
app:layout_constraintStart_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/guideline_hit_area_top" />
<ImageView
android:id="@+id/icon_exit"
android:layout_width="0dp"
android:layout_height="0dp"
android:elevation="6dp"
app:tint="@color/text_light_dimmed"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.088"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.065"
app:layout_constraintWidth_percent="0.08"
app:srcCompat="@drawable/ic_cancel"
android:contentDescription="@string/content_description_profile_back" />
<ImageView
android:id="@+id/icon_settings"
android:layout_width="0dp"
android:layout_height="0dp"
android:elevation="6dp"
app:tint="@color/text_light_dimmed"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.912"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.065"
app:layout_constraintWidth_percent="0.08"
app:srcCompat="@drawable/ic_settings"
android:contentDescription="@string/content_description_profile_settings" />
<ImageView
android:id="@+id/icon_profile"
android:layout_width="0dp"
android:layout_height="0dp"
android:elevation="6dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.212"
app:layout_constraintWidth_percent="0.4"
app:srcCompat="@drawable/ic_profile_zebra_01"
android:contentDescription="@string/content_description_profile_zebra" />
<View
android:id="@+id/hit_area_settings"
android:layout_width="68dp"
android:layout_height="68dp"
android:layout_marginEnd="24dp"
android:background="@android:color/transparent"
android:elevation="6dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/guideline_hit_area_top" />
<TextView
android:id="@+id/text_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
style="@style/TextAppearance.AppCompat.Body1"
android:text="@string/profile_shielded_user"
android:textSize="20sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/icon_profile" />
<TextView
android:id="@+id/text_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Zcash.TextAppearance.AddressPart"
android:textColor="@color/text_light_dimmed"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_username"
tools:text="zs1g7cqw...9qmvyzgm" />
<ImageView
android:id="@+id/image_copy"
android:layout_width="0dp"
android:layout_height="0dp"
android:src="@drawable/ic_content_copy"
app:layout_constraintBottom_toBottomOf="@id/text_address"
app:layout_constraintDimensionRatio="W,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.15"
app:layout_constraintStart_toEndOf="@id/text_address"
app:layout_constraintTop_toTopOf="@id/text_address"
android:contentDescription="@string/content_description_profile_copy" />
<View
android:id="@+id/hit_area_address"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="16dp"
android:onClick="copyAddress"
app:layout_constraintBottom_toTopOf="@id/button_feedback"
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
app:layout_constraintStart_toStartOf="@id/text_address"
app:layout_constraintTop_toTopOf="@id/text_username"
tools:background="@color/spacer" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_feedback"
style="@style/Zcash.Button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="12dp"
android:text="@string/profile_send_feedback"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="#000000"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
app:layout_constraintTop_toBottomOf="@id/text_address"
app:layout_constraintVertical_bias="0.1"
tools:visibility="invisible" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_backup"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/Zcash.Button.OutlinedButton"
android:gravity="center"
android:padding="12dp"
android:text="@string/profile_backup_wallet"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/text_light"
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
app:layout_constraintTop_toBottomOf="@id/button_feedback" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_rescan"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/Zcash.Button.OutlinedButton.Primary"
android:gravity="center"
android:padding="12dp"
android:text="@string/profile_rescan_wallet"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/colorPrimary"
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
app:layout_constraintTop_toBottomOf="@id/button_backup" />
<TextView
android:id="@+id/button_logs"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
style="@style/TextAppearance.AppCompat.Body1"
android:text="@string/profile_see_application_logs"
android:textColor="@color/selector_button_text_light_dimmed"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/button_rescan" />
<!-- -->
<!-- Banner -->
<!-- -->
<TextView
android:id="@+id/text_banner_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@drawable/background_banner"
android:elevation="6dp"
android:paddingBottom="8dp"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:text="@string/profile_ecc_wallet"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/selector_button_text_light"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
app:layout_constraintTop_toBottomOf="@id/button_logs"
tools:ignore="RtlSymmetry" />
<TextView
android:id="@+id/text_version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:elevation="6dp"
tools:text="v1.0.0-alpha05"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/text_light_dimmed"
app:layout_constraintBaseline_toBaselineOf="@id/text_banner_message"
app:layout_constraintEnd_toEndOf="@id/text_banner_message" />
</androidx.constraintlayout.widget.ConstraintLayout>

200
app/src/main/res/layout/fragment_restore.xml

@ -0,0 +1,200 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
android:background="@drawable/background_home">
<TextView
android:id="@+id/text_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="start"
android:paddingStart="32dp"
android:paddingTop="32dp"
android:text="Restoring from a backup"
android:textColor="@color/text_light"
android:textSize="24sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/text_subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="start"
android:paddingBottom="32dp"
android:paddingStart="32dp"
android:paddingTop="18dp"
android:text="You will need to enter all 24 seed words.\nDon't worry, we will find them as you type."
android:textColor="@color/text_light_dimmed"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_title" />
<com.tylersuehr.chips.ChipsInputLayout
android:id="@+id/chips_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/zcashBlack_54"
android:hint="Enter the 1st seed word..."
android:overScrollMode="never"
android:paddingEnd="16dp"
android:paddingStart="16dp"
android:textColor="@color/text_light"
android:textColorHint="#757575"
app:hideKeyboardOnChipClick="false"
app:allowCustomChips="false"
app:chip_showDelete="false"
app:chip_showDetails="true"
app:chip_textColor="@color/text_light_dimmed"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_subtitle" />
<View
android:id="@+id/divider_top"
android:layout_width="match_parent"
android:layout_height="1px"
android:background="@color/text_light_dimmed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/chips_input" />
<View
android:id="@+id/divider_bottom"
android:layout_width="match_parent"
android:layout_height="1px"
android:background="@color/text_light_dimmed"
app:layout_constraintBottom_toBottomOf="@id/chips_input"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/text_layout_birthdate"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="Enter wallet birthday height (recommended)"
android:theme="@style/Zcash.Overlay.TextInputLayout"
app:helperText="e.g. 419,200. This determines where to start scanning for transactions. Leave it blank to scan from the beginning, which takes a while."
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/divider_bottom"
app:layout_constraintWidth_percent="0.84">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_birthdate"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:imeOptions="actionDone"
android:inputType="number"
android:maxLength="8"
android:singleLine="true"
android:textColor="@color/text_light"
android:textColorHint="@color/text_light_dimmed" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_done"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="32dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="Import"
android:textColor="@color/text_dark"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_layout_birthdate" />
<TextView
android:id="@+id/button_clear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:elevation="8dp"
android:text="clear"
android:textAllCaps="true"
android:textColor="@color/text_light"
android:textSize="12dp"
app:layout_constraintEnd_toEndOf="parent"
android:padding="8dp"
app:layout_constraintBottom_toTopOf="@id/divider_top"/>
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lottie_success"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="W,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.33333"
app:layout_constraintWidth_percent="0.4053398058"
app:lottie_autoPlay="true"
app:lottie_loop="false"
app:lottie_rawRes="@raw/lottie_success" />
<!-- <ImageView-->
<!-- android:id="@+id/icon_success"-->
<!-- android:layout_width="0dp"-->
<!-- android:layout_height="0dp"-->
<!-- app:tint="#00FF00"-->
<!-- app:layout_constraintBottom_toBottomOf="parent"-->
<!-- app:layout_constraintDimensionRatio="W,1:1"-->
<!-- app:layout_constraintEnd_toEndOf="parent"-->
<!-- app:layout_constraintStart_toStartOf="parent"-->
<!-- app:layout_constraintTop_toTopOf="parent"-->
<!-- app:layout_constraintVertical_bias="0.33333"-->
<!-- app:layout_constraintWidth_percent="0.4053398058"-->
<!-- app:srcCompat="@drawable/ic_check_shield" />-->
<TextView
android:id="@+id/text_success"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_marginTop="12dp"
android:text="Success"
android:textColor="@color/text_light"
android:textSize="24sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/lottie_success" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_success"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="View Wallet"
android:textColor="@color/text_dark"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_success"
/>
<androidx.constraintlayout.widget.Group
android:id="@+id/group_success"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="lottie_success, text_success, button_success" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_done"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="button_done, text_layout_birthdate" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="text_title, text_subtitle, chips_input, divider_bottom, divider_top" />
</androidx.constraintlayout.widget.ConstraintLayout>

224
app/src/main/res/layout/fragment_scan.xml

@ -0,0 +1,224 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- Guidelines -->
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_overlay_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.136" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_overlay_end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.864" />
<!-- Spacers -->
<Space
android:id="@+id/spacer_bottom_left"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintWidth_percent="0.116"
tools:background="@color/zcashRed" />
<Space
android:id="@+id/spacer_bottom_right"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintWidth_percent="0.116"
tools:background="@color/zcashRed" />
<androidx.camera.view.PreviewView
android:id="@+id/preview"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- Overlays -->
<View
android:id="@+id/overlay_bottom"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@color/scan_overlay_background"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/spacer_overlay" />
<View
android:id="@+id/overlay_end"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@color/scan_overlay_background"
app:layout_constraintBottom_toBottomOf="@id/spacer_overlay"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/spacer_overlay"
app:layout_constraintTop_toTopOf="@id/spacer_overlay" />
<View
android:id="@+id/overlay_start"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@color/scan_overlay_background"
app:layout_constraintBottom_toBottomOf="@id/spacer_overlay"
app:layout_constraintEnd_toStartOf="@id/spacer_overlay"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/spacer_overlay" />
<View
android:id="@+id/overlay_top"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@color/scan_overlay_background"
app:layout_constraintBottom_toTopOf="@id/spacer_overlay"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- being anal with these -->
<View
android:id="@+id/overlay_top_left_notch"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@color/scan_overlay_background"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintStart_toEndOf="@id/overlay_start"
app:layout_constraintTop_toBottomOf="@id/overlay_top"
app:layout_constraintWidth_percent="0.02" />
<View
android:id="@+id/overlay_top_right_notch"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@color/scan_overlay_background"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toStartOf="@id/overlay_end"
app:layout_constraintTop_toBottomOf="@id/overlay_top"
app:layout_constraintWidth_percent="0.02" />
<View
android:id="@+id/overlay_bottom_left_notch"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@color/scan_overlay_background"
app:layout_constraintBottom_toTopOf="@id/overlay_bottom"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintStart_toEndOf="@id/overlay_start"
app:layout_constraintWidth_percent="0.02" />
<View
android:id="@+id/overlay_bottom_right_notch"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@color/scan_overlay_background"
app:layout_constraintBottom_toTopOf="@id/overlay_bottom"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toStartOf="@id/overlay_end"
app:layout_constraintWidth_percent="0.02" />
<ImageView
android:id="@+id/scan_frame"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@string/content_description_scan_frame"
android:scaleType="centerCrop"
android:src="@drawable/ic_scan_frame"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toStartOf="@id/spacer_bottom_right"
app:layout_constraintStart_toEndOf="@id/spacer_bottom_left"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.3" />
<TextView
android:id="@+id/text_scan_error"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:ellipsize="end"
android:fontFamily="@font/inconsolata"
android:gravity="center"
android:paddingBottom="16dp"
android:paddingTop="8dp"
android:textColor="@color/text_light_dimmed"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="@id/scan_frame"
app:layout_constraintStart_toStartOf="@id/scan_frame"
app:layout_constraintTop_toBottomOf="@id/scan_frame"
tools:text="invalid:\nz98yz9uhz98uyz988yz98hzu8hgziouzgi9uzg9z8uogozuz z98yz9uhz98uyz988yz98hzu8hgziouzgi9uzg9z8uogozuzz98yz9uhz98uyz988yz98hzu8hgziouzgi9uzg9z8uogozuzz98yz9uhz98uyz988yz98hzu8hgziouzgi9uzg9z8uogozuzz98yz9uhz98uyz988yz98hzu8hgziouzgi9uzg9z8uogozuzz98yz9uhz98uyz988yz98hzu8hgziouzgi9uzg9z8uogozuzz98yz9uhz98uyz988yz98hzu8hgziouzgi9uzg9z8uogozuz" />
<ImageView
android:id="@+id/back_button"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@string/content_description_scan_back"
app:tint="@color/text_light_dimmed"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.088"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.065"
app:layout_constraintWidth_percent="0.08"
app:srcCompat="@drawable/ic_cancel" />
<View
android:id="@+id/back_button_hit_area"
android:layout_width="68dp"
android:layout_height="68dp"
android:layout_marginStart="24dp"
android:clickable="true"
android:focusable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.045" />
<TextView
android:id="@+id/text_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/TextAppearance.MaterialComponents.Headline6"
android:maxLines="1"
android:text="@string/scan_address_title"
android:textColor="@color/text_light"
app:autoSizeTextType="uniform"
app:layout_constraintBottom_toBottomOf="@id/back_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/back_button_hit_area"
app:layout_constraintTop_toTopOf="@id/back_button" />
<Space
android:id="@+id/spacer_overlay"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toStartOf="@id/guideline_overlay_end"
app:layout_constraintStart_toEndOf="@id/guideline_overlay_start"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.305"
tools:background="#60ff0000" />
</androidx.constraintlayout.widget.ConstraintLayout>

530
app/src/main/res/layout/fragment_send.xml

@ -0,0 +1,530 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/background_home">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_content_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.18" />
<!-- Back Button -->
<ImageView
android:id="@+id/back_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/content_description_send_back"
app:tint="@color/text_light"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.05"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.065"
app:srcCompat="@drawable/ic_arrow_back_black_24dp" />
<View
android:id="@+id/back_button_hit_area"
android:layout_width="56dp"
android:layout_height="56dp"
android:clickable="true"
android:focusable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.01"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.045" />
<TextView
android:id="@+id/text_send_amount"
android:layout_width="0dp"
android:layout_height="48dp"
android:elevation="6dp"
android:gravity="bottom|center_horizontal"
android:includeFontPadding="false"
android:maxLines="1"
android:paddingStart="16dp"
android:paddingTop="0dp"
android:paddingEnd="16dp"
android:paddingBottom="0dp"
android:textAppearance="@style/Zcash.TextAppearance.Zec"
android:textSize="200sp"
app:autoSizeMaxTextSize="40sp"
app:autoSizeMinTextSize="8sp"
app:autoSizeTextType="uniform"
app:layout_constraintBottom_toTopOf="@id/text_banner_message_start"
app:layout_constraintEnd_toStartOf="@id/button_send"
app:layout_constraintStart_toEndOf="@id/spacer_title"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="20.10000000" />
<TextView
android:id="@+id/text_banner_message_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:elevation="6dp"
android:text="@string/send_fund_source_prefix"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:textColor="@color/text_light"
app:layout_constraintBottom_toTopOf="@id/guideline_content_top"
app:layout_constraintEnd_toStartOf="@id/text_banner_wallet_type"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_send_amount" />
<TextView
android:id="@+id/text_banner_wallet_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:elevation="6dp"
android:text="@string/send_fund_source_highlight"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:textColor="@color/colorPrimary"
app:layout_constraintBaseline_toBaselineOf="@id/text_banner_message_start"
app:layout_constraintEnd_toStartOf="@id/text_banner_message_end"
app:layout_constraintStart_toEndOf="@id/text_banner_message_start" />
<TextView
android:id="@+id/text_banner_message_end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:elevation="6dp"
android:text="@string/send_fund_source_suffix"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:textColor="@color/text_light"
app:layout_constraintBaseline_toBaselineOf="@id/text_banner_message_start"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/text_banner_wallet_type" />
<!-- Down Arrow -->
<ImageView
android:id="@+id/image_down_arrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/content_description_send_arrow"
app:tint="@color/colorPrimary"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/text_banner_message_end"
app:layout_constraintStart_toEndOf="@id/text_banner_message_end"
app:layout_constraintTop_toTopOf="@id/text_banner_message_end"
app:srcCompat="@drawable/ic_baseline_keyboard_arrow_down_24" />
<!-- spacer to help with centering the title yet giving it maximum available size -->
<Space
android:id="@+id/spacer_title"
android:layout_width="72dp"
android:layout_height="38dp"
app:layout_constraintBottom_toBottomOf="@id/button_send"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.05"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/button_send" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_send"
android:layout_width="wrap_content"
android:layout_height="38dp"
android:backgroundTint="@color/colorAccent"
android:maxLines="1"
android:paddingStart="2dp"
android:paddingEnd="2dp"
android:text="@string/send_button_primary"
android:textColor="@color/text_dark"
app:autoSizeMinTextSize="6sp"
app:autoSizeTextType="uniform"
app:layout_constraintBottom_toBottomOf="@id/back_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.95"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/back_button" />
<TextView
android:id="@+id/text_address_error"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:fontFamily="@font/inconsolata"
android:gravity="center"
android:maxLines="1"
android:paddingEnd="8dp"
android:paddingStart="8dp"
android:textColor="@color/zcashRed"
android:textSize="14sp"
app:autoSizeMaxTextSize="12sp"
app:autoSizeMinTextSize="6sp"
app:autoSizeTextType="uniform"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_banner_message_start"
tools:text="Please enter a larger amount of money also please enter a shorter sentence" />
<ScrollView
android:id="@+id/scroll_content"
android:layout_width="0dp"
android:layout_height="0dp"
android:fillViewport="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_address_error">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Space
android:id="@+id/spacer_lower_content"
android:layout_width="1dp"
android:layout_height="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.04"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_no_z_address" />
<!-- Input: Address -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/text_layout_address"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/send_address_hint"
android:theme="@style/Zcash.Overlay.TextInputLayout"
app:endIconDrawable="@drawable/ic_qr_scan"
app:endIconMode="custom"
app:helperText="Enter a valid Hush address"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.84">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_zcash_address"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent"
android:imeOptions="actionDone"
android:inputType="textNoSuggestions"
android:maxLength="255"
android:singleLine="true"
android:textColor="@color/text_light"
android:textColorHint="@color/text_light_dimmed" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Input: Memo -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/text_layout_memo"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/send_memo_hint"
android:theme="@style/Zcash.Overlay.TextInputLayout"
app:endIconMode="clear_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_layout_address"
app:layout_constraintWidth_percent="0.84"
tools:helperText="You have 23.23 ZEC available">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_zcash_memo"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent"
android:imeActionLabel="add memo"
android:imeOptions="actionDone"
android:inputType="textMultiLine|textNoSuggestions"
android:maxLength="512"
android:maxLines="3"
android:textColor="@color/text_light"
android:textColorHint="@color/text_light_dimmed"
tools:text="WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW" />
</com.google.android.material.textfield.TextInputLayout>
<!-- <ImageView-->
<!-- android:id="@+id/clear_memo"-->
<!-- android:layout_width="24dp"-->
<!-- android:layout_height="24dp"-->
<!-- android:layout_marginEnd="10dp"-->
<!-- android:layout_marginTop="6dp"-->
<!-- android:elevation="6dp"-->
<!-- android:src="@drawable/ic_close_black_24dp"-->
<!-- app:tint="@color/text_light"-->
<!-- app:layout_constraintEnd_toEndOf="@id/text_layout_memo"-->
<!-- app:layout_constraintTop_toTopOf="@id/text_layout_memo" />-->
<!-- Spacer to help position checkbox under the memo line -->
<Space
android:id="@+id/space_checkbox"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="18dp"
app:layout_constraintBottom_toBottomOf="@id/text_layout_memo"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/text_layout_memo" />
<CheckBox
android:id="@+id/check_include_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="0dp"
android:scaleX="0.84"
android:scaleY="0.84"
android:text="@string/send_checkbox_include_address"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:textColor="@color/text_light_dimmed"
android:textSize="14sp"
android:translationY="-10dp"
app:layout_constraintEnd_toEndOf="@id/text_layout_memo"
app:layout_constraintTop_toBottomOf="@id/space_checkbox" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_address_layout_check_include"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="text_layout_address, check_include_address" />
<TextView
android:id="@+id/text_no_z_address"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/send_no_z_address_message"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:textColor="@color/text_light"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/barrier_address_layout_check_include"
app:layout_constraintWidth_percent="0.84" />
<!-- -->
<!-- Clipboard items -->
<!-- -->
<View
android:id="@+id/background_clipboard"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@color/zcashWhite_12"
app:layout_constraintBottom_toBottomOf="@id/divider_clipboard"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/divider_clipboard" />
<TextView
android:id="@+id/divider_clipboard"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="1dp"
android:text="@string/send_history_clipboard"
android:textAllCaps="true"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.08"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/spacer_lower_content" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/container_clipboard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="16dp"
android:paddingTop="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/divider_clipboard">
<ImageView
android:id="@+id/image_shield"
android:layout_width="16dp"
android:layout_height="16dp"
android:contentDescription="@string/content_description_send_shield"
android:src="@drawable/ic_shielded"
app:tint="@color/colorPrimary"
app:layout_constraintBottom_toBottomOf="@id/clipboard_address_label"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.06"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/clipboard_address_label" />
<TextView
android:id="@+id/clipboard_address_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="@string/unknown"
android:textColor="@color/colorPrimary"
app:layout_constraintBottom_toTopOf="@id/clipboard_address"
app:layout_constraintStart_toEndOf="@id/image_shield"
app:layout_constraintTop_toTopOf="parent"
app:layout_goneMarginStart="0dp" />
<TextView
android:id="@+id/clipboard_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/text_light"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@id/image_shield"
app:layout_constraintTop_toBottomOf="@id/clipboard_address_label"
tools:text="zasdfaksfjaslfjaslfkjaslk;kfjaslkfjasld;kfjaslfjdasflja" />
<ImageView
android:id="@+id/image_clipboard_address_selected"
android:layout_width="22dp"
android:layout_height="22dp"
android:contentDescription="@string/content_description_send_selected"
android:src="@drawable/ic_check_shielded"
app:tint="@color/colorPrimary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias=".95"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- -->
<!-- Last Used items -->
<!-- -->
<View
android:id="@+id/background_last_used"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@color/zcashWhite_12"
app:layout_constraintBottom_toBottomOf="@id/divider_last_used"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/divider_last_used" />
<TextView
android:id="@+id/divider_last_used"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="1dp"
android:text="@string/send_history_last"
android:textAllCaps="true"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.08"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/container_clipboard" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/container_last_used"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/divider_last_used">
<ImageView
android:id="@+id/image_last_used_shield"
android:layout_width="16dp"
android:layout_height="16dp"
android:contentDescription="@string/content_description_send_shield"
android:src="@drawable/ic_shielded"
app:tint="@color/colorPrimary"
app:layout_constraintBottom_toBottomOf="@id/last_used_address_label"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.06"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/last_used_address_label" />
<TextView
android:id="@+id/last_used_address_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginTop="16dp"
android:text="@string/unknown"
android:textColor="@color/colorPrimary"
app:layout_constraintBottom_toTopOf="@id/last_used_address"
app:layout_constraintStart_toEndOf="@id/image_last_used_shield"
app:layout_constraintTop_toTopOf="parent"
app:layout_goneMarginStart="0dp" />
<TextView
android:id="@+id/last_used_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/text_light"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@id/image_last_used_shield"
app:layout_constraintTop_toBottomOf="@id/last_used_address_label"
tools:text="zasdfaksfjaslfjaslfkjaslk;kfjaslkfjasld;kfjaslfjdasflja" />
<ImageView
android:id="@+id/image_last_used_address_selected"
android:layout_width="22dp"
android:layout_height="22dp"
android:contentDescription="@string/content_description_send_selected"
android:src="@drawable/ic_check_shielded"
app:tint="@color/colorPrimary"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias=".95"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- <TextView-->
<!-- android:id="@+id/text_max"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_marginBottom="6dp"-->
<!-- android:fontFamily="@font/inconsolata"-->
<!-- android:padding="16dp"-->
<!-- android:text="MAX"-->
<!-- android:textStyle="bold"-->
<!-- app:layout_constraintBottom_toBottomOf="@id/text_layout_memo"-->
<!-- app:layout_constraintEnd_toEndOf="@id/text_layout_memo"-->
<!-- app:layout_constraintTop_toTopOf="@id/text_layout_memo" />-->
<!-- Scan QR code -->
<ImageView
android:id="@+id/image_scan_qr"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/content_description_send_scan_qr"
android:paddingBottom="24dp"
android:paddingEnd="1dp"
android:paddingStart="6dp"
android:paddingTop="10dp"
app:tint="@color/zcashWhite"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/text_layout_address"
app:layout_constraintEnd_toEndOf="@id/text_layout_address"
app:layout_constraintTop_toTopOf="@id/text_layout_address"
app:srcCompat="@drawable/ic_qrcode_24dp" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_clipboard"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="background_clipboard,divider_clipboard,container_clipboard"
tools:visibility="visible" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_last_used"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="background_last_used,divider_last_used,container_last_used"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

201
app/src/main/res/layout/fragment_send_address.xml

@ -0,0 +1,201 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/background_home">
<!-- Back Button -->
<ImageView
android:id="@+id/back_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:tint="@color/text_light"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.05"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.065"
app:srcCompat="@drawable/ic_arrow_back_black_24dp" />
<View
android:id="@+id/back_button_hit_area"
android:layout_width="56dp"
android:layout_height="56dp"
android:clickable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.01"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.045" />
<TextView
android:id="@+id/text_amount"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/TextAppearance.MaterialComponents.Headline6"
android:autoSizeTextType="uniform"
android:maxLines="1"
android:text="Sending"
android:textColor="@color/text_light"
app:layout_constraintBottom_toBottomOf="@id/back_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/back_button_hit_area"
app:layout_constraintTop_toTopOf="@id/back_button" />
<!-- Input: Address -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/text_layout_address"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:hint="To"
android:theme="@style/Zcash.Overlay.TextInputLayout"
app:endIconDrawable="@drawable/ic_qrcode_24dp"
app:endIconMode="custom"
app:helperText="Enter a valid Hush address"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_banner_message"
app:layout_constraintVertical_bias="0.08"
app:layout_constraintWidth_percent="0.84">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_zcash_address"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:imeOptions="actionDone"
android:inputType="textNoSuggestions"
android:singleLine="true"
android:maxLength="255"
android:textColor="@color/text_light"
android:textColorHint="@color/text_light_dimmed" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Input: Amount -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/text_layout_amount"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:hint="Amount"
android:theme="@style/Zcash.Overlay.TextInputLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_layout_address"
app:layout_constraintWidth_percent="0.84"
tools:helperText="You have 23.23 Hush available">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_zcash_amount"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:imeOptions="actionDone"
android:inputType="numberDecimal"
android:maxLength="20"
android:singleLine="true"
android:textColor="@color/text_light"
android:textColorHint="@color/text_light_dimmed" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/text_max"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="6dp"
android:fontFamily="@font/inconsolata"
android:padding="16dp"
android:text="MAX"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@id/text_layout_amount"
app:layout_constraintEnd_toEndOf="@id/text_layout_amount"
app:layout_constraintTop_toTopOf="@id/text_layout_amount" />
<TextView
android:id="@+id/text_address_error"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:autoSizeTextType="uniform"
android:fontFamily="@font/inconsolata"
android:maxLines="1"
android:textColor="@color/zcashRed"
android:textSize="14dp"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/text_layout_amount"
app:layout_constraintTop_toBottomOf="@+id/button_next"
app:layout_constraintVertical_bias="0.1"
tools:text="Please enter a larger amount of money also please enter a shorter sentence" />
<!-- Scan QR code -->
<ImageView
android:id="@+id/image_scan_qr"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="24dp"
android:paddingEnd="1dp"
android:paddingStart="6dp"
android:paddingTop="10dp"
app:tint="@color/zcashWhite"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/text_layout_address"
app:layout_constraintEnd_toEndOf="@id/text_layout_address"
app:layout_constraintTop_toTopOf="@id/text_layout_address"
app:srcCompat="@drawable/ic_qrcode_24dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Next"
android:textColor="@color/text_dark"
app:layout_constraintEnd_toEndOf="@+id/text_layout_address"
app:layout_constraintTop_toBottomOf="@+id/text_layout_amount" />
<!-- -->
<!-- Banner -->
<!-- -->
<TextView
android:id="@+id/text_banner_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@drawable/background_banner"
android:elevation="6dp"
android:paddingBottom="8dp"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:text="Address on clipboard!"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/text_light"
app:layout_constraintEnd_toEndOf="@+id/text_layout_address"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/text_layout_address"
app:layout_constraintTop_toBottomOf="@id/back_button_hit_area" />
<TextView
android:id="@+id/text_banner_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:elevation="6dp"
android:text="Paste"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/colorPrimary"
app:layout_constraintBaseline_toBaselineOf="@id/text_banner_message"
app:layout_constraintEnd_toEndOf="@id/text_banner_message" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_banner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="visible"
app:constraint_referenced_ids="text_banner_message, text_banner_action"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

155
app/src/main/res/layout/fragment_send_final.xml

@ -0,0 +1,155 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
android:background="@drawable/background_send_final">
<View
android:id="@+id/guide_keys"
android:layout_width="0dp"
android:layout_height="0dp"
android:alpha="0.3"
android:background="@android:color/transparent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.38196601125"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.58"
app:layout_constraintWidth_percent="0.7475728155" />
<!-- Back Button -->
<ImageView
android:id="@+id/back_button"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@string/content_description_send_final_back"
app:tint="@color/zcashBlack_87"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.088"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.065"
app:layout_constraintWidth_percent="0.08"
app:srcCompat="@drawable/ic_cancel"
tools:visibility="visible" />
<View
android:id="@+id/back_button_hit_area"
android:layout_width="68dp"
android:layout_height="68dp"
android:layout_marginStart="24dp"
android:clickable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.045"
android:focusable="true" />
<TextView
android:id="@+id/text_confirmation"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/TextAppearance.MaterialComponents.Headline5"
app:autoSizeTextType="uniform"
android:gravity="center"
android:maxLines="3"
android:paddingEnd="16dp"
android:paddingStart="16dp"
android:textColor="@color/text_dark"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.21"
tools:text="Send 12.345 ZEC to\nzs1g7sqw...mvyzgm?" />
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lottie_sending"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/button_primary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.075"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_confirmation"
app:layout_constraintVertical_bias="0.2"
app:layout_constraintWidth_percent="0.3"
app:lottie_autoPlay="true"
app:lottie_loop="true"
app:lottie_rawRes="@raw/lottie_sending" />
<TextView
android:id="@+id/error_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center"
tools:text="Oops! Failed to send due to insufficient funds!"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/text_dark"
app:layout_constraintBottom_toTopOf="@id/button_primary"
app:layout_constraintEnd_toEndOf="@id/button_primary"
app:layout_constraintStart_toStartOf="@id/button_primary"
app:layout_constraintTop_toBottomOf="@id/lottie_sending" />
<ScrollView
android:layout_width="0dp"
android:layout_height="0dp"
android:paddingTop="16dp"
app:layout_constraintBottom_toBottomOf="@+id/guide_keys"
app:layout_constraintEnd_toEndOf="@+id/error_message"
app:layout_constraintStart_toStartOf="@+id/error_message"
app:layout_constraintTop_toBottomOf="@+id/error_message">
<TextView
android:id="@+id/text_more_info"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:gravity="center"
android:textColor="@color/text_dark"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</ScrollView>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_primary"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/Zcash.Button.OutlinedButton"
android:padding="20dp"
android:text="@string/cancel"
android:textColor="@color/text_dark"
android:translationY="-6dp"
app:layout_constraintBottom_toTopOf="@id/button_more_info"
app:layout_constraintEnd_toEndOf="@id/guide_keys"
app:layout_constraintStart_toStartOf="@id/guide_keys"
app:layout_constraintTop_toBottomOf="@id/guide_keys"
app:layout_constraintVertical_bias="0.2"
app:layout_constraintVertical_chainStyle="packed"
app:strokeColor="@color/text_dark" />
<TextView
android:id="@+id/button_more_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:gravity="center"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/text_dark"
android:visibility="gone"
tools:text="More Info"
tools:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/button_primary"
app:layout_constraintStart_toStartOf="@+id/button_primary"
app:layout_constraintTop_toBottomOf="@id/button_primary" />
</androidx.constraintlayout.widget.ConstraintLayout>

235
app/src/main/res/layout/fragment_send_memo.xml

@ -0,0 +1,235 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/background_home">
<!-- Back Button -->
<ImageView
android:id="@+id/back_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:tint="@color/text_light"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.05"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.065"
app:srcCompat="@drawable/ic_arrow_back_black_24dp" />
<View
android:id="@+id/back_button_hit_area"
android:layout_width="56dp"
android:layout_height="56dp"
android:clickable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.01"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.045" />
<View
android:id="@+id/background_memo"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/background_banner"
android:elevation="6dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.24"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.185"
app:layout_constraintWidth_percent="0.8" />
<EditText
android:id="@+id/input_memo"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@android:color/transparent"
android:elevation="6dp"
android:gravity="top"
android:scrollbars="vertical"
android:scrollbarStyle="outsideOverlay"
tools:text="this\nis\nsome\ntext\nthat\nspans\nmany\nlines"
android:hint="Add a memo here"
android:imeActionLabel="add memo"
android:imeOptions="actionDone"
android:inputType="textMultiLine"
android:maxLength="512"
android:paddingBottom="8dp"
android:paddingEnd="32dp"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/text_light"
app:layout_constraintBottom_toTopOf="@id/text_included_address"
app:layout_constraintEnd_toEndOf="@id/background_memo"
app:layout_constraintStart_toStartOf="@id/background_memo"
app:layout_constraintTop_toTopOf="@id/background_memo" />
<ImageView
android:id="@+id/clear_memo"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="8dp"
android:elevation="6dp"
android:src="@drawable/ic_close_black_24dp"
app:tint="@color/text_light"
app:layout_constraintEnd_toEndOf="@id/background_memo"
app:layout_constraintTop_toTopOf="@id/background_memo" />
<TextView
android:id="@+id/text_included_address"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:elevation="6dp"
android:paddingBottom="8dp"
android:paddingEnd="16dp"
android:paddingStart="16dp"
android:textColor="@color/text_light_dimmed"
app:layout_constraintBottom_toBottomOf="@id/background_memo"
app:layout_constraintEnd_toEndOf="@id/background_memo"
app:layout_constraintStart_toStartOf="@id/background_memo"
tools:text="sent from z23lk4jjl2k3jl43kkj542l3kl4hj2l3k1j41l2kjk423lkj423lklhk2jrhiuhrh2j4hh2hkj23hkj4" />
<View
android:layout_width="0dp"
android:layout_height="1px"
android:layout_marginBottom="4dp"
android:background="@color/text_light_dimmed"
android:elevation="6dp"
app:layout_constraintEnd_toEndOf="@id/text_included_address"
app:layout_constraintStart_toStartOf="@id/text_included_address"
app:layout_constraintTop_toTopOf="@id/text_included_address" />
<CheckBox
android:id="@+id/check_include_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="0dp"
android:layout_marginTop="16dp"
android:padding="0dp"
android:text="Include your sending address in memo"
app:layout_constraintStart_toStartOf="@+id/background_memo"
app:layout_constraintTop_toBottomOf="@+id/background_memo" />
<TextView
android:id="@+id/text_info_shielded"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
android:text="Your transaction is shielded and your address is not available to recipient."
app:layout_constraintEnd_toEndOf="@id/background_memo"
app:layout_constraintStart_toStartOf="@id/background_memo"
app:layout_constraintTop_toBottomOf="@id/check_include_address" />
<ImageView
android:id="@+id/sad_icon"
android:layout_width="36dp"
android:layout_height="36dp"
android:elevation="6dp"
app:layout_constraintBottom_toTopOf="@id/sad_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_info_24dp"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/sad_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:drawableTint="@color/text_light_dimmed"
android:gravity="center"
android:paddingEnd="32dp"
android:paddingStart="32dp"
android:text="Sending to a transparent address will let everyone see the amount and the recipient."
android:textColor="@color/text_light"
android:textSize="18dp"
app:layout_constraintBottom_toTopOf="@id/sad_description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/sad_icon" />
<TextView
android:id="@+id/sad_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:drawablePadding="16dp"
android:drawableTint="@color/text_light_dimmed"
android:gravity="center"
android:paddingStart="32dp"
android:paddingEnd="32dp"
android:text="Your privacy is protected, if you use your wallet to store your funds."
android:textColor="@color/text_light"
android:textSize="18dp"
app:layout_constraintBottom_toTopOf="@id/sad_checkbox"
app:layout_constraintEnd_toEndOf="@id/sad_title"
app:layout_constraintHeight_percent="0.2"
app:layout_constraintStart_toStartOf="@id/sad_title"
app:layout_constraintTop_toBottomOf="@id/sad_title"
app:layout_constraintVertical_bias="0.5263" />
<CheckBox
android:id="@+id/sad_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="Don't show this again"
app:layout_constraintBottom_toTopOf="@id/button_next"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/sad_description" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_next"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:padding="12dp"
android:textColor="@color/text_dark"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.8"
app:layout_constraintWidth_percent="0.68"
tools:text="Add Memo" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_skip"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/Zcash.Button.OutlinedButton"
android:padding="12dp"
android:textColor="@color/text_light"
app:layout_constraintEnd_toEndOf="@id/button_next"
app:layout_constraintStart_toStartOf="@id/button_next"
app:layout_constraintTop_toBottomOf="@id/button_next"
tools:text="Omit memo" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_transparent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:visibility="gone"
app:constraint_referenced_ids="sad_description, sad_icon, sad_title, sad_checkbox" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_shielded"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="clear_memo, background_memo, input_memo, check_include_address, text_info_shielded"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

162
app/src/main/res/layout/fragment_settings.xml

@ -0,0 +1,162 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/background_home">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_hit_area_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.04" />
<View
android:id="@+id/hit_area_exit"
android:layout_width="68dp"
android:layout_height="68dp"
android:layout_marginStart="24dp"
android:alpha="0.3"
android:background="@android:color/transparent"
android:elevation="6dp"
app:layout_constraintStart_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/guideline_hit_area_top" />
<ImageView
android:id="@+id/icon_exit"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@string/content_description_settings_back"
android:elevation="6dp"
app:tint="@color/text_light_dimmed"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.088"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.065"
app:layout_constraintWidth_percent="0.08"
app:srcCompat="@drawable/ic_cancel" />
<TextView
android:id="@+id/text_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/TextAppearance.MaterialComponents.Headline6"
android:maxLines="1"
android:text="@string/settings_change_lightwalletd_server"
android:textColor="@color/text_light"
app:autoSizeTextType="uniform"
app:layout_constraintBottom_toBottomOf="@id/icon_exit"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/hit_area_exit"
app:layout_constraintTop_toTopOf="@id/icon_exit" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/text_input_layout_host"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:hint="@string/settings_server_address"
app:errorEnabled="true"
app:helperText="@string/settings_host_helper_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_title"
app:layout_constraintWidth_percent="0.84">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_host"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:imeOptions="actionNext"
android:maxLength="253"
android:singleLine="true"
android:textColor="@color/text_light"
android:textColorHint="@color/text_light_dimmed" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/text_input_layout_port"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="18dp"
android:hint="@string/settings_server_port"
app:errorEnabled="true"
app:helperText="@string/settings_port_helper_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_input_layout_host"
app:layout_constraintWidth_percent="0.84">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_port"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:inputType="number"
android:maxLength="5"
android:textColor="@color/text_light"
android:textColorHint="@color/text_light_dimmed" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_reset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
style="@style/Zcash.Button.OutlinedButton"
android:text="@string/settings_reset"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/selector_secondary_button_activatable"
app:layout_constraintEnd_toStartOf="@id/button_update"
app:layout_constraintTop_toTopOf="@id/button_update"
app:strokeColor="@color/selector_secondary_button_activatable" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_update"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
style="@style/Zcash.Button"
android:backgroundTint="@color/colorPrimary"
android:text="@string/settings_update"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintEnd_toEndOf="@id/text_input_layout_host"
app:layout_constraintTop_toBottomOf="@+id/text_input_layout_port" />
<View
android:id="@+id/loading_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/zcashWhite_24"
android:clickable="true"
android:elevation="8dp"
android:focusable="true"
android:focusableInTouchMode="true" />
<ProgressBar
android:id="@+id/loading_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
app:layout_constraintBottom_toBottomOf="@id/button_reset"
app:layout_constraintStart_toStartOf="@id/icon_exit"
app:layout_constraintTop_toTopOf="@id/button_reset" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="loading_progress,loading_view" />
</androidx.constraintlayout.widget.ConstraintLayout>

108
app/src/main/res/layout/fragment_tab_layout.xml

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_hit_area_head"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.05" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_hit_area_topp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.04" />
<View
android:id="@+id/hit_area_exit"
android:layout_width="68dp"
android:layout_height="68dp"
android:layout_marginStart="24dp"
android:alpha="0.3"
android:background="@android:color/transparent"
android:elevation="6dp"
app:layout_constraintStart_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/guideline_hit_area_topp" />
<ImageView
android:id="@+id/icon_exit"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@string/content_description_settings_back"
android:elevation="6dp"
app:tint="@color/text_light_dimmed"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.088"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.065"
app:layout_constraintWidth_percent="0.08"
app:srcCompat="@drawable/ic_cancel" />
<TextView
android:id="@+id/text_title"
style="@style/TextAppearance.MaterialComponents.Headline6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
tools:text="Receive HUSH"
app:layout_constraintBottom_toTopOf="@+id/guideline_hit_area_top"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/guideline_hit_area_head" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_hit_area_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.12" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/icon_exit"/>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_below="@id/tabLayout"
android:layout_centerInParent="true"
app:layout_constraintBottom_toTopOf="@id/button_share_address"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tabLayout" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_share_address"
android:layout_width="0dp"
android:layout_height="0dp"
style="@style/Zcash.Button.OutlinedButton"
android:gravity="center"
android:text="Share Address"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/selector_button_text_light"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.088"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.95"
app:layout_constraintWidth_percent="0.7475728155" />
</androidx.constraintlayout.widget.ConstraintLayout>

179
app/src/main/res/layout/fragment_tab_receive_shielded.xml

@ -0,0 +1,179 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/text_title"
style="@style/TextAppearance.MaterialComponents.Body1"
android:textSize="18dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Your Shielded Address"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/background_qr"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="20dp"
android:contentDescription="@string/content_description_receive_qr_background"
android:scaleType="fitXY"
android:src="@drawable/ic_background_qr"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintWidth_percent="0.64"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_title"
app:layout_constraintVertical_bias="0.3" />
<!-- QR code placeholder -->
<ImageView
android:id="@+id/receive_qr_code"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@string/content_description_receive_qr_code"
android:onClick="copyAddress"
android:scaleType="fitCenter"
app:layout_constraintBottom_toBottomOf="@id/background_qr"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="@id/background_qr"
app:layout_constraintStart_toStartOf="@id/background_qr"
app:layout_constraintTop_toTopOf="@id/background_qr"
app:layout_constraintWidth_percent="0.53"
tools:background="@color/zcashWhite"
tools:visibility="visible" />
<ImageView
android:id="@+id/icon_qr_logo"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@string/content_description_receive_qr_logo"
android:onClick="copyAddress"
android:scaleType="fitCenter"
android:src="@drawable/ic_shield_address"
app:layout_constraintBottom_toBottomOf="@id/receive_qr_code"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="@id/receive_qr_code"
app:layout_constraintStart_toStartOf="@id/receive_qr_code"
app:layout_constraintTop_toTopOf="@id/receive_qr_code"
app:layout_constraintWidth_percent="0.18" />
<!-- Someday, there will be an advanced VirtualLayout that helps us do this without nesting but for now, this seems to be the only clean way to center all the fields -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/receive_address_parts"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:foregroundGravity="center"
app:layout_constraintEnd_toEndOf="@+id/receive_qr_code"
app:layout_constraintStart_toStartOf="@+id/receive_qr_code"
app:layout_constraintTop_toBottomOf="@+id/background_qr"
app:layout_constraintVertical_bias="0.0">
<TextView
android:id="@+id/text_address_part_1"
style="@style/Zcash.TextAppearance.AddressPart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_3"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0"
app:layout_constraintVertical_chainStyle="packed"
tools:text=" ztestsaplin" />
<TextView
android:id="@+id/text_address_part_3"
style="@style/Zcash.TextAppearance.AddressPart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_5"
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
app:layout_constraintTop_toBottomOf="@id/text_address_part_1"
tools:text=" jceuu9s2p6t" />
<TextView
android:id="@+id/text_address_part_5"
style="@style/Zcash.TextAppearance.AddressPart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_7"
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
app:layout_constraintTop_toBottomOf="@id/text_address_part_3"
tools:text=" 7u7uarqls7d" />
<TextView
android:id="@+id/text_address_part_7"
style="@style/Zcash.TextAppearance.AddressPart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
app:layout_constraintTop_toBottomOf="@id/text_address_part_5"
tools:text=" rzq85xggu56" />
<TextView
android:id="@+id/text_address_part_2"
style="@style/Zcash.TextAppearance.AddressPart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_4"
app:layout_constraintStart_toEndOf="@id/barrier_left_address_column"
app:layout_constraintTop_toTopOf="@id/text_address_part_1"
tools:text=" g1mwjzlg62j" />
<TextView
android:id="@+id/text_address_part_4"
style="@style/Zcash.TextAppearance.AddressPart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_6"
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
app:layout_constraintTop_toBottomOf="@id/text_address_part_2"
tools:text=" wns6qxwec6v" />
<TextView
android:id="@+id/text_address_part_6"
style="@style/Zcash.TextAppearance.AddressPart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_8"
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
app:layout_constraintTop_toBottomOf="@id/text_address_part_4"
tools:text=" gtg3tpgqxjd" />
<TextView
android:id="@+id/text_address_part_8"
style="@style/Zcash.TextAppearance.AddressPart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
app:layout_constraintTop_toBottomOf="@id/text_address_part_6"
tools:text=" k904xderng6" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_left_address_column"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="150dp"
android:layout_marginRight="150dp"
android:padding="150dp"
app:barrierDirection="end"
app:constraint_referenced_ids="text_address_part_1,text_address_part_3,text_address_part_5,text_address_part_7" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

554
app/src/main/res/layout/fragment_transaction.xml

@ -0,0 +1,554 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/background_home">
<!-- -->
<!-- Guidelines -->
<!-- -->
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_content_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.1812" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_keyline_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.054" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_keyline_end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.946" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_content_bottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.8447" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_subway_line"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.16" />
<Space
android:id="@+id/space_spots"
android:layout_width="12dp"
android:layout_height="1dp"
app:layout_constraintEnd_toEndOf="@id/subway_line"
app:layout_constraintStart_toStartOf="@id/subway_line"
app:layout_constraintTop_toTopOf="@id/subway_line" />
<Space
android:id="@+id/space_spots_memo"
android:layout_width="18dp"
android:layout_height="1dp"
app:layout_constraintEnd_toEndOf="@id/subway_line"
app:layout_constraintStart_toStartOf="@id/subway_line"
app:layout_constraintTop_toTopOf="@id/subway_line" />
<!-- -->
<!-- Header -->
<!-- -->
<!-- Close Button -->
<ImageView
android:id="@+id/close_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/content_description_transaction_details_back"
app:tint="@color/zcashWhite_40"
app:layout_constraintBottom_toTopOf="@id/text_timestamp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.05"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_cancel" />
<View
android:id="@+id/back_button_hit_area"
android:layout_width="56dp"
android:layout_height="56dp"
android:clickable="true"
android:focusable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.01"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.045" />
<TextView
android:id="@+id/text_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:text="@string/transaction_title"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
android:textColor="@color/text_light"
app:layout_constraintBottom_toBottomOf="@id/close_button"
app:layout_constraintStart_toEndOf="@id/close_button"
app:layout_constraintTop_toTopOf="@id/close_button" />
<TextView
android:id="@+id/text_timestamp"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:gravity="bottom"
android:maxLines="1"
android:textColor="@color/tx_text_light_dimmed"
android:textSize="18sp"
app:autoSizeMaxTextSize="18sp"
app:autoSizeMinTextSize="6dp"
app:autoSizeTextType="uniform"
app:layout_constraintBaseline_toBaselineOf="@id/text_block_height"
app:layout_constraintEnd_toStartOf="@id/text_block_height_prefix"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="@id/guideline_keyline_start"
tools:text="2020-04-14 5:12am and this is way long" />
<TextView
android:id="@+id/text_block_height_prefix"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/transaction_block_height_prefix"
android:textColor="@color/tx_text_light_dimmed"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="@id/close_button"
app:layout_constraintBottom_toTopOf="@id/padding_bottom"
app:layout_constraintEnd_toStartOf="@id/text_block_height" />
<TextView
android:id="@+id/text_block_height"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:textColor="@color/tx_text_light_dimmed_less"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="@id/close_button"
app:layout_constraintBottom_toTopOf="@id/padding_bottom"
app:layout_constraintEnd_toEndOf="@id/guideline_keyline_end"
tools:text="796,798" />
<Space
android:id="@+id/padding_bottom"
android:layout_width="100dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/top_box_border"
app:layout_constraintEnd_toEndOf="@id/guideline_keyline_end"
app:layout_constraintHeight_percent="0.021798" />
<!-- -->
<!-- Content: Top -->
<!-- -->
<!-- %height: 75/734 -->
<View
android:id="@+id/top_box_background"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="#25272B"
app:layout_constraintEnd_toEndOf="@id/guideline_keyline_end"
app:layout_constraintHeight_percent="0.1022"
app:layout_constraintStart_toStartOf="@id/guideline_keyline_start"
app:layout_constraintTop_toTopOf="@id/guideline_content_top" />
<View
android:id="@+id/top_box_border"
android:layout_width="0dp"
android:layout_height="2dp"
android:background="@color/tx_primary"
app:layout_constraintEnd_toEndOf="@id/guideline_keyline_end"
app:layout_constraintStart_toStartOf="@id/guideline_keyline_start"
app:layout_constraintTop_toTopOf="@id/top_box_background" />
<!-- Icon: BG -->
<!-- %height: 42/734 -->
<View
android:id="@+id/top_box_icon_background"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/background_circle_solid"
android:backgroundTint="@color/tx_circle_icon_bg"
app:layout_constraintBottom_toBottomOf="@id/top_box_background"
app:layout_constraintDimensionRatio="w,1:1"
app:layout_constraintEnd_toEndOf="@id/top_box_background"
app:layout_constraintHeight_percent="0.0572"
app:layout_constraintHorizontal_bias="0.9556"
app:layout_constraintStart_toStartOf="@id/top_box_background"
app:layout_constraintTop_toTopOf="@id/top_box_background" />
<ImageView
android:id="@+id/top_box_icon"
android:layout_width="0dp"
android:layout_height="0dp"
android:src="@drawable/ic_arrow_back_black_24dp"
app:tint="@color/text_light"
app:layout_constraintBottom_toBottomOf="@id/top_box_icon_background"
app:layout_constraintDimensionRatio="w,1:1"
app:layout_constraintEnd_toEndOf="@id/top_box_icon_background"
app:layout_constraintHeight_percent="0.0408"
app:layout_constraintStart_toStartOf="@id/top_box_icon_background"
app:layout_constraintTop_toTopOf="@id/top_box_icon_background"
android:contentDescription="@string/content_description_transaction_details_direction" />
<TextView
android:id="@+id/top_box_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textColor="@color/colorAccent"
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@id/top_box_value"
app:layout_constraintEnd_toEndOf="@id/top_box_background"
app:layout_constraintHorizontal_bias="0.0444"
app:layout_constraintStart_toStartOf="@id/top_box_background"
app:layout_constraintTop_toBottomOf="@id/top_box_border"
app:layout_constraintVertical_chainStyle="packed"
tools:text="You Sent" />
<TextView
android:id="@+id/top_box_value"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginTop="4dp"
android:maxLines="1"
android:textAppearance="@style/Zcash.TextAppearance.Zec"
android:textColor="@color/text_light"
android:textSize="36sp"
app:autoSizeMaxTextSize="36sp"
app:autoSizeMinTextSize="6sp"
app:autoSizeTextType="uniform"
app:layout_constraintBottom_toBottomOf="@id/top_box_background"
app:layout_constraintEnd_toStartOf="@id/top_box_icon_background"
app:layout_constraintStart_toStartOf="@id/top_box_label"
app:layout_constraintTop_toBottomOf="@id/top_box_label"
tools:text="4.32222222" />
<!-- -->
<!-- Content: Subway -->
<!-- -->
<View
android:id="@+id/subway_line"
android:layout_width="2dp"
android:layout_height="0dp"
android:background="@color/tx_primary"
app:layout_constraintBottom_toTopOf="@id/bottom_box_background"
app:layout_constraintStart_toStartOf="@id/guideline_subway_line"
app:layout_constraintTop_toBottomOf="@id/top_box_background" />
<View
android:id="@+id/subway_spot_fee"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="4dp"
android:background="@drawable/background_circle_solid"
android:backgroundTint="@color/tx_primary"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/subway_label_fee"
app:layout_constraintDimensionRatio="h,1:1"
app:layout_constraintEnd_toEndOf="@id/space_spots"
app:layout_constraintStart_toStartOf="@id/space_spots"
app:layout_constraintTop_toTopOf="@id/subway_label_fee" />
<View
android:id="@+id/subway_spot_source"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="4dp"
android:background="@drawable/background_circle_solid"
android:backgroundTint="@color/tx_primary"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/subway_label_source"
app:layout_constraintDimensionRatio="h,1:1"
app:layout_constraintEnd_toEndOf="@id/space_spots"
app:layout_constraintStart_toStartOf="@id/space_spots"
app:layout_constraintTop_toTopOf="@id/subway_label_source" />
<ImageView
android:id="@+id/subway_spot_memo_content"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="4dp"
android:src="@drawable/ic_expand_memo_enabled"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/subway_label_memo"
app:layout_constraintDimensionRatio="h,1:1"
app:layout_constraintEnd_toEndOf="@id/space_spots_memo"
app:layout_constraintStart_toStartOf="@id/space_spots_memo"
app:layout_constraintTop_toTopOf="@id/subway_label_memo"
tools:visibility="visible"
android:contentDescription="@string/content_description_transaction_details_memo" />
<View
android:id="@+id/subway_spot_address"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="4dp"
android:background="@drawable/background_circle_solid"
android:backgroundTint="@color/tx_primary"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/subway_label_address"
app:layout_constraintDimensionRatio="h,1:1"
app:layout_constraintEnd_toEndOf="@id/space_spots"
app:layout_constraintStart_toStartOf="@id/space_spots"
app:layout_constraintTop_toTopOf="@id/subway_label_address" />
<View
android:id="@+id/subway_spot_confirmations"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="4dp"
android:background="@drawable/background_circle_solid"
android:backgroundTint="@color/tx_primary"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/subway_label_confirmations"
app:layout_constraintDimensionRatio="h,1:1"
app:layout_constraintEnd_toEndOf="@id/space_spots"
app:layout_constraintStart_toStartOf="@id/space_spots"
app:layout_constraintTop_toTopOf="@id/subway_label_confirmations" />
<TextView
android:id="@+id/subway_label_fee"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="24dp"
android:textColor="@color/tx_text_light_dimmed"
android:textSize="18sp"
android:visibility="gone"
tools:visibility="visible"
app:layout_constraintStart_toStartOf="@id/subway_line"
app:layout_constraintTop_toTopOf="@id/subway_line"
tools:text="+0.00001 network fee" />
<TextView
android:id="@+id/subway_label_source"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="24dp"
android:textColor="@color/tx_text_light_dimmed"
android:textSize="18sp"
android:visibility="gone"
app:layout_constraintStart_toStartOf="@id/subway_line"
app:layout_constraintTop_toBottomOf="@id/subway_label_fee"
tools:text="from your shielded wallet" />
<TextView
android:id="@+id/spacer_memo_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="24dp"
android:paddingEnd="8dp"
android:paddingStart="8dp"
android:text="@string/transaction_with_memo"
android:textSize="18sp"
android:visibility="invisible"
app:layout_constraintStart_toStartOf="@id/subway_line"
app:layout_constraintTop_toBottomOf="@id/subway_label_source"
tools:visibility="visible" />
<TextView
android:id="@+id/subway_label_memo"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="24dp"
android:background="@null"
android:clickable="false"
android:fadeScrollbars="false"
android:maxLines="3"
android:scrollbars="vertical"
android:textColor="@color/tx_text_light_dimmed"
android:textSize="18sp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="@id/guideline_keyline_end"
app:layout_constraintStart_toStartOf="@id/subway_line"
app:layout_constraintTop_toBottomOf="@id/subway_label_source"
tools:text="this is a memo with 512 characters Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Intege"
tools:visibility="visible" />
<ImageView
android:id="@+id/icon_memo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_memo"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/spacer_memo_icon"
app:layout_constraintStart_toEndOf="@id/spacer_memo_icon"
app:layout_constraintTop_toTopOf="@id/spacer_memo_icon"
android:contentDescription="@string/content_description_transaction_details_memo_icon" />
<View
android:id="@+id/hit_area_memo_subway"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/subway_label_memo"
app:layout_constraintEnd_toStartOf="@id/subway_label_memo"
app:layout_constraintStart_toStartOf="@id/guideline_keyline_start"
app:layout_constraintTop_toBottomOf="@id/subway_label_source"
tools:alpha="0.3"
tools:background="@color/zcashRed"
tools:visibility="visible" />
<View
android:id="@+id/hit_area_memo_icon"
android:layout_width="56dp"
android:layout_height="56dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/subway_label_memo"
app:layout_constraintEnd_toEndOf="@id/icon_memo"
app:layout_constraintStart_toEndOf="@id/spacer_memo_icon"
app:layout_constraintTop_toTopOf="@id/hit_area_memo_subway"
tools:alpha="0.3"
tools:background="@color/zcashRed"
tools:visibility="visible" />
<TextView
android:id="@+id/subway_label_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="24dp"
android:textColor="@color/text_light"
android:textSize="18sp"
android:visibility="gone"
app:layout_constraintStart_toStartOf="@id/subway_line"
app:layout_constraintTop_toBottomOf="@id/subway_label_memo"
tools:text="to zs34jgefi30f...10ijgek234e" />
<TextView
android:id="@+id/subway_label_confirmations"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:layout_marginStart="16dp"
android:textColor="@color/tx_text_light_dimmed"
android:textSize="18sp"
app:layout_constraintBottom_toTopOf="@id/bottom_box_background"
app:layout_constraintStart_toStartOf="@id/subway_line"
tools:text="confirmed" />
<!-- -->
<!-- Content: Bottom -->
<!-- -->
<!-- %height: 75/734 -->
<View
android:id="@+id/bottom_box_background"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="#25272B"
android:transitionName="test_transition"
app:layout_constraintBottom_toBottomOf="@id/guideline_content_bottom"
app:layout_constraintEnd_toEndOf="@id/guideline_keyline_end"
app:layout_constraintHeight_percent="0.1022"
app:layout_constraintStart_toStartOf="@id/guideline_keyline_start" />
<View
android:id="@+id/bottom_box_border"
android:layout_width="0dp"
android:layout_height="2dp"
android:background="@color/colorAccent"
app:layout_constraintEnd_toEndOf="@id/guideline_keyline_end"
app:layout_constraintStart_toStartOf="@id/guideline_keyline_start"
app:layout_constraintTop_toTopOf="@id/bottom_box_background" />
<TextView
android:id="@+id/bottom_box_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="@string/transaction_details_total"
android:textColor="@color/colorAccent"
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@id/bottom_box_value"
app:layout_constraintEnd_toEndOf="@id/bottom_box_background"
app:layout_constraintHorizontal_bias="0.0444"
app:layout_constraintStart_toStartOf="@id/bottom_box_background"
app:layout_constraintTop_toBottomOf="@id/bottom_box_border"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/bottom_box_value"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="4dp"
android:maxLines="1"
tools:text="4.32222222"
android:textAppearance="@style/Zcash.TextAppearance.Zec"
android:textColor="@color/text_light"
android:textSize="36sp"
app:autoSizeMaxTextSize="36sp"
app:autoSizeMinTextSize="6sp"
app:autoSizeTextType="uniform"
app:layout_constraintBottom_toBottomOf="@id/bottom_box_background"
app:layout_constraintEnd_toEndOf="@id/bottom_box_background"
app:layout_constraintStart_toStartOf="@id/bottom_box_label"
app:layout_constraintTop_toBottomOf="@id/bottom_box_label" />
<!-- -->
<!-- Footer -->
<!-- -->
<com.google.android.material.button.MaterialButton
android:id="@+id/button_explore"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/Zcash.Button.OutlinedButton"
android:gravity="center"
android:padding="12dp"
android:text="@string/transaction_details_button_explore"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/tx_text_light_dimmed_less"
app:icon="@drawable/ic_baseline_launch_24"
app:iconGravity="textEnd"
app:iconTint="@color/tx_text_light_dimmed_less"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/guideline_keyline_end"
app:layout_constraintStart_toStartOf="@id/guideline_keyline_start"
app:layout_constraintTop_toTopOf="@id/guideline_content_bottom"
app:layout_constraintVertical_bias="0.24"
app:strokeColor="@color/tx_text_light_dimmed_less" />
<!-- -->
<!-- Groups -->
<!-- -->
<androidx.constraintlayout.widget.Group
android:id="@+id/group_memo_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="icon_memo, hit_area_memo_icon" />
</androidx.constraintlayout.widget.ConstraintLayout>

19
app/src/main/res/layout/fragment_wallet_import.xml

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/background_home"
xmlns:tools="http://schemas.android.com/tools">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/text_light"
android:text="send memo"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

19
app/src/main/res/layout/fragment_wallet_new.xml

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/background_home"
xmlns:tools="http://schemas.android.com/tools">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/text_light"
android:text="send memo"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

141
app/src/main/res/layout/item_transaction.xml

@ -0,0 +1,141 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container_transaction"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@color/background_banner"
android:elevation="1dp"
tools:ignore="RtlSymmetry">
<!-- -->
<!-- Borders -->
<!-- -->
<View
android:id="@+id/divider_top"
android:layout_width="match_parent"
android:layout_height="1px"
android:background="@color/zcashYellow"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/indicator"
android:layout_width="4dp"
android:layout_height="60dp"
android:background="@color/tx_primary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/transaction_arrow_background"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
android:background="@drawable/background_circle_solid"
android:backgroundTint="@color/tx_circle_icon_bg"
app:layout_constraintBottom_toBottomOf="@id/layout_transaction_text"
app:layout_constraintDimensionRatio="w,1:1"
app:layout_constraintStart_toEndOf="@id/indicator"
app:layout_constraintTop_toTopOf="@id/layout_transaction_text" />
<ImageView
android:id="@+id/image_transaction_arrow"
android:layout_width="0dp"
android:layout_height="20dp"
android:contentDescription="@string/content_description_transaction_details_direction"
android:src="@drawable/ic_arrow_back_black_24dp"
app:layout_constraintBottom_toBottomOf="@id/transaction_arrow_background"
app:layout_constraintDimensionRatio="w,1:1"
app:layout_constraintEnd_toEndOf="@id/transaction_arrow_background"
app:layout_constraintStart_toStartOf="@id/transaction_arrow_background"
app:layout_constraintTop_toTopOf="@id/transaction_arrow_background"
app:tint="@color/text_light" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/layout_transaction_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:paddingBottom="8dp"
app:layout_constraintBottom_toBottomOf="@id/indicator"
app:layout_constraintEnd_toStartOf="@id/text_transaction_amount"
app:layout_constraintStart_toEndOf="@id/transaction_arrow_background"
app:layout_constraintTop_toTopOf="@id/indicator">
<TextView
android:id="@+id/text_transaction_top"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:gravity="center_vertical"
android:maxLines="1"
android:textColor="@color/text_light"
android:textSize="16sp"
app:autoSizeTextType="uniform"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
app:layout_goneMarginStart="16dp"
tools:text="This is a very long sentence and it better not messup the UI" />
<TextView
android:id="@+id/text_transaction_bottom"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:drawablePadding="8dp"
android:ellipsize="end"
android:gravity="top"
android:maxLines="1"
android:paddingEnd="2dp"
android:textColor="@color/text_light_dimmed"
android:textSize="14sp"
app:autoSizeTextType="uniform"
app:layout_constraintEnd_toStartOf="@id/image_memo"
app:layout_constraintStart_toStartOf="@+id/text_transaction_top"
app:layout_constraintTop_toBottomOf="@id/text_transaction_top"
tools:text="This is a very long sentence and it better not messup the UI" />
<ImageView
android:id="@+id/image_memo"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:paddingBottom="3dp"
android:paddingTop="2dp"
android:src="@drawable/ic_memo"
app:layout_constraintBottom_toBottomOf="@id/text_transaction_bottom"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/text_transaction_bottom" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/text_transaction_amount"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:gravity="right"
android:maxLines="1"
android:paddingStart="8dp"
android:textColor="@color/colorPrimary"
android:textSize="16sp"
app:autoSizeMaxTextSize="16sp"
app:autoSizeMinTextSize="8sp"
app:autoSizeTextType="uniform"
app:layout_constraintBottom_toBottomOf="@id/indicator"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/indicator"
app:layout_constraintWidth_percent="0.23"
tools:text="+ 434888.12345678" />
</androidx.constraintlayout.widget.ConstraintLayout>

67
app/src/main/res/layout/main_activity.xml

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main_activity_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MainActivity">
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/mobile_navigation" />
<!-- -->
<!-- Loading Screen -->
<!-- -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/container_loading"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/background_home">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_buttons"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.667" />
<ImageView
android:id="@+id/icon_logo"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="W,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.33333"
app:layout_constraintWidth_percent="0.4053398058"
app:srcCompat="@drawable/ic_logo_landing" />
<TextView
android:id="@+id/text_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/TextAppearance.MaterialComponents.Headline6"
android:gravity="center"
android:paddingEnd="32dp"
android:paddingStart="32dp"
android:text="\u23F3 Loading..."
android:textColor="@color/zcashWhite"
app:layout_constraintBottom_toTopOf="@id/guideline_buttons"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/icon_logo" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

5
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_adaptive_back"/>
<foreground android:drawable="@mipmap/ic_launcher_adaptive_fore"/>
</adaptive-icon>

5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_round_adaptive_back"/>
<foreground android:drawable="@mipmap/ic_launcher_round_adaptive_fore"/>
</adaptive-icon>

BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_back.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_fore.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round_adaptive_back.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round_adaptive_fore.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_back.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_fore.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round_adaptive_back.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round_adaptive_fore.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_back.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round_adaptive_back.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round_adaptive_fore.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_back.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round_adaptive_back.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round_adaptive_fore.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_back.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round_adaptive_back.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round_adaptive_fore.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

310
app/src/main/res/navigation/mobile_navigation.xml

@ -0,0 +1,310 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/mobile_navigation"
app:startDestination="@+id/nav_home">
<fragment
android:id="@+id/nav_home"
android:name="cash.z.ecc.android.ui.home.HomeFragment"
tools:layout="@layout/fragment_home">
<action
android:id="@+id/action_nav_home_to_nav_receive"
app:destination="@id/nav_receive" />
<action
android:id="@+id/action_nav_home_to_nav_history"
app:destination="@id/nav_history" />
<action
android:id="@+id/action_nav_home_to_create_wallet"
app:destination="@id/nav_landing" />
<action
android:id="@+id/action_nav_home_to_autoshielding_info"
app:destination="@id/nav_autoshielding_info" />
<action
android:id="@+id/action_nav_home_to_send"
app:destination="@id/nav_send"
app:exitAnim="@anim/anim_exit_to_left"
app:enterAnim="@anim/anim_enter_from_right"/>
<action
android:id="@+id/action_nav_home_to_nav_scan"
app:destination="@id/nav_scan" />
<action
android:id="@+id/action_nav_home_to_nav_profile"
app:destination="@id/nav_profile" />
<action
android:id="@+id/action_nav_home_to_nav_balance_detail"
app:destination="@id/nav_balance_detail" />
<action
android:id="@+id/action_nav_home_to_nav_feedback"
app:destination="@id/nav_feedback" />
<action
android:id="@+id/action_nav_home_to_nav_funds_available"
app:destination="@id/nav_funds_available" />
<action
android:id="@+id/action_nav_home_to_nav_shield_final"
app:destination="@id/nav_shield_final" />
</fragment>
<fragment
android:id="@+id/nav_balance_detail"
android:name="cash.z.ecc.android.ui.home.BalanceDetailFragment"
tools:layout="@layout/fragment_balance_detail">
<action
android:id="@+id/action_nav_balance_detail_to_shield_final"
app:destination="@id/nav_shield_final" />
</fragment>
<fragment
android:id="@+id/nav_receive"
android:name="cash.z.ecc.android.ui.tab_layout.TabLayoutFragment"
tools:layout="@layout/fragment_tab_layout" >
</fragment>
<fragment
android:id="@+id/nav_scan"
android:name="cash.z.ecc.android.ui.scan.ScanFragment"
tools:layout="@layout/fragment_scan">
<action
android:id="@+id/action_nav_scan_to_nav_send"
app:destination="@id/nav_send"
app:popUpTo="@id/nav_scan"
app:popUpToInclusive="true"/>
</fragment>
<fragment
android:id="@+id/nav_history"
android:name="cash.z.ecc.android.ui.history.HistoryFragment"
tools:layout="@layout/fragment_history">
<action
android:id="@+id/action_nav_history_to_nav_transaction"
app:destination="@id/nav_transaction" />
</fragment>
<fragment
android:id="@+id/nav_transaction"
android:name="cash.z.ecc.android.ui.history.TransactionFragment"
tools:layout="@layout/fragment_transaction" />
<fragment
android:id="@+id/nav_profile"
android:name="cash.z.ecc.android.ui.profile.ProfileFragment"
tools:layout="@layout/fragment_profile" >
<action
android:id="@+id/action_nav_profile_to_nav_backup"
app:destination="@id/nav_backup" />
<action
android:id="@+id/action_nav_profile_to_nav_feedback"
app:destination="@id/nav_feedback" />
<action
android:id="@+id/action_nav_profile_to_nav_settings"
app:destination="@id/nav_settings" />
<action
android:id="@+id/action_nav_profile_to_nav_awesome"
app:destination="@id/nav_awesome" />
</fragment>
<fragment
android:id="@+id/nav_feedback"
android:name="cash.z.ecc.android.ui.profile.FeedbackFragment"
tools:layout="@layout/fragment_feedback">
<argument android:name="rating" app:argType="integer" android:defaultValue="-1"/>
<argument android:name="isSolicited" app:argType="boolean" android:defaultValue="false"/>
</fragment>
<fragment
android:id="@+id/nav_settings"
android:name="cash.z.ecc.android.ui.settings.SettingsFragment"
tools:layout="@layout/fragment_settings" />
<fragment
android:id="@+id/nav_awesome"
android:name="cash.z.ecc.android.ui.profile.AwesomeFragment"
tools:layout="@layout/fragment_awesome">
<action
android:id="@+id/action_nav_awesome_to_nav_history"
app:destination="@id/nav_history"
app:popUpTo="@id/nav_home"
app:popUpToInclusive="false"/>
</fragment>
<fragment
android:id="@+id/nav_autoshielding_info"
android:name="cash.z.ecc.android.ui.home.AutoshieldingInformationFragment"
tools:layout="@layout/fragment_auto_shield_information"
>
<argument android:name="isStartAutoshield"
app:argType="boolean"
android:defaultValue="false"/>
<action
android:id="@+id/action_nav_autoshielding_info_to_home"
app:destination="@id/nav_home"
app:popUpTo="@id/nav_home"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_nav_autoshielding_info_to_autoshield"
app:destination="@id/nav_shield_final" />
<action
android:id="@+id/action_nav_autoshielding_info_to_browser"
app:destination="@id/nav_autoshielding_info_details"
app:popUpTo="@id/nav_home"
app:popUpToInclusive="true" />
</fragment>
<activity
android:id="@+id/nav_autoshielding_info_details"
app:action="android.intent.action.VIEW"
app:data="@string/autoshield_explanation_url" />
<!-- -->
<!-- Send Navigation -->
<!-- -->
<fragment
android:id="@+id/nav_send"
android:name="cash.z.ecc.android.ui.send.SendFragment"
tools:layout="@layout/fragment_send" >
<action
android:id="@+id/action_nav_send_to_nav_scan"
app:destination="@id/nav_scan" />
<action
android:id="@+id/action_nav_send_to_nav_home"
app:destination="@id/nav_home"
app:enterAnim="@anim/anim_enter_from_left"
app:exitAnim="@anim/anim_exit_to_right"/>
<action
android:id="@+id/action_nav_send_to_nav_send_final"
app:destination="@id/nav_send_final"
app:enterAnim="@anim/anim_fade_in"/>
</fragment>
<!-- <fragment-->
<!-- android:id="@+id/nav_send_memo"-->
<!-- android:name="cash.z.ecc.android.ui.send.SendMemoFragment"-->
<!-- tools:layout="@layout/fragment_send_memo" >-->
<!-- <action-->
<!-- android:id="@+id/action_nav_send_memo_to_send_confirm"-->
<!-- app:destination="@id/nav_send_confirm"-->
<!-- app:exitAnim="@anim/anim_exit_to_left"-->
<!-- app:enterAnim="@anim/anim_enter_from_right"/>-->
<!-- <action-->
<!-- android:id="@+id/action_nav_send_memo_to_nav_send_address"-->
<!-- app:destination="@id/nav_send_address"-->
<!-- app:enterAnim="@anim/anim_enter_from_left"-->
<!-- app:exitAnim="@anim/anim_exit_to_right" />-->
<!-- </fragment>-->
<!-- <fragment-->
<!-- android:id="@+id/nav_send_confirm"-->
<!-- android:name="cash.z.ecc.android.ui.send.SendConfirmFragment"-->
<!-- tools:layout="@layout/fragment_send_confirm" >-->
<!-- <action-->
<!-- android:id="@+id/action_nav_send_confirm_to_send_final"-->
<!-- app:destination="@id/nav_send_final"-->
<!-- app:popUpTo="@id/nav_send_confirm"-->
<!-- app:popUpToInclusive="true"-->
<!-- app:enterAnim="@anim/anim_fade_in"/>-->
<!-- <action-->
<!-- android:id="@+id/action_nav_send_confirm_to_nav_send_memo"-->
<!-- app:destination="@id/nav_send_memo"-->
<!-- app:enterAnim="@anim/anim_enter_from_left"-->
<!-- app:exitAnim="@anim/anim_exit_to_right" />-->
<!-- </fragment>-->
<fragment
android:id="@+id/nav_send_final"
android:name="cash.z.ecc.android.ui.send.SendFinalFragment"
tools:layout="@layout/fragment_send_final" >
<action
android:id="@+id/action_nav_send_final_to_nav_home"
app:destination="@id/nav_home"
app:popUpTo="@id/nav_home"
app:popUpToInclusive="false"/>
<action
android:id="@+id/action_nav_send_final_to_nav_history"
app:destination="@id/nav_history"
app:popUpTo="@id/nav_home"
app:popUpToInclusive="false"/>
<action
android:id="@+id/action_nav_send_final_to_nav_send"
app:destination="@id/nav_send"
app:popUpTo="@id/nav_send"
app:popUpToInclusive="false"/>
</fragment>
<fragment
android:id="@+id/nav_funds_available"
android:name="cash.z.ecc.android.ui.send.FundsAvailableFragment"
tools:layout="@layout/fragment_funds_available">
<action
android:id="@+id/action_nav_funds_available_to_nav_shield_final"
app:destination="@id/nav_shield_final"
app:popUpTo="@id/nav_home"
app:popUpToInclusive="false" />
</fragment>
<fragment
android:id="@+id/nav_shield_final"
android:name="cash.z.ecc.android.ui.send.AutoShieldFragment"
tools:layout="@layout/fragment_auto_shield" >
<action
android:id="@+id/action_nav_shield_final_to_nav_home"
app:destination="@id/nav_home"
app:popUpTo="@id/nav_home"
app:popUpToInclusive="false"/>
<action
android:id="@+id/action_nav_shield_final_to_nav_history"
app:destination="@id/nav_history"
app:popUpTo="@id/nav_home"
app:popUpToInclusive="false"/>
<action
android:id="@+id/action_nav_shield_final_to_balance_detail"
app:destination="@id/nav_balance_detail"
app:popUpTo="@id/nav_balance_detail"
app:popUpToInclusive="false"/>
<action
android:id="@+id/action_nav_shield_final_to_profile"
app:destination="@id/nav_profile"
app:popUpTo="@id/nav_home"
app:popUpToInclusive="false"/>
</fragment>
<!-- -->
<!-- Wallet Setup Navigation -->
<!-- -->
<fragment
android:id="@+id/nav_landing"
android:name="cash.z.ecc.android.ui.setup.LandingFragment"
tools:layout="@layout/fragment_landing" >
<action
android:id="@+id/action_nav_landing_to_nav_backup"
app:destination="@id/nav_backup"
app:popUpTo="@id/nav_landing"
app:popUpToInclusive="true"/>
<action
android:id="@+id/action_nav_landing_to_nav_restore"
app:destination="@id/nav_restore" />
</fragment>
<fragment
android:id="@+id/nav_backup"
android:name="cash.z.ecc.android.ui.setup.BackupFragment"
tools:layout="@layout/fragment_backup" >
</fragment>
<fragment
android:id="@+id/nav_restore"
android:name="cash.z.ecc.android.ui.setup.RestoreFragment"
tools:layout="@layout/fragment_restore" >
<action
android:id="@+id/action_nav_restore_to_nav_home"
app:destination="@id/nav_home"
app:popUpTo="@id/nav_landing"
app:popUpToInclusive="true" />
</fragment>
<!-- -->
<!-- Global actions -->
<!-- -->
<action
android:id="@+id/action_global_nav_scan"
app:destination="@id/nav_scan"
app:popUpTo="@id/nav_scan"
app:popUpToInclusive="true" />
</navigation>

1
app/src/main/res/raw/lottie_button_forever.json

@ -0,0 +1 @@
{"v":"5.6.0","fr":30,"ip":0,"op":30,"w":324,"h":64,"nm":"BalanceHome","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[162,-201.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-16.292],[16.292,0],[0,0],[0,16.292],[-16.292,0],[0,0]],"o":[[0,16.292],[0,0],[-16.292,0],[0,-16.292],[0,0],[16.292,0]],"v":[[160,0],[130.5,29.5],[-130.5,29.5],[-160,0],[-130.5,-29.5],[130.5,-29.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.4,0.4,0.4,1],"ix":3},"o":{"a":0,"k":0,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.4,0.4,0.4,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":79,"s":[0]},{"t":90,"s":[100]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,233.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":30,"op":127,"st":30,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[162,-201.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-16.292],[16.292,0],[0,0],[0,16.292],[-16.292,0],[0,0]],"o":[[0,16.292],[0,0],[-16.292,0],[0,-16.292],[0,0],[16.292,0]],"v":[[160,0],[130.5,29.5],[-130.5,29.5],[-160,0],[-130.5,-29.5],[130.5,-29.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.4,0.4,0.4,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[4]},{"t":90,"s":[2]}],"ix":5},"lc":2,"lj":1,"ml":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":0,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,233.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[0]},{"t":85,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[15]},{"t":85,"s":[100]}],"ix":2},"o":{"a":0,"k":-90,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":30,"op":127,"st":30,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[162,-201.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-16.292],[16.292,0],[0,0],[0,16.292],[-16.292,0],[0,0]],"o":[[0,16.292],[0,0],[-16.292,0],[0,-16.292],[0,0],[16.292,0]],"v":[[160,0],[130.5,29.5],[-130.5,29.5],[-160,0],[-130.5,-29.5],[130.5,-29.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.4,0.4,0.4,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":0,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,233.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":30,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[15]},{"t":30,"s":[15]}],"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[-90]},{"t":30,"s":[270]}],"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":30,"st":0,"bm":0}],"markers":[]}

1
app/src/main/res/raw/lottie_button_loading.json

File diff suppressed because one or more lines are too long

1
app/src/main/res/raw/lottie_button_loading_new.json

@ -0,0 +1 @@
{"v":"5.6.0","fr":30,"ip":0,"op":200,"w":324,"h":64,"nm":"FullSync","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Stroke2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[162.5,-201,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false,"d":1,"s":{"a":0,"k":[320,60],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":12,"ix":4}},{"ty":"fl","c":{"a":0,"k":[0.4,0.4,0.4,1],"ix":4},"o":{"a":0,"k":0,"ix":5},"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false,"r":1},{"ty":"st","c":{"a":0,"k":[0.4,0.4,0.4,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"bm":0,"nm":"Stroke 3","mn":"ADBE Vector Graphic - Stroke","hd":false,"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":99,"s":[4]},{"t":174,"s":[2]}],"ix":5},"lc":2,"lj":1,"ml":2},{"ty":"tr","p":{"a":0,"k":[0.5,233.125],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":99,"s":[0]},{"t":192,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":99,"s":[15]},{"t":192,"s":[100]}],"ix":2},"o":{"a":0,"k":1000,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":100,"op":211,"st":99,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Stroke1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[162,-201,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false,"d":1,"s":{"a":0,"k":[320,60],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":12,"ix":4}},{"ty":"st","c":{"a":0,"k":[0.4,0.4,0.4,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 2","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.4,0.4,0.4,1],"ix":4},"o":{"a":0,"k":0,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0.5,233.125],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":100,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[15]},{"t":100,"s":[15]}],"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[-90]},{"t":100,"s":[990]}],"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":100,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"GoldButton 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[162,-201,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false,"d":1,"s":{"a":0,"k":[320,60],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":12,"ix":4}},{"ty":"st","c":{"a":0,"k":[0.4,0.4,0.4,1],"ix":3},"o":{"a":0,"k":0,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.4,0.4,0.4,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":186,"s":[0]},{"t":200,"s":[100]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0.5,233.125],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":15,"op":201,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"DisabledButton","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[162,-201.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[],"ip":0,"op":249,"st":0,"bm":0}],"markers":[]}

1
app/src/main/res/raw/lottie_sending.json

@ -0,0 +1 @@
{"v":"5.5.8","fr":60,"ip":0,"op":90,"w":170,"h":130,"nm":"loader3","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":20,"s":[0]},{"t":89,"s":[180]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.145,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[34,65,0],"to":[0,-2.917,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.588},"o":{"x":0.732,"y":0},"t":20,"s":[34,47.5,0],"to":[0,0,0],"ti":[-55.765,0.067,0]},{"i":{"x":0.096,"y":1},"o":{"x":0.167,"y":0.414},"t":45,"s":[90.504,121.958,0],"to":[42.185,-0.051,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.984,"y":0},"t":70,"s":[139,40,0],"to":[0,0,0],"ti":[0,-0.833,0]},{"t":90,"s":[139,65,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":20,"s":[20,20]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":45,"s":[15,15]},{"t":70,"s":[20,20]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":3,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.125490196078,0.125490196078,0.133333333333,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('CHANGE COLORS').layer('CHANGE COLOR').content('CHANGE COLOR HERE').color;"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false}],"ip":0,"op":90,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.408,"y":1},"o":{"x":0.518,"y":0},"t":35,"s":[139,65,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.396,"y":1},"o":{"x":0.461,"y":0},"t":59,"s":[94,65,0],"to":[0,0,0],"ti":[0,0,0]},{"t":83,"s":[104,65,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"rc","d":1,"s":{"a":0,"k":[20,20],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":3,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.125490196078,0.125490196078,0.133333333333,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('CHANGE COLORS').layer('CHANGE COLOR').content('CHANGE COLOR HERE').color;"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false}],"ip":0,"op":90,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.408,"y":1},"o":{"x":0.518,"y":0},"t":32,"s":[104,65,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.396,"y":1},"o":{"x":0.461,"y":0},"t":56,"s":[59,65,0],"to":[0,0,0],"ti":[0,0,0]},{"t":80,"s":[69,65,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"rc","d":1,"s":{"a":0,"k":[20,20],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":3,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.125490196078,0.125490196078,0.133333333333,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('CHANGE COLORS').layer('CHANGE COLOR').content('CHANGE COLOR HERE').color;"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false}],"ip":0,"op":90,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30,"s":[0]},{"t":54,"s":[-90]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.408,"y":1},"o":{"x":0.518,"y":0},"t":30,"s":[69,65,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.396,"y":1},"o":{"x":0.461,"y":0},"t":54,"s":[24,65,0],"to":[0,0,0],"ti":[0,0,0]},{"t":78,"s":[34,65,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"rc","d":1,"s":{"a":0,"k":[20,20],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":3,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.125490196078,0.125490196078,0.133333333333,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('CHANGE COLORS').layer('CHANGE COLOR').content('CHANGE COLOR HERE').color;"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false}],"ip":0,"op":90,"st":0,"bm":0}],"markers":[]}

1
app/src/main/res/raw/lottie_shielding.json

File diff suppressed because one or more lines are too long

1
app/src/main/res/raw/lottie_success.json

File diff suppressed because one or more lines are too long

16
app/src/main/res/values-es/custom_translations.xml

@ -0,0 +1,16 @@
<resources>
<string name="custom_translation_decimal_key">,</string>
<string name="custom_translation_decimal_separator">,</string>
<string name="custom_translation_format_date_time_brief">d/M HH:mma</string>
<!-- Manual -->
<string name="custom_translation_verify">Verifica</string>
<string name="custom_translation_birthday">Fecha inicial: %1$,d</string>
<string name="custom_translation_expecting">Esperando</string>
<string name="custom_translation_syncing">Sincronizando…%1$d %%</string>
<string name="custom_translation_scanning">Escaneando…%1$d %%</string>
<string name="custom_translation_send_confirm">Confirma que quieres enviar</string>
<string name="custom_translation_chars">caracteres</string>
<string name="custom_translation_sending">Enviando</string>
</resources>

74
app/src/main/res/values-es/translated.xml

@ -0,0 +1,74 @@
<resources>
<string name="translated_app_name">SilentDragon</string>
<string name="translated_loadingtext">Cargando</string>
<string name="translated_welcometext">Bienvenido</string>
<string name="translated_fiattext">fiduciario</string>
<string name="translated_title_backupseed">Tu semilla de respaldo</string>
<string name="translated_copy_backupseed">Te recomendamos hace respaldarla en papel y obtener un administrador de contraseñas</string>
<string name="translated_feedback_walletbackupstatus">Tu Wallet necesita ser respaldada</string>
<string name="translated_seed_verify">Verifica tu frase de respaldo</string>
<string name="translated_seed_verifytext">Ingresa las palabras faltantes para verificar tu frase de respaldo</string>
<string name="translated_seed_restore">Restaurando desde frase de respaldo</string>
<string name="translated_seed_restoretext">Deberás ingresar todas las 24 palabras de tu frase semilla en orden</string>
<string name="translated_seed_reminder">¡Tus fondos están en riesgo!</string>
<string name="translated_seed_remindertext">Recuerda, en Zcash tu eres el banco. Cualquiera que tenga tu frase semilla podrá acceder a tu wallet</string>
<string name="translated_seed_remindertext2">Deberías respaldar tu wallet inmediatamente. Solo tú puedes hacerlo.</string>
<string name="translated_seed_accepted">¡Frase semilla aceptada! ¡Escaneando!</string>
<string name="translated_seed_birthdaytext">Estamos buscando transacciones de tu wallet en la blockchain. Si nos provees una fecha inicial de tu wallet podemos hacerlo más rápido.</string>
<string name="translated_seed_enterdate">Ingresar fecha</string>
<string name="translated_seed_birthday">Fecha inicial</string>
<string name="translated_button_dontknow">No lo sé</string>
<string name="translated_button_backup">Respaldo</string>
<string name="translated_button_skip">Omitir por ahora</string>
<string name="translated_balance_screen">Pantalla de Balance</string>
<string name="translated_balance_amounttosend">Ingresar un monto para enviar</string>
<string name="translated_balance_expecting">esperando XX ZEC</string>
<string name="translated_balance_available">Disponible</string>
<string name="translated_balance_nofunds">No hay fondos disponibles</string>
<string name="translated_balance_syncing">Sincronizando .... %</string>
<string name="translated_balance_scanning">Escaneando .... %</string>
<string name="translated_button_sendamount">Enviar Monto</string>
<string name="translated_button_wallethistory">Historial de Wallet</string>
<string name="translated_address_screen">Tu dirección</string>
<string name="translated_address_shielded">Tu dirección blindada</string>
<string name="translated_address_transparent">Tu dirección transparente</string>
<string name="translated_feedback_addresscopied">¡Dirección Copiada!</string>
<string name="translated_button_copytoclipboard">Copiar al portapapeles</string>
<string name="translated_profile_screen">Usuario Blindado</string>
<string name="translated_button_feedback">Enviar feedback</string>
<string name="translated_button_backup_wallet">Respaldar Wallet</string>
<string name="translated_button_applicationlogs">Ver Logs de Aplicación</string>
<string name="translated_button_close">Cerrar</string>
<string name="translated_send_screen">Pantalla de Envío</string>
<string name="translated_button_send">Enviar</string>
<string name="translated_send_fromshielded">de tu wallet blindada</string>
<string name="translated_send_fromtransparent">de tu wallet transparente</string>
<string name="translated_send_fromboth">desde ambas wallet</string>
<string name="translated_label_to">A</string>
<string name="translated_feedback_default">Ingresa una dirección Zcash Válida</string>
<string name="translated_feedback_shieldedaddress">Esta es una dirección blindada válida</string>
<string name="translated_feedback_transparentaddress">Esta es una dirección transparente válida</string>
<string name="translated_feedback_sameaddress">¡Cuidado, parece que esta es tu propia dirección!</string>
<string name="translated_feedback_invalidaddress">¡Cuidado, esta dirección no es válida!</string>
<string name="translated_label_memo">Memo</string>
<string name="translated_label_charactercount">caracteres</string>
<string name="translated_label_replyto">incluir remitente</string>
<string name="translated_send_onclipboard">En Portapapeles</string>
<string name="translated_send_lastused">Última Utilizada</string>
<string name="translated_send_unknown">Desconocido</string>
<string name="translated_send_scanqr">Escanear Dirección destinatario</string>
<string name="translated_send_securityauth">Identifícate para enviar</string>
<string name="translated_send_securityauth2">Confirma que quieres enviar XX ZEC a</string>
<string name="translated_send_sending">Enviando XX ZEC a</string>
<string name="translated_send_cancelled">Cancelado</string>
<string name="translated_send_sent">¡Enviado!</string>
<string name="translated_button_done">Listo</string>
<string name="translated_button_seedetails">Ver Detalles</string>
<string name="translated_button_back">Volver</string>
<string name="translated_screen_wallethistory">Historial de tu Wallet</string>
</resources>
<!-- ISSUES:
- button_backup isn't unique
-->

16
app/src/main/res/values-it/custom_translations.xml

@ -0,0 +1,16 @@
<resources>
<string name="custom_translation_decimal_key">,</string>
<string name="custom_translation_decimal_separator">,</string>
<string name="custom_translation_format_date_time_brief">d/M HH:mma</string>
<!-- Manual -->
<string name="custom_translation_verify">Controlla</string>
<string name="custom_translation_birthday">Birthday: %1$,d</string>
<string name="custom_translation_expecting">Aspettando</string>
<string name="custom_translation_syncing">Downloading…%1$d%%</string>
<string name="custom_translation_scanning">Scanning…%1$d%%</string>
<string name="custom_translation_send_confirm">Conferma che vuoi inviare</string>
<string name="custom_translation_chars">caratteri</string>
<string name="custom_translation_sending">Inviando</string>
</resources>

63
app/src/main/res/values-it/translated.xml

@ -0,0 +1,63 @@
<resources>
<string name="translated_address_screen">Tuo Indirizzo</string>
<string name="translated_address_shielded">Il tuo indirizzo blindato</string>
<string name="translated_address_transparent">Il tuo indirizzo trasparente</string>
<string name="translated_balance_amounttosend">Inserisci un importo da inviare</string>
<string name="translated_balance_available">Disponibile</string>
<string name="translated_balance_expecting">aspettando %@ ZEC</string>
<string name="translated_balance_nofunds">Nessun fondo disponibile</string>
<string name="translated_balance_screen">Bilancio</string>
<string name="translated_balance_syncing">Sincronizzando %@%%</string>
<string name="translated_button_applicationlogs">Vedere Registri dell\'applicazione</string>
<string name="translated_button_back">ritornare</string>
<string name="translated_button_backup">Fare Backup</string>
<string name="translated_button_backup_wallet">Fare Backup</string>
<string name="translated_button_close">Chiudere</string>
<string name="translated_button_copytoclipboard">copia negli appunti</string>
<string name="translated_button_done">Pronto</string>
<string name="translated_button_dontknow">non lo so</string>
<string name="translated_button_feedback">"Invia feedback</string>
<string name="translated_button_seedetails">Vedi i dettagli</string>
<string name="translated_button_send">Invio</string>
<string name="translated_button_sendamount">Invia importo</string>
<string name="translated_button_skip">Salta per ora</string>
<string name="translated_button_wallethistory">cronologia del wallet</string>
<string name="translated_feedback_default">Inserisci un indirizzo Zcash valido</string>
<string name="translated_feedback_invalidaddress">Attenzione, questo indirizzo non è valido!</string>
<string name="translated_feedback_sameaddress">Attenzione, sembra che questo sia il tuo indirizzo!</string>
<string name="translated_feedback_shieldedaddress">Questo è un indirizzo blindato valido</string>
<string name="translated_feedback_transparentaddress">Questo è un indirizzo trasparente valido</string>
<string name="translated_feedback_walletbackupstatus">La tua Wallet ha bisogno di un backup</string>
<string name="translated_label_charactercount">caratteri</string>
<string name="translated_label_replyto">includere mittente</string>
<string name="translated_label_to">Per</string>
<string name="translated_loadingtext">Caricando</string>
<string name="translated_profile_screen">Utente blindato</string>
<string name="translated_screen_wallethistory">Storia del tuo Wallet</string>
<string name="translated_seed_accepted">Frase seme accettata!</string>
<string name="translated_seed_birthday">Blocco iniziale</string>
<string name="translated_seed_birthdaytext">Stiamo cercando transazioni dal tuo wallet sulla blockchain. Se ci fornisci una data iniziale del tuo Wallet, possiamo farlo più velocemente.</string>
<string name="translated_seed_enterdate">inserici data</string>
<string name="translated_seed_reminder">I tuoi fondi sono a rischio!</string>
<string name="translated_seed_remindertext">Ricorda, in Zcash sei tu la banca. Chiunque abbia la tua frase seme può accedere al tuo Wallet</string>
<string name="translated_seed_remindertext2">Dovresti eseguire immediatamente il backup del tuo portafoglio. Solo tu puoi farlo</string>
<string name="translated_seed_restore">recupero dalla frase di backup</string>
<string name="translated_seed_restoretext">Devi inserire tutte le 24 parole della tua frase seme in ordine</string>
<string name="translated_seed_verify">Controlla la tua frase di backup</string>
<string name="translated_seed_verifytext">Inserisci le parole mancanti per vedere la frase di backup</string>
<string name="translated_send_cancelled">Annullato</string>
<string name="translated_send_fromboth">dalle due wallet</string>
<string name="translated_send_fromshielded">del tuo wallet blindato</string>
<string name="translated_send_fromtransparent">del tuo wallet trasparente</string>
<string name="translated_send_lastused">Ultimo uso</string>
<string name="translated_send_onclipboard">Negli appunti</string>
<string name="translated_send_scanqr">Scansione indirizzo destinatario</string>
<string name="translated_send_screen">Invio</string>
<string name="translated_send_securityauth">Accedi per inviare</string>
<string name="translated_send_securityauth2">Conferma che vuoi inviare %@ ZEC a</string>
<string name="translated_send_sending">Inviando %@ ZEC a</string>
<string name="translated_send_sent">Inviato!</string>
<string name="translated_send_unknown">Sconosciuto</string>
<string name="translated_title_backupseed">Il tuo seme di backup</string>
<string name="translated_welcometext">Benvenuto</string>
</resources>

16
app/src/main/res/values-ko/custom_translations.xml

@ -0,0 +1,16 @@
<resources>
<string name="custom_translation_decimal_key">,</string>
<string name="custom_translation_decimal_separator">,</string>
<string name="custom_translation_format_date_time_brief">d/M HH:mma</string>
<!-- Manual -->
<string name="custom_translation_verify"> 확인</string>
<string name="custom_translation_birthday">생일: %1$,d</string>
<string name="custom_translation_expecting">기대</string>
<string name="custom_translation_syncing">동기화…%1$d %%</string>
<string name="custom_translation_scanning">스캐닝…%1$d %%</string>
<string name="custom_translation_send_confirm">를 보낼 것인지 확인하십시오</string>
<string name="custom_translation_chars">문자</string>
<string name="custom_translation_sending">보내기</string>
</resources>

74
app/src/main/res/values-ko/translated.xml

@ -0,0 +1,74 @@
<resources>
<string name="translated_app_name">ECC 지갑</string>
<string name="translated_loadingtext">로딩</string>
<string name="translated_welcometext">환영</string>
<string name="translated_fiattext">₩ (currency = 통화)</string>
<string name="translated_title_backupseed">당신의 백업 시드</string>
<string name="translated_copy_backupseed">종이 백업과 비밀번호 보관소를 권장합니다.</string>
<string name="translated_feedback_walletbackupstatus">지갑을 백업해야합니다</string>
<string name="translated_seed_verify">백업 확인</string>
<string name="translated_seed_verifytext">누락 된 시드 단어를 입력하여 백업을 확인하십시오.</string>
<string name="translated_seed_restore">백업에서 복원</string>
<string name="translated_seed_restoretext">24 개의 시드 단어를 모두 순서대로 입력해야합니다.</string>
<string name="translated_seed_reminder">당신의 자금이 위험에 처해 있습니다</string>
<string name="translated_seed_remindertext">Zcash에서 당신은 은행이라는 것을 기억하십시오. 시드 문구가있는 사람은 누구나 지갑에 액세스 할 수 있습니다.</string>
<string name="translated_seed_remindertext2">다른 사람이 대신 백업 할 수 없으므로 즉시 백업해야합니다.</string>
<string name="translated_seed_accepted">시드 수락, 지금 스캔!</string>
<string name="translated_seed_birthdaytext">지갑에 대한 거래를 위해 블록 체인을 스캔하고 있습니다. 이 지갑의 생일 날짜를 제공 할 수 있다면 동기화 속도를 높일 수 있습니다.</string>
<string name="translated_seed_enterdate">날짜 입력</string>
<string name="translated_seed_birthday">생일</string>
<string name="translated_button_dontknow">몰라</string>
<string name="translated_button_backup">백업</string>
<string name="translated_button_skip">일단은 스킵</string>
<string name="translated_balance_screen">밸런스 화면</string>
<string name="translated_balance_amounttosend">보낼 금액 입력</string>
<string name="translated_balance_expecting">기대 XX ZEC</string>
<string name="translated_balance_available">사용 가능</string>
<string name="translated_balance_nofunds">사용할 수있는 자금 없습니다</string>
<string name="translated_balance_syncing">동기화</string>
<string name="translated_balance_scanning">스캐닝</string>
<string name="translated_button_sendamount">보내는 금액</string>
<string name="translated_button_wallethistory">지갑 내역</string>
<string name="translated_address_screen">당신의 주소</string>
<string name="translated_address_shielded">당신의 쉴드된 주소</string>
<string name="translated_address_transparent">당신의 투명 주소</string>
<string name="translated_feedback_addresscopied">주소 복사</string>
<string name="translated_button_copytoclipboard">클립 보드에 복사</string>
<string name="translated_profile_screen">쉴드된 사용자</string>
<string name="translated_button_feedback">피드백 보내기</string>
<string name="translated_button_backup_wallet">지갑 백업</string>
<string name="translated_button_applicationlogs">애플리케이션 로그보기</string>
<string name="translated_button_close">닫기</string>
<string name="translated_send_screen">보내는 화면</string>
<string name="translated_button_send">보내기</string>
<string name="translated_send_fromshielded">쉴드된 지갑에서</string>
<string name="translated_send_fromtransparent">투명한 지갑에서</string>
<string name="translated_send_fromboth">두 지갑에서</string>
<string name="translated_label_to"></string>
<string name="translated_feedback_default">유효한 zcash 주소를 입력하십시오</string>
<string name="translated_feedback_shieldedaddress">유효한 쉴드된 주소입니다.</string>
<string name="translated_feedback_transparentaddress">유효한 투명 주소입니다.</string>
<string name="translated_feedback_sameaddress">경고, 이것은 귀하의 주소 인 것 같습니다!</string>
<string name="translated_feedback_invalidaddress">경고,이 주소는 유효하지 않습니다!</string>
<string name="translated_label_memo">메모</string>
<string name="translated_label_charactercount">문자</string>
<string name="translated_label_replyto">회신 포함</string>
<string name="translated_send_onclipboard">클립 보드에서</string>
<string name="translated_send_lastused">마지막 사용</string>
<string name="translated_send_unknown">알 수 없는</string>
<string name="translated_send_scanqr">수신자 주소 스캔</string>
<string name="translated_send_securityauth">보내기 인증</string>
<string name="translated_send_securityauth2">XX ZEC를 보낼 것인지 확인하십시오.</string>
<string name="translated_send_sending">XX ZEC를 보내기</string>
<string name="translated_send_cancelled">취소되었습니다</string>
<string name="translated_send_sent">보냈습니다!</string>
<string name="translated_button_done"></string>
<string name="translated_button_seedetails">세부 사항보기</string>
<string name="translated_button_back">돌아가기</string>
<string name="translated_screen_wallethistory">지갑 내역</string>
</resources>
<!-- ISSUES:
- button_backup isn't unique
-->

16
app/src/main/res/values-ru/custom_translations.xml

@ -0,0 +1,16 @@
<resources>
<string name="custom_translation_decimal_key">,</string>
<string name="custom_translation_decimal_separator">,</string>
<string name="custom_translation_format_date_time_brief">d/M HH:mma</string>
<!-- Manual -->
<string name="custom_translation_verify">Проверьте</string>
<string name="custom_translation_birthday">Создано: %1$,d</string>
<string name="custom_translation_expecting">Ожидание</string>
<string name="custom_translation_syncing">Синхронизация…%1$d%%</string>
<string name="custom_translation_scanning">Сканирование…%1$d%%</string>
<string name="custom_translation_send_confirm">Пожалуйста подтвердите, что хотите отправить</string>
<string name="custom_translation_chars">символы</string>
<string name="custom_translation_sending">Отправка</string>
</resources>

69
app/src/main/res/values-ru/translated.xml

@ -0,0 +1,69 @@
<resources>
<string name="translated_app_name">Кошелек ECC</string>
<string name="translated_loadingtext">Загрузка</string>
<string name="translated_welcometext">Добро пожаловать!</string>
<string name="translated_fiattext">В валюте</string>
<string name="translated_title_backupseed">Ваша фраза восстановления</string>
<string name="translated_copy_backupseed">Мы рекомендуем использовать резервную копию на бумаге и хранилище паролей.</string>
<string name="translated_feedback_walletbackupstatus">Необходимо создать резервную копию вашего кошелька.</string>
<string name="translated_seed_verify">Проверьте свою резервную копию.</string>
<string name="translated_seed_verifytext">Пожалуйста, заполните отсутствующие слова фразы восстановления, чтобы проверить свою резервную копию.</string>
<string name="translated_seed_restore">Восстановление из резервной копии</string>
<string name="translated_seed_restoretext">Вам необходимо будет ввести 24 слова фразы восстановления по порядку.</string>
<string name="translated_seed_reminder">Существует риск потерять средства!</string>
<string name="translated_seed_remindertext">Помните, что с Zcash Вы сами находитесь в роли банка. Любой, кто владеет фразой восстановления, получает полный доступ к кошельку.</string>
<string name="translated_seed_remindertext2">Вам следует незамедлительно создать резервную копию, так она позволит восстановить доступ к Вашему кошельку.</string>
<string name="translated_seed_accepted">Фраза восстановления принята, идёт сканирование блокчейна!</string>
<string name="translated_seed_birthdaytext">Мы сканируем блокчейн на предмет транзакций, которые относятся к вашему кошельку. Если вы можете указать дату создания данного кошелька, процесс ускорится.</string>
<string name="translated_seed_enterdate">Введите дату</string>
<string name="translated_seed_birthday">Кошелёк создан</string>
<string name="translated_button_dontknow">Я не знаю</string>
<string name="translated_button_backup">Резервная копия</string>
<string name="translated_button_skip">Пропустить</string>
<string name="translated_balance_screen">Экран баланса</string>
<string name="translated_balance_amounttosend">Введите сумму для отправки</string>
<string name="translated_balance_expecting">Ожидание XX ZEC</string>
<string name="translated_balance_available">Доступно</string>
<string name="translated_balance_nofunds">Нет доступных средств</string>
<string name="translated_balance_syncing">Синхронизация ... %</string>
<string name="translated_balance_scanning">Сканирование ... %</string>
<string name="translated_button_sendamount">Отправить сумму</string>
<string name="translated_button_wallethistory">История транзакций</string>
<string name="translated_address_screen">Ваш адрес</string>
<string name="translated_address_shielded">Ваш защищённый адрес</string>
<string name="translated_address_transparent">Ваш прозрачный адрес</string>
<string name="translated_feedback_addresscopied">Адрес скопирован!</string>
<string name="translated_button_copytoclipboard">Скопировать в буфер обмена</string>
<string name="translated_profile_screen">Защищенный пользователь</string>
<string name="translated_button_feedback">Отправить отзыв</string>
<string name="translated_button_backup_wallet">Резервная копия кошелька</string>
<string name="translated_button_applicationlogs">Просмотреть журнал операций приложения</string>
<string name="translated_button_close">Закрыть</string>
<string name="translated_send_screen">Отправить экран</string>
<string name="translated_button_send">Отправить</string>
<string name="translated_send_fromshielded">с Вашего кошелька</string>
<string name="translated_send_fromtransparent">с Вашего прозрачного кошелька</string>
<string name="translated_send_fromboth">с двух кошельков</string>
<string name="translated_label_to">на</string>
<string name="translated_feedback_default">Введите действующий Zcash-адрес </string>
<string name="translated_feedback_shieldedaddress">Это действующий защищённый адрес</string>
<string name="translated_feedback_transparentaddress">Это действующий прозрачный адрес</string>
<string name="translated_feedback_sameaddress">Внимание, похоже, это ваш адрес!</string>
<string name="translated_feedback_invalidaddress">Внимание, этот адрес недействителен!</string>
<string name="translated_label_memo">Заметка</string>
<string name="translated_label_charactercount">символы (символы)</string>
<string name="translated_label_replyto">добавить "ответ на"</string>
<string name="translated_send_onclipboard">В буфере обмена</string>
<string name="translated_send_lastused">Последний используемый</string>
<string name="translated_send_unknown">Неизвестно</string>
<string name="translated_send_scanqr">Сканировать адрес получателя</string>
<string name="translated_send_securityauth">Подтвердите, чтобы отправить</string>
<string name="translated_send_securityauth2">Пожалуйста подтвердите, что хотите отправить XX ZEC на адрес</string>
<string name="translated_send_sending">Отправка XX ZEC на</string>
<string name="translated_send_cancelled">Отменено</string>
<string name="translated_send_sent">Отправлено!</string>
<string name="translated_button_done">Готово</string>
<string name="translated_button_seedetails">Смотреть подробности</string>
<string name="translated_button_back">Вернуться назад</string>
<string name="translated_screen_wallethistory">История транзакций</string>
</resources>

16
app/src/main/res/values-zh/custom_translations.xml

@ -0,0 +1,16 @@
<resources>
<string name="custom_translation_decimal_key">,</string>
<string name="custom_translation_decimal_separator">,</string>
<string name="custom_translation_format_date_time_brief">d/M HH:mma</string>
<!-- Manual -->
<string name="custom_translation_verify">验证</string>
<string name="custom_translation_birthday">创建日期: %1$,d</string>
<string name="custom_translation_expecting">预计为</string>
<string name="custom_translation_syncing">正在同步…%1$d%%</string>
<string name="custom_translation_scanning">正在扫描…%1$d%%</string>
<string name="custom_translation_send_confirm">请确认,您现在要发送</string>
<string name="custom_translation_chars">字符</string>
<string name="custom_translation_sending">正在发送</string>
</resources>

69
app/src/main/res/values-zh/translated.xml

@ -0,0 +1,69 @@
<resources>
<string name="translated_app_name">ECC 钱包</string>
<string name="translated_loadingtext">准备工作</string>
<string name="translated_welcometext">欢迎使用 ECC 钱包!</string>
<string name="translated_fiattext">法币</string>
<string name="translated_title_backupseed">您的备份种子(助记词)</string>
<string name="translated_copy_backupseed">我们建议您使用纸笔抄录备份种子,并锁到带密码的保险箱里。</string>
<string name="translated_feedback_walletbackupstatus">您的钱包需要备份。</string>
<string name="translated_seed_verify">需要验证您的备份。</string>
<string name="translated_seed_verifytext">请填入缺失的词语,以验证您的备份无误。</string>
<string name="translated_seed_restore">从备份助记词中恢复钱包</string>
<string name="translated_seed_restoretext">您需要按照顺序输入 24 个助记词。</string>
<string name="translated_seed_reminder">您的资金处在安全风险中!</string>
<string name="translated_seed_remindertext">请记住,使用 Zcash 的时候,没有其他人在托管您的资金,您 是自己的银行。使用资金的唯一凭证就是助记词。如有他人知道了您的助记词,就有了转移您资金的所有权限。</string>
<string name="translated_seed_remindertext2">请立即备份,如果您遗失了自己的备份,没有任何人能帮您恢复钱包、找回资金。</string>
<string name="translated_seed_accepted">已接受助记词,正在扫描您的帐户余额。</string>
<string name="translated_seed_birthdaytext">软件正在扫描区块链以发现与您的钱包有关的交易。如果您能提供这个钱包的创建时间,可以加快扫描速度。</string>
<string name="translated_seed_enterdate">输入日期</string>
<string name="translated_seed_birthday">创建日期</string>
<string name="translated_button_dontknow">我不知道 / 我忘了</string>
<string name="translated_button_backup">备份</string>
<string name="translated_button_skip">跳过</string>
<string name="translated_balance_screen">余额扫描</string>
<string name="translated_balance_amounttosend">输入您想发送的数额</string>
<string name="translated_balance_expecting">预计为 XX ZEC</string>
<string name="translated_balance_available">可用</string>
<string name="translated_balance_nofunds">暂无可用的款项</string>
<string name="translated_balance_syncing">正在同步 ...%</string>
<string name="translated_balance_scanning">正在扫描 ...%</string>
<string name="translated_button_sendamount">发送数额</string>
<string name="translated_button_wallethistory">钱包历史</string>
<string name="translated_address_screen">您的地址</string>
<string name="translated_address_shielded">您的隐蔽地址</string>
<string name="translated_address_transparent">您的透明地址</string>
<string name="translated_feedback_addresscopied">已复制地址</string>
<string name="translated_button_copytoclipboard">已复制到剪贴板</string>
<string name="translated_profile_screen">隐蔽用户</string>
<string name="translated_button_feedback">发送反馈</string>
<string name="translated_button_backup_wallet">备份钱包</string>
<string name="translated_button_applicationlogs">查看应用日志</string>
<string name="translated_button_close">关闭</string>
<string name="translated_send_screen">发送资金功能</string>
<string name="translated_button_send">发送</string>
<string name="translated_send_fromshielded">从您的隐蔽钱包发送</string>
<string name="translated_send_fromtransparent">从您的透明钱包发送</string>
<string name="translated_send_fromboth">同时从您的隐蔽钱包和透明钱包拼凑数额</string>
<string name="translated_label_to"></string>
<string name="translated_feedback_default">请输入一个有效的 Zcash 地址</string>
<string name="translated_feedback_shieldedaddress">这是一个有效的隐蔽地址</string>
<string name="translated_feedback_transparentaddress">这是一个有效的透明地址</string>
<string name="translated_feedback_sameaddress">警告,该地址似乎是您自己的地址!</string>
<string name="translated_feedback_invalidaddress">警告,该地址不是一个有效地址!</string>
<string name="translated_label_memo">备注</string>
<string name="translated_label_charactercount">字符</string>
<string name="translated_label_replyto"></string>
<string name="translated_send_onclipboard">在剪贴板内</string>
<string name="translated_send_lastused">上一次使用</string>
<string name="translated_send_unknown">未知</string>
<string name="translated_send_scanqr">扫描接收人地址</string>
<string name="translated_send_securityauth">授权发送</string>
<string name="translated_send_securityauth2">请确认,您现在要发送 XX ZEC 发送给 </string>
<string name="translated_send_sending">正在发送 XX ZEC 给</string>
<string name="translated_send_cancelled">已取消</string>
<string name="translated_send_sent">已发送!</string>
<string name="translated_button_done">完成</string>
<string name="translated_button_seedetails">查看细节</string>
<string name="translated_button_back">返回</string>
<string name="translated_screen_wallethistory">您的钱包历史</string>
</resources>

91
app/src/main/res/values/colors.xml

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- -->
<!-- palette: Android -->
<!-- -->
<!-- colors here should map to a material design value -->
<color name="colorPrimary">@color/zcashYellow</color>
<color name="colorPrimaryMedium">#7C7C7C</color>
<color name="colorPrimaryDark">#454545</color>
<color name="colorAccent">#A1A1A1</color>
<color name="colorSurface">@color/text_light</color>
<color name="colorOnSurface">@color/background_banner</color>
<!-- -->
<!-- palette: Zcash -->
<!-- -->
<!-- colors here should have a hex value and a generic name, prefixed with zcash -->
<!-- This palette should be kept as small as possible
and should map directly to the design style guide -->
<color name="zcashWhite">#F5F5F5</color>
<color name="zcashGray">#616161</color>
<color name="zcashWhite_12">#1FFFFFFF</color>
<color name="zcashWhite_24">#3DFFFFFF</color>
<color name="zcashWhite_40">#66FFFFFF</color>
<color name="zcashWhite_50">#80FFFFFF</color>
<color name="zcashWhite_60">#A3FFFFFF</color>
<color name="zcashWhite_87">#BFFFFFFF</color>
<color name="zcashWhite_light">#EDEDED</color>
<color name="zcashBlack_light">#2B2B2B</color>
<color name="zcashBlack_12">#1F000000</color>
<color name="zcashBlack_40">#66000000</color>
<color name="zcashBlack_54">#8A000000</color>
<color name="zcashBlack_87">#DD000000</color>
<color name="zcashBlack_dark">#171717</color>
<color name="zcashBlack_0">#00000000</color>
<color name="zcashGreen">#66BB6A</color>
<color name="zcashRed">#BB666A</color>
<color name="zcashBlue">#26DAB6</color>
<color name="zcashBlueDark">#4A90E2</color>
<!-- yellows (greys) -->
<color name="zcashYellow_light">#B3B3B3</color>
<color name="zcashYellow">#666666</color>
<color name="zcashYellow_dark">#333333</color>
<!-- tab layout grey -->
<color name="unselected_tab_grey">#727272</color>
<!-- -->
<!-- named colors -->
<!-- -->
<!-- every color here should be a reference to a palette color
but have a more useful name for use in code -->
<color name="app_icon_background_0">#FF3F3F4F</color>
<color name="app_icon_background_1">#FF000000</color>
<color name="app_icon_foreground">#FFFFFF</color>
<color name="background_banner">@color/zcashBlack_dark</color>
<color name="background_banner_stroke">#282828</color>
<color name="scan_overlay_background">@color/zcashBlack_87</color>
<color name="spacer">#1FBB666A</color>
<color name="text_send_amount_disabled">@color/text_light</color>
<!-- text -->
<color name="text_light">#FFFFFF</color>
<color name="text_light_dimmed">@color/zcashWhite_50</color>
<color name="text_dark">@color/zcashBlack_87</color>
<color name="text_dark_dimmed">@color/zcashBlack_54</color>
<color name="text_shadow">@color/zcashBlack_40</color>
<!-- text : pending design review -->
<!-- these are colors found in designs that fall near but outside the palette but may want to
replace existing palette colors, after design review -->
<color name="tx_text_light_dimmed">#9B9B9B</color>
<color name="tx_text_light_dimmed_less">#D3D3D3</color>
<color name="tx_circle_icon_bg">#494A4C</color>
<color name="tx_primary">#FFFFFF</color>
<!--transaction colors-->
<color name="transaction_sent">#B8B8B8</color>
</resources>

19
app/src/main/res/values/custom_translations.xml

@ -0,0 +1,19 @@
<resources>
<string name="custom_translation_decimal_separator">.</string>
<string name="custom_translation_decimal_key">.</string>
<string name="custom_translation_format_date_time_brief">M/d h:mma</string>
<!-- Manual -->
<string name="custom_translation_verify">Verify</string>
<string name="custom_translation_birthday">Birthday Height: %1$,d</string>
<string name="custom_translation_expecting">Expecting</string>
<string name="custom_translation_syncing">Downloading…%1$d%%</string>
<string name="custom_translation_scanning">Scanning…%1$d%%</string>
<string name="custom_translation_send_confirm">Please confirm that you want to send</string>
<string name="custom_translation_from_your">"from your "</string>
<string name="custom_translation_wallet">" wallet"</string>
<string name="custom_translation_chars">chars</string>
<string name="custom_translation_sending">Sending</string>
</resources>

10
app/src/main/res/values/dimens.xml

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="buttonCornerRadius">0dp</dimen>
<!-- Floats -->
<item name="ratio_golden_small" format="float" type="dimen">0.381966</item>
<item name="ratio_golden_large" format="float" type="dimen">0.618034</item>
<item name="calculator_button_width_percent" format="float" type="dimen">0.145631068</item>
</resources>

6
app/src/main/res/values/integer.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="transaction_arrow_rotation_send">180</integer>
<integer name="transaction_arrow_rotation_pending">135</integer>
<integer name="transaction_arrow_rotation_received">315</integer>
</resources>

221
app/src/main/res/values/missing_translation.xml

@ -0,0 +1,221 @@
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<!-- ======================================================================================= -->
<!-- Strings that are missing translations. Individually, these can be moved into -->
<!-- the 'translated.xml' file, after they are translated to all supported languages -->
<!-- ======================================================================================= -->
<string name="missing_cancel">Cancel</string>
<string name="missing_backup_instruction_store_words">Store these backup words securely.</string>
<string name="missing_backup_slogan">empowering\neveryone\nwith\neconomic\nfreedom</string>
<string name="missing_backup_verification_not_implemented">Backup verification coming soon!</string>
<!-- -->
<!-- SCREENS -->
<!-- -->
<!-- Screen: Feedback -->
<string name="missing_feedback_hint_1">My experience was . . .</string>
<string name="missing_feedback_hint_2">My balance was . . .</string>
<string name="missing_feedback_hint_3">I\'d like . . .</string>
<string name="missing_feedback_question_1">Any details you\'d like to share?</string>
<string name="missing_feedback_question_2">Was your balance clear?</string>
<string name="missing_feedback_question_3">What feature would you like to see next?</string>
<string name="missing_feedback_thanks">Thanks for the feedback!</string>
<!-- Screen: History -->
<string name="missing_history_address_label">Shielded address:</string>
<string name="missing_history_balance_updating">Updating</string>
<string name="missing_history_empty_text">No history yet.</string>
<!-- Screen: Home -->
<string name="missing_home_balance_updating">Updating</string>
<string name="missing_home_button_send_disconnected">Reconnecting . . .</string>
<string name="missing_home_button_send_idle">IDLE</string>
<string name="missing_home_button_send_updating">Updating</string>
<string name="missing_home_button_send_validating">Validating . . .</string>
<string name="missing_home_dialog_no_balance_button_positive">View Address</string>
<string name="missing_home_dialog_no_balance_message">To make full use of this wallet, deposit funds to your address.</string>
<string name="missing_home_instruction_fund_now">Fund Now</string>
<string name="missing_autoshielding_title_text">HUSH enforces <b>private z-z transactions</b> at the protocol level to ensure maximum privacy.</string>
<string name="missing_autoshielding_body_text">All of the transactions you send and receive are completely private.</string>
<string name="missing_autoshielding_button_positive">Great!</string>
<string name="missing_autoshielding_button_neutral">Tell me more</string>
<!-- Screen: Landing -->
<string name="missing_landing_backup_skipped_message_1">Are you sure? Without a backup, funds can be lost FOREVER!</string>
<string name="missing_landing_backup_skipped_message_2">You can\'t backup later. You\'re probably going to lose your funds!</string>
<string name="missing_landing_button_backup_skipped_1">Later</string>
<string name="missing_landing_button_backup_skipped_2">I\'ve been warned</string>
<string name="missing_landing_button_primary">New</string>
<string name="missing_landing_button_progress_create">Creating</string>
<string name="missing_landing_button_secondary">Restore</string>
<string name="missing_landing_create_success_message">Wallet created! Congratulations!</string>
<string name="missing_landing_import_success_message">Wallet imported! Congratulations!</string>
<string name="missing_landing_title">Welcome to SilentDragon!</string>
<!-- Screen: Profile -->
<string name="missing_profile_rescan_wallet">Rescan Wallet</string>
<string name="missing_profile_share_log_title">Share Log Files</string>
<!-- Screen: Receive -->
<string name="missing_receive_address_title">Receive Funds</string>
<!-- Screen: Scan -->
<string name="missing_scan_invalid_address">"Invalid Hush %1$s address:\n\n%2$s"</string>
<!-- Screen: Send Flow -->
<string name="missing_send_failed_button_text">Retry</string>
<string name="missing_send_final_button_primary_cancel">Cancel</string>
<string name="missing_send_final_button_primary_failed">Failed.</string>
<string name="missing_send_final_button_primary_retry">Retry</string>
<string name="missing_send_final_error_encoding">The transaction could not be encoded.</string>
<string name="missing_send_final_error_submitting">Unable to submit transaction to the network.</string>
<string name="missing_send_fund_source_highlight">shielded</string>
<string name="missing_send_hint_input_zcash_address">Enter a Hush address</string>
<string name="missing_send_history_last_and_clipboard">"Last Used and On Clipboard"</string>
<string name="missing_send_memo_excluded_message">Your transaction is shielded and your address is not available to the recipient</string>
<string name="missing_send_memo_hint">Memo</string>
<string name="missing_send_memo_included_message">Your transaction is shielded but your address will be sent to the recipient via the memo</string>
<string name="missing_send_pending_button_text">Cancel</string>
<string name="missing_send_validation_address_invalid">This address appears to be invalid</string>
<string name="missing_send_validation_error_amount_minimum">Please go back and enter at least 1 Zatoshi.</string>
<string name="missing_send_validation_error_dust">Insufficient funds to cover miner\'s fee.</string>
<string name="missing_send_validation_error_memo_length">Memo must be less than %1$d in length.</string>
<string name="missing_send_validation_error_too_much">Please go back and enter no more than %1$s %2$s.</string>
<string name="missing_send_validation_error_unknown_funds">Available funds not found. Please try again in a moment.</string>
<!-- Screen: Settings-->
<string name="missing_settings_buttons_restore">Restore Defaults</string>
<string name="missing_settings_change_lightwalletd_server">Change Lightwalletd Server</string>
<string name="missing_settings_host_helper_text">Enter a valid host name or IP address</string>
<string name="missing_settings_port_helper_text">Enter a valid port number</string>
<string name="missing_settings_reset">Reset</string>
<string name="missing_settings_server_address">Host</string>
<string name="missing_settings_server_port">Port</string>
<string name="missing_settings_toast_change_server_failure">Failed to change server!</string>
<string name="missing_settings_toast_change_server_success">Successfully changed server!</string>
<string name="missing_settings_update">Update</string>
<!-- Screen: Transaction details -->
<string name="missing_transaction_address_paid_you">paid you</string>
<string name="missing_transaction_address_you_paid">You paid</string>
<string name="missing_transaction_address_paying">Paying</string>
<string name="missing_transaction_block_height_prefix">"from block "</string>
<string name="missing_transaction_confirmation_count_unavailable">Confirmation count temporarily unavailable</string>
<string name="missing_transaction_details_button_explore">Explore</string>
<string name="missing_transaction_details_total">Total Spent</string>
<string name="missing_transaction_instruction_tap">tap to view</string>
<string name="missing_transaction_prefix_from">reply-to</string>
<string name="missing_transaction_received">Received</string>
<string name="missing_transaction_received_from">Received from</string>
<string name="missing_transaction_status_confirmed">Confirmed</string>
<string name="missing_transaction_status_confirming">of 10 confirmations</string>
<string name="missing_transaction_status_expired">Expired</string>
<string name="missing_transaction_status_pending">Pending confirmation</string>
<string name="missing_transaction_status_sent">Sent</string>
<string name="missing_transaction_story_inbound">You Received</string>
<string name="missing_transaction_story_inbound_total">Total Received</string>
<string name="missing_transaction_story_network_fee">+ %1$s network fee</string>
<string name="missing_transaction_story_outbound">You Sent</string>
<string name="missing_transaction_story_outbound_total">Total Sent</string>
<string name="missing_transaction_story_to_shielded">to your shielded wallet</string>
<string name="missing_transaction_timestamp_unavailable">Pending</string>
<string name="missing_transaction_title">Transaction Details</string>
<string name="missing_transaction_with_memo">with a memo</string>
<!-- -->
<!-- Content Descriptions -->
<!-- -->
<string name="missing_content_description_backup_zcash_logo">Hush Logo</string>
<string name="missing_content_description_history_back">back</string>
<string name="missing_content_description_history_copy">copy</string>
<string name="missing_content_description_home_icon_profile">Profile</string>
<string name="missing_content_description_home_icon_scan">Scan</string>
<string name="missing_content_description_profile_back">back</string>
<string name="missing_content_description_profile_copy">copy</string>
<string name="missing_content_description_profile_settings">settings</string>
<string name="missing_content_description_profile_zebra">Hush</string>
<string name="missing_content_description_receive_qr_background">QR background</string>
<string name="missing_content_description_receive_qr_code">QR code</string>
<string name="missing_content_description_receive_qr_logo">QR logo</string>
<string name="missing_content_description_scan_back">back</string>
<string name="missing_content_description_scan_frame">scan frame</string>
<string name="missing_content_description_send_arrow">down arrow</string>
<string name="missing_content_description_send_back">back</string>
<string name="missing_content_description_send_scan_qr">scan QR code</string>
<string name="missing_content_description_send_selected">selected</string>
<string name="missing_content_description_send_shield">shield</string>
<string name="missing_content_description_send_final_back">back</string>
<string name="missing_content_description_settings_back">back</string>
<string name="missing_content_description_transaction_shield">Shield</string>
<string name="missing_content_description_transaction_details_back">back</string>
<string name="missing_content_description_transaction_details_direction">direction</string>
<string name="missing_content_description_transaction_details_memo">memo</string>
<string name="missing_content_description_transaction_details_memo_icon">memo icon</string>
<!-- -->
<!-- Dialogs -->
<!-- -->
<string name="missing_dialog_error_change_server_button_positive">Ok</string>
<string name="missing_dialog_error_change_server_title">Failed to Change Server</string>
<string name="missing_dialog_error_critical_processing_button_negative">Exit</string>
<string name="missing_dialog_error_critical_processing_button_positive">Retry</string>
<string name="missing_dialog_error_critical_processing_message">Critical error while processing blocks!</string>
<string name="missing_dialog_error_critical_processing_title">Processor Error</string>
<string name="missing_dialog_error_critical_link_title">Oh no! Invalid Build!</string>
<string name="missing_dialog_error_critical_link_message">Unfortunately, the Rust libraries are missing and the only remedy is to update to a new version.\n\n(Note to developers: try commenting out mavenLocal).</string>
<string name="missing_dialog_error_invalid_seed_phrase_button_positive">Retry</string>
<string name="missing_dialog_error_invalid_seed_phrase_message">That seed phrase appears to be invalid! Please double-check it and try again.\n\n%1$s</string>
<string name="missing_dialog_error_invalid_seed_phrase_title">Oops! Invalid Seed Phrase</string>
<string name="missing_dialog_error_processor_critical_button_negative">Exit</string>
<string name="missing_dialog_error_processor_critical_button_positive">Retry</string>
<string name="missing_dialog_error_processor_critical_message">Critical error while processing blocks!</string>
<string name="missing_dialog_error_processor_critical_title">Processor Error</string>
<string name="missing_dialog_error_scan_button_negative">Ignore</string>
<string name="missing_dialog_error_scan_button_positive">Retry</string>
<string name="missing_dialog_error_scan_failure_button_negative">Ignore</string>
<string name="missing_dialog_error_scan_failure_button_positive">Retry</string>
<string name="missing_dialog_error_scan_failure_title">Scan Failure</string>
<string name="missing_dialog_error_scan_title">Scan Failure</string>
<string name="missing_dialog_error_uninitialized_button_negative">Clear Data</string>
<string name="missing_dialog_error_uninitialized_button_positive">Exit</string>
<string name="missing_dialog_error_uninitialized_message">This wallet has not been initialized correctly! Perhaps an error occurred during install.\n\nThis can be fixed with a reset. First, locate your backup seed phrase, then CLEAR DATA and reimport it.</string>
<string name="missing_dialog_error_uninitialized_title">Wallet Improperly Initialized</string>
<string name="missing_dialog_first_use_view_tx_message">Are you sure you\'d like to leave the app? This could reduce privacy, if you do not trust the destination.</string>
<string name="missing_dialog_first_use_view_tx_negative">Cancel</string>
<string name="missing_dialog_first_use_view_tx_positive">View Tx</string>
<string name="missing_dialog_first_use_view_tx_title">Potential Privacy Risk</string>
<string name="missing_dialog_modify_server_button_negative">Cancel</string>
<string name="missing_dialog_modify_server_button_positive">Update</string>
<string name="missing_dialog_modify_server_message">WARNING: Entering an invalid or untrusted server might result in misconfiguration or loss of funds!</string>
<string name="missing_dialog_modify_server_title">Modify Lightwalletd Server?</string>
<string name="missing_dialog_rescan_wallet_title">Rescan This Wallet?</string>
<string name="missing_dialog_rescan_wallet_message"><![CDATA[This fix may take a while!<br/><br/>A <b>QUICK</b> scan will just rewind %1$s and take about %2$s.<br/><br/>A <b>FULL</b> scan will rewind %3$s blocks to this wallet\'s birthday take around %4$s!<br/><br/>A <b>WIPE</b> is like a full scan but it first deletes all data then restores it from your backup seed words.]]></string>
<string name="missing_dialog_rescan_wallet_button_positive">Quick</string>
<string name="missing_dialog_rescan_wallet_button_negative">Full</string>
<string name="missing_dialog_rescan_wallet_button_neutral">Wipe</string>
<string name="missing_dialog_not_again">Don\'t show me again</string>
<string name="missing_dialog_nuke_wallet_button_negative">Erase Wallet</string>
<string name="missing_dialog_nuke_wallet_button_positive">Cancel</string>
<string name="missing_dialog_nuke_wallet_message">WARNING: Potential Loss of Funds\n\nClearing all wallet data and can result in a loss of funds, if you cannot locate your correct seed phrase.\n\nPlease confirm that you have your 24-word seed phrase available before proceeding.</string>
<string name="missing_dialog_nuke_wallet_title">Nuke Wallet?</string>
<!-- -->
<!-- Misc -->
<!-- -->
<string name="missing_biometric_backup_phrase_title">Authenticate to proceed</string>
<string name="missing_biometric_backup_phrase_description">Load backup phrase</string>
<string name="missing_camera_permission_denied">Well, this is awkward. You denied permission for the camera.</string>
<string name="missing_error_launch_url">Failed to open browser.</string>
<string name="missing_key_decimal">.</string>
</resources>

8
app/src/main/res/values/missing_translation_urls.xml

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<!-- ======================================================================================= -->
<!-- Strings that are missing translations. Individually, these can be moved into -->
<!-- the 'translated-urls.xml' file, after they are translated to all supported languages -->
<!-- ======================================================================================= -->
<string name="missing_autoshield_explanation_url">https://hush.is/</string>
</resources>

4
app/src/main/res/values/strings-urls.xml

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="autoshield_explanation_url">@string/missing_autoshield_explanation_url</string>
</resources>

293
app/src/main/res/values/strings.xml

@ -0,0 +1,293 @@
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<!-- Common -->
<string name="done">@string/translated_button_done</string>
<string name="cancel">@string/missing_cancel</string>
<string name="unknown">@string/translated_send_unknown</string>
<string name="blank"></string>
<!-- -->
<!-- SCREENS -->
<!-- -->
<!-- Screen: Backup -->
<string name="backup_button_done">@string/translated_button_done</string>
<string name="backup_button_primary">@string/custom_translation_verify</string>
<string name="backup_format_birthday_height">@string/custom_translation_birthday</string>
<string name="backup_instruction_store_words">@string/missing_backup_instruction_store_words</string>
<string name="backup_slogan">@string/missing_backup_slogan</string>
<string name="backup_verification_not_implemented">@string/missing_backup_verification_not_implemented</string>
<!-- Screen: Feedback -->
<string name="feedback_hint_1">@string/missing_feedback_hint_1</string>
<string name="feedback_hint_2">@string/missing_feedback_hint_2</string>
<string name="feedback_hint_3">@string/missing_feedback_hint_3</string>
<string name="feedback_question_1">@string/missing_feedback_question_1</string>
<string name="feedback_question_2">@string/missing_feedback_question_2</string>
<string name="feedback_question_3">@string/missing_feedback_question_3</string>
<string name="feedback_thanks">@string/missing_feedback_thanks</string>
<!-- Screen: History -->
<string name="history_address_label">@string/missing_history_address_label</string>
<string name="history_balance_available">@string/translated_balance_available</string>
<string name="history_balance_updating">@string/missing_history_balance_updating</string>
<string name="history_empty_text">@string/missing_history_empty_text</string>
<string name="history_header_transactions">@string/translated_screen_wallethistory</string>
<string name="history_instruction_enter_amount">@string/translated_balance_amounttosend</string>
<!-- Screen: Home -->
<string name="home_balance_available">@string/translated_balance_available</string>
<string name="home_balance_updating">@string/missing_home_balance_updating</string>
<string name="home_banner_expecting">@string/custom_translation_expecting</string>
<string name="home_button_send_disconnected">@string/missing_home_button_send_disconnected</string>
<string name="home_button_send_downloading">@string/custom_translation_syncing</string>
<string name="home_button_send_has_funds">@string/translated_button_sendamount</string>
<string name="home_button_send_idle">@string/missing_home_button_send_idle</string>
<string name="home_button_send_no_funds">@string/translated_balance_nofunds</string>
<string name="home_button_send_scanning">@string/custom_translation_scanning</string>
<string name="home_button_send_updating">@string/missing_home_button_send_updating</string>
<string name="home_button_send_validating">@string/missing_home_button_send_validating</string>
<string name="home_dialog_no_balance_button_positive">@string/missing_home_dialog_no_balance_button_positive</string>
<string name="home_dialog_no_balance_message">@string/missing_home_dialog_no_balance_message</string>
<string name="home_dialog_no_balance_title">@string/translated_balance_nofunds</string>
<string name="home_history_button_text">@string/translated_button_wallethistory</string>
<string name="home_instruction_enter_amount">@string/translated_balance_amounttosend</string>
<string name="home_instruction_fund_now">@string/missing_home_instruction_fund_now</string>
<string name="home_no_balance">@string/translated_balance_nofunds</string>
<string name="home_title">@string/translated_balance_amounttosend</string>
<!-- Screen: Autoshielding prompt -->
<string name="autoshielding_title_text">@string/missing_autoshielding_title_text</string>
<string name="autoshielding_body_text">@string/missing_autoshielding_body_text</string>
<string name="autoshielding_button_positive">@string/missing_autoshielding_button_positive</string>
<string name="autoshielding_button_neutral">@string/missing_autoshielding_button_neutral</string>
<!-- Screen: Balance Detail -->
<string name="balance_detail_button_send_transparent_funds" tools:ignore="MissingTranslation">Shield Transparent Funds</string>
<string name="balance_detail_text_total_title" tools:ignore="MissingTranslation">= TOTAL</string>
<!-- Screen: Landing -->
<string name="landing_backup_skipped_message_1">@string/missing_landing_backup_skipped_message_1</string>
<string name="landing_backup_skipped_message_2">@string/missing_landing_backup_skipped_message_2</string>
<string name="landing_button_backup_skipped_1">@string/missing_landing_button_backup_skipped_1</string>
<string name="landing_button_backup_skipped_2">@string/missing_landing_button_backup_skipped_2</string>
<string name="landing_button_primary">@string/missing_landing_button_primary</string>
<string name="landing_button_primary_create_success">@string/translated_button_backup</string>
<string name="landing_button_progress_create">@string/missing_landing_button_progress_create</string>
<string name="landing_button_secondary">@string/missing_landing_button_secondary</string>
<string name="landing_button_secondary_create_success">@string/translated_button_skip</string>
<string name="landing_button_secondary_import_success">@string/translated_button_skip</string>
<string name="landing_create_success_message">@string/missing_landing_create_success_message</string>
<string name="landing_import_success_message">@string/missing_landing_import_success_message</string>
<string name="landing_import_success_primary_button">@string/translated_button_backup</string>
<string name="landing_title">@string/missing_landing_title</string>
<!-- Screen: Profile -->
<string name="profile_backup_wallet">@string/translated_button_backup_wallet</string>
<string name="profile_rescan_wallet">@string/missing_profile_rescan_wallet</string>
<string name="profile_ecc_wallet">@string/translated_app_name</string>
<string name="profile_see_application_logs">@string/translated_button_applicationlogs</string>
<string name="profile_send_feedback">@string/translated_button_feedback</string>
<string name="profile_share_log_title">@string/missing_profile_share_log_title</string>
<string name="profile_shielded_user">@string/translated_profile_screen</string>
<!-- Screen: Receive -->
<string name="receive_address_label_shielded">@string/translated_address_shielded</string>
<string name="receive_address_title">@string/missing_receive_address_title</string>
<string name="scan_address_title">@string/translated_send_scanqr</string>
<!-- Screen: Scan -->
<string name="scan_invalid_address">@string/missing_scan_invalid_address</string>
<!-- Screen: Send Flow -->
<string name="send_address_hint">@string/translated_label_to</string>
<string name="send_banner_address_unknown">@string/translated_send_unknown</string>
<string name="send_banner_address_user">@string/translated_address_shielded</string>
<string name="send_button_primary">@string/translated_button_send</string>
<string name="send_checkbox_include_address">@string/translated_label_replyto</string>
<string name="send_complete_button_text">@string/translated_button_seedetails</string>
<string name="send_confirmation_prompt">@string/custom_translation_send_confirm</string>
<string name="send_failed_button_text">@string/missing_send_failed_button_text</string>
<string name="send_final_button_primary_back">@string/translated_button_back</string>
<string name="send_final_button_primary_cancel">@string/missing_send_final_button_primary_cancel</string>
<string name="send_final_button_primary_details">@string/translated_button_seedetails</string>
<string name="send_final_button_primary_failed">@string/missing_send_final_button_primary_failed</string>
<string name="send_final_button_primary_retry">@string/missing_send_final_button_primary_retry</string>
<string name="send_final_button_primary_sent">@string/translated_send_sent</string>
<string name="send_final_error_encoding">@string/missing_send_final_error_encoding</string>
<string name="send_final_error_submitting">@string/missing_send_final_error_submitting</string>
<string name="send_final_result_cancelled">@string/translated_send_cancelled</string>
<string name="send_final_sending">@string/custom_translation_sending</string>
<string name="send_final_to">@string/translated_label_to</string>
<string name="send_fund_source_highlight">@string/missing_send_fund_source_highlight</string>
<string name="send_fund_source_prefix">@string/custom_translation_from_your</string>
<string name="send_fund_source_suffix">@string/custom_translation_wallet</string>
<string name="send_hint_input_zcash_address">@string/missing_send_hint_input_zcash_address</string>
<string name="send_hint_input_zcash_amount">@string/translated_balance_amounttosend</string>
<string name="send_history_clipboard">@string/translated_send_onclipboard</string>
<string name="send_history_last">@string/translated_send_lastused</string>
<string name="send_history_last_and_clipboard">@string/missing_send_history_last_and_clipboard</string>
<string name="send_memo_chars_abbreviation">@string/custom_translation_chars</string>
<string name="send_memo_excluded_message">@string/missing_send_memo_excluded_message</string>
<string name="send_memo_hint">@string/missing_send_memo_hint</string>
<string name="send_memo_included_message">@string/missing_send_memo_included_message</string>
<string name="send_pending_button_text">@string/missing_send_pending_button_text</string>
<string name="send_validation_address_invalid">@string/missing_send_validation_address_invalid</string>
<string name="send_validation_address_self">@string/translated_feedback_sameaddress</string>
<string name="send_validation_address_valid_taddr">@string/translated_feedback_transparentaddress</string>
<string name="send_validation_address_valid_zaddr">@string/translated_feedback_shieldedaddress</string>
<string name="send_validation_error_address_invalid">@string/translated_feedback_default</string>
<string name="send_validation_error_amount_minimum">@string/missing_send_validation_error_amount_minimum</string>
<string name="send_validation_error_dust">@string/missing_send_validation_error_dust</string>
<string name="send_validation_error_memo_length">@string/missing_send_validation_error_memo_length</string>
<string name="send_validation_error_no_available_funds">@string/translated_balance_nofunds</string>
<string name="send_validation_error_too_much">@string/missing_send_validation_error_too_much</string>
<string name="send_validation_error_unknown_funds">@string/missing_send_validation_error_unknown_funds</string>
<string name="send_no_z_address_message">To add a memo, enter a shielded address.</string>
<string name="send_more_info">More Info</string>
<!-- Screen: Settings-->
<string name="settings_buttons_restore">@string/missing_settings_buttons_restore</string>
<string name="settings_change_lightwalletd_server">@string/missing_settings_change_lightwalletd_server</string>
<string name="settings_host_helper_text">@string/missing_settings_host_helper_text</string>
<string name="settings_port_helper_text">@string/missing_settings_port_helper_text</string>
<string name="settings_reset">@string/missing_settings_reset</string>
<string name="settings_server_address">@string/missing_settings_server_address</string>
<string name="settings_server_port">@string/missing_settings_server_port</string>
<string name="settings_toast_change_server_failure">@string/missing_settings_toast_change_server_failure</string>
<string name="settings_toast_change_server_success">@string/missing_settings_toast_change_server_success</string>
<string name="settings_update">@string/missing_settings_update</string>
<!-- Screen: Transaction details -->
<string name="transaction_address_paid_you">@string/missing_transaction_address_paid_you</string>
<string name="transaction_address_you_paid">@string/missing_transaction_address_you_paid</string>
<string name="transaction_address_paying">@string/missing_transaction_address_paying</string>
<string name="transaction_block_height_prefix">@string/missing_transaction_block_height_prefix</string>
<string name="transaction_confirmation_count_unavailable">@string/missing_transaction_confirmation_count_unavailable</string>
<string name="transaction_details_button_explore">@string/missing_transaction_details_button_explore</string>
<string name="transaction_details_total">@string/missing_transaction_details_total</string>
<string name="transaction_instruction_tap">@string/missing_transaction_instruction_tap</string>
<string name="transaction_prefix_from">@string/missing_transaction_prefix_from</string>
<string name="transaction_prefix_to">@string/translated_label_to</string>
<string name="transaction_received">@string/missing_transaction_received</string>
<string name="transaction_received_from">@string/missing_transaction_received_from</string>
<string name="transaction_status_confirmed">@string/missing_transaction_status_confirmed</string>
<string name="transaction_status_confirming">@string/missing_transaction_status_confirming</string>
<string name="transaction_status_expired">@string/missing_transaction_status_expired</string>
<string name="transaction_status_pending">@string/missing_transaction_status_pending</string>
<string name="transaction_status_sent">@string/missing_transaction_status_sent</string>
<string name="transaction_story_from_shielded">@string/translated_send_fromshielded</string>
<string name="transaction_story_inbound">@string/missing_transaction_story_inbound</string>
<string name="transaction_story_inbound_total">@string/missing_transaction_story_inbound_total</string>
<string name="transaction_story_network_fee">%1$s network fee</string>
<string name="transaction_story_outbound">@string/missing_transaction_story_outbound</string>
<string name="transaction_story_outbound_total">@string/missing_transaction_story_outbound_total</string>
<string name="transaction_story_to_shielded">@string/missing_transaction_story_to_shielded</string>
<string name="transaction_timestamp_unavailable">@string/missing_transaction_timestamp_unavailable</string>
<string name="transaction_title">@string/missing_transaction_title</string>
<string name="transaction_with_memo">@string/missing_transaction_with_memo</string>
<!-- -->
<!-- Content Descriptions -->
<!-- -->
<string name="content_description_backup_zcash_logo">@string/missing_content_description_backup_zcash_logo</string>
<string name="content_description_history_back">@string/missing_content_description_history_back</string>
<string name="content_description_history_copy">@string/missing_content_description_history_copy</string>
<string name="content_description_home_icon_profile">@string/missing_content_description_home_icon_profile</string>
<string name="content_description_home_icon_scan">@string/missing_content_description_home_icon_scan</string>
<string name="content_description_profile_back">@string/missing_content_description_profile_back</string>
<string name="content_description_profile_copy">@string/missing_content_description_profile_copy</string>
<string name="content_description_profile_settings">@string/missing_content_description_profile_settings</string>
<string name="content_description_profile_zebra">@string/missing_content_description_profile_zebra</string>
<string name="content_description_receive_back">@string/translated_button_back</string>
<string name="content_description_receive_qr_background">@string/missing_content_description_receive_qr_background</string>
<string name="content_description_receive_qr_code">@string/missing_content_description_receive_qr_code</string>
<string name="content_description_receive_qr_logo">@string/missing_content_description_receive_qr_logo</string>
<string name="content_description_scan_back">@string/missing_content_description_scan_back</string>
<string name="content_description_scan_frame">@string/missing_content_description_scan_frame</string>
<string name="content_description_send_arrow">@string/missing_content_description_send_arrow</string>
<string name="content_description_send_back">@string/missing_content_description_send_back</string>
<string name="content_description_send_scan_qr">@string/missing_content_description_send_scan_qr</string>
<string name="content_description_send_selected">@string/missing_content_description_send_selected</string>
<string name="content_description_send_shield">@string/missing_content_description_send_shield</string>
<string name="content_description_send_final_back">@string/missing_content_description_send_final_back</string>
<string name="content_description_settings_back">@string/missing_content_description_settings_back</string>
<string name="content_description_transaction_shield">@string/missing_content_description_transaction_shield</string>
<string name="content_description_transaction_details_back">@string/missing_content_description_transaction_details_back</string>
<string name="content_description_transaction_details_direction">@string/missing_content_description_transaction_details_direction</string>
<string name="content_description_transaction_details_memo">@string/missing_content_description_transaction_details_memo</string>
<string name="content_description_transaction_details_memo_icon">@string/missing_content_description_transaction_details_memo_icon</string>
<!-- -->
<!-- Dialogs -->
<!-- -->
<string name="dialog_error_change_server_button_positive">@string/missing_dialog_error_change_server_button_positive</string>
<string name="dialog_error_change_server_title">@string/missing_dialog_error_change_server_title</string>
<string name="dialog_error_critical_processing_button_negative">@string/missing_dialog_error_critical_processing_button_negative</string>
<string name="dialog_error_critical_processing_button_positive">@string/missing_dialog_error_critical_processing_button_positive</string>
<string name="dialog_error_critical_processing_message">@string/missing_dialog_error_critical_processing_message</string>
<string name="dialog_error_critical_processing_title">@string/missing_dialog_error_critical_processing_title</string>
<string name="dialog_error_critical_link_title">@string/missing_dialog_error_critical_link_title</string>
<string name="dialog_error_critical_link_message">@string/missing_dialog_error_critical_link_message</string>
<string name="dialog_error_invalid_seed_phrase_button_positive">@string/missing_dialog_error_invalid_seed_phrase_button_positive</string>
<string name="dialog_error_invalid_seed_phrase_message">@string/missing_dialog_error_invalid_seed_phrase_message</string>
<string name="dialog_error_invalid_seed_phrase_title">@string/missing_dialog_error_invalid_seed_phrase_title</string>
<string name="dialog_error_processor_critical_button_negative">@string/missing_dialog_error_processor_critical_button_negative</string>
<string name="dialog_error_processor_critical_button_positive">@string/missing_dialog_error_processor_critical_button_positive</string>
<string name="dialog_error_processor_critical_message">@string/missing_dialog_error_processor_critical_message</string>
<string name="dialog_error_processor_critical_title">@string/missing_dialog_error_processor_critical_title</string>
<string name="dialog_error_scan_button_negative">@string/missing_dialog_error_scan_button_negative</string>
<string name="dialog_error_scan_button_positive">@string/missing_dialog_error_scan_button_positive</string>
<string name="dialog_error_scan_failure_button_negative">@string/missing_dialog_error_scan_failure_button_negative</string>
<string name="dialog_error_scan_failure_button_positive">@string/missing_dialog_error_scan_failure_button_positive</string>
<string name="dialog_error_scan_failure_title">@string/missing_dialog_error_scan_failure_title</string>
<string name="dialog_error_scan_title">@string/missing_dialog_error_scan_title</string>
<string name="dialog_error_uninitialized_button_negative">@string/missing_dialog_error_uninitialized_button_negative</string>
<string name="dialog_error_uninitialized_button_positive">@string/missing_dialog_error_uninitialized_button_positive</string>
<string name="dialog_error_uninitialized_message">@string/missing_dialog_error_uninitialized_message</string>
<string name="dialog_error_uninitialized_title">@string/missing_dialog_error_uninitialized_title</string>
<string name="dialog_first_use_view_tx_message">@string/missing_dialog_first_use_view_tx_message</string>
<string name="dialog_first_use_view_tx_negative">@string/missing_dialog_first_use_view_tx_negative</string>
<string name="dialog_first_use_view_tx_positive">@string/missing_dialog_first_use_view_tx_positive</string>
<string name="dialog_first_use_view_tx_title">@string/missing_dialog_first_use_view_tx_title</string>
<string name="dialog_modify_server_button_negative">@string/missing_dialog_modify_server_button_negative</string>
<string name="dialog_modify_server_button_positive">@string/missing_dialog_modify_server_button_positive</string>
<string name="dialog_modify_server_message">@string/missing_dialog_modify_server_message</string>
<string name="dialog_modify_server_title">@string/missing_dialog_modify_server_title</string>
<string name="dialog_rescan_wallet_title">@string/missing_dialog_rescan_wallet_title</string>
<string name="dialog_rescan_wallet_message">@string/missing_dialog_rescan_wallet_message</string>
<string name="dialog_rescan_wallet_button_negative">@string/missing_dialog_rescan_wallet_button_negative</string>
<string name="dialog_rescan_wallet_button_neutral">@string/missing_dialog_rescan_wallet_button_neutral</string>
<string name="dialog_rescan_wallet_button_positive">@string/missing_dialog_rescan_wallet_button_positive</string>
<string name="dialog_not_again">@string/missing_dialog_not_again</string>
<string name="dialog_nuke_wallet_button_negative">@string/missing_dialog_nuke_wallet_button_negative</string>
<string name="dialog_nuke_wallet_button_positive">@string/missing_dialog_nuke_wallet_button_positive</string>
<string name="dialog_nuke_wallet_message">@string/missing_dialog_nuke_wallet_message</string>
<string name="dialog_nuke_wallet_title">@string/missing_dialog_nuke_wallet_title</string>
<!-- -->
<!-- Misc -->
<!-- -->
<string name="app_name">@string/translated_app_name</string>
<string name="biometric_prompt_title">@string/translated_send_securityauth</string>
<string name="biometric_backup_phrase_title">@string/missing_biometric_backup_phrase_title</string>
<string name="biometric_backup_phrase_description">@string/missing_biometric_backup_phrase_description</string>
<string name="camera_permission_denied">@string/missing_camera_permission_denied</string>
<string name="error_launch_url">@string/missing_error_launch_url</string>
<string name="key_backspace"><![CDATA[<]]></string>
<string name="key_decimal">.</string>
<!-- <string name="key_decimal">@string/custom_translation_decimal_key</string>-->
<string name="mixpanel_project">a178e1ef062133fc121079cb12fa43c7</string>
<string name="format_date_time">@string/custom_translation_format_date_time_brief</string>
<string name="format_transaction_history_date_time">@string/transaction_history_format_date_time_brief</string>
<string name="transaction_history_format_date_time_brief" tools:ignore="MissingTranslation">MM/dd/yy h:mma</string>
</resources>

102
app/src/main/res/values/styles.xml

@ -0,0 +1,102 @@
<resources>
<!-- Application theme -->
<style name="ZcashBaseTheme" parent="Theme.MaterialComponents.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="shapeAppearanceSmallComponent">@style/Zcash.ShapeAppearance.SmallComponent</item>
<item name="shapeAppearanceMediumComponent">@style/Zcash.ShapeAppearance.MediumComponent</item>
</style>
<style name="ZcashTheme" parent="ZcashBaseTheme"/>
<style name="ZcashTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<!-- Widgets -->
<style name="Zcash.TextView.NumberPad" parent="Widget.AppCompat.TextView">
<item name="android:textAppearance">@style/Zcash.TextAppearance.NumberPad</item>
<item name="android:background">@drawable/selector_pressed_ripple_circle</item>
</style>
<style name="Zcash.Button" parent="Widget.MaterialComponents.Button">
<item name="backgroundTint">@color/colorPrimary</item>
<item name="android:textColor">@color/text_dark</item>
</style>
<style name="Zcash.Button.White">
<item name="backgroundTint">@android:color/white</item>
</style>
<style name="Zcash.Button.OutlinedButton" parent="Widget.MaterialComponents.Button.OutlinedButton">
<item name="strokeColor">@color/zcashWhite</item>
</style>
<style name="Zcash.Button.OutlinedButton.Primary" parent="Widget.MaterialComponents.Button.OutlinedButton">
<item name="strokeColor">@color/colorPrimary</item>
</style>
<style name="Zcash.TextInputLayout" parent="Widget.MaterialComponents.TextInputLayout.FilledBox">
<item name="boxBackgroundColor">@android:color/transparent</item>
</style>
<!-- Text Appearances -->
<style name="Zcash.TextAppearance.NumberPad" parent="TextAppearance.MaterialComponents.Body1">
<item name="android:textSize">30dp</item>
<item name="android:textColor">@color/selector_button_text_light</item>
</style>
<style name="Zcash.TextAppearance.AddressPart" parent="TextAppearance.AppCompat">
<item name="android:fontFamily">@font/inconsolata</item>
<item name="fontFamily">@font/inconsolata</item>
<item name="android:textSize">20dp</item>
<item name="android:textColor">@color/text_light</item>
</style>
<style name="Zcash.TextAppearance.Zec" parent="TextAppearance.AppCompat">
<item name="android:fontFamily">@font/zboto</item>
<item name="fontFamily">@font/zboto</item>
<item name="android:textSize">22sp</item>
<item name="android:textColor">@color/text_light</item>
</style>
<style name="Zcash.TextAppearance.Headline6" parent="TextAppearance.MaterialComponents.Headline6">
<item name="fontFamily">sans-serif</item>
<item name="android:fontFamily">sans-serif</item>
</style>
<!-- Shape Appearances -->
<!-- General: buttons -->
<style name="Zcash.ShapeAppearance.SmallComponent" parent="ShapeAppearance.MaterialComponents.SmallComponent">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">8dp</item>
</style>
<!-- General: dialogs -->
<style name="Zcash.ShapeAppearance.MediumComponent" parent="ShapeAppearance.MaterialComponents.MediumComponent">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">12dp</item>
</style>
<!-- Component -->
<style name="Zcash.ShapeAppearance.TextInputLayout" parent="ShapeAppearance.MaterialComponents.SmallComponent">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">0dp</item>
</style>
<!-- Theme Overlays -->
<style name="Zcash.Overlay.TextInputLayout" parent="ThemeOverlay.MaterialComponents">
<item name="shapeAppearanceSmallComponent">@style/Zcash.ShapeAppearance.TextInputLayout</item>
<item name="textInputStyle">@style/Zcash.TextInputLayout</item>
</style>
</resources>

75
app/src/main/res/values/translated.xml

@ -0,0 +1,75 @@
<resources>
<string name="translated_app_name">SilentDragon</string>
<string name="translated_loadingtext">Loading</string>
<string name="translated_welcometext">Welcome</string>
<string name="translated_fiattext">Fiat</string>
<string name="translated_title_backupseed">Your backup seed</string>
<string name="translated_copy_backupseed">We recommend a paper backup and a password vault.</string>
<string name="translated_feedback_walletbackupstatus">Your wallet needs to be backed up.</string>
<string name="translated_seed_verify">Verify your backup.</string>
<string name="translated_seed_verifytext">Please fill out the missing seed words to verify your backup.</string>
<string name="translated_seed_restore">Restoring from a Backup</string>
<string name="translated_seed_restoretext">You will need to enter all 24 seed words in order.</string>
<string name="translated_seed_reminder">Your funds are at risk!</string>
<string name="translated_seed_remindertext">Remember, with Zcash YOU are the bank. Anyone with your seed phrase has access to your wallet.</string>
<string name="translated_seed_remindertext2">You should back this up immediately as no one else can restore it for you.</string>
<string name="translated_seed_accepted">Seed accepted, scanning!</string>
<string name="translated_seed_birthdaytext">We are scanning the blockchain for transactions pertaining to your wallet. If you can provide a birthday date for this wallet we can speed it up.</string>
<string name="translated_seed_enterdate">Enter Date</string>
<string name="translated_seed_birthday">Birthday</string>
<string name="translated_button_dontknow">I don\'t know</string>
<string name="translated_button_backup">Backup</string>
<string name="translated_button_skip">Skip for now</string>
<string name="translated_balance_screen">Balance Screen</string>
<string name="translated_balance_amounttosend">Enter an amount to send</string>
<string name="translated_balance_expecting">expecting XX ZEC</string>
<string name="translated_balance_available">Available</string>
<string name="translated_balance_nofunds">No funds available</string>
<string name="translated_balance_syncing">Syncing … %</string>
<string name="translated_balance_scanning">Scanning … %</string>
<string name="translated_button_sendamount">Send Amount</string>
<string name="translated_button_wallethistory">Wallet History</string>
<string name="translated_address_screen">Your Address</string>
<string name="translated_address_shielded">Your Shielded Address</string>
<string name="translated_address_transparent">Your Transparent Address</string>
<string name="translated_feedback_addresscopied">Address Copied!</string>
<string name="translated_button_copytoclipboard">Copy to clipboard</string>
<string name="translated_profile_screen">Shielded Address</string>
<string name="translated_button_feedback">Send Feedback</string>
<string name="translated_button_backup_wallet">Backup Wallet</string>
<string name="translated_button_applicationlogs">See Application Logs</string>
<string name="translated_button_close">Close</string>
<string name="translated_send_screen">Send Screen</string>
<string name="translated_button_send">Send</string>
<string name="translated_send_fromshielded">from your shielded wallet</string>
<string name="translated_send_fromtransparent">from your transparent wallet</string>
<string name="translated_send_fromboth">from both wallets</string>
<string name="translated_label_to">To</string>
<string name="translated_feedback_default">Enter a valid Zcash address</string>
<string name="translated_feedback_shieldedaddress">This is a valid shielded address</string>
<string name="translated_feedback_transparentaddress">This is a valid transparent address</string>
<string name="translated_feedback_sameaddress">Warning, this appears to be your address!</string>
<string name="translated_feedback_invalidaddress">Warning, this address is not valid!</string>
<string name="translated_label_memo">Memo</string>
<string name="translated_label_charactercount">chars (characters)</string>
<string name="translated_label_replyto">include reply-to</string>
<string name="translated_send_onclipboard">On Clipboard</string>
<string name="translated_send_lastused">Last Used</string>
<string name="translated_send_unknown">Unknown</string>
<string name="translated_send_scanqr">Scan Recipient Address</string>
<string name="translated_send_securityauth">Authenticate to send</string>
<string name="translated_send_securityauth2">Please confirm you want to send XX ZEC to</string>
<string name="translated_send_sending">Sending XX ZEC to</string>
<string name="translated_send_cancelled">Cancelled</string>
<string name="translated_send_sent">Sent!</string>
<string name="translated_button_done">Done</string>
<string name="translated_button_seedetails">See Details</string>
<string name="translated_button_back">Go Back</string>
<string name="translated_screen_wallethistory">Your Wallet History</string>
</resources>
<!-- ISSUES:
- button_backup isn't unique
-->

2053
app/src/main/res/values/word_list_bip39.xml

File diff suppressed because it is too large

5
app/src/main/res/xml/file_paths.xml

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<!-- Share files from a dedicated folder on the internal files storage. -->
<files-path name="logs" path="logs/"/>
</paths>

BIN
app/src/qa/res/mipmap-hdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
app/src/qa/res/mipmap-hdpi/ic_launcher_round.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
app/src/qa/res/mipmap-mdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
app/src/qa/res/mipmap-mdpi/ic_launcher_round.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
app/src/qa/res/mipmap-xhdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
app/src/qa/res/mipmap-xhdpi/ic_launcher_round.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

BIN
app/src/qa/res/mipmap-xxhdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
app/src/qa/res/mipmap-xxhdpi/ic_launcher_round.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
app/src/qa/res/mipmap-xxxhdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
app/src/qa/res/mipmap-xxxhdpi/ic_launcher_round.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

6
app/src/qa/res/values/colors.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="app_icon_background_0">#FFB900</color>
<color name="app_icon_background_1">#664B00</color>
<color name="app_icon_foreground">#FF000000</color>
</resources>

3
app/src/qa/res/values/strings.xml

@ -0,0 +1,3 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name" tools:ignore="MissingTranslation">ECC (QA)</string>
</resources>

54
app/src/test/java/cash/z/ecc/android/ScratchPad.kt

@ -0,0 +1,54 @@
package cash.z.ecc.android
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.scanReduce
import kotlinx.coroutines.runBlocking
import org.junit.Test
class ScratchPad {
val t get() = System.currentTimeMillis()
var t0 = 0L
val Δt get() = t - t0
@Test
fun testMarblesCombine() = runBlocking {
var started = false
val flow = flowOf(1, 2, 3, 4, 5, 6, 7, 8, 9).onEach {
delay(100)
if (!started) {
t0 = t
started = true
}
println("$Δt\temitting $it")
}
val flow2 = flowOf("a", "b", "c", "d", "e", "f").onEach { delay(150); println("$Δt\temitting $it") }
val flow3 = flowOf("A", "B").onEach { delay(450); println("$Δt\temitting $it") }
combine(flow, flow2, flow3) { i, s, t -> "$i$s$t" }.onStart {
t0 = t
}.collect {
// if (!started) {
// println("$Δt until first emission")
// t0 = t
// started = true
// }
println("$Δt\t$it") // Will print "1a 2a 2b 2c"
}
}
@Test
fun testMarblesScan() = runBlocking {
val flow = flowOf(1, 2, 3, 4, 5)
flow.scanReduce { accumulator, value ->
println("was: $accumulator now: $value")
value
}.collect {
println("got $it")
}
}
}

112
app/src/test/java/cash/z/ecc/android/SendViewModelTest.kt

@ -0,0 +1,112 @@
package cash.z.ecc.android
import cash.z.ecc.android.feedback.Feedback
import cash.z.ecc.android.sdk.db.entity.PendingTransaction
import cash.z.ecc.android.sdk.db.entity.isCreated
import cash.z.ecc.android.sdk.db.entity.isCreating
import cash.z.ecc.android.sdk.db.entity.isMined
import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
import cash.z.ecc.android.ui.send.SendViewModel
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.newSingleThreadContext
import kotlinx.coroutines.test.setMain
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.mockito.Spy
class SendViewModelTest {
@Mock lateinit var creatingTx: PendingTransaction
@Mock lateinit var createdTx: PendingTransaction
@Mock lateinit var submittedTx: PendingTransaction
@Mock lateinit var minedTx: PendingTransaction
@Mock
lateinit var feedback: Feedback
@Spy
lateinit var sendViewModel: SendViewModel
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
Dispatchers.setMain(newSingleThreadContext("Main thread"))
whenever(creatingTx.id).thenReturn(7)
whenever(creatingTx.submitAttempts).thenReturn(0)
whenever(createdTx.id).thenReturn(7)
whenever(createdTx.raw).thenReturn(byteArrayOf(0x1))
whenever(submittedTx.id).thenReturn(7)
whenever(submittedTx.raw).thenReturn(byteArrayOf(0x1))
whenever(submittedTx.submitAttempts).thenReturn(1)
whenever(minedTx.id).thenReturn(7)
whenever(minedTx.raw).thenReturn(byteArrayOf(0x1))
whenever(minedTx.submitAttempts).thenReturn(1)
whenever(minedTx.minedHeight).thenReturn(500_001)
sendViewModel.feedback = feedback
}
@Test
fun testUpdateMetrics_creating() {
// doNothing().whenever(sendViewModel).report(any())
// assertEquals(true, creatingTx.isCreating())
// sendViewModel.updateMetrics(creatingTx)
//
// verify(sendViewModel).report("7.metric.tx.initialized")
// assertEquals(1, sendViewModel.metrics.size)
// verifyZeroInteractions(feedback)
}
@Test
fun testUpdateMetrics_created() {
assertEquals(false, createdTx.isCreating())
assertEquals(true, createdTx.isCreated())
// sendViewModel.updateMetrics(creatingTx)
// sendViewModel.updateMetrics(createdTx)
// Thread.sleep(100)
// println(sendViewModel.metrics)
//
// verify(sendViewModel).report("7.metric.tx.created")
// assertEquals(1, sendViewModel.metrics.size)
}
@Test
fun testUpdateMetrics_submitted() {
assertEquals(false, submittedTx.isCreating())
assertEquals(false, submittedTx.isCreated())
assertEquals(true, submittedTx.isSubmitSuccess())
// sendViewModel.updateMetrics(creatingTx)
// sendViewModel.updateMetrics(createdTx)
// sendViewModel.updateMetrics(submittedTx)
assertEquals(5, sendViewModel.metrics.size)
Thread.sleep(100)
assertEquals(1, sendViewModel.metrics.size)
verify(feedback).report(sendViewModel.metrics.values.first())
}
@Test
fun testUpdateMetrics_mined() {
assertEquals(true, minedTx.isMined())
assertEquals(true, minedTx.isSubmitSuccess())
// sendViewModel.updateMetrics(creatingTx)
// sendViewModel.updateMetrics(createdTx)
// sendViewModel.updateMetrics(submittedTx)
// sendViewModel.updateMetrics(minedTx)
// assertEquals(7, sendViewModel.metrics.size)
//
// Thread.sleep(100)
// assertEquals(0, sendViewModel.metrics.size)
}
}

1
app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker

@ -0,0 +1 @@
mock-maker-inline

4
app/src/zcashmainnet/res/values/integers.xml

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="zcash_network_id">1</integer>
</resources>

5
app/src/zcashmainnet/res/values/strings.xml

@ -0,0 +1,5 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="api_block_explorer">https://explorer.hush.is/tx/%1$s</string>
<string name="symbol" translatable="false">HUSH</string>
<string name="play_store_url" translatable="false">https://hush.is</string>
</resources>

4
app/src/zcashtestnet/res/values/integers.xml

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="zcash_network_id">0</integer>
</resources>

6
app/src/zcashtestnet/res/values/strings.xml

@ -0,0 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Currently, blockchair does not support testnet so we use our explorer.z.cash for now -->
<string name="api_block_explorer" translatable="false">https://explorer.testnet.z.cash/tx/%1$s</string>
<string name="symbol" translatable="false">TAZ</string>
<string name="play_store_url" translatable="false">https://play.google.com/apps/internaltest/4699113723491212355</string>
</resources>

BIN
app/src/zcashtestnetQa/res/mipmap-hdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
app/src/zcashtestnetQa/res/mipmap-hdpi/ic_launcher_round.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
app/src/zcashtestnetQa/res/mipmap-mdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
app/src/zcashtestnetQa/res/mipmap-mdpi/ic_launcher_round.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
app/src/zcashtestnetQa/res/mipmap-xhdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
app/src/zcashtestnetQa/res/mipmap-xhdpi/ic_launcher_round.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

BIN
app/src/zcashtestnetQa/res/mipmap-xxhdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
app/src/zcashtestnetQa/res/mipmap-xxhdpi/ic_launcher_round.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
app/src/zcashtestnetQa/res/mipmap-xxxhdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
app/src/zcashtestnetQa/res/mipmap-xxxhdpi/ic_launcher_round.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

6
app/src/zcashtestnetQa/res/values/colors.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="app_icon_background_0">#FFB900</color>
<color name="app_icon_background_1">#664B00</color>
<color name="app_icon_foreground">#FF000000</color>
</resources>

3
app/src/zcashtestnetQa/res/values/strings.xml

@ -0,0 +1,3 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name" tools:ignore="MissingTranslation">ECC Testnet</string>
</resources>

23
build-convention/build.gradle.kts

@ -0,0 +1,23 @@
import org.jetbrains.kotlin.konan.properties.loadProperties
plugins {
`kotlin-dsl`
}
buildscript {
dependencyLocking {
lockAllConfigurations()
}
}
dependencyLocking {
lockAllConfigurations()
}
// Per conversation in the KotlinLang Slack, Gradle uses Java 8 compatibility internally
// for all build scripts.
// https://kotlinlang.slack.com/archives/C19FD9681/p1636632870122900?thread_ts=1636572288.117000&cid=C19FD9681
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

45
build-convention/buildscript-gradle.lockfile

@ -0,0 +1,45 @@
# This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
com.github.gundy:semver4j:0.16.4=classpath
com.google.code.findbugs:jsr305:3.0.2=classpath
com.google.code.gson:gson:2.8.9=classpath
com.google.errorprone:error_prone_annotations:2.3.4=classpath
com.google.guava:failureaccess:1.0.1=classpath
com.google.guava:guava:29.0-jre=classpath
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=classpath
com.google.j2objc:j2objc-annotations:1.3=classpath
de.undercouch:gradle-download-task:4.1.1=classpath
net.java.dev.jna:jna:5.6.0=classpath
org.checkerframework:checker-qual:2.11.1=classpath
org.gradle.kotlin.kotlin-dsl:org.gradle.kotlin.kotlin-dsl.gradle.plugin:2.3.3=classpath
org.gradle.kotlin:gradle-kotlin-dsl-plugins:2.3.3=classpath
org.jetbrains.intellij.deps:trove4j:1.0.20200330=classpath
org.jetbrains.kotlin:kotlin-android-extensions:1.6.21=classpath
org.jetbrains.kotlin:kotlin-annotation-processing-gradle:1.6.21=classpath
org.jetbrains.kotlin:kotlin-build-common:1.6.21=classpath
org.jetbrains.kotlin:kotlin-compiler-embeddable:1.6.21=classpath
org.jetbrains.kotlin:kotlin-compiler-runner:1.6.21=classpath
org.jetbrains.kotlin:kotlin-daemon-client:1.6.21=classpath
org.jetbrains.kotlin:kotlin-daemon-embeddable:1.6.21=classpath
org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.6.21=classpath
org.jetbrains.kotlin:kotlin-gradle-plugin-model:1.6.21=classpath
org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.21=classpath
org.jetbrains.kotlin:kotlin-klib-commonizer-api:1.6.21=classpath
org.jetbrains.kotlin:kotlin-native-utils:1.6.21=classpath
org.jetbrains.kotlin:kotlin-project-model:1.6.21=classpath
org.jetbrains.kotlin:kotlin-sam-with-receiver:1.6.21=classpath
org.jetbrains.kotlin:kotlin-scripting-common:1.6.21=classpath
org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.6.21=classpath
org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.6.21=classpath
org.jetbrains.kotlin:kotlin-scripting-jvm:1.6.21=classpath
org.jetbrains.kotlin:kotlin-stdlib-common:1.6.21=classpath
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.21=classpath
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.21=classpath
org.jetbrains.kotlin:kotlin-stdlib:1.6.21=classpath
org.jetbrains.kotlin:kotlin-tooling-metadata:1.6.21=classpath
org.jetbrains.kotlin:kotlin-util-io:1.6.21=classpath
org.jetbrains.kotlin:kotlin-util-klib:1.6.21=classpath
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.0=classpath
org.jetbrains:annotations:13.0=classpath
empty=

25
build-convention/gradle.lockfile

@ -0,0 +1,25 @@
# This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
net.java.dev.jna:jna:5.6.0=kotlinCompilerClasspath
org.jetbrains.intellij.deps:trove4j:1.0.20200330=kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-compiler-embeddable:1.6.21=kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:1.6.21=kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.6.21=kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-gradle-plugin-model:1.6.21=kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-native-utils:1.6.21=kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-project-model:1.6.21=kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-reflect:1.6.21=compileClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-sam-with-receiver:1.6.21=kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-script-runtime:1.6.21=kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-scripting-common:1.6.21=kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.6.21=kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.6.21=kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-scripting-jvm:1.6.21=kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-stdlib-common:1.6.21=compileClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.21=compileClasspath
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.21=compileClasspath
org.jetbrains.kotlin:kotlin-stdlib:1.6.21=compileClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-util-io:1.6.21=kotlinCompilerPluginClasspathMain
org.jetbrains:annotations:13.0=compileClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain
empty=annotationProcessor,runtimeClasspath

31
build-convention/settings.gradle.kts

@ -0,0 +1,31 @@
@Suppress("UnstableApiUsage")
dependencyResolutionManagement {
repositories {
val isRepoRestrictionEnabled = true
google {
if (isRepoRestrictionEnabled) {
content {
includeGroup("androidx.navigation")
includeGroup("com.android.tools")
includeGroup("com.google.testing.platform")
includeGroupByRegex("androidx.*")
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.android\\.tools.*")
}
}
}
mavenCentral {
if (isRepoRestrictionEnabled) {
content {
excludeGroup("androidx.navigation")
excludeGroup("com.android.tools")
excludeGroup("com.google.testing.platform")
excludeGroupByRegex("androidx.*")
excludeGroupByRegex("com\\.android.*")
excludeGroupByRegex("com\\.android\\.tools.*")
}
}
}
}
}

44
build-convention/src/main/kotlin/zcash.ktlint-conventions.gradle.kts

@ -0,0 +1,44 @@
plugins {
id("java")
}
val ktlint by configurations.creating
dependencies {
ktlint("com.pinterest:ktlint:${project.property("KTLINT_VERSION")}") {
attributes {
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named<Bundling>(Bundling.EXTERNAL))
}
}
}
tasks {
val editorConfigFile = rootProject.file(".editorconfig")
val ktlintArgs = listOf("**/src/**/*.kt", "!**/build/**.kt", "--editorconfig=$editorConfigFile")
register("ktlint", org.gradle.api.tasks.JavaExec::class) {
description = "Check code style with ktlint"
classpath = ktlint
mainClass.set("com.pinterest.ktlint.Main")
args = ktlintArgs
}
register("ktlintFormat", org.gradle.api.tasks.JavaExec::class) {
// Workaround for ktlint bug; force to run on an older JDK
// https://github.com/pinterest/ktlint/issues/1274
javaLauncher.set(javaToolchains.launcherFor {
languageVersion.set(JavaLanguageVersion.of(JavaVersion.VERSION_11.majorVersion))
})
description = "Apply code style formatting with ktlint"
classpath = ktlint
mainClass.set("com.pinterest.ktlint.Main")
args = listOf("-F") + ktlintArgs
}
}
java {
val javaVersion = JavaVersion.toVersion(project.property("ANDROID_JVM_TARGET").toString())
sourceCompatibility = javaVersion
targetCompatibility = javaVersion
}

47
build.gradle.kts

@ -0,0 +1,47 @@
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:7.3.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${cash.z.ecc.android.Deps.kotlinVersion}")
classpath("com.bugsnag:bugsnag-android-gradle-plugin:4.7.5")
classpath("androidx.navigation:navigation-safe-args-gradle-plugin:${cash.z.ecc.android.Deps.navigationVersion}")
}
}
plugins {
id("com.github.ben-manes.versions")
id("zcash.ktlint-conventions")
}
defaultTasks("clean", "installZcashmainnetRelease")
tasks {
// named<Delete>("clean") {
// rootProject.buildDir.deleteRecursively()
// }
withType<com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask> {
gradleReleaseChannel = "current"
resolutionStrategy {
componentSelection {
all {
if (isNonStable(candidate.version) && !isNonStable(currentVersion)) {
reject("Unstable")
}
}
}
}
}
}
val unstableKeywords = listOf("alpha", "beta", "rc", "m", "ea", "build")
fun isNonStable(version: String): Boolean {
val versionLowerCase = version.toLowerCase()
return unstableKeywords.any { versionLowerCase.contains(it) }
}

15
buildSrc/build.gradle.kts

@ -0,0 +1,15 @@
plugins {
`kotlin-dsl-base`
}
repositories {
mavenCentral()
}
// Per conversation in the KotlinLang Slack, Gradle uses Java 8 compatibility internally
// for all build scripts.
// https://kotlinlang.slack.com/archives/C19FD9681/p1636632870122900?thread_ts=1636572288.117000&cid=C19FD9681
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

117
buildSrc/src/main/java/cash/z/ecc/android/Dependencies.kt

@ -0,0 +1,117 @@
package cash.z.ecc.android
object Deps {
// For use in the top-level build.gradle which gives an error when provided
// `Deps.Kotlin.version` directly
const val kotlinVersion = "1.7.20"
const val navigationVersion = "2.5.2"
const val compileSdkVersion = 31
const val minSdkVersion = 21
const val targetSdkVersion = 30
const val versionName = "1.0.0-alpha74"
const val versionCode = 1_00_00_174 // last digits are alpha(0XX) beta(2XX) rc(4XX) release(8XX). Ex: 1_08_04_401 is an release candidate build of version 1.8.4 and 1_08_04_800 would be the final release.
const val packageName = "cash.z.ecc.android"
object AndroidX {
const val ANNOTATION = "androidx.annotation:annotation:1.3.0-alpha01"
const val APPCOMPAT = "androidx.appcompat:appcompat:1.4.0-alpha02"
const val BIOMETRICS = "androidx.biometric:biometric:1.2.0-alpha03"
const val CONSTRAINT_LAYOUT = "androidx.constraintlayout:constraintlayout:2.1.0-beta02"
const val CORE_KTX = "androidx.core:core-ktx:1.6.0"
const val FRAGMENT_KTX = "androidx.fragment:fragment-ktx:1.3.6"
const val LEGACY = "androidx.legacy:legacy-support-v4:1.0.0"
const val MULTIDEX = "androidx.multidex:multidex:2.0.1"
const val PAGING = "androidx.paging:paging-runtime-ktx:2.1.2"
const val RECYCLER = "androidx.recyclerview:recyclerview:1.2.1"
object CameraX : Version("1.1.0-alpha05") {
val CAMERA2 = "androidx.camera:camera-camera2:$version"
val CORE = "androidx.camera:camera-core:$version"
val LIFECYCLE = "androidx.camera:camera-lifecycle:$version"
object View : Version("1.0.0-alpha27") {
val EXT = "androidx.camera:camera-extensions:$version"
val VIEW = "androidx.camera:camera-view:$version"
}
}
object Lifecycle : Version("2.4.0-alpha02") {
val LIFECYCLE_RUNTIME_KTX = "androidx.lifecycle:lifecycle-runtime-ktx:$version"
}
object Navigation : Version(navigationVersion) {
val FRAGMENT_KTX = "androidx.navigation:navigation-fragment-ktx:$version"
val UI_KTX = "androidx.navigation:navigation-ui-ktx:$version"
}
object Room : Version("2.3.0") {
val ROOM_COMPILER = "androidx.room:room-compiler:$version"
val ROOM_KTX = "androidx.room:room-ktx:$version"
}
}
object Google {
// solves error: Duplicate class com.google.common.util.concurrent.ListenableFuture found in modules jetified-guava-26.0-android.jar (com.google.guava:guava:26.0-android) and listenablefuture-1.0.jar (com.google.guava:listenablefuture:1.0)
// per this recommendation from Chris Povirk, given guava's decision to split ListenableFuture away from Guava: https://groups.google.com/d/msg/guava-discuss/GghaKwusjcY/bCIAKfzOEwAJ
const val GUAVA = "com.google.guava:guava:27.0.1-android"
const val MATERIAL = "com.google.android.material:material:1.4.0-rc01"
}
object Grpc : Version("1.37.0") {
val ANDROID = "io.grpc:grpc-android:$version"
val OKHTTP = "io.grpc:grpc-okhttp:$version"
val PROTOBUG = "io.grpc:grpc-protobuf-lite:$version"
val STUB = "io.grpc:grpc-stub:$version"
}
object Analytics { // for dogfooding/crash-reporting/feedback only on internal team builds
val BUGSNAG = "com.bugsnag:bugsnag-android:5.9.4"
val MIXPANEL = "com.mixpanel.android:mixpanel-android:5.6.3"
}
object JavaX {
const val INJECT = "javax.inject:javax.inject:1"
const val JAVA_ANNOTATION = "javax.annotation:javax.annotation-api:1.3.2"
}
object Kotlin : Version(kotlinVersion) {
val STDLIB = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$version"
val REFLECT = "org.jetbrains.kotlin:kotlin-reflect:$version"
object Coroutines : Version("1.6.4") {
val ANDROID = "org.jetbrains.kotlinx:kotlinx-coroutines-android:$version"
val CORE = "org.jetbrains.kotlinx:kotlinx-coroutines-core:$version"
val TEST = "org.jetbrains.kotlinx:kotlinx-coroutines-test:$version"
}
}
object Zcash {
const val ANDROID_WALLET_PLUGINS = "cash.z.ecc.android:zcash-android-wallet-plugins:1.0.0"
const val KOTLIN_BIP39 = "cash.z.ecc.android:kotlin-bip39:1.0.4"
/* SDK uses mavenLocal build with HUSH customizations for now
Run the following from Android SDK path to publish SDK locally
./gradlew clean
./gradlew build
./gradlew build publishToMavenLocal
*/
const val SDK = "cash.z.ecc.android:zcash-android-sdk:1.9.0-beta01-SNAPSHOT"
}
object Misc {
const val LOTTIE = "com.airbnb.android:lottie:3.7.0"
const val CHIPS = "com.github.gmale:chips-input-layout:2.3.4"
object Plugins {
const val SECURE_STORAGE = "com.github.gmale:secure-storage-android:0.0.3"//"de.adorsys.android:securestoragelibrary:1.2.2"
const val QR_SCANNER = "com.google.zxing:core:3.4.1"
}
}
object Test {
const val JUNIT = "junit:junit:4.13.2"
const val MOKITO = "org.mockito:mockito-android:3.12.4"
const val MOKITO_KOTLIN = "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
object Android {
const val CORE = "androidx.test:core:1.4.0"
const val RULES = "androidx.test:rules:1.4.0"
const val JUNIT = "androidx.test.ext:junit:1.1.3"
const val FRAGMENT = "androidx.fragment:fragment-testing:1.4.0-alpha08"
const val ESPRESSO = "androidx.test.espresso:espresso-core:3.4.0"
const val ESPRESSO_INTENTS = "androidx.test.espresso:espresso-intents:3.4.0"
const val NAVIGATION = "androidx.navigation:navigation-testing:2.3.0-alpha01"
}
}
}
open class Version(@JvmField val version: String)

1
feedback/.gitignore

@ -0,0 +1 @@
/build

43
feedback/build.gradle

@ -0,0 +1,43 @@
import cash.z.ecc.android.Deps
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion Deps.compileSdkVersion
defaultConfig {
minSdkVersion Deps.minSdkVersion
targetSdkVersion Deps.targetSdkVersion
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles 'consumer-rules.pro'
}
kotlinOptions {
freeCompilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
namespace 'cash.z.ecc.android.feedback'
}
dependencies {
implementation Deps.Kotlin.STDLIB
implementation Deps.AndroidX.APPCOMPAT
implementation Deps.AndroidX.CORE_KTX
implementation Deps.Kotlin.Coroutines.CORE
implementation Deps.Kotlin.Coroutines.TEST
testImplementation Deps.Test.JUNIT
}

0
feedback/consumer-rules.pro

21
feedback/proguard-rules.pro

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# 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

2
feedback/src/main/AndroidManifest.xml

@ -0,0 +1,2 @@
<manifest
xmlns:android="http://schemas.android.com/apk/res/android" />

284
feedback/src/main/java/cash/z/ecc/android/feedback/Feedback.kt

@ -0,0 +1,284 @@
package cash.z.ecc.android.feedback
import cash.z.ecc.android.feedback.util.CompositeJob
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import java.io.PrintWriter
import java.io.StringWriter
import kotlin.coroutines.coroutineContext
// There are deprecations with the use of BroadcastChannel
@OptIn(ObsoleteCoroutinesApi::class)
class Feedback(capacity: Int = 256) {
lateinit var scope: CoroutineScope
private set
private val _metrics = BroadcastChannel<Metric>(capacity)
private val _actions = BroadcastChannel<Action>(capacity)
private var onStartListeners: MutableList<() -> Unit> = mutableListOf()
private val jobs = CompositeJob()
val metrics: Flow<Metric> = _metrics.asFlow()
val actions: Flow<Action> = _actions.asFlow()
/**
* Verifies that this class is not leaking anything. Checks that all underlying channels are
* closed and all launched reporting jobs are inactive.
*/
val isStopped get() = ensureScope() && _metrics.isClosedForSend && _actions.isClosedForSend && !scope.isActive && !jobs.isActive()
/**
* Starts this feedback as a child of the calling coroutineContext, meaning when that context
* ends, this feedback's scope and anything it launced will cancel. Note that the [metrics] and
* [actions] channels will remain open unless [stop] is also called on this instance.
*/
suspend fun start(): Feedback {
if(::scope.isInitialized) {
return this
}
scope = CoroutineScope(Dispatchers.IO + SupervisorJob(coroutineContext[Job]))
invokeOnCompletion {
_metrics.close()
_actions.close()
}
onStartListeners.forEach { it() }
onStartListeners.clear()
return this
}
fun invokeOnCompletion(block: CompletionHandler) {
ensureScope()
scope.coroutineContext[Job]!!.invokeOnCompletion(block)
}
/**
* Invokes the given callback after the scope has been initialized or immediately, if the scope
* has already been initialized. This is used by [FeedbackCoordinator] and things like it that
* want to immediately begin collecting the metrics/actions flows because any emissions that
* occur before subscription are dropped.
*/
fun onStart(onStartListener: () -> Unit) {
if (::scope.isInitialized) {
onStartListener()
} else {
onStartListeners.add(onStartListener)
}
}
/**
* Stop this instance and close all reporting channels. This function will first wait for all
* in-flight reports to complete.
*/
suspend fun stop() {
// expose instances where stop is being called before start occurred.
ensureScope()
await()
scope.cancel()
}
/**
* Suspends until all in-flight reports have completed. This is automatically called before
* [stop].
*/
suspend fun await() {
jobs.await()
}
/**
* Measures the given block of code by surrounding it with time metrics and the reporting the
* result.
*
* Don't measure code that launches coroutines, instead measure the code within the coroutine
* that gets launched. Otherwise, the timing will be incorrect because the launched coroutine
* will run concurrently--meaning a "happens before" relationship between the measurer and the
* measured cannot be established and thereby the concurrent action cannot be timed.
*/
inline fun <T> measure(key: String = "measurement.generic", description: Any = "measurement", block: () -> T): T {
ensureScope()
val metric = TimeMetric(key, description.toString()).markTime()
val result = block()
metric.markTime()
report(metric)
return result
}
/**
* Add the given metric to the stream of metric events.
*
* @param metric the metric to add.
*/
fun report(metric: Metric): Feedback {
jobs += scope.launch {
_metrics.send(metric)
}
return this
}
/**
* Add the given action to the stream of action events.
*
* @param action the action to add.
*/
fun report(action: Action): Feedback {
jobs += scope.launch {
_actions.send(action)
}
return this
}
/**
* Report the given error to everything that is tracking feedback. Converts it to a Crash object
* which is intended for use in property-based analytics.
*
* @param error the uncaught exception that occurred.
*/
fun report(error: Throwable?, isFatal: Boolean = false): Feedback {
return if (isFatal) report(Crash(error)) else report(NonFatal(error, "reported"))
}
/**
* Ensures that the scope for this instance has been initialized.
*/
fun ensureScope(): Boolean {
check(::scope.isInitialized) {
"Error: feedback has not been initialized. Before attempting to use this feedback" +
" object, ensure feedback.start() has been called."
}
return true
}
fun ensureStopped(): Boolean {
val errors = mutableListOf<String>()
if (!_metrics.isClosedForSend && !_actions.isClosedForSend) errors += "both channels are still open"
else if (!_actions.isClosedForSend) errors += "the actions channel is still open"
else if (!_metrics.isClosedForSend) errors += "the metrics channel is still open"
if (scope.isActive) errors += "the scope is still active"
if (jobs.isActive()) errors += "reporting jobs are still active"
if (errors.isEmpty()) return true
throw IllegalStateException("Feedback is still active because ${errors.joinToString(", ")}.")
}
interface Metric : Mappable<String, Any>, Keyed<String> {
override val key: String
val startTime: Long?
val endTime: Long?
val elapsedTime: Long?
val description: String
override fun toMap(): Map<String, Any> {
return mapOf(
"key" to key,
"description" to description,
"startTime" to (startTime ?: 0),
"endTime" to (endTime ?: 0),
"elapsedTime" to (elapsedTime ?: 0)
)
}
}
interface Action : Feedback.Mappable<String, Any>, Keyed<String> {
override val key: String
override fun toMap(): Map<String, Any> {
return mapOf("key" to key)
}
}
abstract class MappedAction private constructor(protected val propertyMap: MutableMap<String, Any> = mutableMapOf()) : Feedback.Action {
constructor(vararg properties: Pair<String, Any>) : this(mutableMapOf(*properties))
override fun toMap(): Map<String, Any> {
return propertyMap.apply { putAll(super.toMap()) }
}
}
abstract class Funnel(funnelName: String, stepName: String, step: Int, vararg properties: Pair<String, Any>) : MappedAction(
"funnelName" to funnelName,
"stepName" to stepName,
"step" to step,
*properties
) {
override fun toString() = key
override val key: String = "funnel.$funnelName.$stepName.$step"
}
interface Keyed<T> {
val key: T
}
interface Mappable<K, V> {
fun toMap(): Map<K, V>
}
data class TimeMetric(
override val key: String,
override val description: String,
val times: MutableList<Long> = mutableListOf()
) : Metric {
override val startTime: Long? get() = times.firstOrNull()
override val endTime: Long? get() = times.lastOrNull()
override val elapsedTime: Long? get() = endTime?.minus(startTime ?: 0)
fun markTime(): TimeMetric {
times.add(System.currentTimeMillis())
return this
}
override fun toString(): String {
return "$description in ${elapsedTime}ms"
}
}
open class AppError(name: String = "unknown", description: String? = null, isFatal: Boolean = false, vararg properties: Pair<String, Any>) : MappedAction(
"isError" to true,
"isFatal" to isFatal,
"errorName" to name,
"message" to (description ?: "None"),
"description" to describe(name, description, isFatal),
*properties
) {
val isFatal: Boolean by propertyMap
val errorName: String by propertyMap
val description: String by propertyMap
constructor(name: String, exception: Throwable? = null, isFatal: Boolean = false) : this(
name, exception?.toString(), isFatal,
"exceptionString" to (exception?.toString() ?: "None"),
"message" to (exception?.message ?: "None"),
"cause" to (exception?.cause?.toString() ?: "None"),
"cause.cause" to (exception?.cause?.cause?.toString() ?: "None"),
"cause.cause.cause" to (exception?.cause?.cause?.cause?.toString() ?: "None")
) {
propertyMap.putAll(exception.stacktraceToMap())
}
override val key = "error.${if (isFatal) "fatal" else "nonfatal"}.$name"
override fun toString() = description
companion object {
fun describe(name: String, description: String?, isFatal: Boolean) =
"${if (isFatal) "Error: FATAL" else "Error: non-fatal"} $name error due to: ${description ?: "unknown error"}"
}
}
class Crash(val exception: Throwable? = null) : AppError( "crash", exception, true)
class NonFatal(val exception: Throwable? = null, name: String) : AppError(name, exception, false)
}
private fun Throwable?.stacktraceToMap(chunkSize: Int = 250): Map<out String, String> {
val properties = mutableMapOf("stacktrace.0" to "None")
if (this == null) return properties
val stringWriter = StringWriter()
printStackTrace(PrintWriter(stringWriter))
stringWriter.toString().chunked(chunkSize).forEachIndexed { index, chunk ->
properties["stacktrace.$index"] = chunk
}
return properties
}

128
feedback/src/main/java/cash/z/ecc/android/feedback/FeedbackCoordinator.kt

@ -0,0 +1,128 @@
package cash.z.ecc.android.feedback
import cash.z.ecc.android.feedback.util.CompositeJob
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlin.coroutines.coroutineContext
/**
* Takes care of the boilerplate involved in processing feedback emissions. Simply provide callbacks
* and emissions will occur in a mutually exclusive way, across all processors, so that things like
* writing to a file can occur without clobbering changes. This class also provides a mechanism for
* waiting for any in-flight emissions to complete. Lastly, all monitoring will cleanly complete
* whenever the feedback is stopped or its parent scope is cancelled.
*/
class FeedbackCoordinator(val feedback: Feedback, defaultObservers: Set<FeedbackObserver> = setOf()) {
init {
feedback.apply {
onStart {
invokeOnCompletion {
flush()
}
}
}
defaultObservers.forEach {
addObserver(it)
}
}
private var contextMetrics = Dispatchers.IO
private var contextActions = Dispatchers.IO
private val jobs = CompositeJob()
val observers = mutableSetOf<FeedbackObserver>()
/**
* Wait for any in-flight listeners to complete.
*/
suspend fun await() {
jobs.await()
flush()
}
/**
* Cancel all in-flight observer functions.
*/
fun cancel() {
jobs.cancel()
flush()
}
/**
* Flush all observers so they can clear all pending buffers.
*/
fun flush() {
observers.forEach { it.flush() }
}
/**
* Inject the context on which to observe metrics, mostly for testing purposes.
*/
fun metricsOn(dispatcher: CoroutineDispatcher): FeedbackCoordinator {
contextMetrics = dispatcher
return this
}
/**
* Inject the context on which to observe actions, mostly for testing purposes.
*/
fun actionsOn(dispatcher: CoroutineDispatcher): FeedbackCoordinator {
contextActions = dispatcher
return this
}
/**
* Add a coordinated observer that will not clobber all other observers because their actions
* are coordinated via a global mutex.
*/
fun addObserver(observer: FeedbackObserver) {
feedback.onStart {
observers += observer.initialize()
observeMetrics(observer::onMetric)
observeActions(observer::onAction)
}
}
inline fun <reified T: FeedbackObserver> findObserver(): T? {
return observers.firstOrNull { it::class == T::class } as T?
}
private fun observeMetrics(onMetricListener: (Feedback.Metric) -> Unit) {
feedback.metrics.onEach {
jobs += feedback.scope.launch {
withContext(contextMetrics) {
mutex.withLock {
onMetricListener(it)
}
}
}
}.launchIn(feedback.scope)
}
private fun observeActions(onActionListener: (Feedback.Action) -> Unit) {
feedback.actions.onEach {
val id = coroutineContext.hashCode()
jobs += feedback.scope.launch {
withContext(contextActions) {
mutex.withLock {
onActionListener(it)
}
}
}
}.launchIn(feedback.scope)
}
interface FeedbackObserver {
fun initialize(): FeedbackObserver { return this }
fun onMetric(metric: Feedback.Metric) {}
fun onAction(action: Feedback.Action) {}
fun flush() {}
}
companion object {
private val mutex: Mutex = Mutex()
}
}

45
feedback/src/main/java/cash/z/ecc/android/feedback/util/CompositeJob.kt

@ -0,0 +1,45 @@
package cash.z.ecc.android.feedback.util
import kotlinx.coroutines.Job
class CompositeJob {
private val activeJobs = mutableListOf<Job>()
val size: Int get() = activeJobs.size
fun add(job: Job) {
activeJobs.add(job)
job.invokeOnCompletion {
remove(job)
}
}
fun remove(job: Job): Boolean {
return activeJobs.remove(job)
}
fun isActive(): Boolean {
return activeJobs.any { isActive() }
}
suspend fun await() {
// allow for concurrent modification since the list isn't coroutine or thread safe
do {
val job = activeJobs.firstOrNull()
if (job?.isActive == true) {
job.join()
} else {
// prevents an infinite loop in the extreme edge case where the list has a null item
try { activeJobs.remove(job) } catch (t: Throwable) {}
}
} while (size > 0)
}
fun cancel() {
activeJobs.filter { isActive() }.forEach { it.cancel() }
}
operator fun plusAssign(also: Job) {
add(also)
}
}

31
feedback/src/test/java/cash/z/ecc/android/feedback/CoroutineTestRule.kt

@ -0,0 +1,31 @@
package cash.z.ecc.android.feedback
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.rules.TestWatcher
import org.junit.runner.Description
class CoroutinesTestRule(
val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
) : TestWatcher() {
lateinit var testScope: TestCoroutineScope
override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(testDispatcher)
testScope = TestCoroutineScope()
}
override fun finished(description: Description?) {
super.finished(description)
Dispatchers.resetMain()
testDispatcher.cleanupTestCoroutines()
if (testScope.coroutineContext[Job]?.isActive == true) testScope.cancel()
}
}

55
feedback/src/test/java/cash/z/ecc/android/feedback/FeedbackObserverTest.kt

@ -0,0 +1,55 @@
package cash.z.ecc.android.feedback
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
class FeedbackObserverTest {
private val feedback: Feedback = Feedback()
private val feedbackCoordinator: FeedbackCoordinator = FeedbackCoordinator(feedback)
private var counter: Int = 0
private val simpleAction = object : Feedback.Action {
override val key = "ButtonClick"
}
@Test
fun testConcurrency() = runBlocking {
val actionCount = 50
val processorCount = 50
val expectedTotal = actionCount * processorCount
repeat(processorCount) {
addObserver()
}
feedback.start()
repeat(actionCount) {
sendAction()
}
feedback.await() // await sends
feedbackCoordinator.await() // await processing
feedback.stop()
assertEquals(
"Concurrent modification happened ${expectedTotal - counter} times",
expectedTotal,
counter
)
}
private fun addObserver() {
feedbackCoordinator.addObserver(object : FeedbackCoordinator.FeedbackObserver {
override fun onAction(action: Feedback.Action) {
counter++
}
})
}
private fun sendAction() {
feedback.report(simpleAction)
}
}

156
feedback/src/test/java/cash/z/ecc/android/feedback/FeedbackTest.kt

@ -0,0 +1,156 @@
package cash.z.ecc.android.feedback
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.Assert.*
import org.junit.Test
import java.lang.RuntimeException
class FeedbackTest {
@Test
fun testMeasure_blocking() = runBlocking {
val duration = 1_100L
val feedback = Feedback().start()
verifyDuration(feedback, duration)
feedback.measure {
workBlocking(duration)
}
}
@Test
fun testMeasure_suspending() = runBlocking {
val duration = 1_100L
val feedback = Feedback().start()
verifyDuration(feedback, duration)
feedback.measure {
workSuspending(duration)
}
}
@Test
fun testTrack() = runBlocking {
val simpleAction = object : Feedback.Action {
override val key = "ButtonClick"
}
val feedback = Feedback().start()
verifyAction(feedback, simpleAction.key)
feedback.report(simpleAction)
Unit
}
@Test
fun testCancellation_stop() = runBlocking {
verifyFeedbackCancellation { feedback, _ ->
feedback.stop()
}
}
@Test
fun testCancellation_cancel() = runBlocking {
verifyFeedbackCancellation { _, parentJob ->
parentJob.cancel()
}
}
@Test(expected = IllegalStateException::class)
fun testCancellation_noCancel() = runBlocking {
verifyFeedbackCancellation { _, _ -> }
}
@Test
fun testCrash() {
val rushing = RuntimeException("rushing")
val speeding = RuntimeException("speeding", rushing)
val runlight = RuntimeException("Run light", speeding)
val crash = Feedback.Crash(RuntimeException("BOOM", runlight))
val map = crash.toMap()
printMap(map)
assertNotNull(map["cause"])
assertNotNull(map["cause.cause"])
assertNotNull(map["cause.cause"])
}
@Test
fun testAppError_exception() {
val rushing = RuntimeException("rushing")
val speeding = RuntimeException("speeding", rushing)
val runlight = RuntimeException("Run light", speeding)
val error = Feedback.AppError("reported", RuntimeException("BOOM", runlight))
val map = error.toMap()
printMap(map)
assertFalse(error.isFatal)
assertNotNull(map["cause"])
assertNotNull(map["cause.cause"])
assertNotNull(map["cause.cause"])
}
@Test
fun testAppError_description() {
val error = Feedback.AppError("reported", "The server was down while downloading blocks!")
val map = error.toMap()
printMap(map)
assertFalse(error.isFatal)
}
private fun printMap(map: Map<String, Any>) {
for (entry in map) {
println("%-20s = %s".format(entry.key, entry.value))
}
}
private fun verifyFeedbackCancellation(testBlock: suspend (Feedback, Job) -> Unit) = runBlocking {
val feedback = Feedback()
var counter = 0
val parentJob = launch {
feedback.start()
feedback.scope.launch {
delay(50)
counter = 1
}
}
// give feedback.start a chance to happen before cancelling
delay(25)
// stop or cancel things here
testBlock(feedback, parentJob)
delay(75)
feedback.ensureStopped()
assertEquals(0, counter)
}
private fun verifyDuration(feedback: Feedback, duration: Long) {
feedback.metrics.onEach {
val metric = (it as? Feedback.TimeMetric)?.elapsedTime
assertTrue(
"Measured time did not match duration. Expected $duration but was $metric",
metric ?: 0 >= duration
)
feedback.stop()
}.launchIn(feedback.scope)
}
private fun verifyAction(feedback: Feedback, name: String) {
feedback.actions.onEach {
assertTrue("Action did not match. Expected $name but was ${it.key}", name == it.key)
feedback.stop()
}.launchIn(feedback.scope)
}
private fun workBlocking(duration: Long) {
Thread.sleep(duration)
}
private suspend fun workSuspending(duration: Long) {
delay(duration)
}
}

19
gradle.properties

@ -0,0 +1,19 @@
# org.gradle.parallel=true
kotlin.code.style=official
android.enableJetifier=true
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m
android.useAndroidX=true
dagger.fastInit=enabled
android.builder.sdkDownload=true
# Optionally configures test orchestrator. While it provides isolated tests, it also nearly doubles
# the time it takes for test to run.
isUseTestOrchestrator=false
# Toggles between using the SDK Maven artifact versus an included build
IS_SDK_INCLUDED_BUILD=false
KTLINT_VERSION=0.45.2
ANDROID_JVM_TARGET=1.8

BIN
gradle/wrapper/gradle-wrapper.jar

Binary file not shown.

6
gradle/wrapper/gradle-wrapper.properties

@ -0,0 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
distributionSha256Sum=f6b8596b10cce501591e92f229816aa4046424f3b24d771751b06779d58c8ec4
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

240
gradlew

@ -0,0 +1,240 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

91
gradlew.bat

@ -0,0 +1,91 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

1
lockbox/.gitignore

@ -0,0 +1 @@
/build

49
lockbox/build.gradle

@ -0,0 +1,49 @@
import cash.z.ecc.android.Deps
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion Deps.compileSdkVersion
useLibrary 'android.test.runner'
defaultConfig {
minSdkVersion Deps.minSdkVersion
targetSdkVersion Deps.targetSdkVersion
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'
consumerProguardFiles 'consumer-rules.pro'
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
namespace 'cash.z.ecc.android.lockbox'
}
dependencies {
implementation Deps.JavaX.INJECT
implementation Deps.Kotlin.STDLIB
implementation Deps.AndroidX.APPCOMPAT
implementation Deps.AndroidX.CORE_KTX
// Zcash
implementation Deps.Zcash.ANDROID_WALLET_PLUGINS
implementation Deps.Misc.Plugins.SECURE_STORAGE
androidTestImplementation Deps.Test.Android.JUNIT
}

0
lockbox/consumer-rules.pro

21
lockbox/proguard-rules.pro

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# 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

52
lockbox/src/androidTest/java/cash/z/ecc/android/lockbox/LockBoxText.kt

@ -0,0 +1,52 @@
package cash.z.ecc.android.lockbox
import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class LockBoxText {
private lateinit var appContext: Context
private lateinit var lockBox: LockBoxProvider
@Before
fun start() {
appContext = InstrumentationRegistry.getInstrumentation().targetContext
lockBox = LockBox(appContext)
}
@Test
fun testSeed_store() {
val testMessage = "Some Bytes To Test"
val testBytes = testMessage.toByteArray()
lockBox.setBytes("seed", testBytes)
assertEquals(testMessage, String(lockBox.getBytes("seed")!!))
}
@Test
fun testSeed_storeNegatives() {
val testBytes = byteArrayOf(0x00, 0x00, -0x0F, -0x0B)
lockBox.setBytes("seed", testBytes)
assertTrue(testBytes.contentEquals(lockBox.getBytes("seed")!!))
}
@Test
fun testSeed_storeLeadingZeros() {
val testBytes = byteArrayOf(0x00, 0x00, 0x0F, 0x0B)
lockBox.setBytes("seed", testBytes)
assertTrue(testBytes.contentEquals(lockBox.getBytes("seed")!!))
}
@Test
fun testPrivateKey_retrieve() {
val testMessage = "Some Bytes To Test"
lockBox.setCharsUtf8("spendingKey", testMessage.toCharArray())
assertEquals(testMessage, String(lockBox.getCharsUtf8("spendingKey")!!))
}
}

2
lockbox/src/main/AndroidManifest.xml

@ -0,0 +1,2 @@
<manifest
xmlns:android="http://schemas.android.com/apk/res/android" />

133
lockbox/src/main/java/cash/z/ecc/android/lockbox/LockBox.kt

@ -0,0 +1,133 @@
package cash.z.ecc.android.lockbox
import android.content.Context
import cash.z.android.plugin.LockBoxPlugin
import de.adorsys.android.securestoragelibrary.SecurePreferences
import java.nio.ByteBuffer
import java.nio.CharBuffer
import java.nio.charset.StandardCharsets
import java.util.*
class LockBox(private val appContext: Context) : LockBoxPlugin {
private val maxLength: Int = 50
override fun setBoolean(key: String, value: Boolean) {
setChunkedString(key, value.toString())
}
override fun getBoolean(key: String): Boolean {
return getChunkedString(key)?.toBoolean() ?: false
}
override fun setBytes(key: String, value: ByteArray) {
// using hex here because this library doesn't really work well for byte arrays
// but hopefully we can code to arrays and then change the underlying library, later
setChunkedString(key, value.toHex())
}
override fun getBytes(key: String): ByteArray? {
return getChunkedString(key)?.fromHex()
}
override fun setCharsUtf8(key: String, value: CharArray) {
// Using string here because this library doesn't work well for char arrays
// but hopefully we can code to arrays and then change the underlying library, later
setChunkedString(key, String(value))
}
override fun getCharsUtf8(key: String): CharArray? {
return getChunkedString(key)?.toCharArray()
}
fun delete(key: String) {
return SecurePreferences.removeValue(appContext, key)
}
fun clear() {
SecurePreferences.clearAllValues(appContext)
}
inline operator fun <reified T> set(key: String, value: T) {
when (T::class) {
Boolean::class -> setBoolean(key, value as Boolean)
ByteArray::class -> setBytes(key, value as ByteArray)
CharArray::class -> setCharsUtf8(key, value as CharArray)
Double::class, Float::class, Integer::class, Long::class, String::class -> setChunkedString(key, value.toString())
else -> throw UnsupportedOperationException("Lockbox does not yet support setting ${T::class.java.simpleName} objects but it can easily be added.")
}
}
inline operator fun <reified T> get(key: String): T? = when (T::class) {
Boolean::class -> getBoolean(key)
ByteArray::class -> getBytes(key)
CharArray::class -> getCharsUtf8(key)
Double::class -> getChunkedString(key)?.let { it.toDoubleOrNull() }
Float::class -> getChunkedString(key)?.let { it.toFloatOrNull() }
Integer::class -> getChunkedString(key)?.let { it.toIntOrNull() }
Long::class -> getChunkedString(key)?.let { it.toLongOrNull() }
String::class -> getChunkedString(key)
else -> throw UnsupportedOperationException("Lockbox does not yet support getting ${T::class.simpleName} objects but it can easily be added")
} as T
/**
* Splits a string value into smaller pieces so as not to exceed the limit on the length of
* String that can be stored.
*/
fun setChunkedString(key: String, value: String) {
if (value.length > maxLength) {
SecurePreferences.setValue(appContext, key, value.chunked(maxLength))
} else {
SecurePreferences.setValue(appContext, key, value)
}
}
/**
* Returns a string value from storage by first fetching the key, directly. If that is missing,
* it checks for a chunked version of the key. If that exists, it will be merged and returned.
* If not, then null will be returned.
*
* @return the key if found and null otherwise.
*/
fun getChunkedString(key: String): String? {
return SecurePreferences.getStringValue(appContext, key, null)
?: SecurePreferences.getStringListValue(appContext, key, listOf()).let { result ->
if (result.size == 0) null else result.joinToString("")
}
}
//
// Extensions (TODO: find library that works better with arrays of bytes and chars)
//
private fun ByteArray.toHex(): String {
val sb = StringBuilder(size * 2)
for (b in this)
sb.append(String.format("%02x", b))
return sb.toString()
}
private fun String.fromHex(): ByteArray {
val len = length
val data = ByteArray(len / 2)
var i = 0
while (i < len) {
data[i / 2] =
((Character.digit(this[i], 16) shl 4) + Character.digit(this[i + 1], 16)).toByte()
i += 2
}
return data
}
private fun CharArray.toBytes(): ByteArray {
val byteBuffer = StandardCharsets.UTF_8.encode(CharBuffer.wrap(this))
return Arrays.copyOf(byteBuffer.array(), byteBuffer.limit());
}
private fun ByteArray.fromBytes(): CharArray {
val charBuffer = StandardCharsets.UTF_8.decode(ByteBuffer.wrap(this))
return Arrays.copyOf(charBuffer.array(), charBuffer.limit())
}
}

1
mnemonic/.gitignore

@ -0,0 +1 @@
/build

14
mnemonic/build.gradle

@ -0,0 +1,14 @@
import cash.z.ecc.android.Deps
apply plugin: 'kotlin'
dependencies {
implementation Deps.JavaX.INJECT
implementation Deps.Kotlin.STDLIB
// Zcash
implementation Deps.Zcash.ANDROID_WALLET_PLUGINS
implementation Deps.Zcash.KOTLIN_BIP39
testImplementation Deps.Test.JUNIT
}

18
mnemonic/src/main/java/cash/z/ecc/kotlin/mnemonic/MnemonicExt.kt

@ -0,0 +1,18 @@
package cash.z.ecc.kotlin.mnemonic
import java.util.*
/**
* Clears out the given char array in memory, for security purposes.
*/
fun CharArray.clear() {
Arrays.fill(this, '0')
}
/**
* Clears out the given byte array in memory, for security purposes.
*/
fun ByteArray.clear() {
Arrays.fill(this, 0.toByte())
}

25
mnemonic/src/main/java/cash/z/ecc/kotlin/mnemonic/Mnemonics.kt

@ -0,0 +1,25 @@
package cash.z.ecc.kotlin.mnemonic
import cash.z.android.plugin.MnemonicPlugin
import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.bip39.Mnemonics.MnemonicCode
import cash.z.ecc.android.bip39.Mnemonics.WordCount
import cash.z.ecc.android.bip39.toEntropy
import cash.z.ecc.android.bip39.toSeed
import java.util.*
import java.util.Locale.ENGLISH
class Mnemonics : MnemonicPlugin {
override fun fullWordList(languageCode: String) = Mnemonics.getCachedWords(Locale.ENGLISH.language)
override fun nextEntropy(): ByteArray = WordCount.COUNT_24.toEntropy()
override fun nextMnemonic(): CharArray = MnemonicCode(WordCount.COUNT_24).chars
override fun nextMnemonic(entropy: ByteArray): CharArray = MnemonicCode(entropy).chars
override fun nextMnemonicList(): List<CharArray> = MnemonicCode(WordCount.COUNT_24).words
override fun nextMnemonicList(entropy: ByteArray): List<CharArray> = MnemonicCode(entropy).words
override fun toSeed(mnemonic: CharArray): ByteArray = MnemonicCode(mnemonic).toSeed()
override fun toWordList(mnemonic: CharArray): List<CharArray> = MnemonicCode(mnemonic).words
fun validate(mnemonic: CharArray) {
MnemonicCode(mnemonic).validate()
}
}

90
mnemonic/src/test/java/cash/z/ecc/android/util/MnemonicTest.kt

@ -0,0 +1,90 @@
package cash.z.ecc.android.util
import cash.z.android.plugin.MnemonicPlugin
import cash.z.ecc.kotlin.mnemonic.Mnemonics
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import java.lang.Math.max
import java.util.*
class MnemonicTest {
lateinit var mnemonics: MnemonicPlugin
@Before
fun start() {
mnemonics = Mnemonics()
}
@Test
fun testSeed_fromMnemonic() {
val seed = mnemonics.run {
toSeed(nextMnemonic())
}
assertEquals(64, seed.size)
}
@Test
fun testMnemonic_create() {
val words = String(mnemonics.nextMnemonic()).split(' ')
assertEquals(24, words.size)
validate(words)
}
@Test
fun testMnemonic_createList() {
val words = mnemonics.nextMnemonicList()
assertEquals(24, words.size)
validate(words.map { String(it) })
}
@Test
fun testMnemonic_toList() {
val words = mnemonics.run {
toWordList(nextMnemonic())
}
assertEquals(24, words.size)
validate(words.map { String(it) })
}
@Test
fun testMnemonic_longestWord() {
var max = 0
val englishWordList = mnemonics.fullWordList(Locale.ENGLISH.language)
repeat(2048) {
max = max(max, englishWordList[it].length)
}
assertEquals(8, max)
}
private fun validate(words: List<String>) {
val englishWordList = mnemonics.fullWordList(Locale.ENGLISH.language)
// return or crash!
words.forEach { word ->
var i = 0
while (true) {
if (englishWordList[i++] == word) {
println(word)
break
}
}
}
}
}
private fun CharSequence.toWords(): List<CharSequence> {
return mutableListOf<CharSequence>().let { result ->
var index = 0
repeat(length) {
if (this[it] == ' ') {
result.add(subSequence(index, it))
index = it + 1
}
}
result.add(subSequence(index, length))
result
}
}

BIN
placeholder.keystore

Binary file not shown.

1
qrecycler/.gitignore

@ -0,0 +1 @@
/build

39
qrecycler/build.gradle

@ -0,0 +1,39 @@
import cash.z.ecc.android.Deps
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion Deps.compileSdkVersion
defaultConfig {
minSdkVersion Deps.minSdkVersion
targetSdkVersion Deps.targetSdkVersion
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
namespace 'cash.z.android.qrecycler'
}
dependencies {
implementation Deps.AndroidX.CORE_KTX
implementation Deps.Kotlin.STDLIB
// dependencies specific to this module, not shared with other modules
implementation Deps.Misc.Plugins.QR_SCANNER
}

21
qrecycler/proguard-rules.pro

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# 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

4
qrecycler/src/main/AndroidManifest.xml

@ -0,0 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.CAMERA" />
</manifest>

62
qrecycler/src/main/java/cash/z/android/qrecycler/QRecycler.kt

@ -0,0 +1,62 @@
package cash.z.android.qrecycler
import android.graphics.Bitmap
import android.graphics.Color
import android.widget.ImageView
import androidx.core.view.doOnLayout
import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType.ERROR_CORRECTION
import com.google.zxing.EncodeHintType.MARGIN
import com.google.zxing.qrcode.QRCodeWriter
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel.*
class QRecycler {
fun load(content: String): Builder {
return Builder(content)
}
// TODO: make this call async such that action can be taken once it is complete
fun encode(builder: Builder) {
builder.target.doOnLayout { measuredView ->
val w = measuredView.width
val h = measuredView.height
val hints = mapOf(ERROR_CORRECTION to builder.errorCorrection, MARGIN to builder.quietZone)
val bitMatrix = QRCodeWriter().encode(builder.content, BarcodeFormat.QR_CODE, w, h, hints)
val pixels = IntArray(w * h)
for (y in 0 until h) {
val offset = y * w
for (x in 0 until w) {
pixels[offset + x] = if (bitMatrix.get(x, y)) Color.BLACK else Color.WHITE
}
}
// TODO: RECYCLE THIS BITMAP MEMORY!!! Do it in a way that is lifecycle-aware and disposes of the memory when the fragment is off-screen
val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
bitmap.setPixels(pixels, 0, w, 0, 0, w, h)
(measuredView as ImageView).setImageBitmap(bitmap)
}
}
inner class Builder(val content: String) {
lateinit var target: ImageView
var errorCorrection: ErrorCorrectionLevel = Q
var quietZone: Int = 4
fun into(imageView: ImageView) {
target = imageView
encode(this)
}
fun withQuietZoneSize(customQuietZone: Int): Builder {
quietZone = customQuietZone
return this
}
fun withCorrectionLevel(level: CorrectionLevel): Builder {
errorCorrection = level.errorCorrectionLevel
return this
}
}
enum class CorrectionLevel(val errorCorrectionLevel: ErrorCorrectionLevel) {
LOW(L), DEFAULT(M), MEDIUM(Q), HIGH(H);
}
}

8
qrecycler/src/main/java/cash/z/android/qrecycler/QScanner.kt

@ -0,0 +1,8 @@
package cash.z.android.qrecycler
/**
* An interface to allow for plugging in any scanner
*/
interface QScanner {
fun scanBarcode(callback: (Result<String>) -> Unit)
}

23
qrecycler/src/main/res/layout/texture_view.xml

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2016 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.
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<TextureView
android:id="@+id/texture_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
/>
</merge>

58
qrecycler/src/main/res/values/attrs.xml

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2016 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.
-->
<resources>
<declare-styleable name="CameraView">
<!--
Set this to true if you want the CameraView to adjust its bounds to preserve the aspect
ratio of its camera preview.
-->
<attr name="android:adjustViewBounds"/>
<!-- Direction the camera faces relative to device screen. -->
<attr name="facing" format="enum">
<!-- The camera device faces the opposite direction as the device's screen. -->
<enum name="back" value="0"/>
<!-- The camera device faces the same direction as the device's screen. -->
<enum name="front" value="1"/>
</attr>
<!-- Aspect ratio of camera preview and pictures. -->
<attr name="aspectRatio" format="string"/>
<!-- Continuous auto focus mode. -->
<attr name="autoFocus" format="boolean"/>
<!-- The flash mode. -->
<attr name="flash" format="enum">
<!-- Flash will not be fired. -->
<enum name="off" value="0"/>
<!--
Flash will always be fired during snapshot.
The flash may also be fired during preview or auto-focus depending on the driver.
-->
<enum name="on" value="1"/>
<!--
Constant emission of light during preview, auto-focus and snapshot.
This can also be used for video recording.
-->
<enum name="torch" value="2"/>
<!--
Flash will be fired automatically when required.
The flash may be fired during preview, auto-focus, or snapshot depending on the
driver.
-->
<enum name="auto" value="3"/>
<!--
Flash will be fired in red-eye reduction mode.
-->
<enum name="redEye" value="4"/>
</attr>
</declare-styleable>
</resources>

21
qrecycler/src/main/res/values/public.xml

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2016 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.
-->
<resources>
<public name="facing" type="attr"/>
<public name="aspectRatio" type="attr"/>
<public name="autoFocus" type="attr"/>
<public name="flash" type="attr"/>
<public name="Widget.CameraView" type="style"/>
</resources>

2
qrecycler/src/main/res/values/strings.xml

@ -0,0 +1,2 @@
<resources>
</resources>

24
qrecycler/src/main/res/values/styles.xml

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2016 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.
-->
<resources>
<style name="Widget.CameraView" parent="android:Widget">
<item name="android:adjustViewBounds">false</item>
<item name="facing">back</item>
<item name="aspectRatio">4:3</item>
<item name="autoFocus">true</item>
<item name="flash">auto</item>
</style>
</resources>

1
responsible_disclosure.md

@ -0,0 +1 @@
Please see https://github.com/zcash/zcash-android-wallet-sdk/blob/master/responsible_disclosure.md

53
settings.gradle.kts

@ -0,0 +1,53 @@
pluginManagement {
repositories {
gradlePluginPortal()
}
plugins {
id("com.github.ben-manes.versions") version ("0.39.0") apply (false)
}
}
dependencyResolutionManagement {
@Suppress("UnstableApiUsage")
repositories {
val isRepoRestrictionEnabled = false
mavenLocal()
google()
mavenCentral()
maven("https://jitpack.io")
jcenter()
/*
// Uncomment to use a snapshot version of the SDK, e.g. when the SDK version ends in -SNAPSHOT
maven("https://oss.sonatype.org/content/repositories/snapshots") {
if (isRepoRestrictionEnabled) {
content {
includeGroup("cash.z.ecc.android")
}
}
}*/
}
}
rootProject.name = "ecc-wallet"
includeBuild("build-convention")
include(":app")
include(":qrecycler")
include(":feedback")
include(":mnemonic")
include(":lockbox")
if (extra["IS_SDK_INCLUDED_BUILD"].toString().toBoolean()) {
// Currently assume the SDK is up one level with a hardcoded directory name
// If this becomes problematic, `IS_SDK_INCLUDED_BUILD` could be turned into a path
// instead.
includeBuild("../zcash-android-sdk") {
dependencySubstitution {
substitute(module("cash.z.ecc.android:zcash-android-sdk")).using(project(":sdk-lib"))
}
}
}
Loading…
Cancel
Save