Compare commits
467 commits
Author | SHA1 | Date | |
---|---|---|---|
|
fdd759b241 | ||
|
57cd649c02 | ||
|
7250435d7f | ||
|
b81abf74a1 | ||
|
208e2c2d42 | ||
|
a5bdd1c042 | ||
|
1e3a74cae1 | ||
|
ca08f71a72 | ||
|
b60ca39df1 | ||
|
696bc86b7c | ||
|
4c163c6244 | ||
|
2ad49ca281 | ||
|
56caeef72b | ||
|
84bb014557 | ||
|
9278e74e85 | ||
|
cf6f09c60d | ||
|
f2cbed48d9 | ||
|
f1ead0c247 | ||
|
5f2c72ec4d | ||
|
a6869eb2e6 | ||
|
cbae6c476a | ||
|
48d257ceaf | ||
|
5355456498 | ||
|
1436895ace | ||
|
6e32f7724f | ||
|
3f5104d60a | ||
|
d8fdb3b818 | ||
|
5c187e7a8d | ||
|
24862550a1 | ||
|
146fced44e | ||
|
d535ed8c98 | ||
|
b675dbad9b | ||
|
dabe9fe691 | ||
|
9ac216504d | ||
|
911ca998e7 | ||
|
36c105d3a7 | ||
|
a655d0112b | ||
|
493c771e94 | ||
|
e9d70dbf87 | ||
|
0849ce2b66 | ||
|
d0a8b3b218 | ||
|
a8cdc4a771 | ||
|
67b883660f | ||
|
f328efb831 | ||
|
b2f56364d6 | ||
|
2761857fe8 | ||
|
d439260d69 | ||
|
f0f0a5028b | ||
|
13e170cb61 | ||
|
127d8052ca | ||
|
1b3086572f | ||
|
6fa308ef96 | ||
|
f3e513fc2d | ||
|
d72a8faec4 | ||
|
fdb4578349 | ||
|
c08237d399 | ||
|
bdf8a58818 | ||
|
e1d51c881a | ||
|
28212808f8 | ||
|
74f08f8f98 | ||
|
cb5c29fbce | ||
|
4d775d3a17 | ||
|
fd94ef9fa7 | ||
|
2e0331305a | ||
|
fb560f8f01 | ||
|
ef80c9f7fd | ||
|
99a4a0a22f | ||
|
136853853a | ||
|
ebf3299c30 | ||
|
036b49a871 | ||
|
67d5f88d34 | ||
|
b1e0b9af33 | ||
|
c30b012787 | ||
|
c8c6305757 | ||
|
ee1d090e62 | ||
|
9e56a86492 | ||
|
e6b83877f1 | ||
|
b567e39aef | ||
|
ae62dba0a6 | ||
|
df1e8abf50 | ||
|
31cfb26c3b | ||
|
e5f34dc464 | ||
|
601031e55d | ||
|
a9aadbe6a8 | ||
|
f9a4b71037 | ||
|
0f10e9dc1f | ||
|
0647deb06c | ||
|
b2f5fec293 | ||
|
daf4e5aca2 | ||
|
c1324efb41 | ||
|
6221de2d3c | ||
|
983bc68af2 | ||
|
f1b167693d | ||
|
68ac64b534 | ||
|
6819ae46f9 | ||
|
6c406c5a85 | ||
|
c179243d22 | ||
|
d0f5504c80 | ||
|
896c566a02 | ||
|
da9352cc68 | ||
|
b8d2375e20 | ||
|
dd52ff9d07 | ||
|
d9891f8a8a | ||
|
ea5fe6842d | ||
|
fc649187df | ||
|
b0f7c41885 | ||
|
45935717c8 | ||
|
ac5e666369 | ||
|
ffeb72a383 | ||
|
0ec09d751c | ||
|
b8e4fcff92 | ||
|
b79f0f4820 | ||
|
a1cd58a214 | ||
|
cfca8facbe | ||
|
066a0a099c | ||
|
afeee2e5df | ||
|
76036acfc7 | ||
|
897bfdaffd | ||
|
7475ae323c | ||
|
faf7f3ccbf | ||
|
4940d1ca33 | ||
|
3d48fa5741 | ||
|
f9887cffae | ||
|
6644907665 | ||
|
5f850685d6 | ||
|
5f9674e49c | ||
|
0d185c6db3 | ||
|
53d22dd22d | ||
|
ff8ffda3c6 | ||
|
64bd540322 | ||
|
919f9a48f3 | ||
|
746d442051 | ||
|
1877b75188 | ||
|
6e8c38cace | ||
|
eab7bab267 | ||
|
aab648c3cf | ||
|
f79ff509bd | ||
|
1197e990ca | ||
|
dce1c0715e | ||
|
6a0263c5bc | ||
|
ca64db0499 | ||
|
a5d4eda4d1 | ||
|
5d210961c1 | ||
|
1b88f565af | ||
|
991a98b571 | ||
|
0073277d6e | ||
|
eeca602f7a | ||
|
6c171560fd | ||
|
66c4c00215 | ||
|
9b9ef9ab74 | ||
|
39a289e7f1 | ||
|
722c829502 | ||
|
7ecb80d136 | ||
|
ea19af04d4 | ||
|
8196b69211 | ||
|
dc861caf6c | ||
|
adb5ffa8d0 | ||
|
d918cb28bd | ||
|
7ce7314ab9 | ||
|
13e5caa0ef | ||
|
ff59a7a89f | ||
|
c3efa4d004 | ||
|
ed50e1300a | ||
|
86dbfd54d1 | ||
|
535120eebd | ||
|
c1106d7186 | ||
|
45be7f2c9b | ||
|
d6baf3d1c8 | ||
|
08c38c1723 | ||
|
af4fa454d3 | ||
|
0620582a4e | ||
|
b11c07e3d1 | ||
|
593b34079c | ||
|
becb533624 | ||
|
8efc0522f2 | ||
|
cead924ca5 | ||
|
f83a043664 | ||
|
e8a0bca5ea | ||
|
1198c2298a | ||
|
7205acb9d6 | ||
|
d21cfd55ab | ||
|
47cbc3624c | ||
|
1f9a0886a0 | ||
|
dd5ea68915 | ||
|
b3bba4d273 | ||
|
10c123ba6a | ||
|
a8ba45b941 | ||
|
3652cdf6bc | ||
|
f2a7a8c439 | ||
|
78d5a99441 | ||
|
6931dbe79c | ||
|
4d024c06cc | ||
|
ddcb190457 | ||
|
dd14b90d7e | ||
|
4b86444478 | ||
|
feb7f260dc | ||
|
74c7a1de4d | ||
|
88a43dc679 | ||
|
1bb5ce72fa | ||
|
b4ce544965 | ||
|
1ec6f6173a | ||
|
8ee19b3f5c | ||
|
1055392b7f | ||
|
c772cf2ead | ||
|
481c50f465 | ||
|
71bc969f2a | ||
|
e4edebfed7 | ||
|
e8a5ab8307 | ||
|
3738f3af21 | ||
|
74d10e4199 | ||
|
080e00becf | ||
|
049d905d7d | ||
|
33094f8c88 | ||
|
6c44e503db | ||
|
fa78c80592 | ||
|
07630ca97b | ||
|
a2b6d4e570 | ||
|
9d6b3ddf81 | ||
|
0a2769859a | ||
|
c4a1ed801a | ||
|
453394bf1b | ||
|
4b7dfba5a1 | ||
|
e699fcf0b3 | ||
|
7e80f707e0 | ||
|
467f037170 | ||
|
89d5b40e5f | ||
|
34fba010e1 | ||
|
d7396bb044 | ||
|
fda0817ad1 | ||
|
895ca75506 | ||
|
a04448ebe8 | ||
|
26ccbf2709 | ||
|
7d6c11a88c | ||
|
8742913c33 | ||
|
8e6e0f0099 | ||
|
a4585de807 | ||
|
6c24749ad5 | ||
|
c73509a4ca | ||
|
673b85b2aa | ||
|
fe6e60cd67 | ||
|
8bcee90d68 | ||
|
084407e129 | ||
|
4a3db5ae20 | ||
|
b36813ebe6 | ||
|
cc1ddfa16e | ||
|
5f1775d478 | ||
|
288e35dd64 | ||
|
d4af28d1c5 | ||
|
37cf85a5f6 | ||
|
c2cbd76f49 | ||
|
3d874435c3 | ||
|
9ed8864f23 | ||
|
61263dd59d | ||
|
2707e135c6 | ||
|
f9de0d7937 | ||
|
f03d58d648 | ||
|
f49a3570e9 | ||
|
1d97f9008d | ||
|
e85ca9114c | ||
|
85ed2da518 | ||
|
dcc91f75cc | ||
|
a40c160ac6 | ||
|
e6861c8436 | ||
|
e6a5d97fb8 | ||
|
ddeb209d51 | ||
|
3283b3a607 | ||
|
83a41ca6ce | ||
|
6c63eb7d66 | ||
|
7cf9de8c2a | ||
|
a859682954 | ||
|
c844c4f896 | ||
|
a49cfe91da | ||
|
2bacba2c87 | ||
|
e473981063 | ||
|
34ea4f216c | ||
|
52e34b3027 | ||
|
c6a6ea0445 | ||
|
59ae6340e1 | ||
|
cbf2aa2311 | ||
|
94b0c7bc01 | ||
|
4c50ffac19 | ||
|
551e736651 | ||
|
0380e35966 | ||
|
5de0906fc1 | ||
|
c2f17b6230 | ||
|
6a3bbe6c0d | ||
|
82307c9f98 | ||
|
d2ec0e2aa1 | ||
|
3951b1080d | ||
|
ef7caeeead | ||
|
7ef8e2029e | ||
|
aaec6ac6a3 | ||
|
731960da7c | ||
|
ac282d2667 | ||
|
7a79601cfc | ||
|
d1167e4d2b | ||
|
613634adcf | ||
|
acbe33c66d | ||
|
053ebbd70b | ||
|
f5dc4fa4e7 | ||
|
343270b757 | ||
|
65f5626aba | ||
|
a051a73c2b | ||
|
6840997793 | ||
|
2c98ed2d8d | ||
|
6a083c4152 | ||
|
b60b5d16c3 | ||
|
52cfe8dc12 | ||
|
737afca031 | ||
|
456f41d28d | ||
|
238f59ad71 | ||
|
cf6d567193 | ||
|
20f8f3852d | ||
|
4bb9f5cb43 | ||
|
540a841255 | ||
|
3544637405 | ||
|
651448dfa1 | ||
|
4bac266b8b | ||
|
f86b61741d | ||
|
6c60d299af | ||
|
08582238c3 | ||
|
b931d0ce7d | ||
|
5e1fc9dbd8 | ||
|
de4e6eee25 | ||
|
d5b4e6990f | ||
|
26703815b2 | ||
|
40c36df414 | ||
|
6eb20d0e08 | ||
|
cc3055f1c9 | ||
|
25a1eac43b | ||
|
ff30e7f6a4 | ||
|
3fcc01597d | ||
|
cbc4a662e6 | ||
|
5aa0513324 | ||
|
4d1f142f9c | ||
|
e9c8f9432f | ||
|
56c375f344 | ||
|
5b165a2339 | ||
|
bfd3c711ab | ||
|
4627e284fa | ||
|
ea8ac783a8 | ||
|
966dd2bf2c | ||
|
364de592fa | ||
|
d38241f0d1 | ||
|
e217be6b67 | ||
|
37f84f0399 | ||
|
b618b9fa2b | ||
|
37b893103d | ||
|
e2d4852df7 | ||
|
1e0298d73a | ||
|
64c13295d6 | ||
|
0261ece709 | ||
|
a5799347ad | ||
|
6c7c4d1a88 | ||
|
f5390316a7 | ||
|
da1ea77aa9 | ||
|
8ea48977ae | ||
|
6721db54e2 | ||
|
6a7079c8b2 | ||
|
8cc7e9d6c9 | ||
|
d0ac41c242 | ||
|
bc8f476ef1 | ||
|
2d01b63cfe | ||
|
88dc31e9af | ||
|
8d1e5eb64d | ||
|
8113ef3f8a | ||
|
a74ed0e93b | ||
|
11f42bf71e | ||
|
8eb96025ec | ||
|
1ece7ff433 | ||
|
e312c451f8 | ||
|
bb64e91648 | ||
|
69398f6a2c | ||
|
bef7aa0ae9 | ||
|
2776696501 | ||
|
fa075df278 | ||
|
05311fd3a1 | ||
|
79001239d9 | ||
|
e344733bf0 | ||
|
282fdbb903 | ||
|
ee7c571101 | ||
|
87e42d6b7e | ||
|
8fcf135280 | ||
|
ae9364ad7e | ||
|
6aa44d8a9e | ||
|
3f013d1272 | ||
|
94d1f5a4eb | ||
|
1d8a7d838b | ||
|
c5d441fa1f | ||
|
4276a78a81 | ||
|
36c24a448f | ||
|
4bf1051549 | ||
|
fc4ab3128c | ||
|
65d32ea13e | ||
|
4cd8c80226 | ||
|
7adcdc43cf | ||
|
77852dc135 | ||
|
746ca62ca7 | ||
|
5e993f0fcc | ||
|
168dbba62c | ||
|
4db7bf814c | ||
|
21a3ff318b | ||
|
116de67d4c | ||
|
c6b8dfd539 | ||
|
37f33e9943 | ||
|
4bae7fdf94 | ||
|
c455bde154 | ||
|
5aa4321787 | ||
|
27a2100c72 | ||
|
4fc82c6527 | ||
|
56486ad274 | ||
|
99a38e655b | ||
|
262050147d | ||
|
5ff2e608c2 | ||
|
4e305faf5b | ||
|
0bdbb843fb | ||
|
c8736ad26e | ||
|
57d6b72f5a | ||
|
d0117b14db | ||
|
6729842ed8 | ||
|
723ade8ba6 | ||
|
83066232b6 | ||
|
4dc0b19c5d | ||
|
a58cc820ef | ||
|
2b3397bc85 | ||
|
a519b5c0b1 | ||
|
fb3a34c871 | ||
|
a1bc3fcd7e | ||
|
7daf3c6f55 | ||
|
7d032179ef | ||
|
156507f8f7 | ||
|
032a1927db | ||
|
f24bf570ef | ||
|
75c2ece016 | ||
|
0c1c1a6c79 | ||
|
5af25a9d43 | ||
|
55fec0f578 | ||
|
b61fc3a148 | ||
|
e3cd0fc2b1 | ||
|
fe01f50c65 | ||
|
cd378d16e3 | ||
|
e951fabf1c | ||
|
48543a48fb | ||
|
2699893581 | ||
|
21914a0bd7 | ||
|
d83667682d | ||
|
a5dc01f589 | ||
|
fb497cebfe | ||
|
9ffaba4358 | ||
|
be85307fe8 | ||
|
b55961bd83 | ||
|
f4911394ba | ||
|
42cf11358e | ||
|
6e6ed07890 | ||
|
22327bf33e | ||
|
57d8bc3311 | ||
|
6589799a2a | ||
|
7d6370163b | ||
|
9a5d63b7fc | ||
|
2d872a57ee | ||
|
f9c0850acd | ||
|
994ec732a4 | ||
|
16d2401e38 | ||
|
648d9103b7 | ||
|
faac434d33 | ||
|
f558cc49f7 | ||
|
15e4dca984 |
1903 changed files with 62973 additions and 196244 deletions
|
@ -1,3 +0,0 @@
|
||||||
.buildozer
|
|
||||||
.buildozer-downloads
|
|
||||||
.gradle
|
|
32
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
32
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
## PR Checklist
|
||||||
|
|
||||||
|
<!-- For the checkbox formatting to work properly, make sure there are no spaces on either side of the "x" -->
|
||||||
|
|
||||||
|
Please check all that apply to this PR using "x":
|
||||||
|
|
||||||
|
- [ ] I have checked that this PR is not a duplicate of an existing PR (open, closed or merged)
|
||||||
|
- [ ] I have checked that this PR does not introduce a breaking change
|
||||||
|
- [ ] This PR introduces breaking changes and I have provided a detailed explanation below
|
||||||
|
|
||||||
|
## PR Type
|
||||||
|
|
||||||
|
What kind of change does this PR introduce?
|
||||||
|
|
||||||
|
- [ ] Bugfix
|
||||||
|
- [ ] Feature
|
||||||
|
- [ ] Code style update (formatting)
|
||||||
|
- [ ] Refactoring (no functional changes)
|
||||||
|
- [ ] Documentation changes
|
||||||
|
- [ ] Other - Please describe:
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
Issue Number:
|
||||||
|
|
||||||
|
## What is the current behavior?
|
||||||
|
|
||||||
|
## What is the new behavior?
|
||||||
|
|
||||||
|
## Other information
|
||||||
|
|
||||||
|
<!-- If this PR contains a breaking change, please describe the impact and solution strategy for existing applications below. -->
|
1
.github/workflows/deploy.yml
vendored
Normal file
1
.github/workflows/deploy.yml
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
|
80
.gitignore
vendored
80
.gitignore
vendored
|
@ -1,17 +1,69 @@
|
||||||
.buildozer
|
# OSX
|
||||||
.buildozer-downloads
|
#
|
||||||
|
.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
|
.gradle
|
||||||
app/node_modules/
|
local.properties
|
||||||
bin
|
*.iml
|
||||||
buildozer.spec
|
|
||||||
build.log
|
# node.js
|
||||||
recipes/**/*.pyc
|
#
|
||||||
src/main/assets/index.android.bundle
|
node_modules/
|
||||||
src/main/assets/index.android.bundle.meta
|
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
|
*.log
|
||||||
.vagrant
|
.vagrant
|
||||||
|
*.hprof
|
||||||
lbry-android.keystore
|
app/build
|
||||||
p4a/pythonforandroid/bootstraps/lbry/build/templates/google-services.json
|
bin
|
||||||
.gitsecret/keys/random_seed
|
app/debuglib
|
||||||
|
|
||||||
|
|
|
@ -1,79 +1,48 @@
|
||||||
stages:
|
stages:
|
||||||
- build
|
- build
|
||||||
- build2
|
|
||||||
- deploy
|
- deploy
|
||||||
- release
|
- release
|
||||||
|
|
||||||
build arm64 apk:
|
|
||||||
|
build apk:
|
||||||
stage: build
|
stage: build
|
||||||
image: lbry/android-base:latest
|
image: lbry/android-base:platform-28
|
||||||
before_script:
|
before_script:
|
||||||
- export BUILD_VERSION=$(cat $CI_PROJECT_DIR/src/main/python/main.py | grep --color=never -oP '([0-9]+\.?)+')
|
- echo "$PGP_PRIVATE_KEY" | gpg --batch --import
|
||||||
- git submodule sync --recursive
|
- echo 'deb https://gitsecret.jfrog.io/artifactory/git-secret-deb git-secret main' >> /etc/apt/sources.list
|
||||||
- git submodule update --init --force --recursive
|
- 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:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
|
- bin/browser-*-release__arm.apk
|
||||||
- bin/browser-*-release__arm64.apk
|
- bin/browser-*-release__arm64.apk
|
||||||
expire_in: 1 week
|
expire_in: 1 week
|
||||||
script:
|
script:
|
||||||
- export PATH=/usr/bin:$PATH
|
- export PATH=/usr/bin:$PATH
|
||||||
- echo "$PGP_PRIVATE_KEY" | gpg --batch --import
|
- export ANDROID_SDK_ROOT=~/.buildozer/android/platform/android-sdk-23
|
||||||
- cd app
|
- chmod u+x ./release.sh
|
||||||
- npm install
|
- ./release.sh
|
||||||
- cd ..
|
- cp bin/browser-$BUILD_VERSION-release__arm.apk /dev/null
|
||||||
- wget -q 'https://eu.crystax.net/download/crystax-ndk-10.3.2-linux-x86_64.tar.xz' -P ~/.buildozer/android/
|
- cp bin/browser-$BUILD_VERSION-release__arm64.apk /dev/null
|
||||||
- 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
|
|
||||||
|
|
||||||
deploy build.lbry.io:
|
deploy build.lbry.io:
|
||||||
image: python:latest
|
image: python:stretch
|
||||||
stage: deploy
|
stage: deploy
|
||||||
dependencies:
|
dependencies:
|
||||||
- build arm apk
|
- build apk
|
||||||
- build arm64 apk
|
|
||||||
before_script:
|
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
|
- 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__32=browser-$BUILD_VERSION-release__arm.apk
|
||||||
- export BUILD_APK_FILENAME__64=browser-$BUILD_VERSION-release__arm64.apk
|
- export BUILD_APK_FILENAME__64=browser-$BUILD_VERSION-release__arm64.apk
|
||||||
script:
|
script:
|
||||||
|
@ -82,16 +51,22 @@ deploy build.lbry.io:
|
||||||
- aws s3 cp bin/$BUILD_APK_FILENAME__64 s3://build.lbry.io/android/push.apk
|
- aws s3 cp bin/$BUILD_APK_FILENAME__64 s3://build.lbry.io/android/push.apk
|
||||||
|
|
||||||
release apk:
|
release apk:
|
||||||
image: python:latest
|
image: python:stretch
|
||||||
stage: release
|
stage: release
|
||||||
only:
|
only:
|
||||||
- tags
|
- tags
|
||||||
dependencies:
|
dependencies:
|
||||||
- build arm apk
|
- build apk
|
||||||
- build arm64 apk
|
|
||||||
before_script:
|
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
|
- 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__32=browser-$BUILD_VERSION-release__arm.apk
|
||||||
- export BUILD_APK_FILENAME__64=browser-$BUILD_VERSION-release__arm64.apk
|
- export BUILD_APK_FILENAME__64=browser-$BUILD_VERSION-release__arm64.apk
|
||||||
script:
|
script:
|
||||||
|
|
4
.gitmodules
vendored
4
.gitmodules
vendored
|
@ -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
BIN
.gitsecret/keys/random_seed
Normal file
Binary file not shown.
|
@ -1,2 +1,3 @@
|
||||||
lbry-android.keystore:
|
lbry-android.keystore:0d958c531870694624cc877ea98ca1c583485f8ebbb3a5acca58b1930c190d65
|
||||||
p4a/pythonforandroid/bootstraps/lbry/build/templates/google-services.json
|
app/google-services.json:896a0bee8294a36d061f10fa926129d8a780528b34d0a2f03113400c4246d67c
|
||||||
|
app/twitter.properties:01212d70712f2041efb5c814bf30ecbf6f72e1ca5179c7647c4f8cbd995dd033
|
||||||
|
|
95
.travis.yml
95
.travis.yml
|
@ -1,95 +0,0 @@
|
||||||
sudo: required
|
|
||||||
dist: xenial
|
|
||||||
language: python
|
|
||||||
python:
|
|
||||||
- '3.7'
|
|
||||||
install:
|
|
||||||
- deactivate
|
|
||||||
- export PATH=/usr/bin:$PATH
|
|
||||||
- export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64
|
|
||||||
- sudo dpkg --add-architecture i386
|
|
||||||
- sudo add-apt-repository ppa:deadsnakes/ppa -y
|
|
||||||
- sudo apt-get -qq update
|
|
||||||
- sudo apt-get -qq install 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 openjdk-8-jdk unzip zlib1g-dev zlib1g:i386 m4 libc6-dev-i386
|
|
||||||
- sudo pip install --upgrade cython==0.28.1 pip setuptools
|
|
||||||
- wget -q https://nodejs.org/dist/v8.11.1/node-v8.11.1-linux-x64.tar.xz
|
|
||||||
- tar -xf node-v8.11.1-linux-x64.tar.xz
|
|
||||||
- sudo ln -s $TRAVIS_BUILD_DIR/node-v8.11.1-linux-x64/bin/node /usr/bin/node
|
|
||||||
- sudo ln -s $TRAVIS_BUILD_DIR/node-v8.11.1-linux-x64/bin/npm /usr/bin/npm
|
|
||||||
- git clone https://github.com/lbryio/buildozer.git
|
|
||||||
- cd app
|
|
||||||
- npm config set registry="http://registry.npmjs.org/"
|
|
||||||
- npm install
|
|
||||||
- sudo npm install -g react-native-cli
|
|
||||||
- sudo ln -s $TRAVIS_BUILD_DIR/node-v8.11.1-linux-x64/bin/react-native /usr/bin/react-native
|
|
||||||
- cd ..
|
|
||||||
- cd buildozer
|
|
||||||
- sudo python setup.py install
|
|
||||||
- cd ..
|
|
||||||
- mv buildozer.spec.travis buildozer.spec
|
|
||||||
- mkdir -p cd ~/.buildozer/android/platform/
|
|
||||||
- wget -q 'https://dist.testnet.lbry.tech/crystax-ndk-10.3.2-linux-x86_64.tar.xz' -P ~/.buildozer/android/
|
|
||||||
- wget -q 'https://dl.google.com/android/android-sdk_r23-linux.tgz' -P ~/.buildozer/android/platform/
|
|
||||||
- wget -q 'https://dl.google.com/android/repository/platform-27_r01.zip' -P ~/.buildozer/android/platform/
|
|
||||||
- wget -q 'https://dl.google.com/android/repository/build-tools_r26.0.2-linux.zip' -P ~/.buildozer/android/platform/
|
|
||||||
- tar -xf ~/.buildozer/android/crystax-ndk-10.3.2-linux-x86_64.tar.xz -C ~/.buildozer/android/
|
|
||||||
- cp -f $TRAVIS_BUILD_DIR/scripts/build-target-python.sh ~/.buildozer/android/crystax-ndk-10.3.2/build/tools/build-target-python.sh
|
|
||||||
- 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
|
|
||||||
- tar -xf ~/.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 -qq ~/.buildozer/android/platform/platform-27_r01.zip -d ~/.buildozer/android/platform/android-sdk-23/platforms
|
|
||||||
- mv ~/.buildozer/android/platform/android-sdk-23/platforms/android-8.1.0 ~/.buildozer/android/platform/android-sdk-23/platforms/android-27
|
|
||||||
- mkdir -p ~/.buildozer/android/platform/android-sdk-23/build-tools
|
|
||||||
- unzip -qq ~/.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
|
|
||||||
- mkdir -p ~/.buildozer/android/platform/android-sdk-23/licenses
|
|
||||||
- echo $'\nd56f5187479451eabf01fb78af6dfcb131a6481e' > ~/.buildozer/android/platform/android-sdk-23/licenses/android-sdk-license
|
|
||||||
- echo $'\n84831b9409646a918e30573bab4c9c91346d8abd' > ~/.buildozer/android/platform/android-sdk-23/licenses/android-sdk-preview-license
|
|
||||||
script:
|
|
||||||
- "./release.sh | grep -Fv -e 'working:' -e 'copy' -e 'Compiling' --line-buffered"
|
|
||||||
- cp $TRAVIS_BUILD_DIR/bin/*.apk /dev/null
|
|
||||||
before_deploy:
|
|
||||||
- cd $TRAVIS_BUILD_DIR/bin
|
|
||||||
- export BUILD_VERSION=$(cat ../src/main/python/main.py | grep --color=never -oP '([0-9]+\.?)+')
|
|
||||||
- cp browser-$BUILD_VERSION-release.apk latest.apk
|
|
||||||
deploy:
|
|
||||||
- provider: releases
|
|
||||||
api_key:
|
|
||||||
secure: m+FYX7vHZoiLSHHiJ2d3y8Fm4qSRoIVjEei+5BV17awiow/U8UKvy/5J1n8qfBdq+dpst5z58pTHCKWPbJz84C3z/posJ5mwEcOAaD/kxSAMHbtlaPW90pRWHUu3aW86UM/ggqtljE9Qz8KS/9a0xNUDfcXLkLgxuxgwodMcacEulAAc9TIOCUeR3IFI+KN0ptTCVahCu2JN8DCHKomaR+yKZHdo/9v9XCAcvmImSDu9nUDLH3+A7xQeRpPJqSspk1dadgdXP76kU8t3OKsYuM7DS5AoKvMIc9lZot4UYYKAx7/zavbzeEmqnyskULgsmV8/UDI1AV9U7uFBdrR6dSjISA1k6EHnCgqzasF+lp0hz5iE/0yPxlE9Z1kLW9gZgxSJtjr6Kv2uqAjHYYmpkjtTwHPwBugRM7PWMTxHNcPwkIHpBSRkXjpyDjkWd/LY4X866Y1g2BdIhbGshjy/9Fb2vnYxNZW6drLHn+wWeHJ41Vfgtg1cn01yZGJqgIkcTkhzNL6Bi++y8EBJXDr4L870s336SpbqRuIrO/C16ZFB+XnOg4Ty50Fk5zkbySMHII58iWqSyDYWNvhqo9zU9jn1XQQeok12129Y4t9TUOcJRbxhQ+511lCmVcFIkWHsXDK2QSZ7TeMK5GQUA8OvcNe+WLCJaQ/YD7OZvwlPTvc=
|
|
||||||
file_glob: true
|
|
||||||
file: browser-*-release.apk
|
|
||||||
overwrite: true
|
|
||||||
skip_cleanup: true
|
|
||||||
on:
|
|
||||||
tags: true
|
|
||||||
- provider: s3
|
|
||||||
access_key_id:
|
|
||||||
secure: qEZZ73DWBn9+M2pS4VwsyX8YZjZOENIMP/eoU1A9Vbn155oZpbUaJ7k+4cAXqmBm0WBMKZDqpzCRSGehLAxHFH5rjkj9gLSgd8fY9cveABHkl50HeuxxNsQM+ytk9sCtP8bWOqf7rT/iCgM1soyF2pYmfHM3tU9l0fWK8oZ7pFRIg91hXxUvhvYY2u6B9D1NqSN9T3xtrwEkVjvmkmyKMLCtoIdyB7QdeQciFdGFhZC9DYJVRLxa3BlzZ1T8Qv4MCyIxPjxIugNvVR64VgGjdBdq0BEIyoRqbeIvtqQjlnne4DfJFeCmbDrSva1wDP1UyFoxsRhiWQ/jXXgIyN2CisI6QRD0J+a2LgmbmtkUzhRMuVQJmBrIauulOzcowwRV2J4TtUaAJK9iSHT9D3RLzpazCOnjvJZV9CK5w252Vs5eHnisCSCQk8Ozox96Sg6XW580NEXfkYoGzXLSGiy9zrZs813blUjssEY+jIQmJEby80C3guK7/G4lzthv57psqBWcYd0tFR+vTestS+EFlC02ToUngJhW7I7lPA2G2yrJ5319jFxUSniijb1n72TQthnbqTBahepvKvuG1iWZnCKxS5sWkutFoqEcpQyhXdf7QdR/VrOr8N5xrhK1B8dCYZM6h8eMZbBvOLH49+N6L9jiJz5x+Lk32wcssv1oOgI=
|
|
||||||
secret_access_key:
|
|
||||||
secure: lPygaaJdjFgWY4GcXUXC4Oc5op/TE85Md8lX2bzW19058lbcqYSdM0WySQCxoU/4rlM4Q0N8du0qQ3kZXDpP9XSqvFTVnTGTuB4yghUR1yXcpt6u3JOeOX+YAc3wyQ/pmod6VGO0n8pm8hBVsSFXufdBTjD2W+tNrDoa8RYnlWrt7BbICGltB7PcqYh1Qw6S1wDyZt8I4B5JHDhyJmX6FT5KfOb5cJyynpxlKUstUfy1rh81KuGkEcuEVOLg1s7HE1/IUkVIgezAuCrMHjc86qbNcHULJMFCVYntvvs07+tctrPxA/cfS24WkW7smyij+gdZAZWNNgkIDCuwqpex1v1nKn56mC8xXyUl4CnSCuubQtqUBzTmd4T5sF7trTtpVr9NInwy+4mUoCpz2UKZekTjZkqpzCAuC/cBVWE1/k3wsNat6dGyc9QnKXBqLVhuwYsCOteqLW50ToMMMW0ccDV6FXodwZmrunGd5wIX+UgZkf4l32vzKUxHtIupfYbsjylcPc3VO0OzMMKP/3sYLAN6QntVDFc30k1uqqpgJN4t0nV7vvjMI+b0Qr+o7GzUV2d+QulQXOySJgB2pH0kV1EoPAJ8KbqDOy8KgCJl0YIbOaz14+SiRQhotJ2hrLdtsvyVYXMX+d/CKHJSWa2MQq+jD7lMCwVaGg82PFN1gI4=
|
|
||||||
bucket: "build.lbry.io"
|
|
||||||
upload-dir: android
|
|
||||||
region: us-east-1
|
|
||||||
overwrite: true
|
|
||||||
skip_cleanup: true
|
|
||||||
on:
|
|
||||||
tags: true
|
|
||||||
- provider: s3
|
|
||||||
access_key_id:
|
|
||||||
secure: qEZZ73DWBn9+M2pS4VwsyX8YZjZOENIMP/eoU1A9Vbn155oZpbUaJ7k+4cAXqmBm0WBMKZDqpzCRSGehLAxHFH5rjkj9gLSgd8fY9cveABHkl50HeuxxNsQM+ytk9sCtP8bWOqf7rT/iCgM1soyF2pYmfHM3tU9l0fWK8oZ7pFRIg91hXxUvhvYY2u6B9D1NqSN9T3xtrwEkVjvmkmyKMLCtoIdyB7QdeQciFdGFhZC9DYJVRLxa3BlzZ1T8Qv4MCyIxPjxIugNvVR64VgGjdBdq0BEIyoRqbeIvtqQjlnne4DfJFeCmbDrSva1wDP1UyFoxsRhiWQ/jXXgIyN2CisI6QRD0J+a2LgmbmtkUzhRMuVQJmBrIauulOzcowwRV2J4TtUaAJK9iSHT9D3RLzpazCOnjvJZV9CK5w252Vs5eHnisCSCQk8Ozox96Sg6XW580NEXfkYoGzXLSGiy9zrZs813blUjssEY+jIQmJEby80C3guK7/G4lzthv57psqBWcYd0tFR+vTestS+EFlC02ToUngJhW7I7lPA2G2yrJ5319jFxUSniijb1n72TQthnbqTBahepvKvuG1iWZnCKxS5sWkutFoqEcpQyhXdf7QdR/VrOr8N5xrhK1B8dCYZM6h8eMZbBvOLH49+N6L9jiJz5x+Lk32wcssv1oOgI=
|
|
||||||
secret_access_key:
|
|
||||||
secure: lPygaaJdjFgWY4GcXUXC4Oc5op/TE85Md8lX2bzW19058lbcqYSdM0WySQCxoU/4rlM4Q0N8du0qQ3kZXDpP9XSqvFTVnTGTuB4yghUR1yXcpt6u3JOeOX+YAc3wyQ/pmod6VGO0n8pm8hBVsSFXufdBTjD2W+tNrDoa8RYnlWrt7BbICGltB7PcqYh1Qw6S1wDyZt8I4B5JHDhyJmX6FT5KfOb5cJyynpxlKUstUfy1rh81KuGkEcuEVOLg1s7HE1/IUkVIgezAuCrMHjc86qbNcHULJMFCVYntvvs07+tctrPxA/cfS24WkW7smyij+gdZAZWNNgkIDCuwqpex1v1nKn56mC8xXyUl4CnSCuubQtqUBzTmd4T5sF7trTtpVr9NInwy+4mUoCpz2UKZekTjZkqpzCAuC/cBVWE1/k3wsNat6dGyc9QnKXBqLVhuwYsCOteqLW50ToMMMW0ccDV6FXodwZmrunGd5wIX+UgZkf4l32vzKUxHtIupfYbsjylcPc3VO0OzMMKP/3sYLAN6QntVDFc30k1uqqpgJN4t0nV7vvjMI+b0Qr+o7GzUV2d+QulQXOySJgB2pH0kV1EoPAJ8KbqDOy8KgCJl0YIbOaz14+SiRQhotJ2hrLdtsvyVYXMX+d/CKHJSWa2MQq+jD7lMCwVaGg82PFN1gI4=
|
|
||||||
bucket: "build.lbry.io"
|
|
||||||
upload-dir: "android/build-${TRAVIS_BUILD_NUMBER}_commit-${TRAVIS_COMMIT:0:7}"
|
|
||||||
region: us-east-1
|
|
||||||
overwrite: true
|
|
||||||
skip_cleanup: true
|
|
||||||
on:
|
|
||||||
all_branches: true
|
|
||||||
env:
|
|
||||||
global:
|
|
||||||
- secure: GS3Cp1QXiX8UPye3kdk2A2f3iFRr02sHKpY+RE+Zvx3Q7GDmhDuepHKzx6Hq5Os5fZN9Y/Bdds+XH+vLIRtT6XsWR7AONPhSifVY3XB5/2F+lDcZ538W8P8GZvXejpY4VecMUWHoWbuyt0s3PpaGXZJcHp8ir+CUJ0NUmU3I9w449pqj9/de2LHtG3qKH1lG0Xz58iOC0mmEeH451cQv3dDw851ihA4ak9vCTV1KKuMJUcv+2u6PxXGVX0mrJLEssjL6ze6G5iZUB4PM1vUpe3HqcVw8CSOa8O79BQxoB00qyA3WD+LpZDPpI0wh6gmBsR/2nCFyMJndJr3CjyB6lHdK7PgBoK0CJjszKawiZqg74O9DOjzTJTO2v9bnkfPrNxu4/3D/tbDg+whY8k5oV1sgDue9KAo/2aEEO0LGlKP4W3Qqt/lzRKsfpMVrMTdCNKJ8rG/wUFWw8ehOCmAsJaQ1saDOZDMNPLLuYpxFgmXFqWV5ThbUHgEJVj+G7qt6CMEussKvuZJoJZx24Pdk5Prr7ENzTyPmE5gk4b8WNfVNleOEC09xu5tFk2yOdzF1dawKsa1Mog6gImirTQ/INC/3BANdKoG9/cLJEIt9boJaFDXE1dpqoLVzoez9znHKOGSAU/1PaH3thjVnbUyO5z24PpPZ12zM3+3P8DbI454=
|
|
||||||
before_install:
|
|
||||||
- openssl aes-256-cbc -K $encrypted_b4c9b905b12e_key -iv $encrypted_b4c9b905b12e_iv
|
|
||||||
-in lbry-android.keystore.enc -out lbry-android.keystore -d
|
|
137
BUILD.md
137
BUILD.md
|
@ -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/templates/google-services.sample.json p4a/pythonforandroid/bootstraps/lbry/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`
|
|
156
DOCKER-DEV.md
156
DOCKER-DEV.md
|
@ -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.
|
|
||||||
|
|
||||||
|
|
71
DOCKER.md
71
DOCKER.md
|
@ -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.
|
|
32
Dockerfile
32
Dockerfile
|
@ -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"]
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
||||||
The MIT License (MIT)
|
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:
|
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:
|
||||||
|
|
||||||
|
|
113
QUICKSTART.md
113
QUICKSTART.md
|
@ -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
|
|
||||||
* 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 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 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, 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, 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
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
**Protip:** You can also make use of Docker to run your builds on macOS or Windows.
|
|
24
README.md
24
README.md
|
@ -2,9 +2,11 @@
|
||||||
[![pipeline status](https://ci.lbry.tech/lbry/lbry-android/badges/master/pipeline.svg)](https://ci.lbry.tech/lbry/lbry-android/commits/master)
|
[![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)
|
[![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" />
|
||||||
|
|
||||||
<img src="https://spee.ch/8/lbry-android.png" alt="LBRY Android Screenshot" width="384px" />
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
The minimum supported Android version is 5.0 Lollipop. There are two ways to install:
|
The minimum supported Android version is 5.0 Lollipop. There are two ways to install:
|
||||||
|
@ -13,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.
|
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
|
## 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
|
## 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
|
## Contributing
|
||||||
Contributions to this project are welcome, encouraged, and compensated. For more details, see https://lbry.io/faq/contributing
|
Contributions to this project are welcome, encouraged, and compensated. For more details, see https://lbry.io/faq/contributing
|
||||||
|
@ -25,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).
|
This project is MIT licensed. For the full license, see [LICENSE](LICENSE).
|
||||||
|
|
||||||
## Security
|
## 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
|
## Contact
|
||||||
The primary contact for this project is [@akinwale](https://github.com/akinwale) (akinwale@lbry.com)
|
The primary contact for this project is [@akinwale](https://github.com/akinwale) (akinwale@lbry.com)
|
||||||
|
|
100
Vagrantfile
vendored
100
Vagrantfile
vendored
|
@ -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
app
|
@ -1 +0,0 @@
|
||||||
Subproject commit 69a760f011368c696407d08dc09051f93022f9c4
|
|
1
app/.gitignore
vendored
Normal file
1
app/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/build
|
144
app/build.gradle
Normal file
144
app/build.gradle
Normal 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
|
BIN
app/google-services.json.secret
Normal file
BIN
app/google-services.json.secret
Normal file
Binary file not shown.
|
@ -37,4 +37,5 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"configuration_version": "1"
|
"configuration_version": "1"
|
||||||
}
|
}
|
||||||
|
|
21
app/proguard-rules.pro
vendored
Normal file
21
app/proguard-rules.pro
vendored
Normal 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
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
||||||
|
}
|
127
app/src/main/AndroidManifest.xml
Normal file
127
app/src/main/AndroidManifest.xml
Normal 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>
|
BIN
app/src/main/assets/font_awesome_5_free_regular.otf
Normal file
BIN
app/src/main/assets/font_awesome_5_free_regular.otf
Normal file
Binary file not shown.
BIN
app/src/main/assets/font_awesome_5_free_solid.otf
Normal file
BIN
app/src/main/assets/font_awesome_5_free_solid.otf
Normal file
Binary file not shown.
BIN
app/src/main/ic_launcher-playstore.png
Normal file
BIN
app/src/main/ic_launcher-playstore.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
252
app/src/main/java/io/lbry/browser/FirstRunActivity.java
Normal file
252
app/src/main/java/io/lbry/browser/FirstRunActivity.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
191
app/src/main/java/io/lbry/browser/LbrynetMessagingService.java
Normal file
191
app/src/main/java/io/lbry/browser/LbrynetMessagingService.java
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
package io.lbry.browser;
|
||||||
|
|
||||||
|
import android.app.NotificationChannel;
|
||||||
|
import android.app.NotificationManager;
|
||||||
|
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 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.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) {
|
||||||
|
if (firebaseAnalytics == null) {
|
||||||
|
firebaseAnalytics = FirebaseAnalytics.getInstance(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> payload = remoteMessage.getData();
|
||||||
|
if (payload != null) {
|
||||||
|
String type = payload.get("type");
|
||||||
|
String url = payload.get("target");
|
||||||
|
String title = payload.get("title");
|
||||||
|
String body = payload.get("body");
|
||||||
|
String name = payload.get("name"); // notification name
|
||||||
|
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(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);
|
||||||
|
|
||||||
|
// If you want to send messages to this application instance or
|
||||||
|
// manage this apps subscriptions on the server side, send the
|
||||||
|
// Instance ID token to your app server.
|
||||||
|
sendRegistrationToServer(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persist token to third-party servers.
|
||||||
|
*
|
||||||
|
* Modify this method to associate the user's FCM InstanceID token with any server-side account
|
||||||
|
* maintained by your application.
|
||||||
|
*
|
||||||
|
* @param token The new token.
|
||||||
|
*/
|
||||||
|
private void sendRegistrationToServer(String token) {
|
||||||
|
// 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.
|
||||||
|
*/
|
||||||
|
private void sendNotification(String title, String messageBody, String type, String url, String name) {
|
||||||
|
//Intent intent = new Intent(this, MainActivity.class);
|
||||||
|
//intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||||
|
if (url == null) {
|
||||||
|
if (TYPE_REWARD.equals(type)) {
|
||||||
|
url = "lbry://?rewards";
|
||||||
|
} else {
|
||||||
|
// default to home page
|
||||||
|
url = "lbry://?discover";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Intent launchIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||||
|
launchIntent.putExtra("notification_name", name);
|
||||||
|
launchIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||||
|
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, launchIntent, PendingIntent.FLAG_ONE_SHOT);
|
||||||
|
|
||||||
|
Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
|
||||||
|
NotificationCompat.Builder notificationBuilder =
|
||||||
|
new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
|
||||||
|
.setColor(ContextCompat.getColor(this, R.color.lbryGreen))
|
||||||
|
.setSmallIcon(R.drawable.ic_lbry)
|
||||||
|
.setContentTitle(title)
|
||||||
|
.setContentText(messageBody)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setSound(defaultSoundUri)
|
||||||
|
.setContentIntent(pendingIntent);
|
||||||
|
|
||||||
|
NotificationManager notificationManager =
|
||||||
|
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
notificationManager.createNotificationChannel(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationManager.notify(3, notificationBuilder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getEnabledTypes() {
|
||||||
|
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
|
||||||
|
List<String> enabledTypes = new ArrayList<String>();
|
||||||
|
|
||||||
|
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(MainActivity.PREFERENCE_KEY_NOTIFICATION_REWARDS, true)) {
|
||||||
|
enabledTypes.add(TYPE_REWARD);
|
||||||
|
}
|
||||||
|
if (sp.getBoolean(MainActivity.PREFERENCE_KEY_NOTIFICATION_CONTENT_INTERESTS, true)) {
|
||||||
|
enabledTypes.add(TYPE_INTERESTS);
|
||||||
|
}
|
||||||
|
if (sp.getBoolean(MainActivity.PREFERENCE_KEY_NOTIFICATION_CREATOR, true)) {
|
||||||
|
enabledTypes.add(TYPE_CREATOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
return enabledTypes;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
package io.lbry.browser;
|
package io.lbry.browser;
|
||||||
|
|
||||||
import android.support.v4.content.FileProvider;
|
import androidx.core.content.FileProvider;
|
||||||
|
|
||||||
public class LocalFileProvider extends FileProvider {
|
public class LocalFileProvider extends FileProvider {
|
||||||
|
|
3882
app/src/main/java/io/lbry/browser/MainActivity.java
Normal file
3882
app/src/main/java/io/lbry/browser/MainActivity.java
Normal file
File diff suppressed because it is too large
Load diff
469
app/src/main/java/io/lbry/browser/VerificationActivity.java
Normal file
469
app/src/main/java/io/lbry/browser/VerificationActivity.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
525
app/src/main/java/io/lbry/browser/adapter/ClaimListAdapter.java
Normal file
525
app/src/main/java/io/lbry/browser/adapter/ClaimListAdapter.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
220
app/src/main/java/io/lbry/browser/adapter/RewardListAdapter.java
Normal file
220
app/src/main/java/io/lbry/browser/adapter/RewardListAdapter.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
109
app/src/main/java/io/lbry/browser/adapter/TagListAdapter.java
Normal file
109
app/src/main/java/io/lbry/browser/adapter/TagListAdapter.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
459
app/src/main/java/io/lbry/browser/data/DatabaseHelper.java
Normal file
459
app/src/main/java/io/lbry/browser/data/DatabaseHelper.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
package io.lbry.browser.exceptions;
|
||||||
|
|
||||||
|
public class AuthTokenInvalidatedException extends Exception {
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package io.lbry.browser.listener;
|
||||||
|
|
||||||
|
public interface CameraPermissionListener {
|
||||||
|
void onCameraPermissionGranted();
|
||||||
|
void onCameraPermissionRefused();
|
||||||
|
void onRecordAudioPermissionGranted();
|
||||||
|
void onRecordAudioPermissionRefused();
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package io.lbry.browser.listener;
|
||||||
|
|
||||||
|
public interface DownloadActionListener {
|
||||||
|
void onDownloadAction(String downloadAction, String uri, String outpoint, String fileInfoJson, double progress);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package io.lbry.browser.listener;
|
||||||
|
|
||||||
|
public interface FilePickerListener {
|
||||||
|
void onFilePicked(String filePath);
|
||||||
|
void onFilePickerCancelled();
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package io.lbry.browser.listener;
|
||||||
|
|
||||||
|
public interface PIPModeListener {
|
||||||
|
void onEnterPIPMode();
|
||||||
|
void onExitPIPMode();
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package io.lbry.browser.listener;
|
||||||
|
|
||||||
|
public interface ScreenOrientationListener {
|
||||||
|
void onPortraitOrientationEntered();
|
||||||
|
void onLandscapeOrientationEntered();
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package io.lbry.browser.listener;
|
||||||
|
|
||||||
|
public interface SdkStatusListener {
|
||||||
|
void onSdkReady();
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package io.lbry.browser.listener;
|
||||||
|
|
||||||
|
public interface SelectionModeListener {
|
||||||
|
void onEnterSelectionMode();
|
||||||
|
void onExitSelectionMode();
|
||||||
|
void onItemSelectionToggled();
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package io.lbry.browser.listener;
|
||||||
|
|
||||||
|
public interface StoragePermissionListener {
|
||||||
|
void onStoragePermissionGranted();
|
||||||
|
void onStoragePermissionRefused();
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package io.lbry.browser.listener;
|
||||||
|
|
||||||
|
import io.lbry.browser.model.WalletBalance;
|
||||||
|
|
||||||
|
public interface WalletBalanceListener {
|
||||||
|
void onWalletBalanceUpdated(WalletBalance walletBalance);
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package io.lbry.browser.listener;
|
||||||
|
|
||||||
|
public interface WalletSyncListener {
|
||||||
|
void onWalletSyncProcessing();
|
||||||
|
void onWalletSyncWaitingForInput();
|
||||||
|
void onWalletSyncEnabled();
|
||||||
|
void onWalletSyncFailed(Exception error);
|
||||||
|
}
|
542
app/src/main/java/io/lbry/browser/model/Claim.java
Normal file
542
app/src/main/java/io/lbry/browser/model/Claim.java
Normal 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
|
||||||
|
}
|
||||||
|
}
|
68
app/src/main/java/io/lbry/browser/model/ClaimCacheKey.java
Normal file
68
app/src/main/java/io/lbry/browser/model/ClaimCacheKey.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
72
app/src/main/java/io/lbry/browser/model/Comment.java
Normal file
72
app/src/main/java/io/lbry/browser/model/Comment.java
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
10
app/src/main/java/io/lbry/browser/model/Fee.java
Normal file
10
app/src/main/java/io/lbry/browser/model/Fee.java
Normal 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;
|
||||||
|
}
|
13
app/src/main/java/io/lbry/browser/model/GalleryItem.java
Normal file
13
app/src/main/java/io/lbry/browser/model/GalleryItem.java
Normal 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;
|
||||||
|
}
|
16
app/src/main/java/io/lbry/browser/model/Language.java
Normal file
16
app/src/main/java/io/lbry/browser/model/Language.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
92
app/src/main/java/io/lbry/browser/model/LbryFile.java
Normal file
92
app/src/main/java/io/lbry/browser/model/LbryFile.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
20
app/src/main/java/io/lbry/browser/model/License.java
Normal file
20
app/src/main/java/io/lbry/browser/model/License.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
13
app/src/main/java/io/lbry/browser/model/Location.java
Normal file
13
app/src/main/java/io/lbry/browser/model/Location.java
Normal 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;
|
||||||
|
}
|
67
app/src/main/java/io/lbry/browser/model/NavMenuItem.java
Normal file
67
app/src/main/java/io/lbry/browser/model/NavMenuItem.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
11
app/src/main/java/io/lbry/browser/model/StartupStage.java
Normal file
11
app/src/main/java/io/lbry/browser/model/StartupStage.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
44
app/src/main/java/io/lbry/browser/model/Tag.java
Normal file
44
app/src/main/java/io/lbry/browser/model/Tag.java
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue