Compare commits

...

440 commits

Author SHA1 Message Date
jessopb fdd759b241
Merge pull request #1236 from ktprograms/readme-google-services
Add google-services.json instructions to README
2022-11-29 13:07:52 -05:00
jessopb 57cd649c02
Update README.md 2022-11-29 13:07:30 -05:00
jessopb 7250435d7f
Create deploy.yml 2022-11-25 14:03:02 -05:00
jessopb b81abf74a1
Merge pull request #1244 from Coolguy3289/patch-1
Update LBRY_TV_CONNECTION_STRING to new Odysee Backend
2022-11-23 13:59:38 -05:00
Ralph 208e2c2d42
Update LBRY_TV_CONNECTION_STRING to new Odysee Backend 2022-11-23 11:29:52 -05:00
kt programs a5bdd1c042 Add google-services.json instructions to README 2022-04-18 12:14:25 +08:00
Alex Grin 1e3a74cae1
Update README.md 2021-09-28 10:19:37 -04:00
Akinwale Ariwodola ca08f71a72 sdk 0.102.0 2021-08-20 15:32:14 +01:00
Akinwale Ariwodola b60ca39df1 fix crash bug when trying to play content 2021-08-19 12:27:18 +01:00
soup-jingle 696bc86b7c
Added QR scanner to wallet send card (#1194)
* Added QR scanner to wallet send card

* * app/src/main/java/io/lbry/browser/ui/wallet/WalletFragment.java:

Changed orientation of QR scanner, along with some style corrections

* Revert "* app/src/main/java/io/lbry/browser/ui/wallet/WalletFragment.java:"

This reverts commit 519c45ae0d.

* Fixed orientation of QR scanner activity, plus style corrections
2021-08-13 09:01:53 +01:00
Akinwale Ariwodola 4c163c6244
Merge pull request #1216 from lbryio/new_cdn_url
Use the new CDN url scheme for thumbnails
2021-08-13 09:01:09 +01:00
Akinwale Ariwodola 2ad49ca281
Merge pull request #1208 from lbryio/claimid-modifier-char
Accept colon or octoshape as claimID modifier character
2021-08-11 10:20:58 +01:00
Javi Rueda 56caeef72b Use the new CDN url scheme for thumbnails 2021-08-10 18:28:20 +02:00
Javi Rueda 84bb014557 Accept colon or octoshape as claimID modifier character and add unit tests 2021-07-29 00:55:45 +02:00
Akinwale Ariwodola 9278e74e85 remove reference to bintray 2021-06-25 19:34:58 +01:00
Akinwale Ariwodola cf6f09c60d fix gitlab CI script 2021-06-25 19:25:29 +01:00
Akinwale Ariwodola f2cbed48d9 fix git-secret install source 2021-06-25 19:23:30 +01:00
Akinwale Ariwodola f1ead0c247 bumpversion 0.17.0 --> 0.17.1 2021-06-25 19:08:30 +01:00
Akinwale Ariwodola 5f2c72ec4d sdk 0.100.0 / switch to maven central for lbrysdk Gradle dependency 2021-06-25 19:07:47 +01:00
Javi Rueda a6869eb2e6
Fix crash for anonymous content or without tags (#1190) 2021-05-10 17:14:22 +02:00
Javi Rueda cbae6c476a
Claim has source (#1188)
* Pass has_source parameter to claim_search to ignore livestreamings

* Update to SDK 0.94.1 and allow user to view livestream content on Odysee
2021-05-10 17:13:28 +02:00
Javi Rueda 48d257ceaf
Show total wallet balance in USD instead of spendable balance conversion (#1186) 2021-04-19 19:55:49 +02:00
Javi Rueda 5355456498
Don't show link to Bittrex (#1181) 2021-04-12 18:53:59 +02:00
Javi Rueda 1436895ace
Copy file to public Downloads directory (#1180) 2021-04-12 18:53:13 +02:00
Javi Rueda 6e32f7724f
Notification improvements (#1175)
* Stop showing notification list when going from notification's content to related content

* Add is_app_readable parameter to notification_list call

* Return current fragment on main activity
2021-03-29 17:39:27 +02:00
Javi Rueda 3f5104d60a
Hide floating wallet on the Channel fragment (#1177)
* Hide floating wallet on the Channel fragment

* Unregister OnPageChangeCallback when exiting channel fragment
2021-03-29 17:38:51 +02:00
Javi Rueda d8fdb3b818
Decode and encode urls with non-authorized characters (#1174)
* Decode URL encoded links and unit test it

* Encode UTF8 characters on LBRY.TV links and unit test it
2021-03-19 11:00:24 +01:00
Akinwale Ariwodola 5c187e7a8d Merge branch 'master' of https://github.com/lbryio/lbry-android 2021-03-16 09:24:53 +01:00
Akinwale Ariwodola 24862550a1 bumpversion 0.16.14 --> 0.17.0 2021-03-16 09:24:33 +01:00
Akinwale Ariwodola 146fced44e
Merge pull request #1162 from kekkyojin/wallet-total
Use total wallet balance instead of spendable
2021-03-16 09:22:57 +01:00
Akinwale Ariwodola d535ed8c98
Merge pull request #1168 from lbryio/enable-phone-verification
Enable phone verification
2021-03-16 09:22:35 +01:00
Akinwale Ariwodola b675dbad9b Merge branch 'master' of https://github.com/lbryio/lbry-android 2021-03-16 08:59:42 +01:00
Javi Rueda dabe9fe691
Disable Javascript (#1171) 2021-03-15 20:08:18 +01:00
Javi Rueda 9ac216504d
Use ':' as separator for LBRY.TV sharing links (#1169) 2021-03-15 16:22:18 +01:00
Javi Rueda 911ca998e7 Use total wallet balance instead of spendable 2021-03-15 12:48:51 +01:00
Javi Rueda 36c105d3a7 Enable phone verification
Show the Close button after phone number has been verified
2021-03-11 17:46:17 +01:00
Akinwale Ariwodola a655d0112b
PR cleanup (#1164)
* simplify code for readability
* code-cleanup. Make non-changing variables final
* Bump buildToolsVersion 29.0.1 -> 29.0.2 for FDroid build compability
* Set gradle version to static 3.6.4 instead of dynamic 3.+
* Use StandardCharsets.UTF_8 instead of string UTF8
* Remove unused imports
* Add missing null check

Co-authored-by: Patric Karlström <patric@pkcab.eu>
2021-03-08 20:15:23 +01:00
Akinwale Ariwodola 493c771e94 sdk 0.91.0 2021-03-08 19:52:15 +01:00
Akinwale Ariwodola e9d70dbf87
Merge pull request #1163 from lbryio/iap-check
allow users to initiate a purchase flow check
2021-03-08 19:40:19 +01:00
Akinwale Ariwodola 0849ce2b66 allow users to initiate a purchase flow check 2021-03-08 19:38:00 +01:00
Javi Rueda d0a8b3b218
Fix mass tips unlocking (#1157) 2021-03-06 00:32:12 +01:00
Javi Rueda a8cdc4a771
Move the wallet floating button away when scroll gets to the bottom at FileViewFragment (#1155) 2021-03-04 19:11:19 +01:00
Javi Rueda 67b883660f
Hide support button and comments consistently with claim tags (#1152)
* Hide support button and comments consistently with claim tags

* Set visibility to GONE instead of INVISIBLE for the tipping button
2021-02-26 19:31:11 +01:00
Javi Rueda f328efb831
Allow media to be played automatically (#1153)
* Allow media to be played automatically

* Fix for media been always autoplayed when opened from miniplayer
2021-02-26 19:24:17 +01:00
Javi Rueda b2f56364d6
Use commentron instead of Comment SDK calls (#1149)
* Use commentron instead of Comment SDK calls

* Use current timestamp as 'id' for comment server request
2021-02-24 16:23:33 +01:00
Akinwale Ariwodola 2761857fe8
Merge pull request #1135 from kekkyojin/totvstring-unittest
Add unit test for LbryUri.toTvString
2021-02-22 20:07:55 +01:00
Akinwale Ariwodola d439260d69
Merge pull request #1132 from kekkyojin/ignore_helpertest
Change HelperTest to JUnit4 and remove example test class
2021-02-16 17:02:48 +01:00
Akinwale Ariwodola f0f0a5028b
Merge pull request #1123 from kekkyojin/style-code-tag
Styling <code> tag for Markdown content
2021-02-16 16:59:36 +01:00
Javi Rueda 13e170cb61 Add unit test for LbryUri.toTvString 2021-02-16 16:42:05 +01:00
Javi Rueda 127d8052ca Change HelperTest to JUnit4 and remove example test class 2021-02-12 01:28:22 +01:00
Javi Rueda 1b3086572f Styling <code> tag for Markdown content 2021-02-02 19:00:17 +01:00
Akinwale Ariwodola 6fa308ef96 bumpversion 0.16.13 --> 0.16.14 2021-01-25 18:54:00 +01:00
Akinwale Ariwodola f3e513fc2d Update translations and content filters 2021-01-25 18:52:21 +01:00
Akinwale Ariwodola d72a8faec4 sdk 0.88.0 2021-01-23 19:23:11 +01:00
Akinwale Ariwodola fdb4578349
Merge pull request #1119 from kekkyojin/remove-sha256
Remove Helper.SHA256() method
2021-01-18 20:24:22 +01:00
Javi Rueda c08237d399 Remove Helper.SHA256() method 2021-01-14 13:04:43 +01:00
Akinwale Ariwodola bdf8a58818 bumpversion 0.16.12 --> 0.16.13 2021-01-12 09:11:26 +01:00
Akinwale Ariwodola e1d51c881a Fix crash bug for anonymous repost claim results. Better handle channel repost navigation. 2021-01-12 09:07:33 +01:00
Akinwale Ariwodola 28212808f8
Merge pull request #1115 from pakar/master
Upgrade ExoPlayer from 2.11.4 to 2.12.2
2021-01-12 08:35:04 +01:00
Akinwale Ariwodola 74f08f8f98
Merge pull request #1102 from kekkyojin/fix-markdown-rendering
Use Base64 to encode html when rendering Markdown files
2021-01-12 08:33:40 +01:00
Akinwale Ariwodola cb5c29fbce
Merge pull request #1117 from kekkyojin/fix-nosuchmethod-commons
Fix NoSuchMethod exception for HelperSHA256 on API Level prior to P
2021-01-12 08:32:08 +01:00
Javi Rueda 4d775d3a17 Fix NoSuchMethod exception for HelperSHA256 on API Level prior to P 2021-01-11 20:44:03 +01:00
Patric Karlström fd94ef9fa7 Upgrade ExoPlayer from 2.11.4 to 2.12.2 2021-01-11 01:10:23 +01:00
Javi Rueda 2e0331305a Use Base64 to encode html when rendering Markdown files 2020-12-29 11:30:05 +01:00
Akinwale Ariwodola fb560f8f01 fix last resource build error 2020-12-29 07:20:46 +01:00
Akinwale Ariwodola ef80c9f7fd fix resource build error 2020-12-29 07:13:57 +01:00
Akinwale Ariwodola 99a4a0a22f Remove comment post confirmation. Fix resources build error. 2020-12-29 07:07:53 +01:00
Akinwale Ariwodola 136853853a bumpversion 0.16.11 --> 0.16.12 2020-12-29 07:02:13 +01:00
Akinwale Ariwodola ebf3299c30 add scrollbars attribute to listview 2020-12-29 07:00:45 +01:00
Akinwale Ariwodola 036b49a871
Merge pull request #1097 from kekkyojin/remove_stages_scrollbar
Avoid showing vertical scroll bar on stage error listview
2020-12-29 07:00:02 +01:00
Akinwale Ariwodola 67d5f88d34
Merge pull request #1100 from clay53/master
Changed wallet send notification to accept 4 digit decimal
2020-12-29 06:52:16 +01:00
Akinwale Ariwodola b1e0b9af33 sdk 0.87.0. Show confirmation for unfollow. 2020-12-29 06:47:10 +01:00
Clayton Hickey c30b012787 Changed wallet send notification to accept 4 digit decimal 2020-12-27 03:54:36 -05:00
Javi Rueda c8c6305757 Avoid showing vertical scroll bar on stage error listview 2020-12-22 02:26:37 +01:00
Akinwale Ariwodola ee1d090e62
Merge pull request #1068 from kekkyojin/fix-lintproblems
Some fixes for lint
2020-12-21 19:43:08 +01:00
Javi Rueda 9e56a86492 Some fixes for lint 2020-12-18 14:30:48 +01:00
Akinwale Ariwodola e6b83877f1
Merge pull request #1063 from clay53/1042
Removed comment tips
2020-12-18 13:01:49 +01:00
Akinwale Ariwodola b567e39aef
Merge pull request #1069 from kekkyojin/add-twitter-instructions
Provide instructions to add dummy Twitter API credentials
2020-12-18 12:22:03 +01:00
Akinwale Ariwodola ae62dba0a6
Merge pull request #1071 from kekkyojin/sha256-unittest
Add unit test for Helper.SHA256(String)
2020-12-18 12:21:39 +01:00
Akinwale Ariwodola df1e8abf50
Merge pull request #1067 from kekkyojin/use-apache-hex
Replace Hex class from GMS with the one from Apache
2020-12-18 12:20:42 +01:00
Javi Rueda 31cfb26c3b Add unit test for Helper.SHA256(String) 2020-12-15 13:45:32 +01:00
Javi Rueda e5f34dc464 Provide instructions to add dummy Twitter API credentials 2020-12-14 20:38:57 +01:00
Javi Rueda 601031e55d Replace Hex class from GMS with the one from Apache 2020-12-14 18:17:13 +01:00
Akinwale Ariwodola a9aadbe6a8 bumpversion 0.16.10 --> 0.16.11 2020-12-13 18:08:10 +01:00
Akinwale Ariwodola f9a4b71037 fix crash error when following doesn't exist in shared user state 2020-12-13 18:07:19 +01:00
Clayton Hickey 0f10e9dc1f Removed comment tips 2020-12-12 20:11:00 -05:00
Akinwale Ariwodola 0647deb06c
Merge pull request #1061 from kekkyojin/startupstage-listview
Use ListView to show startup stage errors
2020-12-12 23:16:17 +01:00
Akinwale Ariwodola b2f5fec293 make the channel filter name alphabet uppercase 2020-12-10 09:57:27 +01:00
Javi Rueda daf4e5aca2 Use ListView to show startup stage errors 2020-12-09 21:05:21 +01:00
Akinwale Ariwodola c1324efb41 bumpversion 0.16.9 --> 0.16.10 2020-12-09 11:47:58 +01:00
Akinwale Ariwodola 6221de2d3c sync notificationsDisabled states for followed channels 2020-12-09 11:31:13 +01:00
Akinwale Ariwodola 983bc68af2 Merge branch 'master' of https://github.com/lbryio/lbry-android 2020-12-09 11:00:25 +01:00
Akinwale Ariwodola f1b167693d SDK 0.86.1. is_seen vs is_read swap. Fix error condition where wallet sync starts if the wallet is not ready. 2020-12-09 10:59:25 +01:00
Akinwale Ariwodola 68ac64b534
Merge pull request #1057 from kekkyojin/sort-commentreplies
Sort replies from oldest to newest
2020-12-05 04:32:41 +01:00
Javi Rueda 6819ae46f9 Sort replies from oldest to newest 2020-12-01 04:58:48 +01:00
Akinwale Ariwodola 6c406c5a85 share_usage_data setting 2020-11-29 05:07:54 +01:00
Akinwale Ariwodola c179243d22 update dependencies 2020-11-26 00:04:37 +01:00
Akinwale Ariwodola d0f5504c80 LbryUri parsing improvements. Remove duplicate code. 2020-11-15 09:11:50 +01:00
Akinwale Ariwodola 896c566a02
Merge pull request #1045 from kekkyojin/open-file-external
Offer opening unsupported filetypes with external app
2020-11-13 15:42:59 +01:00
Javi Rueda da9352cc68 Offer opening unsupported filetypes with external app 2020-10-27 14:28:15 +01:00
Akinwale Ariwodola b8d2375e20
Merge pull request #1044 from ycohen-dev/fix_issue_1043
Fix floating wallet/rewards bar for RTL layout
2020-10-23 13:25:07 +01:00
Akinwale Ariwodola dd52ff9d07
Merge pull request #1033 from kekkyojin/md-text-rendering
Fix for text not rendering after # in markdown
2020-10-23 13:23:15 +01:00
yuval d9891f8a8a Floating wallet balance/rewards now visible in RTL layout
(Fix issue #1043)
2020-10-22 22:22:44 +03:00
Akinwale Ariwodola ea5fe6842d
Merge pull request #1041 from ycohen-dev/fix_issue_1040
fix issue 1040
2020-10-21 00:15:36 +01:00
Akinwale Ariwodola fc649187df
Merge pull request #1039 from ycohen-dev/fix_issue_1038
fix issue 1038
2020-10-21 00:14:15 +01:00
yuval b0f7c41885 fix issue 1040
Transaction amount text Direction changed to LTR.
Transaction amount pinned to end of view
2020-10-17 22:18:14 +03:00
yuval 45935717c8 fix issue 1038
Replaced gravity pinning to "end" instead of "right"
Pinned claim id which could be in rtl and ltr scripts to the start of the layout
2020-10-16 23:40:28 +03:00
Akinwale Ariwodola ac5e666369 bumpversion 0.16.8 --> 0.16.9 2020-10-16 06:58:57 +01:00
Akinwale Ariwodola ffeb72a383 Publish mature tags. String resource updates. 2020-10-16 06:53:08 +01:00
Akinwale Ariwodola 0ec09d751c
Merge pull request #1036 from ycohen-dev/fix_issue_1034
Fix issue #1034
2020-10-16 06:39:45 +01:00
Akinwale Ariwodola b8e4fcff92
Merge pull request #1037 from lbryio/follow-text
add follow text beside follow icon
2020-10-16 06:37:50 +01:00
Akinwale Ariwodola b79f0f4820 add follow text beside follow icon 2020-10-16 06:36:36 +01:00
yuval a1cd58a214 Fix issue #1034
Hide notifications when entering PiP mode
2020-10-16 00:11:08 +03:00
Javi Rueda cfca8facbe Use '%23' instead of '#' when rendering #hashtag style text from markdown 2020-10-15 01:49:53 +02:00
Akinwale Ariwodola 066a0a099c
Merge pull request #1032 from lbryio/wallet-balance-page
use credits icon on wallet balance page
2020-10-14 15:33:25 +01:00
Akinwale Ariwodola afeee2e5df use credits icon on wallet balance page 2020-10-14 15:30:59 +01:00
Akinwale Ariwodola 76036acfc7 Merge branch 'master' of https://github.com/lbryio/lbry-android 2020-10-14 15:17:35 +01:00
Akinwale Ariwodola 897bfdaffd fix build error 2020-10-14 15:16:45 +01:00
Akinwale Ariwodola 7475ae323c
Merge pull request #1031 from ycohen-dev/fix_issue_1030
Fix issue #1030
2020-10-14 15:12:34 +01:00
Akinwale Ariwodola faf7f3ccbf make credits translatable 2020-10-14 15:11:36 +01:00
Akinwale Ariwodola 4940d1ca33 Merge branch 'master' of https://github.com/lbryio/lbry-android 2020-10-14 15:10:25 +01:00
Akinwale Ariwodola 3d48fa5741 also check pending when checking if item is a placeholder 2020-10-14 15:09:59 +01:00
yuval f9887cffae Fix issue #1030
Dismiss all active dialog fragment when entering PiP mode
So that PiP window contain the video only
2020-10-13 23:17:58 +03:00
Akinwale Ariwodola 6644907665
Merge pull request #1027 from ycohen-dev/fix_issue_1025
Fix issue #1025
2020-10-12 19:25:48 +01:00
Akinwale Ariwodola 5f850685d6 bumpversion 0.16.7 --> 0.16.8 2020-10-12 11:48:24 +01:00
Akinwale Ariwodola 5f9674e49c do not open lbry.*/$/verify links in the app 2020-10-12 11:47:45 +01:00
yuval 0d185c6db3 Fix issue #1025
Increased size of currency selection spinner in publish form fragment
Now spinner is in proper size to display "LBRY Credits" default value
2020-10-10 16:58:29 +03:00
Akinwale Ariwodola 53d22dd22d remove debug logs 2020-10-09 07:43:38 +01:00
Akinwale Ariwodola ff8ffda3c6 bumpversion 0.16.6 --> 0.16.7 2020-10-09 07:39:46 +01:00
Akinwale Ariwodola 64bd540322
In-app notification options (#1023)
* pull-down to refresh in-app notifications
* support deleting notifications
2020-10-09 07:38:47 +01:00
Akinwale Ariwodola 919f9a48f3
Merge pull request #1022 from lbryio/bell-icon
per-channel subscribe notification toggle
2020-10-09 06:58:08 +01:00
Akinwale Ariwodola 746d442051 per-channel subscribe notification toggle 2020-10-09 06:56:13 +01:00
Akinwale Ariwodola 1877b75188 fix strings.xml 2020-10-06 07:14:42 +01:00
Akinwale Ariwodola 6e8c38cace update module versions 2020-10-05 16:56:17 +01:00
Akinwale Ariwodola eab7bab267
Credits icon (#1021)
* new credits icon
* remove references to LBC
2020-10-05 16:50:39 +01:00
Akinwale Ariwodola aab648c3cf
Fix errors with deep link handling (#1019) 2020-10-02 12:39:37 +01:00
Javi Rueda f79ff509bd
Support some lbry hosts to deep links. Add unit tests (#1008)
* Support lbry.tv and other supported hosts for deep links. Add unit tests for LbryUri parse() method
* add unit test for lbry protocol schema
* Add unit test for lbry.tv deep-link with only channel
2020-10-02 12:34:38 +01:00
Akinwale Ariwodola 1197e990ca cleanup debug logging 2020-10-02 12:09:33 +01:00
Akinwale Ariwodola dce1c0715e move Unpublish button 2020-10-02 12:08:33 +01:00
Akinwale Ariwodola 6a0263c5bc
Merge pull request #1013 from clay53/987
Add ability to download & delete your own videos and move unpublish button to description
2020-10-02 11:05:44 +01:00
Akinwale Ariwodola ca64db0499 sdk 0.82.0 2020-10-01 12:42:48 +01:00
Akinwale Ariwodola a5d4eda4d1 bumpversion 0.16.5 --> 0.16.6 2020-09-25 14:45:48 +01:00
Akinwale Ariwodola 5d210961c1 apply mature filter to Editor's Choice 2020-09-25 14:41:40 +01:00
Akinwale Ariwodola 1b88f565af bumpversion 0.16.4 --> 0.16.5 2020-09-23 16:37:41 +01:00
Akinwale Ariwodola 991a98b571 fix shuffle mode player getting stuck 2020-09-23 16:35:59 +01:00
Akinwale Ariwodola 0073277d6e
Merge pull request #1017 from lbryio/shuffle-random
keep track of watched content for subsequent shuffle sessions
2020-09-23 16:23:57 +01:00
Akinwale Ariwodola eeca602f7a keep track of watched content for subsequent shuffle sessions 2020-09-23 15:31:34 +01:00
Akinwale Ariwodola 6c171560fd remove Finnish strings 2020-09-18 14:56:35 +01:00
Akinwale Ariwodola 66c4c00215 bumpversion 0.16.3 --> 0.16.4 2020-09-18 14:54:59 +01:00
Akinwale Ariwodola 9b9ef9ab74
Surf mode experiment (#1003)
* surf mode implementation
* occupy entire vertical area
* fix onResume logic
* shuffle mode with selected channel ids
2020-09-18 14:53:19 +01:00
Clayton Hickey 39a289e7f1
Add ability to download & delete your own videos and move unpublish button to description 2020-09-16 14:31:03 -04:00
Akinwale Ariwodola 722c829502 fix paid content stream source 2020-09-14 18:21:31 +01:00
Akinwale Ariwodola 7ecb80d136
Merge pull request #1009 from lbryio/mini-player-margin
add setting for mini-player bottom spacing
2020-09-14 18:05:09 +01:00
Akinwale Ariwodola ea19af04d4 add setting for mini-player bottom spacing 2020-09-14 18:01:38 +01:00
Akinwale Ariwodola 8196b69211 bumpversion 0.16.2 --> 0.16.3 2020-09-14 11:20:47 +01:00
Akinwale Ariwodola dc861caf6c fix websocket for Android < 7.0 2020-09-14 11:19:50 +01:00
Akinwale Ariwodola adb5ffa8d0 exclude x86_64 lib from build 2020-09-14 11:06:39 +01:00
Akinwale Ariwodola d918cb28bd sdk 0.81.0 2020-09-11 13:50:16 +01:00
Akinwale Ariwodola 7ce7314ab9 bumpversion 0.16.1 --> 0.16.2 2020-09-11 13:41:30 +01:00
Javi Rueda 13e5caa0ef
Use Start/End instead of Left/Right to support RTL (#1005)
* Use Start/End instead of Left/Right to support RTL
* Change it also for relative layout properties
2020-09-11 13:28:37 +01:00
Akinwale Ariwodola ff59a7a89f
Merge pull request #1007 from lbryio/first-run-install-id
generate install_id natively
2020-09-11 13:22:42 +01:00
Akinwale Ariwodola c3efa4d004 generate install_id natively 2020-09-10 12:20:49 +01:00
Akinwale Ariwodola ed50e1300a
real time notifications over websocket (#1000)
* real time notifications over websocket
* automatically re-establish websocket connections
2020-09-09 14:16:22 +01:00
Akinwale Ariwodola 86dbfd54d1
Merge pull request #1006 from lbryio/notification-back
proper back navigation to the notifications screen
2020-09-09 14:05:29 +01:00
Akinwale Ariwodola 535120eebd proper back navigation to the notifications screen 2020-09-09 14:03:36 +01:00
Akinwale Ariwodola c1106d7186 display correct playback speed for media 2020-09-04 12:50:22 +01:00
Akinwale Ariwodola 45be7f2c9b
Merge pull request #1004 from lbryio/player-resume-black-screen
display player view correctly after dismissing PIP view
2020-09-04 12:33:53 +01:00
Akinwale Ariwodola d6baf3d1c8 display player view correctly after dismissing PIP view 2020-09-04 12:32:53 +01:00
Akinwale Ariwodola 08c38c1723 bumpversion 0.16.0 --> 0.16.1 2020-08-28 10:07:20 +01:00
Javi Rueda af4fa454d3
Add setting to enable sending buffering events (#993)
* Add setting to enable sending buffering events
* Use true as default value
2020-08-28 10:06:41 +01:00
Akinwale Ariwodola 0620582a4e fix in-app notification read background in dark mode 2020-08-28 10:04:10 +01:00
Akinwale Ariwodola b11c07e3d1 fix comment redirect from device notifications 2020-08-28 10:00:57 +01:00
Akinwale Ariwodola 593b34079c load notifications after app startup 2020-08-20 20:47:14 +01:00
Akinwale Ariwodola becb533624 fix notification relative time display 2020-08-20 20:32:22 +01:00
Akinwale Ariwodola 8efc0522f2 load unread count after fetching remote notifications 2020-08-20 20:03:30 +01:00
Akinwale Ariwodola cead924ca5 Notification timestamp timezone fix. Tweak loading unread notification count. 2020-08-20 19:56:35 +01:00
Akinwale Ariwodola f83a043664 fix notification sorting 2020-08-20 19:00:21 +01:00
Akinwale Ariwodola e8a0bca5ea tweak equality check for LbryNotification 2020-08-20 18:45:02 +01:00
Akinwale Ariwodola 1198c2298a add and update translations 2020-08-20 18:28:32 +01:00
Akinwale Ariwodola 7205acb9d6 Sort newly added notifications. Add remote loading TTL. 2020-08-20 18:06:35 +01:00
Akinwale Ariwodola d21cfd55ab Handle special URLs from notifications. Disable auto load/play for comment notifications. 2020-08-20 16:39:45 +01:00
Akinwale Ariwodola 47cbc3624c
Merge pull request #980 from lbryio/channel-comment-hash
properly handle channel comment notifications
2020-08-20 16:26:08 +01:00
Akinwale Ariwodola 1f9a0886a0 properly handle channel comment notifications 2020-08-20 16:20:46 +01:00
Akinwale Ariwodola dd5ea68915 new YRBL 2020-08-20 14:52:41 +01:00
Akinwale Ariwodola b3bba4d273
Merge pull request #979 from lbryio/is-seen
check is_seen flag. display comment author thumbnails if present.
2020-08-20 13:30:28 +01:00
Akinwale Ariwodola 10c123ba6a check is_seen flag. display comment author thumbnails if present. 2020-08-20 13:26:41 +01:00
Akinwale Ariwodola a8ba45b941 bumpversion 0.15.17 --> 0.16.0 2020-08-19 18:18:37 +01:00
Akinwale Ariwodola 3652cdf6bc CI apt-transport-https 2020-08-19 18:14:13 +01:00
Akinwale Ariwodola f2a7a8c439 cleanup: yarn.lock 2020-08-19 18:10:57 +01:00
Akinwale Ariwodola 78d5a99441
Gradle fix (#978) 2020-08-19 18:09:37 +01:00
Akinwale Ariwodola 6931dbe79c
Instant verification (#974)
* add instant verification options and Google Play Billing
bumpversion 0.15.16 --> 0.15.17
restore account_undergo_review default string
twitter sign-in flow
fix build error
final changes
update api key and secret
* tweak build script
2020-08-19 17:23:35 +01:00
Akinwale Ariwodola 4d024c06cc
In-app notifications (#969)
* display notification bell icon beside URL bar
* notification model
* persist received notifications
* Refresh notification list. Add  is_read flag and display unread count.
* Hide notifications when opening the navigatinon drawer. Open selected notification item before hiding the list.
* update local store with remote notifications
* use card view for notification items
* make notification cards clickable
* handle comment hash
* fix remote notifications not loading. fix comment scroll.
* Improve in-app comment notification linking. Add icons.
2020-08-18 14:19:35 +01:00
Akinwale Ariwodola ddcb190457 do not display global background servce initializing indicator 2020-08-07 10:11:57 +01:00
Akinwale Ariwodola dd14b90d7e
Merge pull request #967 from kekkyojin/sendto-feat
Allow to publish files via Send To feature
2020-07-29 15:20:59 +01:00
Akinwale Ariwodola 4b86444478 bumpversion 0.15.15 --> 0.15.16 2020-07-27 11:11:10 +01:00
Javi Rueda feb7f260dc Allow to publish files via Send To feature 2020-07-24 01:59:59 +02:00
Akinwale Ariwodola 74c7a1de4d sdk 0.79.1. Clear resolved sub cache after sync. 2020-07-23 20:04:48 +01:00
Akinwale Ariwodola 88a43dc679 set sync interval back to 5 minutes 2020-07-22 05:52:44 +01:00
Akinwale Ariwodola 1bb5ce72fa Proper fix for subscription sync retention. Eliminate double sync attempt on startup. 2020-07-22 05:49:48 +01:00
Akinwale Ariwodola b4ce544965 bumpversion 0.15.14 --> 0.15.15 2020-07-21 21:25:26 +01:00
Akinwale Ariwodola 1ec6f6173a sdk 0.79.0 2020-07-21 21:13:05 +01:00
Akinwale Ariwodola 8ee19b3f5c
Sub sync fix (#965)
* improve sub sync merge handling logic
* do not try to re-subscribe local syncs remotely whhen merging
2020-07-21 19:49:22 +01:00
Akinwale Ariwodola 1055392b7f temporarily omit duration 2020-07-21 19:39:47 +01:00
Akinwale Ariwodola c772cf2ead
check buffering events (#952)
* check buffering events
* use the claim permanent url for buffer events
* update request parameters
* hash user ids for buffer events
* update buffer event endpoint
2020-07-21 19:30:45 +01:00
Akinwale Ariwodola 481c50f465 fix open.lbry.com regex 2020-07-17 11:39:59 +01:00
Akinwale Ariwodola 71bc969f2a
Merge pull request #962 from kekkyojin/deeplink
Add open.lbry.com deep-links
2020-07-17 11:29:16 +01:00
Akinwale Ariwodola e4edebfed7 fix for follow / unfollow confusion 2020-07-16 13:51:08 +01:00
Javi Rueda e8a5ab8307 Add open.lbry.com deep-links 2020-07-13 19:00:12 +02:00
Akinwale Ariwodola 3738f3af21
Merge pull request #954 from lbryio/invalidated-auth
gracefully handle invalidated auth tokens
2020-07-05 19:07:21 +01:00
Akinwale Ariwodola 74d10e4199 gracefully handle invalidated auth tokens 2020-07-05 18:40:31 +01:00
Akinwale Ariwodola 080e00becf
Merge pull request #949 from kekkyojin/patch-1
Bintray repository is no longer needed
2020-06-30 15:30:18 +01:00
Javi Rueda 049d905d7d
Bintray repository is no longer needed
LBRY SDK is now hosted on JCenter, which is already available out-of-the-box on Android Studio. The Bintray url is no longer reguired. When building LBRY on F-Droid, I have to remove this line on the build YAML script and it is building correctly.

On my local machine, it builds after commenting it.
2020-06-26 21:06:12 +02:00
Akinwale Ariwodola 33094f8c88 fix empty bindArgs crash error for sql query to clear subscriptions 2020-06-25 23:33:04 +01:00
Akinwale Ariwodola 6c44e503db
fix subscriptions sync (#947) 2020-06-25 21:16:41 +01:00
Akinwale Ariwodola fa78c80592 fix ClaimListAdapter and strings.xml 2020-06-25 20:42:55 +01:00
Akinwale Ariwodola 07630ca97b fix: crash bugs 2020-06-25 20:23:24 +01:00
Akinwale Ariwodola a2b6d4e570 bumpversion 0.15.13 --> 0.15.14 2020-06-25 20:16:38 +01:00
Akinwale Ariwodola 9d6b3ddf81
combined tip / support dialog and signed supports (#944)
* combined tip / support dialog and signed supports
* update button label when amount is updated
2020-06-25 20:15:58 +01:00
Akinwale Ariwodola 0a2769859a fix issue with featured search result crash 2020-06-24 10:45:29 +01:00
Akinwale Ariwodola c4a1ed801a make FontAwesome strings non-translatable 2020-06-19 07:18:19 +01:00
Akinwale Ariwodola 453394bf1b update README 2020-06-19 06:57:48 +01:00
Akinwale Ariwodola 4b7dfba5a1 sdk 0.76.0 2020-06-15 19:43:41 +01:00
Akinwale Ariwodola e699fcf0b3 remove broken Bengali strings.xml 2020-06-15 19:29:26 +01:00
Akinwale Ariwodola 7e80f707e0 bumpversion 0.15.12 --> 0.15.13 2020-06-15 19:17:18 +01:00
Akinwale Ariwodola 467f037170 sFix default sort on channel page. Add Bengali and Javanese translations. 2020-06-15 19:11:20 +01:00
Akinwale Ariwodola 89d5b40e5f only allow sending to addresses that begin with b 2020-06-15 15:18:42 +01:00
Akinwale Ariwodola 34fba010e1 use lbryplayer.xyz streaming URLs for signed in users 2020-06-15 15:07:17 +01:00
Akinwale Ariwodola d7396bb044
add MoonPay 'Buy LBC' button (#937)
* add MoonPay 'Buy LBC' button
* tweak button style
* tweak MoonPay parameters
2020-06-13 16:14:06 +01:00
joaquimbrugues fda0817ad1
Add Catalan language (#928)
* Add Catalan language
* Add 'catalan' string to strings.xml
* Attempt to fix conflicting changes
2020-06-11 19:41:53 +01:00
Akinwale Ariwodola 895ca75506
Merge pull request #935 from lbryio/edit-publish-channel
auto select corresponding channel when editing a publish
2020-06-11 19:38:24 +01:00
Akinwale Ariwodola a04448ebe8 auto select corresponding channel when editing a publish 2020-06-11 19:31:06 +01:00
Akinwale Ariwodola 26ccbf2709 reduce comment cost from 2 to 1 2020-06-10 09:54:07 +01:00
Akinwale Ariwodola 7d6c11a88c Remove lbrysdk AARs. Add bintray dependencies. 2020-06-09 22:27:42 +01:00
Akinwale Ariwodola 8742913c33 SDK 0.75.0. Update translations. 2020-06-05 15:31:13 +01:00
Akinwale Ariwodola 8e6e0f0099 fix player notification pending intent 2020-06-05 09:42:45 +01:00
Akinwale Ariwodola a4585de807 reset playback notification large icon when content changes 2020-06-05 09:40:22 +01:00
Akinwale Ariwodola 6c24749ad5 tweak player view resume 2020-06-05 09:34:45 +01:00
Akinwale Ariwodola c73509a4ca set large icon for playback notification 2020-06-05 09:30:54 +01:00
Akinwale Ariwodola 673b85b2aa bumpversion 0.15.11 --> 0.15.12 2020-06-05 09:07:35 +01:00
Akinwale Ariwodola fe6e60cd67 implement background playback with control notification 2020-06-05 09:04:24 +01:00
Akinwale Ariwodola 8bcee90d68 Add background playback setting. Fix crash bugs. 2020-06-05 08:25:08 +01:00
Akinwale Ariwodola 084407e129 Update translations. Add Catalan and Serbian strings. 2020-06-05 07:50:52 +01:00
Akinwale Ariwodola 4a3db5ae20 enforce minimum spend / deposits 2020-06-05 07:40:44 +01:00
Akinwale Ariwodola b36813ebe6 fix nsfw search flag 2020-06-05 07:23:56 +01:00
Akinwale Ariwodola cc1ddfa16e use only txids for unlocking tips 2020-06-05 07:17:56 +01:00
Akinwale Ariwodola 5f1775d478 fix possible null item in channel id list 2020-06-05 07:04:54 +01:00
Akinwale Ariwodola 288e35dd64 fix margins 2020-06-02 09:40:17 +01:00
Akinwale Ariwodola d4af28d1c5 display publisher title and thumbnail on file page 2020-06-02 09:34:59 +01:00
Akinwale Ariwodola 37cf85a5f6 bumpversion 0.15.10 --> 0.15.11 2020-06-02 09:05:04 +01:00
Akinwale Ariwodola c2cbd76f49 change comment confirmation title 2020-06-02 09:03:56 +01:00
Akinwale Ariwodola 3d874435c3 change BigDecimal initialisation for comment cost 2020-06-02 08:57:12 +01:00
Akinwale Ariwodola 9ed8864f23 More crash fixes. Change comment cost to 2 LBC. 2020-06-02 08:55:34 +01:00
Akinwale Ariwodola 61263dd59d bumpversion 0.15.9 --> 0.15.10 2020-06-01 08:56:38 +01:00
Akinwale Ariwodola 2707e135c6 comment creation event 2020-06-01 08:53:58 +01:00
Akinwale Ariwodola f9de0d7937 do not prompt again for content that is already purchased 2020-06-01 08:48:17 +01:00
Akinwale Ariwodola f03d58d648 tweak PIP player for certain aspect ratios 2020-06-01 08:33:07 +01:00
Akinwale Ariwodola f49a3570e9 fix file view display for cached reposts 2020-05-31 21:22:37 +01:00
Akinwale Ariwodola 1d97f9008d
implement commenting with tips (#922)
* implement commenting with tips
* add comments to channel page
2020-05-31 21:06:03 +01:00
Akinwale Ariwodola e85ca9114c
Merge pull request #921 from lbryio/crash-fixes
fixes for Play Store crash reports
2020-05-31 16:20:59 +01:00
Akinwale Ariwodola 85ed2da518 fixes for Play Store crash reports 2020-05-31 16:19:33 +01:00
Clayton Hickey dcc91f75cc
Added ability to read comments on claims (#920)
* Added ability to read comments on claims
* Added simple handling for pagination in CommentListTask
2020-05-30 00:47:50 +01:00
Akinwale Ariwodola a40c160ac6 fix selection mode for reposts on Publishes page 2020-05-28 03:45:56 +01:00
Akinwale Ariwodola e6861c8436 set timestamp when building claim object from edited claim 2020-05-28 03:25:42 +01:00
Akinwale Ariwodola e6a5d97fb8 bumpversion 0.15.8 --> 0.15.9 2020-05-28 03:23:45 +01:00
Akinwale Ariwodola ddeb209d51 fix publish release time and crash bug on publish form 2020-05-28 03:22:20 +01:00
Akinwale Ariwodola 3283b3a607 fix channel links on history page 2020-05-27 23:22:08 +01:00
Akinwale Ariwodola 83a41ca6ce auto-rotate video to sensor landscape orientation 2020-05-27 23:16:25 +01:00
Akinwale Ariwodola 6c63eb7d66 better handling of permanent storage permission denial on file view page 2020-05-27 23:11:09 +01:00
Akinwale Ariwodola 7cf9de8c2a fix typo 2020-05-27 23:01:00 +01:00
Akinwale Ariwodola a859682954 fix permission request handling for permanently denied permissions 2020-05-27 22:56:07 +01:00
Akinwale Ariwodola c844c4f896 fix PIP mode display for Android version < 10 2020-05-27 11:20:51 +01:00
Akinwale Ariwodola a49cfe91da remove debug logs 2020-05-27 10:58:28 +01:00
Akinwale Ariwodola 2bacba2c87 properly close all activities when service is stopped 2020-05-27 10:52:25 +01:00
Akinwale Ariwodola e473981063 fix more crash bugs reported in Play Store 2020-05-27 08:25:10 +01:00
Akinwale Ariwodola 34ea4f216c add Dutch and Hindi strings 2020-05-27 02:06:41 +01:00
Akinwale Ariwodola 52e34b3027 don't hide play / pause button while buffering 2020-05-27 01:15:02 +01:00
Akinwale Ariwodola c6a6ea0445 reset click listener onStop 2020-05-27 01:11:18 +01:00
Akinwale Ariwodola 59ae6340e1 disable gif animation in thumbnails 2020-05-27 00:56:07 +01:00
Akinwale Ariwodola cbf2aa2311 bumpversion 0.15.6 --> 0.15.7 2020-05-26 16:26:15 +01:00
Akinwale Ariwodola 94b0c7bc01 Fix additional Play Store crash bugs. 2020-05-26 16:25:37 +01:00
Akinwale Ariwodola 4c50ffac19
Merge pull request #898 from lbryio/user-bugs
fix reported bugs from 0.15.3
2020-05-26 16:09:51 +01:00
Akinwale Ariwodola 551e736651 fix reported bugs from 0.15.3 2020-05-26 16:09:14 +01:00
Akinwale Ariwodola 0380e35966 bumpversion 0.15.5 --> 0.15.6 2020-05-26 10:58:47 +01:00
Akinwale Ariwodola 5de0906fc1 make splash view clickable to prevent clicking views underneath 2020-05-26 10:57:12 +01:00
Akinwale Ariwodola c2f17b6230 Add Italian strings. Save and resume from last media playback position. 2020-05-26 10:38:16 +01:00
Akinwale Ariwodola 6a3bbe6c0d bumpversion 0.15.4 --> 0.15.5 2020-05-25 20:10:34 +01:00
Akinwale Ariwodola 82307c9f98 Increase timeout for remote user requests. Wrap long pre lines in markdown display. 2020-05-25 20:08:24 +01:00
Akinwale Ariwodola d2ec0e2aa1 Display reposts on following page. Update Spanish strings. 2020-05-25 15:15:48 +01:00
Akinwale Ariwodola 3951b1080d different new publish from publish update 2020-05-25 12:14:12 +01:00
Akinwale Ariwodola ef7caeeead Fix subsequent web view display. Add Estonian and Russian strings. 2020-05-25 11:33:12 +01:00
Akinwale Ariwodola 7ef8e2029e Tweak transaction pending text display 2020-05-25 06:03:46 +01:00
Akinwale Ariwodola aaec6ac6a3 wait for uploads to finish before saving channel form 2020-05-25 06:00:34 +01:00
Akinwale Ariwodola 731960da7c bumpversion 0.15.3 --> 0.15.4 2020-05-25 05:50:02 +01:00
Akinwale Ariwodola ac282d2667 log cleanup 2020-05-25 05:48:26 +01:00
Akinwale Ariwodola 7a79601cfc don't enable fullscreen mode when entering PIP mode 2020-05-25 05:47:14 +01:00
Akinwale Ariwodola d1167e4d2b Disable selection mode for purchases. Check additional cases for displaying unsupported content message. 2020-05-25 05:38:25 +01:00
Akinwale Ariwodola 613634adcf get transaction id for first_publish and new_channel rewards 2020-05-25 00:32:21 +01:00
Akinwale Ariwodola acbe33c66d Limit gallery items to 150. Download filtering. Open rewards page from Invites reward driver card. 2020-05-25 00:04:40 +01:00
Akinwale Ariwodola 053ebbd70b dDisplay better error message if authentication fails on first run. Change lineHeight to lineSpacingMultiplier. 2020-05-24 21:27:49 +01:00
Akinwale Ariwodola f5dc4fa4e7 Remove new intent debug logs. Update packages in build.gradle 2020-05-24 20:48:01 +01:00
Akinwale Ariwodola 343270b757 Show auto-claim rewards message at activity level. Add German strings. 2020-05-24 20:34:19 +01:00
Akinwale Ariwodola 65f5626aba More tweaks to PIP mode restore. 2020-05-24 20:29:11 +01:00
Akinwale Ariwodola a051a73c2b Tweak PIP restore. Fix Library wunderbar value. 2020-05-24 18:54:33 +01:00
Akinwale Ariwodola 6840997793 Add 'subscription' to special URLs map. Fix PIP restore for onNewIntent. 2020-05-24 18:46:47 +01:00
Akinwale Ariwodola 2c98ed2d8d Always display rewards driver on Invites page. Rewards drivers minimum display amounts. 2020-05-24 18:37:05 +01:00
Akinwale Ariwodola 6a083c4152 Update Afrikaans strings. Add Vietnamese strings. 2020-05-24 18:18:39 +01:00
Akinwale Ariwodola b60b5d16c3 fix crash bugs reported in Play Store 2020-05-24 10:11:31 +01:00
Akinwale Ariwodola 52cfe8dc12 add blocking param to txo_spend 2020-05-24 05:02:55 +01:00
Akinwale Ariwodola 737afca031 fix onNewIntent logging 2020-05-24 04:50:13 +01:00
Akinwale Ariwodola 456f41d28d Prevent image click through. Add Romanian strings. 2020-05-24 04:43:39 +01:00
Akinwale Ariwodola 238f59ad71 display correct label and amount for tip unlocks in tx history 2020-05-24 00:08:14 +01:00
Akinwale Ariwodola cf6d567193 unify button font 2020-05-23 23:47:32 +01:00
Akinwale Ariwodola 20f8f3852d
Merge pull request #888 from lbryio/unlock-tips
add option to unlock all tips
2020-05-23 23:39:16 +01:00
Akinwale Ariwodola 4bb9f5cb43 add option to unlock all tips 2020-05-23 23:37:57 +01:00
Akinwale Ariwodola 540a841255 Fix pip restore and fullscreen quirks. French and Portuguese strings added. 2020-05-23 19:42:30 +01:00
Akinwale Ariwodola 3544637405 Fix reward code claiming. Remove broken French strings file. 2020-05-23 17:48:58 +01:00
Akinwale Ariwodola 651448dfa1
Merge pull request #886 from lbryio/reward-drivers
Add reward drivers. French, Indonesian, Malay and Turkish strings.
2020-05-23 17:46:16 +01:00
Akinwale Ariwodola 4bac266b8b Add reward drivers. French, Indonesian, Malay and Turkish strings. 2020-05-23 17:45:19 +01:00
Akinwale Ariwodola f86b61741d
Merge pull request #885 from lbryio/visual-tweaks
Use thin divider in transaction history. Set font for action bar title.
2020-05-23 15:18:43 +01:00
Akinwale Ariwodola 6c60d299af Use thin divider in transaction history page. Set font for action bar title. 2020-05-23 15:17:43 +01:00
Akinwale Ariwodola 08582238c3
Merge pull request #884 from lbryio/dark-theme-fix
Fix dark theme reset error due to WebView. Apply dark theme to web views.
2020-05-23 09:41:59 +01:00
Akinwale Ariwodola b931d0ce7d Fix dark theme reset error due to WebView. Apply dark theme to web views. 2020-05-23 09:37:58 +01:00
Akinwale Ariwodola 5e1fc9dbd8
Merge pull request #883 from lbryio/fullscreen-on-rotate
switch to fullscreen mode on rotate when viewing media
2020-05-23 09:03:00 +01:00
Akinwale Ariwodola de4e6eee25 switch to fullscreen mode on rotate when viewing media 2020-05-23 09:01:30 +01:00
Akinwale Ariwodola d5b4e6990f update material components version 2020-05-23 07:53:58 +01:00
Akinwale Ariwodola 26703815b2 cleanup top level directory 2020-05-23 07:51:56 +01:00
Akinwale Ariwodola 40c36df414
Native rewrite (#878)
* initial native rewrite commit
* update gitlab CI script
* add printVersionName gradle task
* fix Gitlab CI script
* Fix first time wallet sync. add Discover dialog to  Following page.
* Finish Following and All Content views. Add customize your tags view.
* Wallet sync get and set preferences, update interval.
* Editor's Choice. Reposts. Some ontent page tweaks.
* display no related content view when none loaded
* Search cache. File view updates. Floating wallet balance.
* Send tip dialog. Channel page share and follow/unfollow.
* Handle lbry:// url scheme. Properly set URL bar values. SDK 0.71.0.
* Channel follow/unfollow fixes. Display stream cost.
* Channel management and channel creation / editing
* phone number verification and rewards page
* add Invites page
* tweak player loading and playback when loading new claims
* tweak about page layout
* display text and markdown content
* purchase_uri for free content
* don't display invites history if none exist
* fix channel list adapters
* change launch mode from singleInstance to singleTask
* url history and player fixes
* Library page. URL and view history.
* bumpversion 0.15.0 --> 0.15.1
* Make file view a fragment to prevent headaches with multiple Android task recents
* Better handling of file view URLs. Some issue list fixes.
* Abandon channels and bulk delete files tasks. Some visual tweaks.
* bumpversion 0.15.1 --> 0.15.2
* fix some events
* Some visual tweaks. Wunderbar clear focus hotfix for some devices.
* sdk 0.74.0. Publish and Publishes pages.
* Fix displayed amounts. Send Firebase token with install_new.
* Some dark theme and crash fixes. Implement publish form.
* Fix minor typo for string in 'generate_address_hint'.
* Publish form and publish creation flow. UI tweaks and fixes.
* Basic native mobile publishing
* remove closeDatabase calls causing crashes
* Implement file and channel page delete actions. UI action cleanup.
* publish drivers for unresolved file page and featured search result item
* show URL suggestions and data network (DHT) settings
* Filter own claims from downloads. Fix address input.
* fix edit channel crash
* fix for possible blank / invalid video thumbnails
* adjust minimum deposit. fix channel edit mode.
* quick skip and playback speed media controls
* change play and pause icons
* Fix file size display. Tweak playback speed control.
* Add exoplayer mediasession extension. Set player auto attributes.
* Inline publish address validation error. Increase image upload request timeout.
* fix no related content display
* Claim new_android reward. Use canonical_url for share links.
* force US locale for amount / bid values sent with sdk requests
* Afrikaans and Spanish strings
* Add media player error handling policy. Use : in share links.
* don't proceed with publish if optimization is in progress.
2020-05-23 07:49:00 +01:00
Akinwale Ariwodola 6eb20d0e08 sdk 0.73.1 patch 2020-05-14 14:40:12 +01:00
Akinwale Ariwodola cc3055f1c9 bumpversion 0.14.1 --> 0.14.2 2020-03-31 21:32:35 +01:00
Akinwale Ariwodola 25a1eac43b sdk 0.67.1 2020-03-31 21:18:19 +01:00
Akinwale Ariwodola ff30e7f6a4 bumpversion 0.14.0 --> 0.14.1 2020-03-26 19:06:51 +01:00
Akinwale Ariwodola 3fcc01597d sdk 0.65.0 2020-03-26 19:03:47 +01:00
Akinwale Ariwodola cbc4a662e6 bumpversion 0.13.4 --> 0.14.0 2020-03-23 09:45:21 +01:00
Akinwale Ariwodola 5aa0513324 add getNativeBooleanSetting method 2020-03-21 09:18:48 +01:00
Akinwale Ariwodola 4d1f142f9c check sdk ready 2020-03-20 11:15:41 +01:00
Akinwale Ariwodola e9c8f9432f fix service notification not opening the app 2020-03-20 09:27:54 +01:00
Akinwale Ariwodola 56c375f344
lbry.tv hybrid mode (#869)
* lbry.tv experiment build
* add task for checking if the sdk is ready
* fix for special urls
* send onSdkStatusResponse events
* persist dht setting to file system
2020-03-20 08:30:34 +01:00
Akinwale Ariwodola 5b165a2339 remove dht debug logs 2020-03-17 23:10:10 +01:00
Akinwale Ariwodola bfd3c711ab
0.13.4 patch (#868)
* 0.13.4 patch
* update gitlab CI
2020-03-16 18:50:17 +01:00
Akinwale Ariwodola 4627e284fa sdk 0.64.0. check dht setting on startup. 2020-03-13 17:06:05 +01:00
Akinwale Ariwodola ea8ac783a8 bumpversion 0.13.2 --> 0.13.3 2020-03-13 08:32:09 +01:00
Akinwale Ariwodola 966dd2bf2c add log method to utility module 2020-03-13 02:03:39 +01:00
Akinwale Ariwodola 364de592fa use snake case for payload keys 2020-03-12 21:16:39 +01:00
Akinwale Ariwodola d38241f0d1 check for contentTitle and channelUrl in payload 2020-03-11 15:55:35 +01:00
Akinwale Ariwodola e217be6b67
Fast lite mode (native code) (#864)
* fast loading lite mode
* add react methods for lbrynet dir and platform
* send additional url parameters for cold start
* pending intent
* update gitlab CI
2020-03-11 15:21:41 +01:00
Akinwale Ariwodola 37f84f0399 versionCode for productFlavors 2020-03-03 16:48:22 +01:00
Akinwale Ariwodola b618b9fa2b sdk 0.62.0. update secret files. 2020-03-02 18:39:35 +01:00
Akinwale Ariwodola 37b893103d readme: lbrynet-daemon -> LBRY SDK 2020-03-02 16:45:23 +01:00
Akinwale Ariwodola e2d4852df7 include google-services.sample.json for test / debug builds 2020-03-02 16:41:10 +01:00
Akinwale Ariwodola 1e0298d73a update readme 2020-03-02 16:22:19 +01:00
Akinwale Ariwodola 64c13295d6 update release.sh for unsigned APKs 2020-03-01 21:37:57 +01:00
Akinwale Ariwodola 0261ece709 remove release signingConfig since we sign with jarsigner 2020-03-01 21:18:26 +01:00
Akinwale Ariwodola a5799347ad
New build (#854)
* new project structure compatiable with Android Studio
2020-02-27 19:20:06 +01:00
Akinwale Ariwodola 6c7c4d1a88 app head: following page sign in message 2020-02-25 19:39:58 +01:00
Akinwale Ariwodola f5390316a7 app head: fix suggested subscriptions state handling 2020-02-25 19:35:54 +01:00
Akinwale Ariwodola da1ea77aa9 pass conf to get_loggly_handler 2020-02-25 08:34:50 +01:00
Akinwale Ariwodola 8ea48977ae sdk 0.61.0 2020-02-24 20:29:09 +01:00
Akinwale Ariwodola 6721db54e2 bumpversion 0.13.0 --> 0.13.1 2020-02-24 15:59:57 +01:00
Akinwale Ariwodola 6a7079c8b2 app head: basic repost redirect handling 2020-02-24 15:58:33 +01:00
Akinwale Ariwodola 8cc7e9d6c9
Following rework (#852)
* app head: following rework
* sdk 0.60.0. app head: following rework changes.
* app head: search and related content fixes
* app head: following rework updates
2020-02-24 15:21:13 +01:00
Akinwale Ariwodola d0ac41c242 app head: timing 2020-02-17 18:34:28 +01:00
Akinwale Ariwodola bc8f476ef1
Merge pull request #850 from lbryio/app-timing
app cold and warm start timing
2020-02-17 07:32:49 +01:00
Akinwale Ariwodola 2d01b63cfe app cold and warm start timing 2020-02-17 07:19:16 +01:00
Akinwale Ariwodola 88dc31e9af
Merge pull request #832 from michaeltintiuc/fix-npm-install
Fix npm install
2020-02-07 16:26:05 +01:00
Thomas Zarebczan 8d1e5eb64d
Merge pull request #834 from ykris45/patch-3
Update LICENSE
2020-02-03 17:04:42 -05:00
Akinwale Ariwodola 8113ef3f8a app head: fix splash screen stuck on authenticating 2020-02-03 09:21:22 +01:00
Akinwale Ariwodola a74ed0e93b bumpversion 0.12.3 --> 0.13.0 2020-02-03 07:28:28 +01:00
Akinwale Ariwodola 11f42bf71e app head: resolved search nsfw 2020-02-03 07:27:40 +01:00
YULIUS KURNIAWAN KRISTIANTO 8eb96025ec
Update LICENSE 2020-02-03 04:38:17 +07:00
Akinwale Ariwodola 1ece7ff433 sdk 0.56.0 fixes 2020-01-31 19:43:49 +01:00
Michael Tintiuc e312c451f8 Fix npm install 2020-01-31 19:51:32 +02:00
Akinwale Ariwodola bb64e91648 sdk 0.56.0 2020-01-31 16:47:25 +01:00
Akinwale Ariwodola 69398f6a2c app head: search page fixes 2020-01-31 09:15:37 +01:00
Akinwale Ariwodola bef7aa0ae9 app head: Invites page 2020-01-29 12:26:57 +01:00
Akinwale Ariwodola 2776696501 app head: fix thumbnailUrl check 2020-01-23 02:16:12 +01:00
Akinwale Ariwodola fa075df278 app head: additional thumbnailUrl check 2020-01-23 00:56:11 +01:00
Akinwale Ariwodola 05311fd3a1 app head: fix additional channel creator edit error 2020-01-22 21:21:42 +01:00
Akinwale Ariwodola 79001239d9 app head: fix channel creator edit error 2020-01-22 18:40:19 +01:00
Akinwale Ariwodola e344733bf0 sdk 0.54.1 2020-01-22 15:04:44 +01:00
Akinwale Ariwodola 282fdbb903 app head: download confirmation dialog 2020-01-20 23:36:35 +01:00
Akinwale Ariwodola ee7c571101 download manager tweaks 2020-01-20 23:11:46 +01:00
Akinwale Ariwodola 87e42d6b7e app head: error on publish to existing content address 2020-01-20 21:53:57 +01:00
Akinwale Ariwodola 8fcf135280 add Stop action to download notifications 2020-01-20 21:42:25 +01:00
Akinwale Ariwodola ae9364ad7e add filter to only get active downloads on service startup 2020-01-20 18:55:02 +01:00
Akinwale Ariwodola 6aa44d8a9e tweak sdkCall params and sdk poll interval 2020-01-17 07:24:50 +01:00
Akinwale Ariwodola 3f013d1272 bumpversion 0.12.2 --> 0.12.3 2020-01-17 06:53:51 +01:00
Akinwale Ariwodola 94d1f5a4eb fix queued download file_list response handling 2020-01-17 06:44:43 +01:00
Akinwale Ariwodola 1d8a7d838b
Merge pull request #818 from lbryio/sdk-0.53
sdk 0.53
2020-01-17 06:00:08 +01:00
Akinwale Ariwodola c5d441fa1f sdk 0.53.3 2020-01-17 05:59:04 +01:00
Akinwale Ariwodola 4276a78a81 sdk 0.53.2 service updates 2020-01-16 22:34:36 +01:00
Akinwale Ariwodola 36c24a448f
Playable downloads (#817) 2020-01-16 22:05:22 +01:00
Akinwale Ariwodola 4bf1051549 app head: playable downloads update 2020-01-16 22:03:19 +01:00
Akinwale Ariwodola fc4ab3128c app headd: fix error on opening related content or urls 2020-01-14 02:30:35 +01:00
Akinwale Ariwodola 65d32ea13e app head: react navigation drawer workaround 2020-01-13 12:24:32 +01:00
Akinwale Ariwodola 4cd8c80226 optimise download manager update broadcasts 2020-01-13 12:05:02 +01:00
Akinwale Ariwodola 7adcdc43cf app head: remove calls to doCheckSubscriptionsInit 2020-01-10 23:41:47 +01:00
Akinwale Ariwodola 77852dc135 app head: style fixes 2020-01-10 00:38:25 +01:00
Akinwale Ariwodola 746ca62ca7 app head: fix claim result item channel render checks 2020-01-09 20:34:52 +01:00
Akinwale Ariwodola 5e993f0fcc spinner dropdown styling 2020-01-09 20:20:22 +01:00
Akinwale Ariwodola 168dbba62c app head: fullscreen media player restore tweak 2020-01-09 19:00:25 +01:00
Akinwale Ariwodola 4db7bf814c app head: latest changes 2020-01-09 18:18:41 +01:00
Akinwale Ariwodola 21a3ff318b update font files 2020-01-09 15:44:10 +01:00
Akinwale Ariwodola 116de67d4c app head: downgrade drawer navigator 2020-01-07 11:21:30 +01:00
Akinwale Ariwodola c6b8dfd539 app head: download and open file button fixes 2020-01-07 08:50:24 +01:00
Akinwale Ariwodola 37f33e9943 fix WebView package 2020-01-07 08:35:06 +01:00
Akinwale Ariwodola 4bae7fdf94 app head: fix tag search result 2020-01-06 23:02:06 +01:00
Akinwale Ariwodola c455bde154 app head: performance and fixes. disable hermes engine. 2020-01-06 21:55:59 +01:00
Akinwale Ariwodola 5aa4321787 app head: merge resolved search commits 2020-01-05 19:08:52 +01:00
Akinwale Ariwodola 27a2100c72 app head: resolved search updates 2020-01-05 18:12:49 +01:00
Akinwale Ariwodola 4fc82c6527 app head: resolved search 2020-01-05 13:58:12 +01:00
Akinwale Ariwodola 56486ad274 app head: storage permission checks 2020-01-04 07:11:50 +01:00
Akinwale Ariwodola 99a38e655b bumpversion 0.12.1 --> 0.12.2 2020-01-03 19:12:14 +01:00
Akinwale Ariwodola 262050147d sdk 0.51.2 2020-01-03 18:55:47 +01:00
Akinwale Ariwodola 5ff2e608c2 app head: reactotron and search improvements 2019-12-29 11:17:13 +01:00
Akinwale Ariwodola 4e305faf5b app head: remove console.log 2019-12-29 07:24:26 +01:00
Akinwale Ariwodola 0bdbb843fb add android:exported to activity definition in manifest 2019-12-29 07:20:48 +01:00
Akinwale Ariwodola c8736ad26e update gradle dependencies 2019-12-29 07:15:14 +01:00
Akinwale Ariwodola 57d6b72f5a
First run changes (#809)
* don't request for storage permission on first startup
* app head: download button tweaks
* fix download manager
2019-12-28 16:06:14 +01:00
Akinwale Ariwodola d0117b14db
React Native 0.61 (#808)
* support for RN 0.61.5
* update gradle_dependencies
2019-12-28 15:42:26 +01:00
Akinwale Ariwodola 6729842ed8 app head: channel name and follower background 2019-12-20 18:55:10 +01:00
Akinwale Ariwodola 723ade8ba6 bumpversion 0.12.0 --> 0.12.1 2019-12-20 09:03:00 +01:00
Akinwale Ariwodola 83066232b6 sdk 0.50.1 2019-12-20 09:02:10 +01:00
Akinwale Ariwodola 4dc0b19c5d
Merge pull request #800 from lbryio/snackbars
snackbar support
2019-12-20 08:00:07 +01:00
Akinwale Ariwodola a58cc820ef app head: update master 2019-12-18 16:51:07 +01:00
Akinwale Ariwodola 2b3397bc85 snackbar support 2019-12-18 11:48:29 +01:00
Akinwale Ariwodola a519b5c0b1 app head: display balance on LBC input focus 2019-12-13 17:36:01 +01:00
Akinwale Ariwodola fb3a34c871 disable sound on download notifications 2019-12-13 15:10:02 +01:00
Akinwale Ariwodola a1bc3fcd7e fix notification icon 2019-12-13 14:51:44 +01:00
Akinwale Ariwodola 7daf3c6f55 add headers download to CI for 64-bit build 2019-12-11 17:06:28 +01:00
Akinwale Ariwodola 7d032179ef fix CI build 2019-12-11 16:20:45 +01:00
Akinwale Ariwodola 156507f8f7 remove headers file and dynamically download for build 2019-12-11 16:15:16 +01:00
Akinwale Ariwodola 032a1927db bumpversion 0.11.1 --> 0.12.0 2019-12-11 09:08:42 +01:00
Akinwale Ariwodola f24bf570ef app head: fixes before release 2019-12-11 09:08:26 +01:00
Akinwale Ariwodola 75c2ece016
add headers file to package (#792)
* latest headers
2019-12-11 08:46:34 +01:00
Akinwale Ariwodola 0c1c1a6c79 sdk 0.49.0 2019-12-10 16:30:18 +01:00
Akinwale Ariwodola 5af25a9d43 app head: update 2019-12-06 17:24:34 +01:00
Akinwale Ariwodola 55fec0f578 sdk 0.48.0 2019-12-05 13:42:42 +01:00
Akinwale Ariwodola b61fc3a148
Merge pull request #786 from lbryio/i18n
add language constant for app launch
2019-12-05 13:20:18 +01:00
Akinwale Ariwodola e3cd0fc2b1 add language constant for app launch 2019-12-05 13:19:22 +01:00
g1tman fe01f50c65 Perf: Use of sort and sorted functions optimized (#748)
* Update mixer.py
* Update graph.py
* Update mixer.py
* Update mixer.py
* Update bdistapk.py
* Update toolchain.py
* Update build.py
* Update build.py
2019-11-25 11:28:16 +01:00
1900 changed files with 62802 additions and 196138 deletions

View file

@ -1,3 +0,0 @@
.buildozer
.buildozer-downloads
.gradle

1
.github/workflows/deploy.yml vendored Normal file
View file

@ -0,0 +1 @@

80
.gitignore vendored
View file

@ -1,17 +1,69 @@
.buildozer
.buildozer-downloads
# OSX
#
.DS_Store
# Xcode
#
build/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
# Android/IntelliJ
#
build/
.idea
.gradle
app/node_modules/
bin
buildozer.spec
build.log
recipes/**/*.pyc
src/main/assets/index.android.bundle
src/main/assets/index.android.bundle.meta
local.properties
*.iml
# node.js
#
node_modules/
npm-debug.log
yarn-error.log
# BUCK
buck-out/
\.buckd/
*.keystore
!debug.keystore
# fastlane
#
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/
*/fastlane/report.xml
*/fastlane/Preview.html
*/fastlane/screenshots
# Bundle artifact
*.jsbundle
# CocoaPods
/ios/Pods/
# Other Files
app/google-services.json
app/twitter.properties
*.log
.vagrant
lbry-android.keystore
p4a/pythonforandroid/bootstraps/lbry/build/templates/google-services.json
.gitsecret/keys/random_seed
*.hprof
app/build
bin
app/debuglib

View file

@ -1,79 +1,48 @@
stages:
- build
- build2
- deploy
- release
build arm64 apk:
build apk:
stage: build
image: lbry/android-base:latest
image: lbry/android-base:platform-28
before_script:
- export BUILD_VERSION=$(cat $CI_PROJECT_DIR/src/main/python/main.py | grep --color=never -oP '([0-9]+\.?)+')
- git submodule sync --recursive
- git submodule update --init --force --recursive
- echo "$PGP_PRIVATE_KEY" | gpg --batch --import
- echo 'deb https://gitsecret.jfrog.io/artifactory/git-secret-deb git-secret main' >> /etc/apt/sources.list
- wget -qO - 'https://gitsecret.jfrog.io/artifactory/api/gpg/key/public' | apt-key add -
- apt-get -y update && apt-get -y install build-essential ca-certificates curl git gpg-agent openjdk-8-jdk software-properties-common wget zipalign git-secret
- git secret reveal
- chmod u+x $CI_PROJECT_DIR/gradlew
- export BUILD_VERSION=$($CI_PROJECT_DIR/gradlew -p $CI_PROJECT_DIR -q printVersionName --console=plain | tail -1)
artifacts:
paths:
- bin/browser-*-release__arm.apk
- bin/browser-*-release__arm64.apk
expire_in: 1 week
script:
- export PATH=/usr/bin:$PATH
- echo "$PGP_PRIVATE_KEY" | gpg --batch --import
- cd app
- npm install
- cd ..
- wget -q 'https://eu.crystax.net/download/crystax-ndk-10.3.2-linux-x86_64.tar.xz' -P ~/.buildozer/android/
- tar -xf ~/.buildozer/android/crystax-ndk-10.3.2-linux-x86_64.tar.xz -C ~/.buildozer/android/
- rm -rf ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9
- ln -s ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21 ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9
- cp -f $CI_PROJECT_DIR/scripts/build-target-python.sh ~/.buildozer/android/crystax-ndk-10.3.2/build/tools/build-target-python.sh
- cp -f $CI_PROJECT_DIR/scripts/mangled-glibc-syscalls__arm64.h ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21/arch-arm64/usr/include/crystax/bionic/libc/include/sys/mangled-glibc-syscalls.h
- rm ~/.buildozer/android/crystax-ndk-10.3.2-linux-x86_64.tar.xz
- git secret reveal
- mv buildozer.spec.arm64.ci buildozer.spec
- "./release.sh | grep -Fv -e 'working:' -e 'copy' -e 'Compiling' --line-buffered"
- cp $CI_PROJECT_DIR/bin/browser-$BUILD_VERSION-release.apk $CI_PROJECT_DIR/bin/browser-$BUILD_VERSION-release__arm64.apk
- cp $CI_PROJECT_DIR/bin/browser-$BUILD_VERSION-release.apk /dev/null
build arm apk:
stage: build2
image: lbry/android-base:latest
before_script:
- export BUILD_VERSION=$(cat $CI_PROJECT_DIR/src/main/python/main.py | grep --color=never -oP '([0-9]+\.?)+')
- git submodule sync --recursive
- git submodule update --init --force --recursive
artifacts:
paths:
- bin/browser-*-release__arm.apk
expire_in: 1 week
script:
- export PATH=/usr/bin:$PATH
- echo "$PGP_PRIVATE_KEY" | gpg --batch --import
- cd app
- npm install
- cd ..
- wget -q 'https://eu.crystax.net/download/crystax-ndk-10.3.2-linux-x86_64.tar.xz' -P ~/.buildozer/android/
- tar -xf ~/.buildozer/android/crystax-ndk-10.3.2-linux-x86_64.tar.xz -C ~/.buildozer/android/
- rm -rf ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9
- ln -s ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21 ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9
- cp -f $CI_PROJECT_DIR/p4a/pythonforandroid/bootstraps/lbry/build/templates/build.tmpl.gradle.arm $CI_PROJECT_DIR/p4a/pythonforandroid/bootstraps/lbry/build/templates/build.tmpl.gradle
- cp -f $CI_PROJECT_DIR/scripts/build-target-python.sh ~/.buildozer/android/crystax-ndk-10.3.2/build/tools/build-target-python.sh
- cp -f $CI_PROJECT_DIR/scripts/mangled-glibc-syscalls.h ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21/arch-arm/usr/include/crystax/bionic/libc/include/sys/mangled-glibc-syscalls.h
- rm ~/.buildozer/android/crystax-ndk-10.3.2-linux-x86_64.tar.xz
- git secret reveal
- mv buildozer.spec.arm.ci buildozer.spec
- "./release.sh | grep -Fv -e 'working:' -e 'copy' -e 'Compiling' --line-buffered"
- cp $CI_PROJECT_DIR/bin/browser-$BUILD_VERSION-release.apk $CI_PROJECT_DIR/bin/browser-$BUILD_VERSION-release__arm.apk
- cp $CI_PROJECT_DIR/bin/browser-$BUILD_VERSION-release.apk /dev/null
- export ANDROID_SDK_ROOT=~/.buildozer/android/platform/android-sdk-23
- chmod u+x ./release.sh
- ./release.sh
- cp bin/browser-$BUILD_VERSION-release__arm.apk /dev/null
- cp bin/browser-$BUILD_VERSION-release__arm64.apk /dev/null
deploy build.lbry.io:
image: python:latest
image: python:stretch
stage: deploy
dependencies:
- build arm apk
- build arm64 apk
- build apk
before_script:
- apt-get -y update && apt-get -y install apt-transport-https
- echo "$PGP_PRIVATE_KEY" | gpg --batch --import
- echo 'deb https://gitsecret.jfrog.io/artifactory/git-secret-deb git-secret main' >> /etc/apt/sources.list
- wget -qO - 'https://gitsecret.jfrog.io/artifactory/api/gpg/key/public' | apt-key add -
- apt-get -y update && apt-get -y install openjdk-8-jdk git git-secret
- pip install awscli
- export BUILD_VERSION=$(cat $CI_PROJECT_DIR/src/main/python/main.py | grep --color=never -oP '([0-9]+\.?)+')
- chmod u+x $CI_PROJECT_DIR/gradlew
- git secret reveal
- export BUILD_VERSION=$($CI_PROJECT_DIR/gradlew -p $CI_PROJECT_DIR -q printVersionName --console=plain | tail -1)
- export BUILD_APK_FILENAME__32=browser-$BUILD_VERSION-release__arm.apk
- export BUILD_APK_FILENAME__64=browser-$BUILD_VERSION-release__arm64.apk
script:
@ -82,16 +51,22 @@ deploy build.lbry.io:
- aws s3 cp bin/$BUILD_APK_FILENAME__64 s3://build.lbry.io/android/push.apk
release apk:
image: python:latest
image: python:stretch
stage: release
only:
- tags
dependencies:
- build arm apk
- build arm64 apk
- build apk
before_script:
- apt-get -y update && apt-get -y install apt-transport-https
- echo "$PGP_PRIVATE_KEY" | gpg --batch --import
- echo 'deb https://gitsecret.jfrog.io/artifactory/git-secret-deb git-secret main' >> /etc/apt/sources.list
- wget -qO - 'https://gitsecret.jfrog.io/artifactory/api/gpg/key/public' | apt-key add -
- apt-get -y update && apt-get -y install openjdk-8-jdk git git-secret
- pip install awscli githubrelease
- export BUILD_VERSION=$(cat $CI_PROJECT_DIR/src/main/python/main.py | grep --color=never -oP '([0-9]+\.?)+')
- git secret reveal
- chmod u+x $CI_PROJECT_DIR/gradlew
- export BUILD_VERSION=$($CI_PROJECT_DIR/gradlew -p $CI_PROJECT_DIR -q printVersionName --console=plain | tail -1)
- export BUILD_APK_FILENAME__32=browser-$BUILD_VERSION-release__arm.apk
- export BUILD_APK_FILENAME__64=browser-$BUILD_VERSION-release__arm64.apk
script:

4
.gitmodules vendored
View file

@ -1,3 +1 @@
[submodule "app"]
path = app
url = https://github.com/lbryio/lbry-react-native

Binary file not shown.

Binary file not shown.

BIN
.gitsecret/keys/random_seed Normal file

Binary file not shown.

View file

@ -1,2 +1,3 @@
lbry-android.keystore:
p4a/pythonforandroid/bootstraps/lbry/build/templates/google-services.json
lbry-android.keystore:0d958c531870694624cc877ea98ca1c583485f8ebbb3a5acca58b1930c190d65
app/google-services.json:896a0bee8294a36d061f10fa926129d8a780528b34d0a2f03113400c4246d67c
app/twitter.properties:01212d70712f2041efb5c814bf30ecbf6f72e1ca5179c7647c4f8cbd995dd033

137
BUILD.md
View file

@ -1,137 +0,0 @@
## Linux Build Instructions
This app has currently only been built on Ubuntu 14.04, 16.04, 17.10, and 18.04, but these instructions, or an analog of them, should work on most Linux or macOS environments. An abridged version of these instructions is available at [QUICKSTART.md](QUICKSTART.md).
For instructions on how to build using a Docker image, please see [DOCKER.md](DOCKER.md).
### Install Prerequisites
#### Requirements
* JDK 1.8
* Android SDK
* Crystax Android NDK
* Buildozer
* Node.js
* npm
* yarn
#### apt Packages
Install all apt packages required by running the following commands:
```
sudo dpkg --add-architecture i386
sudo apt-get -y update
sudo apt-get install -y curl ca-certificates software-properties-common gpg-agent wget
sudo add-apt-repository ppa:deadsnakes/ppa -y && \
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt-get -y update && apt-get -y install autoconf autogen automake libtool libffi-dev \
build-essential python3.7 python3.7-dev python3.7-venv python3-pip ccache git libncurses5:i386 libstdc++6:i386 \
libgtk2.0-0:i386 libpangox-1.0-0:i386 libpangoxft-1.0-0:i386 libidn11:i386 python2.7 python2.7-dev \
python-pip openjdk-8-jdk unzip zlib1g-dev zlib1g:i386 m4 libc6-dev-i386 yarn gawk nodejs npm
```
Alternatively, the JDK available from http://www.oracle.com/technetwork/java/javase/downloads/index.html can be installed instead of the `openjdk-8-jdk` package.
#### Install Cython and Setuptools
```
sudo -H pip install --upgrade cython==0.28.1 setuptools
```
#### Install buildozer
A forked version of `buildozer` needs to be installed in order to copy the React Native UI source files into the corresponding directories.
```
git clone https://github.com/lbryio/buildozer.git
cd buildozer && python2.7 setup.py install && cd ..
```
#### Create buildozer.spec
Assuming `lbry-android` as the current working folder:
* Copy `buildozer.spec.sample` to `buildozer.spec` in the `lbry-android` folder. Running `buildozer init` instead will create a new `buildozer.spec` file.
* Update `buildozer.spec` settings to match your environment. The basic recommended settings are outlined below.
| Setting | Description |
|:------------------- |:-----------------------------|
| title | application title |
| package.name | package name (e.g. browser) |
| package.domain | package domain (e.g. io.lbry) |
| source.dir | the location of the application main.py |
| version | application version |
| requirements | the Python module requirements for building the application |
| services | list of Android background services and their corresponding Python entry points |
| android.permissions | Android manifest permissions required by the application. This should be set to `INTERNET` at the very least to enable internet connectivity |
| android.api | Android API version (Should be at least 23 for Gradle build support) |
| android.sdk | Android SDK version (Should be at least 23 for Gradle build support) |
| android.ndk | Android NDK version (not required when using crystax Android NDK) |
| android.ndk_path | Android NDK path. This should be set to the crystax Android NDK path) |
| android.sdk_path | Android SDK path. This should be set to the path where the Android SDK is manually set up (if not set up in the `.buildozer` path). |
| p4a.source_dir | Path to the python-for-android repository folder. Currently set to the included `p4a` folder |
| p4a.local_recipes | Path to a folder containing python_for_android recipes to be used in the build. The included `recipes` folder includes recipes for a successful build |
#### Create google-services.json
The `google-services.json` file is required for the build to be successful due to the Firebase implementation. Simply copy the provided sample file into the same destination folder.
```
cd lbry-android
cp p4a/pythonforandroid/bootstraps/lbry/build/templates/google-services.sample.json p4a/pythonforandroid/bootstraps/lbry/build/templates/google-services.json
```
#### Setup Android SDK for buildozer
Download the Android SDK, platform and build tools archives.
* Android API 23 SDK - https://dl.google.com/android/android-sdk_r23-linux.tgz
* Android API 28 platform - https://dl.google.com/android/repository/platform-28_r06.zip
* Android build tools 26.0.2 - https://dl.google.com/android/repository/build-tools_r26.0.2-linux.zip
Create the `.buildozer` path (and the `android` sub-path) in your home folder if it doesn't already exist.
`mkdir ~/.buildozer`
`mkdir ~/.buildozer/android`
Extract the API 23 SDK to the `~/.buildozer/android` path and rename the extracted folder.
```
tar -xf android-sdk_r23-linux.tgz ~/.buildozer/android/platform/
mv ~/.buildozer/android/platform/android-sdk-linux ~/.buildozer/android/platform/android-sdk-23
```
Extract the API 28 platform archive into the `android-sdk-23` folder and rename the extracted folder.
```
unzip ~/.buildozer/android/platform/platform-28_r06.zip -d ~/.buildozer/android/platform/android-sdk-23/platforms
mv ~/.buildozer/android/platform/android-sdk-23/platforms/android-9 ~/.buildozer/android/platform/android-sdk-23/platforms/android-2
```
Extract the build tools 26.0.2 build tools into the `android-sdk-23` folder and rename the extracted folder.
```
mkdir -p ~/.buildozer/android/platform/android-sdk-23/build-tools
unzip ~/.buildozer/android/platform/build-tools_r26.0.2-linux.zip -d ~/.buildozer/android/platform/android-sdk-23/build-tools
mv ~/.buildozer/android/platform/android-sdk-23/build-tools/android-8.1.0 ~/.buildozer/android/platform/android-sdk-23/build-tools/26.0.2
```
Finally, create the Android SDK license file. This prevents being prompted to accept the SDK license during the build process.
```
mkdir -p ~/.buildozer/android/platform/android-sdk-23/licenses
echo $'\nd56f5187479451eabf01fb78af6dfcb131a6481e' > ~/.buildozer/android/platform/android-sdk-23/licenses/android-sdk-license
```
#### Setup Crystax Android NDK for buildozer
* Download the Crystax Android NDK from https://us.crystax.net/download/crystax-ndk-10.3.2-linux-x86_64.tar.xz and extract. Remember to update `android.ndk_path` in your `buildozer.spec` to the path of the extracted Crystax NDK archive.
* Copy `build-target-python.sh` from the `scripts` folder in the cloned `lbry-android` repository to the `crystax-ndk-10.3.2/build/tools/` folder.
* Copy `mangled-glibc-syscalls.h` from the `scripts` folder in the cloned `lbry-android` repository to the `crystax-ndk-10.3.2/platforms/android-21/arch-arm/usr/include/crystax/bionic/libc/include/sys/` folder.
* Delete the `android-9` folder in `crystax-ndk-10.3.2/platforms`, and create a symbolic link named `android-9` to the `android-21` folder.
#### Build and Deploy
Run `npm install -g react-native-cli` to install React Native CLI tools.
Initialise git submodules by running `git submodule update --init --recursive` in the `lbry-android` folder.
Run `npm i` in the `lbry-android/app` folder to install the necessary modules required by the React Native user interface, and then run `./bundle.sh`.
Run `./build.sh` in `lbry-android` to build the APK. The output can be found in the `bin` subdirectory.
To build and deploy, you can run `./deploy.sh`. This requires a connected device or a running Android emulator.
#### Development
If you already installed `Android SDK` and `adb`
* Run `adb reverse tcp:8081 tcp:8081`
* Then go to the `lbry-android/app` folder and run `npm start`
Note: You need to have your device connected with USB debugging.
Once the bundler is ready, run the LBRY Browser app on your device and then shake the device violently until you see the React Native dev menu. You can enable "Live Reloading" and "Hot Reloading" from this menu, so any changes you make to the React Native code will be visible as you save. This will only reload React Native Javascript files. Native Java code needs to be redeployed by running the command `./deploy.sh`

View file

@ -1,156 +0,0 @@
# lbry-android development environment inside docker
[scripts/lbry-android.sh](scripts/lbry-android.sh) is a bash script to create a
docker container for lbry-android development.
This is a hybrid approach where the apk is built inside docker, but you run
Android Studio, `adb`, and the app hot-reload bundler, directly on your host.
## Features
* Clones lbry-android source code to a directory on the host, and mounts it
inside the container.
* Installs all build dependencies inside the container, leaving your host
computer clean.
* Mounted `.buildozer` directory to save container space. (Docker containers
should stay under 10GB, and `.buildozer` is too big, so it has to stay in a
mounted host directory instead.)
* The biggest downloads are cached in `.buildozer-downloads` directory so you
can easily remove `.buildozer` and not have to re-download the large cryostax
NDK more than once.
* Instructions for installing on a real Android device, and for setting up
hot-reload.
* Only a handful of commands to go from zero->hero.
## Requirements
Install all of the following on your host computer:
* Tested on Linux x86_64 with BASH.
* [Install Android Studio](https://developer.android.com/studio/).
* The normal install auto-creates a directory in `$HOME/Android/Sdk` to store
SDK downloads in your home directory.
* [Install nodejs](https://nodejs.org/en/download/package-manager/).
* [Install yarn](https://yarnpkg.com/lang/en/docs/install).
* [Install Docker](https://docs.docker.com/install/).
* Install `sudo` and give your user account access to run `sudo docker`.
## Install
Clone `lbry-android`:
```
LBRY_GIT=$HOME/git/vendor/lbryio/
mkdir -p $LBRY_GIT
git clone https://github.com/lbryio/lbry-android.git $LBRY_GIT/lbry-android
cd $LBRY_GIT/lbry-android
git submodule update --init --recursive
```
Install a bash alias to the [scripts/lbry-android.sh](scripts/lbry-android.sh)
script:
```
echo "alias lbry-android=$LBRY_GIT/lbry-android/scripts/lbry-android.sh" >> $HOME/.bashrc
source ~/.bashrc
```
## Usage
* First create the base docker image:
```
lbry-android docker-build
```
(If anytime you change the docker build scripts in the [scripts](scripts)
subdirectory, you should rebuild the image again.)
* Setup buildozer and install dependencies:
```
lbry-android setup
```
* Build the apk:
```
lbry-android build
```
The apk will be built and end up in the `lbry-android/bin/` subdirectory.
## Running on a real Android device
Once you have the apk built, you can install it on a real Android device to
start testing.
Follow the Android documentation for [enabling USB
debugging](https://developer.android.com/studio/command-line/adb#Enabling) on
your device.
Once you have enabled debugging, do the following:
* Plug your device into a USB port on the host computer.
* Run:
```~/Android/Sdk/platform-tools/adb devices```
* ADB should list your device like so:
```
[ryan@t440s lbry-android]$ adb devices
List of devices attached
HT71R0000000 device
```
* The first time you connect, you should see a modal dialog on the device
asking to confirm the host id. You must approve it, or adb will fail to
connect.
* If after trying several times, adb is still not connecting to your device,
and adb lists your device as `unauthorized`, you may need to [follow this
advice](https://stackoverflow.com/a/38380384/56560) and delete your
`$HOME/.android` directory.
* Install the apk (whatever the current version is called) to the device:
```
~/Android/Sdk/platform-tools/adb install ./bin/browser-0.7.5-debug.apk
```
* Open the app on your device, and follow the initial steps to get to the main
lbry-android browser page.
* Create a tcp tunnel over the adb bridge, for the app to handle live-reload:
```
~/Android/Sdk/platform-tools/adb reverse tcp:8081 tcp:8081
```
* Start the live-reload server on the host:
```
cd app/
yarn install
yarn start
```
* With your device currently running the lbry-android app, shake the device
from side to side, and a menu will appear. Choose `Enable Hot Reloading`.
* The app should reload itself.
* Now make a change to the source code to test hot reload. From the host, open
up the main page view source code: `app/src/component/uriBar/view.js`. Find
the line that reads `Search movies, music, and more` (This is the main search
bar at the top of the app.) - Change some of the text and save the file.
* If hot-reload is working, within a few seconds the phone should reload the
page automatically and show the change that you made.
* If the hot-reload does not work, try closing the app (dismiss the window from
the tab overview, via android square button) and reload the app.

View file

@ -1,71 +0,0 @@
# Introduction
The purpose of this guide is to help whomever is interested in running the LBRY Android application from scratch on their device, but they're main computing platform is not Linux but macOS.
## Estimated build time
25 - 40 minutes (depending on Internet connection speeds)
## What do you need?
* A computer running the latest OS
* Internet access to download modules and packages
* At least 15GB of free disk space
* Docker
* Patience
## Step 1/6
Create an application on [Firebase](https://console.firebase.google.com). In the **Android package name** field, input `io.lbry.browser`. Download the resulting `google-services.json` file and keep it safe, you'll be needing it later.
## Step 2/6
Start the docker application and paste all of these lines into Terminal:
```bash
docker run -it lbry/android-base:latest /bin/bash
wget "https://www.crystax.net/download/crystax-ndk-10.3.2-linux-x86_64.tar.xz" -P ~/.buildozer/android/
tar -xvf ~/.buildozer/android/crystax-ndk-10.3.2-linux-x86_64.tar.xz -C ~/.buildozer/android/
rm -rf ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9
ln -s ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21 ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9
git clone https://github.com/lbryio/lbry-android
cd lbry-android
git submodule update --init --recursive
cp buildozer.spec.sample buildozer.spec
cd app;npm i;cd ..
cp scripts/build-target-python.sh ~/.buildozer/android/crystax-ndk-10.3.2/build/tools/build-target-python.sh
cp scripts/mangled-glibc-syscalls.h ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21/arch-arm/usr/include/crystax/bionic/libc/include/sys/mangled-glibc-syscalls.h
cd p4a/pythonforandroid/bootstraps/lbry/build/templates
apt install nano -y
```
## Step 3/6
Copy the contents of the `google-services.json` you downloaded earlier and paste them into Terminal after running the next command:
```bash
nano google-services.json
```
Type `^X` to save and exit.
## Step 4/6
Paste more lines and I guess check your email, this will take some time:
```bash
cd /lbry-android/app
./bundle.sh
cd ..
buildozer android debug
```
When the build is complete, you should see a message like: `[INFO]: # APK renamed to browser-0.7.3-debug.apk`. You will need this filename for the next step.
## Step 5/6
In a separate Terminal window:
```bash
docker ps # get container name
docker cp CONTAINER_NAME:/lbry-android/bin/STEP_4_FILENAME ~/Desktop/ # copies STEP_4_FILENAME to your Desktop
```
## Step 6/6
- Download [Android File Transfer](https://www.android.com/filetransfer) and install it.
- On your Android device, install [File Explorer](https://play.google.com/store/apps/details?id=com.mauriciotogneri.fileexplorer).
- Plug in your Android device and swipe down from the top into the "USB for file transfer" settings (or similar name on your device) and make sure "Transfer files" is selected.
- Open **Android File Transfer** on your computer and drag and drop `STEP_4_FILENAME` from your Desktop to the `Downloads` folder on the Android device.
- Back on the Android device, navigate to `STEP_4_FILENAME` in the `Downloads` folder and tap it to begin installation.

View file

@ -1,32 +0,0 @@
FROM thyrlian/android-sdk
## Dependencies to run as root:
ENV DEBIAN_FRONTEND=noninteractive
RUN dpkg --add-architecture i386 && \
apt-get -y update && \
apt-get install -y \
curl ca-certificates software-properties-common gpg-agent wget \
python3.7 python3.7-dev python3-pip python2.7 python2.7-dev python3.7-venv \
python-pip zlib1g-dev m4 zlib1g:i386 libc6-dev-i386 gawk nodejs npm unzip openjdk-8-jdk \
autoconf autogen automake libtool libffi-dev build-essential \
ccache git libncurses5:i386 libstdc++6:i386 \
libgtk2.0-0:i386 libpangox-1.0-0:i386 libpangoxft-1.0-0:i386 libidn11:i386 && \
npm install -g yarn react-native-cli && \
pip2 install --upgrade cython setuptools && \
pip2 install git+https://github.com/lbryio/buildozer.git@master && \
ln -s /src/scripts/build-docker.sh /usr/local/bin/build && \
adduser lbry-android --gecos GECOS --shell /bin/bash --disabled-password --home /home/lbry-android && \
mkdir /home/lbry-android/.npm-packages && \
echo "prefix=/home/lbry-android/.npm-packages" > /home/lbry-android/.npmrc && \
chown -R lbry-android:lbry-android /home/lbry-android && \
mkdir /src && \
chown lbry-android:lbry-android /src && \
mkdir /dist && \
chown lbry-android:lbry-android /dist
## Further setup done by lbry-android user:
USER lbry-android
COPY scripts/docker-build.sh /home/lbry-android/bin/build
COPY scripts/docker-setup.sh /home/lbry-android/bin/setup
CMD ["/home/lbry-android/bin/build"]

View file

@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2017-2019 LBRY Inc
Copyright (c) 2017-2020 LBRY Inc
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:

View file

@ -1,113 +0,0 @@
# Introduction
If you would like to contribute to the Android app, but find the build documentation a little daunting, this guide lets you copy-paste your way to a successful APK build.
### Estimated build time
25 - 40 minutes (depending on Internet connection speeds)
### What do you need?
* A computer running [Ubuntu 18.04](https://ubuntu.com/download/desktop)
* Internet access to download modules and packages.
* At least 15GB of free disk space.
* Alternatively, Docker. You can skip steps 1 through 5 if you make use of the `lbry/android-base` Docker base image. Scroll down to [Fast Track](#Fast-Track) at the bottom of the page if you would prefer to use Docker.
### Step 1 of 10
Install all the apt packages required by running the following commands. You can copy-paste this directly to your terminal.
```
sudo dpkg --add-architecture i386
sudo apt-get -y update
sudo apt-get install -y curl ca-certificates software-properties-common gpg-agent wget
sudo add-apt-repository ppa:deadsnakes/ppa -y && \
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt-get -y update && apt-get -y install autoconf autogen automake libtool libffi-dev \
build-essential python3.7 python3.7-dev python3.7-venv python3-pip ccache git libncurses5:i386 libstdc++6:i386 \
libgtk2.0-0:i386 libpangox-1.0-0:i386 libpangoxft-1.0-0:i386 libidn11:i386 python2.7 python2.7-dev \
python-pip openjdk-8-jdk unzip zlib1g-dev zlib1g:i386 m4 libc6-dev-i386 yarn gawk nodejs npm
```
### Step 2 of 10
Install a couple of packages using the Python package installer
```
sudo -H pip install --upgrade cython==0.28.1 setuptools
```
### Step 3 of 10
Install [buildozer](https://github.com/lbryio/buildozer.git), a tool for creating the apk package using the python for android toolchain.
```
git clone https://github.com/lbryio/buildozer.git
cd buildozer && python2.7 setup.py install && cd ..
```
### Step 4 of 10
The Android SDK needs to be setup for buildozer. This requires creating a few directories and downloading a number of files. Run the following commands to create the buildozer directory, download the required archives and extract them into their proper destination folders.
```
mkdir -p ~/.buildozer/android/platform
wget 'https://dl.google.com/android/android-sdk_r23-linux.tgz' -P ~/.buildozer/android/platform/ && \
wget 'https://dl.google.com/android/repository/platform-28_r06.zip' -P ~/.buildozer/android/platform/ && \
wget 'https://dl.google.com/android/repository/build-tools_r26.0.2-linux.zip' -P ~/.buildozer/android/platform/
tar -xvf ~/.buildozer/android/platform/android-sdk_r23-linux.tgz -C ~/.buildozer/android/platform/ && \
mv ~/.buildozer/android/platform/android-sdk-linux ~/.buildozer/android/platform/android-sdk-23 && \
unzip ~/.buildozer/android/platform/platform-28_r06.zip -d ~/.buildozer/android/platform/android-sdk-23/platforms && \
mv ~/.buildozer/android/platform/android-sdk-23/platforms/android-9 ~/.buildozer/android/platform/android-sdk-23/platforms/android-28 && \
mkdir -p ~/.buildozer/android/platform/android-sdk-23/build-tools && \
unzip ~/.buildozer/android/platform/build-tools_r26.0.2-linux.zip -d ~/.buildozer/android/platform/android-sdk-23/build-tools && \
mkdir -p ~/.buildozer/android/platform/android-sdk-23/licenses && \
echo $'\nd56f5187479451eabf01fb78af6dfcb131a6481e' > ~/.buildozer/android/platform/android-sdk-23/licenses/android-sdk-license
```
### Step 5 of 10
Install the react-native-cli npm package.
```
sudo npm install -g react-native-cli
```
### Step 6 of 10
Install the Crystax NDK which is required for building Python 3.7 for the mobile app, and a number of native C / C++ modules and packages used by the app. Run the following commands to download and extract the NDK.
```
wget 'https://www.crystax.net/download/crystax-ndk-10.3.2-linux-x86_64.tar.xz' -P ~/.buildozer/android/ && \
tar -xvf ~/.buildozer/android/crystax-ndk-10.3.2-linux-x86_64.tar.xz -C ~/.buildozer/android/ && \
rm -rf ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9 && \
ln -s ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21 ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9
```
### Step 7 of 10
Clone the [lbryio/lbry-android git repository](https://github.com/lbryio/lbry-android), initialise submodules and create your `buildozer.spec` and `google-services.json` files. The provided `buildozer.spec.sample` contains defaults provided you followed steps 1 through 5 exactly as described. You can also customise the spec file if you want to. The `google-services.sample.json` can be used to ensure the build completes successfully.
```
git clone https://github.com/lbryio/lbry-android
cd lbry-android
git submodule update --init --recursive
cp buildozer.spec.sample buildozer.spec
cp p4a/pythonforandroid/bootstraps/lbry/templates/google-services.sample.json p4a/pythonforandroid/bootstraps/lbry/templates/google-services.json
```
### Step 8 of 10
Install the npm packages required for the app's React Native code, and create the React Native app bundle.
```
cd app
npm install
./bundle.sh
cd ..
```
### Step 9 of 10
Copy a couple of required files from the repository for the build to be successful.
```
cp scripts/build-target-python.sh ~/.buildozer/android/crystax-ndk-10.3.2/build/tools/build-target-python.sh
cp scripts/mangled-glibc-syscalls.h ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21/arch-arm/usr/include/crystax/bionic/libc/include/sys/mangled-glibc-syscalls.h
```
### Step 10 of 10
If you made it this far, you're finally ready to build the package! You just have to run a single command to generate the APK.
```
buildozer android debug
```
### Fast Track {#Fast-Track}
Install Docker and start a container using the `lbry/android-base` image, which is about 1.72GB in size. Run the following commands for Ubuntu and then follow steps 6 through 10 in the container's bash prompt.
```
sudo apt-get install docker-ce
docker run -it lbry/android-base:latest /bin/bash
```
**Pro Tip:** You can also make use of Docker to run your builds on macOS or Windows.

View file

@ -2,7 +2,7 @@
[![pipeline status](https://ci.lbry.tech/lbry/lbry-android/badges/master/pipeline.svg)](https://ci.lbry.tech/lbry/lbry-android/commits/master)
[![GitHub license](https://img.shields.io/github/license/lbryio/lbry-android)](https://github.com/lbryio/lbry-android/blob/master/LICENSE)
An Android browser and wallet for the [LBRY](https://lbry.com) network. This app bundles [lbrynet-daemon](https://github.com/lbryio/lbry) as a background service with a UI layer built with React Native. The APK is built using buildozer and the Gradle build tool.
An Android browser and wallet for the [LBRY](https://lbry.com) network.
<img src="https://spee.ch/@lbry:3f/android-08-homepage.gif" alt="LBRY Android GIF" width="384px" />
@ -15,10 +15,22 @@ The minimum supported Android version is 5.0 Lollipop. There are two ways to ins
1. Direct APK install available at [http://build.lbry.io/android/latest.apk](http://build.lbry.io/android/latest.apk). You will need to enable installation from third-party sources on your device in order to install from this source.
## Usage
The app can be launched by opening **LBRY Browser** from the device's app drawer or via the shortcut on the home screen if that was created upon installation.
The app can be launched by opening **LBRY** from the device's app drawer or via the shortcut on the home screen if that was created upon installation.
## Running from Source
The app is built from source via [Buildozer](https://github.com/kivy/buildozer). After cloning the repository, copy `buildozer.spec.sample` to `buildozer.spec` and modify this file as necessary for your environment. Please see [BUILD.md](BUILD.md) for detailed build instructions.
Clone the repository and open the project in Android Studio. Android Studio will automatically run the initial build process.
Create file 'twitter.properties' in app/ folder with the following content:
```
twitterConsumerKey=XXXXXX
twitterConsumerSecret=XXXXXX
```
Copy the file 'google-services.sample.json' to 'google-services.json' in the app/ folder.
Click the Sync button and when process finishes, the Run button to launch the app on your simulator or connected debugging device after the build process is complete.
## Contributing
Contributions to this project are welcome, encouraged, and compensated. For more details, see https://lbry.io/faq/contributing
@ -27,7 +39,7 @@ Contributions to this project are welcome, encouraged, and compensated. For more
This project is MIT licensed. For the full license, see [LICENSE](LICENSE).
## Security
We take security seriously. Please contact security@lbry.com regarding any security issues. Our PGP key is [here](https://keybase.io/lbry/key.asc) if you need it.
We take security seriously. Please contact security@lbry.com regarding any security issues. Our PGP key is [here](https://lbry.com/faq/pgp-key) if you need it.
## Contact
The primary contact for this project is [@akinwale](https://github.com/akinwale) (akinwale@lbry.com)

100
Vagrantfile vendored
View file

@ -1,100 +0,0 @@
echoed=false
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/bionic64"
#config.disksize.size = "20GB"
config.vm.provider "virtualbox" do |v|
host = RbConfig::CONFIG['host_os']
# Give VM 1/4 system memory & access to all cpu cores on the host
if host =~ /darwin/
cpus = `sysctl -n hw.ncpu`.to_i
# sysctl returns Bytes and we need to convert to MB
mem = `sysctl -n hw.memsize`.to_i / 1024 / 1024 / 4
elsif host =~ /linux/
cpus = `nproc`.to_i
# meminfo shows KB and we need to convert to MB
mem = `grep 'MemTotal' /proc/meminfo | sed -e 's/MemTotal://' -e 's/ kB//'`.to_i / 1024 / 4
else
cpus = `wmic cpu get NumberOfCores`.split("\n")[2].to_i
mem = `wmic OS get TotalVisibleMemorySize`.split("\n")[2].to_i / 1024 /4
end
mem = mem / 1024 / 4
mem = [mem, 2048].max # Minimum 2048
if echoed === false
echoed=true
puts("Memory", mem)
puts("CPUs", cpus)
end
#v.customize ["setextradata", :id, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/home_vagrant_lbry-android", "1"]
#v.customize ["setextradata", :id, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/vagrant", "1"]
v.customize ["modifyvm", :id, "--memory", mem]
v.customize ["modifyvm", :id, "--cpus", cpus]
end
config.vm.synced_folder "./", "/home/vagrant/lbry-android"
config.vm.provision "shell", inline: <<-SHELL
dpkg --add-architecture i386
apt-get update
apt-get install -y libssl-dev
apt-get install -y python3.6 python3.6-dev python3-pip autoconf libffi-dev pkg-config libtool build-essential ccache git libncurses5:i386 libstdc++6:i386 libgtk2.0-0:i386 libpangox-1.0-0:i386 libpangoxft-1.0-0:i386 libidn11:i386 python2.7 python2.7-dev openjdk-8-jdk unzip zlib1g-dev zlib1g:i386 m4 libc6-dev-i386 python-pip
pip install -f --upgrade setuptools pyopenssl
git clone https://github.com/lbryio/buildozer.git
cd buildozer
python2.7 setup.py install
cd ../
rm -rf ./buildozer
# Install additonal buildozer dependencies
sudo apt-get install cython
# Install node
curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -
sudo apt-get install -y nodejs
export HOME=/home/vagrant
cp $HOME/lbry-android/buildozer.spec.vagrant $HOME/lbry-android/buildozer.spec
mkdir -p cd $HOME/.buildozer/android/platform/
wget -q 'https://us.crystax.net/download/crystax-ndk-10.3.2-linux-x86_64.tar.xz' -P $HOME/.buildozer/android/
wget -q 'https://dl.google.com/android/android-sdk_r23-linux.tgz' -P $HOME/.buildozer/android/platform/
wget -q 'https://dl.google.com/android/repository/platform-27_r01.zip' -P $HOME/.buildozer/android/platform/
wget -q 'https://dl.google.com/android/repository/build-tools_r26.0.1-linux.zip' -P $HOME/.buildozer/android/platform/
tar -xf ~/.buildozer/android/crystax-ndk-10.3.2-linux-x86_64.tar.xz -C $HOME/.buildozer/android/
rm $HOME/.buildozer/android/crystax-ndk-10.3.2-linux-x86_64.tar.xz
ln -s $HOME/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21 $HOME/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9
cp -f $HOME/lbry-android/scripts/build-target-python.sh $HOME/.buildozer/android/crystax-ndk-10.3.2/build/tools/build-target-python.sh
rm -rf $HOME/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9
tar -xf $HOME/.buildozer/android/platform/android-sdk_r23-linux.tgz -C $HOME/.buildozer/android/platform/
rm $HOME/.buildozer/android/platform/android-sdk_r23-linux.tgz
mv $HOME/.buildozer/android/platform/android-sdk-linux $HOME/.buildozer/android/platform/android-sdk-23
unzip -qq $HOME/.buildozer/android/platform/android-23_r02.zip -d $HOME/.buildozer/android/platform/android-sdk-23/platforms
rm $HOME/.buildozer/android/platform/platform-27_r01.zip
mv $HOME/.buildozer/android/platform/android-sdk-23/platforms/android-8.1.0 $HOME/.buildozer/android/platform/android-sdk-23/platforms/android-27
mkdir -p $HOME/.buildozer/android/platform/android-sdk-23/build-tools
unzip -qq $HOME/.buildozer/android/platform/build-tools_r26.0.1-linux.zip -d $HOME/.buildozer/android/platform/android-sdk-23/build-tools
rm $HOME/.buildozer/android/platform/build-tools_r26.0.1-linux.zip
mv $HOME/.buildozer/android/platform/android-sdk-23/build-tools/android-8.0.0 $HOME/.buildozer/android/platform/android-sdk-23/build-tools/26.0.1
mkdir -p $HOME/.buildozer/android/platform/android-sdk-23/licenses
rm -rf $HOME/.buildozer/android/platform/android-sdk-23/tools
# https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip
wget -q https://dl.google.com/android/repository/tools_r25.2.5-linux.zip
unzip -o -q ./tools_r25.2.5-linux.zip -d $HOME/.buildozer/android/platform/android-sdk-23/
rm sdk-tools-linux-3859397.zip
echo $'\nd56f5187479451eabf01fb78af6dfcb131a6481e' > $HOME/.buildozer/android/platform/android-sdk-23/licenses/android-sdk-license
sudo chown -r vagrant $HOME
echo "Installing React Native via NPM..."
sudo npm install -g react-native-cli
SHELL
end

1
app

@ -1 +0,0 @@
Subproject commit e901e8107439dbe9a0a236515e9a584e0bdcd3d0

1
app/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

144
app/build.gradle Normal file
View file

@ -0,0 +1,144 @@
import com.google.gms.googleservices.GoogleServicesPlugin
apply plugin: 'com.android.application'
android {
compileSdkVersion 29
buildToolsVersion "29.0.2"
flavorDimensions "default"
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
applicationId "io.lbry.browser"
minSdkVersion 21
targetSdkVersion 29
versionCode 1701
versionName "0.17.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
packagingOptions {
exclude 'META-INF/DEPENDENCIES'
exclude 'lib/x86_64/darwin/libscrypt.dylib'
}
productFlavors {
__32bit {
versionCode android.defaultConfig.versionCode * 10 + 1
ndk {
abiFilter "armeabi-v7a"
}
}
__64bit {
versionCode android.defaultConfig.versionCode * 10 + 2
ndk {
abiFilter "arm64-v8a"
}
}
}
buildTypes {
debug {
Properties twitterProps = new Properties()
twitterProps.load(project.file('twitter.properties').newDataInputStream())
resValue "string", "TWITTER_CONSUMER_KEY", "\"${twitterProps.getProperty("twitterConsumerKey")}\""
resValue "string", "TWITTER_CONSUMER_SECRET", "\"${twitterProps.getProperty("twitterConsumerSecret")}\""
}
release {
Properties twitterProps = new Properties()
twitterProps.load(project.file('twitter.properties').newDataInputStream())
resValue "string", "TWITTER_CONSUMER_KEY", "\"${twitterProps.getProperty("twitterConsumerKey")}\""
resValue "string", "TWITTER_CONSUMER_SECRET", "\"${twitterProps.getProperty("twitterConsumerSecret")}\""
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
task printVersionName {
doLast {
println android.defaultConfig.versionName
}
}
configurations {
all {
exclude module: 'httpclient'
exclude module: 'commons-logging'
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.3.0-alpha01'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'com.google.android.material:material:1.3.0-alpha01'
implementation "androidx.cardview:cardview:1.0.0"
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.navigation:navigation-fragment:2.3.1'
implementation 'androidx.navigation:navigation-ui:2.3.1'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.webkit:webkit:1.4.0-rc01'
implementation 'androidx.camera:camera-core:1.0.0-beta03'
implementation 'androidx.camera:camera-camera2:1.0.0-beta03'
implementation 'androidx.camera:camera-lifecycle:1.0.0-beta03'
implementation 'androidx.camera:camera-view:1.0.0-alpha10'
implementation 'androidx.browser:browser:1.2.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.github.bumptech.glide:glide:4.11.0'
implementation 'com.squareup.okhttp3:okhttp:4.4.1'
implementation 'com.google.firebase:firebase-analytics:18.0.0'
implementation 'com.google.android.gms:play-services-base:17.5.0'
implementation 'com.google.firebase:firebase-messaging:21.0.0'
implementation 'com.google.oauth-client:google-oauth-client:1.30.4'
implementation 'com.android.billingclient:billing:3.0.2'
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.google.android.exoplayer:exoplayer-core:2.12.2'
implementation 'com.google.android.exoplayer:exoplayer-dash:2.12.2'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.12.2'
implementation 'com.google.android.exoplayer:extension-cast:2.12.2'
implementation 'com.google.android.exoplayer:extension-mediasession:2.12.2'
implementation 'com.google.android:flexbox:2.0.1'
implementation 'com.hbb20:ccp:2.3.8'
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
implementation 'com.atlassian.commonmark:commonmark:0.14.0'
implementation 'com.arthenica:mobile-ffmpeg-full-gpl:4.3.1.LTS'
implementation 'commons-codec:commons-codec:1.15'
implementation 'org.bitcoinj:bitcoinj-tools:0.14.7'
implementation 'org.java-websocket:Java-WebSocket:1.5.1'
implementation ('com.journeyapps:zxing-android-embedded:4.1.0') { transitive = false }
implementation 'com.google.zxing:core:3.3.0'
compileOnly 'org.projectlombok:lombok:1.18.10'
annotationProcessor 'org.projectlombok:lombok:1.18.10'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'androidx.test:rules:1.3.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
__32bitImplementation 'io.lbry:lbrysdk32:0.102.0'
__64bitImplementation 'io.lbry:lbrysdk64:0.102.0'
//__64bitImplementation(name: 'lbrysdk', ext: 'aar')
}
apply plugin: 'com.google.gms.google-services'
GoogleServicesPlugin.config.disableVersionCheck = true

Binary file not shown.

21
app/proguard-rules.pro vendored Normal file
View file

@ -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

View file

@ -0,0 +1,27 @@
package io.lbry.browser;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("io.lbry.browser", appContext.getPackageName());
}
}

View file

@ -0,0 +1,12 @@
package io.lbry.browser.utils;
import androidx.test.filters.SmallTest;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
@SmallTest
public class HelperTest {
}

View file

@ -0,0 +1,127 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="io.lbry.browser"
android:installLocation="auto">
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="com.android.vending.BILLING" />
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-sdk tools:overrideLibrary="com.google.zxing.client.android" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true">
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"/>
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_lbry" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_color"
android:resource="@color/lbryGreen" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="@string/default_notification_channel_id"/>
<meta-data android:name="wakelock" android:value="0"/>
<activity
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|screenLayout"
android:name=".MainActivity"
android:label="@string/app_name"
android:supportsPictureInPicture="true"
android:theme="@style/AppTheme.NoActionBar"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="video/*" />
<data android:mimeType="image/*" />
<data android:mimeType="text/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="lbry" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" android:host="open.lbry.com"/>
<data android:scheme="https" android:host="lbry.tv" android:pathPattern="/..*/*" />
<data android:scheme="https" android:host="lbry.tv" android:pathPattern="/.*:.*" />
<data android:scheme="https" android:host="lbry.tv" android:pathPattern="/.*#.*" />
<data android:scheme="https" android:host="lbry.lat" android:pathPattern="/..*/*" />
<data android:scheme="https" android:host="lbry.lat" android:pathPattern="/.*:.*" />
<data android:scheme="https" android:host="lbry.lat" android:pathPattern="/.*#.*" />
<data android:scheme="https" android:host="lbry.fr" android:pathPattern="/..*/*" />
<data android:scheme="https" android:host="lbry.fr" android:pathPattern="/.*:.*" />
<data android:scheme="https" android:host="lbry.fr" android:pathPattern="/.*#.*" />
<data android:scheme="https" android:host="lbry.in" android:pathPattern="/..*/*" />
<data android:scheme="https" android:host="lbry.in" android:pathPattern="/.*:.*" />
<data android:scheme="https" android:host="lbry.in" android:pathPattern="/.*#.*" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name=".FirstRunActivity"
android:launchMode="singleTask"
android:parentActivityName=".MainActivity"
android:theme="@style/AppTheme.NoActionBarTranslucent" />
<activity
android:name=".VerificationActivity"
android:launchMode="singleTask"
android:parentActivityName=".MainActivity"
android:theme="@style/AppTheme.NoActionBarTranslucent"
android:windowSoftInputMode="adjustResize" />
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="fullSensor"
tools:replace="screenOrientation" />
<service
android:name="io.lbry.browser.LbrynetMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<provider
android:name="io.lbry.browser.LocalFileProvider"
android:authorities="io.lbry.browser.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
</application>
</manifest>

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View file

@ -0,0 +1,252 @@
package io.lbry.browser;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.method.LinkMovementMethod;
import android.view.View;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.text.HtmlCompat;
import androidx.preference.PreferenceManager;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import io.lbry.browser.exceptions.AuthTokenInvalidatedException;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbry;
import io.lbry.browser.utils.LbryAnalytics;
import io.lbry.browser.utils.Lbryio;
import io.lbry.lbrysdk.LbrynetService;
import io.lbry.lbrysdk.ServiceHelper;
import io.lbry.lbrysdk.Utils;
public class FirstRunActivity extends AppCompatActivity {
private BroadcastReceiver sdkReceiver;
private BroadcastReceiver authReceiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_first_run);
TextView welcomeTos = findViewById(R.id.welcome_text_view_tos);
welcomeTos.setMovementMethod(LinkMovementMethod.getInstance());
welcomeTos.setText(HtmlCompat.fromHtml(getString(R.string.welcome_tos), HtmlCompat.FROM_HTML_MODE_LEGACY));
findViewById(R.id.welcome_link_use_lbry).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
finishFirstRun();
}
});
registerAuthReceiver();
findViewById(R.id.welcome_wait_container).setVisibility(View.VISIBLE);
IntentFilter filter = new IntentFilter();
filter.addAction(MainActivity.ACTION_SDK_READY);
filter.addAction(LbrynetService.ACTION_STOP_SERVICE);
sdkReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (MainActivity.ACTION_SDK_READY.equals(action)) {
// authenticate after we receive the sdk ready event
authenticate();
} else if (LbrynetService.ACTION_STOP_SERVICE.equals(action)) {
finish();
if (MainActivity.instance != null) {
MainActivity.instance.finish();
}
}
}
};
registerReceiver(sdkReceiver, filter);
CheckInstallIdTask task = new CheckInstallIdTask(this, new CheckInstallIdTask.InstallIdHandler() {
@Override
public void onInstallIdChecked(boolean result) {
// start the sdk from FirstRun
boolean serviceRunning = MainActivity.isServiceRunning(MainActivity.instance, LbrynetService.class);
if (!serviceRunning) {
Lbry.SDK_READY = false;
ServiceHelper.start(MainActivity.instance, "", LbrynetService.class, "lbrynetservice");
}
if (result) {
// install_id generated and validated, authenticate now
authenticate();
return;
}
// we weren't able to generate the install_id ourselves, depend on the sdk for that
if (Lbry.SDK_READY) {
authenticate();
return;
}
}
});
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
public void onResume() {
super.onResume();
LbryAnalytics.setCurrentScreen(this, "First Run", "FirstRun");
}
private void registerAuthReceiver() {
IntentFilter filter = new IntentFilter();
filter.addAction(MainActivity.ACTION_USER_AUTHENTICATION_SUCCESS);
filter.addAction(MainActivity.ACTION_USER_AUTHENTICATION_FAILED);
authReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (MainActivity.ACTION_USER_AUTHENTICATION_SUCCESS.equals(intent.getAction())) {
handleAuthenticationSuccess();
} else {
handleAuthenticationFailed();
}
}
};
registerReceiver(authReceiver, filter);
}
private void handleAuthenticationSuccess() {
// first_auth completed event
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
boolean firstAuthCompleted = sp.getBoolean(MainActivity.PREFERENCE_KEY_INTERNAL_FIRST_AUTH_COMPLETED, false);
if (!firstAuthCompleted) {
LbryAnalytics.logEvent(LbryAnalytics.EVENT_FIRST_USER_AUTH);
sp.edit().putBoolean(MainActivity.PREFERENCE_KEY_INTERNAL_FIRST_AUTH_COMPLETED, true).apply();
}
findViewById(R.id.welcome_wait_container).setVisibility(View.GONE);
findViewById(R.id.welcome_display).setVisibility(View.VISIBLE);
findViewById(R.id.welcome_link_use_lbry).setVisibility(View.VISIBLE);
}
private void handleAuthenticationFailed() {
findViewById(R.id.welcome_progress_bar).setVisibility(View.GONE);
((TextView) findViewById(R.id.welcome_wait_text)).setText(R.string.startup_failed);
}
private void authenticate() {
new AuthenticateTask(this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void finishFirstRun() {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
sp.edit().putBoolean(MainActivity.PREFERENCE_KEY_INTERNAL_FIRST_RUN_COMPLETED, true).apply();
// first_run_completed event
LbryAnalytics.logEvent(LbryAnalytics.EVENT_FIRST_RUN_COMPLETED);
finish();
}
@Override
public void onBackPressed() {
return;
}
@Override
protected void onDestroy() {
Helper.unregisterReceiver(authReceiver, this);
Helper.unregisterReceiver(sdkReceiver, this);
super.onDestroy();
}
private void generateIdAndAuthenticate() {
}
private static class CheckInstallIdTask extends AsyncTask<Void, Void, Boolean> {
private final Context context;
private final InstallIdHandler handler;
public CheckInstallIdTask(Context context, InstallIdHandler handler) {
this.context = context;
this.handler = handler;
}
protected Boolean doInBackground(Void... params) {
// Load the installation id from the file system
String lbrynetDir = String.format("%s/%s", Utils.getAppInternalStorageDir(context), "lbrynet");
File dir = new File(lbrynetDir);
boolean dirExists = dir.isDirectory();
if (!dirExists) {
dirExists = dir.mkdirs();
}
if (!dirExists) {
return false;
}
String installIdPath = String.format("%s/install_id", lbrynetDir);
File file = new File(installIdPath);
String installId = null;
if (!file.exists()) {
// generate the install_id
installId = Lbry.generateId();
BufferedWriter writer = null;
try {
writer = new BufferedWriter(new FileWriter(file));
writer.write(installId);
} catch (IOException ex) {
return false;
} finally {
Helper.closeCloseable(writer);
}
} else {
// read the installation id from the file
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(new FileInputStream(installIdPath)));
installId = reader.readLine();
} catch (IOException ex) {
return false;
} finally {
Helper.closeCloseable(reader);
}
}
if (!Helper.isNullOrEmpty(installId)) {
Lbry.INSTALLATION_ID = installId;
}
return !Helper.isNullOrEmpty(installId);
}
protected void onPostExecute(Boolean result) {
if (handler != null) {
handler.onInstallIdChecked(result);
}
}
public interface InstallIdHandler {
void onInstallIdChecked(boolean result);
}
}
private static class AuthenticateTask extends AsyncTask<Void, Void, Void> {
private final Context context;
public AuthenticateTask(Context context) {
this.context = context;
}
protected Void doInBackground(Void... params) {
try {
Lbryio.authenticate(context);
} catch (AuthTokenInvalidatedException ex) {
// pass
}
return null;
}
}
}

View file

@ -6,43 +6,45 @@ import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.sqlite.SQLiteDatabase;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.ContextCompat;
import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager;
import android.util.Log;
import com.google.firebase.analytics.FirebaseAnalytics;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
import io.lbry.browser.reactmodules.UtilityModule;
import io.lbry.browser.data.DatabaseHelper;
import io.lbry.browser.model.lbryinc.LbryNotification;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.LbryAnalytics;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
public class LbrynetMessagingService extends FirebaseMessagingService {
public static final String ACTION_NOTIFICATION_RECEIVED = "io.lbry.browser.Broadcast.NotificationReceived";
private static final String TAG = "LbrynetMessagingService";
private static final String NOTIFICATION_CHANNEL_ID = "io.lbry.browser.LBRY_ENGAGEMENT_CHANNEL";
private static final String TYPE_COMMENT = "comment";
private static final String TYPE_SUBSCRIPTION = "subscription";
private static final String TYPE_REWARD = "reward";
private static final String TYPE_INTERESTS = "interests";
private static final String TYPE_CREATOR = "creator";
private FirebaseAnalytics firebaseAnalytics;
@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
Log.d(TAG, "From: " + remoteMessage.getFrom());
if (firebaseAnalytics == null) {
firebaseAnalytics = FirebaseAnalytics.getInstance(this);
}
@ -54,22 +56,51 @@ public class LbrynetMessagingService extends FirebaseMessagingService {
String title = payload.get("title");
String body = payload.get("body");
String name = payload.get("name"); // notification name
if (type != null && getEnabledTypes().indexOf(type) > -1 && body != null && body.trim().length() > 0) {
String hash = payload.get("hash"); // comment hash
if (type != null && getEnabledTypes().contains(type) && body != null && body.trim().length() > 0) {
// only log the receive event for valid notifications received
if (firebaseAnalytics != null) {
Bundle bundle = new Bundle();
bundle.putString("name", name);
firebaseAnalytics.logEvent("lbry_notification_receive", bundle);
firebaseAnalytics.logEvent(LbryAnalytics.EVENT_LBRY_NOTIFICATION_RECEIVE, bundle);
}
if (!Helper.isNullOrEmpty(hash)) {
url = String.format("%s?comment_hash=%s", url, hash);
}
sendNotification(title, body, type, url, name);
}
// persist the notification data
try {
DatabaseHelper helper = DatabaseHelper.getInstance();
SQLiteDatabase db = helper.getWritableDatabase();
LbryNotification lnotification = new LbryNotification();
lnotification.setTitle(title);
lnotification.setDescription(body);
lnotification.setTargetUrl(url);
lnotification.setTimestamp(new Date());
DatabaseHelper.createOrUpdateNotification(lnotification, db);
// send a broadcast
Intent intent = new Intent(ACTION_NOTIFICATION_RECEIVED);
intent.putExtra("title", title);
intent.putExtra("body", body);
intent.putExtra("url", url);
intent.putExtra("timestamp", lnotification.getTimestamp().getTime());
sendBroadcast(intent);
} catch (Exception ex) {
// don't fail if any error occurs while saving a notification
Log.e(TAG, "could not save notification", ex);
}
}
}
@Override
public void onNewToken(String token) {
Log.d(TAG, "Refreshed token: " + token);
//Log.d(TAG, "Refreshed token: " + token);
// If you want to send messages to this application instance or
// manage this apps subscriptions on the server side, send the
@ -89,7 +120,7 @@ public class LbrynetMessagingService extends FirebaseMessagingService {
// TODO: Implement this method to send token to your app server.
}
/**
/**
* Create and show a simple notification containing the received FCM message.
*
* @param messageBody FCM message body received.
@ -128,27 +159,30 @@ public class LbrynetMessagingService extends FirebaseMessagingService {
// Since android Oreo notification channel is needed.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
NOTIFICATION_CHANNEL_ID, "LBRY Engagement", NotificationManager.IMPORTANCE_DEFAULT);
NOTIFICATION_CHANNEL_ID, "LBRY Engagement", NotificationManager.IMPORTANCE_DEFAULT);
notificationManager.createNotificationChannel(channel);
}
notificationManager.notify(9898, notificationBuilder.build());
notificationManager.notify(3, notificationBuilder.build());
}
public List<String> getEnabledTypes() {
SharedPreferences sp = getSharedPreferences(MainActivity.SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
List<String> enabledTypes = new ArrayList<String>();
if (sp.getBoolean(UtilityModule.RECEIVE_SUBSCRIPTION_NOTIFICATIONS, true)) {
if (sp.getBoolean(MainActivity.PREFERENCE_KEY_NOTIFICATION_COMMENTS, true)) {
enabledTypes.add(TYPE_COMMENT);
}
if (sp.getBoolean(MainActivity.PREFERENCE_KEY_NOTIFICATION_SUBSCRIPTIONS, true)) {
enabledTypes.add(TYPE_SUBSCRIPTION);
}
if (sp.getBoolean(UtilityModule.RECEIVE_REWARD_NOTIFICATIONS, true)) {
if (sp.getBoolean(MainActivity.PREFERENCE_KEY_NOTIFICATION_REWARDS, true)) {
enabledTypes.add(TYPE_REWARD);
}
if (sp.getBoolean(UtilityModule.RECEIVE_INTERESTS_NOTIFICATIONS, true)) {
if (sp.getBoolean(MainActivity.PREFERENCE_KEY_NOTIFICATION_CONTENT_INTERESTS, true)) {
enabledTypes.add(TYPE_INTERESTS);
}
if (sp.getBoolean(UtilityModule.RECEIVE_CREATOR_NOTIFICATIONS, true)) {
if (sp.getBoolean(MainActivity.PREFERENCE_KEY_NOTIFICATION_CREATOR, true)) {
enabledTypes.add(TYPE_CREATOR);
}

View file

@ -1,6 +1,6 @@
package io.lbry.browser;
import android.support.v4.content.FileProvider;
import androidx.core.content.FileProvider;
public class LocalFileProvider extends FileProvider {

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,469 @@
package io.lbry.browser;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Color;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.widget.ViewPager2;
import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsParams;
import com.android.billingclient.api.SkuDetailsResponseListener;
import com.google.android.material.snackbar.Snackbar;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import io.lbry.browser.adapter.VerificationPagerAdapter;
import io.lbry.browser.listener.SdkStatusListener;
import io.lbry.browser.listener.SignInListener;
import io.lbry.browser.listener.WalletSyncListener;
import io.lbry.browser.model.lbryinc.RewardVerified;
import io.lbry.browser.model.lbryinc.User;
import io.lbry.browser.tasks.RewardVerifiedHandler;
import io.lbry.browser.tasks.lbryinc.FetchCurrentUserTask;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.LbryAnalytics;
import io.lbry.browser.utils.Lbryio;
import io.lbry.lbrysdk.LbrynetService;
public class VerificationActivity extends FragmentActivity implements SignInListener, WalletSyncListener {
public static final int VERIFICATION_FLOW_SIGN_IN = 1;
public static final int VERIFICATION_FLOW_REWARDS = 2;
public static final int VERIFICATION_FLOW_WALLET = 3;
private List<SdkStatusListener> sdkStatusListeners;
private BillingClient billingClient;
private BroadcastReceiver sdkReceiver;
private String email;
private boolean signedIn;
private int flow;
private final PurchasesUpdatedListener purchasesUpdatedListener = new PurchasesUpdatedListener() {
@Override
public void onPurchasesUpdated(@NonNull BillingResult billingResult, @Nullable List<Purchase> purchases) {
int responseCode = billingResult.getResponseCode();
if (responseCode == BillingClient.BillingResponseCode.OK && purchases != null)
{
for (Purchase purchase : purchases) {
if (MainActivity.SKU_SKIP.equalsIgnoreCase(purchase.getSku())) {
showLoading();
MainActivity.handleBillingPurchase(
purchase,
billingClient,
VerificationActivity.this, null, new RewardVerifiedHandler() {
@Override
public void onSuccess(RewardVerified rewardVerified) {
if (Lbryio.currentUser != null) {
Lbryio.currentUser.setRewardApproved(rewardVerified.isRewardApproved());
}
if (!rewardVerified.isRewardApproved()) {
// show pending purchase message (possible slow card tx)
Snackbar.make(findViewById(R.id.verification_pager), R.string.purchase_request_pending, Snackbar.LENGTH_LONG).show();
} else {
Snackbar.make(findViewById(R.id.verification_pager), R.string.reward_verification_successful, Snackbar.LENGTH_LONG).show();
}
setResult(RESULT_OK);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
finish();
}
}, 3000);
}
@Override
public void onError(Exception error) {
showFetchUserError(getString(R.string.purchase_request_failed_error));
hideLoading();
}
});
}
}
}
}
};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
sdkStatusListeners = new ArrayList<>();
signedIn = Lbryio.isSignedIn();
Intent intent = getIntent();
if (intent != null) {
flow = intent.getIntExtra("flow", -1);
if (flow == -1 || (flow == VERIFICATION_FLOW_SIGN_IN && signedIn)) {
// no flow specified (or user is already signed in), just exit
setResult(signedIn ? RESULT_OK : RESULT_CANCELED);
finish();
return;
}
}
if (!Arrays.asList(VERIFICATION_FLOW_SIGN_IN, VERIFICATION_FLOW_REWARDS, VERIFICATION_FLOW_WALLET).contains(flow)) {
// invalid flow specified
setResult(RESULT_CANCELED);
finish();
return;
}
IntentFilter filter = new IntentFilter();
filter.addAction(LbrynetService.ACTION_STOP_SERVICE);
filter.addAction(MainActivity.ACTION_SDK_READY);
sdkReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (MainActivity.ACTION_SDK_READY.equals(action)) {
for (SdkStatusListener listener : sdkStatusListeners) {
if (listener != null) {
listener.onSdkReady();
}
}
} else if (LbrynetService.ACTION_STOP_SERVICE.equals(action)) {
finish();
}
}
};
registerReceiver(sdkReceiver, filter);
billingClient = BillingClient.newBuilder(this)
.setListener(purchasesUpdatedListener)
.enablePendingPurchases()
.build();
establishBillingClientConnection();
setContentView(R.layout.activity_verification);
ViewPager2 viewPager = findViewById(R.id.verification_pager);
viewPager.setUserInputEnabled(false);
viewPager.setSaveEnabled(false);
viewPager.setAdapter(new VerificationPagerAdapter(this));
findViewById(R.id.verification_close_button).setVisibility(View.VISIBLE);
findViewById(R.id.verification_close_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
setResult(RESULT_CANCELED);
finish();
}
});
}
private void establishBillingClientConnection() {
if (billingClient != null) {
billingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
// no need to do anything here. purchases are always checked server-side
}
}
@Override
public void onBillingServiceDisconnected() {
establishBillingClientConnection();
}
});
}
}
public void onResume() {
super.onResume();
LbryAnalytics.setCurrentScreen(this, "Verification", "Verification");
checkFlow();
}
public void checkFlow() {
ViewPager2 viewPager = findViewById(R.id.verification_pager);
if (Lbryio.isSignedIn()) {
boolean flowHandled = false;
if (flow == VERIFICATION_FLOW_WALLET) {
viewPager.setCurrentItem(VerificationPagerAdapter.PAGE_VERIFICATION_WALLET, false);
flowHandled = true;
} else if (flow == VERIFICATION_FLOW_REWARDS) {
User user = Lbryio.currentUser;
// disable phone verification for now
if (!user.isIdentityVerified()) {
// phone number verification required
viewPager.setCurrentItem(VerificationPagerAdapter.PAGE_VERIFICATION_PHONE, false);
flowHandled = true;
} else {
if (!user.isRewardApproved()) {
// manual verification required
viewPager.setCurrentItem(VerificationPagerAdapter.PAGE_VERIFICATION_MANUAL, false);
flowHandled = true;
}
}
}
if (!flowHandled) {
// user has already been verified and or reward approved
setResult(RESULT_CANCELED);
finish();
return;
}
}
}
public void showPhoneVerification() {
ViewPager2 viewPager = findViewById(R.id.verification_pager);
viewPager.setCurrentItem(VerificationPagerAdapter.PAGE_VERIFICATION_PHONE, false);
}
public void showLoading() {
findViewById(R.id.verification_loading_progress).setVisibility(View.VISIBLE);
findViewById(R.id.verification_pager).setVisibility(View.INVISIBLE);
findViewById(R.id.verification_close_button).setVisibility(View.GONE);
}
public void hideLoading() {
findViewById(R.id.verification_loading_progress).setVisibility(View.GONE);
findViewById(R.id.verification_pager).setVisibility(View.VISIBLE);
}
@Override
public void onBackPressed() {
ViewPager2 viewPager = findViewById(R.id.verification_pager);
if (viewPager.getCurrentItem() != VerificationPagerAdapter.PAGE_VERIFICATION_MANUAL)
viewPager.setCurrentItem(VerificationPagerAdapter.PAGE_VERIFICATION_MANUAL);
else
super.onBackPressed();
}
public void onEmailAdded(String email) {
this.email = email;
findViewById(R.id.verification_close_button).setVisibility(View.GONE);
Bundle bundle = new Bundle();
bundle.putString("email", email);
LbryAnalytics.logEvent(LbryAnalytics.EVENT_EMAIL_ADDED, bundle);
}
public void onEmailEdit() {
findViewById(R.id.verification_close_button).setVisibility(View.VISIBLE);
}
public void onEmailVerified() {
Snackbar.make(findViewById(R.id.verification_pager), R.string.sign_in_successful, Snackbar.LENGTH_LONG).show();
sendBroadcast(new Intent(MainActivity.ACTION_USER_SIGN_IN_SUCCESS));
Bundle bundle = new Bundle();
bundle.putString("email", email);
LbryAnalytics.logEvent(LbryAnalytics.EVENT_EMAIL_VERIFIED, bundle);
if (flow == VERIFICATION_FLOW_SIGN_IN) {
final Intent resultIntent = new Intent();
resultIntent.putExtra("flow", VERIFICATION_FLOW_SIGN_IN);
resultIntent.putExtra("email", email);
// only sign in required, don't do anything else
showLoading();
FetchCurrentUserTask task = new FetchCurrentUserTask(this, new FetchCurrentUserTask.FetchUserTaskHandler() {
@Override
public void onSuccess(User user) {
Lbryio.currentUser = user;
setResult(RESULT_OK, resultIntent);
finish();
}
@Override
public void onError(Exception error) {
showFetchUserError(error != null ? error.getMessage() : getString(R.string.fetch_current_user_error));
hideLoading();
}
});
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} else {
// change pager view depending on flow
showLoading();
FetchCurrentUserTask task = new FetchCurrentUserTask(this, new FetchCurrentUserTask.FetchUserTaskHandler() {
@Override
public void onSuccess(User user) {
hideLoading();
findViewById(R.id.verification_close_button).setVisibility(View.VISIBLE);
Lbryio.currentUser = user;
ViewPager2 viewPager = findViewById(R.id.verification_pager);
// for rewards, (show phone verification if not done, or manual verification if required)
if (flow == VERIFICATION_FLOW_REWARDS) {
if (!user.isIdentityVerified()) {
// phone number verification required
viewPager.setCurrentItem(VerificationPagerAdapter.PAGE_VERIFICATION_PHONE, false);
} else {
if (!user.isRewardApproved()) {
// manual verification required
viewPager.setCurrentItem(VerificationPagerAdapter.PAGE_VERIFICATION_MANUAL, false);
} else {
// fully verified
setResult(RESULT_OK);
finish();
}
}
} else if (flow == VERIFICATION_FLOW_WALLET) {
// for wallet sync, if password unlock is required, show password entry page
viewPager.setCurrentItem(VerificationPagerAdapter.PAGE_VERIFICATION_WALLET, false);
}
}
@Override
public void onError(Exception error) {
showFetchUserError(error != null ? error.getMessage() : getString(R.string.fetch_current_user_error));
hideLoading();
}
});
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
@Override
public void onPhoneAdded(String countryCode, String phoneNumber) {
}
@Override
public void onPhoneVerified() {
showLoading();
FetchCurrentUserTask task = new FetchCurrentUserTask(this, new FetchCurrentUserTask.FetchUserTaskHandler() {
@Override
public void onSuccess(User user) {
Lbryio.currentUser = user;
if (user.isIdentityVerified() && user.isRewardApproved()) {
// verified for rewards
LbryAnalytics.logEvent(LbryAnalytics.EVENT_REWARD_ELIGIBILITY_COMPLETED);
setResult(RESULT_OK);
finish();
return;
}
findViewById(R.id.verification_close_button).setVisibility(View.VISIBLE);
// show manual verification page if the user is still not reward approved
ViewPager2 viewPager = findViewById(R.id.verification_pager);
viewPager.setCurrentItem(VerificationPagerAdapter.PAGE_VERIFICATION_MANUAL, false);
hideLoading();
}
@Override
public void onError(Exception error) {
showFetchUserError(error != null ? error.getMessage() : getString(R.string.fetch_current_user_error));
hideLoading();
}
});
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void showFetchUserError(String message) {
Snackbar.make(findViewById(R.id.verification_pager), message, Snackbar.LENGTH_LONG).
setBackgroundTint(Color.RED).setTextColor(Color.WHITE).show();
}
@Override
public void onManualVerifyContinue() {
setResult(RESULT_OK);
finish();
}
@Override
public void onWalletSyncProcessing() {
findViewById(R.id.verification_close_button).setVisibility(View.GONE);
}
@Override
public void onWalletSyncWaitingForInput() {
findViewById(R.id.verification_close_button).setVisibility(View.VISIBLE);
}
@Override
public void onWalletSyncEnabled() {
findViewById(R.id.verification_close_button).setVisibility(View.VISIBLE);
setResult(RESULT_OK);
finish();
}
@Override
public void onWalletSyncFailed(Exception error) {
findViewById(R.id.verification_close_button).setVisibility(View.VISIBLE);
}
@Override
public void onSkipQueueAction() {
if (billingClient != null) {
List<String> skuList = new ArrayList<>();
skuList.add(MainActivity.SKU_SKIP);
SkuDetailsParams detailsParams = SkuDetailsParams.newBuilder().
setType(BillingClient.SkuType.INAPP).
setSkusList(skuList).build();
billingClient.querySkuDetailsAsync(detailsParams, new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(@NonNull BillingResult billingResult, @Nullable List<SkuDetails> list) {
if (list != null && list.size() > 0) {
// we only queried one product, so it should be the first item in the list
SkuDetails skuDetails = list.get(0);
// launch the billing flow for skip queue
BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder().
setSkuDetails(skuDetails).build();
billingClient.launchBillingFlow(VerificationActivity.this, billingFlowParams);
}
}
});
}
}
@Override
public void onTwitterVerified() {
Snackbar.make(findViewById(R.id.verification_pager), R.string.reward_verification_successful, Snackbar.LENGTH_LONG).show();
setResult(RESULT_OK);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
finish();
}
}, 3000);
}
@Override
public void onManualProgress(boolean progress) {
if (progress) {
findViewById(R.id.verification_close_button).setVisibility(View.GONE);
} else {
findViewById(R.id.verification_close_button).setVisibility(View.VISIBLE);
}
}
@Override
public void onDestroy() {
Helper.unregisterReceiver(sdkReceiver, this);
super.onDestroy();
}
public void addSdkStatusListener(SdkStatusListener listener) {
if (!sdkStatusListeners.contains(listener)) {
sdkStatusListeners.add(listener);
}
}
public void removeSdkStatusListener(SdkStatusListener listener) {
sdkStatusListeners.remove(listener);
}
}

View file

@ -0,0 +1,125 @@
package io.lbry.browser.adapter;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
import java.util.ArrayList;
import java.util.List;
import io.lbry.browser.R;
import io.lbry.browser.model.Claim;
import io.lbry.browser.listener.ChannelItemSelectionListener;
import io.lbry.browser.utils.Helper;
import lombok.Getter;
import lombok.Setter;
public class ChannelFilterListAdapter extends RecyclerView.Adapter<ChannelFilterListAdapter.ViewHolder> {
private final Context context;
private List<Claim> items;
@Getter
@Setter
private Claim selectedItem;
@Setter
private ChannelItemSelectionListener listener;
public ChannelFilterListAdapter(Context context) {
this.context = context;
this.items = new ArrayList<>();
// Always list the placeholder as the first item
Claim claim = new Claim();
claim.setPlaceholder(true);
items.add(claim);
}
public static class ViewHolder extends RecyclerView.ViewHolder {
protected final View mediaContainer;
protected final View alphaContainer;
protected final View allView;
protected final ImageView thumbnailView;
protected final TextView alphaView;
protected final TextView titleView;
public ViewHolder(View v) {
super(v);
mediaContainer = v.findViewById(R.id.channel_filter_media_container);
alphaContainer = v.findViewById(R.id.channel_filter_no_thumbnail);
alphaView = v.findViewById(R.id.channel_filter_alpha_view);
thumbnailView = v.findViewById(R.id.channel_filter_thumbnail);
titleView = v.findViewById(R.id.channel_filter_title);
allView = v.findViewById(R.id.channel_filter_all_container);
}
}
public int getItemCount() {
return items != null ? items.size() : 0;
}
public boolean isClaimSelected(Claim claim) {
return claim.equals(selectedItem);
}
public void clearClaims() {
items = new ArrayList<>(items.subList(0, 1));
notifyDataSetChanged();
}
public void addClaims(List<Claim> claims) {
for (Claim claim : claims) {
if (!items.contains(claim)) {
items.add(claim);
}
}
notifyDataSetChanged();
}
@Override
public ChannelFilterListAdapter.ViewHolder onCreateViewHolder(ViewGroup root, int viewType) {
View v = LayoutInflater.from(context).inflate(R.layout.list_item_channel_filter, root, false);
return new ChannelFilterListAdapter.ViewHolder(v);
}
@Override
public void onBindViewHolder(ChannelFilterListAdapter.ViewHolder vh, int position) {
Claim claim = items.get(position);
vh.alphaView.setVisibility(claim.isPlaceholder() ? View.GONE : View.VISIBLE);
vh.titleView.setVisibility(claim.isPlaceholder() ? View.INVISIBLE : View.VISIBLE);
vh.allView.setVisibility(claim.isPlaceholder() ? View.VISIBLE : View.GONE);
vh.titleView.setText(Helper.isNullOrEmpty(claim.getTitle()) ? claim.getName() : claim.getTitle());
String thumbnailUrl = claim.getThumbnailUrl(vh.thumbnailView.getLayoutParams().width, vh.thumbnailView.getLayoutParams().height, 85);
if (!Helper.isNullOrEmpty(thumbnailUrl) && context != null) {
Glide.with(context.getApplicationContext()).load(thumbnailUrl).apply(RequestOptions.circleCropTransform()).into(vh.thumbnailView);
}
vh.alphaContainer.setVisibility(claim.isPlaceholder() || Helper.isNullOrEmpty(thumbnailUrl) ? View.VISIBLE : View.GONE);
vh.thumbnailView.setVisibility(claim.isPlaceholder() || Helper.isNullOrEmpty(thumbnailUrl) ? View.GONE : View.VISIBLE);
vh.alphaView.setText(claim.isPlaceholder() ? null : claim.getName() != null ? claim.getName().substring(1, 2).toUpperCase() : "");
int bgColor = Helper.generateRandomColorForValue(claim.getClaimId());
Helper.setIconViewBackgroundColor(vh.alphaContainer, bgColor, claim.isPlaceholder(), context);
vh.itemView.setSelected(isClaimSelected(claim));
vh.itemView.setOnClickListener(view -> {
if (claim.isPlaceholder()) {
selectedItem = null;
if (listener != null) {
listener.onChannelSelectionCleared();
}
} else if (!claim.equals(selectedItem)) {
selectedItem = claim;
if (listener != null) {
listener.onChannelItemSelected(claim);
}
}
notifyDataSetChanged();
});
}
}

View file

@ -0,0 +1,525 @@
package io.lbry.browser.adapter;
import android.content.Context;
import android.text.format.DateUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
import com.google.android.material.snackbar.Snackbar;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import io.lbry.browser.R;
import io.lbry.browser.listener.SelectionModeListener;
import io.lbry.browser.model.Claim;
import io.lbry.browser.model.LbryFile;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.LbryUri;
import io.lbry.browser.utils.Lbryio;
import lombok.Getter;
import lombok.Setter;
public class ClaimListAdapter extends RecyclerView.Adapter<ClaimListAdapter.ViewHolder> {
private static final int VIEW_TYPE_STREAM = 1;
private static final int VIEW_TYPE_CHANNEL = 2;
private static final int VIEW_TYPE_FEATURED = 3; // featured search result
private final Map<String, Claim> quickClaimIdMap;
private final Map<String, Claim> quickClaimUrlMap;
private final Map<String, Boolean> notFoundClaimIdMap;
private final Map<String, Boolean> notFoundClaimUrlMap;
@Setter
private boolean hideFee;
@Setter
private boolean canEnterSelectionMode;
private final Context context;
private List<Claim> items;
private final List<Claim> selectedItems;
@Setter
private ClaimListItemListener listener;
@Getter
@Setter
private boolean inSelectionMode;
@Setter
private SelectionModeListener selectionModeListener;
private float scale;
public ClaimListAdapter(List<Claim> items, Context context) {
this.context = context;
this.items = new ArrayList<>();
for (Claim item : items) {
if (item != null) {
this.items.add(item);
}
}
this.selectedItems = new ArrayList<>();
quickClaimIdMap = new HashMap<>();
quickClaimUrlMap = new HashMap<>();
notFoundClaimIdMap = new HashMap<>();
notFoundClaimUrlMap = new HashMap<>();
if (context != null) {
scale = context.getResources().getDisplayMetrics().density;
}
}
public List<Claim> getSelectedItems() {
return this.selectedItems;
}
public int getSelectedCount() {
return selectedItems != null ? selectedItems.size() : 0;
}
public void clearSelectedItems() {
this.selectedItems.clear();
}
public boolean isClaimSelected(Claim claim) {
return selectedItems.contains(claim);
}
public Claim getFeaturedItem() {
for (Claim claim : items) {
if (claim.isFeatured()) {
return claim;
}
}
return null;
}
public void removeFeaturedItem() {
int featuredIndex = -1;
for (int i = 0; i < items.size(); i++) {
if (items.get(i).isFeatured()) {
featuredIndex = i;
break;
}
}
if (featuredIndex > -1) {
items.remove(featuredIndex);
}
}
public List<Claim> getItems() {
return new ArrayList<>(this.items);
}
public void updateSigningChannelForClaim(Claim resolvedClaim) {
for (Claim claim : items) {
if (claim.getClaimId().equalsIgnoreCase(resolvedClaim.getClaimId())) {
claim.setSigningChannel(resolvedClaim.getSigningChannel());
}
}
}
public void clearItems() {
clearSelectedItems();
this.items.clear();
quickClaimIdMap.clear();
quickClaimUrlMap.clear();
notFoundClaimIdMap.clear();
notFoundClaimUrlMap.clear();
notifyDataSetChanged();
}
public Claim getLastItem() {
return items.size() > 0 ? items.get(items.size() - 1) : null;
}
public void addFeaturedItem(Claim claim) {
items.add(0, claim);
notifyDataSetChanged();
}
public void addItems(List<Claim> claims) {
for (Claim claim : claims) {
if (claim != null && !items.contains(claim)) {
items.add(claim);
}
}
notFoundClaimUrlMap.clear();
notFoundClaimIdMap.clear();
notifyDataSetChanged();
}
public void setItems(List<Claim> claims) {
items = new ArrayList<>();
for (Claim claim : claims) {
if (claim != null) {
items.add(claim);
}
}
notifyDataSetChanged();
}
public void removeItems(List<Claim> claims) {
items.removeAll(claims);
notifyDataSetChanged();
}
public void removeItem(Claim claim) {
items.remove(claim);
selectedItems.remove(claim);
notifyDataSetChanged();
}
public static class ViewHolder extends RecyclerView.ViewHolder {
protected final View feeContainer;
protected final TextView feeView;
protected final ImageView thumbnailView;
protected final View noThumbnailView;
protected final TextView alphaView;
protected final TextView vanityUrlView;
protected final TextView durationView;
protected final TextView titleView;
protected final TextView publisherView;
protected final TextView publishTimeView;
protected final TextView pendingTextView;
protected final View repostInfoView;
protected final TextView repostChannelView;
protected final View selectedOverlayView;
protected final TextView fileSizeView;
protected final ProgressBar downloadProgressView;
protected final TextView deviceView;
protected final View loadingImagePlaceholder;
protected final View loadingTextPlaceholder1;
protected final View loadingTextPlaceholder2;
public ViewHolder(View v) {
super(v);
feeContainer = v.findViewById(R.id.claim_fee_container);
feeView = v.findViewById(R.id.claim_fee);
alphaView = v.findViewById(R.id.claim_thumbnail_alpha);
noThumbnailView = v.findViewById(R.id.claim_no_thumbnail);
thumbnailView = v.findViewById(R.id.claim_thumbnail);
vanityUrlView = v.findViewById(R.id.claim_vanity_url);
durationView = v.findViewById(R.id.claim_duration);
titleView = v.findViewById(R.id.claim_title);
publisherView = v.findViewById(R.id.claim_publisher);
publishTimeView = v.findViewById(R.id.claim_publish_time);
pendingTextView = v.findViewById(R.id.claim_pending_text);
repostInfoView = v.findViewById(R.id.claim_repost_info);
repostChannelView = v.findViewById(R.id.claim_repost_channel);
selectedOverlayView = v.findViewById(R.id.claim_selected_overlay);
fileSizeView = v.findViewById(R.id.claim_file_size);
downloadProgressView = v.findViewById(R.id.claim_download_progress);
deviceView = v.findViewById(R.id.claim_view_device);
loadingImagePlaceholder = v.findViewById(R.id.claim_thumbnail_placeholder);
loadingTextPlaceholder1 = v.findViewById(R.id.claim_text_loading_placeholder_1);
loadingTextPlaceholder2 = v.findViewById(R.id.claim_text_loading_placeholder_2);
}
}
@Override
public int getItemCount() {
return items != null ? items.size() : 0;
}
@Override
public int getItemViewType(int position) {
if (items.get(position).isFeatured()) {
return VIEW_TYPE_FEATURED;
}
Claim claim = items.get(position);
String valueType = items.get(position).getValueType();
Claim actualClaim = Claim.TYPE_REPOST.equalsIgnoreCase(valueType) ? claim.getRepostedClaim() : claim;
return Claim.TYPE_CHANNEL.equalsIgnoreCase(actualClaim.getValueType()) ? VIEW_TYPE_CHANNEL : VIEW_TYPE_STREAM;
}
public void updateFileForClaimByIdOrUrl(LbryFile file, String claimId, String url) {
updateFileForClaimByIdOrUrl(file, claimId, url, false);
}
public void updateFileForClaimByIdOrUrl(LbryFile file, String claimId, String url, boolean skipNotFound) {
if (!skipNotFound) {
if (notFoundClaimIdMap.containsKey(claimId) && notFoundClaimUrlMap.containsKey(url)) {
return;
}
}
if (quickClaimIdMap.containsKey(claimId)) {
quickClaimIdMap.get(claimId).setFile(file);
notifyDataSetChanged();
return;
}
if (quickClaimUrlMap.containsKey(claimId)) {
quickClaimUrlMap.get(claimId).setFile(file);
notifyDataSetChanged();
return;
}
boolean claimFound = false;
for (int i = 0; i < items.size(); i++) {
Claim claim = items.get(i);
if (claimId.equalsIgnoreCase(claim.getClaimId()) || url.equalsIgnoreCase(claim.getPermanentUrl())) {
quickClaimIdMap.put(claimId, claim);
quickClaimUrlMap.put(url, claim);
claim.setFile(file);
notifyDataSetChanged();
claimFound = true;
break;
}
}
if (!claimFound) {
notFoundClaimIdMap.put(claimId, true);
notFoundClaimUrlMap.put(url, true);
}
}
public void clearFileForClaimOrUrl(String outpoint, String url) {
clearFileForClaimOrUrl(outpoint, url, false);
notifyDataSetChanged();
}
public void clearFileForClaimOrUrl(String outpoint, String url, boolean remove) {
int claimIndex = -1;
for (int i = 0; i < items.size(); i++) {
Claim claim = items.get(i);
if (outpoint.equalsIgnoreCase(claim.getOutpoint()) || url.equalsIgnoreCase(claim.getPermanentUrl())) {
claimIndex = i;
claim.setFile(null);
break;
}
}
if (remove && claimIndex > -1) {
Claim removed = items.remove(claimIndex);
selectedItems.remove(removed);
}
notifyDataSetChanged();
}
@Override
public ClaimListAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
int viewResourceId = -1;
switch (viewType) {
case VIEW_TYPE_FEATURED: viewResourceId = R.layout.list_item_featured_search_result; break;
case VIEW_TYPE_CHANNEL: viewResourceId = R.layout.list_item_channel; break;
case VIEW_TYPE_STREAM: default: viewResourceId = R.layout.list_item_stream; break;
}
View v = LayoutInflater.from(context).inflate(viewResourceId, parent, false);
return new ClaimListAdapter.ViewHolder(v);
}
@Override
public void onBindViewHolder(ClaimListAdapter.ViewHolder vh, int position) {
int type = getItemViewType(position);
int paddingTop = position == 0 ? 16 : 8;
int paddingBottom = position == getItemCount() - 1 ? 16 : 8;
int paddingTopScaled = Helper.getScaledValue(paddingTop, scale);
int paddingBottomScaled = Helper.getScaledValue(paddingBottom, scale);
vh.itemView.setPadding(vh.itemView.getPaddingStart(), paddingTopScaled, vh.itemView.getPaddingEnd(), paddingBottomScaled);
Claim original = items.get(position);
boolean isRepost = Claim.TYPE_REPOST.equalsIgnoreCase(original.getValueType());
final Claim item = Claim.TYPE_REPOST.equalsIgnoreCase(original.getValueType()) ?
(original.getRepostedClaim() != null ? original.getRepostedClaim() : original): original;
Claim.GenericMetadata metadata = item.getValue();
Claim signingChannel = item.getSigningChannel();
Claim.StreamMetadata streamMetadata = null;
if (metadata instanceof Claim.StreamMetadata) {
streamMetadata = (Claim.StreamMetadata) metadata;
}
String thumbnailUrl = item.getThumbnailUrl(vh.thumbnailView.getLayoutParams().width, vh.thumbnailView.getLayoutParams().height, 85);
long publishTime = (streamMetadata != null && streamMetadata.getReleaseTime() > 0) ? streamMetadata.getReleaseTime() * 1000 : item.getTimestamp() * 1000;
int bgColor = Helper.generateRandomColorForValue(item.getClaimId());
if (bgColor == 0) {
bgColor = Helper.generateRandomColorForValue(item.getName());
}
boolean isPending = item.getConfirmations() == 0;
boolean isSelected = isClaimSelected(original);
vh.itemView.setSelected(isSelected);
vh.selectedOverlayView.setVisibility(isSelected ? View.VISIBLE : View.GONE);
vh.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (isPending) {
Snackbar snackbar = Snackbar.make(vh.itemView, R.string.item_pending_blockchain, Snackbar.LENGTH_LONG);
TextView snackbarText = snackbar.getView().findViewById(com.google.android.material.R.id.snackbar_text);
snackbarText.setMaxLines(5);
snackbar.show();
return;
}
if (inSelectionMode) {
toggleSelectedClaim(original);
} else {
if (listener != null) {
listener.onClaimClicked(item);
}
}
}
});
vh.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
if (!canEnterSelectionMode) {
return false;
}
if (isPending) {
Snackbar snackbar = Snackbar.make(vh.itemView, R.string.item_pending_blockchain, Snackbar.LENGTH_LONG);
TextView snackbarText = snackbar.getView().findViewById(com.google.android.material.R.id.snackbar_text);
snackbarText.setMaxLines(5);
snackbar.show();
return false;
}
if (!inSelectionMode) {
inSelectionMode = true;
if (selectionModeListener != null) {
selectionModeListener.onEnterSelectionMode();
}
}
toggleSelectedClaim(original);
return true;
}
});
vh.publisherView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (listener != null && signingChannel != null) {
listener.onClaimClicked(signingChannel);
}
}
});
vh.publishTimeView.setVisibility(!isPending ? View.VISIBLE : View.GONE);
vh.pendingTextView.setVisibility(isPending && !item.isLoadingPlaceholder() ? View.VISIBLE : View.GONE);
vh.repostInfoView.setVisibility(isRepost && type != VIEW_TYPE_FEATURED ? View.VISIBLE : View.GONE);
vh.repostChannelView.setText(isRepost && original.getSigningChannel() != null ? original.getSigningChannel().getName() : null);
vh.repostChannelView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (listener != null) {
listener.onClaimClicked(original.getSigningChannel());
}
}
});
vh.titleView.setText(Helper.isNullOrEmpty(item.getTitle()) ? item.getName() : item.getTitle());
if (type == VIEW_TYPE_FEATURED) {
LbryUri vanityUrl = new LbryUri();
vanityUrl.setClaimName(item.getName());
vh.vanityUrlView.setText(vanityUrl.toString());
}
vh.feeContainer.setVisibility(item.isUnresolved() || !Claim.TYPE_STREAM.equalsIgnoreCase(item.getValueType()) ? View.GONE : View.VISIBLE);
vh.noThumbnailView.setVisibility(Helper.isNullOrEmpty(thumbnailUrl) ? View.VISIBLE : View.GONE);
Helper.setIconViewBackgroundColor(vh.noThumbnailView, bgColor, false, context);
Helper.setViewVisibility(vh.loadingImagePlaceholder, item.isLoadingPlaceholder() ? View.VISIBLE : View.GONE);
Helper.setViewVisibility(vh.loadingTextPlaceholder1, item.isLoadingPlaceholder() ? View.VISIBLE : View.GONE);
Helper.setViewVisibility(vh.loadingTextPlaceholder2, item.isLoadingPlaceholder() ? View.VISIBLE : View.GONE);
Helper.setViewVisibility(vh.titleView, !item.isLoadingPlaceholder() ? View.VISIBLE : View.GONE);
Helper.setViewVisibility(vh.publisherView, !item.isLoadingPlaceholder() ? View.VISIBLE : View.GONE);
Helper.setViewVisibility(vh.publishTimeView, !item.isLoadingPlaceholder() && !isPending ? View.VISIBLE : View.GONE);
if (type == VIEW_TYPE_FEATURED && item.isUnresolved()) {
vh.durationView.setVisibility(View.GONE);
vh.titleView.setText("Nothing here. Publish something!");
vh.alphaView.setText(item.getName().substring(0, Math.min(5, item.getName().length() - 1)));
} else {
if (Claim.TYPE_STREAM.equalsIgnoreCase(item.getValueType())) {
long duration = item.getDuration();
if (!Helper.isNullOrEmpty(thumbnailUrl)) {
Glide.with(context.getApplicationContext()).
asBitmap().
load(thumbnailUrl).
centerCrop().
placeholder(R.drawable.bg_thumbnail_placeholder).
into(vh.thumbnailView);
vh.thumbnailView.setVisibility(View.VISIBLE);
} else {
vh.thumbnailView.setVisibility(View.GONE);
}
BigDecimal cost = item.getActualCost(Lbryio.LBCUSDRate);
vh.feeContainer.setVisibility(cost.doubleValue() > 0 && !hideFee ? View.VISIBLE : View.GONE);
vh.feeView.setText(cost.doubleValue() > 0 ? Helper.shortCurrencyFormat(cost.doubleValue()) : "Paid");
vh.alphaView.setText(item.getName().substring(0, Math.min(5, item.getName().length() - 1)));
vh.publisherView.setText(signingChannel != null ? signingChannel.getName() : context.getString(R.string.anonymous));
vh.publishTimeView.setText(DateUtils.getRelativeTimeSpanString(
publishTime, System.currentTimeMillis(), 0, DateUtils.FORMAT_ABBREV_RELATIVE));
vh.durationView.setVisibility(duration > 0 ? View.VISIBLE : View.GONE);
vh.durationView.setText(Helper.formatDuration(duration));
LbryFile claimFile = item.getFile();
boolean isDownloading = false;
int progress = 0;
String fileSizeString = claimFile == null ? null : Helper.formatBytes(claimFile.getTotalBytes(), false);
if (claimFile != null &&
!Helper.isNullOrEmpty(claimFile.getDownloadPath()) &&
!claimFile.isCompleted() &&
claimFile.getWrittenBytes() < claimFile.getTotalBytes()) {
isDownloading = true;
progress = claimFile.getTotalBytes() > 0 ?
Double.valueOf(((double) claimFile.getWrittenBytes() / (double) claimFile.getTotalBytes()) * 100.0).intValue() : 0;
fileSizeString = String.format("%s / %s",
Helper.formatBytes(claimFile.getWrittenBytes(), false),
Helper.formatBytes(claimFile.getTotalBytes(), false));
}
Helper.setViewText(vh.fileSizeView, claimFile != null && !Helper.isNullOrEmpty(claimFile.getDownloadPath()) ? fileSizeString : null);
Helper.setViewVisibility(vh.downloadProgressView, isDownloading ? View.VISIBLE : View.INVISIBLE);
Helper.setViewProgress(vh.downloadProgressView, progress);
Helper.setViewText(vh.deviceView, item.getDevice());
} else if (Claim.TYPE_CHANNEL.equalsIgnoreCase(item.getValueType())) {
if (!Helper.isNullOrEmpty(thumbnailUrl)) {
Glide.with(context.getApplicationContext()).
load(thumbnailUrl).
centerCrop().
placeholder(R.drawable.bg_thumbnail_placeholder).
apply(RequestOptions.circleCropTransform()).
into(vh.thumbnailView);
}
vh.alphaView.setText(item.getName().substring(1, 2).toUpperCase());
vh.publisherView.setText(item.getName());
vh.publishTimeView.setText(DateUtils.getRelativeTimeSpanString(
publishTime, System.currentTimeMillis(), 0, DateUtils.FORMAT_ABBREV_RELATIVE));
}
}
}
private void toggleSelectedClaim(Claim claim) {
if (selectedItems.contains(claim)) {
selectedItems.remove(claim);
} else {
selectedItems.add(claim);
}
if (selectionModeListener != null) {
selectionModeListener.onItemSelectionToggled();
}
if (selectedItems.size() == 0) {
inSelectionMode = false;
if (selectionModeListener != null) {
selectionModeListener.onExitSelectionMode();
}
}
notifyDataSetChanged();
}
public interface ClaimListItemListener {
void onClaimClicked(Claim claim);
}
}

View file

@ -0,0 +1,228 @@
package io.lbry.browser.adapter;
import android.content.Context;
import android.text.format.DateUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
import io.lbry.browser.R;
import io.lbry.browser.model.Claim;
import io.lbry.browser.model.ClaimCacheKey;
import io.lbry.browser.model.Comment;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbry;
import io.lbry.browser.utils.LbryUri;
import lombok.Setter;
public class CommentListAdapter extends RecyclerView.Adapter<CommentListAdapter.ViewHolder> {
private final List<Comment> items;
private final Context context;
private final boolean nested;
private float scale;
@Setter
private ClaimListAdapter.ClaimListItemListener listener;
@Setter
private ReplyClickListener replyListener;
public CommentListAdapter(List<Comment> items, Context context) {
this(items, context, false);
}
public CommentListAdapter(List<Comment> items, Context context, boolean nested) {
this.items = new ArrayList<>(items);
this.context = context;
this.nested = nested;
if (context != null) {
scale = context.getResources().getDisplayMetrics().density;
}
for (Comment item : this.items) {
ClaimCacheKey key = new ClaimCacheKey();
key.setClaimId(item.getChannelId());
if (Lbry.claimCache.containsKey(key)) {
item.setPoster(Lbry.claimCache.get(key));
}
}
}
public void clearItems() {
items.clear();
notifyDataSetChanged();
}
public int getPositionForComment(String commentHash) {
for (int i = 0; i < items.size(); i++) {
if (commentHash.equalsIgnoreCase(items.get(i).getId())) {
return i;
}
}
return -1;
}
@Override
public int getItemCount() {
return items != null ? items.size() : 0;
}
public List<String> getClaimUrlsToResolve() {
List<String> urls = new ArrayList<>();
for (int i = 0; i < items.size(); i++) {
Comment item = items.get(i);
if (item.getPoster() == null) {
LbryUri url = LbryUri.tryParse(String.format("%s#%s", item.getChannelName(), item.getChannelId()));
if (url != null && !urls.contains(url.toString())) {
urls.add(url.toString());
}
}
if (item.getReplies().size() > 0) {
for (int j = 0; j < item.getReplies().size(); j++) {
Comment reply = item.getReplies().get(j);
if (reply.getPoster() == null) {
LbryUri url = LbryUri.tryParse(String.format("%s#%s", reply.getChannelName(), reply.getChannelId()));
if (url != null && !urls.contains(url.toString())) {
urls.add(url.toString());
}
}
}
}
}
return urls;
}
public static class ViewHolder extends RecyclerView.ViewHolder {
protected final TextView channelName;
protected final TextView commentText;
protected final ImageView thumbnailView;
protected final View noThumbnailView;
protected final TextView alphaView;
protected final TextView commentTimeView;
protected final View replyLink;
protected final RecyclerView repliesList;
public ViewHolder (View v) {
super(v);
channelName = v.findViewById(R.id.comment_channel_name);
commentTimeView = v.findViewById(R.id.comment_time);
commentText = v.findViewById(R.id.comment_text);
replyLink = v.findViewById(R.id.comment_reply_link);
thumbnailView = v.findViewById(R.id.comment_thumbnail);
noThumbnailView = v.findViewById(R.id.comment_no_thumbnail);
alphaView = v.findViewById(R.id.comment_thumbnail_alpha);
repliesList = v.findViewById(R.id.comment_replies);
}
}
public void insert(int index, Comment comment) {
if (!items.contains(comment)) {
items.add(index, comment);
notifyDataSetChanged();
}
}
public void addReply(Comment comment) {
for (int i = 0; i < items.size(); i++) {
Comment parent = items.get(i);
if (parent.getId().equalsIgnoreCase(comment.getParentId())) {
parent.addReply(comment);
notifyDataSetChanged();
break;
}
}
}
public void updatePosterForComment(String channelId, Claim channel) {
for (int i = 0 ; i < items.size(); i++) {
Comment item = items.get(i);
List<Comment> replies = item.getReplies();
if (replies != null && replies.size() > 0) {
for (int j = 0; j < replies.size(); j++) {
Comment reply = item.getReplies().get(j);
if (channelId.equalsIgnoreCase(reply.getChannelId())) {
reply.setPoster(channel);
break;
}
}
}
if (channelId.equalsIgnoreCase(item.getChannelId())) {
item.setPoster(channel);
break;
}
}
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(context).inflate(R.layout.list_item_comment, parent, false);
return new CommentListAdapter.ViewHolder(v);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
Comment comment = items.get(position);
holder.itemView.setPadding(
nested ? Helper.getScaledValue(56, scale) : holder.itemView.getPaddingStart(),
holder.itemView.getPaddingTop(),
nested ? 0 : holder.itemView.getPaddingEnd(),
holder.itemView.getPaddingBottom());
holder.channelName.setText(comment.getChannelName());
holder.commentTimeView.setText(DateUtils.getRelativeTimeSpanString(
(comment.getTimestamp() * 1000), System.currentTimeMillis(), 0, DateUtils.FORMAT_ABBREV_RELATIVE));
holder.commentText.setText(comment.getText());
holder.replyLink.setVisibility(!nested ? View.VISIBLE : View.GONE);
boolean hasThumbnail = comment.getPoster() != null && !Helper.isNullOrEmpty(comment.getPoster().getThumbnailUrl());
holder.thumbnailView.setVisibility(hasThumbnail ? View.VISIBLE : View.INVISIBLE);
holder.noThumbnailView.setVisibility(!hasThumbnail ? View.VISIBLE : View.INVISIBLE);
int bgColor = Helper.generateRandomColorForValue(comment.getChannelId());
Helper.setIconViewBackgroundColor(holder.noThumbnailView, bgColor, false, context);
if (hasThumbnail) {
Glide.with(context.getApplicationContext()).asBitmap().load(comment.getPoster().getThumbnailUrl(holder.thumbnailView.getLayoutParams().width, holder.thumbnailView.getLayoutParams().height, 85)).
apply(RequestOptions.circleCropTransform()).into(holder.thumbnailView);
}
holder.alphaView.setText(comment.getChannelName() != null ? comment.getChannelName().substring(1, 2).toUpperCase() : null);
List<Comment> replies = comment.getReplies();
boolean hasReplies = replies != null && replies.size() > 0;
if (hasReplies) {
holder.repliesList.setLayoutManager(new LinearLayoutManager(context));
holder.repliesList.setAdapter(new CommentListAdapter(replies, context, true));
} else {
holder.repliesList.setAdapter(null);
}
holder.channelName.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (listener != null && comment.getPoster() != null) {
listener.onClaimClicked(comment.getPoster());
}
}
});
holder.replyLink.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (replyListener != null) {
replyListener.onReplyClicked(comment);
}
}
});
}
public interface ReplyClickListener {
void onReplyClicked(Comment comment);
}
}

View file

@ -0,0 +1,118 @@
package io.lbry.browser.adapter;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import java.util.ArrayList;
import java.util.List;
import io.lbry.browser.R;
import io.lbry.browser.model.EditorsChoiceItem;
import io.lbry.browser.utils.Helper;
import lombok.Setter;
public class EditorsChoiceItemAdapter extends RecyclerView.Adapter<EditorsChoiceItemAdapter.ViewHolder> {
private static final int VIEW_TYPE_HEADER = 1;
private static final int VIEW_TYPE_CONTENT = 2;
private final Context context;
private final List<EditorsChoiceItem> items;
@Setter
private EditorsChoiceItemListener listener;
public EditorsChoiceItemAdapter(List<EditorsChoiceItem> items, Context context) {
this.context = context;
this.items = new ArrayList<>(items);
}
public void addFeaturedItem(EditorsChoiceItem item) {
items.add(0, item);
notifyDataSetChanged();
}
public void addItems(List<EditorsChoiceItem> items) {
for (EditorsChoiceItem item : items) {
if (!this.items.contains(item)) {
this.items.add(item);
}
}
notifyDataSetChanged();
}
public static class ViewHolder extends RecyclerView.ViewHolder {
protected final ImageView thumbnailView;
protected final TextView descriptionView;
protected final TextView headerView;
protected final TextView titleView;
protected final View cardView;
public ViewHolder(View v) {
super(v);
cardView = v.findViewById(R.id.editors_choice_content_card);
descriptionView = v.findViewById(R.id.editors_choice_content_description);
titleView = v.findViewById(R.id.editors_choice_content_title);
thumbnailView = v.findViewById(R.id.editors_choice_content_thumbnail);
headerView = v.findViewById(R.id.editors_choice_header_title);
}
}
@Override
public int getItemCount() {
return items != null ? items.size() : 0;
}
@Override
public int getItemViewType(int position) {
return items.get(position).isHeader() ? VIEW_TYPE_HEADER : VIEW_TYPE_CONTENT;
}
@Override
public EditorsChoiceItemAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(context).inflate(R.layout.list_item_editors_choice, parent, false);
return new EditorsChoiceItemAdapter.ViewHolder(v);
}
@Override
public void onBindViewHolder(EditorsChoiceItemAdapter.ViewHolder vh, int position) {
int type = getItemViewType(position);
EditorsChoiceItem item = items.get(position);
vh.headerView.setVisibility(type == VIEW_TYPE_HEADER ? View.VISIBLE : View.GONE);
vh.cardView.setVisibility(type == VIEW_TYPE_CONTENT ? View.VISIBLE : View.GONE);
vh.headerView.setText(item.getTitle());
vh.titleView.setText(item.getTitle());
vh.descriptionView.setText(item.getDescription());
if (!Helper.isNullOrEmpty(item.getThumbnailUrl())) {
Glide.with(context.getApplicationContext()).
asBitmap().
load(item.getThumbnailUrl()).
centerCrop().
placeholder(R.drawable.bg_thumbnail_placeholder).
into(vh.thumbnailView);
}
vh.cardView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (listener != null) {
listener.onEditorsChoiceItemClicked(item);
}
}
});
}
public interface EditorsChoiceItemListener {
void onEditorsChoiceItemClicked(EditorsChoiceItem item);
}
}

View file

@ -0,0 +1,119 @@
package io.lbry.browser.adapter;
import android.content.Context;
import android.graphics.Rect;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import java.util.ArrayList;
import java.util.List;
import io.lbry.browser.R;
import io.lbry.browser.model.GalleryItem;
import io.lbry.browser.utils.Helper;
import lombok.Setter;
public class GalleryGridAdapter extends RecyclerView.Adapter<GalleryGridAdapter.ViewHolder> {
private final Context context;
private final List<GalleryItem> items;
@Setter
private GalleryItemClickListener listener;
public GalleryGridAdapter(List<GalleryItem> items, Context context) {
this.items = new ArrayList<>(items);
this.context = context;
}
public static class ViewHolder extends RecyclerView.ViewHolder {
protected final ImageView thumbnailView;
protected final TextView durationView;
public ViewHolder(View v) {
super(v);
thumbnailView = v.findViewById(R.id.gallery_item_thumbnail);
durationView = v.findViewById(R.id.gallery_item_duration);
}
}
public int getItemCount() {
return items != null ? items.size() : 0;
}
public void addItem(GalleryItem item) {
if (!items.contains(item)) {
items.add(item);
notifyDataSetChanged();
}
}
public void addItems(List<GalleryItem> items) {
for (GalleryItem item : items) {
if (!this.items.contains(item)) {
this.items.add(item);
notifyDataSetChanged();
}
}
}
public void clearItems() {
items.clear();
notifyDataSetChanged();
}
@Override
public GalleryGridAdapter.ViewHolder onCreateViewHolder(ViewGroup root, int viewType) {
View v = LayoutInflater.from(context).inflate(R.layout.list_item_gallery, root, false);
return new GalleryGridAdapter.ViewHolder(v);
}
@Override
public void onBindViewHolder(GalleryGridAdapter.ViewHolder vh, int position) {
GalleryItem item = items.get(position);
String thumbnailUrl = item.getThumbnailPath();
Glide.with(context.getApplicationContext()).load(thumbnailUrl).centerCrop().into(vh.thumbnailView);
vh.durationView.setVisibility(item.getDuration() > 0 ? View.VISIBLE : View.INVISIBLE);
vh.durationView.setText(item.getDuration() > 0 ? Helper.formatDuration(Double.valueOf(item.getDuration() / 1000.0).longValue()) : null);
vh.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (listener != null) {
listener.onGalleryItemClicked(item);
}
}
});
}
public interface GalleryItemClickListener {
void onGalleryItemClicked(GalleryItem item);
}
public static class GalleryGridItemDecoration extends RecyclerView.ItemDecoration {
private final int spanCount;
private final int spacing;
public GalleryGridItemDecoration(int spanCount, int spacing) {
this.spanCount = spanCount;
this.spacing = spacing;
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int position = parent.getChildAdapterPosition(view); // item position
int column = position % spanCount; // item column
outRect.left = column * spacing / spanCount; // column * ((1f / spanCount) * spacing)
outRect.right = spacing - (column + 1) * spacing / spanCount; // spacing - (column + 1) * ((1f / spanCount) * spacing)
if (position >= spanCount) {
outRect.top = spacing; // item top
}
}
}
}

View file

@ -0,0 +1,98 @@
package io.lbry.browser.adapter;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import io.lbry.browser.R;
import io.lbry.browser.model.Claim;
public class InlineChannelSpinnerAdapter extends ArrayAdapter<Claim> {
private final List<Claim> channels;
private final int layoutResourceId;
private final LayoutInflater inflater;
public InlineChannelSpinnerAdapter(Context context, int resource, List<Claim> channels) {
super(context, resource, 0, channels);
inflater = LayoutInflater.from(context);
layoutResourceId = resource;
this.channels = new ArrayList<>(channels);
}
public void addPlaceholder(boolean includeAnonymous) {
Claim placeholder = new Claim();
placeholder.setPlaceholder(true);
insert(placeholder, 0);
channels.add(0, placeholder);
if (includeAnonymous) {
Claim anonymous = new Claim();
anonymous.setPlaceholderAnonymous(true);
insert(anonymous, 1);
channels.add(1, anonymous);
}
}
public void addAnonymousPlaceholder() {
Claim anonymous = new Claim();
anonymous.setPlaceholderAnonymous(true);
insert(anonymous, 0);
channels.add(0, anonymous);
}
public void addAll(Collection<? extends Claim> collection) {
for (Claim claim : collection) {
if (!channels.contains(claim)) {
channels.add(claim);
}
}
super.addAll(collection);
}
public void clear() {
channels.clear();
super.clear();
}
public int getItemPosition(Claim item) {
for (int i = 0; i < channels.size(); i++) {
Claim channel = channels.get(i);
if (item.getClaimId() != null && item.getClaimId().equalsIgnoreCase(channel.getClaimId())) {
return i;
}
}
return -1;
}
@Override
public View getDropDownView(int position, View view, ViewGroup parent) {
return createView(position, view, parent);
}
@Override
public View getView(int position, View view, ViewGroup parent) {
return createView(position, view, parent);
}
private View createView(int position, View convertView, ViewGroup parent){
View view = inflater.inflate(layoutResourceId, parent, false);
Context context = getContext();
Claim channel = getItem(position);
String name = channel.getName();
if (channel.isPlaceholder()) {
name = context.getString(R.string.create_a_channel);
} else if (channel.isPlaceholderAnonymous()) {
name = context.getString(R.string.anonymous);
}
TextView label = view.findViewById(R.id.channel_item_name);
label.setText(name);
return view;
}
}

View file

@ -0,0 +1,85 @@
package io.lbry.browser.adapter;
import android.content.Context;
import android.graphics.Typeface;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
import io.lbry.browser.R;
import io.lbry.browser.model.lbryinc.Invitee;
public class InviteeListAdapter extends RecyclerView.Adapter<InviteeListAdapter.ViewHolder> {
private final Context context;
private final List<Invitee> items;
public InviteeListAdapter(List<Invitee> invitees, Context context) {
this.context = context;
this.items = new ArrayList<>(invitees);
}
public void clear() {
items.clear();
notifyDataSetChanged();
}
public List<Invitee> getItems() {
return new ArrayList<>(items);
}
public void addHeader() {
Invitee header = new Invitee();
header.setHeader(true);
items.add(0, header);
}
public void addInvitees(List<Invitee> Invitees) {
for (Invitee tx : Invitees) {
if (!items.contains(tx)) {
items.add(tx);
}
}
notifyDataSetChanged();
}
public int getItemCount() {
return items != null ? items.size() : 0;
}
@Override
public InviteeListAdapter.ViewHolder onCreateViewHolder(ViewGroup root, int viewType) {
View v = LayoutInflater.from(context).inflate(R.layout.list_item_invitee, root, false);
return new InviteeListAdapter.ViewHolder(v);
}
@Override
public void onBindViewHolder(InviteeListAdapter.ViewHolder vh, int position) {
Invitee item = items.get(position);
vh.emailView.setText(item.isHeader() ? context.getString(R.string.email) : item.getEmail());
vh.emailView.setTypeface(null, item.isHeader() ? Typeface.BOLD : Typeface.NORMAL);
String rewardText = context.getString(
item.isInviteRewardClaimed() ? R.string.claimed :
(item.isInviteRewardClaimable() ? R.string.claimable : R.string.unclaimable));
vh.rewardView.setText(item.isHeader() ? context.getString(R.string.reward) : rewardText);
vh.rewardView.setTypeface(null, item.isHeader() ? Typeface.BOLD : Typeface.NORMAL);
}
public static class ViewHolder extends RecyclerView.ViewHolder {
protected final TextView emailView;
protected final TextView rewardView;
public ViewHolder(View v) {
super(v);
emailView = v.findViewById(R.id.invitee_email);
rewardView = v.findViewById(R.id.invitee_reward);
}
}
}

View file

@ -0,0 +1,52 @@
package io.lbry.browser.adapter;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import androidx.annotation.NonNull;
import io.lbry.browser.R;
import io.lbry.browser.model.Language;
import io.lbry.browser.utils.Predefined;
public class LanguageSpinnerAdapter extends ArrayAdapter<Language> {
private final int layoutResourceId;
private final LayoutInflater inflater;
public LanguageSpinnerAdapter(Context context, int resource) {
super(context, resource, 0, Predefined.PUBLISH_LANGUAGES);
inflater = LayoutInflater.from(context);
layoutResourceId = resource;
}
public int getItemPosition(String languageCode) {
for (int i = 0; i < Predefined.PUBLISH_LANGUAGES.size(); i++) {
Language lang = Predefined.PUBLISH_LANGUAGES.get(i);
if (lang.getCode().equalsIgnoreCase(languageCode)) {
return i;
}
}
return -1;
}
@Override
public View getDropDownView(int position, View view, @NonNull ViewGroup parent) {
return createView(position, view, parent);
}
@Override
public View getView(int position, View view, @NonNull ViewGroup parent) {
return createView(position, view, parent);
}
private View createView(int position, View convertView, ViewGroup parent) {
Language item = getItem(position);
View view = inflater.inflate(layoutResourceId, parent, false);
TextView label = view.findViewById(R.id.item_display_name);
label.setText(item != null ? item.getStringResourceId() : 0);
return view;
}
}

View file

@ -0,0 +1,51 @@
package io.lbry.browser.adapter;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import androidx.annotation.NonNull;
import io.lbry.browser.R;
import io.lbry.browser.model.License;
import io.lbry.browser.utils.Predefined;
public class LicenseSpinnerAdapter extends ArrayAdapter<License> {
private final int layoutResourceId;
private final LayoutInflater inflater;
public LicenseSpinnerAdapter(Context context, int resource) {
super(context, resource, 0, Predefined.LICENSES);
inflater = LayoutInflater.from(context);
layoutResourceId = resource;
}
public int getItemPosition(String name) {
for (int i = 0; i < Predefined.LICENSES.size(); i++) {
License lic = Predefined.LICENSES.get(i);
if (lic.getName().equalsIgnoreCase(name)) {
return i;
}
}
return -1;
}
@Override
public View getDropDownView(int position, View view, @NonNull ViewGroup parent) {
return createView(position, view, parent);
}
@Override
public View getView(int position, View view, @NonNull ViewGroup parent) {
return createView(position, view, parent);
}
private View createView(int position, View convertView, ViewGroup parent) {
License item = getItem(position);
View view = inflater.inflate(layoutResourceId, parent, false);
TextView label = view.findViewById(R.id.item_display_name);
label.setText(item != null ? item.getStringResourceId() : 0);
return view;
}
}

View file

@ -0,0 +1,114 @@
package io.lbry.browser.adapter;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
import io.lbry.browser.R;
import io.lbry.browser.model.NavMenuItem;
import io.lbry.browser.ui.controls.SolidIconView;
import io.lbry.browser.utils.Helper;
import lombok.Setter;
public class NavigationMenuAdapter extends RecyclerView.Adapter<NavigationMenuAdapter.ViewHolder> {
private static final int TYPE_GROUP = 1;
private static final int TYPE_ITEM = 2;
private final Context context;
private final List<NavMenuItem> menuItems;
private NavMenuItem currentItem;
@Setter
private NavigationMenuItemClickListener listener;
public NavigationMenuAdapter(List<NavMenuItem> menuItems, Context context) {
this.menuItems = new ArrayList<>(menuItems);
this.context = context;
}
public void setCurrentItem(int id) {
for (NavMenuItem item : menuItems) {
if (item.getId() == id) {
this.currentItem = item;
break;
}
}
notifyDataSetChanged();
}
public void setExtraLabelForItem(int id, String extraLabel) {
for (NavMenuItem item : menuItems) {
if (item.getId() == id) {
item.setExtraLabel(extraLabel);
break;
}
}
notifyDataSetChanged();
}
public void setCurrentItem(NavMenuItem currentItem) {
this.currentItem = currentItem;
notifyDataSetChanged();
}
public int getCurrentItemId() {
return currentItem != null ? currentItem.getId() : -1;
}
public static class ViewHolder extends RecyclerView.ViewHolder {
protected final SolidIconView iconView;
protected final TextView titleView;
public ViewHolder(View v) {
super(v);
titleView = v.findViewById(R.id.nav_menu_title);
iconView = v.findViewById(R.id.nav_menu_item_icon);
}
}
@Override
public int getItemCount() {
return menuItems != null ? menuItems.size() : 0;
}
@Override
public int getItemViewType(int position) {
return menuItems.get(position).isGroup() ? TYPE_GROUP : TYPE_ITEM;
}
@Override
public NavigationMenuAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(context).inflate(viewType == TYPE_GROUP ?
R.layout.list_item_nav_menu_group : R.layout.list_item_nav_menu_item, parent, false);
return new NavigationMenuAdapter.ViewHolder(v);
}
@Override
public void onBindViewHolder(ViewHolder vh, int position) {
int type = getItemViewType(position);
NavMenuItem item = menuItems.get(position);
String displayTitle = !Helper.isNullOrEmpty(item.getExtraLabel()) ? String.format("%s (%s)", item.getTitle(), item.getExtraLabel()) : item.getTitle();
vh.titleView.setText(displayTitle);
if (type == TYPE_ITEM && vh.iconView != null) {
vh.iconView.setText(item.getIcon());
}
vh.itemView.setSelected(item.equals(currentItem));
vh.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (listener != null) {
listener.onNavigationMenuItemClicked(item);
}
}
});
}
public interface NavigationMenuItemClickListener {
void onNavigationMenuItemClicked(NavMenuItem menuItem);
}
}

View file

@ -0,0 +1,265 @@
package io.lbry.browser.adapter;
import android.content.Context;
import android.graphics.Color;
import android.text.format.DateUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.TimeZone;
import io.lbry.browser.R;
import io.lbry.browser.listener.SelectionModeListener;
import io.lbry.browser.model.Claim;
import io.lbry.browser.model.lbryinc.LbryNotification;
import io.lbry.browser.ui.controls.SolidIconView;
import io.lbry.browser.utils.Helper;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
@Data
@EqualsAndHashCode(callSuper = false)
public class NotificationListAdapter extends RecyclerView.Adapter<NotificationListAdapter.ViewHolder> {
private static final String RULE_CREATOR_SUBSCRIBER = "creator_subscriber";
private static final String RULE_COMMENT = "comment";
private final Context context;
private final List<LbryNotification> items;
private final List<LbryNotification> selectedItems;
@Setter
private NotificationClickListener clickListener;
@Getter
@Setter
private boolean inSelectionMode;
@Setter
private SelectionModeListener selectionModeListener;
public NotificationListAdapter(List<LbryNotification> notifications, Context context) {
this.context = context;
this.items = new ArrayList<>(notifications);
this.selectedItems = new ArrayList<>();
Collections.sort(items, Collections.reverseOrder(new LbryNotification()));
}
public static class ViewHolder extends RecyclerView.ViewHolder {
protected final View layoutView;
protected final TextView titleView;
protected final TextView bodyView;
protected final TextView timeView;
protected final SolidIconView iconView;
protected final ImageView thumbnailView;
protected final View selectedOverlayView;
public ViewHolder(View v) {
super(v);
layoutView = v.findViewById(R.id.notification_layout);
titleView = v.findViewById(R.id.notification_title);
bodyView = v.findViewById(R.id.notification_body);
timeView = v.findViewById(R.id.notification_time);
iconView = v.findViewById(R.id.notification_icon);
thumbnailView = v.findViewById(R.id.notification_author_thumbnail);
selectedOverlayView = v.findViewById(R.id.notification_selected_overlay);
}
}
public int getItemCount() {
return items != null ? items.size() : 0;
}
public List<LbryNotification> getSelectedItems() {
return this.selectedItems;
}
public int getSelectedCount() {
return selectedItems != null ? selectedItems.size() : 0;
}
public void clearSelectedItems() {
this.selectedItems.clear();
}
public boolean isNotificationSelected(LbryNotification notification) {
return selectedItems.contains(notification);
}
public void insertNotification(LbryNotification notification, int index) {
if (!items.contains(notification)) {
items.add(index, notification);
}
notifyDataSetChanged();
}
public void addNotification(LbryNotification notification) {
if (!items.contains(notification)) {
items.add(notification);
}
notifyDataSetChanged();
}
public void removeNotifications(List<LbryNotification> notifications) {
for (LbryNotification notification : notifications) {
items.remove(notification);
}
notifyDataSetChanged();
}
public List<String> getAuthorUrls() {
List<String> urls = new ArrayList<>();
for (LbryNotification item : items) {
if (!Helper.isNullOrEmpty(item.getAuthorUrl())) {
urls.add(item.getAuthorUrl());
}
}
return urls;
}
public void updateAuthorClaims(List<Claim> claims) {
for (Claim claim : claims) {
if (claim != null && claim.getThumbnailUrl() != null) {
updateClaimForAuthorUrl(claim);
}
}
notifyDataSetChanged();
}
private void updateClaimForAuthorUrl(Claim claim) {
for (LbryNotification item : items) {
if (claim.getPermanentUrl().equalsIgnoreCase(item.getAuthorUrl())) {
item.setCommentAuthor(claim);
}
}
}
public void addNotifications(List<LbryNotification> notifications) {
for (LbryNotification notification : notifications) {
if (!items.contains(notification)) {
items.add(notification);
}
}
Collections.sort(items, Collections.reverseOrder(new LbryNotification()));
notifyDataSetChanged();
}
@Override
public NotificationListAdapter.ViewHolder onCreateViewHolder(ViewGroup root, int viewType) {
View v = LayoutInflater.from(context).inflate(R.layout.list_item_notification, root, false);
return new NotificationListAdapter.ViewHolder(v);
}
private int getStringIdForRule(String rule) {
if (RULE_CREATOR_SUBSCRIBER.equalsIgnoreCase(rule)) {
return R.string.fa_heart;
}
if (RULE_COMMENT.equalsIgnoreCase(rule)) {
return R.string.fa_comment_alt;
}
return R.string.fa_asterisk;
}
private int getColorForRule(String rule) {
if (RULE_CREATOR_SUBSCRIBER.equalsIgnoreCase(rule)) {
return Color.RED;
}
if (RULE_COMMENT.equalsIgnoreCase(rule)) {
return ContextCompat.getColor(context, R.color.nextLbryGreen);
}
return ContextCompat.getColor(context, R.color.lbryGreen);
}
@Override
public void onBindViewHolder(NotificationListAdapter.ViewHolder vh, int position) {
LbryNotification notification = items.get(position);
vh.layoutView.setBackgroundColor(ContextCompat.getColor(context, notification.isSeen() ? android.R.color.transparent : R.color.nextLbryGreenSemiTransparent));
vh.selectedOverlayView.setVisibility(isNotificationSelected(notification) ? View.VISIBLE : View.GONE);
vh.titleView.setVisibility(!Helper.isNullOrEmpty(notification.getTitle()) ? View.VISIBLE : View.GONE);
vh.titleView.setText(notification.getTitle());
vh.bodyView.setText(notification.getDescription());
vh.timeView.setText(DateUtils.getRelativeTimeSpanString(
getLocalNotificationTime(notification), System.currentTimeMillis(), 0, DateUtils.FORMAT_ABBREV_RELATIVE));
vh.thumbnailView.setVisibility(notification.getCommentAuthor() == null ? View.INVISIBLE : View.VISIBLE);
if (notification.getCommentAuthor() != null) {
Glide.with(context.getApplicationContext()).load(
notification.getCommentAuthor().getThumbnailUrl(vh.thumbnailView.getLayoutParams().width, vh.thumbnailView.getLayoutParams().height, 85)).apply(RequestOptions.circleCropTransform()).into(vh.thumbnailView);
}
vh.iconView.setVisibility(notification.getCommentAuthor() != null ? View.INVISIBLE : View.VISIBLE);
vh.iconView.setText(getStringIdForRule(notification.getRule()));
vh.iconView.setTextColor(getColorForRule(notification.getRule()));
vh.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (inSelectionMode) {
toggleSelectedNotification(notification);
} else {
if (clickListener != null) {
clickListener.onNotificationClicked(notification);
}
}
}
});
vh.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
if (!inSelectionMode) {
inSelectionMode = true;
if (selectionModeListener != null) {
selectionModeListener.onEnterSelectionMode();
}
}
toggleSelectedNotification(notification);
return true;
}
});
}
private void toggleSelectedNotification(LbryNotification notification) {
if (selectedItems.contains(notification)) {
selectedItems.remove(notification);
} else {
selectedItems.add(notification);
}
if (selectionModeListener != null) {
selectionModeListener.onItemSelectionToggled();
}
if (selectedItems.size() == 0) {
inSelectionMode = false;
if (selectionModeListener != null) {
selectionModeListener.onExitSelectionMode();
}
}
notifyDataSetChanged();
}
private long getLocalNotificationTime(LbryNotification notification) {
TimeZone utcTZ = TimeZone.getTimeZone("UTC");
TimeZone targetTZ = TimeZone.getDefault();
Calendar cal = new GregorianCalendar(utcTZ);
cal.setTimeInMillis(notification.getTimestamp().getTime());
cal.add(Calendar.MILLISECOND, utcTZ.getRawOffset() * -1);
cal.add(Calendar.MILLISECOND, targetTZ.getRawOffset());
return cal.getTimeInMillis();
}
public interface NotificationClickListener {
void onNotificationClicked(LbryNotification notification);
}
}

View file

@ -0,0 +1,220 @@
package io.lbry.browser.adapter;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.net.Uri;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.snackbar.Snackbar;
import java.util.ArrayList;
import java.util.List;
import io.lbry.browser.R;
import io.lbry.browser.model.lbryinc.Reward;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbryio;
import lombok.Getter;
import lombok.Setter;
public class RewardListAdapter extends RecyclerView.Adapter<RewardListAdapter.ViewHolder> {
public static final int DISPLAY_MODE_ALL = 1;
public static final int DISPLAY_MODE_UNCLAIMED = 2;
private final Context context;
@Setter
private List<Reward> all;
private List<Reward> items;
@Setter
private RewardClickListener clickListener;
@Getter
private int displayMode;
public RewardListAdapter(List<Reward> all, Context context) {
this.all = new ArrayList<>(all);
this.items = new ArrayList<>(all);
this.context = context;
this.displayMode = DISPLAY_MODE_ALL;
addCustomReward();
}
public void setRewards(List<Reward> rewards) {
this.all = new ArrayList<>(rewards);
updateItemsForDisplayMode();
notifyDataSetChanged();
}
public void setDisplayMode(int displayMode) {
this.displayMode = displayMode;
updateItemsForDisplayMode();
notifyDataSetChanged();
}
private void updateItemsForDisplayMode() {
if (displayMode == DISPLAY_MODE_ALL) {
items = new ArrayList<>(all);
} else if (displayMode == DISPLAY_MODE_UNCLAIMED) {
items = new ArrayList<>();
for (Reward reward : all) {
if (!reward.isClaimed()) {
items.add(reward);
}
}
}
addCustomReward();
}
private void addCustomReward() {
Reward custom = new Reward();
custom.setCustom(true);
custom.setRewardTitle(context.getString(R.string.custom_reward_title));
custom.setRewardDescription(context.getString(R.string.custom_reward_description));
items.add(custom);
}
public static class ViewHolder extends RecyclerView.ViewHolder {
protected final View iconClaimed;
protected final View loading;
protected final View upTo;
protected final TextView textTitle;
protected final TextView textDescription;
protected final TextView textLbcValue;
protected final TextView textUsdValue;
protected final TextView textLinkTransaction;
protected final EditText inputCustomCode;
protected final MaterialButton buttonClaimCustom;
public ViewHolder(View v) {
super(v);
iconClaimed = v.findViewById(R.id.reward_item_claimed_icon);
upTo = v.findViewById(R.id.reward_item_up_to);
loading = v.findViewById(R.id.reward_item_loading);
textTitle = v.findViewById(R.id.reward_item_title);
textDescription = v.findViewById(R.id.reward_item_description);
textLbcValue = v.findViewById(R.id.reward_item_lbc_value);
textLinkTransaction = v.findViewById(R.id.reward_item_tx_link);
textUsdValue = v.findViewById(R.id.reward_item_usd_value);
inputCustomCode = v.findViewById(R.id.reward_item_custom_code_input);
buttonClaimCustom = v.findViewById(R.id.reward_item_custom_claim_button);
}
}
public int getItemCount() {
return items != null ? items.size() : 0;
}
public void addReward(Reward reward) {
if (!items.contains(reward)) {
items.add(reward);
}
notifyDataSetChanged();
}
public List<Reward> getRewards() {
return new ArrayList<>(items);
}
@Override
public RewardListAdapter.ViewHolder onCreateViewHolder(ViewGroup root, int viewType) {
View v = LayoutInflater.from(context).inflate(R.layout.list_item_reward, root, false);
return new RewardListAdapter.ViewHolder(v);
}
@Override
public void onBindViewHolder(RewardListAdapter.ViewHolder vh, int position) {
Reward reward = items.get(position);
String displayAmount = reward.getDisplayAmount();
double rewardAmount = 0;
if (!"?".equals(displayAmount)) {
rewardAmount = Double.valueOf(displayAmount);
}
boolean hasTransaction = !Helper.isNullOrEmpty(reward.getTransactionId()) && reward.getTransactionId().length() > 7;
vh.iconClaimed.setVisibility(reward.isClaimed() ? View.VISIBLE : View.INVISIBLE);
vh.inputCustomCode.setVisibility(reward.isCustom() ? View.VISIBLE : View.GONE);
vh.buttonClaimCustom.setVisibility(reward.isCustom() ? View.VISIBLE : View.GONE);
vh.textTitle.setText(reward.getRewardTitle());
vh.textDescription.setText(reward.getRewardDescription());
vh.upTo.setVisibility(reward.shouldDisplayRange() ? View.VISIBLE : View.GONE);
vh.textLbcValue.setText(reward.isCustom() ? "?" : Helper.LBC_CURRENCY_FORMAT.format(Helper.parseDouble(reward.getDisplayAmount(), 0)));
vh.textLinkTransaction.setVisibility(hasTransaction ? View.VISIBLE : View.GONE);
vh.textLinkTransaction.setText(hasTransaction ? reward.getTransactionId().substring(0, 7) : null);
vh.textLinkTransaction.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (context != null) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(String.format("%s/%s", Helper.EXPLORER_TX_PREFIX, reward.getTransactionId())));
context.startActivity(intent);
}
}
});
vh.textUsdValue.setText(reward.isCustom() || Lbryio.LBCUSDRate == 0 ? null :
String.format("≈$%s", Helper.SIMPLE_CURRENCY_FORMAT.format(rewardAmount * Lbryio.LBCUSDRate)));
vh.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (reward.isClaimed()) {
return;
}
if (vh.inputCustomCode != null && !vh.inputCustomCode.hasFocus()) {
vh.inputCustomCode.requestFocus();
}
if (clickListener != null) {
clickListener.onRewardClicked(reward, vh.loading);
}
}
});
vh.inputCustomCode.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
String value = charSequence.toString().trim();
vh.buttonClaimCustom.setEnabled(value.length() > 0);
}
@Override
public void afterTextChanged(Editable editable) {
}
});
vh.buttonClaimCustom.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String claimCode = Helper.getValue(vh.inputCustomCode.getText());
if (Helper.isNullOrEmpty(claimCode)) {
Snackbar.make(view, R.string.please_enter_claim_code, Snackbar.LENGTH_LONG).
setBackgroundTint(Color.RED).setTextColor(Color.WHITE).show();
return;
}
if (clickListener != null) {
clickListener.onCustomClaimButtonClicked(claimCode, vh.inputCustomCode, vh.buttonClaimCustom, vh.loading);
}
}
});
}
public interface RewardClickListener {
void onRewardClicked(Reward reward, View loadingView);
void onCustomClaimButtonClicked(String code, EditText inputCustomCode, MaterialButton buttonClaim, View loadingView);
}
}

View file

@ -0,0 +1,70 @@
package io.lbry.browser.adapter;
import android.content.Context;
import android.graphics.Color;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.List;
import io.lbry.browser.R;
import io.lbry.browser.model.StartupStage;
public class StartupStageAdapter extends BaseAdapter {
private final List<StartupStage> list;
private final LayoutInflater inflater;
private final String[] stagesString;
public StartupStageAdapter(Context ctx, List<StartupStage> rows) {
this.list = rows;
this.inflater = LayoutInflater.from(ctx);
stagesString = new String[9];
stagesString[0] = ctx.getResources().getString(R.string.installation_id_loaded);
stagesString[1] = ctx.getResources().getString(R.string.known_tags_loaded);
stagesString[2] = ctx.getResources().getString(R.string.exchange_rate_loaded);
stagesString[3] = ctx.getResources().getString(R.string.user_authenticated);
stagesString[4] = ctx.getResources().getString(R.string.installation_registered);
stagesString[5] = ctx.getResources().getString(R.string.subscriptions_loaded);
stagesString[6] = ctx.getResources().getString(R.string.subscriptions_resolved);
stagesString[7] = ctx.getResources().getString(R.string.block_list_loaded);
stagesString[8] = ctx.getResources().getString(R.string.filter_list_loaded);
}
@Override
public int getCount() {
return list.size();
}
@Override
public Object getItem(int i) {
return list.get(i);
}
@Override
public long getItemId(int i) {
return i;
}
@Override
public View getView(int i, View view, ViewGroup viewGroup) {
if (view == null) {
view = inflater.inflate(R.layout.list_item_startupstage, viewGroup, false);
ImageView iconView = view.findViewById(R.id.startup_stage_icon);
TextView textView = view.findViewById(R.id.startup_stage_text);
StartupStage item = (StartupStage) getItem(i);
iconView.setImageResource(item.stageDone ? R.drawable.ic_check : R.drawable.ic_close);
iconView.setColorFilter(item.stageDone ? Color.WHITE : Color.RED);
textView.setText(stagesString[item.stage - 1]);
}
return view;
}
}

View file

@ -0,0 +1,128 @@
package io.lbry.browser.adapter;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
import java.util.ArrayList;
import java.util.List;
import io.lbry.browser.R;
import io.lbry.browser.model.Claim;
import io.lbry.browser.listener.ChannelItemSelectionListener;
import io.lbry.browser.utils.Helper;
import lombok.Setter;
public class SuggestedChannelGridAdapter extends RecyclerView.Adapter<SuggestedChannelGridAdapter.ViewHolder> {
private final Context context;
private final List<Claim> items;
private final List<Claim> selectedItems;
@Setter
private ChannelItemSelectionListener listener;
public SuggestedChannelGridAdapter(List<Claim> items, Context context) {
this.items = new ArrayList<>(items);
this.selectedItems = new ArrayList<>();
this.context = context;
}
public static class ViewHolder extends RecyclerView.ViewHolder {
protected final View noThumbnailView;
protected final ImageView thumbnailView;
protected final TextView alphaView;
protected final TextView titleView;
protected final TextView tagView;
public ViewHolder(View v) {
super(v);
noThumbnailView = v.findViewById(R.id.suggested_channel_no_thumbnail);
alphaView = v.findViewById(R.id.suggested_channel_alpha_view);
thumbnailView = v.findViewById(R.id.suggested_channel_thumbnail);
titleView = v.findViewById(R.id.suggested_channel_title);
tagView = v.findViewById(R.id.suggested_channel_tag);
}
}
public int getItemCount() {
return items != null ? items.size() : 0;
}
public int getSelectedCount() { return selectedItems.size(); }
public void clearItems() {
items.clear();
notifyDataSetChanged();
}
public List<Claim> getSelectedItems() {
return this.selectedItems;
}
public void clearSelectedItems() {
this.selectedItems.clear();
}
public boolean isClaimSelected(Claim claim) {
return selectedItems.contains(claim);
}
public void addClaims(List<Claim> claims) {
for (Claim claim : claims) {
if (!items.contains(claim)) {
items.add(claim);
}
}
notifyDataSetChanged();
}
@Override
public SuggestedChannelGridAdapter.ViewHolder onCreateViewHolder(ViewGroup root, int viewType) {
View v = LayoutInflater.from(context).inflate(R.layout.list_item_suggested_channel, root, false);
return new SuggestedChannelGridAdapter.ViewHolder(v);
}
@Override
public void onBindViewHolder(SuggestedChannelGridAdapter.ViewHolder vh, int position) {
Claim claim = items.get(position);
ViewGroup.LayoutParams lp = vh.thumbnailView.getLayoutParams();
String thumbnailUrl = claim.getThumbnailUrl(lp.width, lp.height, 85);
int bgColor = Helper.generateRandomColorForValue(claim.getClaimId());
Helper.setIconViewBackgroundColor(vh.noThumbnailView, bgColor, false, context);
vh.noThumbnailView.setVisibility(Helper.isNullOrEmpty(thumbnailUrl) ? View.INVISIBLE : View.VISIBLE);
vh.alphaView.setText(claim.getName().substring(1, 2));
if (!Helper.isNullOrEmpty(thumbnailUrl)) {
vh.thumbnailView.setVisibility(View.VISIBLE);
Glide.with(context.getApplicationContext()).load(thumbnailUrl).apply(RequestOptions.circleCropTransform()).into(vh.thumbnailView);
} else {
vh.thumbnailView.setVisibility(View.GONE);
}
vh.titleView.setText(Helper.isNullOrEmpty(claim.getTitle()) ? claim.getName() : claim.getTitle());
String firstTag = claim.getFirstTag();
vh.tagView.setVisibility(Helper.isNullOrEmpty(firstTag) ? View.INVISIBLE : View.VISIBLE);
vh.tagView.setBackgroundResource(R.drawable.bg_tag);
vh.tagView.setText(firstTag);
vh.itemView.setSelected(isClaimSelected(claim));
vh.itemView.setOnClickListener(view -> {
if (selectedItems.contains(claim)) {
selectedItems.remove(claim);
if (listener != null) {
listener.onChannelItemDeselected(claim);
}
} else {
selectedItems.add(claim);
if (listener != null) {
listener.onChannelItemSelected(claim);
}
}
notifyDataSetChanged();
});
}
}

View file

@ -0,0 +1,109 @@
package io.lbry.browser.adapter;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
import io.lbry.browser.R;
import io.lbry.browser.model.Tag;
import lombok.Getter;
import lombok.Setter;
public class TagListAdapter extends RecyclerView.Adapter<TagListAdapter.ViewHolder> {
public static final int CUSTOMIZE_MODE_NONE = 0;
public static final int CUSTOMIZE_MODE_ADD = 1;
public static final int CUSTOMIZE_MODE_REMOVE = 2;
private final Context context;
private List<Tag> items;
@Setter
private TagClickListener clickListener;
@Setter
@Getter
private int customizeMode;
public TagListAdapter(List<Tag> tags, Context context) {
this.context = context;
this.items = new ArrayList<>(tags);
this.customizeMode = CUSTOMIZE_MODE_NONE;
}
public static class ViewHolder extends RecyclerView.ViewHolder {
protected final ImageView iconView;
protected final TextView nameView;
public ViewHolder(View v) {
super(v);
iconView = v.findViewById(R.id.tag_action);
nameView = v.findViewById(R.id.tag_name);
}
}
public int getItemCount() {
return items != null ? items.size() : 0;
}
public void addTag(Tag tag) {
if (!items.contains(tag)) {
items.add(tag);
}
notifyDataSetChanged();
}
public List<Tag> getTags() {
return new ArrayList<>(items);
}
public void setTags(List<Tag> tags) {
items = new ArrayList<>(tags);
notifyDataSetChanged();
}
public void addTags(List<Tag> tags) {
for (Tag tag : tags) {
if (!items.contains(tag)) {
items.add(tag);
}
}
notifyDataSetChanged();
}
public void removeTag(Tag tag) {
items.remove(tag);
notifyDataSetChanged();
}
@Override
public TagListAdapter.ViewHolder onCreateViewHolder(ViewGroup root, int viewType) {
View v = LayoutInflater.from(context).inflate(R.layout.list_item_tag, root, false);
return new TagListAdapter.ViewHolder(v);
}
@Override
public void onBindViewHolder(TagListAdapter.ViewHolder vh, int position) {
Tag tag = items.get(position);
vh.nameView.setText(tag.getName().toLowerCase());
vh.iconView.setVisibility(customizeMode == CUSTOMIZE_MODE_NONE ? View.GONE : View.VISIBLE);
vh.iconView.setImageResource(customizeMode == CUSTOMIZE_MODE_REMOVE ? R.drawable.ic_close : R.drawable.ic_add);
vh.itemView.setBackgroundResource(tag.isMature() ? R.drawable.bg_tag_mature : R.drawable.bg_tag);
vh.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (clickListener != null) {
clickListener.onTagClicked(tag, customizeMode);
}
}
});
}
public interface TagClickListener {
void onTagClicked(Tag tag, int customizeMode);
}
}

View file

@ -0,0 +1,136 @@
package io.lbry.browser.adapter;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import io.lbry.browser.R;
import io.lbry.browser.model.Transaction;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.LbryUri;
import lombok.Setter;
public class TransactionListAdapter extends RecyclerView.Adapter<TransactionListAdapter.ViewHolder> {
private static final DecimalFormat TX_LIST_AMOUNT_FORMAT = new DecimalFormat("#,##0.0000");
private static final SimpleDateFormat TX_LIST_DATE_FORMAT = new SimpleDateFormat("MMM d");
private final Context context;
private final List<Transaction> items;
@Setter
private TransactionClickListener listener;
public TransactionListAdapter(List<Transaction> transactions, Context context) {
this.context = context;
this.items = new ArrayList<>(transactions);
}
public void clear() {
items.clear();
notifyDataSetChanged();
}
public List<Transaction> getItems() {
return new ArrayList<>(items);
}
public void addTransactions(List<Transaction> transactions) {
for (Transaction tx : transactions) {
if (!items.contains(tx)) {
items.add(tx);
}
}
notifyDataSetChanged();
}
public int getItemCount() {
return items != null ? items.size() : 0;
}
@Override
public TransactionListAdapter.ViewHolder onCreateViewHolder(ViewGroup root, int viewType) {
View v = LayoutInflater.from(context).inflate(R.layout.list_item_transaction, root, false);
return new TransactionListAdapter.ViewHolder(v);
}
@Override
public void onBindViewHolder(TransactionListAdapter.ViewHolder vh, int position) {
Transaction item = items.get(position);
vh.descView.setText(item.getDescriptionStringId());
vh.amountView.setText(TX_LIST_AMOUNT_FORMAT.format(item.getValue().doubleValue()));
vh.claimView.setText(item.getClaim());
vh.feeView.setText(context.getString(R.string.tx_list_fee, TX_LIST_AMOUNT_FORMAT.format(item.getFee().doubleValue())));
vh.txidLinkView.setText(item.getTxid().substring(0, 7));
vh.dateView.setVisibility(item.getConfirmations() > 0 ? View.VISIBLE : View.GONE);
vh.dateView.setText(item.getConfirmations() > 0 ? TX_LIST_DATE_FORMAT.format(item.getTxDate()) : null);
vh.pendingView.setVisibility(item.getConfirmations() == 0 ? View.VISIBLE : View.GONE);
vh.infoFeeContainer.setVisibility(!Helper.isNullOrEmpty(item.getClaim()) || Math.abs(item.getFee().doubleValue()) > 0 ?
View.VISIBLE : View.GONE);
vh.claimView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
LbryUri claimUrl = item.getClaimUrl();
if (claimUrl != null && listener != null) {
listener.onClaimUrlClicked(claimUrl);
}
}
});
vh.txidLinkView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (context != null) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(String.format("%s/%s", Helper.EXPLORER_TX_PREFIX, item.getTxid())));
context.startActivity(intent);
}
}
});
vh.itemView.setOnClickListener(view -> {
if (listener != null) {
listener.onTransactionClicked(item);
}
});
}
public static class ViewHolder extends RecyclerView.ViewHolder {
protected final TextView descView;
protected final TextView amountView;
protected final TextView claimView;
protected final TextView feeView;
protected final TextView txidLinkView;
protected final TextView dateView;
protected final TextView pendingView;
protected final View infoFeeContainer;
public ViewHolder(View v) {
super(v);
descView = v.findViewById(R.id.transaction_desc);
amountView = v.findViewById(R.id.transaction_amount);
claimView = v.findViewById(R.id.transaction_claim);
feeView = v.findViewById(R.id.transaction_fee);
txidLinkView = v.findViewById(R.id.transaction_id_link);
dateView = v.findViewById(R.id.transaction_date);
pendingView = v.findViewById(R.id.transaction_pending_text);
infoFeeContainer = v.findViewById(R.id.transaction_info_fee_container);
}
}
public interface TransactionClickListener {
void onTransactionClicked(Transaction transaction);
void onClaimUrlClicked(LbryUri uri);
}
}

View file

@ -0,0 +1,147 @@
package io.lbry.browser.adapter;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
import io.lbry.browser.R;
import io.lbry.browser.exceptions.LbryUriException;
import io.lbry.browser.model.Claim;
import io.lbry.browser.model.UrlSuggestion;
import io.lbry.browser.ui.controls.SolidIconView;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.LbryUri;
import lombok.Setter;
public class UrlSuggestionListAdapter extends RecyclerView.Adapter<UrlSuggestionListAdapter.ViewHolder> {
private final Context context;
private final List<UrlSuggestion> items;
@Setter
private UrlSuggestionClickListener listener;
public UrlSuggestionListAdapter(Context context) {
this.context = context;
this.items = new ArrayList<>();
}
public void clear() {
items.clear();
notifyDataSetChanged();
}
public List<UrlSuggestion> getItems() {
return new ArrayList<>(items);
}
public List<String> getItemUrls() {
List<String> uris = new ArrayList<>();
for (int i = 0; i < items.size(); i++) {
LbryUri uri = items.get(i).getUri();
if (uri != null) {
uris.add(uri.toString());
}
}
return uris;
}
public void setClaimForUrl(LbryUri url, Claim claim) {
for (int i = 0; i < items.size(); i++) {
LbryUri thisUrl = items.get(i).getUri();
try {
if (thisUrl != null) {
LbryUri vanity = LbryUri.parse(thisUrl.toVanityString());
if (thisUrl.equals(url) || vanity.equals(url)) {
items.get(i).setClaim(claim);
}
}
} catch (LbryUriException ex) {
// pass
}
}
}
public void addUrlSuggestions(List<UrlSuggestion> urlSuggestions) {
for (UrlSuggestion urlSuggestion : urlSuggestions) {
if (!items.contains(urlSuggestion)) {
items.add(urlSuggestion);
}
}
notifyDataSetChanged();
}
public int getItemCount() {
return items != null ? items.size() : 0;
}
@Override
public UrlSuggestionListAdapter.ViewHolder onCreateViewHolder(ViewGroup root, int viewType) {
View v = LayoutInflater.from(context).inflate(R.layout.list_item_url_suggestion, root, false);
return new UrlSuggestionListAdapter.ViewHolder(v);
}
@Override
public void onBindViewHolder(UrlSuggestionListAdapter.ViewHolder vh, int position) {
UrlSuggestion item = items.get(position);
String fullTitle, desc;
int iconStringId;
switch (item.getType()) {
case UrlSuggestion.TYPE_CHANNEL:
iconStringId = R.string.fa_at;
fullTitle = item.getTitle();
desc = item.getClaim() != null ? item.getClaim().getTitle() :
((item.isUseTextAsDescription() && !Helper.isNullOrEmpty(item.getText())) ? item.getText() : String.format(context.getString(R.string.view_channel_url_desc), item.getText()));
break;
case UrlSuggestion.TYPE_TAG:
iconStringId = R.string.fa_hashtag;
fullTitle = String.format(context.getString(R.string.tag_url_title), item.getText());
desc = String.format(context.getString(R.string.explore_tag_url_desc), item.getText());
break;
case UrlSuggestion.TYPE_SEARCH:
iconStringId = R.string.fa_search;
fullTitle = String.format(context.getString(R.string.search_url_title), item.getText());
desc = String.format(context.getString(R.string.search_url_desc), item.getText());
break;
case UrlSuggestion.TYPE_FILE:
default:
iconStringId = R.string.fa_file;
fullTitle = item.getTitle();
desc = item.getClaim() != null ? item.getClaim().getTitle() :
((item.isUseTextAsDescription() && !Helper.isNullOrEmpty(item.getText())) ? item.getText() : String.format(context.getString(R.string.view_file_url_desc), item.getText()));
break;
}
vh.iconView.setText(iconStringId);
vh.titleView.setText(fullTitle);
vh.descView.setText(desc);
vh.itemView.setOnClickListener(view -> {
if (listener != null) {
listener.onUrlSuggestionClicked(item);
}
});
}
public static class ViewHolder extends RecyclerView.ViewHolder {
protected final SolidIconView iconView;
protected final TextView titleView;
protected final TextView descView;
public ViewHolder(View v) {
super(v);
iconView = v.findViewById(R.id.url_suggestion_icon);
titleView = v.findViewById(R.id.url_suggestion_title);
descView = v.findViewById(R.id.url_suggestion_description);
}
}
public interface UrlSuggestionClickListener {
void onUrlSuggestionClicked(UrlSuggestion urlSuggestion);
}
}

View file

@ -0,0 +1,71 @@
package io.lbry.browser.adapter;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import io.lbry.browser.listener.SignInListener;
import io.lbry.browser.listener.WalletSyncListener;
import io.lbry.browser.ui.verification.EmailVerificationFragment;
import io.lbry.browser.ui.verification.ManualVerificationFragment;
import io.lbry.browser.ui.verification.PhoneVerificationFragment;
import io.lbry.browser.ui.verification.WalletVerificationFragment;
import lombok.SneakyThrows;
/**
* 4 fragments
* - Email collect / verify (sign in)
* - Phone number collect / verify (rewards)
* - Wallet password
* - Manual verification page
*/
public class VerificationPagerAdapter extends FragmentStateAdapter {
public static final int PAGE_VERIFICATION_EMAIL = 0;
public static final int PAGE_VERIFICATION_PHONE = 1;
public static final int PAGE_VERIFICATION_WALLET = 2;
public static final int PAGE_VERIFICATION_MANUAL = 3;
private final FragmentActivity activity;
public VerificationPagerAdapter(FragmentActivity activity) {
super(activity);
this.activity = activity;
}
@SneakyThrows
@Override
public Fragment createFragment(int position) {
switch (position) {
case 0:
default:
EmailVerificationFragment evFragment = EmailVerificationFragment.class.newInstance();
if (activity instanceof SignInListener) {
evFragment.setListener((SignInListener) activity);
}
return evFragment;
case 1:
PhoneVerificationFragment pvFragment = PhoneVerificationFragment.class.newInstance();
if (activity instanceof SignInListener) {
pvFragment.setListener((SignInListener) activity);
}
return pvFragment;
case 2:
WalletVerificationFragment wvFragment = WalletVerificationFragment.class.newInstance();
if (activity instanceof WalletSyncListener) {
wvFragment.setListener((WalletSyncListener) activity);
}
return wvFragment;
case 3:
ManualVerificationFragment mvFragment = ManualVerificationFragment.class.newInstance();
if (activity instanceof SignInListener) {
mvFragment.setListener((SignInListener) activity);
}
return mvFragment;
}
}
@Override
public int getItemCount() {
return 4;
}
}

View file

@ -0,0 +1,100 @@
package io.lbry.browser.adapter;
import android.content.Context;
import android.content.DialogInterface;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageButton;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import java.util.List;
import io.lbry.browser.MainActivity;
import io.lbry.browser.R;
import io.lbry.browser.model.WalletDetailItem;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.views.CreditsBalanceView;
public class WalletDetailAdapter extends BaseAdapter {
private final List<WalletDetailItem> list;
private final LayoutInflater inflater;
public WalletDetailAdapter(Context ctx, List<WalletDetailItem> rows) {
this.list = rows;
this.inflater = LayoutInflater.from(ctx);
}
@Override
public int getCount() {
return list.size();
}
@Override
public Object getItem(int i) {
return list.get(i);
}
@Override
public long getItemId(int i) {
return i;
}
@Override
public View getView(int i, View view, ViewGroup viewGroup) {
if (view == null) {
view = inflater.inflate(R.layout.list_item_boosting_balance, viewGroup, false);
CreditsBalanceView balanceView = view.findViewById(R.id.wallet_supporting_balance);
TextView detailTextView = view.findViewById(R.id.detail);
TextView detailExplanationTextView = view.findViewById(R.id.detail_explanation);
WalletDetailItem item = (WalletDetailItem) getItem(i);
detailTextView.setText(item.detail);
detailExplanationTextView.setText(item.detailDesc);
Helper.setViewText(balanceView, item.detailAmount);
ProgressBar progressUnlockTips = view.findViewById(R.id.wallet_unlock_tips_progress);
progressUnlockTips.setVisibility(item.isInProgress ? View.VISIBLE : View.GONE);
ImageButton buttonLock = view.findViewById(R.id.lock_button);
buttonLock.setVisibility((item.isUnlockable && !item.isInProgress) ? View.VISIBLE : View.GONE);
if (item.isUnlockable) {
buttonLock.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (view.getContext() != null) {
AlertDialog.Builder builder = new AlertDialog.Builder(view.getContext()).
setTitle(R.string.unlock_tips).
setMessage(R.string.confirm_unlock_tips)
.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
unlockTips(view);
}
}).setNegativeButton(R.string.no, null);
builder.show();
}
}
});
}
}
return view;
}
private void unlockTips(View v) {
Context ctx = v.getContext();
if (ctx instanceof MainActivity) {
v.setVisibility(View.GONE);
View progress = v.getRootView().findViewById(R.id.wallet_unlock_tips_progress);
progress.setVisibility(View.VISIBLE);
((MainActivity) ctx).unlockTips();
}
}
}

View file

@ -0,0 +1,459 @@
package io.lbry.browser.data;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import java.math.BigDecimal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import io.lbry.browser.model.Tag;
import io.lbry.browser.model.UrlSuggestion;
import io.lbry.browser.model.ViewHistory;
import io.lbry.browser.model.lbryinc.LbryNotification;
import io.lbry.browser.model.lbryinc.Subscription;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.LbryUri;
public class DatabaseHelper extends SQLiteOpenHelper {
public static final int DATABASE_VERSION = 8;
public static final String DATABASE_NAME = "LbryApp.db";
private static DatabaseHelper instance;
private static final String[] SQL_CREATE_TABLES = {
// local subscription store
"CREATE TABLE subscriptions (url TEXT PRIMARY KEY NOT NULL, channel_name TEXT NOT NULL, is_notifications_disabled INTEGER DEFAULT 0 NOT NULL)",
// url entry / suggestion history
"CREATE TABLE url_history (id INTEGER PRIMARY KEY NOT NULL, value TEXT NOT NULL, url TEXT, type INTEGER NOT NULL, timestamp TEXT NOT NULL)",
// tags (known and followed)
"CREATE TABLE tags (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, is_followed INTEGER NOT NULL)",
// view history (stores only stream claims that have resolved)
"CREATE TABLE view_history (" +
" id INTEGER PRIMARY KEY NOT NULL" +
", url TEXT NOT NULL" +
", claim_id TEXT" +
", claim_name TEXT" +
", cost REAL " +
", currency TEXT " +
", title TEXT " +
", publisher_claim_id TEXT" +
", publisher_name TEXT" +
", publisher_title TEXT" +
", thumbnail_url TEXT" +
", release_time INTEGER " +
", device TEXT" +
", timestamp TEXT NOT NULL)",
"CREATE TABLE notifications (" +
" id INTEGER PRIMARY KEY NOT NULL" +
", remote_id INTEGER NOT NULL" +
", author_url TEXT" +
", title TEXT" +
", description TEXT" +
", thumbnail_url TEXT" +
", target_url TEXT" +
", rule TEXT" +
", is_read INTEGER DEFAULT 0 NOT NULL" +
", is_seen INTEGER DEFAULT 0 NOT NULL " +
", timestamp TEXT NOT NULL)",
"CREATE TABLE shuffle_watched (id INTEGER PRIMARY KEY NOT NULL, claim_id TEXT NOT NULL)"
};
private static final String[] SQL_CREATE_INDEXES = {
"CREATE UNIQUE INDEX idx_subscription_url ON subscriptions (url)",
"CREATE UNIQUE INDEX idx_url_history_value ON url_history (value)",
"CREATE UNIQUE INDEX idx_url_history_url ON url_history (url)",
"CREATE UNIQUE INDEX idx_tag_name ON tags (name)",
"CREATE UNIQUE INDEX idx_view_history_url_device ON view_history (url, device)",
"CREATE INDEX idx_view_history_device ON view_history (device)",
"CREATE UNIQUE INDEX idx_notification_remote_id ON notifications (remote_id)",
"CREATE INDEX idx_notification_timestamp ON notifications (timestamp)",
"CREATE UNIQUE INDEX idx_shuffle_watched_claim ON shuffle_watched (claim_id)",
};
private static final String[] SQL_V1_V2_UPGRADE = {
"ALTER TABLE view_history ADD COLUMN currency TEXT"
};
private static final String[] SQL_V2_V3_UPGRADE = {
"CREATE TABLE notifications (" +
" id INTEGER PRIMARY KEY NOT NULL" +
", title TEXT" +
", description TEXT" +
", thumbnail_url TEXT" +
", target_url TEXT" +
", is_read INTEGER DEFAULT 0 NOT NULL" +
", timestamp TEXT NOT NULL)",
"CREATE INDEX idx_notification_timestamp ON notifications (timestamp)"
};
private static final String[] SQL_V3_V4_UPGRADE = {
"ALTER TABLE notifications ADD COLUMN remote_id INTEGER",
"CREATE UNIQUE INDEX idx_notification_remote_id ON notifications (remote_id)"
};
private static final String[] SQL_V4_V5_UPGRADE = {
"ALTER TABLE notifications ADD COLUMN rule TEXT",
"ALTER TABLE notifications ADD COLUMN is_seen TEXT"
};
private static final String[] SQL_V5_V6_UPGRADE = {
"ALTER TABLE notifications ADD COLUMN author_url TEXT"
};
private static final String[] SQL_V6_V7_UPGRADE = {
"CREATE TABLE shuffle_watched (id INTEGER PRIMARY KEY NOT NULL, claim_id TEXT NOT NULL)",
"CREATE UNIQUE INDEX idx_shuffle_watched_claim ON shuffle_watched (claim_id)"
};
private static final String[] SQL_V7_V8_UPGRADE = {
"AlTER TABLE subscriptions ADD COLUMN is_notifications_disabled INTEGER DEFAULT 0 NOT NULL"
};
private static final String SQL_INSERT_SUBSCRIPTION = "REPLACE INTO subscriptions (channel_name, url, is_notifications_disabled) VALUES (?, ?, ?)";
private static final String SQL_UPDATE_SUBSCRIPTION_NOTIFICATION = "UPDATE subscriptions SET is_notification_disabled = ? WHERE url = ?";
private static final String SQL_CLEAR_SUBSCRIPTIONS = "DELETE FROM subscriptions";
private static final String SQL_DELETE_SUBSCRIPTION = "DELETE FROM subscriptions WHERE url = ?";
private static final String SQL_GET_SUBSCRIPTIONS = "SELECT channel_name, url, is_notifications_disabled FROM subscriptions";
private static final String SQL_INSERT_URL_HISTORY = "REPLACE INTO url_history (value, url, type, timestamp) VALUES (?, ?, ?, ?)";
private static final String SQL_CLEAR_URL_HISTORY = "DELETE FROM url_history";
private static final String SQL_CLEAR_URL_HISTORY_BEFORE_TIME = "DELETE FROM url_history WHERE timestamp < ?";
private static final String SQL_GET_RECENT_URL_HISTORY = "SELECT value, url, type FROM url_history ORDER BY timestamp DESC LIMIT 10";
private static final String SQL_INSERT_NOTIFICATION = "REPLACE INTO notifications (remote_id, author_url, title, description, rule, target_url, is_read, is_seen, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
private static final String SQL_GET_NOTIFICATIONS = "SELECT id, remote_id, author_url, title, description, rule, target_url, is_read, is_seen, timestamp FROM notifications ORDER BY timestamp DESC LIMIT 500";
private static final String SQL_GET_UNREAD_NOTIFICATIONS_COUNT = "SELECT COUNT(id) FROM notifications WHERE is_read <> 1";
private static final String SQL_GET_UNSEEN_NOTIFICATIONS_COUNT = "SELECT COUNT(id) FROM notifications WHERE is_seen <> 1";
private static final String SQL_MARK_NOTIFICATIONS_READ = "UPDATE notifications SET is_read = 1 WHERE is_read = 0";
private static final String SQL_MARK_NOTIFICATIONS_SEEN = "UPDATE notifications SET is_seen = 1 WHERE is_seen = 0";
private static final String SQL_MARK_NOTIFICATION_READ_AND_SEEN = "UPDATE notifications SET is_read = 1, is_seen = 1 WHERE id = ?";
private static final String SQL_INSERT_SHUFFLE_WATCHED = "REPLACE INTO shuffle_watched (claim_id) VALUES (?)";
private static final String SQL_GET_SHUFFLE_WATCHED_CLAIMS = "SELECT claim_id FROM shuffle_watched";
private static final String SQL_INSERT_VIEW_HISTORY =
"REPLACE INTO view_history (url, claim_id, claim_name, cost, currency, title, publisher_claim_id, publisher_name, publisher_title, thumbnail_url, device, release_time, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
private static final String SQL_GET_VIEW_HISTORY =
"SELECT url, claim_id, claim_name, cost, currency, title, publisher_claim_id, publisher_name, publisher_title, thumbnail_url, device, release_time, timestamp " +
"FROM view_history WHERE '' = ? OR timestamp < ? ORDER BY timestamp DESC LIMIT %d";
private static final String SQL_CLEAR_VIEW_HISTORY = "DELETE FROM view_history";
private static final String SQL_CLEAR_VIEW_HISTORY_BY_DEVICE = "DELETE FROM view_history WHERE device = ?";
private static final String SQL_CLEAR_VIEW_HISTORY_BEFORE_TIME = "DELETE FROM view_history WHERE timestamp < ?";
private static final String SQL_CLEAR_VIEW_HISTORY_BY_DEVICE_BEFORE_TIME = "DELETE FROM view_history WHERE device = ? AND timestamp < ?";
private static final String SQL_INSERT_TAG = "REPLACE INTO tags (name, is_followed) VALUES (?, ?)";
private static final String SQL_GET_KNOWN_TAGS = "SELECT name, is_followed FROM tags";
private static final String SQL_UNFOLLOW_TAGS = "UPDATE tags SET is_followed = 0";
private static final String SQL_GET_FOLLOWED_TAGS = "SELECT name FROM tags WHERE is_followed = 1";
public DatabaseHelper(Context context) {
super(context, String.format("%s/%s", context.getFilesDir().getAbsolutePath(), DATABASE_NAME), null, DATABASE_VERSION);
instance = this;
}
public static DatabaseHelper getInstance() {
return instance;
}
public void onCreate(SQLiteDatabase db) {
for (String sql : SQL_CREATE_TABLES) {
db.execSQL(sql);
}
for (String sql : SQL_CREATE_INDEXES) {
db.execSQL(sql);
}
}
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion < 2) {
for (String sql : SQL_V1_V2_UPGRADE) {
db.execSQL(sql);
}
}
if (oldVersion < 3) {
for (String sql : SQL_V2_V3_UPGRADE) {
db.execSQL(sql);
}
}
if (oldVersion < 4) {
for (String sql : SQL_V3_V4_UPGRADE) {
db.execSQL(sql);
}
}
if (oldVersion < 5) {
for (String sql : SQL_V4_V5_UPGRADE) {
db.execSQL(sql);
}
}
if (oldVersion < 6) {
for (String sql : SQL_V5_V6_UPGRADE) {
db.execSQL(sql);
}
}
if (oldVersion < 7) {
for (String sql : SQL_V6_V7_UPGRADE) {
db.execSQL(sql);
}
}
if (oldVersion < 8) {
for (String sql : SQL_V7_V8_UPGRADE) {
db.execSQL(sql);
}
}
}
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
public static void createOrUpdateUrlHistoryItem(String text, String url, int type, SQLiteDatabase db) {
db.execSQL(SQL_INSERT_URL_HISTORY, new Object[] {
text, url, type, new SimpleDateFormat(Helper.ISO_DATE_FORMAT_PATTERN).format(new Date())
});
}
public static void clearUrlHistory(SQLiteDatabase db) {
db.execSQL(SQL_CLEAR_URL_HISTORY);
}
public static void clearUrlHistoryBefore(Date date, SQLiteDatabase db) {
db.execSQL(SQL_CLEAR_URL_HISTORY_BEFORE_TIME, new Object[] { new SimpleDateFormat(Helper.ISO_DATE_FORMAT_PATTERN).format(new Date()) });
}
// History items are essentially url suggestions
public static List<UrlSuggestion> getRecentHistory(SQLiteDatabase db) {
List<UrlSuggestion> suggestions = new ArrayList<>();
Cursor cursor = null;
try {
cursor = db.rawQuery(SQL_GET_RECENT_URL_HISTORY, null);
while (cursor.moveToNext()) {
UrlSuggestion suggestion = new UrlSuggestion();
suggestion.setText(cursor.getString(0));
suggestion.setUri(cursor.isNull(1) ? null : LbryUri.tryParse(cursor.getString(1)));
suggestion.setType(cursor.getInt(2));
suggestion.setTitleUrlOnly(true);
suggestions.add(suggestion);
}
} finally {
Helper.closeCursor(cursor);
}
return suggestions;
}
// View history items are stream claims
public static void createOrUpdateViewHistoryItem(ViewHistory viewHistory, SQLiteDatabase db) {
db.execSQL(SQL_INSERT_VIEW_HISTORY, new Object[] {
viewHistory.getUri().toString(),
viewHistory.getClaimId(),
viewHistory.getClaimName(),
viewHistory.getCost() != null ? viewHistory.getCost().doubleValue() : 0,
viewHistory.getCurrency(),
viewHistory.getTitle(),
viewHistory.getPublisherClaimId(),
viewHistory.getPublisherName(),
viewHistory.getPublisherTitle(),
viewHistory.getThumbnailUrl(),
viewHistory.getDevice(),
viewHistory.getReleaseTime(),
new SimpleDateFormat(Helper.ISO_DATE_FORMAT_PATTERN).format(new Date())
});
}
public static List<ViewHistory> getViewHistory(String lastTimestamp, int pageLimit, SQLiteDatabase db) {
List<ViewHistory> history = new ArrayList<>();
Cursor cursor = null;
try {
String arg = lastTimestamp == null ? "" : lastTimestamp;
cursor = db.rawQuery(String.format(SQL_GET_VIEW_HISTORY, pageLimit), new String[] { arg, arg });
while (cursor.moveToNext()) {
ViewHistory item = new ViewHistory();
int cursorIndex = 0;
item.setUri(LbryUri.tryParse(cursor.getString(cursorIndex++)));
item.setClaimId(cursor.getString(cursorIndex++));
item.setClaimName(cursor.getString(cursorIndex++));
item.setCost(new BigDecimal(cursor.getDouble(cursorIndex++)));
item.setCurrency(cursor.getString(cursorIndex++));
item.setTitle(cursor.getString(cursorIndex++));
item.setPublisherClaimId(cursor.getString(cursorIndex++));
item.setPublisherName(cursor.getString(cursorIndex++));
item.setPublisherTitle(cursor.getString(cursorIndex++));
item.setThumbnailUrl(cursor.getString(cursorIndex++));
item.setDevice(cursor.getString(cursorIndex++));
item.setReleaseTime(cursor.getLong(cursorIndex++));
try {
item.setTimestamp(new SimpleDateFormat(Helper.ISO_DATE_FORMAT_PATTERN).parse(cursor.getString(cursorIndex)));
} catch (ParseException ex) {
// invalid timestamp (which shouldn't happen). Skip this item
continue;
}
history.add(item);
}
} finally {
Helper.closeCursor(cursor);
}
return history;
}
public static void createOrUpdateTag(Tag tag, SQLiteDatabase db) {
db.execSQL(SQL_INSERT_TAG, new Object[] { tag.getLowercaseName(), tag.isFollowed() ? 1 : 0 });
}
public static void setAllTagsUnfollowed(SQLiteDatabase db) {
db.execSQL(SQL_UNFOLLOW_TAGS);
}
public static List<Tag> getTags(SQLiteDatabase db) {
List<Tag> tags = new ArrayList<>();
Cursor cursor = null;
try {
cursor = db.rawQuery(SQL_GET_KNOWN_TAGS, null);
while (cursor.moveToNext()) {
Tag tag = new Tag();
tag.setName(cursor.getString(0));
tag.setFollowed(cursor.getInt(1) == 1);
tags.add(tag);
}
} finally {
Helper.closeCursor(cursor);
}
return tags;
}
public static void createOrUpdateSubscription(Subscription subscription, SQLiteDatabase db) {
db.execSQL(SQL_INSERT_SUBSCRIPTION, new Object[] {
subscription.getChannelName(),
subscription.getUrl(),
subscription.isNotificationsDisabled() ? 1 : 0
});
}
public static void setSubscriptionNotificationDisabled(boolean flag, String url, SQLiteDatabase db) {
db.execSQL(SQL_UPDATE_SUBSCRIPTION_NOTIFICATION, new Object[] { flag ? 1 : 0, url });
}
public static void deleteSubscription(Subscription subscription, SQLiteDatabase db) {
db.execSQL(SQL_DELETE_SUBSCRIPTION, new Object[] { subscription.getUrl() });
}
public static void clearSubscriptions(SQLiteDatabase db) {
db.execSQL(SQL_CLEAR_SUBSCRIPTIONS);
}
public static List<Subscription> getSubscriptions(SQLiteDatabase db) {
List<Subscription> subscriptions = new ArrayList<>();
Cursor cursor = null;
try {
cursor = db.rawQuery(SQL_GET_SUBSCRIPTIONS, null);
while (cursor.moveToNext()) {
Subscription subscription = new Subscription();
subscription.setChannelName(cursor.getString(0));
subscription.setUrl(cursor.getString(1));
subscription.setNotificationsDisabled(cursor.getInt(2) == 1);
subscriptions.add(subscription);
}
} finally {
Helper.closeCursor(cursor);
}
return subscriptions;
}
public static void createOrUpdateNotification(LbryNotification notification, SQLiteDatabase db) {
db.execSQL(SQL_INSERT_NOTIFICATION, new Object[] {
notification.getRemoteId(),
notification.getAuthorUrl(),
notification.getTitle(),
notification.getDescription(),
notification.getRule(),
notification.getTargetUrl(),
notification.isRead() ? 1 : 0,
notification.isSeen() ? 1 : 0,
new SimpleDateFormat(Helper.ISO_DATE_FORMAT_PATTERN).format(notification.getTimestamp() != null ? notification.getTimestamp() : new Date())
});
}
public static List<LbryNotification> getNotifications(SQLiteDatabase db) {
List<LbryNotification> notifications = new ArrayList<>();
Cursor cursor = null;
try {
cursor = db.rawQuery(SQL_GET_NOTIFICATIONS, null);
while (cursor.moveToNext()) {
LbryNotification notification = new LbryNotification();
int columnIndex = 0;
notification.setId(cursor.getLong(columnIndex++));
notification.setRemoteId(cursor.getLong(columnIndex++));
notification.setAuthorUrl(cursor.getString(columnIndex++));
notification.setTitle(cursor.getString(columnIndex++));
notification.setDescription(cursor.getString(columnIndex++));
notification.setRule(cursor.getString(columnIndex++));
notification.setTargetUrl(cursor.getString(columnIndex++));
notification.setRead(cursor.getInt(columnIndex++) == 1);
notification.setSeen(cursor.getInt(columnIndex++) == 1);
try {
notification.setTimestamp(new SimpleDateFormat(Helper.ISO_DATE_FORMAT_PATTERN).parse(cursor.getString(columnIndex++)));
} catch (ParseException ex) {
// invalid timestamp (which shouldn't happen). Skip this item
continue;
}
notifications.add(notification);
}
} finally {
Helper.closeCursor(cursor);
}
return notifications;
}
public static void deleteNotifications(List<LbryNotification> notifications, SQLiteDatabase db) {
StringBuilder sb = new StringBuilder("DELETE FROM notifications WHERE remote_id IN (");
List<Object> remoteIds = new ArrayList<>();
String delim = "";
for (int i = 0; i < notifications.size(); i++) {
remoteIds.add(String.valueOf(notifications.get(i).getRemoteId()));
sb.append(delim).append("?");
delim = ",";
}
sb.append(")");
String sql = sb.toString();
db.execSQL(sql, remoteIds.toArray());
}
public static int getUnreadNotificationsCount(SQLiteDatabase db) {
int count = 0;
Cursor cursor = null;
try {
cursor = db.rawQuery(SQL_GET_UNREAD_NOTIFICATIONS_COUNT, null);
if (cursor.moveToNext()) {
count = cursor.getInt(0);
}
} finally {
Helper.closeCursor(cursor);
}
return count;
}
public static int getUnseenNotificationsCount(SQLiteDatabase db) {
int count = 0;
Cursor cursor = null;
try {
cursor = db.rawQuery(SQL_GET_UNSEEN_NOTIFICATIONS_COUNT, null);
if (cursor.moveToNext()) {
count = cursor.getInt(0);
}
} finally {
Helper.closeCursor(cursor);
}
return count;
}
public static void markNotificationsSeen(SQLiteDatabase db) {
db.execSQL(SQL_MARK_NOTIFICATIONS_SEEN);
}
public static void markNotificationsRead(SQLiteDatabase db) {
db.execSQL(SQL_MARK_NOTIFICATIONS_READ);
}
public static void markNotificationReadAndSeen(long notificationId, SQLiteDatabase db) {
db.execSQL(SQL_MARK_NOTIFICATION_READ_AND_SEEN, new Object[] { notificationId });
}
public static void createOrUpdateShuffleWatched(String claimId, SQLiteDatabase db) {
db.execSQL(SQL_INSERT_SHUFFLE_WATCHED, new Object[] { claimId });
}
public static List<String> getShuffleWatchedClaims(SQLiteDatabase db) {
List<String> claimIds = new ArrayList<>();
Cursor cursor = null;
try {
cursor = db.rawQuery(SQL_GET_SHUFFLE_WATCHED_CLAIMS, null);
while (cursor.moveToNext()) {
claimIds.add(cursor.getString(0));
}
} finally {
Helper.closeCursor(cursor);
}
return claimIds;
}
}

View file

@ -0,0 +1,112 @@
package io.lbry.browser.dialog;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import io.lbry.browser.R;
import lombok.Setter;
public class ContentFromDialogFragment extends BottomSheetDialogFragment {
public static final String TAG = "ContentFromDialog";
public static final int ITEM_FROM_PAST_24_HOURS = 1;
public static final int ITEM_FROM_PAST_WEEK = 2;
public static final int ITEM_FROM_PAST_MONTH = 3;
public static final int ITEM_FROM_PAST_YEAR = 4;
public static final int ITEM_FROM_ALL_TIME = 5;
@Setter
private ContentFromListener contentFromListener;
private int currentFromItem;
public static ContentFromDialogFragment newInstance() {
return new ContentFromDialogFragment();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.dialog_content_from, container,false);
ContentFromItemClickListener clickListener = new ContentFromItemClickListener(this, contentFromListener);
view.findViewById(R.id.content_from_past_24_hours_item).setOnClickListener(clickListener);
view.findViewById(R.id.content_from_past_week_item).setOnClickListener(clickListener);
view.findViewById(R.id.content_from_past_month_item).setOnClickListener(clickListener);
view.findViewById(R.id.content_from_past_year_item).setOnClickListener(clickListener);
view.findViewById(R.id.content_from_all_time_item).setOnClickListener(clickListener);
checkSelectedFromItem(currentFromItem, view);
return view;
}
public static void checkSelectedFromItem(int fromItem, View parent) {
int checkViewId = -1;
switch (fromItem) {
case ITEM_FROM_PAST_24_HOURS: checkViewId = R.id.content_from_past_24_hours_item_selected; break;
case ITEM_FROM_PAST_WEEK: checkViewId = R.id.content_from_past_week_item_selected; break;
case ITEM_FROM_PAST_MONTH: checkViewId = R.id.content_from_past_month_item_selected; break;
case ITEM_FROM_PAST_YEAR: checkViewId = R.id.content_from_past_year_item_selected; break;
case ITEM_FROM_ALL_TIME: checkViewId = R.id.content_from_all_time_item_selected; break;
}
if (parent != null && checkViewId > -1) {
parent.findViewById(checkViewId).setVisibility(View.VISIBLE);
}
}
public void setCurrentFromItem(int fromItem) {
this.currentFromItem = fromItem;
}
private static class ContentFromItemClickListener implements View.OnClickListener {
private final int[] checkViewIds = {
R.id.content_from_past_24_hours_item,
R.id.content_from_past_week_item,
R.id.content_from_past_month_item,
R.id.content_from_past_year_item,
R.id.content_from_all_time_item
};
private final BottomSheetDialogFragment dialog;
private final ContentFromListener listener;
public ContentFromItemClickListener(BottomSheetDialogFragment dialog, ContentFromListener listener) {
this.dialog = dialog;
this.listener = listener;
}
public void onClick(View view) {
int currentFromItem = -1;
if (dialog != null) {
View dialogView = dialog.getView();
if (dialogView != null) {
for (int id : checkViewIds) {
dialogView.findViewById(id).setVisibility(View.GONE);
}
}
}
switch (view.getId()) {
case R.id.content_from_past_24_hours_item: currentFromItem = ITEM_FROM_PAST_24_HOURS; break;
case R.id.content_from_past_week_item: currentFromItem = ITEM_FROM_PAST_WEEK; break;
case R.id.content_from_past_month_item: currentFromItem = ITEM_FROM_PAST_MONTH; break;
case R.id.content_from_past_year_item: currentFromItem = ITEM_FROM_PAST_YEAR; break;
case R.id.content_from_all_time_item: currentFromItem = ITEM_FROM_ALL_TIME; break;
}
checkSelectedFromItem(currentFromItem, view);
if (listener != null) {
listener.onContentFromItemSelected(currentFromItem);
}
if (dialog != null) {
dialog.dismiss();
}
}
}
public interface ContentFromListener {
void onContentFromItemSelected(int contentFromItem);
}
}

View file

@ -0,0 +1,96 @@
package io.lbry.browser.dialog;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import io.lbry.browser.R;
import lombok.Setter;
public class ContentScopeDialogFragment extends BottomSheetDialogFragment {
public static final String TAG = "ContentScopeDialog";
public static final int ITEM_EVERYONE = 1;
public static final int ITEM_TAGS = 2;
@Setter
private ContentScopeListener contentScopeListener;
private int currentScopeItem;
public static ContentScopeDialogFragment newInstance() {
return new ContentScopeDialogFragment();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.dialog_content_scope, container,false);
ContentScopeItemClickListener clickListener = new ContentScopeItemClickListener(this, contentScopeListener);
view.findViewById(R.id.content_scope_everyone_item).setOnClickListener(clickListener);
view.findViewById(R.id.content_scope_tags_item).setOnClickListener(clickListener);
checkSelectedScopeItem(currentScopeItem, view);
return view;
}
public static void checkSelectedScopeItem(int scope, View parent) {
int checkViewId = -1;
switch (scope) {
case ITEM_EVERYONE: checkViewId = R.id.content_scope_everyone_item_selected; break;
case ITEM_TAGS: checkViewId = R.id.content_scope_tags_item_selected; break;
}
if (parent != null && checkViewId > -1) {
parent.findViewById(checkViewId).setVisibility(View.VISIBLE);
}
}
public void setCurrentScopeItem(int scopeItem) {
this.currentScopeItem = scopeItem;
}
private static class ContentScopeItemClickListener implements View.OnClickListener {
private final int[] checkViewIds = {
R.id.content_scope_everyone_item_selected, R.id.content_scope_tags_item_selected
};
private final BottomSheetDialogFragment dialog;
private final ContentScopeListener listener;
public ContentScopeItemClickListener(BottomSheetDialogFragment dialog, ContentScopeListener listener) {
this.dialog = dialog;
this.listener = listener;
}
public void onClick(View view) {
int scopeItem = -1;
if (dialog != null) {
View dialogView = dialog.getView();
if (dialogView != null) {
for (int id : checkViewIds) {
dialogView.findViewById(id).setVisibility(View.GONE);
}
}
}
switch (view.getId()) {
case R.id.content_scope_everyone_item: scopeItem = ITEM_EVERYONE; break;
case R.id.content_scope_tags_item: scopeItem = ITEM_TAGS; break;
}
checkSelectedScopeItem(scopeItem, view);
if (listener != null) {
listener.onContentScopeItemSelected(scopeItem);
}
if (dialog != null) {
dialog.dismiss();
}
}
}
public interface ContentScopeListener {
void onContentScopeItemSelected(int scopeItem);
}
}

View file

@ -0,0 +1,100 @@
package io.lbry.browser.dialog;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import io.lbry.browser.R;
import lombok.Setter;
public class ContentSortDialogFragment extends BottomSheetDialogFragment {
public static final String TAG = "ContentSortDialog";
public static final int ITEM_SORT_BY_TRENDING = 1;
public static final int ITEM_SORT_BY_NEW = 2;
public static final int ITEM_SORT_BY_TOP = 3;
@Setter
private SortByListener sortByListener;
private int currentSortByItem;
public static ContentSortDialogFragment newInstance() {
return new ContentSortDialogFragment();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.dialog_content_sort, container,false);
SortByItemClickListener clickListener = new SortByItemClickListener(this, sortByListener);
view.findViewById(R.id.sort_by_trending_item).setOnClickListener(clickListener);
view.findViewById(R.id.sort_by_new_item).setOnClickListener(clickListener);
view.findViewById(R.id.sort_by_top_item).setOnClickListener(clickListener);
checkSelectedSortByItem(currentSortByItem, view);
return view;
}
public static void checkSelectedSortByItem(int sortByItem, View parent) {
int checkViewId = -1;
switch (sortByItem) {
case ITEM_SORT_BY_TRENDING: checkViewId = R.id.sort_by_trending_item_selected; break;
case ITEM_SORT_BY_NEW: checkViewId = R.id.sort_by_new_item_selected; break;
case ITEM_SORT_BY_TOP: checkViewId = R.id.sort_by_top_item_selected; break;
}
if (parent != null && checkViewId > -1) {
parent.findViewById(checkViewId).setVisibility(View.VISIBLE);
}
}
public void setCurrentSortByItem(int sortByItem) {
this.currentSortByItem = sortByItem;
}
private static class SortByItemClickListener implements View.OnClickListener {
private final int[] checkViewIds = {
R.id.sort_by_trending_item_selected, R.id.sort_by_new_item_selected, R.id.sort_by_top_item_selected
};
private final BottomSheetDialogFragment dialog;
private final SortByListener listener;
public SortByItemClickListener(BottomSheetDialogFragment dialog, SortByListener listener) {
this.dialog = dialog;
this.listener = listener;
}
public void onClick(View view) {
int selectedSortByItem = -1;
if (dialog != null) {
View dialogView = dialog.getView();
if (dialogView != null) {
for (int id : checkViewIds) {
dialogView.findViewById(id).setVisibility(View.GONE);
}
}
}
switch (view.getId()) {
case R.id.sort_by_trending_item: selectedSortByItem = ITEM_SORT_BY_TRENDING; break;
case R.id.sort_by_new_item: selectedSortByItem = ITEM_SORT_BY_NEW; break;
case R.id.sort_by_top_item: selectedSortByItem = ITEM_SORT_BY_TOP; break;
}
checkSelectedSortByItem(selectedSortByItem, view);
if (listener != null) {
listener.onSortByItemSelected(selectedSortByItem);
}
if (dialog != null) {
dialog.dismiss();
}
}
}
public interface SortByListener {
void onSortByItemSelected(int sortBy);
}
}

View file

@ -0,0 +1,349 @@
package io.lbry.browser.dialog;
import android.app.Dialog;
import android.content.Context;
import android.graphics.Color;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.text.method.LinkMovementMethod;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CompoundButton;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.appcompat.widget.AppCompatSpinner;
import androidx.core.text.HtmlCompat;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.snackbar.Snackbar;
import com.google.android.material.switchmaterial.SwitchMaterial;
import com.google.android.material.textfield.TextInputEditText;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import io.lbry.browser.MainActivity;
import io.lbry.browser.R;
import io.lbry.browser.adapter.InlineChannelSpinnerAdapter;
import io.lbry.browser.listener.WalletBalanceListener;
import io.lbry.browser.model.Claim;
import io.lbry.browser.model.WalletBalance;
import io.lbry.browser.tasks.GenericTaskHandler;
import io.lbry.browser.tasks.claim.ClaimListResultHandler;
import io.lbry.browser.tasks.claim.ClaimListTask;
import io.lbry.browser.tasks.wallet.SupportCreateTask;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbry;
public class CreateSupportDialogFragment extends BottomSheetDialogFragment implements WalletBalanceListener {
public static final String TAG = "CreateSupportDialog";
private MaterialButton sendButton;
private View cancelLink;
private TextInputEditText inputAmount;
private View inlineBalanceContainer;
private TextView inlineBalanceValue;
private ProgressBar sendProgress;
private InlineChannelSpinnerAdapter channelSpinnerAdapter;
private AppCompatSpinner channelSpinner;
private SwitchMaterial switchTip;
private boolean fetchingChannels;
private ProgressBar progressLoadingChannels;
private final CreateSupportListener listener;
private final Claim claim;
private CreateSupportDialogFragment(Claim claim, CreateSupportListener listener) {
super();
this.claim = claim;
this.listener = listener;
}
public static CreateSupportDialogFragment newInstance(Claim claim, CreateSupportListener listener) {
return new CreateSupportDialogFragment(claim, listener);
}
private void disableControls() {
Dialog dialog = getDialog();
if (dialog != null) {
dialog.setCanceledOnTouchOutside(false);
}
channelSpinner.setEnabled(false);
switchTip.setEnabled(false);
sendButton.setEnabled(false);
cancelLink.setEnabled(false);
}
private void enableControls() {
Dialog dialog = getDialog();
if (dialog != null) {
dialog.setCanceledOnTouchOutside(true);
}
channelSpinner.setEnabled(true);
switchTip.setEnabled(true);
sendButton.setEnabled(true);
cancelLink.setEnabled(true);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.dialog_create_support, container, false);
inputAmount = view.findViewById(R.id.create_support_input_amount);
inlineBalanceContainer = view.findViewById(R.id.create_support_inline_balance_container);
inlineBalanceValue = view.findViewById(R.id.create_support_inline_balance_value);
sendProgress = view.findViewById(R.id.create_support_progress);
cancelLink = view.findViewById(R.id.create_support_cancel_link);
sendButton = view.findViewById(R.id.create_support_send);
channelSpinner = view.findViewById(R.id.create_support_channel_spinner);
switchTip = view.findViewById(R.id.create_support_make_tip_switch);
progressLoadingChannels = view.findViewById(R.id.create_support_channel_progress);
inputAmount.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View view, boolean hasFocus) {
inputAmount.setHint(hasFocus ? getString(R.string.zero) : "");
inlineBalanceContainer.setVisibility(hasFocus ? View.VISIBLE : View.INVISIBLE);
}
});
inputAmount.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
updateSendButtonText();
}
@Override
public void afterTextChanged(Editable editable) {
}
});
updateInfoText();
updateSendButtonText();
String channel = null;
if (Claim.TYPE_CHANNEL.equalsIgnoreCase(claim.getValueType())) {
channel = claim.getTitleOrName();
} else if (claim.getSigningChannel() != null) {
channel = claim.getPublisherTitle();
}
TextView titleView = view.findViewById(R.id.create_support_title);
String tipTitleText = Helper.isNullOrEmpty(channel) ? getString(R.string.send_a_tip) : getString(R.string.send_a_tip_to, channel);
titleView.setText(tipTitleText);
switchTip.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean checked) {
if (checked) {
// show tip info
titleView.setText(tipTitleText);
updateSendButtonText();
} else {
// show support info
titleView.setText(R.string.support_this_content);
sendButton.setText(R.string.send_revocable_support);
}
updateInfoText();
}
});
sendButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String amountString = Helper.getValue(inputAmount.getText());
if (Helper.isNullOrEmpty(amountString)) {
showError(getString(R.string.invalid_amount));
return;
}
BigDecimal amount = new BigDecimal(amountString);
if (amount.doubleValue() > Lbry.walletBalance.getAvailable().doubleValue()) {
showError(getString(R.string.insufficient_balance));
return;
}
if (amount.doubleValue() < Helper.MIN_SPEND) {
showError(getString(R.string.min_spend_required));
return;
}
Claim selectedChannel = (Claim) channelSpinner.getSelectedItem();
String channelId = !fetchingChannels && selectedChannel != null ? selectedChannel.getClaimId() : null;
boolean isTip = switchTip.isChecked();
SupportCreateTask task = new SupportCreateTask(
claim.getClaimId(), channelId, amount, isTip, sendProgress, new GenericTaskHandler() {
@Override
public void beforeStart() {
disableControls();
}
@Override
public void onSuccess() {
enableControls();
if (listener != null) {
listener.onSupportCreated(amount, isTip);
}
dismiss();
}
@Override
public void onError(Exception error) {
showError(error.getMessage());
enableControls();
}
});
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
});
cancelLink.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
dismiss();
}
});
onWalletBalanceUpdated(Lbry.walletBalance);
updateInfoText();
return view;
}
private void updateSendButtonText() {
boolean isTip = switchTip.isChecked();
if (!isTip) {
sendButton.setText(R.string.send_revocable_support);
} else {
String amountString = Helper.getValue(inputAmount.getText(), "0");
double parsedAmount = Helper.parseDouble(amountString, 0);
String text = getResources().getQuantityString(R.plurals.send_lbc_tip, parsedAmount == 1.0 ? 1 : 2, amountString);
sendButton.setText(text);
}
}
private void updateInfoText() {
View view = getView();
if (view != null && switchTip != null) {
TextView infoText = view.findViewById(R.id.create_support_info);
boolean isTip = switchTip.isChecked();
infoText.setMovementMethod(LinkMovementMethod.getInstance());
if (!isTip) {
infoText.setText(HtmlCompat.fromHtml(getString(R.string.support_info), HtmlCompat.FROM_HTML_MODE_LEGACY));
} else if (claim != null) {
infoText.setText(HtmlCompat.fromHtml(
Claim.TYPE_CHANNEL.equalsIgnoreCase(claim.getValueType()) ?
getString(R.string.send_tip_info_channel, claim.getTitleOrName()) :
getString(R.string.send_tip_info_content, claim.getTitleOrName()),
HtmlCompat.FROM_HTML_MODE_LEGACY));
}
}
}
private void fetchChannels() {
if (Lbry.ownChannels != null && Lbry.ownChannels.size() > 0) {
updateChannelList(Lbry.ownChannels);
return;
}
fetchingChannels = true;
disableChannelSpinner();
ClaimListTask task = new ClaimListTask(Claim.TYPE_CHANNEL, progressLoadingChannels, new ClaimListResultHandler() {
@Override
public void onSuccess(List<Claim> claims) {
Lbry.ownChannels = new ArrayList<>(claims);
updateChannelList(Lbry.ownChannels);
enableChannelSpinner();
fetchingChannels = false;
}
@Override
public void onError(Exception error) {
enableChannelSpinner();
fetchingChannels = false;
}
});
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void disableChannelSpinner() {
Helper.setViewEnabled(channelSpinner, false);
}
private void enableChannelSpinner() {
Helper.setViewEnabled(channelSpinner, true);
}
private void updateChannelList(List<Claim> channels) {
if (channelSpinnerAdapter == null) {
Context context = getContext();
if (context != null) {
channelSpinnerAdapter = new InlineChannelSpinnerAdapter(context, R.layout.spinner_item_channel, new ArrayList<>(channels));
channelSpinnerAdapter.addAnonymousPlaceholder();
channelSpinnerAdapter.notifyDataSetChanged();
}
} else {
channelSpinnerAdapter.clear();
channelSpinnerAdapter.addAll(channels);
channelSpinnerAdapter.addAnonymousPlaceholder();
channelSpinnerAdapter.notifyDataSetChanged();
}
if (channelSpinner != null) {
channelSpinner.setAdapter(channelSpinnerAdapter);
}
if (channelSpinnerAdapter != null && channelSpinner != null) {
if (channelSpinnerAdapter.getCount() > 1) {
channelSpinner.setSelection(1);
}
}
}
public void onResume() {
super.onResume();
Context context = getContext();
if (context instanceof MainActivity) {
((MainActivity) context).addWalletBalanceListener(this);
}
updateInfoText();
fetchChannels();
}
public void onPause() {
Context context = getContext();
if (context instanceof MainActivity) {
((MainActivity) context).removeWalletBalanceListener(this);
}
super.onPause();
}
@Override
public void onWalletBalanceUpdated(WalletBalance walletBalance) {
if (walletBalance != null && inlineBalanceValue != null) {
inlineBalanceValue.setText(Helper.shortCurrencyFormat(walletBalance.getAvailable().doubleValue()));
}
}
private void showError(String message) {
Snackbar.make(getView(), message, Snackbar.LENGTH_LONG).
setBackgroundTint(Color.RED).
setTextColor(Color.WHITE).
show();
}
public interface CreateSupportListener {
void onSupportCreated(BigDecimal amount, boolean isTip);
}
}

View file

@ -0,0 +1,186 @@
package io.lbry.browser.dialog;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.flexbox.FlexboxLayoutManager;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.snackbar.Snackbar;
import com.google.android.material.textfield.TextInputEditText;
import java.util.ArrayList;
import java.util.List;
import io.lbry.browser.R;
import io.lbry.browser.adapter.TagListAdapter;
import io.lbry.browser.listener.TagListener;
import io.lbry.browser.model.Tag;
import io.lbry.browser.tasks.UpdateSuggestedTagsTask;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbry;
import lombok.Setter;
public class CustomizeTagsDialogFragment extends BottomSheetDialogFragment {
public static final String TAG = "CustomizeTagsDialog";
private static final int SUGGESTED_LIMIT = 8;
private String currentFilter;
private RecyclerView followedTagsList;
private RecyclerView suggestedTagsList;
private TagListAdapter followedTagsAdapter;
private TagListAdapter suggestedTagsAdapter;
private View noTagsView;
private View noResultsView;
@Setter
private TagListener listener;
private void checkNoTags() {
Helper.setViewVisibility(noTagsView, followedTagsAdapter == null || followedTagsAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE);
}
private void checkNoResults() {
Helper.setViewVisibility(noResultsView, suggestedTagsAdapter == null || suggestedTagsAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE);
}
public void addTag(Tag tag) {
if (followedTagsAdapter.getTags().contains(tag)) {
Snackbar.make(getView(), getString(R.string.tag_already_added, tag.getName()), Snackbar.LENGTH_LONG).show();
return;
}
tag.setFollowed(true);
followedTagsAdapter.addTag(tag);
if (suggestedTagsAdapter != null) {
suggestedTagsAdapter.removeTag(tag);
}
updateKnownTags(currentFilter, SUGGESTED_LIMIT, false);
if (listener != null) {
listener.onTagAdded(tag);
}
checkNoTags();
checkNoResults();
}
public void removeTag(Tag tag) {
tag.setFollowed(false);
followedTagsAdapter.removeTag(tag);
updateKnownTags(currentFilter, SUGGESTED_LIMIT, false);
if (listener != null) {
listener.onTagRemoved(tag);
}
checkNoTags();
checkNoResults();
}
public void setFilter(String filter) {
currentFilter = filter;
updateKnownTags(currentFilter, SUGGESTED_LIMIT, true);
}
public static CustomizeTagsDialogFragment newInstance() {
return new CustomizeTagsDialogFragment();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.dialog_customize_tags, container, false);
noResultsView = view.findViewById(R.id.customize_no_tag_results);
noTagsView = view.findViewById(R.id.customize_no_followed_tags);
followedTagsAdapter = new TagListAdapter(Lbry.followedTags, getContext());
followedTagsAdapter.setCustomizeMode(TagListAdapter.CUSTOMIZE_MODE_REMOVE);
followedTagsAdapter.setClickListener(customizeTagClickListener);
suggestedTagsAdapter = new TagListAdapter(new ArrayList<>(), getContext());
suggestedTagsAdapter.setCustomizeMode(TagListAdapter.CUSTOMIZE_MODE_ADD);
suggestedTagsAdapter.setClickListener(customizeTagClickListener);
FlexboxLayoutManager flm1 = new FlexboxLayoutManager(getContext());
followedTagsList = view.findViewById(R.id.customize_tags_followed_list);
followedTagsList.setLayoutManager(flm1);
followedTagsList.setAdapter(followedTagsAdapter);
FlexboxLayoutManager flm2 = new FlexboxLayoutManager(getContext());
suggestedTagsList = view.findViewById(R.id.customize_tags_suggested_list);
suggestedTagsList.setLayoutManager(flm2);
suggestedTagsList.setAdapter(suggestedTagsAdapter);
TextInputEditText filterInput = view.findViewById(R.id.customize_tag_filter_input);
filterInput.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
String value = Helper.getValue(charSequence);
setFilter(value);
}
@Override
public void afterTextChanged(Editable editable) {
}
});
MaterialButton doneButton = view.findViewById(R.id.customize_done_button);
doneButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
dismiss();
}
});
checkNoTags();
return view;
}
private final TagListAdapter.TagClickListener customizeTagClickListener = new TagListAdapter.TagClickListener() {
@Override
public void onTagClicked(Tag tag, int customizeMode) {
if (customizeMode == TagListAdapter.CUSTOMIZE_MODE_ADD) {
addTag(tag);
} else if (customizeMode == TagListAdapter.CUSTOMIZE_MODE_REMOVE) {
removeTag(tag);
}
}
};
public void onResume() {
super.onResume();
updateKnownTags(null, SUGGESTED_LIMIT, true);
}
private void updateKnownTags(String filter, int limit, boolean clearPrevious) {
UpdateSuggestedTagsTask task = new UpdateSuggestedTagsTask(
filter,
SUGGESTED_LIMIT,
followedTagsAdapter,
suggestedTagsAdapter,
clearPrevious,
false, new UpdateSuggestedTagsTask.KnownTagsHandler() {
@Override
public void onSuccess(List<Tag> tags) {
if (suggestedTagsAdapter == null) {
suggestedTagsAdapter = new TagListAdapter(tags, getContext());
suggestedTagsAdapter.setCustomizeMode(TagListAdapter.CUSTOMIZE_MODE_ADD);
suggestedTagsAdapter.setClickListener(customizeTagClickListener);
if (suggestedTagsList != null) {
suggestedTagsList.setAdapter(suggestedTagsAdapter);
}
} else {
suggestedTagsAdapter.setTags(tags);
}
checkNoResults();
}
});
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}

View file

@ -0,0 +1,106 @@
package io.lbry.browser.dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.google.android.material.button.MaterialButton;
import io.lbry.browser.R;
import io.lbry.browser.adapter.SuggestedChannelGridAdapter;
import lombok.Getter;
import lombok.Setter;
public class DiscoverDialogFragment extends BottomSheetDialogFragment {
public static final String TAG = "DiscoverDialog";
@Getter
private SuggestedChannelGridAdapter adapter;
@Setter
private DiscoverDialogListener dialogActionsListener;
public static DiscoverDialogFragment newInstance() {
return new DiscoverDialogFragment();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.dialog_discover, container, false);
RecyclerView grid = view.findViewById(R.id.discover_channel_grid);
GridLayoutManager glm = new GridLayoutManager(getContext(), 3);
grid.setLayoutManager(glm);
grid.setAdapter(adapter);
grid.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
GridLayoutManager lm = (GridLayoutManager) recyclerView.getLayoutManager();
if (lm != null) {
int visibleItemCount = lm.getChildCount();
int totalItemCount = lm.getItemCount();
int pastVisibleItems = lm.findFirstVisibleItemPosition();
if (pastVisibleItems + visibleItemCount >= totalItemCount) {
if (dialogActionsListener != null) {
dialogActionsListener.onScrollEndReached();
}
}
}
}
});
MaterialButton doneButton = view.findViewById(R.id.discover_done_button);
doneButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
dismiss();
}
});
return view;
}
public void setAdapter(SuggestedChannelGridAdapter adapter) {
this.adapter = adapter;
if (getView() != null) {
((RecyclerView) getView().findViewById(R.id.discover_channel_grid)).setAdapter(adapter);
}
}
public void setLoading(boolean loading) {
if (getView() != null) {
getView().findViewById(R.id.discover_loading).setVisibility(loading ? View.VISIBLE : View.GONE);
}
}
@Override
public void onResume() {
super.onResume();
if (dialogActionsListener != null) {
dialogActionsListener.onResume();
}
}
@Override
public void onCancel(DialogInterface dialog) {
super.onDismiss(dialog);
if (dialogActionsListener != null) {
dialogActionsListener.onCancel();
}
}
@Override
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
if (dialogActionsListener != null) {
dialogActionsListener.onCancel();
}
}
public interface DiscoverDialogListener {
void onResume();
void onCancel();
void onScrollEndReached();
}
}

View file

@ -0,0 +1,309 @@
package io.lbry.browser.dialog;
import android.app.Dialog;
import android.content.Context;
import android.graphics.Color;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.appcompat.widget.AppCompatSpinner;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.snackbar.Snackbar;
import com.google.android.material.textfield.TextInputEditText;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import io.lbry.browser.MainActivity;
import io.lbry.browser.R;
import io.lbry.browser.adapter.InlineChannelSpinnerAdapter;
import io.lbry.browser.listener.WalletBalanceListener;
import io.lbry.browser.model.Claim;
import io.lbry.browser.model.WalletBalance;
import io.lbry.browser.tasks.claim.ClaimListResultHandler;
import io.lbry.browser.tasks.claim.ClaimListTask;
import io.lbry.browser.tasks.claim.ClaimResultHandler;
import io.lbry.browser.tasks.claim.StreamRepostTask;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbry;
import io.lbry.browser.utils.LbryUri;
public class RepostClaimDialogFragment extends BottomSheetDialogFragment implements WalletBalanceListener {
public static final String TAG = "RepostClaimDialog";
private MaterialButton buttonRepost;
private View linkCancel;
private TextInputEditText inputDeposit;
private View inlineBalanceContainer;
private TextView inlineBalanceValue;
private ProgressBar repostProgress;
private TextView textTitle;
private AppCompatSpinner channelSpinner;
private InlineChannelSpinnerAdapter channelSpinnerAdapter;
private TextView textNamePrefix;
private EditText inputName;
private TextView linkToggleAdvanced;
private View advancedContainer;
private final RepostClaimListener listener;
private final Claim claim;
private RepostClaimDialogFragment(Claim claim, RepostClaimListener listener) {
super();
this.listener = listener;
this.claim = claim;
}
public static RepostClaimDialogFragment newInstance(Claim claim, RepostClaimListener listener) {
return new RepostClaimDialogFragment(claim, listener);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.dialog_repost_claim, container, false);
buttonRepost = view.findViewById(R.id.repost_button);
linkCancel = view.findViewById(R.id.repost_cancel_link);
inputDeposit = view.findViewById(R.id.repost_input_deposit);
inlineBalanceContainer = view.findViewById(R.id.repost_inline_balance_container);
inlineBalanceValue = view.findViewById(R.id.repost_inline_balance_value);
repostProgress = view.findViewById(R.id.repost_progress);
textTitle = view.findViewById(R.id.repost_title);
channelSpinner = view.findViewById(R.id.repost_channel_spinner);
textNamePrefix = view.findViewById(R.id.repost_name_prefix);
inputName = view.findViewById(R.id.repost_name_input);
linkToggleAdvanced = view.findViewById(R.id.repost_toggle_advanced);
advancedContainer = view.findViewById(R.id.repost_advanced_container);
textTitle.setText(getString(R.string.repost_title, claim.getTitle()));
inputName.setText(claim.getName());
inputDeposit.setText(R.string.min_deposit);
channelSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int position, long l) {
Object item = adapterView.getItemAtPosition(position);
if (item instanceof Claim) {
Claim claim = (Claim) item;
textNamePrefix.setText(String.format("%s%s/", LbryUri.PROTO_DEFAULT, claim.getName()));
}
}
@Override
public void onNothingSelected(AdapterView<?> adapterView) {
}
});
inputDeposit.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View view, boolean hasFocus) {
inputDeposit.setHint(hasFocus ? getString(R.string.zero) : "");
inlineBalanceContainer.setVisibility(hasFocus ? View.VISIBLE : View.INVISIBLE);
}
});
linkCancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
dismiss();
}
});
linkToggleAdvanced.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (advancedContainer.getVisibility() != View.VISIBLE) {
advancedContainer.setVisibility(View.VISIBLE);
linkToggleAdvanced.setText(R.string.hide_advanced);
} else {
advancedContainer.setVisibility(View.GONE);
linkToggleAdvanced.setText(R.string.show_advanced);
}
}
});
buttonRepost.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
validateAndRepostClaim();
}
});
onWalletBalanceUpdated(Lbry.walletBalance);
return view;
}
public void onResume() {
super.onResume();
Context context = getContext();
if (context instanceof MainActivity) {
((MainActivity) context).addWalletBalanceListener(this);
}
fetchChannels();
}
public void onPause() {
Context context = getContext();
if (context instanceof MainActivity) {
((MainActivity) context).removeWalletBalanceListener(this);
}
inputDeposit.clearFocus();
super.onPause();
}
private void fetchChannels() {
if (Lbry.ownChannels == null || Lbry.ownChannels.size() == 0) {
startLoading();
ClaimListTask task = new ClaimListTask(Claim.TYPE_CHANNEL, repostProgress, new ClaimListResultHandler() {
@Override
public void onSuccess(List<Claim> claims) {
Lbry.ownChannels = new ArrayList<>(claims);
loadChannels(claims);
finishLoading();
}
@Override
public void onError(Exception error) {
// could not fetch channels
Context context = getContext();
if (context instanceof MainActivity) {
((MainActivity) context).showError(error.getMessage());
}
dismiss();
}
});
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} else {
loadChannels(Lbry.ownChannels);
}
}
private void loadChannels(List<Claim> channels) {
if (channelSpinnerAdapter == null) {
Context context = getContext();
if (context != null) {
channelSpinnerAdapter = new InlineChannelSpinnerAdapter(context, R.layout.spinner_item_channel, channels);
channelSpinnerAdapter.notifyDataSetChanged();
}
} else {
channelSpinnerAdapter.clear();
channelSpinnerAdapter.addAll(channels);
channelSpinnerAdapter.notifyDataSetChanged();
}
if (channelSpinner != null && channelSpinnerAdapter != null) {
channelSpinner.setAdapter(channelSpinnerAdapter);
}
}
@Override
public void onWalletBalanceUpdated(WalletBalance walletBalance) {
if (walletBalance != null && inlineBalanceValue != null) {
inlineBalanceValue.setText(Helper.shortCurrencyFormat(walletBalance.getAvailable().doubleValue()));
}
}
private void validateAndRepostClaim() {
String name = Helper.getValue(inputName.getText());
if (Helper.isNullOrEmpty(name) || !LbryUri.isNameValid(name)) {
showError(getString(R.string.repost_name_invalid_characters));
return;
}
String depositString = Helper.getValue(inputDeposit.getText());
if (Helper.isNullOrEmpty(depositString)) {
showError(getString(R.string.invalid_amount));
return;
}
BigDecimal bid = new BigDecimal(depositString);
if (bid.doubleValue() > Lbry.walletBalance.getAvailable().doubleValue()) {
showError(getString(R.string.insufficient_balance));
return;
}
if (bid.doubleValue() < Helper.MIN_DEPOSIT) {
String message = getResources().getQuantityString(R.plurals.min_deposit_required, 2, String.valueOf(Helper.MIN_DEPOSIT));
showError(message);
return;
}
Claim channel = (Claim) channelSpinner.getSelectedItem();
if (channel == null) {
showError(getString(R.string.please_select_repost_channel));
return;
}
StreamRepostTask task = new StreamRepostTask(name, bid, claim.getClaimId(), channel.getClaimId(), repostProgress, new ClaimResultHandler() {
@Override
public void beforeStart() {
startLoading();
}
@Override
public void onSuccess(Claim claimResult) {
if (listener != null) {
listener.onClaimReposted(claimResult);
}
finishLoading();
dismiss();
}
@Override
public void onError(Exception error) {
showError(error.getMessage());
finishLoading();
}
});
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void showError(String message) {
View view = getView();
if (view != null && !Helper.isNullOrEmpty(message)) {
Snackbar.make(view, message, Snackbar.LENGTH_LONG).
setBackgroundTint(Color.RED).
setTextColor(Color.WHITE).
show();
}
}
private void startLoading() {
Dialog dialog = getDialog();
if (dialog != null) {
dialog.setCanceledOnTouchOutside(false);
}
linkCancel.setEnabled(false);
buttonRepost.setEnabled(false);
inputName.setEnabled(false);
channelSpinner.setEnabled(false);
linkToggleAdvanced.setVisibility(View.INVISIBLE);
}
private void finishLoading() {
Dialog dialog = getDialog();
if (dialog != null) {
dialog.setCanceledOnTouchOutside(true);
}
linkCancel.setEnabled(true);
buttonRepost.setEnabled(true);
inputName.setEnabled(true);
channelSpinner.setEnabled(true);
linkToggleAdvanced.setVisibility(View.VISIBLE);
}
public interface RepostClaimListener {
void onClaimReposted(Claim claim);
}
}

View file

@ -0,0 +1,13 @@
package io.lbry.browser.exceptions;
public class ApiCallException extends Exception {
public ApiCallException() {
super();
}
public ApiCallException(String message) {
super(message);
}
public ApiCallException(String message, Throwable cause) {
super(message, cause);
}
}

View file

@ -0,0 +1,4 @@
package io.lbry.browser.exceptions;
public class AuthTokenInvalidatedException extends Exception {
}

View file

@ -0,0 +1,13 @@
package io.lbry.browser.exceptions;
public class LbryRequestException extends Exception {
public LbryRequestException() {
super();
}
public LbryRequestException(String message) {
super(message);
}
public LbryRequestException(String message, Throwable cause) {
super(message, cause);
}
}

View file

@ -0,0 +1,13 @@
package io.lbry.browser.exceptions;
public class LbryResponseException extends Exception {
public LbryResponseException() {
super();
}
public LbryResponseException(String message) {
super(message);
}
public LbryResponseException(String message, Throwable cause) {
super(message, cause);
}
}

View file

@ -0,0 +1,13 @@
package io.lbry.browser.exceptions;
public class LbryUriException extends Exception {
public LbryUriException() {
super();
}
public LbryUriException(String message) {
super(message);
}
public LbryUriException(String message, Throwable cause) {
super(message, cause);
}
}

View file

@ -0,0 +1,13 @@
package io.lbry.browser.exceptions;
public class LbryioRequestException extends Exception {
public LbryioRequestException() {
super();
}
public LbryioRequestException(String message) {
super(message);
}
public LbryioRequestException(String message, Throwable cause) {
super(message, cause);
}
}

View file

@ -0,0 +1,25 @@
package io.lbry.browser.exceptions;
import lombok.Getter;
public class LbryioResponseException extends Exception {
@Getter
private int statusCode;
public LbryioResponseException() {
super();
}
public LbryioResponseException(String message) {
super(message);
}
public LbryioResponseException(String message, Throwable cause) {
super(message, cause);
}
public LbryioResponseException(String message, int statusCode) {
super(message);
this.statusCode = statusCode;
}
public LbryioResponseException(String message, Throwable cause, int statusCode) {
super(message, cause);
this.statusCode = statusCode;
}
}

View file

@ -0,0 +1,13 @@
package io.lbry.browser.exceptions;
public class WalletException extends Exception {
public WalletException() {
super();
}
public WalletException(String message) {
super(message);
}
public WalletException(String message, Throwable cause) {
super(message, cause);
}
}

View file

@ -0,0 +1,8 @@
package io.lbry.browser.listener;
public interface CameraPermissionListener {
void onCameraPermissionGranted();
void onCameraPermissionRefused();
void onRecordAudioPermissionGranted();
void onRecordAudioPermissionRefused();
}

View file

@ -0,0 +1,9 @@
package io.lbry.browser.listener;
import io.lbry.browser.model.Claim;
public interface ChannelItemSelectionListener {
void onChannelItemSelected(Claim claim);
void onChannelItemDeselected(Claim claim);
void onChannelSelectionCleared();
}

View file

@ -0,0 +1,5 @@
package io.lbry.browser.listener;
public interface DownloadActionListener {
void onDownloadAction(String downloadAction, String uri, String outpoint, String fileInfoJson, double progress);
}

View file

@ -0,0 +1,9 @@
package io.lbry.browser.listener;
import java.util.List;
import io.lbry.browser.model.Claim;
public interface FetchChannelsListener {
void onChannelsFetched(List<Claim> channels);
}

View file

@ -0,0 +1,9 @@
package io.lbry.browser.listener;
import java.util.List;
import io.lbry.browser.model.Claim;
public interface FetchClaimsListener {
void onClaimsFetched(List<Claim> claims);
}

View file

@ -0,0 +1,6 @@
package io.lbry.browser.listener;
public interface FilePickerListener {
void onFilePicked(String filePath);
void onFilePickerCancelled();
}

View file

@ -0,0 +1,6 @@
package io.lbry.browser.listener;
public interface PIPModeListener {
void onEnterPIPMode();
void onExitPIPMode();
}

View file

@ -0,0 +1,6 @@
package io.lbry.browser.listener;
public interface ScreenOrientationListener {
void onPortraitOrientationEntered();
void onLandscapeOrientationEntered();
}

View file

@ -0,0 +1,5 @@
package io.lbry.browser.listener;
public interface SdkStatusListener {
void onSdkReady();
}

View file

@ -0,0 +1,7 @@
package io.lbry.browser.listener;
public interface SelectionModeListener {
void onEnterSelectionMode();
void onExitSelectionMode();
void onItemSelectionToggled();
}

View file

@ -0,0 +1,13 @@
package io.lbry.browser.listener;
public interface SignInListener {
void onEmailAdded(String email);
void onEmailEdit();
void onEmailVerified();
void onPhoneAdded(String countryCode, String phoneNumber);
void onPhoneVerified();
void onManualVerifyContinue();
void onSkipQueueAction();
void onTwitterVerified();
void onManualProgress(boolean progress);
}

View file

@ -0,0 +1,6 @@
package io.lbry.browser.listener;
public interface StoragePermissionListener {
void onStoragePermissionGranted();
void onStoragePermissionRefused();
}

View file

@ -0,0 +1,8 @@
package io.lbry.browser.listener;
import io.lbry.browser.model.Tag;
public interface TagListener {
void onTagAdded(Tag tag);
void onTagRemoved(Tag tag);
}

View file

@ -0,0 +1,7 @@
package io.lbry.browser.listener;
import io.lbry.browser.model.WalletBalance;
public interface WalletBalanceListener {
void onWalletBalanceUpdated(WalletBalance walletBalance);
}

View file

@ -0,0 +1,8 @@
package io.lbry.browser.listener;
public interface WalletSyncListener {
void onWalletSyncProcessing();
void onWalletSyncWaitingForInput();
void onWalletSyncEnabled();
void onWalletSyncFailed(Exception error);
}

View file

@ -0,0 +1,542 @@
package io.lbry.browser.model;
import androidx.annotation.Nullable;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import org.json.JSONException;
import org.json.JSONObject;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.LbryUri;
import io.lbry.browser.utils.Predefined;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Claim {
public static final String CLAIM_TYPE_CLAIM = "claim";
public static final String CLAIM_TYPE_UPDATE = "update";
public static final String CLAIM_TYPE_SUPPORT = "support";
public static final String TYPE_STREAM = "stream";
public static final String TYPE_CHANNEL = "channel";
public static final String TYPE_REPOST = "repost";
public static final String STREAM_TYPE_AUDIO = "audio";
public static final String STREAM_TYPE_IMAGE = "image";
public static final String STREAM_TYPE_VIDEO = "video";
public static final String STREAM_TYPE_SOFTWARE = "software";
public static final String ORDER_BY_EFFECTIVE_AMOUNT = "effective_amount";
public static final String ORDER_BY_RELEASE_TIME = "release_time";
public static final String ORDER_BY_TRENDING_GROUP = "trending_group";
public static final String ORDER_BY_TRENDING_MIXED = "trending_mixed";
public static final List<String> CLAIM_TYPES = Arrays.asList(TYPE_CHANNEL, TYPE_STREAM);
public static final List<String> STREAM_TYPES = Arrays.asList(
STREAM_TYPE_AUDIO, STREAM_TYPE_IMAGE, STREAM_TYPE_SOFTWARE, STREAM_TYPE_VIDEO
);
public static final String RELEASE_TIME_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
@EqualsAndHashCode.Include
private boolean placeholder;
private boolean placeholderAnonymous;
private boolean loadingPlaceholder;
private boolean featured;
private boolean unresolved; // used for featured
private String address;
private String amount;
private String canonicalUrl;
@EqualsAndHashCode.Include
private String claimId;
private int claimSequence;
private String claimOp;
private long confirmations;
private boolean decodedClaim;
private long timestamp;
private long height;
private boolean isMine;
private String name;
private String normalizedName;
private int nout;
private String permanentUrl;
private String shortUrl;
private String txid;
private String type; // claim | update | support
private String valueType; // stream | channel | repost
private Claim repostedClaim;
private Claim signingChannel;
private String repostChannelUrl;
private boolean isChannelSignatureValid;
private GenericMetadata value;
private LbryFile file; // associated file if it exists
// device it was viewed on (for view history)
private String device;
public static Claim claimFromOutput(JSONObject item) {
// we only need name, permanent_url, txid and nout
Claim claim = new Claim();
claim.setClaimId(Helper.getJSONString("claim_id", null, item));
claim.setName(Helper.getJSONString("name", null, item));
claim.setPermanentUrl(Helper.getJSONString("permanent_url", null, item));
claim.setTxid(Helper.getJSONString("txid", null, item));
claim.setNout(Helper.getJSONInt("nout", -1, item));
return claim;
}
public String getOutpoint() {
return String.format("%s:%d", txid, nout);
}
public boolean isFree() {
if (!(value instanceof StreamMetadata)) {
return true;
}
Fee fee = ((StreamMetadata) value).getFee();
return fee == null || Helper.parseDouble(fee.getAmount(), 0) == 0;
}
public BigDecimal getActualCost(double usdRate) {
if (!(value instanceof StreamMetadata)) {
return new BigDecimal(0);
}
Fee fee = ((StreamMetadata) value).getFee();
if (fee != null) {
double amount = Helper.parseDouble(fee.getAmount(), 0);
if ("usd".equalsIgnoreCase(fee.getCurrency())) {
return new BigDecimal(String.valueOf(amount / usdRate));
}
return new BigDecimal(String.valueOf(amount)); // deweys
}
return new BigDecimal(0);
}
public String getMediaType() {
if (value instanceof StreamMetadata) {
StreamMetadata metadata = (StreamMetadata) value;
String mediaType = metadata.getSource() != null ? metadata.getSource().getMediaType() : null;
return mediaType;
}
return null;
}
public boolean hasSource() {
if (value instanceof StreamMetadata) {
StreamMetadata metadata = (StreamMetadata) value;
return metadata.getSource() != null;
}
return false;
}
public boolean isPlayable() {
if (value instanceof StreamMetadata) {
StreamMetadata metadata = (StreamMetadata) value;
String mediaType = metadata.getSource() != null ? metadata.getSource().getMediaType() : null;
if (mediaType != null) {
return mediaType.startsWith("video") || mediaType.startsWith("audio");
}
}
return false;
}
public boolean isViewable() {
if (value instanceof StreamMetadata) {
StreamMetadata metadata = (StreamMetadata) value;
String mediaType = metadata.getSource() != null ? metadata.getSource().getMediaType() : null;
if (mediaType != null) {
return mediaType.startsWith("image") || mediaType.startsWith("text");
}
}
return false;
}
public boolean isMature() {
List<String> tags = getTags();
if (tags != null && tags.size() > 0) {
for (String tag : tags) {
if (Predefined.MATURE_TAGS.contains(tag.toLowerCase())) {
return true;
}
}
}
return false;
}
public String getThumbnailUrl() {
if (value != null && value.getThumbnail() != null) {
return value.getThumbnail().getUrl();
}
return null;
}
/**
* Gets the URL from the CDN where getting the image file
* @param width Pass zero for width and height for the full size image file
* @param height Pass zero for width and height for the full size image file
* @param q Desired quality for the image to be retrieved
* @return URL from the CDN from where image can be retrieved
*/
public String getThumbnailUrl(int width, int height, int q) {
if (value != null && value.getThumbnail() != null) {
ImageCDNUrl imageCDNUrl = new ImageCDNUrl(Math.max(width, 0), Math.max(height, 0), q, null, value.getThumbnail().getUrl());
return imageCDNUrl.toString();
}
return null;
}
public String getCoverUrl() {
if (TYPE_CHANNEL.equals(valueType) && value != null && value instanceof ChannelMetadata && ((ChannelMetadata) value).getCover() != null) {
return ((ChannelMetadata) value).getCover().getUrl();
}
return null;
}
public String getFirstCharacter() {
if (name != null) {
return name.startsWith("@") ? name.substring(1) : name;
}
return "";
}
public String getFirstTag() {
if (value != null && value.tags != null && value.tags.size() > 0) {
return value.tags.get(0);
}
return null;
}
public String getDescription() {
return (value != null) ? value.getDescription() : null;
}
public String getWebsiteUrl() {
return (value instanceof ChannelMetadata) ? ((ChannelMetadata) value).getWebsiteUrl() : null;
}
public String getEmail() {
return (value instanceof ChannelMetadata) ? ((ChannelMetadata) value).getEmail() : null;
}
public String getPublisherName() {
if (signingChannel != null) {
return signingChannel.getName();
}
return "Anonymous";
}
public String getPublisherTitle() {
if (signingChannel != null) {
return Helper.isNullOrEmpty(signingChannel.getTitle()) ? signingChannel.getName() : signingChannel.getTitle();
}
return "Anonymous";
}
public List<String> getTags() {
return (value != null && value.getTags() != null) ? new ArrayList<>(value.getTags()) : new ArrayList<>();
}
public List<Tag> getTagObjects() {
List<Tag> tags = new ArrayList<>();
if (value != null && value.getTags() != null) {
for (String value : value.getTags()) {
tags.add(new Tag(value));
}
}
return tags;
}
public String getTitle() {
return (value != null) ? value.getTitle() : null;
}
public String getTitleOrName() {
return (value != null) ? value.getTitle() : getName();
}
public long getDuration() {
if (value instanceof StreamMetadata) {
StreamMetadata metadata = (StreamMetadata) value;
if (STREAM_TYPE_VIDEO.equalsIgnoreCase(metadata.getStreamType()) && metadata.getVideo() != null) {
return metadata.getVideo().getDuration();
} else if (STREAM_TYPE_AUDIO.equalsIgnoreCase(metadata.getStreamType()) && metadata.getAudio() != null) {
return metadata.getAudio().getDuration();
}
}
return 0;
}
public static Claim fromViewHistory(ViewHistory viewHistory) {
// only for stream claims
Claim claim = new Claim();
claim.setClaimId(viewHistory.getClaimId());
claim.setName(viewHistory.getClaimName());
claim.setValueType(TYPE_STREAM);
claim.setPermanentUrl(viewHistory.getUri().toString());
claim.setDevice(viewHistory.getDevice());
claim.setConfirmations(1);
StreamMetadata value = new StreamMetadata();
value.setTitle(viewHistory.getTitle());
value.setReleaseTime(viewHistory.getReleaseTime());
if (!Helper.isNullOrEmpty(viewHistory.getThumbnailUrl())) {
Resource thumbnail = new Resource();
thumbnail.setUrl(viewHistory.getThumbnailUrl());
value.setThumbnail(thumbnail);
}
if (viewHistory.getCost() != null && viewHistory.getCost().doubleValue() > 0) {
Fee fee = new Fee();
fee.setAmount(String.valueOf(viewHistory.getCost().doubleValue()));
fee.setCurrency(viewHistory.getCurrency());
value.setFee(fee);
}
claim.setValue(value);
if (!Helper.isNullOrEmpty(viewHistory.getPublisherClaimId())) {
Claim signingChannel = new Claim();
signingChannel.setClaimId(viewHistory.getPublisherClaimId());
signingChannel.setName(viewHistory.getPublisherName());
LbryUri channelUrl = LbryUri.tryParse(String.format("%s#%s", signingChannel.getName(), signingChannel.getClaimId()));
signingChannel.setPermanentUrl(channelUrl != null ? channelUrl.toString() : null);
if (!Helper.isNullOrEmpty(viewHistory.getPublisherTitle())) {
GenericMetadata channelValue = new GenericMetadata();
channelValue.setTitle(viewHistory.getPublisherTitle());
signingChannel.setValue(channelValue);
}
claim.setSigningChannel(signingChannel);
}
return claim;
}
public static Claim fromJSONObject(JSONObject claimObject) {
Claim claim = null;
String claimJson = claimObject.toString();
Type type = new TypeToken<Claim>(){}.getType();
Type streamMetadataType = new TypeToken<StreamMetadata>(){}.getType();
Type channelMetadataType = new TypeToken<ChannelMetadata>(){}.getType();
Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
claim = gson.fromJson(claimJson, type);
try {
String valueType = claim.getValueType();
// Specific value type parsing
if (TYPE_REPOST.equalsIgnoreCase(valueType)) {
JSONObject repostedClaimObject = claimObject.getJSONObject("reposted_claim");
claim.setRepostedClaim(Claim.fromJSONObject(repostedClaimObject));
} else {
JSONObject value = claimObject.getJSONObject("value");
if (value != null) {
String valueJson = value.toString();
if (TYPE_STREAM.equalsIgnoreCase(valueType)) {
claim.setValue(gson.fromJson(valueJson, streamMetadataType));
} else if (TYPE_CHANNEL.equalsIgnoreCase(valueType)) {
claim.setValue(gson.fromJson(valueJson, channelMetadataType));
}
}
}
} catch (JSONException ex) {
// pass
}
return claim;
}
public static Claim fromSearchJSONObject(JSONObject searchResultObject) {
Claim claim = new Claim();
LbryUri claimUri = new LbryUri();
try {
claim.setClaimId(searchResultObject.getString("claimId"));
claim.setName(searchResultObject.getString("name"));
claim.setConfirmations(1);
if (claim.getName().startsWith("@")) {
claimUri.setChannelClaimId(claim.getClaimId());
claimUri.setChannelName(claim.getName());
claim.setValueType(TYPE_CHANNEL);
} else {
claimUri.setStreamClaimId(claim.getClaimId());
claimUri.setStreamName(claim.getName());
claim.setValueType(TYPE_STREAM);
}
int duration = searchResultObject.isNull("duration") ? 0 : searchResultObject.getInt("duration");
long feeAmount = searchResultObject.isNull("fee") ? 0 : searchResultObject.getLong("fee");
String releaseTimeString = !searchResultObject.isNull("release_time") ? searchResultObject.getString("release_time") : null;
long releaseTime = 0;
try {
releaseTime = Double.valueOf(new SimpleDateFormat(RELEASE_TIME_DATE_FORMAT).parse(releaseTimeString).getTime() / 1000.0).longValue();
} catch (ParseException ex) {
// pass
}
GenericMetadata metadata = (duration > 0 || releaseTime > 0 || feeAmount > 0) ? new StreamMetadata() : new GenericMetadata();
metadata.setTitle(searchResultObject.getString("title"));
if (metadata instanceof StreamMetadata) {
StreamInfo streamInfo = new StreamInfo();
if (duration > 0) {
// assume stream type video
((StreamMetadata) metadata).setStreamType(STREAM_TYPE_VIDEO);
streamInfo.setDuration(duration);
}
Fee fee = null;
if (feeAmount > 0) {
fee = new Fee();
fee.setAmount(String.valueOf(new BigDecimal(String.valueOf(feeAmount)).divide(new BigDecimal(100000000))));
fee.setCurrency("LBC");
}
((StreamMetadata) metadata).setFee(fee);
((StreamMetadata) metadata).setVideo(streamInfo);
((StreamMetadata) metadata).setReleaseTime(releaseTime);
}
claim.setValue(metadata);
if (!searchResultObject.isNull("thumbnail_url")) {
Resource thumbnail = new Resource();
thumbnail.setUrl(searchResultObject.getString("thumbnail_url"));
claim.getValue().setThumbnail(thumbnail);
}
if (!searchResultObject.isNull("channel_claim_id") && !searchResultObject.isNull("channel")) {
Claim signingChannel = new Claim();
signingChannel.setClaimId(searchResultObject.getString("channel_claim_id"));
signingChannel.setName(searchResultObject.getString("channel"));
LbryUri channelUri = new LbryUri();
channelUri.setChannelClaimId(signingChannel.getClaimId());
channelUri.setChannelName(signingChannel.getName());
signingChannel.setPermanentUrl(channelUri.toString());
claim.setSigningChannel(signingChannel);
}
} catch (JSONException ex) {
// pass
}
claim.setPermanentUrl(claimUri.toString());
return claim;
}
@Data
public static class Meta {
private long activationHeight;
private int claimsInChannel;
private int creationHeight;
private int creationTimestamp;
private String effectiveAmount;
private long expirationHeight;
private boolean isControlling;
private String supportAmount;
private int reposted;
private double trendingGlobal;
private double trendingGroup;
private double trendingLocal;
private double trendingMixed;
}
@Data
public static class GenericMetadata {
private String title;
private String description;
private Resource thumbnail;
private List<String> languages;
private List<String> tags;
private List<Location> locations;
}
@Data
@EqualsAndHashCode(callSuper = true)
public static class ChannelMetadata extends GenericMetadata {
private String publicKey;
private String publicKeyId;
private Resource cover;
private String email;
private String websiteUrl;
private List<String> featured;
}
@Data
@EqualsAndHashCode(callSuper = true)
public static class StreamMetadata extends GenericMetadata {
private String license;
private String licenseUrl;
private long releaseTime;
private String author;
private Fee fee;
private String streamType; // video | audio | image | software
private Source source;
private StreamInfo video;
private StreamInfo audio;
private StreamInfo image;
private StreamInfo software;
@Data
public static class Source {
private String sdHash;
private String mediaType;
private String hash;
private String name;
private long size;
}
}
// only support "url" for now
@Data
public static class Resource {
private String url;
}
/**
* Object to be instantiated. In order to get the URLto the CDN, call toString() on it
*/
static class ImageCDNUrl {
private String appendedPath = "";
public ImageCDNUrl(int width, int height, int quality, @Nullable String format, String thumbnailUrl) {
if (width != 0 && height != 0)
appendedPath = "s:".concat(String.valueOf(width)).concat(":").concat(String.valueOf(height)).concat("/");
appendedPath = appendedPath.concat("quality:").concat(String.valueOf(quality)).concat("/");
appendedPath = appendedPath.concat("plain/").concat(thumbnailUrl);
if (format != null)
appendedPath = appendedPath.concat("@").concat(format);
}
@Override
public String toString() {
String url = "https://image-processor.vanwanet.com/optimize/";
return url.concat(appendedPath);
}
}
@Data
public static class StreamInfo {
private long duration; // video / audio
private long height; // video / image
private long width; // video / image
private String os; // software
}
}

View file

@ -0,0 +1,68 @@
package io.lbry.browser.model;
import androidx.annotation.Nullable;
import io.lbry.browser.utils.Helper;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
/**
* Class to represent a key to check equality with another object
*/
@ToString
public class ClaimCacheKey {
@Getter
@Setter
private String claimId;
@Getter
@Setter
private String url;
public static ClaimCacheKey fromClaimShortUrl(Claim claim) {
ClaimCacheKey key = new ClaimCacheKey();
key.setUrl(claim.getShortUrl());
return key;
}
public static ClaimCacheKey fromClaimPermanentUrl(Claim claim) {
ClaimCacheKey key = new ClaimCacheKey();
key.setUrl(claim.getPermanentUrl());
return key;
}
public static ClaimCacheKey fromClaim(Claim claim) {
ClaimCacheKey key = new ClaimCacheKey();
key.setClaimId(claim.getClaimId());
key.setUrl(!Helper.isNullOrEmpty(claim.getShortUrl()) ? claim.getShortUrl() : claim.getPermanentUrl());
return key;
}
@Override
public boolean equals(@Nullable Object obj) {
if (!(obj instanceof ClaimCacheKey)) {
return false;
}
ClaimCacheKey key = (ClaimCacheKey) obj;
if (!Helper.isNullOrEmpty(claimId) && !Helper.isNullOrEmpty(key.getClaimId())) {
return claimId.equalsIgnoreCase(key.getClaimId());
}
if (!Helper.isNullOrEmpty(url) && !Helper.isNullOrEmpty(key.getUrl())) {
return url.equalsIgnoreCase(key.getUrl());
}
return false;
}
@Override
public int hashCode() {
if (!Helper.isNullOrEmpty(url)) {
return url.hashCode();
}
if (!Helper.isNullOrEmpty(claimId)) {
return claimId.hashCode();
}
return super.hashCode();
}
}

View file

@ -0,0 +1,22 @@
package io.lbry.browser.model;
import java.util.ArrayList;
import java.util.List;
import lombok.Getter;
public class ClaimSearchCacheValue {
@Getter
private final List<Claim> claims;
@Getter
private final long timestamp;
public ClaimSearchCacheValue(List<Claim> claims, long timestamp) {
this.claims = new ArrayList<>(claims);
this.timestamp = timestamp;
}
public boolean isExpired(long ttl) {
return System.currentTimeMillis() - timestamp > ttl;
}
}

View file

@ -0,0 +1,72 @@
package io.lbry.browser.model;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
import io.lbry.browser.utils.Helper;
import lombok.Data;
@Data
public class Comment implements Comparable<Comment> {
public static final int MAX_LENGTH = 2000;
private Claim poster;
private String claimId;
private List<Comment> replies;
private long timestamp;
private String channelId;
private String channelName, text, id, parentId;
public Comment(String channelId, String channelName, String text, String id, String parentId) {
this.channelId = channelId;
this.channelName = channelName;
this.text = text;
this.id = id;
this.parentId = parentId;
this.replies = new ArrayList<>();
}
public Comment() {
replies = new ArrayList<>();
}
public void addReply(Comment reply) {
if (replies == null) {
replies = new ArrayList<>();
}
if (!replies.contains(reply)) {
replies.add(reply);
}
}
public static Comment fromJSONObject(JSONObject jsonObject) {
try {
String parentId = null;
if (jsonObject.has("parent_id")) {
parentId = jsonObject.getString("parent_id");
}
Comment comment = new Comment(
Helper.getJSONString("channel_id", null, jsonObject),
jsonObject.getString("channel_name"),
jsonObject.getString("comment"),
jsonObject.getString("comment_id"),
parentId
);
comment.setClaimId(Helper.getJSONString("claim_id", null, jsonObject));
comment.setTimestamp(Helper.getJSONLong("timestamp", 0, jsonObject));
return comment;
} catch (JSONException ex) {
return null;
}
}
@Override
public int compareTo(Comment comment) {
return (int)(this.getTimestamp() - comment.getTimestamp());
}
}

View file

@ -0,0 +1,23 @@
package io.lbry.browser.model;
import lombok.Data;
@Data
public class EditorsChoiceItem {
private boolean header;
private String title;
private String parent;
private String description;
private String thumbnailUrl;
private String permanentUrl;
public static EditorsChoiceItem fromClaim(Claim claim) {
EditorsChoiceItem item = new EditorsChoiceItem();
item.setTitle(claim.getTitle());
item.setDescription(claim.getDescription());
item.setThumbnailUrl(claim.getThumbnailUrl());
item.setPermanentUrl(claim.getPermanentUrl());
return item;
}
}

View file

@ -0,0 +1,10 @@
package io.lbry.browser.model;
import lombok.Data;
@Data
public class Fee {
private String amount;
private String currency;
private String address;
}

View file

@ -0,0 +1,13 @@
package io.lbry.browser.model;
import lombok.Data;
@Data
public class GalleryItem {
private String id;
private String name;
private String filePath;
private String type;
private String thumbnailPath;
private long duration;
}

View file

@ -0,0 +1,16 @@
package io.lbry.browser.model;
import lombok.Data;
@Data
public class Language {
private final String code;
private final String name;
private final int stringResourceId;
public Language(String code, String name, int stringResourceId) {
this.code = code;
this.name = name;
this.stringResourceId = stringResourceId;
}
}

View file

@ -0,0 +1,92 @@
package io.lbry.browser.model;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import org.json.JSONObject;
import java.lang.reflect.Type;
import io.lbry.browser.utils.LbryUri;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class LbryFile {
private Claim.StreamMetadata metadata;
private long addedOn;
private int blobsCompleted;
private int blobsInStream;
private int blobsRemaining;
private String channelClaimId;
private String channelName;
@EqualsAndHashCode.Include
private String claimId;
private String claimName;
private boolean completed;
private String downloadDirectory;
private String downloadPath;
private String fileName;
private String key;
private String mimeType;
private int nout;
private String outpoint;
private int pointsPaid;
private String protobuf;
private String sdHash;
private String status;
private boolean stopped;
private String streamHash;
private String streamName;
private String streamingUrl;
private String suggestedFileName;
private long timestamp;
private long totalBytes;
private long totalBytesLowerBound;
private String txid;
private long writtenBytes;
private Claim generatedClaim;
public Claim getClaim() {
if (generatedClaim != null) {
return generatedClaim;
}
generatedClaim = new Claim();
generatedClaim.setValueType(Claim.TYPE_STREAM);
generatedClaim.setPermanentUrl(LbryUri.tryParse(String.format("%s#%s", claimName, claimId)).toString());
generatedClaim.setClaimId(claimId);
generatedClaim.setName(claimName);
generatedClaim.setValue(metadata);
generatedClaim.setConfirmations(1);
generatedClaim.setTxid(txid);
generatedClaim.setNout(nout);
generatedClaim.setFile(this);
if (channelClaimId != null) {
Claim signingChannel = new Claim();
signingChannel.setClaimId(channelClaimId);
signingChannel.setName(channelName);
signingChannel.setPermanentUrl(LbryUri.tryParse(String.format("%s#%s", claimName, claimId)).toString());
generatedClaim.setSigningChannel(signingChannel);
}
return generatedClaim;
}
public static LbryFile fromJSONObject(JSONObject fileObject) {
String fileJson = fileObject.toString();
Type type = new TypeToken<LbryFile>(){}.getType();
Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
LbryFile file = gson.fromJson(fileJson, type);
if (file.getMetadata() != null && file.getMetadata().getReleaseTime() == 0) {
file.getMetadata().setReleaseTime(file.getTimestamp());
}
return file;
}
}

View file

@ -0,0 +1,20 @@
package io.lbry.browser.model;
import lombok.Data;
@Data
public class License {
private final String name;
private String url;
private final int stringResourceId;
public License(String name, int stringResourceId) {
this.name = name;
this.stringResourceId = stringResourceId;
}
public License(String name, String url, int stringResourceId) {
this.name = name;
this.url = url;
this.stringResourceId = stringResourceId;
}
}

View file

@ -0,0 +1,13 @@
package io.lbry.browser.model;
import lombok.Data;
@Data
public class Location {
private double latitude;
private double longitude;
private String country;
private String state;
private String city;
private String code;
}

View file

@ -0,0 +1,67 @@
package io.lbry.browser.model;
import android.content.Context;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
@Data
public class NavMenuItem {
public static final int ID_GROUP_FIND_CONTENT = 100;
public static final int ID_GROUP_YOUR_CONTENT = 200;
public static final int ID_GROUP_WALLET = 300;
public static final int ID_GROUP_OTHER = 400;
// Find Content
public static final int ID_ITEM_FOLLOWING = 101;
public static final int ID_ITEM_EDITORS_CHOICE = 102;
public static final int ID_ITEM_ALL_CONTENT = 103;
public static final int ID_ITEM_SHUFFLE = 104;
// Your Content
public static final int ID_ITEM_CHANNELS = 201;
public static final int ID_ITEM_LIBRARY = 202;
public static final int ID_ITEM_PUBLISHES = 203;
public static final int ID_ITEM_NEW_PUBLISH = 204;
// Wallet
public static final int ID_ITEM_WALLET = 301;
public static final int ID_ITEM_REWARDS = 302;
public static final int ID_ITEM_INVITES = 303;
// Other
public static final int ID_ITEM_SETTINGS = 401;
public static final int ID_ITEM_ABOUT = 402;
private final Context context;
private final int id;
private boolean group;
private int icon;
private String title;
private String extraLabel;
private String name; // same as title, but only as en lang for events
private List<NavMenuItem> items;
public NavMenuItem(int id, int titleResourceId, boolean group, Context context) {
this.context = context;
this.id = id;
this.group = group;
if (titleResourceId > 0) {
this.title = context.getString(titleResourceId);
}
if (group) {
this.items = new ArrayList<>();
}
}
public NavMenuItem(int id, int iconStringId, int titleResourceId, String name, Context context) {
this.context = context;
this.id = id;
this.icon = iconStringId;
this.title = context.getString(titleResourceId);
this.name = name;
}
}

View file

@ -0,0 +1,11 @@
package io.lbry.browser.model;
public class StartupStage {
public final Integer stage;
public final Boolean stageDone;
public StartupStage(Integer stage, Boolean stageDone) {
this.stage = stage;
this.stageDone = stageDone;
}
}

View file

@ -0,0 +1,44 @@
package io.lbry.browser.model;
import java.util.Comparator;
import io.lbry.browser.utils.Predefined;
import lombok.Getter;
import lombok.Setter;
public class Tag implements Comparator<Tag> {
@Getter
@Setter
private String name;
@Getter
@Setter
private boolean followed;
public Tag() {
}
public Tag(String name) {
this.name = name;
}
public String getLowercaseName() {
return name.toLowerCase();
}
public boolean isMature() {
return Predefined.MATURE_TAGS.contains(name.toLowerCase());
}
public String toString() {
return getLowercaseName();
}
public boolean equals(Object o) {
return (o instanceof Tag) && ((Tag) o).getName().equalsIgnoreCase(name);
}
public int hashCode() {
return name.toLowerCase().hashCode();
}
public int compare(Tag a, Tag b) {
return a.getLowercaseName().compareToIgnoreCase(b.getLowercaseName());
}
}

View file

@ -0,0 +1,154 @@
package io.lbry.browser.model;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import io.lbry.browser.R;
import io.lbry.browser.exceptions.LbryUriException;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.LbryUri;
import lombok.Data;
@Data
public class Transaction {
private int confirmations;
private Date txDate;
private String date;
private String claim;
private String claimId;
private String txid;
private BigDecimal value;
private BigDecimal fee;
private long timestamp;
private int descriptionStringId;
private TransactionInfo abandonInfo;
private TransactionInfo claimInfo;
private TransactionInfo supportInfo;
private TransactionInfo updateInfo;
public LbryUri getClaimUrl() {
if (!Helper.isNullOrEmpty(claim) && !Helper.isNullOrEmpty(claimId)) {
try {
return LbryUri.parse(LbryUri.normalize(String.format("%s#%s", claim, claimId)));
} catch (LbryUriException ex) {
// pass
}
}
return null;
}
public static Transaction fromJSONObject(JSONObject jsonObject) {
Transaction transaction = new Transaction();
transaction.setConfirmations(Helper.getJSONInt("confirmations", -1, jsonObject));
transaction.setDate(Helper.getJSONString("date", null, jsonObject));
transaction.setTxid(Helper.getJSONString("txid", null, jsonObject));
transaction.setValue(new BigDecimal(Helper.getJSONString("value", "0", jsonObject)));
transaction.setFee(new BigDecimal(Helper.getJSONString("fee", "0", jsonObject)));
transaction.setTimestamp(Helper.getJSONLong("timestamp", 0, jsonObject) * 1000);
transaction.setTxDate(new Date(transaction.getTimestamp()));
int descStringId = -1;
TransactionInfo info = null;
List<TransactionInfo> infos = new ArrayList<>();
try {
if (jsonObject.has("abandon_info")) {
JSONArray array = jsonObject.getJSONArray("abandon_info");
if (array.length() > 0) {
if (array.length() == 1) {
info = TransactionInfo.fromJSONObject(array.getJSONObject(0));
descStringId = info.getBalanceDelta().doubleValue() == info.getAmount().doubleValue() ? R.string.unlock : R.string.abandon;
transaction.setAbandonInfo(info);
} else {
// multiple abandon infos (txo_spend unlock tip)
descStringId = R.string.unlock;
for (int i = 0; i < array.length(); i++) {
infos.add(TransactionInfo.fromJSONObject(array.getJSONObject(i)));
}
}
}
}
if (info == null && jsonObject.has("claim_info")) {
JSONArray array = jsonObject.getJSONArray("claim_info");
if (array.length() > 0) {
info = TransactionInfo.fromJSONObject(array.getJSONObject(0));
descStringId = info.getClaimName().startsWith("@") ? R.string.channel : R.string.publish;
transaction.setClaimInfo(info);
}
}
if (info == null && jsonObject.has("support_info")) {
JSONArray array = jsonObject.getJSONArray("support_info");
if (array.length() > 0) {
info = TransactionInfo.fromJSONObject(array.getJSONObject(0));
descStringId = info.isTip() ? R.string.tip : R.string.support;
transaction.setSupportInfo(info);
}
}
if (info == null && jsonObject.has("update_info")) {
JSONArray array = jsonObject.getJSONArray("update_info");
if (array.length() > 0) {
info = TransactionInfo.fromJSONObject(array.getJSONObject(0));
descStringId = info.getClaimName().startsWith("@") ? R.string.channel_update : R.string.publish_update;
transaction.setUpdateInfo(info);
}
}
if (info != null) {
transaction.setClaim(info.getClaimName());
transaction.setClaimId(info.getClaimId());
}
} catch (JSONException ex) {
// pass
}
if (transaction.getValue().doubleValue() == 0) {
if (info != null && info.getBalanceDelta().doubleValue() != 0) {
transaction.setValue(info.getBalanceDelta());
} else if (infos.size() > 0) {
BigDecimal total = new BigDecimal(0);
for (TransactionInfo txInfo : infos) {
total = total.add(txInfo.getAmount());
}
transaction.setValue(total);
}
}
if (descStringId == -1) {
descStringId = transaction.getValue().signum() == -1 || transaction.getFee().signum() == -1 ? R.string.spend : R.string.receive;
}
transaction.setDescriptionStringId(descStringId);
return transaction;
}
@Data
public static class TransactionInfo {
private String address;
private BigDecimal amount;
private BigDecimal balanceDelta;
private String claimId;
private String claimName;
private boolean isTip;
private int nout;
public static TransactionInfo fromJSONObject(JSONObject jsonObject) {
TransactionInfo info = new TransactionInfo();
info.setAddress(Helper.getJSONString("address", null, jsonObject));
info.setAmount(new BigDecimal(Helper.getJSONString("amount", "0", jsonObject)));
info.setBalanceDelta(new BigDecimal(Helper.getJSONString("balance_delta", "0", jsonObject)));
info.setClaimId(Helper.getJSONString("claim_id", null, jsonObject));
info.setClaimName(Helper.getJSONString("claim_name", null, jsonObject));
info.setTip(Helper.getJSONBoolean("is_tip", false, jsonObject));
info.setNout(Helper.getJSONInt("nout", -1, jsonObject));
return info;
}
}
}

View file

@ -0,0 +1,9 @@
package io.lbry.browser.model;
import lombok.Data;
@Data
public class TwitterOauth {
private String oauthToken;
private String oauthTokenSecret;
}

Some files were not shown because too many files have changed in this diff Show more