mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Compare commits
340 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 916d052688 | |||
| 993b812c3b | |||
| 43887586b5 | |||
| 03d53f21a3 | |||
| 23d7e8e5b6 | |||
| cce117c585 | |||
| 303bd1b805 | |||
| c7f4a40342 | |||
| 208c6c0776 | |||
| 7d5c8347ce | |||
| bd70131252 | |||
| 43a373eceb | |||
| 5bb3466ffe | |||
| 75e97ed008 | |||
| ee28604c11 | |||
| a7d89e1bfb | |||
| cbfd9ea559 | |||
| dae50c3bc3 | |||
| e651e59dc4 | |||
| 80d78761bf | |||
| fb85aa4f32 | |||
| 9635c95efe | |||
| 033a237488 | |||
| ec22c58822 | |||
| 274942b5ba | |||
| 94ab3da0e4 | |||
| 5d44f0f2b6 | |||
| f051e6b452 | |||
| 46a4284253 | |||
| 0a708c6892 | |||
| 0f96164dc3 | |||
| 91c4917021 | |||
| c32ebe016b | |||
| ea26eefc2d | |||
| 418f4a6075 | |||
| 0ec921709a | |||
| e0811cfd93 | |||
| f6b0778eb6 | |||
| 18aec34c0e | |||
| bd185776e7 | |||
| fca5fe38bb | |||
| 1c2c7b376d | |||
| 670df86114 | |||
| 55fb4d4562 | |||
| c703d018bd | |||
| 425a27e130 | |||
| bd1b0e875b | |||
| 1509c11f64 | |||
| 57c1097fbc | |||
| 1d1728b92b | |||
| 5f9f6dbde8 | |||
| cc3639180b | |||
| 8aa4de7522 | |||
| ed1f7e7c72 | |||
| 1ecd1f5e04 | |||
| 1aa9adc899 | |||
| f8b2da93b9 | |||
| b794ff47ef | |||
| 6962a0547a | |||
| b906c1d36b | |||
| af337b1874 | |||
| 542235cca0 | |||
| f5673425b7 | |||
| 94965cf3ba | |||
| 120ded5274 | |||
| 705eb6a3fa | |||
| 1eb62b31d2 | |||
| b145187fa8 | |||
| 4da1e44fd1 | |||
| 4e70279982 | |||
| 233c8ee26e | |||
| 875adb4d79 | |||
| 456514c4d4 | |||
| dac1918b95 | |||
| 1d7429ad86 | |||
| 5d0e6615ab | |||
| dc415df8c0 | |||
| 45ce251c4c | |||
| 2bc702112f | |||
| abd73bf797 | |||
| e7e67b9572 | |||
| 1a58b693c1 | |||
| 50ecb909b4 | |||
| 5e480be8db | |||
| 48a67e51a6 | |||
| 5052bad824 | |||
| 5be92052bb | |||
| e20945692e | |||
| 191a6e2460 | |||
| c813fb4fad | |||
| bf7001b578 | |||
| 18102a2a73 | |||
| 780c1dbde1 | |||
| 879aab0d99 | |||
| 6f37bc2f5d | |||
| fc59b841d6 | |||
| c07fcdd489 | |||
| a49db10ade | |||
| 77bae98d77 | |||
| 254df7211c | |||
| f9caab48c4 | |||
| e0b5e7b808 | |||
| ac3a8da002 | |||
| 1aa45c2156 | |||
| 3cf8abd409 | |||
| db8426779c | |||
| b419e033f3 | |||
| d686fa327b | |||
| a1ce5eda43 | |||
| 1e790d1aa9 | |||
| d1d304b758 | |||
| e12b500144 | |||
| bd77651a1e | |||
| 35dc186395 | |||
| 07e78e0d12 | |||
| 5b8905c1d2 | |||
| 158a27cbae | |||
| 5769b39d78 | |||
| 5c96262c75 | |||
| 766f57dc9d | |||
| 9986078582 | |||
| e047ab5684 | |||
| a100785ad7 | |||
| 156eb4d15e | |||
| dabcfd965f | |||
| d44a71f3be | |||
| f8edd6cf3d | |||
| 2baf53c5a4 | |||
| c26e9c281f | |||
| 9f78e9b7dd | |||
| fdaf41b605 | |||
| 89526efe7a | |||
| 5e3a25c18f | |||
| cf11c4283e | |||
| 2dde04b979 | |||
| 8384f227be | |||
| 697b3bc5f5 | |||
| 9e2041521e | |||
| ee7b89ec6e | |||
| 5b143bdc76 | |||
| e3800426c9 | |||
| d9d00e452e | |||
| 14500e281c | |||
| c4623c80ff | |||
| 9e17dce9a9 | |||
| 4acc867634 | |||
| 1a061268de | |||
| daa91986ef | |||
| 63761cfc9a | |||
| 5091a5485a | |||
| d10026acd1 | |||
| f8f1cababe | |||
| ad46841397 | |||
| 20fb1e0fd0 | |||
| 9347351c37 | |||
| 0ef1f2d40f | |||
| b460f9915d | |||
| 4e195dfbc3 | |||
| 38b9fe3017 | |||
| 3c7f7bfca7 | |||
| 05230971b3 | |||
| dccdf72c73 | |||
| ca15983a72 | |||
| 4b6a2c9829 | |||
| 1755d03a6b | |||
| 869b1fc15e | |||
| ce2a2f8582 | |||
| 7b355139fb | |||
| b14518edb1 | |||
| 7d64003d1c | |||
| 0a59e04f19 | |||
| b57abb646f | |||
| dd6bde97a9 | |||
| b545545712 | |||
| c1993ffa03 | |||
| 7f7ebafa46 | |||
| b652597924 | |||
| 258fe77928 | |||
| 5a9fcd6fab | |||
| 3c05521a5b | |||
| 034b8b15ae | |||
| 7bd687331b | |||
| bdae35b1a8 | |||
| 54d58df4b6 | |||
| 9165a9f7cb | |||
| b556d1e81d | |||
| 7c25678211 | |||
| c83a9924e2 | |||
| bbeb9b83a0 | |||
| 06478f3e36 | |||
| 40f20002b2 | |||
| 442272f517 | |||
| 88dae8e9c4 | |||
| 1bbfa7d39e | |||
| edc2b3d295 | |||
| 0006da7385 | |||
| 470b7bd2e5 | |||
| 9014fb581d | |||
| b5ac8b3ec6 | |||
| 78f5169880 | |||
| 7ffa6b1bb3 | |||
| 3cd4b4503f | |||
| 3361b77aec | |||
| 8b7c9df286 | |||
| 157d5b4c36 | |||
| 44c8800bec | |||
| 2f0ba1b1f7 | |||
| 36c51f1a0c | |||
| 1dfe18aa6f | |||
| b9bbfb44c5 | |||
| 83843f192d | |||
| 8839d9f1c6 | |||
| 0630ec1d46 | |||
| 4dce8d6a80 | |||
| 3b62f999bf | |||
| d63fa521a1 | |||
| ca781dfe15 | |||
| 4bc561ceab | |||
| 3d258180bd | |||
| 65ae8610fd | |||
| c1c2000c98 | |||
| 287c2d82a1 | |||
| 5cde1650f4 | |||
| a4b90f14ab | |||
| 4826b40136 | |||
| 62618224da | |||
| 49f15e1637 | |||
| d5cab0910e | |||
| d4ccf232c1 | |||
| e36047c890 | |||
| 8f1199bd08 | |||
| d6e045ea4e | |||
| 304e48996b | |||
| f350dc83b8 | |||
| ebb7beda8c | |||
| a01f3da66e | |||
| 72f5b5fbc0 | |||
| 330aa495c8 | |||
| 0b529ae94d | |||
| 83b35183d0 | |||
| daf1d42a0f | |||
| 2cd01eb1fe | |||
| 07378f665a | |||
| bfd5f24f4c | |||
| 3d617187af | |||
| d040b93ca9 | |||
| a1d460385d | |||
| a410e2962a | |||
| f5aa8f37bb | |||
| 7e932df450 | |||
| 3d4741727e | |||
| a03b63ef74 | |||
| 15ce3e9f20 | |||
| d2ed0c65ca | |||
| da58b72f9d | |||
| 1639bd7af1 | |||
| d474121f85 | |||
| 978f76ffb6 | |||
| 084bac00f5 | |||
| 94454172dd | |||
| 891d3cf966 | |||
| 561d5ec7ab | |||
| 7ce437d50a | |||
| 4b02d4ce90 | |||
| 3107185869 | |||
| 2e3584a353 | |||
| e5b1be195c | |||
| dde30c9d76 | |||
| 3830e65de8 | |||
| c589cf167e | |||
| 2fde367c82 | |||
| 8fd188268e | |||
| b65257df42 | |||
| aaa2d7f08d | |||
| f73e25ece6 | |||
| 78d427f208 | |||
| eaeaf3538f | |||
| 85e381a85e | |||
| 1b7ee8231b | |||
| 1b8b8f5738 | |||
| 53df19b477 | |||
| ccf21b7580 | |||
| 4189d62a57 | |||
| 9a3e3af614 | |||
| f7187400dc | |||
| f55a7f0a7b | |||
| d6d35a645e | |||
| e719dcc7f5 | |||
| bc5bc5450c | |||
| f4bade0c2e | |||
| 9be59c674d | |||
| a1dec23c20 | |||
| ed926c4e37 | |||
| ab360ed6f6 | |||
| 569ba3d651 | |||
| 60fe28c2fe | |||
| 2787e29a07 | |||
| c77a4d08d6 | |||
| 9b3f90f922 | |||
| c88d457021 | |||
| b20b625820 | |||
| fd95311920 | |||
| 6da5c11731 | |||
| 4e58231308 | |||
| ef0ecf249a | |||
| 4981617f7a | |||
| 2070bc7007 | |||
| 231d2461b3 | |||
| 3b457f87c4 | |||
| de3ced4d3c | |||
| 891777e89e | |||
| 287239dd1c | |||
| 7cdded8fd7 | |||
| 8c9d045e1d | |||
| 620f5a0459 | |||
| 178d874ba0 | |||
| d44f30c8a6 | |||
| ce66937429 | |||
| 9823337375 | |||
| 11f5f0dfe1 | |||
| e1882f19e8 | |||
| 6a8b9f06c2 | |||
| 752fc8787d | |||
| 90a1cd8280 | |||
| aa570ac29d | |||
| fb7b6363f9 | |||
| 23afe7994c | |||
| 7557e6f6ba | |||
| 86b6938911 | |||
| 8f30a45fa8 | |||
| 7c9e9d5f52 | |||
| 4066ce73a8 | |||
| b5722dba1a | |||
| 81765ecafc | |||
| 84b42e9d19 | |||
| ed319a0e5f | |||
| dd55d10194 | |||
| 2084b46090 | |||
| 53443a6cf2 | |||
| 92715b5642 |
@@ -0,0 +1,2 @@
|
|||||||
|
aar/* filter=lfs diff=lfs merge=lfs -text
|
||||||
|
app/aar/* filter=lfs diff=lfs merge=lfs -text
|
||||||
@@ -1,19 +1,19 @@
|
|||||||
name: Bug Report
|
name: Bug Report
|
||||||
description: Let us know about an unexpected error, a crash, or an incorrect behavior.
|
description: Let us know about an unexpected error, a crash, or an incorrect behavior.
|
||||||
labels: ["bug", "new"]
|
labels: ["Bug"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
# Thank you for taking the time to fill out this bug report.
|
# Thank you for taking the time to fill out this bug report.
|
||||||
|
|
||||||
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application
|
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application
|
||||||
|
|
||||||
For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
|
For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
|
||||||
|
|
||||||
## Filing a bug report
|
## Filing a bug report
|
||||||
|
|
||||||
To fix your issues faster, we need clear reproduction cases - ideally allowing us to make it happen locally.
|
To fix your issues faster, we need clear reproduction cases - ideally allowing us to make it happen locally.
|
||||||
* Please include all needed context. For example, Device, OS, Application, your Grayjay Configurations and Plugin versioning info.
|
* Please include all needed context. For example, Device, OS, Application, your Grayjay Configurations and Plugin versioning info.
|
||||||
* if you've found out a particular series of UI interactions can introduce buggy behavior, please label those steps 1-n with markdown
|
* if you've found out a particular series of UI interactions can introduce buggy behavior, please label those steps 1-n with markdown
|
||||||
|
|
||||||
@@ -41,18 +41,21 @@ body:
|
|||||||
label: What plugins are you seeing the problem on?
|
label: What plugins are you seeing the problem on?
|
||||||
multiple: true
|
multiple: true
|
||||||
options:
|
options:
|
||||||
- All
|
- "All"
|
||||||
- Youtube
|
- "Youtube"
|
||||||
- BiliBili (CN)
|
- "Odysee"
|
||||||
- Twitch
|
- "Rumble"
|
||||||
- Odysee
|
- "Kick"
|
||||||
- Rumble
|
- "Twitch"
|
||||||
- Kick
|
- "PeerTube"
|
||||||
- PeerTube
|
- "Patreon"
|
||||||
- Patreon
|
- "Nebula"
|
||||||
- Nebula
|
- "BiliBili (CN)"
|
||||||
- SoundCloud
|
- "Bitchute"
|
||||||
- Other
|
- "SoundCloud"
|
||||||
|
- "Dailymotion"
|
||||||
|
- "Apple Podcasts"
|
||||||
|
- "Other"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -72,6 +75,17 @@ body:
|
|||||||
- label: While logged out
|
- label: While logged out
|
||||||
- label: N/A
|
- label: N/A
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: vpn
|
||||||
|
attributes:
|
||||||
|
label: Are you using a VPN?
|
||||||
|
multiple: false
|
||||||
|
options:
|
||||||
|
- "No"
|
||||||
|
- "Yes"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: logs
|
id: logs
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
name: Documentation Issue
|
name: Documentation Issue
|
||||||
description: Report an issue or suggest a change in the documentation.
|
description: Report an issue or suggest a change in the documentation.
|
||||||
labels: ["documentation", "new"]
|
labels: ["Documentation"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
# Thank you for opening a documentation change request.
|
# Thank you for opening a documentation change request.
|
||||||
|
|
||||||
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application. Use the `Documentation` issue type to report problems with the documentation in our code repositories, inside the application, or on [https://grayjay.app](https://grayjay.app)
|
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application. Use the `Documentation` issue type to report problems with the documentation in our code repositories, inside the application, or on [https://grayjay.app](https://grayjay.app)
|
||||||
Technical writers monitor this issue type, so report Grayjay bugs or feature requests with the `Bug report` or `Feature Request` issue types instead to get engineering attention.
|
Technical writers monitor this issue type, so report Grayjay bugs or feature requests with the `Bug report` or `Feature Request` issue types instead to get engineering attention.
|
||||||
|
|
||||||
For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
|
For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name: Feature Request
|
name: Feature Request
|
||||||
description: Suggest a new feature or other enhancement.
|
description: Suggest a new feature or other enhancement.
|
||||||
labels: ["enhancement", "new"]
|
labels: ["Enhancement"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
@@ -9,8 +9,6 @@ body:
|
|||||||
|
|
||||||
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application
|
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application
|
||||||
|
|
||||||
[External Contributions are closed at this time](https://github.com/tom-futo/grayjay-android/blob/master/CONTRIBUTION.md#contributing-to-core)
|
|
||||||
|
|
||||||
For discussion related to enhancements, please see: [The FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
|
For discussion related to enhancements, please see: [The FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
@@ -55,4 +53,4 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
**Note:** If the submit button is disabled and you have filled out all required fields, please check that you did not forget a **Title** for the issue.
|
**Note:** If the submit button is disabled and you have filled out all required fields, please check that you did not forget a **Title** for the issue.
|
||||||
|
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
name: Issue labeler
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [ opened ]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
label-component:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
# required for all workflows
|
|
||||||
issues: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Parse issue form
|
|
||||||
uses: stefanbuck/github-issue-parser@v3
|
|
||||||
id: issue-parser
|
|
||||||
with:
|
|
||||||
template-path: .github/ISSUE_TEMPLATE/bug_report.yml
|
|
||||||
|
|
||||||
- name: Set labels based on plugin field
|
|
||||||
uses: redhat-plumbers-in-action/advanced-issue-labeler@v2
|
|
||||||
with:
|
|
||||||
issue-form: ${{ steps.issue-parser.outputs.jsonString }}
|
|
||||||
section: plugin
|
|
||||||
block-list: |
|
|
||||||
None
|
|
||||||
Other
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
+24
@@ -82,3 +82,27 @@
|
|||||||
[submodule "app/src/stable/assets/sources/dailymotion"]
|
[submodule "app/src/stable/assets/sources/dailymotion"]
|
||||||
path = app/src/stable/assets/sources/dailymotion
|
path = app/src/stable/assets/sources/dailymotion
|
||||||
url = ../plugins/dailymotion.git
|
url = ../plugins/dailymotion.git
|
||||||
|
[submodule "app/src/stable/assets/sources/apple-podcast"]
|
||||||
|
path = app/src/stable/assets/sources/apple-podcasts
|
||||||
|
url = ../plugins/apple-podcasts.git
|
||||||
|
[submodule "app/src/unstable/assets/sources/apple-podcasts"]
|
||||||
|
path = app/src/unstable/assets/sources/apple-podcasts
|
||||||
|
url = ../plugins/apple-podcasts.git
|
||||||
|
[submodule "app/src/stable/assets/sources/tedtalks"]
|
||||||
|
path = app/src/stable/assets/sources/tedtalks
|
||||||
|
url = ../plugins/tedtalks.git
|
||||||
|
[submodule "app/src/unstable/assets/sources/tedtalks"]
|
||||||
|
path = app/src/unstable/assets/sources/tedtalks
|
||||||
|
url = ../plugins/tedtalks.git
|
||||||
|
[submodule "app/src/stable/assets/sources/curiositystream"]
|
||||||
|
path = app/src/stable/assets/sources/curiositystream
|
||||||
|
url = ../plugins/curiositystream.git
|
||||||
|
[submodule "app/src/unstable/assets/sources/curiositystream"]
|
||||||
|
path = app/src/unstable/assets/sources/curiositystream
|
||||||
|
url = ../plugins/curiositystream.git
|
||||||
|
[submodule "app/src/unstable/assets/sources/crunchyroll"]
|
||||||
|
path = app/src/unstable/assets/sources/crunchyroll
|
||||||
|
url = ../plugins/crunchyroll.git
|
||||||
|
[submodule "app/src/stable/assets/sources/crunchyroll"]
|
||||||
|
path = app/src/stable/assets/sources/crunchyroll
|
||||||
|
url = ../plugins/crunchyroll.git
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:ea10d3c5562c9f449a4e89e9c3dfcf881ed79a952f3409bc005bcc62c2cf4b81
|
||||||
|
size 65512557
|
||||||
+2
-1
@@ -197,7 +197,8 @@ dependencies {
|
|||||||
implementation 'org.jsoup:jsoup:1.15.3'
|
implementation 'org.jsoup:jsoup:1.15.3'
|
||||||
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
implementation 'com.arthenica:ffmpeg-kit-full:5.1'
|
implementation fileTree(dir: 'aar', include: ['*.aar'])
|
||||||
|
implementation 'com.arthenica:smart-exception-java:0.2.1'
|
||||||
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0'
|
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0'
|
||||||
implementation 'com.github.dhaval2404:imagepicker:2.1'
|
implementation 'com.github.dhaval2404:imagepicker:2.1'
|
||||||
implementation 'com.google.zxing:core:3.4.1'
|
implementation 'com.google.zxing:core:3.4.1'
|
||||||
|
|||||||
@@ -0,0 +1,338 @@
|
|||||||
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import com.futo.platformplayer.noise.protocol.Noise
|
||||||
|
import com.futo.platformplayer.sync.internal.*
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.selects.select
|
||||||
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Test
|
||||||
|
import java.net.Socket
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import kotlin.random.Random
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
class SyncServerTests {
|
||||||
|
|
||||||
|
//private val relayHost = "relay.grayjay.app"
|
||||||
|
//private val relayKey = "xGbHRzDOvE6plRbQaFgSen82eijF+gxS0yeUaeEErkw="
|
||||||
|
private val relayKey = "XlUaSpIlRaCg0TGzZ7JYmPupgUHDqTZXUUBco2K7ejw="
|
||||||
|
private val relayHost = "192.168.1.138"
|
||||||
|
private val relayPort = 9000
|
||||||
|
|
||||||
|
/** Creates a client connected to the live relay server. */
|
||||||
|
private suspend fun createClient(
|
||||||
|
onHandshakeComplete: ((SyncSocketSession) -> Unit)? = null,
|
||||||
|
onData: ((SyncSocketSession, UByte, UByte, ByteBuffer) -> Unit)? = null,
|
||||||
|
onNewChannel: ((SyncSocketSession, ChannelRelayed) -> Unit)? = null,
|
||||||
|
isHandshakeAllowed: ((LinkType, SyncSocketSession, String, String?, UInt) -> Boolean)? = null,
|
||||||
|
onException: ((Throwable) -> Unit)? = null
|
||||||
|
): SyncSocketSession = withContext(Dispatchers.IO) {
|
||||||
|
val p = Noise.createDH("25519")
|
||||||
|
p.generateKeyPair()
|
||||||
|
val socket = Socket(relayHost, relayPort)
|
||||||
|
val inputStream = LittleEndianDataInputStream(socket.getInputStream())
|
||||||
|
val outputStream = LittleEndianDataOutputStream(socket.getOutputStream())
|
||||||
|
val tcs = CompletableDeferred<Boolean>()
|
||||||
|
val socketSession = SyncSocketSession(
|
||||||
|
relayHost,
|
||||||
|
p,
|
||||||
|
inputStream,
|
||||||
|
outputStream,
|
||||||
|
onClose = { socket.close() },
|
||||||
|
onHandshakeComplete = { s ->
|
||||||
|
onHandshakeComplete?.invoke(s)
|
||||||
|
tcs.complete(true)
|
||||||
|
},
|
||||||
|
onData = onData ?: { _, _, _, _ -> },
|
||||||
|
onNewChannel = onNewChannel ?: { _, _ -> },
|
||||||
|
isHandshakeAllowed = isHandshakeAllowed ?: { _, _, _, _, _ -> true }
|
||||||
|
)
|
||||||
|
socketSession.authorizable = AlwaysAuthorized()
|
||||||
|
try {
|
||||||
|
socketSession.startAsInitiator(relayKey)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
onException?.invoke(e)
|
||||||
|
}
|
||||||
|
withTimeout(5000.milliseconds) { tcs.await() }
|
||||||
|
return@withContext socketSession
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun multipleClientsHandshake_Success() = runBlocking {
|
||||||
|
val client1 = createClient()
|
||||||
|
val client2 = createClient()
|
||||||
|
assertNotNull(client1.remotePublicKey, "Client 1 handshake failed")
|
||||||
|
assertNotNull(client2.remotePublicKey, "Client 2 handshake failed")
|
||||||
|
client1.stop()
|
||||||
|
client2.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun publishAndRequestConnectionInfo_Authorized_Success() = runBlocking {
|
||||||
|
val clientA = createClient()
|
||||||
|
val clientB = createClient()
|
||||||
|
val clientC = createClient()
|
||||||
|
clientA.publishConnectionInformation(arrayOf(clientB.localPublicKey), 12345, true, true, true, true)
|
||||||
|
delay(100.milliseconds)
|
||||||
|
val infoB = clientB.requestConnectionInfo(clientA.localPublicKey)
|
||||||
|
val infoC = clientC.requestConnectionInfo(clientA.localPublicKey)
|
||||||
|
assertNotNull("Client B should receive connection info", infoB)
|
||||||
|
assertEquals(12345.toUShort(), infoB!!.port)
|
||||||
|
assertNull("Client C should not receive connection info (unauthorized)", infoC)
|
||||||
|
clientA.stop()
|
||||||
|
clientB.stop()
|
||||||
|
clientC.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun relayedTransport_Bidirectional_Success() = runBlocking {
|
||||||
|
val tcsA = CompletableDeferred<ChannelRelayed>()
|
||||||
|
val tcsB = CompletableDeferred<ChannelRelayed>()
|
||||||
|
val clientA = createClient(onNewChannel = { _, c -> tcsA.complete(c) })
|
||||||
|
val clientB = createClient(onNewChannel = { _, c -> tcsB.complete(c) })
|
||||||
|
val channelTask = async { clientA.startRelayedChannel(clientB.localPublicKey) }
|
||||||
|
val channelA = withTimeout(5000.milliseconds) { tcsA.await() }
|
||||||
|
channelA.authorizable = AlwaysAuthorized()
|
||||||
|
val channelB = withTimeout(5000.milliseconds) { tcsB.await() }
|
||||||
|
channelB.authorizable = AlwaysAuthorized()
|
||||||
|
channelTask.await()
|
||||||
|
|
||||||
|
val tcsDataB = CompletableDeferred<ByteArray>()
|
||||||
|
channelB.setDataHandler { _, _, o, so, d ->
|
||||||
|
val b = ByteArray(d.remaining())
|
||||||
|
d.get(b)
|
||||||
|
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataB.complete(b)
|
||||||
|
}
|
||||||
|
channelA.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(byteArrayOf(1, 2, 3)))
|
||||||
|
|
||||||
|
val tcsDataA = CompletableDeferred<ByteArray>()
|
||||||
|
channelA.setDataHandler { _, _, o, so, d ->
|
||||||
|
val b = ByteArray(d.remaining())
|
||||||
|
d.get(b)
|
||||||
|
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataA.complete(b)
|
||||||
|
}
|
||||||
|
channelB.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(byteArrayOf(4, 5, 6)))
|
||||||
|
|
||||||
|
val receivedB = withTimeout(5000.milliseconds) { tcsDataB.await() }
|
||||||
|
val receivedA = withTimeout(5000.milliseconds) { tcsDataA.await() }
|
||||||
|
assertArrayEquals(byteArrayOf(1, 2, 3), receivedB)
|
||||||
|
assertArrayEquals(byteArrayOf(4, 5, 6), receivedA)
|
||||||
|
clientA.stop()
|
||||||
|
clientB.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun relayedTransport_MaximumMessageSize_Success() = runBlocking {
|
||||||
|
val MAX_DATA_PER_PACKET = SyncSocketSession.MAXIMUM_PACKET_SIZE - SyncSocketSession.HEADER_SIZE - 8 - 16 - 16
|
||||||
|
val maxSizeData = ByteArray(MAX_DATA_PER_PACKET).apply { Random.nextBytes(this) }
|
||||||
|
val tcsA = CompletableDeferred<ChannelRelayed>()
|
||||||
|
val tcsB = CompletableDeferred<ChannelRelayed>()
|
||||||
|
val clientA = createClient(onNewChannel = { _, c -> tcsA.complete(c) })
|
||||||
|
val clientB = createClient(onNewChannel = { _, c -> tcsB.complete(c) })
|
||||||
|
val channelTask = async { clientA.startRelayedChannel(clientB.localPublicKey) }
|
||||||
|
val channelA = withTimeout(5000.milliseconds) { tcsA.await() }
|
||||||
|
channelA.authorizable = AlwaysAuthorized()
|
||||||
|
val channelB = withTimeout(5000.milliseconds) { tcsB.await() }
|
||||||
|
channelB.authorizable = AlwaysAuthorized()
|
||||||
|
channelTask.await()
|
||||||
|
|
||||||
|
val tcsDataB = CompletableDeferred<ByteArray>()
|
||||||
|
channelB.setDataHandler { _, _, o, so, d ->
|
||||||
|
val b = ByteArray(d.remaining())
|
||||||
|
d.get(b)
|
||||||
|
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataB.complete(b)
|
||||||
|
}
|
||||||
|
channelA.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(maxSizeData))
|
||||||
|
val receivedData = withTimeout(5000.milliseconds) { tcsDataB.await() }
|
||||||
|
assertArrayEquals(maxSizeData, receivedData)
|
||||||
|
clientA.stop()
|
||||||
|
clientB.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun publishAndGetRecord_Success() = runBlocking {
|
||||||
|
val clientA = createClient()
|
||||||
|
val clientB = createClient()
|
||||||
|
val clientC = createClient()
|
||||||
|
val data = byteArrayOf(1, 2, 3)
|
||||||
|
val success = clientA.publishRecords(listOf(clientB.localPublicKey), "testKey", data)
|
||||||
|
val recordB = clientB.getRecord(clientA.localPublicKey, "testKey")
|
||||||
|
val recordC = clientC.getRecord(clientA.localPublicKey, "testKey")
|
||||||
|
assertTrue(success)
|
||||||
|
assertNotNull(recordB)
|
||||||
|
assertArrayEquals(data, recordB!!.first)
|
||||||
|
assertNull("Unauthorized client should not access record", recordC)
|
||||||
|
clientA.stop()
|
||||||
|
clientB.stop()
|
||||||
|
clientC.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getNonExistentRecord_ReturnsNull() = runBlocking {
|
||||||
|
val clientA = createClient()
|
||||||
|
val clientB = createClient()
|
||||||
|
val record = clientB.getRecord(clientA.localPublicKey, "nonExistentKey")
|
||||||
|
assertNull("Getting non-existent record should return null", record)
|
||||||
|
clientA.stop()
|
||||||
|
clientB.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun updateRecord_TimestampUpdated() = runBlocking {
|
||||||
|
val clientA = createClient()
|
||||||
|
val clientB = createClient()
|
||||||
|
val key = "updateKey"
|
||||||
|
val data1 = byteArrayOf(1)
|
||||||
|
val data2 = byteArrayOf(2)
|
||||||
|
clientA.publishRecords(listOf(clientB.localPublicKey), key, data1)
|
||||||
|
val record1 = clientB.getRecord(clientA.localPublicKey, key)
|
||||||
|
delay(1000.milliseconds)
|
||||||
|
clientA.publishRecords(listOf(clientB.localPublicKey), key, data2)
|
||||||
|
val record2 = clientB.getRecord(clientA.localPublicKey, key)
|
||||||
|
assertNotNull(record1)
|
||||||
|
assertNotNull(record2)
|
||||||
|
assertTrue(record2!!.second > record1!!.second)
|
||||||
|
assertArrayEquals(data2, record2.first)
|
||||||
|
clientA.stop()
|
||||||
|
clientB.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun deleteRecord_Success() = runBlocking {
|
||||||
|
val clientA = createClient()
|
||||||
|
val clientB = createClient()
|
||||||
|
val data = byteArrayOf(1, 2, 3)
|
||||||
|
clientA.publishRecords(listOf(clientB.localPublicKey), "toDelete", data)
|
||||||
|
val success = clientB.deleteRecords(clientA.localPublicKey, clientB.localPublicKey, listOf("toDelete"))
|
||||||
|
val record = clientB.getRecord(clientA.localPublicKey, "toDelete")
|
||||||
|
assertTrue(success)
|
||||||
|
assertNull(record)
|
||||||
|
clientA.stop()
|
||||||
|
clientB.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun listRecordKeys_Success() = runBlocking {
|
||||||
|
val clientA = createClient()
|
||||||
|
val clientB = createClient()
|
||||||
|
val keys = arrayOf("key1", "key2", "key3")
|
||||||
|
keys.forEach { key ->
|
||||||
|
clientA.publishRecords(listOf(clientB.localPublicKey), key, byteArrayOf(1))
|
||||||
|
}
|
||||||
|
val listedKeys = clientB.listRecordKeys(clientA.localPublicKey, clientB.localPublicKey)
|
||||||
|
assertArrayEquals(keys, listedKeys.map { it.first }.toTypedArray())
|
||||||
|
clientA.stop()
|
||||||
|
clientB.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun singleLargeMessageViaRelayedChannel_Success() = runBlocking {
|
||||||
|
val largeData = ByteArray(100000).apply { Random.nextBytes(this) }
|
||||||
|
val tcsA = CompletableDeferred<ChannelRelayed>()
|
||||||
|
val tcsB = CompletableDeferred<ChannelRelayed>()
|
||||||
|
val clientA = createClient(onNewChannel = { _, c -> tcsA.complete(c) })
|
||||||
|
val clientB = createClient(onNewChannel = { _, c -> tcsB.complete(c) })
|
||||||
|
val channelTask = async { clientA.startRelayedChannel(clientB.localPublicKey) }
|
||||||
|
val channelA = withTimeout(5000.milliseconds) { tcsA.await() }
|
||||||
|
channelA.authorizable = AlwaysAuthorized()
|
||||||
|
val channelB = withTimeout(5000.milliseconds) { tcsB.await() }
|
||||||
|
channelB.authorizable = AlwaysAuthorized()
|
||||||
|
channelTask.await()
|
||||||
|
|
||||||
|
val tcsDataB = CompletableDeferred<ByteArray>()
|
||||||
|
channelB.setDataHandler { _, _, o, so, d ->
|
||||||
|
val b = ByteArray(d.remaining())
|
||||||
|
d.get(b)
|
||||||
|
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataB.complete(b)
|
||||||
|
}
|
||||||
|
channelA.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(largeData))
|
||||||
|
val receivedData = withTimeout(10000.milliseconds) { tcsDataB.await() }
|
||||||
|
assertArrayEquals(largeData, receivedData)
|
||||||
|
clientA.stop()
|
||||||
|
clientB.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun publishAndGetLargeRecord_Success() = runBlocking {
|
||||||
|
val largeData = ByteArray(1000000).apply { Random.nextBytes(this) }
|
||||||
|
val clientA = createClient()
|
||||||
|
val clientB = createClient()
|
||||||
|
val success = clientA.publishRecords(listOf(clientB.localPublicKey), "largeRecord", largeData)
|
||||||
|
val record = clientB.getRecord(clientA.localPublicKey, "largeRecord")
|
||||||
|
assertTrue(success)
|
||||||
|
assertNotNull(record)
|
||||||
|
assertArrayEquals(largeData, record!!.first)
|
||||||
|
clientA.stop()
|
||||||
|
clientB.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun relayedTransport_WithValidAppId_Success() = runBlocking {
|
||||||
|
// Arrange: Set up clients
|
||||||
|
val allowedAppId = 1234u
|
||||||
|
val tcsB = CompletableDeferred<ChannelRelayed>()
|
||||||
|
|
||||||
|
// Client B requires appId 1234
|
||||||
|
val clientB = createClient(
|
||||||
|
onNewChannel = { _, c -> tcsB.complete(c) },
|
||||||
|
isHandshakeAllowed = { linkType, _, _, _, appId -> linkType == LinkType.Relayed && appId == allowedAppId }
|
||||||
|
)
|
||||||
|
|
||||||
|
val clientA = createClient()
|
||||||
|
|
||||||
|
// Act: Start relayed channel with valid appId
|
||||||
|
val channelTask = async { clientA.startRelayedChannel(clientB.localPublicKey, appId = allowedAppId) }
|
||||||
|
val channelB = withTimeout(5.seconds) { tcsB.await() }
|
||||||
|
withTimeout(5.seconds) { channelTask.await() }
|
||||||
|
|
||||||
|
// Assert: Channel is established
|
||||||
|
assertNotNull("Channel should be created on target with valid appId", channelB)
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
clientA.stop()
|
||||||
|
clientB.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun relayedTransport_WithInvalidAppId_Fails() = runBlocking {
|
||||||
|
// Arrange: Set up clients
|
||||||
|
val allowedAppId = 1234u
|
||||||
|
val invalidAppId = 5678u
|
||||||
|
val tcsB = CompletableDeferred<ChannelRelayed>()
|
||||||
|
|
||||||
|
// Client B requires appId 1234
|
||||||
|
val clientB = createClient(
|
||||||
|
onNewChannel = { _, c -> tcsB.complete(c) },
|
||||||
|
isHandshakeAllowed = { linkType, _, _, _, appId -> linkType == LinkType.Relayed && appId == allowedAppId },
|
||||||
|
onException = { }
|
||||||
|
)
|
||||||
|
|
||||||
|
val clientA = createClient()
|
||||||
|
|
||||||
|
// Act & Assert: Attempt with invalid appId should fail
|
||||||
|
try {
|
||||||
|
withTimeout(5.seconds) {
|
||||||
|
clientA.startRelayedChannel(clientB.localPublicKey, appId = invalidAppId)
|
||||||
|
}
|
||||||
|
fail("Starting relayed channel with invalid appId should fail")
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
// Expected: The channel creation should time out or fail
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure no channel was created on client B
|
||||||
|
val completedTask = select {
|
||||||
|
tcsB.onAwait { "channel" }
|
||||||
|
async { delay(1.seconds); "timeout" }.onAwait { "timeout" }
|
||||||
|
}
|
||||||
|
assertEquals("No channel should be created with invalid appId", "timeout", completedTask)
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
clientA.stop()
|
||||||
|
clientB.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AlwaysAuthorized : IAuthorizable {
|
||||||
|
override val isAuthorized: Boolean get() = true
|
||||||
|
}
|
||||||
@@ -0,0 +1,512 @@
|
|||||||
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import com.futo.platformplayer.noise.protocol.DHState
|
||||||
|
import com.futo.platformplayer.noise.protocol.Noise
|
||||||
|
import com.futo.platformplayer.sync.internal.*
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Test
|
||||||
|
import java.io.PipedInputStream
|
||||||
|
import java.io.PipedOutputStream
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import kotlin.random.Random
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
data class PipeStreams(
|
||||||
|
val initiatorInput: LittleEndianDataInputStream,
|
||||||
|
val initiatorOutput: LittleEndianDataOutputStream,
|
||||||
|
val responderInput: LittleEndianDataInputStream,
|
||||||
|
val responderOutput: LittleEndianDataOutputStream
|
||||||
|
)
|
||||||
|
|
||||||
|
typealias OnHandshakeComplete = (SyncSocketSession) -> Unit
|
||||||
|
typealias IsHandshakeAllowed = (LinkType, SyncSocketSession, String, String?, UInt) -> Boolean
|
||||||
|
typealias OnClose = (SyncSocketSession) -> Unit
|
||||||
|
typealias OnData = (SyncSocketSession, UByte, UByte, ByteBuffer) -> Unit
|
||||||
|
|
||||||
|
class SyncSocketTests {
|
||||||
|
private fun createPipeStreams(): PipeStreams {
|
||||||
|
val initiatorOutput = PipedOutputStream()
|
||||||
|
val responderOutput = PipedOutputStream()
|
||||||
|
val responderInput = PipedInputStream(initiatorOutput)
|
||||||
|
val initiatorInput = PipedInputStream(responderOutput)
|
||||||
|
return PipeStreams(
|
||||||
|
LittleEndianDataInputStream(initiatorInput), LittleEndianDataOutputStream(initiatorOutput),
|
||||||
|
LittleEndianDataInputStream(responderInput), LittleEndianDataOutputStream(responderOutput)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateKeyPair(): DHState {
|
||||||
|
val p = Noise.createDH("25519")
|
||||||
|
p.generateKeyPair()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createSessions(
|
||||||
|
initiatorInput: LittleEndianDataInputStream,
|
||||||
|
initiatorOutput: LittleEndianDataOutputStream,
|
||||||
|
responderInput: LittleEndianDataInputStream,
|
||||||
|
responderOutput: LittleEndianDataOutputStream,
|
||||||
|
initiatorKeyPair: DHState,
|
||||||
|
responderKeyPair: DHState,
|
||||||
|
onInitiatorHandshakeComplete: OnHandshakeComplete,
|
||||||
|
onResponderHandshakeComplete: OnHandshakeComplete,
|
||||||
|
onInitiatorClose: OnClose? = null,
|
||||||
|
onResponderClose: OnClose? = null,
|
||||||
|
onClose: OnClose? = null,
|
||||||
|
isHandshakeAllowed: IsHandshakeAllowed? = null,
|
||||||
|
onDataA: OnData? = null,
|
||||||
|
onDataB: OnData? = null
|
||||||
|
): Pair<SyncSocketSession, SyncSocketSession> {
|
||||||
|
val initiatorSession = SyncSocketSession(
|
||||||
|
"", initiatorKeyPair, initiatorInput, initiatorOutput,
|
||||||
|
onClose = {
|
||||||
|
onClose?.invoke(it)
|
||||||
|
onInitiatorClose?.invoke(it)
|
||||||
|
},
|
||||||
|
onHandshakeComplete = onInitiatorHandshakeComplete,
|
||||||
|
onData = onDataA,
|
||||||
|
isHandshakeAllowed = isHandshakeAllowed
|
||||||
|
)
|
||||||
|
|
||||||
|
val responderSession = SyncSocketSession(
|
||||||
|
"", responderKeyPair, responderInput, responderOutput,
|
||||||
|
onClose = {
|
||||||
|
onClose?.invoke(it)
|
||||||
|
onResponderClose?.invoke(it)
|
||||||
|
},
|
||||||
|
onHandshakeComplete = onResponderHandshakeComplete,
|
||||||
|
onData = onDataB,
|
||||||
|
isHandshakeAllowed = isHandshakeAllowed
|
||||||
|
)
|
||||||
|
|
||||||
|
return Pair(initiatorSession, responderSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handshake_WithValidPairingCode_Succeeds(): Unit = runBlocking {
|
||||||
|
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
|
||||||
|
val initiatorKeyPair = generateKeyPair()
|
||||||
|
val responderKeyPair = generateKeyPair()
|
||||||
|
val validPairingCode = "secret"
|
||||||
|
|
||||||
|
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
|
||||||
|
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
|
||||||
|
|
||||||
|
val (initiatorSession, responderSession) = createSessions(
|
||||||
|
initiatorInput, initiatorOutput, responderInput, responderOutput,
|
||||||
|
initiatorKeyPair, responderKeyPair,
|
||||||
|
{ handshakeInitiatorCompleted.complete(true) },
|
||||||
|
{ handshakeResponderCompleted.complete(true) },
|
||||||
|
isHandshakeAllowed = { _, _, _, pairingCode, _ -> pairingCode == validPairingCode }
|
||||||
|
)
|
||||||
|
|
||||||
|
initiatorSession.startAsInitiator(responderSession.localPublicKey, pairingCode = validPairingCode)
|
||||||
|
responderSession.startAsResponder()
|
||||||
|
|
||||||
|
withTimeout(5.seconds) {
|
||||||
|
handshakeInitiatorCompleted.await()
|
||||||
|
handshakeResponderCompleted.await()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handshake_WithInvalidPairingCode_Fails() = runBlocking {
|
||||||
|
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
|
||||||
|
val initiatorKeyPair = generateKeyPair()
|
||||||
|
val responderKeyPair = generateKeyPair()
|
||||||
|
val validPairingCode = "secret"
|
||||||
|
val invalidPairingCode = "wrong"
|
||||||
|
|
||||||
|
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
|
||||||
|
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
|
||||||
|
val initiatorClosed = CompletableDeferred<Boolean>()
|
||||||
|
val responderClosed = CompletableDeferred<Boolean>()
|
||||||
|
|
||||||
|
val (initiatorSession, responderSession) = createSessions(
|
||||||
|
initiatorInput, initiatorOutput, responderInput, responderOutput,
|
||||||
|
initiatorKeyPair, responderKeyPair,
|
||||||
|
{ handshakeInitiatorCompleted.complete(true) },
|
||||||
|
{ handshakeResponderCompleted.complete(true) },
|
||||||
|
onInitiatorClose = {
|
||||||
|
initiatorClosed.complete(true)
|
||||||
|
},
|
||||||
|
onResponderClose = {
|
||||||
|
responderClosed.complete(true)
|
||||||
|
},
|
||||||
|
isHandshakeAllowed = { _, _, _, pairingCode, _ -> pairingCode == validPairingCode }
|
||||||
|
)
|
||||||
|
|
||||||
|
initiatorSession.startAsInitiator(responderSession.localPublicKey, pairingCode = invalidPairingCode)
|
||||||
|
responderSession.startAsResponder()
|
||||||
|
|
||||||
|
withTimeout(100.seconds) {
|
||||||
|
initiatorClosed.await()
|
||||||
|
responderClosed.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFalse(handshakeInitiatorCompleted.isCompleted)
|
||||||
|
assertFalse(handshakeResponderCompleted.isCompleted)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handshake_WithoutPairingCodeWhenRequired_Fails() = runBlocking {
|
||||||
|
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
|
||||||
|
val initiatorKeyPair = generateKeyPair()
|
||||||
|
val responderKeyPair = generateKeyPair()
|
||||||
|
val validPairingCode = "secret"
|
||||||
|
|
||||||
|
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
|
||||||
|
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
|
||||||
|
val initiatorClosed = CompletableDeferred<Boolean>()
|
||||||
|
val responderClosed = CompletableDeferred<Boolean>()
|
||||||
|
|
||||||
|
val (initiatorSession, responderSession) = createSessions(
|
||||||
|
initiatorInput, initiatorOutput, responderInput, responderOutput,
|
||||||
|
initiatorKeyPair, responderKeyPair,
|
||||||
|
{ handshakeInitiatorCompleted.complete(true) },
|
||||||
|
{ handshakeResponderCompleted.complete(true) },
|
||||||
|
onInitiatorClose = {
|
||||||
|
initiatorClosed.complete(true)
|
||||||
|
},
|
||||||
|
onResponderClose = {
|
||||||
|
responderClosed.complete(true)
|
||||||
|
},
|
||||||
|
isHandshakeAllowed = { _, _, _, pairingCode, _ -> pairingCode == validPairingCode }
|
||||||
|
)
|
||||||
|
|
||||||
|
initiatorSession.startAsInitiator(responderSession.localPublicKey) // No pairing code
|
||||||
|
responderSession.startAsResponder()
|
||||||
|
|
||||||
|
withTimeout(5.seconds) {
|
||||||
|
initiatorClosed.await()
|
||||||
|
responderClosed.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFalse(handshakeInitiatorCompleted.isCompleted)
|
||||||
|
assertFalse(handshakeResponderCompleted.isCompleted)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handshake_WithPairingCodeWhenNotRequired_Succeeds(): Unit = runBlocking {
|
||||||
|
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
|
||||||
|
val initiatorKeyPair = generateKeyPair()
|
||||||
|
val responderKeyPair = generateKeyPair()
|
||||||
|
val pairingCode = "unnecessary"
|
||||||
|
|
||||||
|
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
|
||||||
|
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
|
||||||
|
|
||||||
|
val (initiatorSession, responderSession) = createSessions(
|
||||||
|
initiatorInput, initiatorOutput, responderInput, responderOutput,
|
||||||
|
initiatorKeyPair, responderKeyPair,
|
||||||
|
{ handshakeInitiatorCompleted.complete(true) },
|
||||||
|
{ handshakeResponderCompleted.complete(true) },
|
||||||
|
isHandshakeAllowed = { _, _, _, _, _ -> true } // Always allow
|
||||||
|
)
|
||||||
|
|
||||||
|
initiatorSession.startAsInitiator(responderSession.localPublicKey, pairingCode = pairingCode)
|
||||||
|
responderSession.startAsResponder()
|
||||||
|
|
||||||
|
withTimeout(10.seconds) {
|
||||||
|
handshakeInitiatorCompleted.await()
|
||||||
|
handshakeResponderCompleted.await()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sendAndReceive_SmallDataPacket_Succeeds() = runBlocking {
|
||||||
|
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
|
||||||
|
val initiatorKeyPair = generateKeyPair()
|
||||||
|
val responderKeyPair = generateKeyPair()
|
||||||
|
|
||||||
|
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
|
||||||
|
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
|
||||||
|
val tcsDataReceived = CompletableDeferred<ByteArray>()
|
||||||
|
|
||||||
|
val (initiatorSession, responderSession) = createSessions(
|
||||||
|
initiatorInput, initiatorOutput, responderInput, responderOutput,
|
||||||
|
initiatorKeyPair, responderKeyPair,
|
||||||
|
{ handshakeInitiatorCompleted.complete(true) },
|
||||||
|
{ handshakeResponderCompleted.complete(true) },
|
||||||
|
onDataB = { _, opcode, subOpcode, data ->
|
||||||
|
if (opcode == Opcode.DATA.value && subOpcode == 0u.toUByte()) {
|
||||||
|
val b = ByteArray(data.remaining())
|
||||||
|
data.get(b)
|
||||||
|
tcsDataReceived.complete(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
initiatorSession.startAsInitiator(responderSession.localPublicKey)
|
||||||
|
responderSession.startAsResponder()
|
||||||
|
|
||||||
|
withTimeout(10.seconds) {
|
||||||
|
handshakeInitiatorCompleted.await()
|
||||||
|
handshakeResponderCompleted.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure both sessions are authorized
|
||||||
|
initiatorSession.authorizable = Authorized()
|
||||||
|
responderSession.authorizable = Authorized()
|
||||||
|
|
||||||
|
val smallData = byteArrayOf(1, 2, 3)
|
||||||
|
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(smallData))
|
||||||
|
|
||||||
|
val receivedData = withTimeout(10.seconds) { tcsDataReceived.await() }
|
||||||
|
assertArrayEquals(smallData, receivedData)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sendAndReceive_ExactlyMaximumPacketSize_Succeeds() = runBlocking {
|
||||||
|
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
|
||||||
|
val initiatorKeyPair = generateKeyPair()
|
||||||
|
val responderKeyPair = generateKeyPair()
|
||||||
|
|
||||||
|
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
|
||||||
|
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
|
||||||
|
val tcsDataReceived = CompletableDeferred<ByteArray>()
|
||||||
|
|
||||||
|
val (initiatorSession, responderSession) = createSessions(
|
||||||
|
initiatorInput, initiatorOutput, responderInput, responderOutput,
|
||||||
|
initiatorKeyPair, responderKeyPair,
|
||||||
|
{ handshakeInitiatorCompleted.complete(true) },
|
||||||
|
{ handshakeResponderCompleted.complete(true) },
|
||||||
|
onDataB = { _, opcode, subOpcode, data ->
|
||||||
|
if (opcode == Opcode.DATA.value && subOpcode == 0u.toUByte()) {
|
||||||
|
val b = ByteArray(data.remaining())
|
||||||
|
data.get(b)
|
||||||
|
tcsDataReceived.complete(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
initiatorSession.startAsInitiator(responderSession.localPublicKey)
|
||||||
|
responderSession.startAsResponder()
|
||||||
|
|
||||||
|
withTimeout(10.seconds) {
|
||||||
|
handshakeInitiatorCompleted.await()
|
||||||
|
handshakeResponderCompleted.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure both sessions are authorized
|
||||||
|
initiatorSession.authorizable = Authorized()
|
||||||
|
responderSession.authorizable = Authorized()
|
||||||
|
|
||||||
|
val maxData = ByteArray(SyncSocketSession.MAXIMUM_PACKET_SIZE - SyncSocketSession.HEADER_SIZE).apply { Random.nextBytes(this) }
|
||||||
|
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(maxData))
|
||||||
|
|
||||||
|
val receivedData = withTimeout(10.seconds) { tcsDataReceived.await() }
|
||||||
|
assertArrayEquals(maxData, receivedData)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun stream_LargeData_Succeeds() = runBlocking {
|
||||||
|
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
|
||||||
|
val initiatorKeyPair = generateKeyPair()
|
||||||
|
val responderKeyPair = generateKeyPair()
|
||||||
|
|
||||||
|
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
|
||||||
|
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
|
||||||
|
val tcsDataReceived = CompletableDeferred<ByteArray>()
|
||||||
|
|
||||||
|
val (initiatorSession, responderSession) = createSessions(
|
||||||
|
initiatorInput, initiatorOutput, responderInput, responderOutput,
|
||||||
|
initiatorKeyPair, responderKeyPair,
|
||||||
|
{ handshakeInitiatorCompleted.complete(true) },
|
||||||
|
{ handshakeResponderCompleted.complete(true) },
|
||||||
|
onDataB = { _, opcode, subOpcode, data ->
|
||||||
|
if (opcode == Opcode.DATA.value && subOpcode == 0u.toUByte()) {
|
||||||
|
val b = ByteArray(data.remaining())
|
||||||
|
data.get(b)
|
||||||
|
tcsDataReceived.complete(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
initiatorSession.startAsInitiator(responderSession.localPublicKey)
|
||||||
|
responderSession.startAsResponder()
|
||||||
|
|
||||||
|
withTimeout(10.seconds) {
|
||||||
|
handshakeInitiatorCompleted.await()
|
||||||
|
handshakeResponderCompleted.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure both sessions are authorized
|
||||||
|
initiatorSession.authorizable = Authorized()
|
||||||
|
responderSession.authorizable = Authorized()
|
||||||
|
|
||||||
|
val largeData = ByteArray(2 * (SyncSocketSession.MAXIMUM_PACKET_SIZE - SyncSocketSession.HEADER_SIZE)).apply { Random.nextBytes(this) }
|
||||||
|
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(largeData))
|
||||||
|
|
||||||
|
val receivedData = withTimeout(10.seconds) { tcsDataReceived.await() }
|
||||||
|
assertArrayEquals(largeData, receivedData)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun authorizedSession_CanSendData() = runBlocking {
|
||||||
|
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
|
||||||
|
val initiatorKeyPair = generateKeyPair()
|
||||||
|
val responderKeyPair = generateKeyPair()
|
||||||
|
|
||||||
|
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
|
||||||
|
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
|
||||||
|
val tcsDataReceived = CompletableDeferred<ByteArray>()
|
||||||
|
|
||||||
|
val (initiatorSession, responderSession) = createSessions(
|
||||||
|
initiatorInput, initiatorOutput, responderInput, responderOutput,
|
||||||
|
initiatorKeyPair, responderKeyPair,
|
||||||
|
{ handshakeInitiatorCompleted.complete(true) },
|
||||||
|
{ handshakeResponderCompleted.complete(true) },
|
||||||
|
onDataB = { _, opcode, subOpcode, data ->
|
||||||
|
if (opcode == Opcode.DATA.value && subOpcode == 0u.toUByte()) {
|
||||||
|
val b = ByteArray(data.remaining())
|
||||||
|
data.get(b)
|
||||||
|
tcsDataReceived.complete(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
initiatorSession.startAsInitiator(responderSession.localPublicKey)
|
||||||
|
responderSession.startAsResponder()
|
||||||
|
|
||||||
|
withTimeout(10.seconds) {
|
||||||
|
handshakeInitiatorCompleted.await()
|
||||||
|
handshakeResponderCompleted.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authorize both sessions
|
||||||
|
initiatorSession.authorizable = Authorized()
|
||||||
|
responderSession.authorizable = Authorized()
|
||||||
|
|
||||||
|
val data = byteArrayOf(1, 2, 3)
|
||||||
|
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(data))
|
||||||
|
|
||||||
|
val receivedData = withTimeout(10.seconds) { tcsDataReceived.await() }
|
||||||
|
assertArrayEquals(data, receivedData)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun unauthorizedSession_CannotSendData() = runBlocking {
|
||||||
|
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
|
||||||
|
val initiatorKeyPair = generateKeyPair()
|
||||||
|
val responderKeyPair = generateKeyPair()
|
||||||
|
|
||||||
|
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
|
||||||
|
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
|
||||||
|
val tcsDataReceived = CompletableDeferred<ByteArray>()
|
||||||
|
|
||||||
|
val (initiatorSession, responderSession) = createSessions(
|
||||||
|
initiatorInput, initiatorOutput, responderInput, responderOutput,
|
||||||
|
initiatorKeyPair, responderKeyPair,
|
||||||
|
{ handshakeInitiatorCompleted.complete(true) },
|
||||||
|
{ handshakeResponderCompleted.complete(true) },
|
||||||
|
onDataB = { _, _, _, _ -> }
|
||||||
|
)
|
||||||
|
|
||||||
|
initiatorSession.startAsInitiator(responderSession.localPublicKey)
|
||||||
|
responderSession.startAsResponder()
|
||||||
|
|
||||||
|
withTimeout(10.seconds) {
|
||||||
|
handshakeInitiatorCompleted.await()
|
||||||
|
handshakeResponderCompleted.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authorize initiator but not responder
|
||||||
|
initiatorSession.authorizable = Authorized()
|
||||||
|
responderSession.authorizable = Unauthorized()
|
||||||
|
|
||||||
|
val data = byteArrayOf(1, 2, 3)
|
||||||
|
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(data))
|
||||||
|
|
||||||
|
delay(1.seconds)
|
||||||
|
assertFalse(tcsDataReceived.isCompleted)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun directHandshake_WithValidAppId_Succeeds() = runBlocking {
|
||||||
|
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
|
||||||
|
val initiatorKeyPair = generateKeyPair()
|
||||||
|
val responderKeyPair = generateKeyPair()
|
||||||
|
val allowedAppId = 1234u
|
||||||
|
|
||||||
|
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
|
||||||
|
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
|
||||||
|
|
||||||
|
val responderIsHandshakeAllowed = { linkType: LinkType, _: SyncSocketSession, _: String, _: String?, appId: UInt ->
|
||||||
|
linkType == LinkType.Direct && appId == allowedAppId
|
||||||
|
}
|
||||||
|
|
||||||
|
val (initiatorSession, responderSession) = createSessions(
|
||||||
|
initiatorInput, initiatorOutput, responderInput, responderOutput,
|
||||||
|
initiatorKeyPair, responderKeyPair,
|
||||||
|
{ handshakeInitiatorCompleted.complete(true) },
|
||||||
|
{ handshakeResponderCompleted.complete(true) },
|
||||||
|
isHandshakeAllowed = responderIsHandshakeAllowed
|
||||||
|
)
|
||||||
|
|
||||||
|
initiatorSession.startAsInitiator(responderSession.localPublicKey, appId = allowedAppId)
|
||||||
|
responderSession.startAsResponder()
|
||||||
|
|
||||||
|
withTimeout(5.seconds) {
|
||||||
|
handshakeInitiatorCompleted.await()
|
||||||
|
handshakeResponderCompleted.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNotNull(initiatorSession.remotePublicKey)
|
||||||
|
assertNotNull(responderSession.remotePublicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun directHandshake_WithInvalidAppId_Fails() = runBlocking {
|
||||||
|
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
|
||||||
|
val initiatorKeyPair = generateKeyPair()
|
||||||
|
val responderKeyPair = generateKeyPair()
|
||||||
|
val allowedAppId = 1234u
|
||||||
|
val invalidAppId = 5678u
|
||||||
|
|
||||||
|
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
|
||||||
|
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
|
||||||
|
val initiatorClosed = CompletableDeferred<Boolean>()
|
||||||
|
val responderClosed = CompletableDeferred<Boolean>()
|
||||||
|
|
||||||
|
val responderIsHandshakeAllowed = { linkType: LinkType, _: SyncSocketSession, _: String, _: String?, appId: UInt ->
|
||||||
|
linkType == LinkType.Direct && appId == allowedAppId
|
||||||
|
}
|
||||||
|
|
||||||
|
val (initiatorSession, responderSession) = createSessions(
|
||||||
|
initiatorInput, initiatorOutput, responderInput, responderOutput,
|
||||||
|
initiatorKeyPair, responderKeyPair,
|
||||||
|
{ handshakeInitiatorCompleted.complete(true) },
|
||||||
|
{ handshakeResponderCompleted.complete(true) },
|
||||||
|
onInitiatorClose = {
|
||||||
|
initiatorClosed.complete(true)
|
||||||
|
},
|
||||||
|
onResponderClose = {
|
||||||
|
responderClosed.complete(true)
|
||||||
|
},
|
||||||
|
isHandshakeAllowed = responderIsHandshakeAllowed
|
||||||
|
)
|
||||||
|
|
||||||
|
initiatorSession.startAsInitiator(responderSession.localPublicKey, appId = invalidAppId)
|
||||||
|
responderSession.startAsResponder()
|
||||||
|
|
||||||
|
withTimeout(5.seconds) {
|
||||||
|
initiatorClosed.await()
|
||||||
|
responderClosed.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFalse(handshakeInitiatorCompleted.isCompleted)
|
||||||
|
assertFalse(handshakeResponderCompleted.isCompleted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Authorized : IAuthorizable {
|
||||||
|
override val isAuthorized: Boolean = true
|
||||||
|
}
|
||||||
|
|
||||||
|
class Unauthorized : IAuthorizable {
|
||||||
|
override val isAuthorized: Boolean = false
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions"/>
|
<uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions"/>
|
||||||
|
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
@@ -36,6 +37,12 @@
|
|||||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
|
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
|
<receiver android:name=".receivers.MediaButtonReceiver" android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<service android:name=".services.MediaPlaybackService"
|
<service android:name=".services.MediaPlaybackService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:foregroundServiceType="mediaPlayback" />
|
android:foregroundServiceType="mediaPlayback" />
|
||||||
@@ -49,10 +56,10 @@
|
|||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.MainActivity"
|
android:name=".activities.MainActivity"
|
||||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
|
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|uiMode"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar"
|
android:theme="@style/Theme.FutoVideo.NoActionBar"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleInstance"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:supportsPictureInPicture="true">
|
android:supportsPictureInPicture="true">
|
||||||
|
|
||||||
@@ -150,7 +157,6 @@
|
|||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.SettingsActivity"
|
android:name=".activities.SettingsActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.DeveloperActivity"
|
android:name=".activities.DeveloperActivity"
|
||||||
@@ -234,4 +240,4 @@
|
|||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -11,7 +11,8 @@ let Type = {
|
|||||||
Streams: "STREAMS",
|
Streams: "STREAMS",
|
||||||
Mixed: "MIXED",
|
Mixed: "MIXED",
|
||||||
Live: "LIVE",
|
Live: "LIVE",
|
||||||
Subscriptions: "SUBSCRIPTIONS"
|
Subscriptions: "SUBSCRIPTIONS",
|
||||||
|
Shorts: "SHORTS"
|
||||||
},
|
},
|
||||||
Order: {
|
Order: {
|
||||||
Chronological: "CHRONOLOGICAL"
|
Chronological: "CHRONOLOGICAL"
|
||||||
@@ -31,7 +32,8 @@ let Type = {
|
|||||||
Text: {
|
Text: {
|
||||||
RAW: 0,
|
RAW: 0,
|
||||||
HTML: 1,
|
HTML: 1,
|
||||||
MARKUP: 2
|
MARKUP: 2,
|
||||||
|
CODE: 3
|
||||||
},
|
},
|
||||||
Chapter: {
|
Chapter: {
|
||||||
NORMAL: 0,
|
NORMAL: 0,
|
||||||
@@ -244,6 +246,7 @@ class PlatformVideo extends PlatformContent {
|
|||||||
this.viewCount = obj.viewCount ?? -1; //Long
|
this.viewCount = obj.viewCount ?? -1; //Long
|
||||||
|
|
||||||
this.isLive = obj.isLive ?? false; //Boolean
|
this.isLive = obj.isLive ?? false; //Boolean
|
||||||
|
this.isShort = !!obj.isShort ?? false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class PlatformVideoDetails extends PlatformVideo {
|
class PlatformVideoDetails extends PlatformVideo {
|
||||||
@@ -260,6 +263,11 @@ class PlatformVideoDetails extends PlatformVideo {
|
|||||||
|
|
||||||
this.rating = obj.rating ?? null; //IRating
|
this.rating = obj.rating ?? null; //IRating
|
||||||
this.subtitles = obj.subtitles ?? [];
|
this.subtitles = obj.subtitles ?? [];
|
||||||
|
this.isShort = !!obj.isShort ?? false;
|
||||||
|
|
||||||
|
if (obj.getContentRecommendations) {
|
||||||
|
this.getContentRecommendations = obj.getContentRecommendations
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,15 +292,39 @@ class PlatformPostDetails extends PlatformPost {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlatformArticleDetails extends PlatformContent {
|
class PlatformWeb extends PlatformContent {
|
||||||
|
constructor(obj) {
|
||||||
|
super(obj, 7);
|
||||||
|
obj = obj ?? {};
|
||||||
|
this.plugin_type = "PlatformWeb";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class PlatformWebDetails extends PlatformWeb {
|
||||||
|
constructor(obj) {
|
||||||
|
super(obj, 7);
|
||||||
|
obj = obj ?? {};
|
||||||
|
this.plugin_type = "PlatformWebDetails";
|
||||||
|
this.html = obj.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlatformArticle extends PlatformContent {
|
||||||
|
constructor(obj) {
|
||||||
|
super(obj, 3);
|
||||||
|
obj = obj ?? {};
|
||||||
|
this.plugin_type = "PlatformArticle";
|
||||||
|
this.rating = obj.rating ?? new RatingLikes(-1);
|
||||||
|
this.summary = obj.summary ?? "";
|
||||||
|
this.thumbnails = obj.thumbnails ?? new Thumbnails([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class PlatformArticleDetails extends PlatformArticle {
|
||||||
constructor(obj) {
|
constructor(obj) {
|
||||||
super(obj, 3);
|
super(obj, 3);
|
||||||
obj = obj ?? {};
|
obj = obj ?? {};
|
||||||
this.plugin_type = "PlatformArticleDetails";
|
this.plugin_type = "PlatformArticleDetails";
|
||||||
this.rating = obj.rating ?? new RatingLikes(-1);
|
this.rating = obj.rating ?? new RatingLikes(-1);
|
||||||
this.summary = obj.summary ?? "";
|
|
||||||
this.segments = obj.segments ?? [];
|
this.segments = obj.segments ?? [];
|
||||||
this.thumbnails = obj.thumbnails ?? new Thumbnails([]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class ArticleSegment {
|
class ArticleSegment {
|
||||||
@@ -308,9 +340,17 @@ class ArticleTextSegment extends ArticleSegment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
class ArticleImagesSegment extends ArticleSegment {
|
class ArticleImagesSegment extends ArticleSegment {
|
||||||
constructor(images) {
|
constructor(images, caption) {
|
||||||
super(2);
|
super(2);
|
||||||
this.images = images;
|
this.images = images;
|
||||||
|
this.caption = caption;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class ArticleHeaderSegment extends ArticleSegment {
|
||||||
|
constructor(content, level) {
|
||||||
|
super(3);
|
||||||
|
this.level = level;
|
||||||
|
this.content = content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class ArticleNestedSegment extends ArticleSegment {
|
class ArticleNestedSegment extends ArticleSegment {
|
||||||
@@ -588,6 +628,8 @@ class PlatformComment {
|
|||||||
this.date = obj.date ?? 0;
|
this.date = obj.date ?? 0;
|
||||||
this.replyCount = obj.replyCount ?? 0;
|
this.replyCount = obj.replyCount ?? 0;
|
||||||
this.context = obj.context ?? {};
|
this.context = obj.context ?? {};
|
||||||
|
if(obj.getReplies)
|
||||||
|
this.getReplies = obj.getReplies;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import java.text.DecimalFormat
|
|||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.roundToInt
|
|
||||||
import kotlin.math.roundToLong
|
import kotlin.math.roundToLong
|
||||||
|
|
||||||
|
|
||||||
@@ -226,6 +225,25 @@ fun Long.toHumanTime(isMs: Boolean): String {
|
|||||||
else
|
else
|
||||||
return "${prefix}${minsStr}:${secsStr}"
|
return "${prefix}${minsStr}:${secsStr}"
|
||||||
}
|
}
|
||||||
|
fun Long.toHumanDuration(isMs: Boolean): String {
|
||||||
|
var scaler = 1;
|
||||||
|
if(isMs)
|
||||||
|
scaler = 1000;
|
||||||
|
val v = Math.abs(this);
|
||||||
|
val hours = Math.max(v/(secondsInHour*scaler), 0);
|
||||||
|
val mins = Math.max((v % (secondsInHour*scaler)) / (secondsInMinute * scaler), 0);
|
||||||
|
val minsStr = mins.toString();
|
||||||
|
val seconds = Math.max(((v % (secondsInHour*scaler)) % (secondsInMinute * scaler))/scaler, 0);
|
||||||
|
val secsStr = seconds.toString().padStart(2, '0');
|
||||||
|
val prefix = if (this < 0) { "-" } else { "" };
|
||||||
|
|
||||||
|
return listOf(
|
||||||
|
if(hours > 0) "${hours}h" else null,
|
||||||
|
if(mins > 0) "${mins}m" else null ,
|
||||||
|
if(seconds > 0) "${seconds}s" else null
|
||||||
|
).filterNotNull().joinToString(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//TODO: Determine if below stuff should have its own proper class, seems a bit too complex for a utility method
|
//TODO: Determine if below stuff should have its own proper class, seems a bit too complex for a utility method
|
||||||
fun String.fixHtmlWhitespace(): Spanned {
|
fun String.fixHtmlWhitespace(): Spanned {
|
||||||
@@ -357,14 +375,19 @@ private val slds = hashSetOf(".com.ac", ".net.ac", ".gov.ac", ".org.ac", ".mil.a
|
|||||||
fun String.matchesDomain(queryDomain: String): Boolean {
|
fun String.matchesDomain(queryDomain: String): Boolean {
|
||||||
|
|
||||||
if(queryDomain.startsWith(".")) {
|
if(queryDomain.startsWith(".")) {
|
||||||
|
val parts = this.lowercase().split(".");
|
||||||
val parts = queryDomain.lowercase().split(".");
|
val queryParts = queryDomain.lowercase().trimStart("."[0]).split(".");
|
||||||
if(parts.size < 3)
|
if(queryParts.size < 2)
|
||||||
throw IllegalStateException("Illegal use of wildcards on First-Level-Domain (" + queryDomain + ")");
|
throw IllegalStateException("Illegal use of wildcards on First-Level-Domain (" + queryDomain + ")");
|
||||||
if(parts.size >= 3){
|
else {
|
||||||
val isSLD = slds.contains("." + parts[parts.size - 2] + "." + parts[parts.size - 1]);
|
val possibleDomain = "." + queryParts.joinToString(".");
|
||||||
if(isSLD && parts.size <= 3)
|
if(slds.contains(possibleDomain))
|
||||||
throw IllegalStateException("Illegal use of wildcards on Second-Level-Domain (" + queryDomain + ")");
|
throw IllegalStateException("Illegal use of wildcards on Second-Level-Domain (" + queryDomain + ")");
|
||||||
|
/*
|
||||||
|
val isSLD = slds.contains("." + queryParts[queryParts.size - 2] + "." + queryParts[queryParts.size - 1]);
|
||||||
|
if(isSLD && queryParts.size <= 3)
|
||||||
|
throw IllegalStateException("Illegal use of wildcards on Second-Level-Domain (" + queryDomain + ")");
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: Should be safe, but double verify if can't be exploited
|
//TODO: Should be safe, but double verify if can't be exploited
|
||||||
@@ -376,9 +399,11 @@ fun String.matchesDomain(queryDomain: String): Boolean {
|
|||||||
|
|
||||||
fun String.getSubdomainWildcardQuery(): String {
|
fun String.getSubdomainWildcardQuery(): String {
|
||||||
val domainParts = this.split(".");
|
val domainParts = this.split(".");
|
||||||
val sldParts = "." + domainParts[domainParts.size - 2].lowercase() + "." + domainParts[domainParts.size - 1].lowercase();
|
var wildcardDomain = if(domainParts.size > 2)
|
||||||
if(slds.contains(sldParts))
|
"." + domainParts.drop(1).joinToString(".")
|
||||||
return "." + domainParts.drop(domainParts.size - 3).joinToString(".");
|
|
||||||
else
|
else
|
||||||
return "." + domainParts.drop(domainParts.size - 2).joinToString(".");
|
"." + domainParts.joinToString(".");
|
||||||
|
if(slds.contains(wildcardDomain.lowercase()))
|
||||||
|
"." + domainParts.joinToString(".");
|
||||||
|
return wildcardDomain;
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ import java.io.ByteArrayOutputStream
|
|||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.net.Inet4Address
|
import java.net.Inet4Address
|
||||||
|
import java.net.Inet6Address
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.net.Socket
|
import java.net.Socket
|
||||||
@@ -215,9 +216,16 @@ private fun ByteArray.toInetAddress(): InetAddress {
|
|||||||
return InetAddress.getByAddress(this);
|
return InetAddress.getByAddress(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
|
fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket? {
|
||||||
|
ensureNotMainThread()
|
||||||
|
|
||||||
val timeout = 2000
|
val timeout = 2000
|
||||||
|
|
||||||
|
|
||||||
|
val addresses = if(!Settings.instance.casting.allowIpv6) attemptAddresses.filterIsInstance<Inet4Address>() else attemptAddresses;
|
||||||
|
if(addresses.isEmpty())
|
||||||
|
throw IllegalStateException("No valid addresses found (ipv6: ${(if(Settings.instance.casting.allowIpv6) "enabled" else "disabled")})");
|
||||||
|
|
||||||
if (addresses.isEmpty()) {
|
if (addresses.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package com.futo.platformplayer
|
package com.futo.platformplayer
|
||||||
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.states.AnnouncementType
|
import com.futo.platformplayer.states.AnnouncementType
|
||||||
import com.futo.platformplayer.states.StateAnnouncement
|
import com.futo.platformplayer.states.StateAnnouncement
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.polycentric.core.ProcessHandle
|
import com.futo.polycentric.core.ProcessHandle
|
||||||
import com.futo.polycentric.core.Store
|
import com.futo.polycentric.core.Store
|
||||||
import com.futo.polycentric.core.SystemState
|
import com.futo.polycentric.core.SystemState
|
||||||
|
import com.futo.polycentric.core.base64UrlToByteArray
|
||||||
import userpackage.Protocol
|
import userpackage.Protocol
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
@@ -40,33 +40,25 @@ fun Protocol.ImageBundle?.selectHighestResolutionImage(): Protocol.ImageManifest
|
|||||||
return imageManifestsList.filter { it.byteCount < maximumFileSize }.maxByOrNull { abs(it.width * it.height) }
|
return imageManifestsList.filter { it.byteCount < maximumFileSize }.maxByOrNull { abs(it.width * it.height) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun String.getDataLinkFromUrl(): Protocol.URLInfoDataLink? {
|
||||||
|
val urlData = if (this.startsWith("polycentric://")) {
|
||||||
|
this.substring("polycentric://".length)
|
||||||
|
} else this;
|
||||||
|
|
||||||
|
val urlBytes = urlData.base64UrlToByteArray();
|
||||||
|
val urlInfo = Protocol.URLInfo.parseFrom(urlBytes);
|
||||||
|
if (urlInfo.urlType != 4L) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val dataLink = Protocol.URLInfoDataLink.parseFrom(urlInfo.body);
|
||||||
|
return dataLink
|
||||||
|
}
|
||||||
|
|
||||||
fun Protocol.Claim.resolveChannelUrl(): String? {
|
fun Protocol.Claim.resolveChannelUrl(): String? {
|
||||||
return StatePlatform.instance.resolveChannelUrlByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
|
return StatePlatform.instance.resolveChannelUrlByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Protocol.Claim.resolveChannelUrls(): List<String> {
|
fun Protocol.Claim.resolveChannelUrls(): List<String> {
|
||||||
return StatePlatform.instance.resolveChannelUrlsByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
|
return StatePlatform.instance.resolveChannelUrlsByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun ProcessHandle.fullyBackfillServersAnnounceExceptions() {
|
|
||||||
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(system))
|
|
||||||
if (!systemState.servers.contains(PolycentricCache.SERVER)) {
|
|
||||||
Logger.w("Backfill", "Polycentric prod server not added, adding it.")
|
|
||||||
addServer(PolycentricCache.SERVER)
|
|
||||||
}
|
|
||||||
|
|
||||||
val exceptions = fullyBackfillServers()
|
|
||||||
for (pair in exceptions) {
|
|
||||||
val server = pair.key
|
|
||||||
val exception = pair.value
|
|
||||||
|
|
||||||
StateAnnouncement.instance.registerAnnouncement(
|
|
||||||
"backfill-failed",
|
|
||||||
"Backfill failed",
|
|
||||||
"Failed to backfill server $server. $exception",
|
|
||||||
AnnouncementType.SESSION_RECURRING
|
|
||||||
);
|
|
||||||
|
|
||||||
Logger.e("Backfill", "Failed to backfill server $server.", exception)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,9 @@ import java.net.InetAddress
|
|||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.net.URISyntaxException
|
import java.net.URISyntaxException
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
|
||||||
//Syntax sugaring
|
//Syntax sugaring
|
||||||
inline fun <reified T> Any.assume(): T?{
|
inline fun <reified T> Any.assume(): T?{
|
||||||
@@ -33,13 +36,37 @@ fun Boolean?.toYesNo(): String {
|
|||||||
fun InetAddress?.toUrlAddress(): String {
|
fun InetAddress?.toUrlAddress(): String {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
is Inet6Address -> {
|
is Inet6Address -> {
|
||||||
"[${toString()}]"
|
val hostAddr = this.hostAddress ?: throw Exception("Invalid address: hostAddress is null")
|
||||||
|
val index = hostAddr.indexOf('%')
|
||||||
|
if (index != -1) {
|
||||||
|
val addrPart = hostAddr.substring(0, index)
|
||||||
|
val scopeId = hostAddr.substring(index + 1)
|
||||||
|
"[${addrPart}%25${scopeId}]" // %25 is URL-encoded '%'
|
||||||
|
} else {
|
||||||
|
"[$hostAddr]"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
is Inet4Address -> {
|
is Inet4Address -> {
|
||||||
toString()
|
this.hostAddress ?: throw Exception("Invalid address: hostAddress is null")
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
throw Exception("Invalid address type")
|
throw Exception("Invalid address type")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Long?.sToOffsetDateTimeUTC(): OffsetDateTime {
|
||||||
|
if (this == null || this < 0)
|
||||||
|
return OffsetDateTime.MIN
|
||||||
|
if(this > 4070912400)
|
||||||
|
return OffsetDateTime.MAX;
|
||||||
|
return OffsetDateTime.ofInstant(Instant.ofEpochSecond(this), ZoneOffset.UTC)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Long?.msToOffsetDateTimeUTC(): OffsetDateTime {
|
||||||
|
if (this == null || this < 0)
|
||||||
|
return OffsetDateTime.MIN
|
||||||
|
if(this > 4070912400)
|
||||||
|
return OffsetDateTime.MAX;
|
||||||
|
return OffsetDateTime.ofInstant(Instant.ofEpochMilli(this), ZoneOffset.UTC)
|
||||||
}
|
}
|
||||||
@@ -205,7 +205,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
var home = HomeSettings();
|
var home = HomeSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
class HomeSettings {
|
class HomeSettings {
|
||||||
@FormField(R.string.feed_style, FieldForm.DROPDOWN, R.string.may_require_restart, 5)
|
@FormField(R.string.feed_style, FieldForm.DROPDOWN, R.string.may_require_restart, 3)
|
||||||
@DropdownFieldOptionsId(R.array.feed_style)
|
@DropdownFieldOptionsId(R.array.feed_style)
|
||||||
var homeFeedStyle: Int = 1;
|
var homeFeedStyle: Int = 1;
|
||||||
|
|
||||||
@@ -216,6 +216,11 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
return FeedStyle.THUMBNAIL;
|
return FeedStyle.THUMBNAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.show_home_filters, FieldForm.TOGGLE, R.string.show_home_filters_description, 4)
|
||||||
|
var showHomeFilters: Boolean = true;
|
||||||
|
@FormField(R.string.show_home_filters_plugin_names, FieldForm.TOGGLE, R.string.show_home_filters_plugin_names_description, 5)
|
||||||
|
var showHomeFiltersPluginNames: Boolean = false;
|
||||||
|
|
||||||
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
|
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
|
||||||
var previewFeedItems: Boolean = true;
|
var previewFeedItems: Boolean = true;
|
||||||
|
|
||||||
@@ -254,6 +259,9 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
|
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
|
||||||
var progressBar: Boolean = true;
|
var progressBar: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.hide_hidden_from_search, FieldForm.TOGGLE, R.string.hide_hidden_from_search_description, 7)
|
||||||
|
var hidefromSearch: Boolean = false;
|
||||||
|
|
||||||
|
|
||||||
fun getSearchFeedStyle(): FeedStyle {
|
fun getSearchFeedStyle(): FeedStyle {
|
||||||
if(searchFeedStyle == 0)
|
if(searchFeedStyle == 0)
|
||||||
@@ -291,6 +299,9 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.show_subscription_group, FieldForm.TOGGLE, R.string.show_subscription_group_description, 5)
|
@FormField(R.string.show_subscription_group, FieldForm.TOGGLE, R.string.show_subscription_group_description, 5)
|
||||||
var showSubscriptionGroups: Boolean = true;
|
var showSubscriptionGroups: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.use_subscription_exchange, FieldForm.TOGGLE, R.string.use_subscription_exchange_description, 6)
|
||||||
|
var useSubscriptionExchange: Boolean = false;
|
||||||
|
|
||||||
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
|
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
|
||||||
var previewFeedItems: Boolean = true;
|
var previewFeedItems: Boolean = true;
|
||||||
|
|
||||||
@@ -353,7 +364,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
var playback = PlaybackSettings();
|
var playback = PlaybackSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
class PlaybackSettings {
|
class PlaybackSettings {
|
||||||
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, -1)
|
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, -2)
|
||||||
@DropdownFieldOptionsId(R.array.audio_languages)
|
@DropdownFieldOptionsId(R.array.audio_languages)
|
||||||
var primaryLanguage: Int = 0;
|
var primaryLanguage: Int = 0;
|
||||||
|
|
||||||
@@ -377,6 +388,8 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@FormField(R.string.prefer_original_audio, FieldForm.TOGGLE, R.string.prefer_original_audio_description, -1)
|
||||||
|
var preferOriginalAudio: Boolean = true;
|
||||||
|
|
||||||
//= context.resources.getStringArray(R.array.audio_languages)[primaryLanguage];
|
//= context.resources.getStringArray(R.array.audio_languages)[primaryLanguage];
|
||||||
|
|
||||||
@@ -412,15 +425,13 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
var preferredPreviewQuality: Int = 5;
|
var preferredPreviewQuality: Int = 5;
|
||||||
fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);
|
fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);
|
||||||
|
|
||||||
|
|
||||||
@FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4)
|
@FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4)
|
||||||
var simplifySources: Boolean = true;
|
var simplifySources: Boolean = true;
|
||||||
|
|
||||||
@FormField(R.string.auto_rotate, FieldForm.DROPDOWN, -1, 5)
|
@FormField(R.string.always_allow_reverse_landscape_auto_rotate, FieldForm.TOGGLE, R.string.always_allow_reverse_landscape_auto_rotate_description, 5)
|
||||||
@DropdownFieldOptionsId(R.array.system_enabled_disabled_array)
|
var alwaysAllowReverseLandscapeAutoRotate: Boolean = true
|
||||||
var autoRotate: Int = 2;
|
|
||||||
|
|
||||||
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 7)
|
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 6)
|
||||||
@DropdownFieldOptionsId(R.array.player_background_behavior)
|
@DropdownFieldOptionsId(R.array.player_background_behavior)
|
||||||
var backgroundPlay: Int = 2;
|
var backgroundPlay: Int = 2;
|
||||||
|
|
||||||
@@ -488,6 +499,22 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@FormField(R.string.delete_watchlist_on_finish, FieldForm.TOGGLE, R.string.delete_watchlist_on_finish_description, 22)
|
@FormField(R.string.delete_watchlist_on_finish, FieldForm.TOGGLE, R.string.delete_watchlist_on_finish_description, 22)
|
||||||
var deleteFromWatchLaterAuto: Boolean = true;
|
var deleteFromWatchLaterAuto: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.seek_offset, FieldForm.DROPDOWN, R.string.seek_offset_description, 23)
|
||||||
|
@DropdownFieldOptionsId(R.array.seek_offset_duration)
|
||||||
|
var seekOffset: Int = 2;
|
||||||
|
|
||||||
|
fun getSeekOffset(): Long {
|
||||||
|
return when(seekOffset) {
|
||||||
|
0 -> 3_000L;
|
||||||
|
1 -> 5_000L;
|
||||||
|
2 -> 10_000L;
|
||||||
|
3 -> 20_000L;
|
||||||
|
4 -> 30_000L;
|
||||||
|
5 -> 60_000L;
|
||||||
|
else -> 10_000L;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.comments, "group", R.string.comments_description, 6)
|
@FormField(R.string.comments, "group", R.string.comments_description, 6)
|
||||||
@@ -572,10 +599,15 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var keepScreenOn: Boolean = true;
|
var keepScreenOn: Boolean = true;
|
||||||
|
|
||||||
@FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 1)
|
@FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 3)
|
||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var alwaysProxyRequests: Boolean = false;
|
var alwaysProxyRequests: Boolean = false;
|
||||||
|
|
||||||
|
|
||||||
|
@FormField(R.string.allow_ipv6, FieldForm.TOGGLE, R.string.allow_ipv6_description, 4)
|
||||||
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
|
var allowIpv6: Boolean = true;
|
||||||
|
|
||||||
/*TODO: Should we have a different casting quality?
|
/*TODO: Should we have a different casting quality?
|
||||||
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
|
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
|
||||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
||||||
@@ -643,6 +675,9 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@Serializable
|
@Serializable
|
||||||
class Plugins {
|
class Plugins {
|
||||||
|
|
||||||
|
@FormField(R.string.check_disabled_plugin_updates, FieldForm.TOGGLE, R.string.check_disabled_plugin_updates_description, -1)
|
||||||
|
var checkDisabledPluginsForUpdates: Boolean = false;
|
||||||
|
|
||||||
@FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0)
|
@FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0)
|
||||||
var clearCookiesOnLogout: Boolean = true;
|
var clearCookiesOnLogout: Boolean = true;
|
||||||
|
|
||||||
@@ -863,11 +898,13 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
class Other {
|
class Other {
|
||||||
@FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2)
|
@FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2)
|
||||||
var playlistDeleteConfirmation: Boolean = true;
|
var playlistDeleteConfirmation: Boolean = true;
|
||||||
|
@FormField(R.string.playlist_allow_dups, FieldForm.TOGGLE, R.string.playlist_allow_dups_description, 3)
|
||||||
|
var playlistAllowDups: Boolean = true;
|
||||||
|
|
||||||
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 3)
|
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 4)
|
||||||
var polycentricEnabled: Boolean = true;
|
var polycentricEnabled: Boolean = true;
|
||||||
|
|
||||||
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 4)
|
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 5)
|
||||||
var polycentricLocalCache: Boolean = true;
|
var polycentricLocalCache: Boolean = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -905,7 +942,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@Serializable
|
@Serializable
|
||||||
class Synchronization {
|
class Synchronization {
|
||||||
@FormField(R.string.enabled, FieldForm.TOGGLE, R.string.enabled_description, 1)
|
@FormField(R.string.enabled, FieldForm.TOGGLE, R.string.enabled_description, 1)
|
||||||
var enabled: Boolean = true;
|
var enabled: Boolean = false;
|
||||||
|
|
||||||
@FormField(R.string.broadcast, FieldForm.TOGGLE, R.string.broadcast_description, 1)
|
@FormField(R.string.broadcast, FieldForm.TOGGLE, R.string.broadcast_description, 1)
|
||||||
var broadcast: Boolean = false;
|
var broadcast: Boolean = false;
|
||||||
@@ -915,6 +952,21 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@FormField(R.string.connect_last, FieldForm.TOGGLE, R.string.connect_last_description, 3)
|
@FormField(R.string.connect_last, FieldForm.TOGGLE, R.string.connect_last_description, 3)
|
||||||
var connectLast: Boolean = true;
|
var connectLast: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.discover_through_relay, FieldForm.TOGGLE, R.string.discover_through_relay_description, 3)
|
||||||
|
var discoverThroughRelay: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.pair_through_relay, FieldForm.TOGGLE, R.string.pair_through_relay_description, 3)
|
||||||
|
var pairThroughRelay: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.connect_through_relay, FieldForm.TOGGLE, R.string.connect_through_relay_description, 3)
|
||||||
|
var connectThroughRelay: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.connect_local_direct_through_relay, FieldForm.TOGGLE, R.string.connect_local_direct_through_relay_description, 3)
|
||||||
|
var connectLocalDirectThroughRelay: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.local_connections, FieldForm.TOGGLE, R.string.local_connections_description, 3)
|
||||||
|
var localConnections: Boolean = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.info, FieldForm.GROUP, -1, 21)
|
@FormField(R.string.info, FieldForm.GROUP, -1, 21)
|
||||||
@@ -983,4 +1035,4 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
//endregion
|
//endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.futo.platformplayer
|
package com.futo.platformplayer
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
import androidx.work.Data
|
import androidx.work.Data
|
||||||
import androidx.work.OneTimeWorkRequestBuilder
|
import androidx.work.OneTimeWorkRequestBuilder
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.app.AlertDialog
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.Animatable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.text.Layout
|
import android.text.Layout
|
||||||
import android.text.method.ScrollingMovementMethod
|
import android.text.method.ScrollingMovementMethod
|
||||||
@@ -199,16 +200,21 @@ class UIDialogs {
|
|||||||
dialog.show();
|
dialog.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action) {
|
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
|
||||||
|
return showDialog(context, icon, false, text, textDetails, code, defaultCloseAction, *actions);
|
||||||
|
}
|
||||||
|
fun showDialog(context: Context, icon: Int, animated: Boolean, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
|
||||||
val builder = AlertDialog.Builder(context);
|
val builder = AlertDialog.Builder(context);
|
||||||
val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null);
|
val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null);
|
||||||
builder.setView(view);
|
builder.setView(view);
|
||||||
|
builder.setCancelable(defaultCloseAction > -2);
|
||||||
val dialog = builder.create();
|
val dialog = builder.create();
|
||||||
registerDialogOpened(dialog);
|
registerDialogOpened(dialog);
|
||||||
|
|
||||||
view.findViewById<ImageView>(R.id.dialog_icon).apply {
|
view.findViewById<ImageView>(R.id.dialog_icon).apply {
|
||||||
this.setImageResource(icon);
|
this.setImageResource(icon);
|
||||||
|
if(animated)
|
||||||
|
this.drawable.assume<Animatable, Unit> { it.start() };
|
||||||
}
|
}
|
||||||
view.findViewById<TextView>(R.id.dialog_text).apply {
|
view.findViewById<TextView>(R.id.dialog_text).apply {
|
||||||
this.text = text;
|
this.text = text;
|
||||||
@@ -275,6 +281,7 @@ class UIDialogs {
|
|||||||
registerDialogClosed(dialog);
|
registerDialogClosed(dialog);
|
||||||
}
|
}
|
||||||
dialog.show();
|
dialog.show();
|
||||||
|
return dialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showGeneralErrorDialog(context: Context, msg: String, ex: Throwable? = null, button: String = "Ok", onOk: (()->Unit)? = null) {
|
fun showGeneralErrorDialog(context: Context, msg: String, ex: Throwable? = null, button: String = "Ok", onOk: (()->Unit)? = null) {
|
||||||
@@ -368,8 +375,8 @@ class UIDialogs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showChangelogDialog(context: Context, lastVersion: Int) {
|
fun showChangelogDialog(context: Context, lastVersion: Int, changelogs: Map<Int, String>? = null) {
|
||||||
val dialog = ChangelogDialog(context);
|
val dialog = ChangelogDialog(context, changelogs);
|
||||||
registerDialogOpened(dialog);
|
registerDialogOpened(dialog);
|
||||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||||
dialog.show();
|
dialog.show();
|
||||||
|
|||||||
@@ -4,8 +4,14 @@ import android.app.NotificationManager
|
|||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.media3.exoplayer.hls.playlist.DefaultHlsPlaylistParserFactory
|
||||||
|
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist
|
||||||
|
import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.activities.SettingsActivity
|
import com.futo.platformplayer.activities.SettingsActivity
|
||||||
@@ -37,6 +43,9 @@ import com.futo.platformplayer.models.Playlist
|
|||||||
import com.futo.platformplayer.models.Subscription
|
import com.futo.platformplayer.models.Subscription
|
||||||
import com.futo.platformplayer.models.SubscriptionGroup
|
import com.futo.platformplayer.models.SubscriptionGroup
|
||||||
import com.futo.platformplayer.parsers.HLS
|
import com.futo.platformplayer.parsers.HLS
|
||||||
|
import com.futo.platformplayer.parsers.HLS.MediaRendition
|
||||||
|
import com.futo.platformplayer.parsers.HLS.StreamInfo
|
||||||
|
import com.futo.platformplayer.parsers.HLS.VariantPlaylistReference
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateDownloads
|
import com.futo.platformplayer.states.StateDownloads
|
||||||
import com.futo.platformplayer.states.StateHistory
|
import com.futo.platformplayer.states.StateHistory
|
||||||
@@ -63,6 +72,8 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
|
||||||
class UISlideOverlays {
|
class UISlideOverlays {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -79,6 +90,36 @@ class UISlideOverlays {
|
|||||||
return menu;
|
return menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showQueueOptionsOverlay(context: Context, container: ViewGroup) {
|
||||||
|
UISlideOverlays.showOverlay(container, "Queue options", null, {
|
||||||
|
|
||||||
|
}, SlideUpMenuItem(context, R.drawable.ic_playlist, "Save as playlist", "", "Creates a new playlist with queue as videos", null, {
|
||||||
|
val nameInput = SlideUpMenuTextInput(container.context, container.context.getString(R.string.name));
|
||||||
|
val addPlaylistOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.create_new_playlist), container.context.getString(R.string.ok), false, nameInput);
|
||||||
|
|
||||||
|
addPlaylistOverlay.onOK.subscribe {
|
||||||
|
val text = nameInput.text.trim()
|
||||||
|
if (text.isBlank()) {
|
||||||
|
return@subscribe;
|
||||||
|
}
|
||||||
|
|
||||||
|
addPlaylistOverlay.hide();
|
||||||
|
nameInput.deactivate();
|
||||||
|
nameInput.clear();
|
||||||
|
StatePlayer.instance.saveQueueAsPlaylist(text);
|
||||||
|
UIDialogs.appToast("Playlist [${text}] created");
|
||||||
|
};
|
||||||
|
|
||||||
|
addPlaylistOverlay.onCancel.subscribe {
|
||||||
|
nameInput.deactivate();
|
||||||
|
nameInput.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
addPlaylistOverlay.show();
|
||||||
|
nameInput.activate();
|
||||||
|
}, false));
|
||||||
|
}
|
||||||
|
|
||||||
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup): SlideUpMenuOverlay {
|
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup): SlideUpMenuOverlay {
|
||||||
val items = arrayListOf<View>();
|
val items = arrayListOf<View>();
|
||||||
|
|
||||||
@@ -269,6 +310,7 @@ class UISlideOverlays {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
fun showHlsPicker(video: IPlatformVideoDetails, source: Any, sourceUrl: String, container: ViewGroup): SlideUpMenuOverlay {
|
fun showHlsPicker(video: IPlatformVideoDetails, source: Any, sourceUrl: String, container: ViewGroup): SlideUpMenuOverlay {
|
||||||
val items = arrayListOf<View>(LoaderView(container.context))
|
val items = arrayListOf<View>(LoaderView(container.context))
|
||||||
val slideUpMenuOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items)
|
val slideUpMenuOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items)
|
||||||
@@ -280,6 +322,8 @@ class UISlideOverlays {
|
|||||||
val masterPlaylistContent = masterPlaylistResponse.body?.string()
|
val masterPlaylistContent = masterPlaylistResponse.body?.string()
|
||||||
?: throw Exception("Master playlist content is empty")
|
?: throw Exception("Master playlist content is empty")
|
||||||
|
|
||||||
|
val resolvedPlaylistUrl = masterPlaylistResponse.url
|
||||||
|
|
||||||
val videoButtons = arrayListOf<SlideUpMenuItem>()
|
val videoButtons = arrayListOf<SlideUpMenuItem>()
|
||||||
val audioButtons = arrayListOf<SlideUpMenuItem>()
|
val audioButtons = arrayListOf<SlideUpMenuItem>()
|
||||||
//TODO: Implement subtitles
|
//TODO: Implement subtitles
|
||||||
@@ -292,53 +336,103 @@ class UISlideOverlays {
|
|||||||
|
|
||||||
val masterPlaylist: HLS.MasterPlaylist
|
val masterPlaylist: HLS.MasterPlaylist
|
||||||
try {
|
try {
|
||||||
masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl)
|
val inputStream = ByteArrayInputStream(masterPlaylistContent.toByteArray())
|
||||||
|
val playlist = DefaultHlsPlaylistParserFactory().createPlaylistParser()
|
||||||
|
.parse(sourceUrl.toUri(), inputStream)
|
||||||
|
|
||||||
masterPlaylist.getAudioSources().forEach { it ->
|
if (playlist is HlsMediaPlaylist) {
|
||||||
|
if (source is IHLSManifestAudioSource) {
|
||||||
|
val variant = HLS.mediaRenditionToVariant(MediaRendition("AUDIO", playlist.baseUri, "Single Playlist", null, null, null, null, null))!!
|
||||||
|
|
||||||
val estSize = VideoHelper.estimateSourceSize(it);
|
val estSize = VideoHelper.estimateSourceSize(variant);
|
||||||
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||||
audioButtons.add(SlideUpMenuItem(
|
audioButtons.add(SlideUpMenuItem(
|
||||||
container.context,
|
container.context,
|
||||||
R.drawable.ic_music,
|
R.drawable.ic_music,
|
||||||
it.name,
|
variant.name,
|
||||||
listOf(it.language, it.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "),
|
listOf(variant.language, variant.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "),
|
||||||
(prefix + it.codec).trim(),
|
(prefix + variant.codec).trim(),
|
||||||
tag = it,
|
tag = variant,
|
||||||
call = {
|
call = {
|
||||||
selectedAudioVariant = it
|
selectedAudioVariant = variant
|
||||||
slideUpMenuOverlay.selectOption(audioButtons, it)
|
slideUpMenuOverlay.selectOption(audioButtons, variant)
|
||||||
|
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
val variant = HLS.variantReferenceToVariant(VariantPlaylistReference(playlist.baseUri, StreamInfo(null, null, null, null, null, null, null, null, null)))
|
||||||
|
|
||||||
|
val estSize = VideoHelper.estimateSourceSize(variant);
|
||||||
|
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||||
|
videoButtons.add(SlideUpMenuItem(
|
||||||
|
container.context,
|
||||||
|
R.drawable.ic_movie,
|
||||||
|
variant.name,
|
||||||
|
"${variant.width}x${variant.height}",
|
||||||
|
(prefix + variant.codec).trim(),
|
||||||
|
tag = variant,
|
||||||
|
call = {
|
||||||
|
selectedVideoVariant = variant
|
||||||
|
slideUpMenuOverlay.selectOption(videoButtons, variant)
|
||||||
|
if (audioButtons.isEmpty()){
|
||||||
|
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
))
|
||||||
|
}
|
||||||
|
} else if (playlist is HlsMultivariantPlaylist) {
|
||||||
|
masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, resolvedPlaylistUrl)
|
||||||
|
|
||||||
|
masterPlaylist.getAudioSources().forEach { it ->
|
||||||
|
|
||||||
|
val estSize = VideoHelper.estimateSourceSize(it);
|
||||||
|
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||||
|
audioButtons.add(SlideUpMenuItem(
|
||||||
|
container.context,
|
||||||
|
R.drawable.ic_music,
|
||||||
|
it.name,
|
||||||
|
listOf(it.language, it.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "),
|
||||||
|
(prefix + it.codec).trim(),
|
||||||
|
tag = it,
|
||||||
|
call = {
|
||||||
|
selectedAudioVariant = it
|
||||||
|
slideUpMenuOverlay.selectOption(audioButtons, it)
|
||||||
|
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/*masterPlaylist.getSubtitleSources().forEach { it ->
|
||||||
|
subtitleButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, listOf(it.format).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "), it, {
|
||||||
|
selectedSubtitleVariant = it
|
||||||
|
slideUpMenuOverlay.selectOption(subtitleButtons, it)
|
||||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||||
},
|
}, false))
|
||||||
invokeParent = false
|
}*/
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
/*masterPlaylist.getSubtitleSources().forEach { it ->
|
masterPlaylist.getVideoSources().forEach {
|
||||||
subtitleButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, listOf(it.format).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "), it, {
|
val estSize = VideoHelper.estimateSourceSize(it);
|
||||||
selectedSubtitleVariant = it
|
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||||
slideUpMenuOverlay.selectOption(subtitleButtons, it)
|
videoButtons.add(SlideUpMenuItem(
|
||||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
container.context,
|
||||||
}, false))
|
R.drawable.ic_movie,
|
||||||
}*/
|
it.name,
|
||||||
|
"${it.width}x${it.height}",
|
||||||
masterPlaylist.getVideoSources().forEach {
|
(prefix + it.codec).trim(),
|
||||||
val estSize = VideoHelper.estimateSourceSize(it);
|
tag = it,
|
||||||
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
call = {
|
||||||
videoButtons.add(SlideUpMenuItem(
|
selectedVideoVariant = it
|
||||||
container.context,
|
slideUpMenuOverlay.selectOption(videoButtons, it)
|
||||||
R.drawable.ic_movie,
|
if (audioButtons.isEmpty()){
|
||||||
it.name,
|
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||||
"${it.width}x${it.height}",
|
}
|
||||||
(prefix + it.codec).trim(),
|
},
|
||||||
tag = it,
|
invokeParent = false
|
||||||
call = {
|
))
|
||||||
selectedVideoVariant = it
|
}
|
||||||
slideUpMenuOverlay.selectOption(videoButtons, it)
|
|
||||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
|
||||||
},
|
|
||||||
invokeParent = false
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val newItems = arrayListOf<View>()
|
val newItems = arrayListOf<View>()
|
||||||
@@ -366,11 +460,11 @@ class UISlideOverlays {
|
|||||||
if (masterPlaylistContent.lines().any { it.startsWith("#EXTINF:") }) {
|
if (masterPlaylistContent.lines().any { it.startsWith("#EXTINF:") }) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (source is IHLSManifestSource) {
|
if (source is IHLSManifestSource) {
|
||||||
StateDownloads.instance.download(video, HLSVariantVideoUrlSource("variant", 0, 0, "application/vnd.apple.mpegurl", "", null, 0, false, sourceUrl), null, null)
|
StateDownloads.instance.download(video, HLSVariantVideoUrlSource("variant", 0, 0, "application/vnd.apple.mpegurl", "", null, 0, false, resolvedPlaylistUrl), null, null)
|
||||||
UIDialogs.toast(container.context, "Variant video HLS playlist download started")
|
UIDialogs.toast(container.context, "Variant video HLS playlist download started")
|
||||||
slideUpMenuOverlay.hide()
|
slideUpMenuOverlay.hide()
|
||||||
} else if (source is IHLSManifestAudioSource) {
|
} else if (source is IHLSManifestAudioSource) {
|
||||||
StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, sourceUrl), null)
|
StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, false, resolvedPlaylistUrl), null)
|
||||||
UIDialogs.toast(container.context, "Variant audio HLS playlist download started")
|
UIDialogs.toast(container.context, "Variant audio HLS playlist download started")
|
||||||
slideUpMenuOverlay.hide()
|
slideUpMenuOverlay.hide()
|
||||||
} else {
|
} else {
|
||||||
@@ -417,7 +511,7 @@ class UISlideOverlays {
|
|||||||
}
|
}
|
||||||
|
|
||||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.video), videoSources,
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.video), videoSources,
|
||||||
listOf(listOf(SlideUpMenuItem(
|
listOf((if (audioSources != null) listOf(SlideUpMenuItem(
|
||||||
container.context,
|
container.context,
|
||||||
R.drawable.ic_movie,
|
R.drawable.ic_movie,
|
||||||
container.context.getString(R.string.none),
|
container.context.getString(R.string.none),
|
||||||
@@ -430,7 +524,7 @@ class UISlideOverlays {
|
|||||||
menu?.setOk(container.context.getString(R.string.download));
|
menu?.setOk(container.context.getString(R.string.download));
|
||||||
},
|
},
|
||||||
invokeParent = false
|
invokeParent = false
|
||||||
)) +
|
)) else listOf()) +
|
||||||
videoSources
|
videoSources
|
||||||
.filter { it.isDownloadable() }
|
.filter { it.isDownloadable() }
|
||||||
.map {
|
.map {
|
||||||
@@ -652,6 +746,10 @@ class UISlideOverlays {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if(!Settings.instance.downloads.shouldDownload()) {
|
||||||
|
UIDialogs.appToast("Download will start when you're back on wifi.\n" +
|
||||||
|
"(You can change this in settings)", true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return menu.apply { show() };
|
return menu.apply { show() };
|
||||||
@@ -895,7 +993,8 @@ class UISlideOverlays {
|
|||||||
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
|
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
|
||||||
tag = "",
|
tag = "",
|
||||||
call = {
|
call = {
|
||||||
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
|
if(StatePlaylists.instance.addToPlaylist(lastUpdated.id, video))
|
||||||
|
UIDialogs.appToast("Added to playlist [${lastUpdated?.name}]", false);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
@@ -906,7 +1005,7 @@ class UISlideOverlays {
|
|||||||
val watchLater = StatePlaylists.instance.getWatchLater();
|
val watchLater = StatePlaylists.instance.getWatchLater();
|
||||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
|
||||||
(listOf(
|
(listOf(
|
||||||
if(!isLimited)
|
if(!isLimited && !video.isLive)
|
||||||
SlideUpMenuItem(
|
SlideUpMenuItem(
|
||||||
container.context,
|
container.context,
|
||||||
R.drawable.ic_download,
|
R.drawable.ic_download,
|
||||||
@@ -947,26 +1046,30 @@ class UISlideOverlays {
|
|||||||
+ actions).filterNotNull()
|
+ actions).filterNotNull()
|
||||||
));
|
));
|
||||||
items.add(
|
items.add(
|
||||||
SlideUpMenuGroup(container.context, container.context.getString(R.string.add_to), "addto",
|
SlideUpMenuGroup(
|
||||||
SlideUpMenuItem(container.context,
|
container.context, container.context.getString(R.string.add_to), "addto",
|
||||||
|
SlideUpMenuItem(
|
||||||
|
container.context,
|
||||||
R.drawable.ic_queue_add,
|
R.drawable.ic_queue_add,
|
||||||
container.context.getString(R.string.add_to_queue),
|
container.context.getString(R.string.add_to_queue),
|
||||||
"${queue.size} " + container.context.getString(R.string.videos),
|
"${queue.size} " + container.context.getString(R.string.videos),
|
||||||
tag = "queue",
|
tag = "queue",
|
||||||
call = { StatePlayer.instance.addToQueue(video); }),
|
call = { StatePlayer.instance.addToQueue(video); }),
|
||||||
SlideUpMenuItem(container.context,
|
SlideUpMenuItem(
|
||||||
|
container.context,
|
||||||
R.drawable.ic_watchlist_add,
|
R.drawable.ic_watchlist_add,
|
||||||
"${container.context.getString(R.string.add_to)} " + StatePlayer.TYPE_WATCHLATER + "",
|
"${container.context.getString(R.string.add_to)} " + StatePlayer.TYPE_WATCHLATER + "",
|
||||||
"${watchLater.size} " + container.context.getString(R.string.videos),
|
"${watchLater.size} " + container.context.getString(R.string.videos),
|
||||||
tag = "watch later",
|
tag = "watch later",
|
||||||
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true); }),
|
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true); }),
|
||||||
SlideUpMenuItem(container.context,
|
SlideUpMenuItem(
|
||||||
|
container.context,
|
||||||
R.drawable.ic_history,
|
R.drawable.ic_history,
|
||||||
container.context.getString(R.string.add_to_history),
|
container.context.getString(R.string.add_to_history),
|
||||||
"Mark as watched",
|
"Mark as watched",
|
||||||
tag = "history",
|
tag = "history",
|
||||||
call = { StateHistory.instance.markAsWatched(video); }),
|
call = { StateHistory.instance.markAsWatched(video); }),
|
||||||
));
|
));
|
||||||
|
|
||||||
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
||||||
playlistItems.add(SlideUpMenuItem(
|
playlistItems.add(SlideUpMenuItem(
|
||||||
@@ -991,7 +1094,8 @@ class UISlideOverlays {
|
|||||||
"${playlist.videos.size} " + container.context.getString(R.string.videos),
|
"${playlist.videos.size} " + container.context.getString(R.string.videos),
|
||||||
tag = "",
|
tag = "",
|
||||||
call = {
|
call = {
|
||||||
StatePlaylists.instance.addToPlaylist(playlist.id, video);
|
if(StatePlaylists.instance.addToPlaylist(playlist.id, video))
|
||||||
|
UIDialogs.appToast("Added to playlist [${playlist.name}]", false);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -1018,7 +1122,8 @@ class UISlideOverlays {
|
|||||||
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
|
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
|
||||||
tag = "",
|
tag = "",
|
||||||
call = {
|
call = {
|
||||||
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
|
if(StatePlaylists.instance.addToPlaylist(lastUpdated.id, video))
|
||||||
|
UIDialogs.appToast("Added to playlist [${lastUpdated?.name}]", false);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
@@ -1028,20 +1133,26 @@ class UISlideOverlays {
|
|||||||
val queue = StatePlayer.instance.getQueue();
|
val queue = StatePlayer.instance.getQueue();
|
||||||
val watchLater = StatePlaylists.instance.getWatchLater();
|
val watchLater = StatePlaylists.instance.getWatchLater();
|
||||||
items.add(
|
items.add(
|
||||||
SlideUpMenuGroup(container.context, container.context.getString(R.string.other), "other",
|
SlideUpMenuGroup(
|
||||||
SlideUpMenuItem(container.context,
|
container.context, container.context.getString(R.string.other), "other",
|
||||||
|
SlideUpMenuItem(
|
||||||
|
container.context,
|
||||||
R.drawable.ic_queue_add,
|
R.drawable.ic_queue_add,
|
||||||
container.context.getString(R.string.queue),
|
container.context.getString(R.string.queue),
|
||||||
"${queue.size} " + container.context.getString(R.string.videos),
|
"${queue.size} " + container.context.getString(R.string.videos),
|
||||||
tag = "queue",
|
tag = "queue",
|
||||||
call = { StatePlayer.instance.addToQueue(video); }),
|
call = { StatePlayer.instance.addToQueue(video); }),
|
||||||
SlideUpMenuItem(container.context,
|
SlideUpMenuItem(
|
||||||
|
container.context,
|
||||||
R.drawable.ic_watchlist_add,
|
R.drawable.ic_watchlist_add,
|
||||||
StatePlayer.TYPE_WATCHLATER,
|
StatePlayer.TYPE_WATCHLATER,
|
||||||
"${watchLater.size} " + container.context.getString(R.string.videos),
|
"${watchLater.size} " + container.context.getString(R.string.videos),
|
||||||
tag = "watch later",
|
tag = "watch later",
|
||||||
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true); }),
|
call = {
|
||||||
)
|
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true))
|
||||||
|
UIDialogs.appToast("Added to watch later", false);
|
||||||
|
}),
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
||||||
@@ -1067,7 +1178,8 @@ class UISlideOverlays {
|
|||||||
"${playlist.videos.size} " + container.context.getString(R.string.videos),
|
"${playlist.videos.size} " + container.context.getString(R.string.videos),
|
||||||
tag = "",
|
tag = "",
|
||||||
call = {
|
call = {
|
||||||
StatePlaylists.instance.addToPlaylist(playlist.id, video);
|
if(StatePlaylists.instance.addToPlaylist(playlist.id, video))
|
||||||
|
UIDialogs.appToast("Added to playlist [${playlist.name}]", false);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -1078,8 +1190,8 @@ class UISlideOverlays {
|
|||||||
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.add_to), null, true, items).apply { show() };
|
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.add_to), null, true, items).apply { show() };
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showFiltersOverlay(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>, isChannelSearch: Boolean = false): SlideUpMenuFilters {
|
fun showFiltersOverlay(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>): SlideUpMenuFilters {
|
||||||
val overlay = SlideUpMenuFilters(lifecycleScope, container, enabledClientsIds, filterValues, isChannelSearch);
|
val overlay = SlideUpMenuFilters(lifecycleScope, container, enabledClientsIds, filterValues);
|
||||||
overlay.show();
|
overlay.show();
|
||||||
return overlay;
|
return overlay;
|
||||||
}
|
}
|
||||||
@@ -1109,7 +1221,7 @@ class UISlideOverlays {
|
|||||||
container.context.getString(R.string.decide_which_buttons_should_be_pinned),
|
container.context.getString(R.string.decide_which_buttons_should_be_pinned),
|
||||||
tag = "",
|
tag = "",
|
||||||
call = {
|
call = {
|
||||||
showOrderOverlay(container, container.context.getString(R.string.select_your_pins_in_order), (visible + hidden).map { Pair(it.text.text.toString(), it.tagRef!!) }) {
|
showOrderOverlay(container, container.context.getString(R.string.select_your_pins_in_order), (visible + hidden).map { Pair(it.text.text.toString(), it.tagRef!!) }, {
|
||||||
val selected = it
|
val selected = it
|
||||||
.map { x -> visible.find { it.tagRef == x } ?: hidden.find { it.tagRef == x } }
|
.map { x -> visible.find { it.tagRef == x } ?: hidden.find { it.tagRef == x } }
|
||||||
.filter { it != null }
|
.filter { it != null }
|
||||||
@@ -1117,7 +1229,7 @@ class UISlideOverlays {
|
|||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
onPinnedbuttons?.invoke(selected + (visible + hidden).filter { !selected.contains(it) });
|
onPinnedbuttons?.invoke(selected + (visible + hidden).filter { !selected.contains(it) });
|
||||||
}
|
});
|
||||||
},
|
},
|
||||||
invokeParent = false
|
invokeParent = false
|
||||||
))
|
))
|
||||||
@@ -1125,29 +1237,40 @@ class UISlideOverlays {
|
|||||||
|
|
||||||
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.more_options), null, true, *views).apply { show() };
|
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.more_options), null, true, *views).apply { show() };
|
||||||
}
|
}
|
||||||
|
fun showOrderOverlay(container: ViewGroup, title: String, options: List<Pair<String, Any>>, onOrdered: (List<Any>)->Unit, description: String? = null) {
|
||||||
fun showOrderOverlay(container: ViewGroup, title: String, options: List<Pair<String, Any>>, onOrdered: (List<Any>)->Unit) {
|
|
||||||
val selection: MutableList<Any> = mutableListOf();
|
val selection: MutableList<Any> = mutableListOf();
|
||||||
|
|
||||||
var overlay: SlideUpMenuOverlay? = null;
|
var overlay: SlideUpMenuOverlay? = null;
|
||||||
|
|
||||||
overlay = SlideUpMenuOverlay(container.context, container, title, container.context.getString(R.string.save), true,
|
overlay = SlideUpMenuOverlay(container.context, container, title, container.context.getString(R.string.save), true,
|
||||||
options.map { SlideUpMenuItem(
|
listOf(
|
||||||
|
if(!description.isNullOrEmpty()) SlideUpMenuGroup(container.context, "", description, "", listOf()) else null,
|
||||||
|
).filterNotNull() +
|
||||||
|
(options.map { SlideUpMenuItem(
|
||||||
container.context,
|
container.context,
|
||||||
R.drawable.ic_move_up,
|
R.drawable.ic_move_up,
|
||||||
it.first,
|
it.first,
|
||||||
"",
|
"",
|
||||||
tag = it.second,
|
tag = it.second,
|
||||||
call = {
|
call = {
|
||||||
|
val overlayItem = overlay?.getSlideUpItemByTag(it.second);
|
||||||
if(overlay!!.selectOption(null, it.second, true, true)) {
|
if(overlay!!.selectOption(null, it.second, true, true)) {
|
||||||
if(!selection.contains(it.second))
|
if(!selection.contains(it.second)) {
|
||||||
selection.add(it.second);
|
selection.add(it.second);
|
||||||
} else
|
if(overlayItem != null) {
|
||||||
|
overlayItem.setSubText(selection.indexOf(it.second).toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
selection.remove(it.second);
|
selection.remove(it.second);
|
||||||
|
if(overlayItem != null) {
|
||||||
|
overlayItem.setSubText("");
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
invokeParent = false
|
invokeParent = false
|
||||||
)
|
)
|
||||||
});
|
}));
|
||||||
overlay.onOK.subscribe {
|
overlay.onOK.subscribe {
|
||||||
onOrdered.invoke(selection);
|
onOrdered.invoke(selection);
|
||||||
overlay.hide();
|
overlay.hide();
|
||||||
|
|||||||
@@ -27,14 +27,23 @@ import com.futo.platformplayer.logging.Logger
|
|||||||
import com.futo.platformplayer.models.PlatformVideoWithTime
|
import com.futo.platformplayer.models.PlatformVideoWithTime
|
||||||
import com.futo.platformplayer.others.PlatformLinkMovementMethod
|
import com.futo.platformplayer.others.PlatformLinkMovementMethod
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.File
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
import java.net.Inet4Address
|
||||||
|
import java.net.Inet6Address
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.net.InterfaceAddress
|
||||||
|
import java.net.NetworkInterface
|
||||||
|
import java.net.SocketException
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder
|
import java.security.SecureRandom
|
||||||
|
import java.time.OffsetDateTime
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.ThreadLocalRandom
|
import java.util.concurrent.ThreadLocalRandom
|
||||||
|
import java.util.zip.GZIPInputStream
|
||||||
|
import java.util.zip.GZIPOutputStream
|
||||||
|
|
||||||
private val _allowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ";
|
private val _allowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ";
|
||||||
fun getRandomString(sizeOfRandomString: Int): String {
|
fun getRandomString(sizeOfRandomString: Int): String {
|
||||||
@@ -66,7 +75,14 @@ fun warnIfMainThread(context: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun ensureNotMainThread() {
|
fun ensureNotMainThread() {
|
||||||
if (Looper.myLooper() == Looper.getMainLooper()) {
|
val isMainLooper = try {
|
||||||
|
Looper.myLooper() == Looper.getMainLooper()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
//Ignore, for unit tests where its not mocked
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMainLooper) {
|
||||||
Logger.e("Utility", "Throwing exception because a function that should not be called on main thread, is called on main thread")
|
Logger.e("Utility", "Throwing exception because a function that should not be called on main thread, is called on main thread")
|
||||||
throw IllegalStateException("Cannot run on main thread")
|
throw IllegalStateException("Cannot run on main thread")
|
||||||
}
|
}
|
||||||
@@ -269,7 +285,7 @@ fun <T> findNewIndex(originalArr: List<T>, newArr: List<T>, item: T): Int{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(newIndex < 0)
|
if(newIndex < 0)
|
||||||
return originalArr.size;
|
return newArr.size;
|
||||||
else
|
else
|
||||||
return newIndex;
|
return newIndex;
|
||||||
}
|
}
|
||||||
@@ -279,3 +295,140 @@ fun ByteBuffer.toUtf8String(): String {
|
|||||||
get(remainingBytes)
|
get(remainingBytes)
|
||||||
return String(remainingBytes, Charsets.UTF_8)
|
return String(remainingBytes, Charsets.UTF_8)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun generateReadablePassword(length: Int): String {
|
||||||
|
val validChars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789"
|
||||||
|
val secureRandom = SecureRandom()
|
||||||
|
val randomBytes = ByteArray(length)
|
||||||
|
secureRandom.nextBytes(randomBytes)
|
||||||
|
val sb = StringBuilder(length)
|
||||||
|
for (byte in randomBytes) {
|
||||||
|
val index = (byte.toInt() and 0xFF) % validChars.length
|
||||||
|
sb.append(validChars[index])
|
||||||
|
}
|
||||||
|
return sb.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ByteArray.toGzip(): ByteArray {
|
||||||
|
if (this == null || this.isEmpty()) return ByteArray(0)
|
||||||
|
|
||||||
|
val gzipTimeStart = OffsetDateTime.now();
|
||||||
|
|
||||||
|
val outputStream = ByteArrayOutputStream()
|
||||||
|
GZIPOutputStream(outputStream).use { gzip ->
|
||||||
|
gzip.write(this)
|
||||||
|
}
|
||||||
|
val result = outputStream.toByteArray();
|
||||||
|
Logger.i("Utility", "Gzip compression time: ${gzipTimeStart.getNowDiffMiliseconds()}ms");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ByteArray.fromGzip(): ByteArray {
|
||||||
|
if (this == null || this.isEmpty()) return ByteArray(0)
|
||||||
|
|
||||||
|
val inputStream = ByteArrayInputStream(this)
|
||||||
|
val outputStream = ByteArrayOutputStream()
|
||||||
|
|
||||||
|
GZIPInputStream(inputStream).use { gzip ->
|
||||||
|
val buffer = ByteArray(1024)
|
||||||
|
var bytesRead: Int
|
||||||
|
while (gzip.read(buffer).also { bytesRead = it } != -1) {
|
||||||
|
outputStream.write(buffer, 0, bytesRead)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return outputStream.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findPreferredAddress(): InetAddress? {
|
||||||
|
val candidates = NetworkInterface.getNetworkInterfaces()
|
||||||
|
.toList()
|
||||||
|
.asSequence()
|
||||||
|
.filter(::isUsableInterface)
|
||||||
|
.flatMap { nif ->
|
||||||
|
nif.interfaceAddresses
|
||||||
|
.asSequence()
|
||||||
|
.mapNotNull { ia ->
|
||||||
|
ia.address.takeIf(::isUsableAddress)?.let { addr ->
|
||||||
|
nif to ia
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
|
||||||
|
return candidates
|
||||||
|
.minWithOrNull(
|
||||||
|
compareBy<Pair<NetworkInterface, InterfaceAddress>>(
|
||||||
|
{ addressScore(it.second.address) },
|
||||||
|
{ interfaceScore(it.first) },
|
||||||
|
{ -it.second.networkPrefixLength.toInt() },
|
||||||
|
{ -it.first.mtu }
|
||||||
|
)
|
||||||
|
)?.second?.address
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isUsableInterface(nif: NetworkInterface): Boolean {
|
||||||
|
val name = nif.name.lowercase()
|
||||||
|
return try {
|
||||||
|
// must be up, not loopback/virtual/PtP, have a MAC, not Docker/tun/etc.
|
||||||
|
nif.isUp
|
||||||
|
&& !nif.isLoopback
|
||||||
|
&& !nif.isPointToPoint
|
||||||
|
&& !nif.isVirtual
|
||||||
|
&& !name.startsWith("docker")
|
||||||
|
&& !name.startsWith("veth")
|
||||||
|
&& !name.startsWith("br-")
|
||||||
|
&& !name.startsWith("virbr")
|
||||||
|
&& !name.startsWith("vmnet")
|
||||||
|
&& !name.startsWith("tun")
|
||||||
|
&& !name.startsWith("tap")
|
||||||
|
} catch (e: SocketException) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isUsableAddress(addr: InetAddress): Boolean {
|
||||||
|
return when {
|
||||||
|
addr.isAnyLocalAddress -> false // 0.0.0.0 / ::
|
||||||
|
addr.isLoopbackAddress -> false
|
||||||
|
addr.isLinkLocalAddress -> false // 169.254.x.x or fe80::/10
|
||||||
|
addr.isMulticastAddress -> false
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun interfaceScore(nif: NetworkInterface): Int {
|
||||||
|
val name = nif.name.lowercase()
|
||||||
|
return when {
|
||||||
|
name.matches(Regex("^(eth|enp|eno|ens|em)\\d+")) -> 0
|
||||||
|
name.startsWith("eth") || name.contains("ethernet") -> 0
|
||||||
|
name.matches(Regex("^(wlan|wlp)\\d+")) -> 1
|
||||||
|
name.contains("wi-fi") || name.contains("wifi") -> 1
|
||||||
|
else -> 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addressScore(addr: InetAddress): Int {
|
||||||
|
return when (addr) {
|
||||||
|
is Inet4Address -> {
|
||||||
|
val octets = addr.address.map { it.toInt() and 0xFF }
|
||||||
|
when {
|
||||||
|
octets[0] == 10 -> 0 // 10/8
|
||||||
|
octets[0] == 192 && octets[1] == 168 -> 0 // 192.168/16
|
||||||
|
octets[0] == 172 && octets[1] in 16..31 -> 0 // 172.16–31/12
|
||||||
|
else -> 1 // public IPv4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is Inet6Address -> {
|
||||||
|
// ULA (fc00::/7) vs global vs others
|
||||||
|
val b0 = addr.address[0].toInt() and 0xFF
|
||||||
|
when {
|
||||||
|
(b0 and 0xFE) == 0xFC -> 2 // ULA
|
||||||
|
(b0 and 0xE0) == 0x20 -> 3 // global
|
||||||
|
else -> 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> Enumeration<T>.toList(): List<T> = Collections.list(this)
|
||||||
@@ -10,11 +10,13 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
|
||||||
import com.google.zxing.integration.android.IntentIntegrator
|
import com.google.zxing.integration.android.IntentIntegrator
|
||||||
|
|
||||||
class AddSourceOptionsActivity : AppCompatActivity() {
|
class AddSourceOptionsActivity : AppCompatActivity() {
|
||||||
lateinit var _buttonBack: ImageButton;
|
lateinit var _buttonBack: ImageButton;
|
||||||
|
|
||||||
|
lateinit var _overlayContainer: FrameLayout;
|
||||||
lateinit var _buttonQR: BigButton;
|
lateinit var _buttonQR: BigButton;
|
||||||
lateinit var _buttonBrowse: BigButton;
|
lateinit var _buttonBrowse: BigButton;
|
||||||
lateinit var _buttonURL: BigButton;
|
lateinit var _buttonURL: BigButton;
|
||||||
@@ -54,6 +56,7 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
|||||||
setContentView(R.layout.activity_add_source_options);
|
setContentView(R.layout.activity_add_source_options);
|
||||||
setNavigationBarColorAndIcons();
|
setNavigationBarColorAndIcons();
|
||||||
|
|
||||||
|
_overlayContainer = findViewById(R.id.overlay_container);
|
||||||
_buttonBack = findViewById(R.id.button_back);
|
_buttonBack = findViewById(R.id.button_back);
|
||||||
|
|
||||||
_buttonQR = findViewById(R.id.option_qr);
|
_buttonQR = findViewById(R.id.option_qr);
|
||||||
@@ -81,7 +84,25 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_buttonURL.onClick.subscribe {
|
_buttonURL.onClick.subscribe {
|
||||||
UIDialogs.toast(this, getString(R.string.not_implemented_yet));
|
val nameInput = SlideUpMenuTextInput(this, "ex. https://yourplugin.com/config.json");
|
||||||
|
UISlideOverlays.showOverlay(_overlayContainer, "Enter your url", "Install", {
|
||||||
|
|
||||||
|
val content = nameInput.text;
|
||||||
|
|
||||||
|
val url = if (content.startsWith("https://")) {
|
||||||
|
content
|
||||||
|
} else if (content.startsWith("grayjay://plugin/")) {
|
||||||
|
content.substring("grayjay://plugin/".length)
|
||||||
|
} else {
|
||||||
|
UIDialogs.toast(this, getString(R.string.not_a_plugin_url))
|
||||||
|
return@showOverlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
val intent = Intent(this, AddSourceActivity::class.java).apply {
|
||||||
|
data = Uri.parse(url);
|
||||||
|
};
|
||||||
|
startActivity(intent);
|
||||||
|
}, nameInput)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,7 +113,7 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = "LoginActivity";
|
private val TAG = "LoginActivity";
|
||||||
private val REGEX_LOGIN_BUTTON = Regex("[a-zA-Z\\-\\.#_ ]*");
|
private val REGEX_LOGIN_BUTTON = Regex("[a-zA-Z\\-\\.#:_ ]*");
|
||||||
|
|
||||||
private var _callback: ((SourceAuth?) -> Unit)? = null;
|
private var _callback: ((SourceAuth?) -> Unit)? = null;
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.app.UiModeManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.StrictMode
|
import android.os.StrictMode
|
||||||
import android.os.StrictMode.VmPolicy
|
import android.os.StrictMode.VmPolicy
|
||||||
@@ -19,6 +22,7 @@ import android.widget.ImageView
|
|||||||
import androidx.activity.result.ActivityResult
|
import androidx.activity.result.ActivityResult
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.annotation.OptIn
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
@@ -28,6 +32,8 @@ import androidx.fragment.app.Fragment
|
|||||||
import androidx.fragment.app.FragmentContainerView
|
import androidx.fragment.app.FragmentContainerView
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.whenStateAtLeast
|
||||||
|
import androidx.lifecycle.withStateAtLeast
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import com.futo.platformplayer.BuildConfig
|
import com.futo.platformplayer.BuildConfig
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
@@ -36,7 +42,9 @@ import com.futo.platformplayer.UIDialogs
|
|||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.dp
|
||||||
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.ArticleDetailFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.BrowserFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.BrowserFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.BuyFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.BuyFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
|
||||||
@@ -63,7 +71,9 @@ import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionsFeedFragm
|
|||||||
import com.futo.platformplayer.fragment.mainactivity.main.SuggestionsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.SuggestionsFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.TutorialFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.TutorialFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment.State
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.WatchLaterFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.WatchLaterFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.WebDetailFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.GeneralTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.GeneralTopBarFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.ImportTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.ImportTopBarFragment
|
||||||
@@ -107,6 +117,7 @@ import java.util.LinkedList
|
|||||||
import java.util.Queue
|
import java.util.Queue
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity, IWithResultLauncher {
|
class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
|
|
||||||
//TODO: Move to dimensions
|
//TODO: Move to dimensions
|
||||||
@@ -142,6 +153,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
//Frags Main
|
//Frags Main
|
||||||
lateinit var _fragMainHome: HomeFragment;
|
lateinit var _fragMainHome: HomeFragment;
|
||||||
lateinit var _fragPostDetail: PostDetailFragment;
|
lateinit var _fragPostDetail: PostDetailFragment;
|
||||||
|
lateinit var _fragArticleDetail: ArticleDetailFragment;
|
||||||
|
lateinit var _fragWebDetail: WebDetailFragment;
|
||||||
lateinit var _fragMainVideoSearchResults: ContentSearchResultsFragment;
|
lateinit var _fragMainVideoSearchResults: ContentSearchResultsFragment;
|
||||||
lateinit var _fragMainCreatorSearchResults: CreatorSearchResultsFragment;
|
lateinit var _fragMainCreatorSearchResults: CreatorSearchResultsFragment;
|
||||||
lateinit var _fragMainPlaylistSearchResults: PlaylistSearchResultsFragment;
|
lateinit var _fragMainPlaylistSearchResults: PlaylistSearchResultsFragment;
|
||||||
@@ -181,6 +194,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
private var _isVisible = true;
|
private var _isVisible = true;
|
||||||
private var _wasStopped = false;
|
private var _wasStopped = false;
|
||||||
|
private var _privateModeEnabled = false
|
||||||
|
private var _pictureInPictureEnabled = false
|
||||||
|
private var _isFullscreen = false
|
||||||
|
|
||||||
private val _urlQrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
private val _urlQrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
||||||
@@ -192,7 +208,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
runBlocking {
|
lifecycleScope.launch {
|
||||||
handleUrlAll(content)
|
handleUrlAll(content)
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@@ -258,6 +274,10 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
StateApp.instance.mainAppStarting(this);
|
StateApp.instance.mainAppStarting(this);
|
||||||
|
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
val uiMode = getSystemService(UiModeManager::class.java)
|
||||||
|
uiMode.setApplicationNightMode(UiModeManager.MODE_NIGHT_YES)
|
||||||
|
}
|
||||||
setContentView(R.layout.activity_main);
|
setContentView(R.layout.activity_main);
|
||||||
setNavigationBarColorAndIcons();
|
setNavigationBarColorAndIcons();
|
||||||
if (Settings.instance.playback.allowVideoToGoUnderCutout)
|
if (Settings.instance.playback.allowVideoToGoUnderCutout)
|
||||||
@@ -265,7 +285,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
StatePlatform.instance.updateAvailableClients(this@MainActivity);
|
try {
|
||||||
|
StatePlatform.instance.updateAvailableClients(this@MainActivity);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Unhandled exception in updateAvailableClients", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Preload common files to memory
|
//Preload common files to memory
|
||||||
@@ -309,6 +333,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
_fragMainPlaylist = PlaylistFragment.newInstance();
|
_fragMainPlaylist = PlaylistFragment.newInstance();
|
||||||
_fragMainRemotePlaylist = RemotePlaylistFragment.newInstance();
|
_fragMainRemotePlaylist = RemotePlaylistFragment.newInstance();
|
||||||
_fragPostDetail = PostDetailFragment.newInstance();
|
_fragPostDetail = PostDetailFragment.newInstance();
|
||||||
|
_fragArticleDetail = ArticleDetailFragment.newInstance();
|
||||||
|
_fragWebDetail = WebDetailFragment.newInstance();
|
||||||
_fragWatchlist = WatchLaterFragment.newInstance();
|
_fragWatchlist = WatchLaterFragment.newInstance();
|
||||||
_fragHistory = HistoryFragment.newInstance();
|
_fragHistory = HistoryFragment.newInstance();
|
||||||
_fragSourceDetail = SourceDetailFragment.newInstance();
|
_fragSourceDetail = SourceDetailFragment.newInstance();
|
||||||
@@ -350,22 +376,18 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
_fragMainSubscriptionsFeed.setPreviewsEnabled(true);
|
_fragMainSubscriptionsFeed.setPreviewsEnabled(true);
|
||||||
_fragContainerVideoDetail.visibility = View.INVISIBLE;
|
_fragContainerVideoDetail.visibility = View.INVISIBLE;
|
||||||
updateSegmentPaddings();
|
updateSegmentPaddings();
|
||||||
|
updatePrivateModeVisibility()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
_buttonIncognito = findViewById(R.id.incognito_button);
|
_buttonIncognito = findViewById(R.id.incognito_button);
|
||||||
_buttonIncognito.elevation = -99f;
|
updatePrivateModeVisibility()
|
||||||
_buttonIncognito.alpha = 0f;
|
|
||||||
StateApp.instance.privateModeChanged.subscribe {
|
StateApp.instance.privateModeChanged.subscribe {
|
||||||
//Messing with visibility causes some issues with layout ordering?
|
//Messing with visibility causes some issues with layout ordering?
|
||||||
if (it) {
|
_privateModeEnabled = it
|
||||||
_buttonIncognito.elevation = 99f;
|
updatePrivateModeVisibility()
|
||||||
_buttonIncognito.alpha = 1f;
|
|
||||||
} else {
|
|
||||||
_buttonIncognito.elevation = -99f;
|
|
||||||
_buttonIncognito.alpha = 0f;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_buttonIncognito.setOnClickListener {
|
_buttonIncognito.setOnClickListener {
|
||||||
if (!StateApp.instance.privateMode)
|
if (!StateApp.instance.privateMode)
|
||||||
return@setOnClickListener;
|
return@setOnClickListener;
|
||||||
@@ -382,19 +404,16 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
};
|
};
|
||||||
_fragVideoDetail.onFullscreenChanged.subscribe {
|
_fragVideoDetail.onFullscreenChanged.subscribe {
|
||||||
Logger.i(TAG, "onFullscreenChanged ${it}");
|
Logger.i(TAG, "onFullscreenChanged ${it}");
|
||||||
|
_isFullscreen = it
|
||||||
|
updatePrivateModeVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
if (it) {
|
_fragVideoDetail.onMinimize.subscribe {
|
||||||
_buttonIncognito.elevation = -99f;
|
updatePrivateModeVisibility()
|
||||||
_buttonIncognito.alpha = 0f;
|
}
|
||||||
} else {
|
|
||||||
if (StateApp.instance.privateMode) {
|
_fragVideoDetail.onMaximized.subscribe {
|
||||||
_buttonIncognito.elevation = 99f;
|
updatePrivateModeVisibility()
|
||||||
_buttonIncognito.alpha = 1f;
|
|
||||||
} else {
|
|
||||||
_buttonIncognito.elevation = -99f;
|
|
||||||
_buttonIncognito.alpha = 0f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StatePlayer.instance.also {
|
StatePlayer.instance.also {
|
||||||
@@ -442,6 +461,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
_fragMainPlaylist.topBar = _fragTopBarNavigation;
|
_fragMainPlaylist.topBar = _fragTopBarNavigation;
|
||||||
_fragMainRemotePlaylist.topBar = _fragTopBarNavigation;
|
_fragMainRemotePlaylist.topBar = _fragTopBarNavigation;
|
||||||
_fragPostDetail.topBar = _fragTopBarNavigation;
|
_fragPostDetail.topBar = _fragTopBarNavigation;
|
||||||
|
_fragArticleDetail.topBar = _fragTopBarNavigation;
|
||||||
|
_fragWebDetail.topBar = _fragTopBarNavigation;
|
||||||
_fragWatchlist.topBar = _fragTopBarNavigation;
|
_fragWatchlist.topBar = _fragTopBarNavigation;
|
||||||
_fragHistory.topBar = _fragTopBarNavigation;
|
_fragHistory.topBar = _fragTopBarNavigation;
|
||||||
_fragSourceDetail.topBar = _fragTopBarNavigation;
|
_fragSourceDetail.topBar = _fragTopBarNavigation;
|
||||||
@@ -609,8 +630,18 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
UIDialogs.toast(this, "No external file permissions\nExporting and auto backups will not work");
|
UIDialogs.toast(this, "No external file permissions\nExporting and auto backups will not work");
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
|
private var _qrCodeLoadingDialog: AlertDialog? = null
|
||||||
|
|
||||||
fun showUrlQrCodeScanner() {
|
fun showUrlQrCodeScanner() {
|
||||||
try {
|
try {
|
||||||
|
_qrCodeLoadingDialog = UIDialogs.showDialog(this, R.drawable.ic_loader_animated, true,
|
||||||
|
"Launching QR scanner",
|
||||||
|
"Make sure your camera is enabled", null, -2,
|
||||||
|
UIDialogs.Action("Close", {
|
||||||
|
_qrCodeLoadingDialog?.dismiss()
|
||||||
|
_qrCodeLoadingDialog = null
|
||||||
|
}));
|
||||||
|
|
||||||
val integrator = IntentIntegrator(this)
|
val integrator = IntentIntegrator(this)
|
||||||
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
|
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
|
||||||
integrator.setPrompt(getString(R.string.scan_a_qr_code))
|
integrator.setPrompt(getString(R.string.scan_a_qr_code))
|
||||||
@@ -626,6 +657,18 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
private fun updatePrivateModeVisibility() {
|
||||||
|
if (_privateModeEnabled && (_fragVideoDetail.state == State.CLOSED || !_pictureInPictureEnabled && !_isFullscreen)) {
|
||||||
|
_buttonIncognito.elevation = 99f;
|
||||||
|
_buttonIncognito.alpha = 1f;
|
||||||
|
_buttonIncognito.translationY = if (_fragVideoDetail.state == State.MINIMIZED) -60.dp(resources).toFloat() else 0f
|
||||||
|
} else {
|
||||||
|
_buttonIncognito.elevation = -99f;
|
||||||
|
_buttonIncognito.alpha = 0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
Logger.v(TAG, "onResume")
|
Logger.v(TAG, "onResume")
|
||||||
@@ -636,6 +679,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
super.onPause();
|
super.onPause();
|
||||||
Logger.v(TAG, "onPause")
|
Logger.v(TAG, "onPause")
|
||||||
_isVisible = false;
|
_isVisible = false;
|
||||||
|
|
||||||
|
_qrCodeLoadingDialog?.dismiss()
|
||||||
|
_qrCodeLoadingDialog = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
@@ -674,7 +720,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
"VIDEO" -> {
|
"VIDEO" -> {
|
||||||
val url = intent.getStringExtra("VIDEO");
|
val url = intent.getStringExtra("VIDEO");
|
||||||
navigate(_fragVideoDetail, url);
|
navigateWhenReady(_fragVideoDetail, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
"IMPORT_OPTIONS" -> {
|
"IMPORT_OPTIONS" -> {
|
||||||
@@ -692,11 +738,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
"Sources" -> {
|
"Sources" -> {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
StatePlatform.instance.updateAvailableClients(this@MainActivity, true) //Ideally this is not needed..
|
StatePlatform.instance.updateAvailableClients(this@MainActivity, true) //Ideally this is not needed..
|
||||||
navigate(_fragMainSources);
|
navigateWhenReady(_fragMainSources);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
"BROWSE_PLUGINS" -> {
|
"BROWSE_PLUGINS" -> {
|
||||||
navigate(_fragBrowser, BrowserFragment.NavigateOptions("https://plugins.grayjay.app/phone.html", mapOf(
|
navigateWhenReady(_fragBrowser, BrowserFragment.NavigateOptions("https://plugins.grayjay.app/phone.html", mapOf(
|
||||||
Pair("grayjay") { req ->
|
Pair("grayjay") { req ->
|
||||||
StateApp.instance.contextOrNull?.let {
|
StateApp.instance.contextOrNull?.let {
|
||||||
if (it is MainActivity) {
|
if (it is MainActivity) {
|
||||||
@@ -714,8 +760,12 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (targetData != null) {
|
if (targetData != null) {
|
||||||
runBlocking {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
handleUrlAll(targetData)
|
try {
|
||||||
|
handleUrlAll(targetData)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Unhandled exception in handleUrlAll", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (ex: Throwable) {
|
} catch (ex: Throwable) {
|
||||||
@@ -743,10 +793,10 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
} else if (url.startsWith("grayjay://video/")) {
|
} else if (url.startsWith("grayjay://video/")) {
|
||||||
val videoUrl = url.substring("grayjay://video/".length);
|
val videoUrl = url.substring("grayjay://video/".length);
|
||||||
navigate(_fragVideoDetail, videoUrl);
|
navigateWhenReady(_fragVideoDetail, videoUrl);
|
||||||
} else if (url.startsWith("grayjay://channel/")) {
|
} else if (url.startsWith("grayjay://channel/")) {
|
||||||
val channelUrl = url.substring("grayjay://channel/".length);
|
val channelUrl = url.substring("grayjay://channel/".length);
|
||||||
navigate(_fragMainChannel, channelUrl);
|
navigateWhenReady(_fragMainChannel, channelUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -812,29 +862,29 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
Logger.i(TAG, "handleUrl(url=$url) on IO");
|
Logger.i(TAG, "handleUrl(url=$url) on IO");
|
||||||
if (StatePlatform.instance.hasEnabledVideoClient(url)) {
|
if (StatePlatform.instance.hasEnabledContentClient(url)) {
|
||||||
Logger.i(TAG, "handleUrl(url=$url) found video client");
|
Logger.i(TAG, "handleUrl(url=$url) found video client");
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (position > 0)
|
if (position > 0)
|
||||||
navigate(_fragVideoDetail, UrlVideoWithTime(url, position.toLong(), true));
|
navigateWhenReady(_fragVideoDetail, UrlVideoWithTime(url, position.toLong(), true));
|
||||||
else
|
else
|
||||||
navigate(_fragVideoDetail, url);
|
navigateWhenReady(_fragVideoDetail, url);
|
||||||
|
|
||||||
_fragVideoDetail.maximizeVideoDetail(true);
|
_fragVideoDetail.maximizeVideoDetail(true);
|
||||||
}
|
}
|
||||||
return@withContext true;
|
return@withContext true;
|
||||||
} else if (StatePlatform.instance.hasEnabledChannelClient(url)) {
|
} else if (StatePlatform.instance.hasEnabledChannelClient(url)) {
|
||||||
Logger.i(TAG, "handleUrl(url=$url) found channel client");
|
Logger.i(TAG, "handleUrl(url=$url) found channel client");
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
navigate(_fragMainChannel, url);
|
navigateWhenReady(_fragMainChannel, url);
|
||||||
delay(100);
|
delay(100);
|
||||||
_fragVideoDetail.minimizeVideoDetail();
|
_fragVideoDetail.minimizeVideoDetail();
|
||||||
};
|
};
|
||||||
return@withContext true;
|
return@withContext true;
|
||||||
} else if (StatePlatform.instance.hasEnabledPlaylistClient(url)) {
|
} else if (StatePlatform.instance.hasEnabledPlaylistClient(url)) {
|
||||||
Logger.i(TAG, "handleUrl(url=$url) found playlist client");
|
Logger.i(TAG, "handleUrl(url=$url) found playlist client");
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
navigate(_fragMainRemotePlaylist, url);
|
navigateWhenReady(_fragMainRemotePlaylist, url);
|
||||||
delay(100);
|
delay(100);
|
||||||
_fragVideoDetail.minimizeVideoDetail();
|
_fragVideoDetail.minimizeVideoDetail();
|
||||||
};
|
};
|
||||||
@@ -1046,6 +1096,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
Logger.v(TAG, "onPictureInPictureModeChanged isInPictureInPictureMode=$isInPictureInPictureMode isStop=$isStop")
|
Logger.v(TAG, "onPictureInPictureModeChanged isInPictureInPictureMode=$isInPictureInPictureMode isStop=$isStop")
|
||||||
_fragVideoDetail.onPictureInPictureModeChanged(isInPictureInPictureMode, isStop, newConfig);
|
_fragVideoDetail.onPictureInPictureModeChanged(isInPictureInPictureMode, isStop, newConfig);
|
||||||
Logger.v(TAG, "onPictureInPictureModeChanged Ready");
|
Logger.v(TAG, "onPictureInPictureModeChanged Ready");
|
||||||
|
|
||||||
|
_pictureInPictureEnabled = isInPictureInPictureMode
|
||||||
|
updatePrivateModeVisibility()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
@@ -1058,6 +1111,18 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
return fragCurrent is T;
|
return fragCurrent is T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun navigateWhenReady(segment: MainFragment, parameter: Any? = null, withHistory: Boolean = true, isBack: Boolean = false) {
|
||||||
|
if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
|
||||||
|
navigate(segment, parameter, withHistory, isBack)
|
||||||
|
} else {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
lifecycle.withStateAtLeast(Lifecycle.State.RESUMED) {
|
||||||
|
navigate(segment, parameter, withHistory, isBack)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigate takes a MainFragment, and makes them the current main visible view
|
* Navigate takes a MainFragment, and makes them the current main visible view
|
||||||
* A parameter can be provided which becomes available in the onShow of said fragment
|
* A parameter can be provided which becomes available in the onShow of said fragment
|
||||||
@@ -1182,6 +1247,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
PlaylistFragment::class -> _fragMainPlaylist as T;
|
PlaylistFragment::class -> _fragMainPlaylist as T;
|
||||||
RemotePlaylistFragment::class -> _fragMainRemotePlaylist as T;
|
RemotePlaylistFragment::class -> _fragMainRemotePlaylist as T;
|
||||||
PostDetailFragment::class -> _fragPostDetail as T;
|
PostDetailFragment::class -> _fragPostDetail as T;
|
||||||
|
ArticleDetailFragment::class -> _fragArticleDetail as T;
|
||||||
|
WebDetailFragment::class -> _fragWebDetail as T;
|
||||||
WatchLaterFragment::class -> _fragWatchlist as T;
|
WatchLaterFragment::class -> _fragWatchlist as T;
|
||||||
HistoryFragment::class -> _fragHistory as T;
|
HistoryFragment::class -> _fragHistory as T;
|
||||||
SourceDetailFragment::class -> _fragSourceDetail as T;
|
SourceDetailFragment::class -> _fragSourceDetail as T;
|
||||||
@@ -1277,7 +1344,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
if (toast.long)
|
if (toast.long)
|
||||||
delay(5000);
|
delay(5000);
|
||||||
else
|
else
|
||||||
delay(3000);
|
delay(2500);
|
||||||
}
|
}
|
||||||
Logger.i(TAG, "Ending appToast loop");
|
Logger.i(TAG, "Ending appToast loop");
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
|||||||
+3
-3
@@ -11,16 +11,16 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.polycentric.PolycentricStorage
|
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.platformplayer.views.LoaderView
|
import com.futo.platformplayer.views.LoaderView
|
||||||
|
import com.futo.polycentric.core.ApiMethods
|
||||||
import com.futo.polycentric.core.ProcessHandle
|
import com.futo.polycentric.core.ProcessHandle
|
||||||
import com.futo.polycentric.core.Store
|
import com.futo.polycentric.core.Store
|
||||||
|
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -87,7 +87,7 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
|
|||||||
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
|
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
processHandle.addServer(PolycentricCache.SERVER);
|
processHandle.addServer(ApiMethods.SERVER);
|
||||||
processHandle.setUsername(username);
|
processHandle.setUsername(username);
|
||||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
|||||||
+2
-2
@@ -12,12 +12,12 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.polycentric.PolycentricStorage
|
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.platformplayer.views.overlays.LoaderOverlay
|
import com.futo.platformplayer.views.overlays.LoaderOverlay
|
||||||
|
import com.futo.polycentric.core.ApiMethods
|
||||||
import com.futo.polycentric.core.KeyPair
|
import com.futo.polycentric.core.KeyPair
|
||||||
import com.futo.polycentric.core.Process
|
import com.futo.polycentric.core.Process
|
||||||
import com.futo.polycentric.core.ProcessSecret
|
import com.futo.polycentric.core.ProcessSecret
|
||||||
@@ -145,7 +145,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||||
processHandle.fullyBackfillClient(PolycentricCache.SERVER);
|
processHandle.fullyBackfillClient(ApiMethods.SERVER);
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
|
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
|
||||||
finish();
|
finish();
|
||||||
|
|||||||
@@ -21,10 +21,8 @@ import com.bumptech.glide.Glide
|
|||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
|
||||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.polycentric.PolycentricStorage
|
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
@@ -32,8 +30,10 @@ import com.futo.platformplayer.states.StateApp
|
|||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
import com.futo.platformplayer.views.overlays.LoaderOverlay
|
import com.futo.platformplayer.views.overlays.LoaderOverlay
|
||||||
|
import com.futo.polycentric.core.ApiMethods
|
||||||
import com.futo.polycentric.core.Store
|
import com.futo.polycentric.core.Store
|
||||||
import com.futo.polycentric.core.SystemState
|
import com.futo.polycentric.core.SystemState
|
||||||
|
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
||||||
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
|
||||||
import com.futo.polycentric.core.toBase64Url
|
import com.futo.polycentric.core.toBase64Url
|
||||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
@@ -145,7 +145,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
processHandle.fullyBackfillClient(PolycentricCache.SERVER)
|
processHandle.fullyBackfillClient(ApiMethods.SERVER)
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
updateUI();
|
updateUI();
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import android.widget.LinearLayout
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateSync
|
import com.futo.platformplayer.states.StateSync
|
||||||
@@ -29,6 +31,16 @@ class SyncHomeActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
if (StateApp.instance.contextOrNull == null) {
|
||||||
|
Logger.w(TAG, "No main activity, restarting main.")
|
||||||
|
val intent = Intent(this, MainActivity::class.java)
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setContentView(R.layout.activity_sync_home)
|
setContentView(R.layout.activity_sync_home)
|
||||||
setNavigationBarColorAndIcons()
|
setNavigationBarColorAndIcons()
|
||||||
|
|
||||||
@@ -54,7 +66,6 @@ class SyncHomeActivity : AppCompatActivity() {
|
|||||||
val view = _viewMap[publicKey]
|
val view = _viewMap[publicKey]
|
||||||
if (!session.isAuthorized) {
|
if (!session.isAuthorized) {
|
||||||
if (view != null) {
|
if (view != null) {
|
||||||
_layoutDevices.removeView(view)
|
|
||||||
_viewMap.remove(publicKey)
|
_viewMap.remove(publicKey)
|
||||||
}
|
}
|
||||||
return@launch
|
return@launch
|
||||||
@@ -89,6 +100,20 @@ class SyncHomeActivity : AppCompatActivity() {
|
|||||||
updateEmptyVisibility()
|
updateEmptyVisibility()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StateSync.instance.confirmStarted(this, onStarted = {
|
||||||
|
if (StateSync.instance.syncService?.serverSocketFailedToStart == true) {
|
||||||
|
UIDialogs.toast(this, "Server socket failed to start, is the port in use?", true)
|
||||||
|
}
|
||||||
|
if (StateSync.instance.syncService?.relayConnected == false) {
|
||||||
|
UIDialogs.toast(this, "Not connected to relay, remote connections will work.", false)
|
||||||
|
}
|
||||||
|
if (StateSync.instance.syncService?.serverSocketStarted == false) {
|
||||||
|
UIDialogs.toast(this, "Listener not started, local connections will not work.", false)
|
||||||
|
}
|
||||||
|
}, onNotStarted = {
|
||||||
|
finish()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
@@ -100,9 +125,12 @@ class SyncHomeActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun updateDeviceView(syncDeviceView: SyncDeviceView, publicKey: String, session: SyncSession?): SyncDeviceView {
|
private fun updateDeviceView(syncDeviceView: SyncDeviceView, publicKey: String, session: SyncSession?): SyncDeviceView {
|
||||||
val connected = session?.connected ?: false
|
val connected = session?.connected ?: false
|
||||||
syncDeviceView.setLinkType(if (connected) LinkType.Local else LinkType.None)
|
val authorized = session?.isAuthorized ?: false
|
||||||
.setName(publicKey)
|
|
||||||
.setStatus(if (connected) "Connected" else "Disconnected")
|
syncDeviceView.setLinkType(session?.linkType ?: LinkType.None)
|
||||||
|
.setName(session?.displayName ?: StateSync.instance.getCachedName(publicKey) ?: publicKey)
|
||||||
|
//TODO: also display public key?
|
||||||
|
.setStatus(if (connected && authorized) "Connected" else "Disconnected or unauthorized")
|
||||||
return syncDeviceView
|
return syncDeviceView
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ class SyncPairActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
_layoutPairingSuccess.setOnClickListener {
|
_layoutPairingSuccess.setOnClickListener {
|
||||||
_layoutPairingSuccess.visibility = View.GONE
|
_layoutPairingSuccess.visibility = View.GONE
|
||||||
|
finish()
|
||||||
}
|
}
|
||||||
_layoutPairingError.setOnClickListener {
|
_layoutPairingError.setOnClickListener {
|
||||||
_layoutPairingError.visibility = View.GONE
|
_layoutPairingError.visibility = View.GONE
|
||||||
@@ -109,11 +110,17 @@ class SyncPairActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
StateSync.instance.connect(deviceInfo) { session, complete, message ->
|
StateSync.instance.syncService?.connect(deviceInfo) { complete, message ->
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
if (complete) {
|
if (complete != null) {
|
||||||
_layoutPairingSuccess.visibility = View.VISIBLE
|
if (complete) {
|
||||||
_layoutPairing.visibility = View.GONE
|
_layoutPairingSuccess.visibility = View.VISIBLE
|
||||||
|
_layoutPairing.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
_textError.text = message
|
||||||
|
_layoutPairingError.visibility = View.VISIBLE
|
||||||
|
_layoutPairing.visibility = View.GONE
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
_textPairingStatus.text = message
|
_textPairingStatus.text = message
|
||||||
}
|
}
|
||||||
@@ -122,7 +129,11 @@ class SyncPairActivity : AppCompatActivity() {
|
|||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
_layoutPairingError.visibility = View.VISIBLE
|
_layoutPairingError.visibility = View.VISIBLE
|
||||||
_textError.text = e.message
|
if(e.message == "Failed to connect") {
|
||||||
|
_textError.text = "Failed to connect.\n\nThis may be due to not being on the same network, due to firewall, or vpn.\nSync currently operates only over local direct connections."
|
||||||
|
}
|
||||||
|
else
|
||||||
|
_textError.text = e.message
|
||||||
_layoutPairing.visibility = View.GONE
|
_layoutPairing.visibility = View.GONE
|
||||||
Logger.e(TAG, "Failed to pair", e)
|
Logger.e(TAG, "Failed to pair", e)
|
||||||
}
|
}
|
||||||
@@ -133,8 +144,6 @@ class SyncPairActivity : AppCompatActivity() {
|
|||||||
_textError.text = e.message
|
_textError.text = e.message
|
||||||
_layoutPairing.visibility = View.GONE
|
_layoutPairing.visibility = View.GONE
|
||||||
Logger.e(TAG, "Failed to pair", e)
|
Logger.e(TAG, "Failed to pair", e)
|
||||||
} finally {
|
|
||||||
_layoutPairing.visibility = View.GONE
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-5
@@ -67,11 +67,18 @@ class SyncShowPairingCodeActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val ips = getIPs()
|
val ips = getIPs()
|
||||||
val selfDeviceInfo = SyncDeviceInfo(StateSync.instance.publicKey!!, ips.toTypedArray(), StateSync.PORT)
|
val publicKey = StateSync.instance.syncService?.publicKey
|
||||||
val json = Json.encodeToString(selfDeviceInfo)
|
val pairingCode = StateSync.instance.syncService?.pairingCode
|
||||||
val base64 = Base64.encodeToString(json.toByteArray(), Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
|
if (publicKey == null || pairingCode == null) {
|
||||||
val url = "grayjay://sync/${base64}"
|
setCode("Public key or pairing code was not known, is sync enabled?")
|
||||||
setCode(url)
|
} else {
|
||||||
|
val selfDeviceInfo = SyncDeviceInfo(publicKey, ips.toTypedArray(), StateSync.PORT, pairingCode)
|
||||||
|
val json = Json.encodeToString(selfDeviceInfo)
|
||||||
|
val base64 = Base64.encodeToString(json.toByteArray(), Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
|
||||||
|
val url = "grayjay://sync/${base64}"
|
||||||
|
setCode(url)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setCode(code: String?) {
|
fun setCode(code: String?) {
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import com.futo.platformplayer.SettingsDev
|
|||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.ensureNotMainThread
|
import com.futo.platformplayer.ensureNotMainThread
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
@@ -63,7 +65,7 @@ open class ManagedHttpClient {
|
|||||||
|
|
||||||
constructor(builder: OkHttpClient.Builder = OkHttpClient.Builder()) {
|
constructor(builder: OkHttpClient.Builder = OkHttpClient.Builder()) {
|
||||||
_builderTemplate = builder;
|
_builderTemplate = builder;
|
||||||
if(SettingsDev.instance.developerMode && SettingsDev.instance.networking.allowAllCertificates)
|
if(FragmentedStorage.isInitialized && StateApp.instance.isMainActive && SettingsDev.instance.developerMode && SettingsDev.instance.networking.allowAllCertificates)
|
||||||
trustAllCertificates(builder);
|
trustAllCertificates(builder);
|
||||||
client = builder.addNetworkInterceptor { chain ->
|
client = builder.addNetworkInterceptor { chain ->
|
||||||
val request = beforeRequest(chain.request());
|
val request = beforeRequest(chain.request());
|
||||||
@@ -88,6 +90,7 @@ open class ManagedHttpClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun tryHead(url: String): Map<String, String>? {
|
fun tryHead(url: String): Map<String, String>? {
|
||||||
|
ensureNotMainThread()
|
||||||
try {
|
try {
|
||||||
val result = head(url);
|
val result = head(url);
|
||||||
if(result.isOk)
|
if(result.isOk)
|
||||||
@@ -102,7 +105,7 @@ open class ManagedHttpClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun socket(url: String, headers: MutableMap<String, String> = HashMap(), listener: SocketListener): Socket {
|
fun socket(url: String, headers: MutableMap<String, String> = HashMap(), listener: SocketListener): Socket {
|
||||||
|
ensureNotMainThread()
|
||||||
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
|
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
|
||||||
.url(url);
|
.url(url);
|
||||||
if(user_agent.isNotEmpty() && !headers.any { it.key.lowercase() == "user-agent" })
|
if(user_agent.isNotEmpty() && !headers.any { it.key.lowercase() == "user-agent" })
|
||||||
@@ -298,6 +301,7 @@ open class ManagedHttpClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun send(msg: String) {
|
fun send(msg: String) {
|
||||||
|
ensureNotMainThread()
|
||||||
socket.send(msg);
|
socket.send(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -73,7 +73,7 @@ class HttpFileHandler(method: String, path: String, private val contentType: Str
|
|||||||
Logger.v(TAG, "Sent bytes $current-${current + bytesToSend}, totalBytesSent=$totalBytesSent")
|
Logger.v(TAG, "Sent bytes $current-${current + bytesToSend}, totalBytesSent=$totalBytesSent")
|
||||||
|
|
||||||
current += bytesToSend.toLong()
|
current += bytesToSend.toLong()
|
||||||
if (current >= end) {
|
if (current > end) {
|
||||||
Logger.i(TAG, "Expected amount of bytes sent")
|
Logger.i(TAG, "Expected amount of bytes sent")
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.futo.platformplayer.api.media
|
package com.futo.platformplayer.api.media
|
||||||
|
|
||||||
|
import com.futo.platformplayer.api.media.models.IPlatformChannelContent
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
@@ -66,6 +67,11 @@ interface IPlatformClient {
|
|||||||
*/
|
*/
|
||||||
fun searchChannels(query: String): IPager<PlatformAuthorLink>;
|
fun searchChannels(query: String): IPager<PlatformAuthorLink>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches for channels and returns a content pager
|
||||||
|
*/
|
||||||
|
fun searchChannelsAsContent(query: String): IPager<IPlatformContent>;
|
||||||
|
|
||||||
|
|
||||||
//Video Pages
|
//Video Pages
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -14,14 +14,16 @@ class PlatformClientPool {
|
|||||||
private var _poolCounter = 0;
|
private var _poolCounter = 0;
|
||||||
private val _poolName: String?;
|
private val _poolName: String?;
|
||||||
private val _privatePool: Boolean;
|
private val _privatePool: Boolean;
|
||||||
|
private val _isolatedInitialization: Boolean
|
||||||
|
|
||||||
var isDead: Boolean = false
|
var isDead: Boolean = false
|
||||||
private set;
|
private set;
|
||||||
val onDead = Event2<JSClient, PlatformClientPool>();
|
val onDead = Event2<JSClient, PlatformClientPool>();
|
||||||
|
|
||||||
constructor(parentClient: IPlatformClient, name: String? = null, privatePool: Boolean = false) {
|
constructor(parentClient: IPlatformClient, name: String? = null, privatePool: Boolean = false, isolatedInitialization: Boolean = false) {
|
||||||
_poolName = name;
|
_poolName = name;
|
||||||
_privatePool = privatePool;
|
_privatePool = privatePool;
|
||||||
|
_isolatedInitialization = isolatedInitialization
|
||||||
if(parentClient !is JSClient)
|
if(parentClient !is JSClient)
|
||||||
throw IllegalArgumentException("Pooling only supported for JSClients right now");
|
throw IllegalArgumentException("Pooling only supported for JSClients right now");
|
||||||
Logger.i(TAG, "Pool for ${parentClient.name} was started");
|
Logger.i(TAG, "Pool for ${parentClient.name} was started");
|
||||||
@@ -53,7 +55,7 @@ class PlatformClientPool {
|
|||||||
reserved = _pool.keys.find { !it.isBusy };
|
reserved = _pool.keys.find { !it.isBusy };
|
||||||
if(reserved == null && _pool.size < capacity) {
|
if(reserved == null && _pool.size < capacity) {
|
||||||
Logger.i(TAG, "Started additional [${_parent.name}] client in pool [${_poolName}] (${_pool.size + 1}/${capacity})");
|
Logger.i(TAG, "Started additional [${_parent.name}] client in pool [${_poolName}] (${_pool.size + 1}/${capacity})");
|
||||||
reserved = _parent.getCopy(_privatePool);
|
reserved = _parent.getCopy(_privatePool, _isolatedInitialization);
|
||||||
|
|
||||||
reserved?.onCaptchaException?.subscribe { client, ex ->
|
reserved?.onCaptchaException?.subscribe { client, ex ->
|
||||||
StateApp.instance.handleCaptchaException(client, ex);
|
StateApp.instance.handleCaptchaException(client, ex);
|
||||||
|
|||||||
@@ -7,13 +7,15 @@ class PlatformMultiClientPool {
|
|||||||
|
|
||||||
private var _isFake = false;
|
private var _isFake = false;
|
||||||
private var _privatePool = false;
|
private var _privatePool = false;
|
||||||
|
private val _isolatedInitialization: Boolean
|
||||||
|
|
||||||
constructor(name: String, maxCap: Int = -1, isPrivatePool: Boolean = false) {
|
constructor(name: String, maxCap: Int = -1, isPrivatePool: Boolean = false, isolatedInitialization: Boolean = false) {
|
||||||
_name = name;
|
_name = name;
|
||||||
_maxCap = if(maxCap > 0)
|
_maxCap = if(maxCap > 0)
|
||||||
maxCap
|
maxCap
|
||||||
else 99;
|
else 99;
|
||||||
_privatePool = isPrivatePool;
|
_privatePool = isPrivatePool;
|
||||||
|
_isolatedInitialization = isolatedInitialization
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getClientPooled(parentClient: IPlatformClient, capacity: Int = _maxCap): IPlatformClient {
|
fun getClientPooled(parentClient: IPlatformClient, capacity: Int = _maxCap): IPlatformClient {
|
||||||
@@ -21,7 +23,7 @@ class PlatformMultiClientPool {
|
|||||||
return parentClient;
|
return parentClient;
|
||||||
val pool = synchronized(_clientPools) {
|
val pool = synchronized(_clientPools) {
|
||||||
if(!_clientPools.containsKey(parentClient))
|
if(!_clientPools.containsKey(parentClient))
|
||||||
_clientPools[parentClient] = PlatformClientPool(parentClient, _name, _privatePool).apply {
|
_clientPools[parentClient] = PlatformClientPool(parentClient, _name, _privatePool, _isolatedInitialization).apply {
|
||||||
this.onDead.subscribe { _, pool ->
|
this.onDead.subscribe { _, pool ->
|
||||||
synchronized(_clientPools) {
|
synchronized(_clientPools) {
|
||||||
if(_clientPools[parentClient] == pool)
|
if(_clientPools[parentClient] == pool)
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ package com.futo.platformplayer.api.media.models
|
|||||||
|
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.JSContent
|
||||||
import com.futo.platformplayer.getOrDefault
|
import com.futo.platformplayer.getOrDefault
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
@@ -42,4 +45,21 @@ open class PlatformAuthorLink {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPlatformChannelContent : IPlatformContent {
|
||||||
|
val thumbnail: String?
|
||||||
|
val subscribers: Long?
|
||||||
|
}
|
||||||
|
|
||||||
|
open class JSChannelContent : JSContent, IPlatformChannelContent {
|
||||||
|
override val contentType: ContentType get() = ContentType.CHANNEL
|
||||||
|
override val thumbnail: String?
|
||||||
|
override val subscribers: Long?
|
||||||
|
|
||||||
|
constructor(config: SourcePluginConfig, obj: V8ValueObject) : super(config, obj) {
|
||||||
|
val contextName = "Channel";
|
||||||
|
thumbnail = obj.getOrDefault<String>(config, "thumbnail", contextName, null)
|
||||||
|
subscribers = if(obj.has("subscribers")) obj.getOrThrow(config,"subscribers", contextName) else null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -30,6 +30,7 @@ class ResultCapabilities(
|
|||||||
const val TYPE_POSTS = "POSTS";
|
const val TYPE_POSTS = "POSTS";
|
||||||
const val TYPE_MIXED = "MIXED";
|
const val TYPE_MIXED = "MIXED";
|
||||||
const val TYPE_SUBSCRIPTIONS = "SUBSCRIPTIONS";
|
const val TYPE_SUBSCRIPTIONS = "SUBSCRIPTIONS";
|
||||||
|
const val TYPE_SHORTS = "SHORTS";
|
||||||
|
|
||||||
const val ORDER_CHONOLOGICAL = "CHRONOLOGICAL";
|
const val ORDER_CHONOLOGICAL = "CHRONOLOGICAL";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.futo.platformplayer.api.media.models.article
|
||||||
|
|
||||||
|
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
|
||||||
|
interface IPlatformArticle: IPlatformContent {
|
||||||
|
val summary: String?;
|
||||||
|
val thumbnails: Thumbnails?;
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
package com.futo.platformplayer.api.media.models.article
|
||||||
|
|
||||||
|
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||||
|
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.IJSArticleSegment
|
||||||
|
|
||||||
|
interface IPlatformArticleDetails: IPlatformContent, IPlatformArticle, IPlatformContentDetails {
|
||||||
|
val segments: List<IJSArticleSegment>;
|
||||||
|
val rating : IRating;
|
||||||
|
}
|
||||||
@@ -8,10 +8,12 @@ enum class ContentType(val value: Int) {
|
|||||||
POST(2),
|
POST(2),
|
||||||
ARTICLE(3),
|
ARTICLE(3),
|
||||||
PLAYLIST(4),
|
PLAYLIST(4),
|
||||||
|
WEB(7),
|
||||||
|
|
||||||
URL(9),
|
URL(9),
|
||||||
|
|
||||||
NESTED_VIDEO(11),
|
NESTED_VIDEO(11),
|
||||||
|
CHANNEL(60),
|
||||||
|
|
||||||
LOCKED(70),
|
LOCKED(70),
|
||||||
|
|
||||||
|
|||||||
+2
@@ -2,6 +2,8 @@ package com.futo.platformplayer.api.media.models.contents
|
|||||||
|
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.JsonNames
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
interface IPlatformContent {
|
interface IPlatformContent {
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import com.futo.platformplayer.api.media.exceptions.UnknownPlatformException
|
|||||||
enum class TextType(val value: Int) {
|
enum class TextType(val value: Int) {
|
||||||
RAW(0),
|
RAW(0),
|
||||||
HTML(1),
|
HTML(1),
|
||||||
MARKUP(2);
|
MARKUP(2),
|
||||||
|
CODE(3);
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromInt(value: Int): TextType
|
fun fromInt(value: Int): TextType
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ package com.futo.platformplayer.api.media.models.streams
|
|||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
import com.futo.platformplayer.downloads.VideoLocal
|
import com.futo.platformplayer.downloads.VideoLocal
|
||||||
|
|
||||||
class LocalVideoMuxedSourceDescriptor(
|
class DownloadedVideoMuxedSourceDescriptor(
|
||||||
private val video: VideoLocal
|
private val video: VideoLocal
|
||||||
) : VideoMuxedSourceDescriptor() {
|
) : VideoMuxedSourceDescriptor() {
|
||||||
override val videoSources: Array<IVideoSource> get() = video.videoSource.toTypedArray();
|
override val videoSources: Array<IVideoSource> get() = video.videoSource.toTypedArray();
|
||||||
+5
-2
@@ -13,7 +13,8 @@ class AudioUrlSource(
|
|||||||
override val codec: String = "",
|
override val codec: String = "",
|
||||||
override val language: String = Language.UNKNOWN,
|
override val language: String = Language.UNKNOWN,
|
||||||
override val duration: Long? = null,
|
override val duration: Long? = null,
|
||||||
override var priority: Boolean = false
|
override var priority: Boolean = false,
|
||||||
|
override var original: Boolean = false
|
||||||
) : IAudioUrlSource, IStreamMetaDataSource{
|
) : IAudioUrlSource, IStreamMetaDataSource{
|
||||||
override var streamMetaData: StreamMetaData? = null;
|
override var streamMetaData: StreamMetaData? = null;
|
||||||
|
|
||||||
@@ -36,7 +37,9 @@ class AudioUrlSource(
|
|||||||
source.container,
|
source.container,
|
||||||
source.codec,
|
source.codec,
|
||||||
source.language,
|
source.language,
|
||||||
source.duration
|
source.duration,
|
||||||
|
source.priority,
|
||||||
|
source.original
|
||||||
);
|
);
|
||||||
ret.streamMetaData = streamData;
|
ret.streamMetaData = streamData;
|
||||||
|
|
||||||
|
|||||||
+1
@@ -27,6 +27,7 @@ class HLSVariantAudioUrlSource(
|
|||||||
override val language: String,
|
override val language: String,
|
||||||
override val duration: Long?,
|
override val duration: Long?,
|
||||||
override val priority: Boolean,
|
override val priority: Boolean,
|
||||||
|
override val original: Boolean,
|
||||||
val url: String
|
val url: String
|
||||||
) : IAudioUrlSource {
|
) : IAudioUrlSource {
|
||||||
override fun getAudioUrl(): String {
|
override fun getAudioUrl(): String {
|
||||||
|
|||||||
+1
@@ -8,4 +8,5 @@ interface IAudioSource {
|
|||||||
val language : String;
|
val language : String;
|
||||||
val duration : Long?;
|
val duration : Long?;
|
||||||
val priority: Boolean;
|
val priority: Boolean;
|
||||||
|
val original: Boolean;
|
||||||
}
|
}
|
||||||
+3
-2
@@ -15,6 +15,7 @@ class LocalAudioSource : IAudioSource, IStreamMetaDataSource {
|
|||||||
override val duration: Long? = null;
|
override val duration: Long? = null;
|
||||||
|
|
||||||
override var priority: Boolean = false;
|
override var priority: Boolean = false;
|
||||||
|
override val original: Boolean = false;
|
||||||
|
|
||||||
val filePath : String;
|
val filePath : String;
|
||||||
val fileSize: Long;
|
val fileSize: Long;
|
||||||
@@ -33,13 +34,13 @@ class LocalAudioSource : IAudioSource, IStreamMetaDataSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromSource(source: IAudioSource, path: String, fileSize: Long): LocalAudioSource {
|
fun fromSource(source: IAudioSource, path: String, fileSize: Long, overrideContainer: String? = null): LocalAudioSource {
|
||||||
return LocalAudioSource(
|
return LocalAudioSource(
|
||||||
source.name,
|
source.name,
|
||||||
path,
|
path,
|
||||||
fileSize,
|
fileSize,
|
||||||
source.bitrate,
|
source.bitrate,
|
||||||
source.container,
|
overrideContainer ?: source.container,
|
||||||
source.codec,
|
source.codec,
|
||||||
source.language
|
source.language
|
||||||
);
|
);
|
||||||
|
|||||||
+2
-2
@@ -35,7 +35,7 @@ class LocalVideoSource : IVideoSource, IStreamMetaDataSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromSource(source: IVideoSource, path: String, fileSize: Long): LocalVideoSource {
|
fun fromSource(source: IVideoSource, path: String, fileSize: Long, overrideContainer: String? = null): LocalVideoSource {
|
||||||
return LocalVideoSource(
|
return LocalVideoSource(
|
||||||
source.name,
|
source.name,
|
||||||
path,
|
path,
|
||||||
@@ -43,7 +43,7 @@ class LocalVideoSource : IVideoSource, IStreamMetaDataSource {
|
|||||||
source.width,
|
source.width,
|
||||||
source.height,
|
source.height,
|
||||||
source.duration,
|
source.duration,
|
||||||
source.container,
|
overrideContainer ?: source.container,
|
||||||
source.codec,
|
source.codec,
|
||||||
source.bitrate?:0
|
source.bitrate?:0
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,4 +13,6 @@ interface IPlatformVideo : IPlatformContent {
|
|||||||
val viewCount: Long;
|
val viewCount: Long;
|
||||||
|
|
||||||
val isLive : Boolean;
|
val isLive : Boolean;
|
||||||
|
|
||||||
|
val isShort: Boolean;
|
||||||
}
|
}
|
||||||
+5
-1
@@ -10,23 +10,26 @@ import com.futo.polycentric.core.combineHashCodes
|
|||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonNames
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
open class SerializedPlatformVideo(
|
open class SerializedPlatformVideo(
|
||||||
|
override val contentType: ContentType = ContentType.MEDIA,
|
||||||
override val id: PlatformID,
|
override val id: PlatformID,
|
||||||
override val name: String,
|
override val name: String,
|
||||||
override val thumbnails: Thumbnails,
|
override val thumbnails: Thumbnails,
|
||||||
override val author: PlatformAuthorLink,
|
override val author: PlatformAuthorLink,
|
||||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||||
|
@JsonNames("datetime", "dateTime")
|
||||||
override val datetime: OffsetDateTime? = null,
|
override val datetime: OffsetDateTime? = null,
|
||||||
override val url: String,
|
override val url: String,
|
||||||
override val shareUrl: String = "",
|
override val shareUrl: String = "",
|
||||||
|
|
||||||
override val duration: Long,
|
override val duration: Long,
|
||||||
override val viewCount: Long,
|
override val viewCount: Long,
|
||||||
|
override val isShort: Boolean = false
|
||||||
) : IPlatformVideo, SerializedPlatformContent {
|
) : IPlatformVideo, SerializedPlatformContent {
|
||||||
override val contentType: ContentType = ContentType.MEDIA;
|
|
||||||
|
|
||||||
override val isLive: Boolean = false;
|
override val isLive: Boolean = false;
|
||||||
|
|
||||||
@@ -43,6 +46,7 @@ open class SerializedPlatformVideo(
|
|||||||
companion object {
|
companion object {
|
||||||
fun fromVideo(video: IPlatformVideo) : SerializedPlatformVideo {
|
fun fromVideo(video: IPlatformVideo) : SerializedPlatformVideo {
|
||||||
return SerializedPlatformVideo(
|
return SerializedPlatformVideo(
|
||||||
|
ContentType.MEDIA,
|
||||||
video.id,
|
video.id,
|
||||||
video.name,
|
video.name,
|
||||||
video.thumbnails,
|
video.thumbnails,
|
||||||
|
|||||||
+2
-1
@@ -38,7 +38,8 @@ open class SerializedPlatformVideoDetails(
|
|||||||
override val video: ISerializedVideoSourceDescriptor,
|
override val video: ISerializedVideoSourceDescriptor,
|
||||||
override val preview: ISerializedVideoSourceDescriptor?,
|
override val preview: ISerializedVideoSourceDescriptor?,
|
||||||
|
|
||||||
override val subtitles: List<SubtitleRawSource> = listOf()
|
override val subtitles: List<SubtitleRawSource> = listOf(),
|
||||||
|
override val isShort: Boolean = false
|
||||||
) : IPlatformVideo, IPlatformVideoDetails {
|
) : IPlatformVideo, IPlatformVideoDetails {
|
||||||
final override val contentType: ContentType get() = ContentType.MEDIA;
|
final override val contentType: ContentType get() = ContentType.MEDIA;
|
||||||
|
|
||||||
|
|||||||
@@ -54,8 +54,11 @@ class DevJSClient : JSClient {
|
|||||||
return DevJSClient(context, config, _devScript, _auth, _captcha, devID, descriptor.settings);
|
return DevJSClient(context, config, _devScript, _auth, _captcha, devID, descriptor.settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCopy(privateCopy: Boolean): JSClient {
|
override fun getCopy(privateCopy: Boolean, noSaveState: Boolean): JSClient {
|
||||||
return DevJSClient(_context, descriptor, _script, if(!privateCopy) _auth else null, _captcha, saveState(), devID);
|
val client = DevJSClient(_context, descriptor, _script, if(!privateCopy) _auth else null, _captcha, if (noSaveState) null else saveState(), devID);
|
||||||
|
if (noSaveState)
|
||||||
|
client.initialize()
|
||||||
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun initialize() {
|
override fun initialize() {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import com.caoccao.javet.values.reference.V8ValueObject
|
|||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.IPlatformClient
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
import com.futo.platformplayer.api.media.PlatformClientCapabilities
|
import com.futo.platformplayer.api.media.PlatformClientCapabilities
|
||||||
|
import com.futo.platformplayer.api.media.models.IPlatformChannelContent
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
@@ -31,6 +32,7 @@ import com.futo.platformplayer.api.media.platforms.js.internal.JSParameterDocs
|
|||||||
import com.futo.platformplayer.api.media.platforms.js.models.IJSContent
|
import com.futo.platformplayer.api.media.platforms.js.models.IJSContent
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.IJSContentDetails
|
import com.futo.platformplayer.api.media.platforms.js.models.IJSContentDetails
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSChannel
|
import com.futo.platformplayer.api.media.platforms.js.models.JSChannel
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.JSChannelContentPager
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSChannelPager
|
import com.futo.platformplayer.api.media.platforms.js.models.JSChannelPager
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSChapter
|
import com.futo.platformplayer.api.media.platforms.js.models.JSChapter
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSComment
|
import com.futo.platformplayer.api.media.platforms.js.models.JSComment
|
||||||
@@ -193,8 +195,11 @@ open class JSClient : IPlatformClient {
|
|||||||
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
|
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun getCopy(withoutCredentials: Boolean = false): JSClient {
|
open fun getCopy(withoutCredentials: Boolean = false, noSaveState: Boolean = false): JSClient {
|
||||||
return JSClient(_context, descriptor, saveState(), _script, withoutCredentials);
|
val client = JSClient(_context, descriptor, if (noSaveState) null else saveState(), _script, withoutCredentials);
|
||||||
|
if (noSaveState)
|
||||||
|
client.initialize()
|
||||||
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getUnderlyingPlugin(): V8Plugin {
|
fun getUnderlyingPlugin(): V8Plugin {
|
||||||
@@ -209,6 +214,8 @@ open class JSClient : IPlatformClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun initialize() {
|
override fun initialize() {
|
||||||
|
if (_initialized) return
|
||||||
|
|
||||||
Logger.i(TAG, "Plugin [${config.name}] initializing");
|
Logger.i(TAG, "Plugin [${config.name}] initializing");
|
||||||
plugin.start();
|
plugin.start();
|
||||||
plugin.execute("plugin.config = ${Json.encodeToString(config)}");
|
plugin.execute("plugin.config = ${Json.encodeToString(config)}");
|
||||||
@@ -361,6 +368,10 @@ open class JSClient : IPlatformClient {
|
|||||||
return@isBusyWith JSChannelPager(config, this,
|
return@isBusyWith JSChannelPager(config, this,
|
||||||
plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"));
|
plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"));
|
||||||
}
|
}
|
||||||
|
override fun searchChannelsAsContent(query: String): IPager<IPlatformContent> = isBusyWith("searchChannels") {
|
||||||
|
ensureEnabled();
|
||||||
|
return@isBusyWith JSChannelContentPager(config, this, plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"), );
|
||||||
|
}
|
||||||
|
|
||||||
@JSDocs(6, "source.isChannelUrl(url)", "Validates if an channel url is for this platform")
|
@JSDocs(6, "source.isChannelUrl(url)", "Validates if an channel url is for this platform")
|
||||||
@JSDocsParameter("url", "A channel url (May not be your platform)")
|
@JSDocsParameter("url", "A channel url (May not be your platform)")
|
||||||
|
|||||||
+20
-1
@@ -33,6 +33,7 @@ class SourcePluginConfig(
|
|||||||
override val allowEval: Boolean = false,
|
override val allowEval: Boolean = false,
|
||||||
override val allowUrls: List<String> = listOf(),
|
override val allowUrls: List<String> = listOf(),
|
||||||
override val packages: List<String> = listOf(),
|
override val packages: List<String> = listOf(),
|
||||||
|
override val packagesOptional: List<String> = listOf(),
|
||||||
|
|
||||||
val settings: List<Setting> = listOf(),
|
val settings: List<Setting> = listOf(),
|
||||||
|
|
||||||
@@ -52,6 +53,7 @@ class SourcePluginConfig(
|
|||||||
var allowAllHttpHeaderAccess: Boolean = false,
|
var allowAllHttpHeaderAccess: Boolean = false,
|
||||||
var maxDownloadParallelism: Int = 0,
|
var maxDownloadParallelism: Int = 0,
|
||||||
var reduceFunctionsInLimitedVersion: Boolean = false,
|
var reduceFunctionsInLimitedVersion: Boolean = false,
|
||||||
|
var changelog: HashMap<String, List<String>>? = null
|
||||||
) : IV8PluginConfig {
|
) : IV8PluginConfig {
|
||||||
|
|
||||||
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
||||||
@@ -101,6 +103,10 @@ class SourcePluginConfig(
|
|||||||
if(!packages.contains(pack))
|
if(!packages.contains(pack))
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
for(pack in newConfig.packagesOptional) {
|
||||||
|
if(!packagesOptional.contains(pack))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
//Developer Submit Url should be same or empty
|
//Developer Submit Url should be same or empty
|
||||||
if(!newConfig.developerSubmitUrl.isNullOrEmpty() && developerSubmitUrl != newConfig.developerSubmitUrl)
|
if(!newConfig.developerSubmitUrl.isNullOrEmpty() && developerSubmitUrl != newConfig.developerSubmitUrl)
|
||||||
return false;
|
return false;
|
||||||
@@ -129,7 +135,7 @@ class SourcePluginConfig(
|
|||||||
|
|
||||||
val currentlyInstalledPlugin = StatePlugins.instance.getPlugin(id);
|
val currentlyInstalledPlugin = StatePlugins.instance.getPlugin(id);
|
||||||
if (currentlyInstalledPlugin != null) {
|
if (currentlyInstalledPlugin != null) {
|
||||||
if (currentlyInstalledPlugin.config.scriptPublicKey != scriptPublicKey) {
|
if (currentlyInstalledPlugin.config.scriptPublicKey != scriptPublicKey && !currentlyInstalledPlugin.config.scriptPublicKey.isNullOrEmpty()) {
|
||||||
list.add(Pair(
|
list.add(Pair(
|
||||||
"Different Author",
|
"Different Author",
|
||||||
"This plugin was signed by a different author. Please ensure that this is correct and that the plugin was not provided by a malicious actor."));
|
"This plugin was signed by a different author. Please ensure that this is correct and that the plugin was not provided by a malicious actor."));
|
||||||
@@ -178,6 +184,19 @@ class SourcePluginConfig(
|
|||||||
return _allowUrlsLower.any { it == host || (it.length > 0 && it[0] == '.' && host.matchesDomain(it)) };
|
return _allowUrlsLower.any { it == host || (it.length > 0 && it[0] == '.' && host.matchesDomain(it)) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getChangelogString(version: String): String?{
|
||||||
|
if(changelog == null || !changelog!!.containsKey(version))
|
||||||
|
return null;
|
||||||
|
val changelog = changelog!![version]!!;
|
||||||
|
if(changelog.size > 1) {
|
||||||
|
return "Changelog (${version})\n" + changelog.map { " - " + it.trim() }.joinToString("\n");
|
||||||
|
}
|
||||||
|
else if(changelog.size == 1) {
|
||||||
|
return "Changelog (${version})\n" + changelog[0].trim();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromJson(json: String, sourceUrl: String? = null): SourcePluginConfig {
|
fun fromJson(json: String, sourceUrl: String? = null): SourcePluginConfig {
|
||||||
val obj = Serializer.json.decodeFromString<SourcePluginConfig>(json);
|
val obj = Serializer.json.decodeFromString<SourcePluginConfig>(json);
|
||||||
|
|||||||
+7
-1
@@ -127,7 +127,7 @@ class JSHttpClient : ManagedHttpClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(doApplyCookies) {
|
if(doApplyCookies) {
|
||||||
if (_currentCookieMap.isNotEmpty()) {
|
if (_currentCookieMap.isNotEmpty() || _otherCookieMap.isNotEmpty()) {
|
||||||
val cookiesToApply = hashMapOf<String, String>();
|
val cookiesToApply = hashMapOf<String, String>();
|
||||||
synchronized(_currentCookieMap) {
|
synchronized(_currentCookieMap) {
|
||||||
for(cookie in _currentCookieMap
|
for(cookie in _currentCookieMap
|
||||||
@@ -135,6 +135,12 @@ class JSHttpClient : ManagedHttpClient {
|
|||||||
.flatMap { it.value.toList() })
|
.flatMap { it.value.toList() })
|
||||||
cookiesToApply[cookie.first] = cookie.second;
|
cookiesToApply[cookie.first] = cookie.second;
|
||||||
};
|
};
|
||||||
|
synchronized(_otherCookieMap) {
|
||||||
|
for(cookie in _otherCookieMap
|
||||||
|
.filter { domain.matchesDomain(it.key) }
|
||||||
|
.flatMap { it.value.toList() })
|
||||||
|
cookiesToApply[cookie.first] = cookie.second;
|
||||||
|
}
|
||||||
|
|
||||||
if(cookiesToApply.size > 0) {
|
if(cookiesToApply.size > 0) {
|
||||||
val cookieString = cookiesToApply.map { it.key + "=" + it.value }.joinToString("; ");
|
val cookieString = cookiesToApply.map { it.key + "=" + it.value }.joinToString("; ");
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.futo.platformplayer.api.media.platforms.js.models
|
package com.futo.platformplayer.api.media.platforms.js.models
|
||||||
|
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
|
import com.futo.platformplayer.api.media.models.JSChannelContent
|
||||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
@@ -26,6 +27,9 @@ interface IJSContent: IPlatformContent {
|
|||||||
ContentType.NESTED_VIDEO -> JSNestedMediaContent(config, obj);
|
ContentType.NESTED_VIDEO -> JSNestedMediaContent(config, obj);
|
||||||
ContentType.PLAYLIST -> JSPlaylist(config, obj);
|
ContentType.PLAYLIST -> JSPlaylist(config, obj);
|
||||||
ContentType.LOCKED -> JSLockedContent(config, obj);
|
ContentType.LOCKED -> JSLockedContent(config, obj);
|
||||||
|
ContentType.CHANNEL -> JSChannelContent(config, obj);
|
||||||
|
ContentType.ARTICLE -> JSArticle(config, obj);
|
||||||
|
ContentType.WEB -> JSWeb(config, obj);
|
||||||
else -> throw NotImplementedError("Unknown content type ${type}");
|
else -> throw NotImplementedError("Unknown content type ${type}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -17,6 +17,7 @@ interface IJSContentDetails: IPlatformContent {
|
|||||||
ContentType.MEDIA -> JSVideoDetails(plugin, obj);
|
ContentType.MEDIA -> JSVideoDetails(plugin, obj);
|
||||||
ContentType.POST -> JSPostDetails(plugin.config, obj);
|
ContentType.POST -> JSPostDetails(plugin.config, obj);
|
||||||
ContentType.ARTICLE -> JSArticleDetails(plugin, obj);
|
ContentType.ARTICLE -> JSArticleDetails(plugin, obj);
|
||||||
|
ContentType.WEB -> JSWebDetails(plugin, obj);
|
||||||
else -> throw NotImplementedError("Unknown content type ${type}");
|
else -> throw NotImplementedError("Unknown content type ${type}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.js.models
|
||||||
|
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
|
import com.futo.platformplayer.api.media.IPluginSourced
|
||||||
|
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||||
|
import com.futo.platformplayer.api.media.models.article.IPlatformArticle
|
||||||
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||||
|
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||||
|
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
||||||
|
import com.futo.platformplayer.api.media.models.post.TextType
|
||||||
|
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||||
|
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.getOrDefault
|
||||||
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
import com.futo.platformplayer.getOrThrowNullableList
|
||||||
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
|
|
||||||
|
open class JSArticle : JSContent, IPlatformArticle, IPluginSourced {
|
||||||
|
final override val contentType: ContentType get() = ContentType.ARTICLE;
|
||||||
|
|
||||||
|
override val summary: String;
|
||||||
|
override val thumbnails: Thumbnails?;
|
||||||
|
|
||||||
|
constructor(config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
|
||||||
|
val contextName = "PlatformArticle";
|
||||||
|
|
||||||
|
summary = _content.getOrDefault(config, "summary", contextName, "") ?: "";
|
||||||
|
thumbnails = Thumbnails.fromV8(config, _content.getOrThrow(config, "thumbnails", contextName));
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
-6
@@ -4,6 +4,8 @@ import com.caoccao.javet.values.reference.V8ValueObject
|
|||||||
import com.futo.platformplayer.api.media.IPlatformClient
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
import com.futo.platformplayer.api.media.IPluginSourced
|
import com.futo.platformplayer.api.media.IPluginSourced
|
||||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||||
|
import com.futo.platformplayer.api.media.models.article.IPlatformArticle
|
||||||
|
import com.futo.platformplayer.api.media.models.article.IPlatformArticleDetails
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
@@ -21,20 +23,20 @@ import com.futo.platformplayer.getOrThrow
|
|||||||
import com.futo.platformplayer.getOrThrowNullableList
|
import com.futo.platformplayer.getOrThrowNullableList
|
||||||
import com.futo.platformplayer.states.StateDeveloper
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
|
|
||||||
open class JSArticleDetails : JSContent, IPluginSourced, IPlatformContentDetails {
|
open class JSArticleDetails : JSContent, IPlatformArticleDetails, IPluginSourced, IPlatformContentDetails {
|
||||||
final override val contentType: ContentType get() = ContentType.ARTICLE;
|
final override val contentType: ContentType get() = ContentType.ARTICLE;
|
||||||
|
|
||||||
private val _hasGetComments: Boolean;
|
private val _hasGetComments: Boolean;
|
||||||
private val _hasGetContentRecommendations: Boolean;
|
private val _hasGetContentRecommendations: Boolean;
|
||||||
|
|
||||||
val rating: IRating;
|
override val rating: IRating;
|
||||||
|
|
||||||
val summary: String;
|
override val summary: String;
|
||||||
val thumbnails: Thumbnails?;
|
override val thumbnails: Thumbnails?;
|
||||||
val segments: List<IJSArticleSegment>;
|
override val segments: List<IJSArticleSegment>;
|
||||||
|
|
||||||
constructor(client: JSClient, obj: V8ValueObject): super(client.config, obj) {
|
constructor(client: JSClient, obj: V8ValueObject): super(client.config, obj) {
|
||||||
val contextName = "PlatformPost";
|
val contextName = "PlatformArticle";
|
||||||
|
|
||||||
rating = obj.getOrDefault<V8ValueObject>(client.config, "rating", contextName, null)?.let { IRating.fromV8(client.config, it, contextName) } ?: RatingLikes(0);
|
rating = obj.getOrDefault<V8ValueObject>(client.config, "rating", contextName, null)?.let { IRating.fromV8(client.config, it, contextName) } ?: RatingLikes(0);
|
||||||
summary = _content.getOrThrow(client.config, "summary", contextName);
|
summary = _content.getOrThrow(client.config, "summary", contextName);
|
||||||
@@ -99,6 +101,7 @@ open class JSArticleDetails : JSContent, IPluginSourced, IPlatformContentDetails
|
|||||||
return when(SegmentType.fromInt(obj.getOrThrow(client.config, "type", "JSArticle.Segment"))) {
|
return when(SegmentType.fromInt(obj.getOrThrow(client.config, "type", "JSArticle.Segment"))) {
|
||||||
SegmentType.TEXT -> JSTextSegment(client, obj);
|
SegmentType.TEXT -> JSTextSegment(client, obj);
|
||||||
SegmentType.IMAGES -> JSImagesSegment(client, obj);
|
SegmentType.IMAGES -> JSImagesSegment(client, obj);
|
||||||
|
SegmentType.HEADER -> JSHeaderSegment(client, obj);
|
||||||
SegmentType.NESTED -> JSNestedSegment(client, obj);
|
SegmentType.NESTED -> JSNestedSegment(client, obj);
|
||||||
else -> null;
|
else -> null;
|
||||||
}
|
}
|
||||||
@@ -110,6 +113,7 @@ enum class SegmentType(val value: Int) {
|
|||||||
UNKNOWN(0),
|
UNKNOWN(0),
|
||||||
TEXT(1),
|
TEXT(1),
|
||||||
IMAGES(2),
|
IMAGES(2),
|
||||||
|
HEADER(3),
|
||||||
|
|
||||||
NESTED(9);
|
NESTED(9);
|
||||||
|
|
||||||
@@ -150,6 +154,17 @@ class JSImagesSegment: IJSArticleSegment {
|
|||||||
caption = obj.getOrDefault(client.config, "caption", contextName, "") ?: "";
|
caption = obj.getOrDefault(client.config, "caption", contextName, "") ?: "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
class JSHeaderSegment: IJSArticleSegment {
|
||||||
|
override val type = SegmentType.HEADER;
|
||||||
|
val content: String;
|
||||||
|
val level: Int;
|
||||||
|
|
||||||
|
constructor(client: JSClient, obj: V8ValueObject) {
|
||||||
|
val contextName = "JSHeaderSegment";
|
||||||
|
content = obj.getOrDefault(client.config, "content", contextName, "") ?: "";
|
||||||
|
level = obj.getOrDefault(client.config, "level", contextName, 1) ?: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
class JSNestedSegment: IJSArticleSegment {
|
class JSNestedSegment: IJSArticleSegment {
|
||||||
override val type = SegmentType.NESTED;
|
override val type = SegmentType.NESTED;
|
||||||
val nested: IPlatformContent;
|
val nested: IPlatformContent;
|
||||||
|
|||||||
-1
@@ -5,7 +5,6 @@ import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
|||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
|
||||||
|
|
||||||
class JSChannelPager : JSPager<PlatformAuthorLink>, IPager<PlatformAuthorLink> {
|
class JSChannelPager : JSPager<PlatformAuthorLink>, IPager<PlatformAuthorLink> {
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -49,8 +49,8 @@ open class JSContent : IPlatformContent, IPluginSourced {
|
|||||||
else
|
else
|
||||||
author = PlatformAuthorLink.UNKNOWN;
|
author = PlatformAuthorLink.UNKNOWN;
|
||||||
|
|
||||||
val datetimeInt = _content.getOrThrow<Int>(config, "datetime", contextName).toLong();
|
val datetimeInt = _content.getOrDefault<Int>(config, "datetime", contextName, null)?.toLong();
|
||||||
if(datetimeInt == 0.toLong())
|
if(datetimeInt == null || datetimeInt == 0.toLong())
|
||||||
datetime = null;
|
datetime = null;
|
||||||
else
|
else
|
||||||
datetime = OffsetDateTime.of(LocalDateTime.ofEpochSecond(datetimeInt, 0, ZoneOffset.UTC), ZoneOffset.UTC);
|
datetime = OffsetDateTime.of(LocalDateTime.ofEpochSecond(datetimeInt, 0, ZoneOffset.UTC), ZoneOffset.UTC);
|
||||||
|
|||||||
+11
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.platforms.js.models
|
|||||||
|
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.api.media.IPluginSourced
|
import com.futo.platformplayer.api.media.IPluginSourced
|
||||||
|
import com.futo.platformplayer.api.media.models.JSChannelContent
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
@@ -15,4 +16,14 @@ class JSContentPager : JSPager<IPlatformContent>, IPluginSourced {
|
|||||||
override fun convertResult(obj: V8ValueObject): IPlatformContent {
|
override fun convertResult(obj: V8ValueObject): IPlatformContent {
|
||||||
return IJSContent.fromV8(plugin, obj);
|
return IJSContent.fromV8(plugin, obj);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class JSChannelContentPager : JSPager<IPlatformContent>, IPluginSourced {
|
||||||
|
override val sourceConfig: SourcePluginConfig get() = config;
|
||||||
|
|
||||||
|
constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) {}
|
||||||
|
|
||||||
|
override fun convertResult(obj: V8ValueObject): IPlatformContent {
|
||||||
|
return JSChannelContent(config, obj);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.models.Thumbnails
|
|||||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
|
import com.futo.platformplayer.getOrDefault
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
|
open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
|
||||||
@@ -17,6 +18,7 @@ open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
|
|||||||
final override val viewCount: Long;
|
final override val viewCount: Long;
|
||||||
|
|
||||||
final override val isLive: Boolean;
|
final override val isLive: Boolean;
|
||||||
|
final override val isShort: Boolean;
|
||||||
|
|
||||||
constructor(config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
|
constructor(config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
|
||||||
val contextName = "PlatformVideo";
|
val contextName = "PlatformVideo";
|
||||||
@@ -26,5 +28,6 @@ open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
|
|||||||
duration = _content.getOrThrow<Int>(config, "duration", contextName).toLong();
|
duration = _content.getOrThrow<Int>(config, "duration", contextName).toLong();
|
||||||
viewCount = _content.getOrThrow(config, "viewCount", contextName);
|
viewCount = _content.getOrThrow(config, "viewCount", contextName);
|
||||||
isLive = _content.getOrThrow(config, "isLive", contextName);
|
isLive = _content.getOrThrow(config, "isLive", contextName);
|
||||||
|
isShort = _content.getOrDefault(config, "isShort", contextName, false) ?: false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.js.models
|
||||||
|
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
|
import com.futo.platformplayer.api.media.IPluginSourced
|
||||||
|
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||||
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||||
|
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||||
|
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
||||||
|
import com.futo.platformplayer.api.media.models.post.TextType
|
||||||
|
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||||
|
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.getOrDefault
|
||||||
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
import com.futo.platformplayer.getOrThrowNullableList
|
||||||
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
|
|
||||||
|
open class JSWeb : JSContent, IPluginSourced {
|
||||||
|
final override val contentType: ContentType get() = ContentType.WEB;
|
||||||
|
|
||||||
|
constructor(config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
|
||||||
|
val contextName = "PlatformWeb";
|
||||||
|
}
|
||||||
|
}
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.js.models
|
||||||
|
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
|
import com.futo.platformplayer.api.media.IPluginSourced
|
||||||
|
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||||
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||||
|
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||||
|
import com.futo.platformplayer.api.media.models.post.TextType
|
||||||
|
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||||
|
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.getOrDefault
|
||||||
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
import com.futo.platformplayer.getOrThrowNullableList
|
||||||
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
|
|
||||||
|
open class JSWebDetails : JSContent, IPluginSourced, IPlatformContentDetails {
|
||||||
|
final override val contentType: ContentType get() = ContentType.WEB;
|
||||||
|
|
||||||
|
val html: String?;
|
||||||
|
//TODO: Options?
|
||||||
|
|
||||||
|
|
||||||
|
constructor(client: JSClient, obj: V8ValueObject): super(client.config, obj) {
|
||||||
|
val contextName = "PlatformWeb";
|
||||||
|
|
||||||
|
html = obj.getOrDefault(client.config, "html", contextName, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;
|
||||||
|
override fun getPlaybackTracker(): IPlaybackTracker? = null;
|
||||||
|
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? = null;
|
||||||
|
|
||||||
|
}
|
||||||
+3
@@ -21,6 +21,8 @@ open class JSAudioUrlSource : IAudioUrlSource, JSSource {
|
|||||||
|
|
||||||
override var priority: Boolean = false;
|
override var priority: Boolean = false;
|
||||||
|
|
||||||
|
override var original: Boolean = false;
|
||||||
|
|
||||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_AUDIOURL, plugin, obj) {
|
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_AUDIOURL, plugin, obj) {
|
||||||
val contextName = "AudioUrlSource";
|
val contextName = "AudioUrlSource";
|
||||||
val config = plugin.config;
|
val config = plugin.config;
|
||||||
@@ -35,6 +37,7 @@ open class JSAudioUrlSource : IAudioUrlSource, JSSource {
|
|||||||
name = _obj.getOrDefault(config, "name", contextName, "${container} ${bitrate}") ?: "${container} ${bitrate}";
|
name = _obj.getOrDefault(config, "name", contextName, "${container} ${bitrate}") ?: "${container} ${bitrate}";
|
||||||
|
|
||||||
priority = if(_obj.has("priority")) obj.getOrThrow(config, "priority", contextName) else false;
|
priority = if(_obj.has("priority")) obj.getOrThrow(config, "priority", contextName) else false;
|
||||||
|
original = if(_obj.has("original")) obj.getOrThrow(config, "original", contextName) else false;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAudioUrl() : String {
|
override fun getAudioUrl() : String {
|
||||||
|
|||||||
+24
-4
@@ -4,6 +4,8 @@ import com.caoccao.javet.values.reference.V8ValueObject
|
|||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.other.IStreamMetaDataSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.other.StreamMetaData
|
||||||
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
@@ -14,13 +16,14 @@ import com.futo.platformplayer.getOrThrow
|
|||||||
import com.futo.platformplayer.others.Language
|
import com.futo.platformplayer.others.Language
|
||||||
import com.futo.platformplayer.states.StateDeveloper
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
|
|
||||||
class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawSource {
|
class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawSource, IStreamMetaDataSource {
|
||||||
override val container : String = "application/dash+xml";
|
override val container : String;
|
||||||
override val name : String;
|
override val name : String;
|
||||||
override val codec: String;
|
override val codec: String;
|
||||||
override val bitrate: Int;
|
override val bitrate: Int;
|
||||||
override val duration: Long;
|
override val duration: Long;
|
||||||
override val priority: Boolean;
|
override val priority: Boolean;
|
||||||
|
override var original: Boolean = false;
|
||||||
|
|
||||||
override val language: String;
|
override val language: String;
|
||||||
|
|
||||||
@@ -29,17 +32,21 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
|
|||||||
|
|
||||||
override val hasGenerate: Boolean;
|
override val hasGenerate: Boolean;
|
||||||
|
|
||||||
|
override var streamMetaData: StreamMetaData? = null;
|
||||||
|
|
||||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
|
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
|
||||||
val contextName = "DashRawSource";
|
val contextName = "DashRawSource";
|
||||||
val config = plugin.config;
|
val config = plugin.config;
|
||||||
name = _obj.getOrThrow(config, "name", contextName);
|
name = _obj.getOrThrow(config, "name", contextName);
|
||||||
url = _obj.getOrThrow(config, "url", contextName);
|
url = _obj.getOrThrow(config, "url", contextName);
|
||||||
|
container = _obj.getOrDefault<String>(config, "container", contextName, null) ?: "application/dash+xml";
|
||||||
manifest = _obj.getOrThrow(config, "manifest", contextName);
|
manifest = _obj.getOrThrow(config, "manifest", contextName);
|
||||||
codec = _obj.getOrDefault(config, "codec", contextName, "") ?: "";
|
codec = _obj.getOrDefault(config, "codec", contextName, "") ?: "";
|
||||||
bitrate = _obj.getOrDefault(config, "bitrate", contextName, 0) ?: 0;
|
bitrate = _obj.getOrDefault(config, "bitrate", contextName, 0) ?: 0;
|
||||||
duration = _obj.getOrDefault(config, "duration", contextName, 0) ?: 0;
|
duration = _obj.getOrDefault(config, "duration", contextName, 0) ?: 0;
|
||||||
priority = _obj.getOrDefault(config, "priority", contextName, false) ?: false;
|
priority = _obj.getOrDefault(config, "priority", contextName, false) ?: false;
|
||||||
language = _obj.getOrDefault(config, "language", contextName, Language.UNKNOWN) ?: Language.UNKNOWN;
|
language = _obj.getOrDefault(config, "language", contextName, Language.UNKNOWN) ?: Language.UNKNOWN;
|
||||||
|
original = if(_obj.has("original")) obj.getOrThrow(config, "original", contextName) else false;
|
||||||
hasGenerate = _obj.has("generate");
|
hasGenerate = _obj.has("generate");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,15 +57,28 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
|
|||||||
throw IllegalStateException("Source object already closed");
|
throw IllegalStateException("Source object already closed");
|
||||||
|
|
||||||
val plugin = _plugin.getUnderlyingPlugin();
|
val plugin = _plugin.getUnderlyingPlugin();
|
||||||
|
|
||||||
|
var result: String? = null;
|
||||||
if(_plugin is DevJSClient)
|
if(_plugin is DevJSClient)
|
||||||
return StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) {
|
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) {
|
||||||
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
||||||
_obj.invokeString("generate");
|
_obj.invokeString("generate");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
return _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
||||||
_obj.invokeString("generate");
|
_obj.invokeString("generate");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(result != null){
|
||||||
|
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
val indexEnd = _obj.getOrDefault<Int>(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
|
||||||
|
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+28
-6
@@ -6,6 +6,8 @@ import com.caoccao.javet.values.reference.V8ValueObject
|
|||||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.other.IStreamMetaDataSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.other.StreamMetaData
|
||||||
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
@@ -20,8 +22,8 @@ interface IJSDashManifestRawSource {
|
|||||||
var manifest: String?;
|
var manifest: String?;
|
||||||
fun generate(): String?;
|
fun generate(): String?;
|
||||||
}
|
}
|
||||||
open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource {
|
open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource, IStreamMetaDataSource {
|
||||||
override val container : String = "application/dash+xml";
|
override val container : String;
|
||||||
override val name : String;
|
override val name : String;
|
||||||
override val width: Int;
|
override val width: Int;
|
||||||
override val height: Int;
|
override val height: Int;
|
||||||
@@ -36,11 +38,14 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
|
|||||||
override val hasGenerate: Boolean;
|
override val hasGenerate: Boolean;
|
||||||
val canMerge: Boolean;
|
val canMerge: Boolean;
|
||||||
|
|
||||||
|
override var streamMetaData: StreamMetaData? = null;
|
||||||
|
|
||||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
|
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
|
||||||
val contextName = "DashRawSource";
|
val contextName = "DashRawSource";
|
||||||
val config = plugin.config;
|
val config = plugin.config;
|
||||||
name = _obj.getOrThrow(config, "name", contextName);
|
name = _obj.getOrThrow(config, "name", contextName);
|
||||||
url = _obj.getOrThrow(config, "url", contextName);
|
url = _obj.getOrThrow(config, "url", contextName);
|
||||||
|
container = _obj.getOrDefault<String>(config, "container", contextName, null) ?: "application/dash+xml";
|
||||||
manifest = _obj.getOrDefault<String>(config, "manifest", contextName, null);
|
manifest = _obj.getOrDefault<String>(config, "manifest", contextName, null);
|
||||||
width = _obj.getOrDefault(config, "width", contextName, 0) ?: 0;
|
width = _obj.getOrDefault(config, "width", contextName, 0) ?: 0;
|
||||||
height = _obj.getOrDefault(config, "height", contextName, 0) ?: 0;
|
height = _obj.getOrDefault(config, "height", contextName, 0) ?: 0;
|
||||||
@@ -57,17 +62,30 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
|
|||||||
return manifest;
|
return manifest;
|
||||||
if(_obj.isClosed)
|
if(_obj.isClosed)
|
||||||
throw IllegalStateException("Source object already closed");
|
throw IllegalStateException("Source object already closed");
|
||||||
|
|
||||||
|
var result: String? = null;
|
||||||
if(_plugin is DevJSClient) {
|
if(_plugin is DevJSClient) {
|
||||||
return StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") {
|
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") {
|
||||||
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
||||||
_obj.invokeString("generate");
|
_obj.invokeString("generate");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
return _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
||||||
_obj.invokeString("generate");
|
_obj.invokeString("generate");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if(result != null){
|
||||||
|
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
val indexEnd = _obj.getOrDefault<Int>(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
|
||||||
|
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,12 +118,16 @@ class JSDashManifestMergingRawSource(
|
|||||||
if(videoDash == null) return null;
|
if(videoDash == null) return null;
|
||||||
|
|
||||||
//TODO: Temporary simple solution..make more reliable version
|
//TODO: Temporary simple solution..make more reliable version
|
||||||
|
|
||||||
|
var result: String? = null;
|
||||||
val audioAdaptationSet = adaptationSetRegex.find(audioDash!!);
|
val audioAdaptationSet = adaptationSetRegex.find(audioDash!!);
|
||||||
if(audioAdaptationSet != null) {
|
if(audioAdaptationSet != null) {
|
||||||
return videoDash.replace("</AdaptationSet>", "</AdaptationSet>\n" + audioAdaptationSet.value)
|
result = videoDash.replace("</AdaptationSet>", "</AdaptationSet>\n" + audioAdaptationSet.value)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
return videoDash;
|
result = videoDash;
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
+2
@@ -21,6 +21,7 @@ class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource {
|
|||||||
override val language: String;
|
override val language: String;
|
||||||
|
|
||||||
override var priority: Boolean = false;
|
override var priority: Boolean = false;
|
||||||
|
override var original: Boolean = false;
|
||||||
|
|
||||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_HLS, plugin, obj) {
|
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_HLS, plugin, obj) {
|
||||||
val contextName = "HLSAudioSource";
|
val contextName = "HLSAudioSource";
|
||||||
@@ -32,6 +33,7 @@ class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource {
|
|||||||
language = _obj.getOrThrow(config, "language", contextName);
|
language = _obj.getOrThrow(config, "language", contextName);
|
||||||
|
|
||||||
priority = obj.getOrNull(config, "priority", contextName) ?: false;
|
priority = obj.getOrNull(config, "priority", contextName) ?: false;
|
||||||
|
original = if(_obj.has("original")) obj.getOrThrow(config, "original", contextName) else false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.local
|
||||||
|
|
||||||
|
class LocalClient {
|
||||||
|
//TODO
|
||||||
|
}
|
||||||
+85
@@ -0,0 +1,85 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.local.models
|
||||||
|
|
||||||
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
|
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||||
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||||
|
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||||
|
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.DownloadedVideoMuxedSourceDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
|
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoFileSource
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.downloads.VideoLocal
|
||||||
|
import java.io.File
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import java.time.ZoneId
|
||||||
|
|
||||||
|
class LocalVideoDetails: IPlatformVideoDetails {
|
||||||
|
|
||||||
|
override val contentType: ContentType get() = ContentType.UNKNOWN;
|
||||||
|
|
||||||
|
override val id: PlatformID;
|
||||||
|
override val name: String;
|
||||||
|
override val author: PlatformAuthorLink;
|
||||||
|
|
||||||
|
override val datetime: OffsetDateTime?;
|
||||||
|
|
||||||
|
override val url: String;
|
||||||
|
override val shareUrl: String;
|
||||||
|
override val rating: IRating = RatingLikes(0);
|
||||||
|
override val description: String = "";
|
||||||
|
|
||||||
|
override val video: IVideoSourceDescriptor;
|
||||||
|
override val preview: IVideoSourceDescriptor? = null;
|
||||||
|
override val live: IVideoSource? = null;
|
||||||
|
override val dash: IDashManifestSource? = null;
|
||||||
|
override val hls: IHLSManifestSource? = null;
|
||||||
|
override val subtitles: List<ISubtitleSource> = listOf()
|
||||||
|
|
||||||
|
override val thumbnails: Thumbnails;
|
||||||
|
override val duration: Long;
|
||||||
|
override val viewCount: Long = 0;
|
||||||
|
override val isLive: Boolean = false;
|
||||||
|
override val isShort: Boolean = false;
|
||||||
|
|
||||||
|
constructor(file: File) {
|
||||||
|
id = PlatformID("Local", file.path, "LOCAL")
|
||||||
|
name = file.name;
|
||||||
|
author = PlatformAuthorLink.UNKNOWN;
|
||||||
|
|
||||||
|
url = file.canonicalPath;
|
||||||
|
shareUrl = "";
|
||||||
|
|
||||||
|
duration = 0;
|
||||||
|
thumbnails = Thumbnails(arrayOf());
|
||||||
|
|
||||||
|
datetime = OffsetDateTime.ofInstant(
|
||||||
|
Instant.ofEpochMilli(file.lastModified()),
|
||||||
|
ZoneId.systemDefault()
|
||||||
|
);
|
||||||
|
video = LocalVideoMuxedSourceDescriptor(LocalVideoFileSource(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPlaybackTracker(): IPlaybackTracker? {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.local.models
|
||||||
|
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoFileSource
|
||||||
|
import com.futo.platformplayer.downloads.VideoLocal
|
||||||
|
|
||||||
|
class LocalVideoMuxedSourceDescriptor(
|
||||||
|
private val video: LocalVideoFileSource
|
||||||
|
) : VideoMuxedSourceDescriptor() {
|
||||||
|
override val videoSources: Array<IVideoSource> get() = arrayOf(video);
|
||||||
|
}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.local.models
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.provider.MediaStore.Video
|
||||||
|
|
||||||
|
class MediaStoreVideo {
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val URI = MediaStore.Files.getContentUri("external");
|
||||||
|
val PROJECTION = arrayOf(Video.Media._ID, Video.Media.TITLE, Video.Media.DURATION, Video.Media.HEIGHT, Video.Media.WIDTH, Video.Media.MIME_TYPE);
|
||||||
|
val ORDER = MediaStore.Video.Media.TITLE;
|
||||||
|
|
||||||
|
fun readMediaStoreVideo(cursor: Cursor) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun query(context: Context, selection: String, args: Array<String>, order: String? = null): Cursor? {
|
||||||
|
val cursor = context.contentResolver.query(URI, PROJECTION, selection, args, order ?: ORDER, null);
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.local.models.sources
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.provider.MediaStore.Video
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
|
||||||
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class LocalVideoFileSource: IVideoSource {
|
||||||
|
|
||||||
|
|
||||||
|
override val name: String;
|
||||||
|
override val width: Int;
|
||||||
|
override val height: Int;
|
||||||
|
override val container: String;
|
||||||
|
override val codec: String = ""
|
||||||
|
override val bitrate: Int = 0
|
||||||
|
override val duration: Long;
|
||||||
|
override val priority: Boolean = false;
|
||||||
|
|
||||||
|
constructor(file: File) {
|
||||||
|
name = file.name;
|
||||||
|
width = 0;
|
||||||
|
height = 0;
|
||||||
|
container = VideoHelper.videoExtensionToMimetype(file.extension) ?: "";
|
||||||
|
duration = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import com.futo.platformplayer.constructs.Event1
|
|||||||
* A RefreshPager represents a pager that can be modified overtime (eg. By getting more results later, by recreating the pager)
|
* A RefreshPager represents a pager that can be modified overtime (eg. By getting more results later, by recreating the pager)
|
||||||
* When the onPagerChanged event is emitted, a new pager instance is passed, or requested via getCurrentPager
|
* When the onPagerChanged event is emitted, a new pager instance is passed, or requested via getCurrentPager
|
||||||
*/
|
*/
|
||||||
interface IRefreshPager<T> {
|
interface IRefreshPager<T>: IPager<T> {
|
||||||
val onPagerChanged: Event1<IPager<T>>;
|
val onPagerChanged: Event1<IPager<T>>;
|
||||||
val onPagerError: Event1<Throwable>;
|
val onPagerError: Event1<Throwable>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.futo.platformplayer.api.media.structures
|
package com.futo.platformplayer.api.media.structures
|
||||||
|
|
||||||
|
import com.futo.platformplayer.api.media.structures.ReusablePager.Window
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -9,8 +11,8 @@ import com.futo.platformplayer.logging.Logger
|
|||||||
* A "Window" is effectively a pager that just reads previous results from the shared results, but when the end is reached, it will call nextPage on the parent if possible for new results.
|
* A "Window" is effectively a pager that just reads previous results from the shared results, but when the end is reached, it will call nextPage on the parent if possible for new results.
|
||||||
* This allows multiple Windows to exist of the same pager, without messing with position, or duplicate requests
|
* This allows multiple Windows to exist of the same pager, without messing with position, or duplicate requests
|
||||||
*/
|
*/
|
||||||
class ReusablePager<T>: INestedPager<T>, IPager<T> {
|
open class ReusablePager<T>: INestedPager<T>, IReusablePager<T> {
|
||||||
private val _pager: IPager<T>;
|
protected var _pager: IPager<T>;
|
||||||
val previousResults = arrayListOf<T>();
|
val previousResults = arrayListOf<T>();
|
||||||
|
|
||||||
constructor(subPager: IPager<T>) {
|
constructor(subPager: IPager<T>) {
|
||||||
@@ -44,7 +46,7 @@ class ReusablePager<T>: INestedPager<T>, IPager<T> {
|
|||||||
return previousResults;
|
return previousResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getWindow(): Window<T> {
|
override fun getWindow(): Window<T> {
|
||||||
return Window(this);
|
return Window(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,4 +97,118 @@ class ReusablePager<T>: INestedPager<T>, IPager<T> {
|
|||||||
return ReusablePager(this);
|
return ReusablePager(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class ReusableRefreshPager<T>: INestedPager<T>, IReusablePager<T> {
|
||||||
|
protected var _pager: IRefreshPager<T>;
|
||||||
|
val previousResults = arrayListOf<T>();
|
||||||
|
|
||||||
|
private var _currentPage: IPager<T>;
|
||||||
|
|
||||||
|
|
||||||
|
val onPagerChanged = Event1<IPager<T>>()
|
||||||
|
val onPagerError = Event1<Throwable>()
|
||||||
|
|
||||||
|
constructor(subPager: IRefreshPager<T>) {
|
||||||
|
this._pager = subPager;
|
||||||
|
_currentPage = this;
|
||||||
|
synchronized(previousResults) {
|
||||||
|
previousResults.addAll(subPager.getResults());
|
||||||
|
}
|
||||||
|
_pager.onPagerError.subscribe(onPagerError::emit);
|
||||||
|
_pager.onPagerChanged.subscribe {
|
||||||
|
_currentPage = it;
|
||||||
|
synchronized(previousResults) {
|
||||||
|
previousResults.clear();
|
||||||
|
previousResults.addAll(it.getResults());
|
||||||
|
}
|
||||||
|
|
||||||
|
onPagerChanged.emit(_currentPage);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun findPager(query: (IPager<T>) -> Boolean): IPager<T>? {
|
||||||
|
if(query(_pager))
|
||||||
|
return _pager;
|
||||||
|
else if(_pager is INestedPager<*>)
|
||||||
|
return (_pager as INestedPager<T>).findPager(query);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hasMorePages(): Boolean {
|
||||||
|
return _pager.hasMorePages();
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun nextPage() {
|
||||||
|
_pager.nextPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getResults(): List<T> {
|
||||||
|
val results = _pager.getResults();
|
||||||
|
synchronized(previousResults) {
|
||||||
|
previousResults.addAll(results);
|
||||||
|
}
|
||||||
|
return previousResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getWindow(): RefreshWindow<T> {
|
||||||
|
return RefreshWindow(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class RefreshWindow<T>: IPager<T>, INestedPager<T>, IRefreshPager<T> {
|
||||||
|
private val _parent: ReusableRefreshPager<T>;
|
||||||
|
private var _position: Int = 0;
|
||||||
|
private var _read: Int = 0;
|
||||||
|
|
||||||
|
private var _currentResults: List<T>;
|
||||||
|
|
||||||
|
override val onPagerChanged = Event1<IPager<T>>();
|
||||||
|
override val onPagerError = Event1<Throwable>();
|
||||||
|
|
||||||
|
|
||||||
|
override fun getCurrentPager(): IPager<T> {
|
||||||
|
return _parent.getWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(parent: ReusableRefreshPager<T>) {
|
||||||
|
_parent = parent;
|
||||||
|
|
||||||
|
synchronized(_parent.previousResults) {
|
||||||
|
_currentResults = _parent.previousResults.toList();
|
||||||
|
_read += _currentResults.size;
|
||||||
|
}
|
||||||
|
parent.onPagerChanged.subscribe(onPagerChanged::emit);
|
||||||
|
parent.onPagerError.subscribe(onPagerError::emit);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun hasMorePages(): Boolean {
|
||||||
|
return _parent.previousResults.size > _read || _parent.hasMorePages();
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun nextPage() {
|
||||||
|
synchronized(_parent.previousResults) {
|
||||||
|
if (_parent.previousResults.size <= _read) {
|
||||||
|
_parent.nextPage();
|
||||||
|
_parent.getResults();
|
||||||
|
}
|
||||||
|
_currentResults = _parent.previousResults.drop(_read).toList();
|
||||||
|
_read += _currentResults.size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getResults(): List<T> {
|
||||||
|
return _currentResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun findPager(query: (IPager<T>) -> Boolean): IPager<T>? {
|
||||||
|
return _parent.findPager(query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IReusablePager<T>: IPager<T> {
|
||||||
|
fun getWindow(): IPager<T>;
|
||||||
}
|
}
|
||||||
@@ -149,6 +149,7 @@ class AirPlayCastingDevice : CastingDevice {
|
|||||||
break;
|
break;
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.w(TAG, "Failed to get setup initial connection to AirPlay device.", e)
|
Logger.w(TAG, "Failed to get setup initial connection to AirPlay device.", e)
|
||||||
|
delay(1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ import com.futo.platformplayer.toHexString
|
|||||||
import com.futo.platformplayer.toInetAddress
|
import com.futo.platformplayer.toInetAddress
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
@@ -56,6 +58,10 @@ class ChromecastCastingDevice : CastingDevice {
|
|||||||
private var _mediaSessionId: Int? = null;
|
private var _mediaSessionId: Int? = null;
|
||||||
private var _thread: Thread? = null;
|
private var _thread: Thread? = null;
|
||||||
private var _pingThread: Thread? = null;
|
private var _pingThread: Thread? = null;
|
||||||
|
private var _launchRetries = 0
|
||||||
|
private val MAX_LAUNCH_RETRIES = 3
|
||||||
|
private var _lastLaunchTime_ms = 0L
|
||||||
|
private var _retryJob: Job? = null
|
||||||
|
|
||||||
constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() {
|
constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
@@ -229,6 +235,7 @@ class ChromecastCastingDevice : CastingDevice {
|
|||||||
launchObject.put("appId", "CC1AD845");
|
launchObject.put("appId", "CC1AD845");
|
||||||
launchObject.put("requestId", _requestId++);
|
launchObject.put("requestId", _requestId++);
|
||||||
sendChannelMessage("sender-0", "receiver-0", "urn:x-cast:com.google.cast.receiver", launchObject.toString());
|
sendChannelMessage("sender-0", "receiver-0", "urn:x-cast:com.google.cast.receiver", launchObject.toString());
|
||||||
|
_lastLaunchTime_ms = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getStatus() {
|
private fun getStatus() {
|
||||||
@@ -268,6 +275,7 @@ class ChromecastCastingDevice : CastingDevice {
|
|||||||
_contentType = null;
|
_contentType = null;
|
||||||
_streamType = null;
|
_streamType = null;
|
||||||
_sessionId = null;
|
_sessionId = null;
|
||||||
|
_launchRetries = 0
|
||||||
_transportId = null;
|
_transportId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,6 +290,7 @@ class ChromecastCastingDevice : CastingDevice {
|
|||||||
|
|
||||||
_started = true;
|
_started = true;
|
||||||
_sessionId = null;
|
_sessionId = null;
|
||||||
|
_launchRetries = 0
|
||||||
_mediaSessionId = null;
|
_mediaSessionId = null;
|
||||||
|
|
||||||
Logger.i(TAG, "Starting...");
|
Logger.i(TAG, "Starting...");
|
||||||
@@ -322,6 +331,7 @@ class ChromecastCastingDevice : CastingDevice {
|
|||||||
break;
|
break;
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.w(TAG, "Failed to get setup initial connection to ChromeCast device.", e)
|
Logger.w(TAG, "Failed to get setup initial connection to ChromeCast device.", e)
|
||||||
|
Thread.sleep(1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,7 +402,7 @@ class ChromecastCastingDevice : CastingDevice {
|
|||||||
try {
|
try {
|
||||||
val inputStream = _inputStream ?: break;
|
val inputStream = _inputStream ?: break;
|
||||||
|
|
||||||
synchronized(_inputStreamLock)
|
val message = synchronized(_inputStreamLock)
|
||||||
{
|
{
|
||||||
Log.d(TAG, "Receiving next packet...");
|
Log.d(TAG, "Receiving next packet...");
|
||||||
val b1 = inputStream.readUnsignedByte();
|
val b1 = inputStream.readUnsignedByte();
|
||||||
@@ -404,7 +414,7 @@ class ChromecastCastingDevice : CastingDevice {
|
|||||||
if (size > buffer.size) {
|
if (size > buffer.size) {
|
||||||
Logger.w(TAG, "Skipping packet that is too large $size bytes.")
|
Logger.w(TAG, "Skipping packet that is too large $size bytes.")
|
||||||
inputStream.skip(size.toLong());
|
inputStream.skip(size.toLong());
|
||||||
return@synchronized
|
return@synchronized null
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "Received header indicating $size bytes. Waiting for message.");
|
Log.d(TAG, "Received header indicating $size bytes. Waiting for message.");
|
||||||
@@ -413,15 +423,19 @@ class ChromecastCastingDevice : CastingDevice {
|
|||||||
//TODO: In the future perhaps this size-1 will cause issues, why is there a 0 on the end?
|
//TODO: In the future perhaps this size-1 will cause issues, why is there a 0 on the end?
|
||||||
val messageBytes = buffer.sliceArray(IntRange(0, size - 1));
|
val messageBytes = buffer.sliceArray(IntRange(0, size - 1));
|
||||||
Log.d(TAG, "Received $size bytes: ${messageBytes.toHexString()}.");
|
Log.d(TAG, "Received $size bytes: ${messageBytes.toHexString()}.");
|
||||||
val message = ChromeCast.CastMessage.parseFrom(messageBytes);
|
val msg = ChromeCast.CastMessage.parseFrom(messageBytes);
|
||||||
if (message.namespace != "urn:x-cast:com.google.cast.tp.heartbeat") {
|
if (msg.namespace != "urn:x-cast:com.google.cast.tp.heartbeat") {
|
||||||
Logger.i(TAG, "Received message: $message");
|
Logger.i(TAG, "Received message: $msg");
|
||||||
}
|
}
|
||||||
|
return@synchronized msg
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message != null) {
|
||||||
try {
|
try {
|
||||||
handleMessage(message);
|
handleMessage(message);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.w(TAG, "Failed to handle message.", e);
|
Logger.w(TAG, "Failed to handle message.", e);
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: java.net.SocketException) {
|
} catch (e: java.net.SocketException) {
|
||||||
@@ -511,6 +525,7 @@ class ChromecastCastingDevice : CastingDevice {
|
|||||||
if (_sessionId == null) {
|
if (_sessionId == null) {
|
||||||
connectionState = CastConnectionState.CONNECTED;
|
connectionState = CastConnectionState.CONNECTED;
|
||||||
_sessionId = applicationUpdate.getString("sessionId");
|
_sessionId = applicationUpdate.getString("sessionId");
|
||||||
|
_launchRetries = 0
|
||||||
|
|
||||||
val transportId = applicationUpdate.getString("transportId");
|
val transportId = applicationUpdate.getString("transportId");
|
||||||
connectMediaChannel(transportId);
|
connectMediaChannel(transportId);
|
||||||
@@ -525,21 +540,40 @@ class ChromecastCastingDevice : CastingDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!sessionIsRunning) {
|
if (!sessionIsRunning) {
|
||||||
_sessionId = null;
|
if (System.currentTimeMillis() - _lastLaunchTime_ms > 5000) {
|
||||||
_mediaSessionId = null;
|
_sessionId = null
|
||||||
setTime(0.0);
|
_mediaSessionId = null
|
||||||
_transportId = null;
|
setTime(0.0)
|
||||||
Logger.w(TAG, "Session not found.");
|
_transportId = null
|
||||||
|
|
||||||
if (_launching) {
|
if (_launching && _launchRetries < MAX_LAUNCH_RETRIES) {
|
||||||
Logger.i(TAG, "Player not found, launching.");
|
Logger.i(TAG, "No player yet; attempting launch #${_launchRetries + 1}")
|
||||||
launchPlayer();
|
_launchRetries++
|
||||||
|
launchPlayer()
|
||||||
|
} else if (!_launching && _launchRetries < MAX_LAUNCH_RETRIES) {
|
||||||
|
// Maybe the first GET_STATUS came back empty; still try launching
|
||||||
|
Logger.i(TAG, "Player not found; triggering launch #${_launchRetries + 1}")
|
||||||
|
_launching = true
|
||||||
|
_launchRetries++
|
||||||
|
launchPlayer()
|
||||||
|
} else {
|
||||||
|
Logger.e(TAG, "Player not found after $_launchRetries attempts; giving up.")
|
||||||
|
Logger.i(TAG, "Unable to start media receiver on device")
|
||||||
|
stop()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Logger.i(TAG, "Player not found, disconnecting.");
|
if (_retryJob == null) {
|
||||||
stop();
|
Logger.i(TAG, "Scheduled retry job over 5 seconds")
|
||||||
|
_retryJob = _scopeIO?.launch(Dispatchers.IO) {
|
||||||
|
delay(5000)
|
||||||
|
getStatus()
|
||||||
|
_retryJob = null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_launching = false;
|
_launching = false
|
||||||
|
_launchRetries = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
val volume = status.getJSONObject("volume");
|
val volume = status.getJSONObject("volume");
|
||||||
@@ -581,6 +615,8 @@ class ChromecastCastingDevice : CastingDevice {
|
|||||||
if (message.sourceId == "receiver-0") {
|
if (message.sourceId == "receiver-0") {
|
||||||
Logger.i(TAG, "Close received.");
|
Logger.i(TAG, "Close received.");
|
||||||
stop();
|
stop();
|
||||||
|
} else if (_transportId == message.sourceId) {
|
||||||
|
throw Exception("Transport id closed.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -615,6 +651,9 @@ class ChromecastCastingDevice : CastingDevice {
|
|||||||
localAddress = null;
|
localAddress = null;
|
||||||
_started = false;
|
_started = false;
|
||||||
|
|
||||||
|
_retryJob?.cancel()
|
||||||
|
_retryJob = null
|
||||||
|
|
||||||
val socket = _socket;
|
val socket = _socket;
|
||||||
val scopeIO = _scopeIO;
|
val scopeIO = _scopeIO;
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.futo.platformplayer.casting
|
|||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.casting.models.FCastDecryptedMessage
|
import com.futo.platformplayer.casting.models.FCastDecryptedMessage
|
||||||
import com.futo.platformplayer.casting.models.FCastEncryptedMessage
|
import com.futo.platformplayer.casting.models.FCastEncryptedMessage
|
||||||
@@ -24,6 +25,7 @@ import com.futo.platformplayer.toInetAddress
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
@@ -32,6 +34,7 @@ import java.io.IOException
|
|||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
|
import java.net.Inet4Address
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.net.Socket
|
import java.net.Socket
|
||||||
@@ -90,7 +93,7 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
private var _version: Long = 1;
|
private var _version: Long = 1;
|
||||||
private var _thread: Thread? = null
|
private var _thread: Thread? = null
|
||||||
private var _pingThread: Thread? = null
|
private var _pingThread: Thread? = null
|
||||||
private var _lastPongTime = -1L
|
@Volatile private var _lastPongTime = System.currentTimeMillis()
|
||||||
private var _outputStreamLock = Object()
|
private var _outputStreamLock = Object()
|
||||||
|
|
||||||
constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() {
|
constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() {
|
||||||
@@ -287,6 +290,7 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
break;
|
break;
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.w(TAG, "Failed to get setup initial connection to FastCast device.", e)
|
Logger.w(TAG, "Failed to get setup initial connection to FastCast device.", e)
|
||||||
|
Thread.sleep(1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,9 +328,9 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
localAddress = _socket?.localAddress;
|
localAddress = _socket?.localAddress
|
||||||
connectionState = CastConnectionState.CONNECTED;
|
_lastPongTime = System.currentTimeMillis()
|
||||||
_lastPongTime = -1L
|
connectionState = CastConnectionState.CONNECTED
|
||||||
|
|
||||||
val buffer = ByteArray(4096);
|
val buffer = ByteArray(4096);
|
||||||
|
|
||||||
@@ -402,36 +406,32 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
|
|
||||||
_pingThread = Thread {
|
_pingThread = Thread {
|
||||||
Logger.i(TAG, "Started ping loop.")
|
Logger.i(TAG, "Started ping loop.")
|
||||||
|
|
||||||
while (_scopeIO?.isActive == true) {
|
while (_scopeIO?.isActive == true) {
|
||||||
try {
|
if (connectionState == CastConnectionState.CONNECTED) {
|
||||||
send(Opcode.Ping)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Log.w(TAG, "Failed to send ping.")
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
_socket?.close()
|
send(Opcode.Ping)
|
||||||
_inputStream?.close()
|
if (System.currentTimeMillis() - _lastPongTime > 15000) {
|
||||||
_outputStream?.close()
|
Logger.w(TAG, "Closing socket due to last pong time being larger than 15 seconds.")
|
||||||
|
try {
|
||||||
|
_socket?.close()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.w(TAG, "Failed to close socket.", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Log.w(TAG, "Failed to close socket.", e)
|
Log.w(TAG, "Failed to send ping.")
|
||||||
|
try {
|
||||||
|
_socket?.close()
|
||||||
|
_inputStream?.close()
|
||||||
|
_outputStream?.close()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.w(TAG, "Failed to close socket.", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Thread.sleep(5000)
|
||||||
/*if (_lastPongTime != -1L && System.currentTimeMillis() - _lastPongTime > 6000) {
|
|
||||||
Logger.w(TAG, "Closing socket due to last pong time being larger than 6 seconds.")
|
|
||||||
|
|
||||||
try {
|
|
||||||
_socket?.close()
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Log.w(TAG, "Failed to close socket.", e)
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
|
|
||||||
Thread.sleep(2000)
|
|
||||||
}
|
}
|
||||||
|
Logger.i(TAG, "Stopped ping loop.")
|
||||||
Logger.i(TAG, "Stopped ping loop.");
|
|
||||||
}.apply { start() }
|
}.apply { start() }
|
||||||
} else {
|
} else {
|
||||||
Log.i(TAG, "Thread was still alive, not restarted")
|
Log.i(TAG, "Thread was still alive, not restarted")
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
package com.futo.platformplayer.casting
|
package com.futo.platformplayer.casting
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.net.nsd.NsdManager
|
||||||
|
import android.net.nsd.NsdServiceInfo
|
||||||
|
import android.os.Build
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.Xml
|
import java.net.NetworkInterface
|
||||||
|
import java.net.Inet4Address
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
@@ -37,9 +43,8 @@ import com.futo.platformplayer.builders.DashBuilder
|
|||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.constructs.Event2
|
||||||
import com.futo.platformplayer.exceptions.UnsupportedCastException
|
import com.futo.platformplayer.exceptions.UnsupportedCastException
|
||||||
|
import com.futo.platformplayer.findPreferredAddress
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.mdns.DnsService
|
|
||||||
import com.futo.platformplayer.mdns.ServiceDiscoverer
|
|
||||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
import com.futo.platformplayer.parsers.HLS
|
import com.futo.platformplayer.parsers.HLS
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
@@ -53,10 +58,11 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.ByteArrayInputStream
|
import java.net.Inet6Address
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
|
import java.util.Collections
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
class StateCasting {
|
class StateCasting {
|
||||||
@@ -64,11 +70,10 @@ class StateCasting {
|
|||||||
private val _scopeMain = CoroutineScope(Dispatchers.Main);
|
private val _scopeMain = CoroutineScope(Dispatchers.Main);
|
||||||
private val _storage: CastingDeviceInfoStorage = FragmentedStorage.get();
|
private val _storage: CastingDeviceInfoStorage = FragmentedStorage.get();
|
||||||
|
|
||||||
private val _castServer = ManagedHttpServer(9999);
|
private val _castServer = ManagedHttpServer();
|
||||||
private var _started = false;
|
private var _started = false;
|
||||||
|
|
||||||
var devices: HashMap<String, CastingDevice> = hashMapOf();
|
var devices: HashMap<String, CastingDevice> = hashMapOf();
|
||||||
var rememberedDevices: ArrayList<CastingDevice> = arrayListOf();
|
|
||||||
val onDeviceAdded = Event1<CastingDevice>();
|
val onDeviceAdded = Event1<CastingDevice>();
|
||||||
val onDeviceChanged = Event1<CastingDevice>();
|
val onDeviceChanged = Event1<CastingDevice>();
|
||||||
val onDeviceRemoved = Event1<CastingDevice>();
|
val onDeviceRemoved = Event1<CastingDevice>();
|
||||||
@@ -82,48 +87,15 @@ class StateCasting {
|
|||||||
private var _audioExecutor: JSRequestExecutor? = null
|
private var _audioExecutor: JSRequestExecutor? = null
|
||||||
private val _client = ManagedHttpClient();
|
private val _client = ManagedHttpClient();
|
||||||
var _resumeCastingDevice: CastingDeviceInfo? = null;
|
var _resumeCastingDevice: CastingDeviceInfo? = null;
|
||||||
val _serviceDiscoverer = ServiceDiscoverer(arrayOf(
|
private var _nsdManager: NsdManager? = null
|
||||||
"_googlecast._tcp.local",
|
|
||||||
"_airplay._tcp.local",
|
|
||||||
"_fastcast._tcp.local",
|
|
||||||
"_fcast._tcp.local"
|
|
||||||
)) { handleServiceUpdated(it) }
|
|
||||||
|
|
||||||
val isCasting: Boolean get() = activeDevice != null;
|
val isCasting: Boolean get() = activeDevice != null;
|
||||||
|
|
||||||
private fun handleServiceUpdated(services: List<DnsService>) {
|
private val _discoveryListeners = mapOf(
|
||||||
for (s in services) {
|
"_googlecast._tcp" to createDiscoveryListener(::addOrUpdateChromeCastDevice),
|
||||||
//TODO: Addresses IPv4 only?
|
"_airplay._tcp" to createDiscoveryListener(::addOrUpdateAirPlayDevice),
|
||||||
val addresses = s.addresses.toTypedArray()
|
"_fastcast._tcp" to createDiscoveryListener(::addOrUpdateFastCastDevice),
|
||||||
val port = s.port.toInt()
|
"_fcast._tcp" to createDiscoveryListener(::addOrUpdateFastCastDevice)
|
||||||
var name = s.texts.firstOrNull { it.startsWith("md=") }?.substring("md=".length)
|
)
|
||||||
if (s.name.endsWith("._googlecast._tcp.local")) {
|
|
||||||
if (name == null) {
|
|
||||||
name = s.name.substring(0, s.name.length - "._googlecast._tcp.local".length)
|
|
||||||
}
|
|
||||||
|
|
||||||
addOrUpdateChromeCastDevice(name, addresses, port)
|
|
||||||
} else if (s.name.endsWith("._airplay._tcp.local")) {
|
|
||||||
if (name == null) {
|
|
||||||
name = s.name.substring(0, s.name.length - "._airplay._tcp.local".length)
|
|
||||||
}
|
|
||||||
|
|
||||||
addOrUpdateAirPlayDevice(name, addresses, port)
|
|
||||||
} else if (s.name.endsWith("._fastcast._tcp.local")) {
|
|
||||||
if (name == null) {
|
|
||||||
name = s.name.substring(0, s.name.length - "._fastcast._tcp.local".length)
|
|
||||||
}
|
|
||||||
|
|
||||||
addOrUpdateFastCastDevice(name, addresses, port)
|
|
||||||
} else if (s.name.endsWith("._fcast._tcp.local")) {
|
|
||||||
if (name == null) {
|
|
||||||
name = s.name.substring(0, s.name.length - "._fcast._tcp.local".length)
|
|
||||||
}
|
|
||||||
|
|
||||||
addOrUpdateFastCastDevice(name, addresses, port)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun handleUrl(context: Context, url: String) {
|
fun handleUrl(context: Context, url: String) {
|
||||||
val uri = Uri.parse(url)
|
val uri = Uri.parse(url)
|
||||||
@@ -188,30 +160,33 @@ class StateCasting {
|
|||||||
|
|
||||||
Logger.i(TAG, "CastingService starting...");
|
Logger.i(TAG, "CastingService starting...");
|
||||||
|
|
||||||
rememberedDevices.clear();
|
|
||||||
rememberedDevices.addAll(_storage.deviceInfos.map { deviceFromCastingDeviceInfo(it) });
|
|
||||||
|
|
||||||
_castServer.start();
|
_castServer.start();
|
||||||
enableDeveloper(true);
|
enableDeveloper(true);
|
||||||
|
|
||||||
Logger.i(TAG, "CastingService started.");
|
Logger.i(TAG, "CastingService started.");
|
||||||
|
|
||||||
|
_nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun startDiscovering() {
|
fun startDiscovering() {
|
||||||
try {
|
_nsdManager?.apply {
|
||||||
_serviceDiscoverer.start()
|
_discoveryListeners.forEach {
|
||||||
} catch (e: Throwable) {
|
discoverServices(it.key, NsdManager.PROTOCOL_DNS_SD, it.value)
|
||||||
Logger.i(TAG, "Failed to start ServiceDiscoverer", e)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun stopDiscovering() {
|
fun stopDiscovering() {
|
||||||
try {
|
_nsdManager?.apply {
|
||||||
_serviceDiscoverer.stop()
|
_discoveryListeners.forEach {
|
||||||
} catch (e: Throwable) {
|
try {
|
||||||
Logger.i(TAG, "Failed to stop ServiceDiscoverer", e)
|
stopServiceDiscovery(it.value)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to stop service discovery", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,8 +212,90 @@ class StateCasting {
|
|||||||
_castServer.removeAllHandlers();
|
_castServer.removeAllHandlers();
|
||||||
|
|
||||||
Logger.i(TAG, "CastingService stopped.")
|
Logger.i(TAG, "CastingService stopped.")
|
||||||
|
|
||||||
|
_nsdManager = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createDiscoveryListener(addOrUpdate: (String, Array<InetAddress>, Int) -> Unit): NsdManager.DiscoveryListener {
|
||||||
|
return object : NsdManager.DiscoveryListener {
|
||||||
|
override fun onDiscoveryStarted(regType: String) {
|
||||||
|
Log.d(TAG, "Service discovery started for $regType")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDiscoveryStopped(serviceType: String) {
|
||||||
|
Log.i(TAG, "Discovery stopped: $serviceType")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceLost(service: NsdServiceInfo) {
|
||||||
|
Log.e(TAG, "service lost: $service")
|
||||||
|
// TODO: Handle service lost, e.g., remove device
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
|
||||||
|
Log.e(TAG, "Discovery failed for $serviceType: Error code:$errorCode")
|
||||||
|
try {
|
||||||
|
_nsdManager?.stopServiceDiscovery(this)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to stop service discovery", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {
|
||||||
|
Log.e(TAG, "Stop discovery failed for $serviceType: Error code:$errorCode")
|
||||||
|
try {
|
||||||
|
_nsdManager?.stopServiceDiscovery(this)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to stop service discovery", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceFound(service: NsdServiceInfo) {
|
||||||
|
Log.v(TAG, "Service discovery success for ${service.serviceType}: $service")
|
||||||
|
val addresses = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
service.hostAddresses.toTypedArray()
|
||||||
|
} else {
|
||||||
|
arrayOf(service.host)
|
||||||
|
}
|
||||||
|
addOrUpdate(service.serviceName, addresses, service.port)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
_nsdManager?.registerServiceInfoCallback(service, { it.run() }, object : NsdManager.ServiceInfoCallback {
|
||||||
|
override fun onServiceUpdated(serviceInfo: NsdServiceInfo) {
|
||||||
|
Log.v(TAG, "onServiceUpdated: $serviceInfo")
|
||||||
|
addOrUpdate(serviceInfo.serviceName, serviceInfo.hostAddresses.toTypedArray(), serviceInfo.port)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceLost() {
|
||||||
|
Log.v(TAG, "onServiceLost: $service")
|
||||||
|
// TODO: Handle service lost
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) {
|
||||||
|
Log.v(TAG, "onServiceInfoCallbackRegistrationFailed: $errorCode")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceInfoCallbackUnregistered() {
|
||||||
|
Log.v(TAG, "onServiceInfoCallbackUnregistered")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
_nsdManager?.resolveService(service, object : NsdManager.ResolveListener {
|
||||||
|
override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
|
||||||
|
Log.v(TAG, "Resolve failed: $errorCode")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
|
||||||
|
Log.v(TAG, "Resolve Succeeded: $serviceInfo")
|
||||||
|
addOrUpdate(serviceInfo.serviceName, arrayOf(serviceInfo.host), serviceInfo.port)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _castingDialogLock = Any();
|
||||||
|
private var _currentDialog: AlertDialog? = null;
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun connectDevice(device: CastingDevice) {
|
fun connectDevice(device: CastingDevice) {
|
||||||
if (activeDevice == device)
|
if (activeDevice == device)
|
||||||
@@ -272,10 +329,41 @@ class StateCasting {
|
|||||||
invokeInMainScopeIfRequired {
|
invokeInMainScopeIfRequired {
|
||||||
StateApp.withContext(false) { context ->
|
StateApp.withContext(false) { context ->
|
||||||
context.let {
|
context.let {
|
||||||
|
Logger.i(TAG, "Casting state changed to ${castConnectionState}");
|
||||||
when (castConnectionState) {
|
when (castConnectionState) {
|
||||||
CastConnectionState.CONNECTED -> UIDialogs.toast(it, "Connected to device")
|
CastConnectionState.CONNECTED -> {
|
||||||
CastConnectionState.CONNECTING -> UIDialogs.toast(it, "Connecting to device...")
|
Logger.i(TAG, "Casting connected to [${device.name}]");
|
||||||
CastConnectionState.DISCONNECTED -> UIDialogs.toast(it, "Disconnected from device")
|
UIDialogs.appToast("Connected to device")
|
||||||
|
synchronized(_castingDialogLock) {
|
||||||
|
if(_currentDialog != null) {
|
||||||
|
_currentDialog?.hide();
|
||||||
|
_currentDialog = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CastConnectionState.CONNECTING -> {
|
||||||
|
Logger.i(TAG, "Casting connecting to [${device.name}]");
|
||||||
|
UIDialogs.toast(it, "Connecting to device...")
|
||||||
|
synchronized(_castingDialogLock) {
|
||||||
|
if(_currentDialog == null) {
|
||||||
|
_currentDialog = UIDialogs.showDialog(context, R.drawable.ic_loader_animated, true,
|
||||||
|
"Connecting to [${device.name}]",
|
||||||
|
"Make sure you are on the same network\n\nVPNs and guest networks can cause issues", null, -2,
|
||||||
|
UIDialogs.Action("Disconnect", {
|
||||||
|
device.stop();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CastConnectionState.DISCONNECTED -> {
|
||||||
|
UIDialogs.toast(it, "Disconnected from device")
|
||||||
|
synchronized(_castingDialogLock) {
|
||||||
|
if(_currentDialog != null) {
|
||||||
|
_currentDialog?.hide();
|
||||||
|
_currentDialog = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -295,9 +383,6 @@ class StateCasting {
|
|||||||
invokeInMainScopeIfRequired { onActiveDeviceTimeChanged.emit(it) };
|
invokeInMainScopeIfRequired { onActiveDeviceTimeChanged.emit(it) };
|
||||||
};
|
};
|
||||||
|
|
||||||
addRememberedDevice(device);
|
|
||||||
Logger.i(TAG, "Device added to active discovery. Active discovery now contains ${_storage.getDevicesCount()} devices.")
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
device.start();
|
device.start();
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@@ -319,21 +404,22 @@ class StateCasting {
|
|||||||
return addRememberedDevice(device);
|
return addRememberedDevice(device);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getRememberedCastingDevices(): List<CastingDevice> {
|
||||||
|
return _storage.getDevices().map { deviceFromCastingDeviceInfo(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getRememberedCastingDeviceNames(): List<String> {
|
||||||
|
return _storage.getDeviceNames()
|
||||||
|
}
|
||||||
|
|
||||||
fun addRememberedDevice(device: CastingDevice): CastingDeviceInfo {
|
fun addRememberedDevice(device: CastingDevice): CastingDeviceInfo {
|
||||||
val deviceInfo = device.getDeviceInfo()
|
val deviceInfo = device.getDeviceInfo()
|
||||||
val foundInfo = _storage.addDevice(deviceInfo)
|
return _storage.addDevice(deviceInfo)
|
||||||
if (foundInfo == deviceInfo) {
|
|
||||||
rememberedDevices.add(device);
|
|
||||||
return foundInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
return foundInfo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeRememberedDevice(device: CastingDevice) {
|
fun removeRememberedDevice(device: CastingDevice) {
|
||||||
val name = device.name ?: return;
|
val name = device.name ?: return
|
||||||
_storage.removeDevice(name);
|
_storage.removeDevice(name)
|
||||||
rememberedDevices.remove(device);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun invokeInMainScopeIfRequired(action: () -> Unit){
|
private fun invokeInMainScopeIfRequired(action: () -> Unit){
|
||||||
@@ -402,7 +488,7 @@ class StateCasting {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val proxyStreams = Settings.instance.casting.alwaysProxyRequests;
|
val proxyStreams = Settings.instance.casting.alwaysProxyRequests;
|
||||||
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
val url = getLocalUrl(ad);
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
if (videoSource is IVideoUrlSource) {
|
if (videoSource is IVideoUrlSource) {
|
||||||
@@ -497,7 +583,7 @@ class StateCasting {
|
|||||||
private fun castLocalVideo(video: IPlatformVideoDetails, videoSource: LocalVideoSource, resumePosition: Double, speed: Double?) : List<String> {
|
private fun castLocalVideo(video: IPlatformVideoDetails, videoSource: LocalVideoSource, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
val url = getLocalUrl(ad);
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
val videoPath = "/video-${id}"
|
val videoPath = "/video-${id}"
|
||||||
val videoUrl = url + videoPath;
|
val videoUrl = url + videoPath;
|
||||||
@@ -516,7 +602,7 @@ class StateCasting {
|
|||||||
private fun castLocalAudio(video: IPlatformVideoDetails, audioSource: LocalAudioSource, resumePosition: Double, speed: Double?) : List<String> {
|
private fun castLocalAudio(video: IPlatformVideoDetails, audioSource: LocalAudioSource, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
val url = getLocalUrl(ad);
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
val audioPath = "/audio-${id}"
|
val audioPath = "/audio-${id}"
|
||||||
val audioUrl = url + audioPath;
|
val audioUrl = url + audioPath;
|
||||||
@@ -535,7 +621,7 @@ class StateCasting {
|
|||||||
private fun castLocalHls(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?): List<String> {
|
private fun castLocalHls(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?): List<String> {
|
||||||
val ad = activeDevice ?: return listOf()
|
val ad = activeDevice ?: return listOf()
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}"
|
val url = getLocalUrl(ad)
|
||||||
val id = UUID.randomUUID()
|
val id = UUID.randomUUID()
|
||||||
|
|
||||||
val hlsPath = "/hls-${id}"
|
val hlsPath = "/hls-${id}"
|
||||||
@@ -631,7 +717,7 @@ class StateCasting {
|
|||||||
private fun castLocalDash(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
private fun castLocalDash(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
val url = getLocalUrl(ad);
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
val dashPath = "/dash-${id}"
|
val dashPath = "/dash-${id}"
|
||||||
@@ -681,7 +767,7 @@ class StateCasting {
|
|||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
|
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
val url = getLocalUrl(ad);
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
val videoPath = "/video-${id}"
|
val videoPath = "/video-${id}"
|
||||||
@@ -746,7 +832,7 @@ class StateCasting {
|
|||||||
_castServer.removeAllHandlers("castProxiedHlsMaster")
|
_castServer.removeAllHandlers("castProxiedHlsMaster")
|
||||||
|
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
val url = getLocalUrl(ad);
|
||||||
|
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
val hlsPath = "/hls-${id}"
|
val hlsPath = "/hls-${id}"
|
||||||
@@ -916,7 +1002,7 @@ class StateCasting {
|
|||||||
|
|
||||||
private suspend fun castHlsIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
private suspend fun castHlsIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
val url = getLocalUrl(ad);
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
val hlsPath = "/hls-${id}"
|
val hlsPath = "/hls-${id}"
|
||||||
@@ -1046,7 +1132,7 @@ class StateCasting {
|
|||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
|
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
val url = getLocalUrl(ad);
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
val dashPath = "/dash-${id}"
|
val dashPath = "/dash-${id}"
|
||||||
@@ -1132,6 +1218,15 @@ class StateCasting {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getLocalUrl(ad: CastingDevice): String {
|
||||||
|
var address = ad.localAddress!!
|
||||||
|
if (address.isLinkLocalAddress) {
|
||||||
|
address = findPreferredAddress() ?: address
|
||||||
|
Logger.i(TAG, "Selected casting address: $address")
|
||||||
|
}
|
||||||
|
return "http://${address.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
private suspend fun castDashRaw(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: JSDashManifestRawSource?, audioSource: JSDashManifestRawAudioSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
private suspend fun castDashRaw(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: JSDashManifestRawSource?, audioSource: JSDashManifestRawAudioSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
@@ -1139,7 +1234,7 @@ class StateCasting {
|
|||||||
cleanExecutors()
|
cleanExecutors()
|
||||||
_castServer.removeAllHandlers("castDashRaw")
|
_castServer.removeAllHandlers("castDashRaw")
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
val url = getLocalUrl(ad);
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
val dashPath = "/dash-${id}"
|
val dashPath = "/dash-${id}"
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import android.app.PendingIntent.getBroadcast
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageInstaller
|
import android.content.pm.PackageInstaller
|
||||||
|
import android.content.pm.PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -155,6 +157,9 @@ class AutoUpdateDialog(context: Context?) : AlertDialog(context) {
|
|||||||
|
|
||||||
val packageInstaller: PackageInstaller = context.packageManager.packageInstaller;
|
val packageInstaller: PackageInstaller = context.packageManager.packageInstaller;
|
||||||
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL);
|
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL);
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
params.setRequireUserAction(USER_ACTION_NOT_REQUIRED)
|
||||||
|
}
|
||||||
val sessionId = packageInstaller.createSession(params);
|
val sessionId = packageInstaller.createSession(params);
|
||||||
session = packageInstaller.openSession(sessionId)
|
session = packageInstaller.openSession(sessionId)
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +1,24 @@
|
|||||||
package com.futo.platformplayer.dialogs
|
package com.futo.platformplayer.dialogs
|
||||||
|
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.app.PendingIntent.*
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageInstaller
|
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.method.ScrollingMovementMethod
|
import android.text.method.ScrollingMovementMethod
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.WindowManager
|
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.receivers.InstallReceiver
|
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
|
||||||
import com.futo.platformplayer.states.StateUpdate
|
import com.futo.platformplayer.states.StateUpdate
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import java.io.File
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
class ChangelogDialog(context: Context?) : AlertDialog(context) {
|
class ChangelogDialog(context: Context?, val changelogs: Map<Int, String>? = null) : AlertDialog(context) {
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = "ChangelogDialog";
|
private val TAG = "ChangelogDialog";
|
||||||
}
|
}
|
||||||
@@ -48,7 +35,11 @@ class ChangelogDialog(context: Context?) : AlertDialog(context) {
|
|||||||
private var _maxVersion: Int = 0;
|
private var _maxVersion: Int = 0;
|
||||||
private var _managedHttpClient = ManagedHttpClient();
|
private var _managedHttpClient = ManagedHttpClient();
|
||||||
|
|
||||||
private val _taskDownloadChangelog = TaskHandler<Int, String?>(StateApp.instance.scopeGetter, { version -> StateUpdate.instance.downloadChangelog(_managedHttpClient, version) })
|
private val _taskDownloadChangelog = TaskHandler<Int, String?>(StateApp.instance.scopeGetter, { version -> if(changelogs == null)
|
||||||
|
StateUpdate.instance.downloadChangelog(_managedHttpClient, version)
|
||||||
|
else
|
||||||
|
changelogs[version]
|
||||||
|
})
|
||||||
.success { setChangelog(it); }
|
.success { setChangelog(it); }
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(TAG, "Failed to load changelog.", it);
|
Logger.w(TAG, "Failed to load changelog.", it);
|
||||||
@@ -97,7 +88,7 @@ class ChangelogDialog(context: Context?) : AlertDialog(context) {
|
|||||||
setVersion(version);
|
setVersion(version);
|
||||||
|
|
||||||
val currentVersion = BuildConfig.VERSION_CODE;
|
val currentVersion = BuildConfig.VERSION_CODE;
|
||||||
_buttonUpdate.visibility = if (currentVersion == _maxVersion) View.GONE else View.VISIBLE;
|
_buttonUpdate.visibility = if (currentVersion == _maxVersion || changelogs != null) View.GONE else View.VISIBLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setVersion(version: Int) {
|
private fun setVersion(version: Int) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.graphics.Color
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
|
import android.view.KeyEvent
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
@@ -21,7 +22,6 @@ import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComm
|
|||||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
|
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
@@ -29,6 +29,7 @@ import com.futo.platformplayer.states.StatePolycentric
|
|||||||
import com.futo.polycentric.core.ClaimType
|
import com.futo.polycentric.core.ClaimType
|
||||||
import com.futo.polycentric.core.Store
|
import com.futo.polycentric.core.Store
|
||||||
import com.futo.polycentric.core.SystemState
|
import com.futo.polycentric.core.SystemState
|
||||||
|
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
||||||
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
|
||||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
import com.google.android.material.button.MaterialButton
|
import com.google.android.material.button.MaterialButton
|
||||||
@@ -57,11 +58,21 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
|||||||
_editComment = findViewById(R.id.edit_comment);
|
_editComment = findViewById(R.id.edit_comment);
|
||||||
_textCharacterCount = findViewById(R.id.character_count);
|
_textCharacterCount = findViewById(R.id.character_count);
|
||||||
_textCharacterCountMax = findViewById(R.id.character_count_max);
|
_textCharacterCountMax = findViewById(R.id.character_count_max);
|
||||||
|
setCanceledOnTouchOutside(false)
|
||||||
|
setOnKeyListener { _, keyCode, event ->
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) {
|
||||||
|
handleCloseAttempt()
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_editComment.addTextChangedListener(object : TextWatcher {
|
_editComment.addTextChangedListener(object : TextWatcher {
|
||||||
override fun afterTextChanged(s: Editable?) = Unit
|
override fun afterTextChanged(s: Editable?) = Unit
|
||||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
|
||||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, c: Int) {
|
||||||
|
val count = s?.length ?: 0;
|
||||||
_textCharacterCount.text = count.toString();
|
_textCharacterCount.text = count.toString();
|
||||||
|
|
||||||
if (count > PolycentricPlatformComment.MAX_COMMENT_SIZE) {
|
if (count > PolycentricPlatformComment.MAX_COMMENT_SIZE) {
|
||||||
@@ -79,10 +90,13 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
|||||||
_inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager;
|
_inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager;
|
||||||
|
|
||||||
_buttonCancel.setOnClickListener {
|
_buttonCancel.setOnClickListener {
|
||||||
clearFocus();
|
handleCloseAttempt()
|
||||||
dismiss();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
setOnCancelListener {
|
||||||
|
handleCloseAttempt()
|
||||||
|
}
|
||||||
|
|
||||||
_buttonCreate.setOnClickListener {
|
_buttonCreate.setOnClickListener {
|
||||||
clearFocus();
|
clearFocus();
|
||||||
|
|
||||||
@@ -134,6 +148,22 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
|||||||
focus();
|
focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleCloseAttempt() {
|
||||||
|
if (_editComment.text.isEmpty()) {
|
||||||
|
clearFocus()
|
||||||
|
dismiss()
|
||||||
|
} else {
|
||||||
|
UIDialogs.showConfirmationDialog(
|
||||||
|
context,
|
||||||
|
context.resources.getString(R.string.not_empty_close),
|
||||||
|
action = {
|
||||||
|
clearFocus()
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun focus() {
|
private fun focus() {
|
||||||
_editComment.requestFocus();
|
_editComment.requestFocus();
|
||||||
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
|
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import android.view.View
|
|||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
@@ -21,22 +23,21 @@ import com.futo.platformplayer.casting.StateCasting
|
|||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.views.adapters.DeviceAdapter
|
import com.futo.platformplayer.views.adapters.DeviceAdapter
|
||||||
|
import com.futo.platformplayer.views.adapters.DeviceAdapterEntry
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
||||||
private lateinit var _imageLoader: ImageView;
|
private lateinit var _imageLoader: ImageView;
|
||||||
private lateinit var _buttonClose: Button;
|
private lateinit var _buttonClose: Button;
|
||||||
private lateinit var _buttonAdd: ImageButton;
|
private lateinit var _buttonAdd: LinearLayout;
|
||||||
private lateinit var _buttonScanQR: ImageButton;
|
private lateinit var _buttonScanQR: LinearLayout;
|
||||||
private lateinit var _textNoDevicesFound: TextView;
|
private lateinit var _textNoDevicesFound: TextView;
|
||||||
private lateinit var _textNoDevicesRemembered: TextView;
|
|
||||||
private lateinit var _recyclerDevices: RecyclerView;
|
private lateinit var _recyclerDevices: RecyclerView;
|
||||||
private lateinit var _recyclerRememberedDevices: RecyclerView;
|
|
||||||
private lateinit var _adapter: DeviceAdapter;
|
private lateinit var _adapter: DeviceAdapter;
|
||||||
private lateinit var _rememberedAdapter: DeviceAdapter;
|
private val _devices: MutableSet<String> = mutableSetOf()
|
||||||
private val _devices: ArrayList<CastingDevice> = arrayListOf();
|
private val _rememberedDevices: MutableSet<String> = mutableSetOf()
|
||||||
private val _rememberedDevices: ArrayList<CastingDevice> = arrayListOf();
|
private val _unifiedDevices: MutableList<DeviceAdapterEntry> = mutableListOf()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
@@ -45,42 +46,40 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
_imageLoader = findViewById(R.id.image_loader);
|
_imageLoader = findViewById(R.id.image_loader);
|
||||||
_buttonClose = findViewById(R.id.button_close);
|
_buttonClose = findViewById(R.id.button_close);
|
||||||
_buttonAdd = findViewById(R.id.button_add);
|
_buttonAdd = findViewById(R.id.button_add);
|
||||||
_buttonScanQR = findViewById(R.id.button_scan_qr);
|
_buttonScanQR = findViewById(R.id.button_qr);
|
||||||
_recyclerDevices = findViewById(R.id.recycler_devices);
|
_recyclerDevices = findViewById(R.id.recycler_devices);
|
||||||
_recyclerRememberedDevices = findViewById(R.id.recycler_remembered_devices);
|
|
||||||
_textNoDevicesFound = findViewById(R.id.text_no_devices_found);
|
_textNoDevicesFound = findViewById(R.id.text_no_devices_found);
|
||||||
_textNoDevicesRemembered = findViewById(R.id.text_no_devices_remembered);
|
|
||||||
|
|
||||||
_adapter = DeviceAdapter(_devices, false);
|
_adapter = DeviceAdapter(_unifiedDevices)
|
||||||
_recyclerDevices.adapter = _adapter;
|
_recyclerDevices.adapter = _adapter;
|
||||||
_recyclerDevices.layoutManager = LinearLayoutManager(context);
|
_recyclerDevices.layoutManager = LinearLayoutManager(context);
|
||||||
|
|
||||||
_rememberedAdapter = DeviceAdapter(_rememberedDevices, true);
|
_adapter.onPin.subscribe { d ->
|
||||||
_rememberedAdapter.onRemove.subscribe { d ->
|
val isRemembered = _rememberedDevices.contains(d.name)
|
||||||
if (StateCasting.instance.activeDevice == d) {
|
val newIsRemembered = !isRemembered
|
||||||
d.stopCasting();
|
if (newIsRemembered) {
|
||||||
|
StateCasting.instance.addRememberedDevice(d)
|
||||||
|
val name = d.name
|
||||||
|
if (name != null) {
|
||||||
|
_rememberedDevices.add(name)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
StateCasting.instance.removeRememberedDevice(d)
|
||||||
|
_rememberedDevices.remove(d.name)
|
||||||
}
|
}
|
||||||
|
updateUnifiedList()
|
||||||
StateCasting.instance.removeRememberedDevice(d);
|
|
||||||
val index = _rememberedDevices.indexOf(d);
|
|
||||||
if (index != -1) {
|
|
||||||
_rememberedDevices.removeAt(index);
|
|
||||||
_rememberedAdapter.notifyItemRemoved(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
_textNoDevicesRemembered.visibility = if (_rememberedDevices.isEmpty()) View.VISIBLE else View.GONE;
|
|
||||||
_recyclerRememberedDevices.visibility = if (_rememberedDevices.isNotEmpty()) View.VISIBLE else View.GONE;
|
|
||||||
};
|
|
||||||
_rememberedAdapter.onConnect.subscribe { _ ->
|
|
||||||
dismiss()
|
|
||||||
UIDialogs.showCastingDialog(context)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO: Integrate remembered into the main list
|
||||||
|
//TODO: Add green indicator to indicate a device is oneline
|
||||||
|
//TODO: Add pinning
|
||||||
|
//TODO: Implement QR code as an option in add manually
|
||||||
|
//TODO: Remove start button
|
||||||
|
|
||||||
_adapter.onConnect.subscribe { _ ->
|
_adapter.onConnect.subscribe { _ ->
|
||||||
dismiss()
|
dismiss()
|
||||||
UIDialogs.showCastingDialog(context)
|
//UIDialogs.showCastingDialog(context)
|
||||||
}
|
}
|
||||||
_recyclerRememberedDevices.adapter = _rememberedAdapter;
|
|
||||||
_recyclerRememberedDevices.layoutManager = LinearLayoutManager(context);
|
|
||||||
|
|
||||||
_buttonClose.setOnClickListener { dismiss(); };
|
_buttonClose.setOnClickListener { dismiss(); };
|
||||||
_buttonAdd.setOnClickListener {
|
_buttonAdd.setOnClickListener {
|
||||||
@@ -105,77 +104,112 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
Logger.i(TAG, "Dialog shown.");
|
Logger.i(TAG, "Dialog shown.");
|
||||||
|
|
||||||
StateCasting.instance.startDiscovering()
|
StateCasting.instance.startDiscovering()
|
||||||
|
|
||||||
(_imageLoader.drawable as Animatable?)?.start();
|
(_imageLoader.drawable as Animatable?)?.start();
|
||||||
|
|
||||||
_devices.clear();
|
synchronized(StateCasting.instance.devices) {
|
||||||
synchronized (StateCasting.instance.devices) {
|
_devices.addAll(StateCasting.instance.devices.values.mapNotNull { it.name })
|
||||||
_devices.addAll(StateCasting.instance.devices.values);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_rememberedDevices.clear();
|
_rememberedDevices.addAll(StateCasting.instance.getRememberedCastingDeviceNames())
|
||||||
synchronized (StateCasting.instance.rememberedDevices) {
|
updateUnifiedList()
|
||||||
_rememberedDevices.addAll(StateCasting.instance.rememberedDevices);
|
|
||||||
|
StateCasting.instance.onDeviceAdded.subscribe(this) { d ->
|
||||||
|
val name = d.name
|
||||||
|
if (name != null)
|
||||||
|
_devices.add(name)
|
||||||
|
updateUnifiedList()
|
||||||
|
}
|
||||||
|
|
||||||
|
StateCasting.instance.onDeviceChanged.subscribe(this) { d ->
|
||||||
|
val index = _unifiedDevices.indexOfFirst { it.castingDevice.name == d.name }
|
||||||
|
if (index != -1) {
|
||||||
|
_unifiedDevices[index] = DeviceAdapterEntry(d, _unifiedDevices[index].isPinnedDevice, _unifiedDevices[index].isOnlineDevice)
|
||||||
|
_adapter.notifyItemChanged(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StateCasting.instance.onDeviceRemoved.subscribe(this) { d ->
|
||||||
|
_devices.remove(d.name)
|
||||||
|
updateUnifiedList()
|
||||||
|
}
|
||||||
|
|
||||||
|
StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, connectionState ->
|
||||||
|
if (connectionState == CastConnectionState.CONNECTED) {
|
||||||
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_textNoDevicesFound.visibility = if (_devices.isEmpty()) View.VISIBLE else View.GONE;
|
_textNoDevicesFound.visibility = if (_devices.isEmpty()) View.VISIBLE else View.GONE;
|
||||||
_recyclerDevices.visibility = if (_devices.isNotEmpty()) View.VISIBLE else View.GONE;
|
_recyclerDevices.visibility = if (_devices.isNotEmpty()) View.VISIBLE else View.GONE;
|
||||||
_textNoDevicesRemembered.visibility = if (_rememberedDevices.isEmpty()) View.VISIBLE else View.GONE;
|
|
||||||
_recyclerRememberedDevices.visibility = if (_rememberedDevices.isNotEmpty()) View.VISIBLE else View.GONE;
|
|
||||||
|
|
||||||
StateCasting.instance.onDeviceAdded.subscribe(this) { d ->
|
|
||||||
_devices.add(d);
|
|
||||||
_adapter.notifyItemInserted(_devices.size - 1);
|
|
||||||
_textNoDevicesFound.visibility = View.GONE;
|
|
||||||
_recyclerDevices.visibility = View.VISIBLE;
|
|
||||||
};
|
|
||||||
|
|
||||||
StateCasting.instance.onDeviceChanged.subscribe(this) { d ->
|
|
||||||
val index = _devices.indexOf(d);
|
|
||||||
if (index == -1) {
|
|
||||||
return@subscribe;
|
|
||||||
}
|
|
||||||
|
|
||||||
_devices[index] = d;
|
|
||||||
_adapter.notifyItemChanged(index);
|
|
||||||
};
|
|
||||||
|
|
||||||
StateCasting.instance.onDeviceRemoved.subscribe(this) { d ->
|
|
||||||
val index = _devices.indexOf(d);
|
|
||||||
if (index == -1) {
|
|
||||||
return@subscribe;
|
|
||||||
}
|
|
||||||
|
|
||||||
_devices.removeAt(index);
|
|
||||||
_adapter.notifyItemRemoved(index);
|
|
||||||
_textNoDevicesFound.visibility = if (_devices.isEmpty()) View.VISIBLE else View.GONE;
|
|
||||||
_recyclerDevices.visibility = if (_devices.isNotEmpty()) View.VISIBLE else View.GONE;
|
|
||||||
};
|
|
||||||
|
|
||||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, connectionState ->
|
|
||||||
if (connectionState != CastConnectionState.CONNECTED) {
|
|
||||||
return@subscribe;
|
|
||||||
}
|
|
||||||
|
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
|
||||||
dismiss();
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
_adapter.notifyDataSetChanged();
|
|
||||||
_rememberedAdapter.notifyDataSetChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun dismiss() {
|
override fun dismiss() {
|
||||||
super.dismiss();
|
super.dismiss()
|
||||||
|
(_imageLoader.drawable as Animatable?)?.stop()
|
||||||
(_imageLoader.drawable as Animatable?)?.stop();
|
|
||||||
|
|
||||||
StateCasting.instance.stopDiscovering()
|
StateCasting.instance.stopDiscovering()
|
||||||
StateCasting.instance.onDeviceAdded.remove(this);
|
StateCasting.instance.onDeviceAdded.remove(this)
|
||||||
StateCasting.instance.onDeviceChanged.remove(this);
|
StateCasting.instance.onDeviceChanged.remove(this)
|
||||||
StateCasting.instance.onDeviceRemoved.remove(this);
|
StateCasting.instance.onDeviceRemoved.remove(this)
|
||||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this);
|
StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateUnifiedList() {
|
||||||
|
val oldList = ArrayList(_unifiedDevices)
|
||||||
|
val newList = buildUnifiedList()
|
||||||
|
|
||||||
|
val diffResult = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
|
||||||
|
override fun getOldListSize() = oldList.size
|
||||||
|
override fun getNewListSize() = newList.size
|
||||||
|
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||||
|
val oldItem = oldList[oldItemPosition]
|
||||||
|
val newItem = newList[newItemPosition]
|
||||||
|
return oldItem.castingDevice.name == newItem.castingDevice.name
|
||||||
|
&& oldItem.castingDevice.isReady == newItem.castingDevice.isReady
|
||||||
|
&& oldItem.isOnlineDevice == newItem.isOnlineDevice
|
||||||
|
&& oldItem.isPinnedDevice == newItem.isPinnedDevice
|
||||||
|
}
|
||||||
|
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||||
|
val oldItem = oldList[oldItemPosition]
|
||||||
|
val newItem = newList[newItemPosition]
|
||||||
|
return oldItem.castingDevice.name == newItem.castingDevice.name
|
||||||
|
&& oldItem.castingDevice.isReady == newItem.castingDevice.isReady
|
||||||
|
&& oldItem.isOnlineDevice == newItem.isOnlineDevice
|
||||||
|
&& oldItem.isPinnedDevice == newItem.isPinnedDevice
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
_unifiedDevices.clear()
|
||||||
|
_unifiedDevices.addAll(newList)
|
||||||
|
diffResult.dispatchUpdatesTo(_adapter)
|
||||||
|
|
||||||
|
_textNoDevicesFound.visibility = if (_unifiedDevices.isEmpty()) View.VISIBLE else View.GONE
|
||||||
|
_recyclerDevices.visibility = if (_unifiedDevices.isNotEmpty()) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildUnifiedList(): List<DeviceAdapterEntry> {
|
||||||
|
val onlineDevices = StateCasting.instance.devices.values.associateBy { it.name }
|
||||||
|
val rememberedDevices = StateCasting.instance.getRememberedCastingDevices().associateBy { it.name }
|
||||||
|
|
||||||
|
val unifiedList = mutableListOf<DeviceAdapterEntry>()
|
||||||
|
|
||||||
|
val intersectionNames = _devices.intersect(_rememberedDevices)
|
||||||
|
for (name in intersectionNames) {
|
||||||
|
onlineDevices[name]?.let { unifiedList.add(DeviceAdapterEntry(it, true, true)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val onlineOnlyNames = _devices - _rememberedDevices
|
||||||
|
for (name in onlineOnlyNames) {
|
||||||
|
onlineDevices[name]?.let { unifiedList.add(DeviceAdapterEntry(it, false, true)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val rememberedOnlyNames = _rememberedDevices - _devices
|
||||||
|
for (name in rememberedOnlyNames) {
|
||||||
|
rememberedDevices[name]?.let { unifiedList.add(DeviceAdapterEntry(it, true, false)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return unifiedList
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ class PluginUpdateDialog : AlertDialog {
|
|||||||
private lateinit var _buttonInstall: LinearLayout;
|
private lateinit var _buttonInstall: LinearLayout;
|
||||||
|
|
||||||
private lateinit var _textPlugin: TextView;
|
private lateinit var _textPlugin: TextView;
|
||||||
|
private lateinit var _textChangelog: TextView;
|
||||||
private lateinit var _textProgres: TextView;
|
private lateinit var _textProgres: TextView;
|
||||||
private lateinit var _textError: TextView;
|
private lateinit var _textError: TextView;
|
||||||
private lateinit var _textResult: TextView;
|
private lateinit var _textResult: TextView;
|
||||||
@@ -94,6 +95,7 @@ class PluginUpdateDialog : AlertDialog {
|
|||||||
_buttonInstall = findViewById(R.id.button_install);
|
_buttonInstall = findViewById(R.id.button_install);
|
||||||
|
|
||||||
_textPlugin = findViewById(R.id.text_plugin);
|
_textPlugin = findViewById(R.id.text_plugin);
|
||||||
|
_textChangelog = findViewById(R.id.text_changelog);
|
||||||
_textProgres = findViewById(R.id.text_progress);
|
_textProgres = findViewById(R.id.text_progress);
|
||||||
_textError = findViewById(R.id.text_error);
|
_textError = findViewById(R.id.text_error);
|
||||||
_textResult = findViewById(R.id.text_result);
|
_textResult = findViewById(R.id.text_result);
|
||||||
@@ -110,6 +112,27 @@ class PluginUpdateDialog : AlertDialog {
|
|||||||
_updateSpinner = findViewById(R.id.update_spinner);
|
_updateSpinner = findViewById(R.id.update_spinner);
|
||||||
_iconPlugin = findViewById(R.id.icon_plugin);
|
_iconPlugin = findViewById(R.id.icon_plugin);
|
||||||
|
|
||||||
|
try {
|
||||||
|
var changelogVersion = _newConfig.version.toString();
|
||||||
|
if (_newConfig.changelog != null && _newConfig.changelog?.containsKey(changelogVersion) == true) {
|
||||||
|
_textChangelog.movementMethod = ScrollingMovementMethod();
|
||||||
|
val changelog = _newConfig.changelog!![changelogVersion]!!;
|
||||||
|
if(changelog.size > 1) {
|
||||||
|
_textChangelog.text = "Changelog (${_newConfig.version})\n" + changelog.map { " - " + it.trim() }.joinToString("\n");
|
||||||
|
}
|
||||||
|
else if(changelog.size == 1) {
|
||||||
|
_textChangelog.text = "Changelog (${_newConfig.version})\n" + changelog[0].trim();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
_textChangelog.visibility = View.GONE;
|
||||||
|
} else
|
||||||
|
_textChangelog.visibility = View.GONE;
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
_textChangelog.visibility = View.GONE;
|
||||||
|
Logger.e(TAG, "Invalid changelog? ", ex);
|
||||||
|
}
|
||||||
|
|
||||||
_buttonCancel1.setOnClickListener {
|
_buttonCancel1.setOnClickListener {
|
||||||
dismiss();
|
dismiss();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import com.futo.platformplayer.states.StatePlatform
|
|||||||
import com.futo.platformplayer.states.StatePlugins
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
import com.futo.platformplayer.toHumanBitrate
|
import com.futo.platformplayer.toHumanBitrate
|
||||||
import com.futo.platformplayer.toHumanBytesSpeed
|
import com.futo.platformplayer.toHumanBytesSpeed
|
||||||
|
import com.futo.polycentric.core.hexStringToByteArray
|
||||||
import hasAnySource
|
import hasAnySource
|
||||||
import isDownloadable
|
import isDownloadable
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
@@ -59,16 +60,21 @@ import kotlinx.coroutines.suspendCancellableCoroutine
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.Contextual
|
import kotlinx.serialization.Contextual
|
||||||
import kotlinx.serialization.Transient
|
import kotlinx.serialization.Transient
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.lang.Thread.sleep
|
import java.lang.Thread.sleep
|
||||||
|
import java.nio.ByteBuffer
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import java.util.concurrent.ForkJoinPool
|
import java.util.concurrent.ForkJoinPool
|
||||||
import java.util.concurrent.ForkJoinTask
|
import java.util.concurrent.ForkJoinTask
|
||||||
import java.util.concurrent.ThreadLocalRandom
|
import java.util.concurrent.ThreadLocalRandom
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.spec.IvParameterSpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
import kotlin.coroutines.resumeWithException
|
import kotlin.coroutines.resumeWithException
|
||||||
import kotlin.time.times
|
import kotlin.time.times
|
||||||
|
|
||||||
@@ -100,6 +106,7 @@ class VideoDownload {
|
|||||||
|
|
||||||
var requireVideoSource: Boolean = false;
|
var requireVideoSource: Boolean = false;
|
||||||
var requireAudioSource: Boolean = false;
|
var requireAudioSource: Boolean = false;
|
||||||
|
var requiredCheck: Boolean = false;
|
||||||
|
|
||||||
@Contextual
|
@Contextual
|
||||||
@Transient
|
@Transient
|
||||||
@@ -140,11 +147,17 @@ class VideoDownload {
|
|||||||
var error: String? = null;
|
var error: String? = null;
|
||||||
|
|
||||||
var videoFilePath: String? = null;
|
var videoFilePath: String? = null;
|
||||||
var videoFileName: String? = null;
|
var videoFileNameBase: String? = null;
|
||||||
|
var videoFileNameExt: String? = null;
|
||||||
|
val videoFileName: String? get() = if(videoFileNameBase.isNullOrEmpty()) null else videoFileNameBase + (if(!videoFileNameExt.isNullOrEmpty()) "." + videoFileNameExt else "");
|
||||||
|
var videoOverrideContainer: String? = null;
|
||||||
var videoFileSize: Long? = null;
|
var videoFileSize: Long? = null;
|
||||||
|
|
||||||
var audioFilePath: String? = null;
|
var audioFilePath: String? = null;
|
||||||
var audioFileName: String? = null;
|
var audioFileNameBase: String? = null;
|
||||||
|
var audioFileNameExt: String? = null;
|
||||||
|
val audioFileName: String? get() = if(audioFileNameBase.isNullOrEmpty()) null else audioFileNameBase + (if(!audioFileNameExt.isNullOrEmpty()) "." + audioFileNameExt else "");
|
||||||
|
var audioOverrideContainer: String? = null;
|
||||||
var audioFileSize: Long? = null;
|
var audioFileSize: Long? = null;
|
||||||
|
|
||||||
var subtitleFilePath: String? = null;
|
var subtitleFilePath: String? = null;
|
||||||
@@ -164,7 +177,7 @@ class VideoDownload {
|
|||||||
onStateChanged.emit(newState);
|
onStateChanged.emit(newState);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(video: IPlatformVideo, targetPixelCount: Long? = null, targetBitrate: Long? = null) {
|
constructor(video: IPlatformVideo, targetPixelCount: Long? = null, targetBitrate: Long? = null, optionalSources: Boolean = false) {
|
||||||
this.video = SerializedPlatformVideo.fromVideo(video);
|
this.video = SerializedPlatformVideo.fromVideo(video);
|
||||||
this.videoSource = null;
|
this.videoSource = null;
|
||||||
this.audioSource = null;
|
this.audioSource = null;
|
||||||
@@ -175,8 +188,9 @@ class VideoDownload {
|
|||||||
this.requiresLiveVideoSource = false;
|
this.requiresLiveVideoSource = false;
|
||||||
this.requiresLiveAudioSource = false;
|
this.requiresLiveAudioSource = false;
|
||||||
this.targetVideoName = videoSource?.name;
|
this.targetVideoName = videoSource?.name;
|
||||||
this.requireVideoSource = targetPixelCount != null
|
this.requireVideoSource = targetPixelCount != null;
|
||||||
this.requireAudioSource = targetBitrate != null; //TODO: May not be a valid check.. can only be determined after live fetch?
|
this.requireAudioSource = targetBitrate != null; //TODO: May not be a valid check.. can only be determined after live fetch?
|
||||||
|
this.requiredCheck = optionalSources;
|
||||||
}
|
}
|
||||||
constructor(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: SubtitleRawSource?) {
|
constructor(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: SubtitleRawSource?) {
|
||||||
this.video = SerializedPlatformVideo.fromVideo(video);
|
this.video = SerializedPlatformVideo.fromVideo(video);
|
||||||
@@ -233,11 +247,13 @@ class VideoDownload {
|
|||||||
videoDetails = null;
|
videoDetails = null;
|
||||||
videoSource = null;
|
videoSource = null;
|
||||||
videoSourceLive = null;
|
videoSourceLive = null;
|
||||||
|
videoOverrideContainer = null;
|
||||||
}
|
}
|
||||||
if(requiresLiveAudioSource && !isLiveAudioSourceValid) {
|
if(requiresLiveAudioSource && !isLiveAudioSourceValid) {
|
||||||
videoDetails = null;
|
videoDetails = null;
|
||||||
audioSource = null;
|
audioSource = null;
|
||||||
videoSourceLive = null;
|
videoSourceLive = null;
|
||||||
|
audioOverrideContainer = null;
|
||||||
}
|
}
|
||||||
if(video == null && videoDetails == null)
|
if(video == null && videoDetails == null)
|
||||||
throw IllegalStateException("Missing information for download to complete");
|
throw IllegalStateException("Missing information for download to complete");
|
||||||
@@ -250,6 +266,30 @@ class VideoDownload {
|
|||||||
if(original !is IPlatformVideoDetails)
|
if(original !is IPlatformVideoDetails)
|
||||||
throw IllegalStateException("Original content is not media?");
|
throw IllegalStateException("Original content is not media?");
|
||||||
|
|
||||||
|
if(requiredCheck) {
|
||||||
|
if(original.video is VideoUnMuxedSourceDescriptor) {
|
||||||
|
if(requireVideoSource) {
|
||||||
|
if((original.video as VideoUnMuxedSourceDescriptor).audioSources.any() && !original.video.videoSources.any()) {
|
||||||
|
requireVideoSource = false;
|
||||||
|
targetPixelCount = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(requireAudioSource) {
|
||||||
|
if(!(original.video as VideoUnMuxedSourceDescriptor).audioSources.any() && original.video.videoSources.any()) {
|
||||||
|
requireAudioSource = false;
|
||||||
|
targetBitrate = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if(requireAudioSource) {
|
||||||
|
requireAudioSource = false;
|
||||||
|
targetBitrate = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
requiredCheck = false;
|
||||||
|
}
|
||||||
|
|
||||||
if(original.video.hasAnySource() && !original.isDownloadable()) {
|
if(original.video.hasAnySource() && !original.isDownloadable()) {
|
||||||
Logger.i(TAG, "Attempted to download unsupported video [${original.name}]:${original.url}");
|
Logger.i(TAG, "Attempted to download unsupported video [${original.name}]:${original.url}");
|
||||||
throw DownloadException("Unsupported video for downloading", false);
|
throw DownloadException("Unsupported video for downloading", false);
|
||||||
@@ -284,6 +324,10 @@ class VideoDownload {
|
|||||||
if(vsource == null)
|
if(vsource == null)
|
||||||
vsource = VideoHelper.selectBestVideoSource(videoSources, targetPixelCount!!.toInt(), arrayOf())
|
vsource = VideoHelper.selectBestVideoSource(videoSources, targetPixelCount!!.toInt(), arrayOf())
|
||||||
// ?: throw IllegalStateException("Could not find a valid video source for video");
|
// ?: throw IllegalStateException("Could not find a valid video source for video");
|
||||||
|
if(vsource is JSSource) {
|
||||||
|
this.hasVideoRequestExecutor = this.hasVideoRequestExecutor || vsource.hasRequestExecutor;
|
||||||
|
this.requiresLiveVideoSource = this.hasVideoRequestExecutor || (vsource is JSDashManifestRawSource && vsource.hasGenerate);
|
||||||
|
}
|
||||||
|
|
||||||
if(vsource == null) {
|
if(vsource == null) {
|
||||||
videoSource = null;
|
videoSource = null;
|
||||||
@@ -335,6 +379,12 @@ class VideoDownload {
|
|||||||
asource = VideoHelper.selectBestAudioSource(audioSources, arrayOf(), null, targetBitrate)
|
asource = VideoHelper.selectBestAudioSource(audioSources, arrayOf(), null, targetBitrate)
|
||||||
?: if(videoSource != null ) null
|
?: if(videoSource != null ) null
|
||||||
else throw DownloadException("Could not find a valid video or audio source for download")
|
else throw DownloadException("Could not find a valid video or audio source for download")
|
||||||
|
|
||||||
|
if(asource is JSSource) {
|
||||||
|
this.hasAudioRequestExecutor = this.hasAudioRequestExecutor || asource.hasRequestExecutor;
|
||||||
|
this.requiresLiveAudioSource = this.hasAudioRequestExecutor || (asource is JSDashManifestRawSource && asource.hasGenerate);
|
||||||
|
}
|
||||||
|
|
||||||
if(asource == null) {
|
if(asource == null) {
|
||||||
audioSource = null;
|
audioSource = null;
|
||||||
if(!original.video.isUnMuxed || original.video.videoSources.size == 0)
|
if(!original.video.isUnMuxed || original.video.videoSources.size == 0)
|
||||||
@@ -374,11 +424,13 @@ class VideoDownload {
|
|||||||
else audioSource;
|
else audioSource;
|
||||||
|
|
||||||
if(actualVideoSource != null) {
|
if(actualVideoSource != null) {
|
||||||
videoFileName = "${videoDetails!!.id.value!!} [${actualVideoSource!!.width}x${actualVideoSource!!.height}].${videoContainerToExtension(actualVideoSource!!.container)}".sanitizeFileName();
|
videoFileNameBase = "${videoDetails!!.id.value!!} [${actualVideoSource!!.width}x${actualVideoSource!!.height}]".sanitizeFileName();
|
||||||
|
videoFileNameExt = videoContainerToExtension(actualVideoSource!!.container);
|
||||||
videoFilePath = File(downloadDir, videoFileName!!).absolutePath;
|
videoFilePath = File(downloadDir, videoFileName!!).absolutePath;
|
||||||
}
|
}
|
||||||
if(actualAudioSource != null) {
|
if(actualAudioSource != null) {
|
||||||
audioFileName = "${videoDetails!!.id.value!!} [${actualAudioSource!!.language}-${actualAudioSource!!.bitrate}].${audioContainerToExtension(actualAudioSource!!.container)}".sanitizeFileName();
|
audioFileNameBase = "${videoDetails!!.id.value!!} [${actualAudioSource!!.language}-${actualAudioSource!!.bitrate}]".sanitizeFileName();
|
||||||
|
audioFileNameExt = audioContainerToExtension(actualAudioSource!!.container);
|
||||||
audioFilePath = File(downloadDir, audioFileName!!).absolutePath;
|
audioFilePath = File(downloadDir, audioFileName!!).absolutePath;
|
||||||
}
|
}
|
||||||
if(subtitleSource != null) {
|
if(subtitleSource != null) {
|
||||||
@@ -518,6 +570,14 @@ class VideoDownload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun decryptSegment(encryptedSegment: ByteArray, key: ByteArray, iv: ByteArray): ByteArray {
|
||||||
|
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||||
|
val secretKey = SecretKeySpec(key, "AES")
|
||||||
|
val ivSpec = IvParameterSpec(iv)
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec)
|
||||||
|
return cipher.doFinal(encryptedSegment)
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun downloadHlsSource(context: Context, name: String, client: ManagedHttpClient, hlsUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
private suspend fun downloadHlsSource(context: Context, name: String, client: ManagedHttpClient, hlsUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||||
if(targetFile.exists())
|
if(targetFile.exists())
|
||||||
targetFile.delete();
|
targetFile.delete();
|
||||||
@@ -533,6 +593,14 @@ class VideoDownload {
|
|||||||
?: throw Exception("Variant playlist content is empty")
|
?: throw Exception("Variant playlist content is empty")
|
||||||
|
|
||||||
val variantPlaylist = HLS.parseVariantPlaylist(vpContent, hlsUrl)
|
val variantPlaylist = HLS.parseVariantPlaylist(vpContent, hlsUrl)
|
||||||
|
val decryptionInfo: DecryptionInfo? = if (variantPlaylist.decryptionInfo != null) {
|
||||||
|
val keyResponse = client.get(variantPlaylist.decryptionInfo.keyUrl)
|
||||||
|
check(keyResponse.isOk) { "HLS request failed for decryption key: ${keyResponse.code}" }
|
||||||
|
DecryptionInfo(keyResponse.body!!.bytes(), variantPlaylist.decryptionInfo.iv?.hexStringToByteArray())
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
variantPlaylist.segments.forEachIndexed { index, segment ->
|
variantPlaylist.segments.forEachIndexed { index, segment ->
|
||||||
if (segment !is HLS.MediaSegment) {
|
if (segment !is HLS.MediaSegment) {
|
||||||
return@forEachIndexed
|
return@forEachIndexed
|
||||||
@@ -544,7 +612,7 @@ class VideoDownload {
|
|||||||
try {
|
try {
|
||||||
segmentFiles.add(segmentFile)
|
segmentFiles.add(segmentFile)
|
||||||
|
|
||||||
val segmentLength = downloadSource_Sequential(client, outputStream, segment.uri) { segmentLength, totalRead, lastSpeed ->
|
val segmentLength = downloadSource_Sequential(client, outputStream, segment.uri, if (index == 0) null else decryptionInfo, index) { segmentLength, totalRead, lastSpeed ->
|
||||||
val averageSegmentLength = if (index == 0) segmentLength else downloadedTotalLength / index
|
val averageSegmentLength = if (index == 0) segmentLength else downloadedTotalLength / index
|
||||||
val expectedTotalLength = averageSegmentLength * (variantPlaylist.segments.size - 1) + segmentLength
|
val expectedTotalLength = averageSegmentLength * (variantPlaylist.segments.size - 1) + segmentLength
|
||||||
onProgress(expectedTotalLength, downloadedTotalLength + totalRead, lastSpeed)
|
onProgress(expectedTotalLength, downloadedTotalLength + totalRead, lastSpeed)
|
||||||
@@ -584,10 +652,8 @@ class VideoDownload {
|
|||||||
|
|
||||||
private suspend fun combineSegments(context: Context, segmentFiles: List<File>, targetFile: File) = withContext(Dispatchers.IO) {
|
private suspend fun combineSegments(context: Context, segmentFiles: List<File>, targetFile: File) = withContext(Dispatchers.IO) {
|
||||||
suspendCancellableCoroutine { continuation ->
|
suspendCancellableCoroutine { continuation ->
|
||||||
val fileList = File(context.cacheDir, "fileList-${UUID.randomUUID()}.txt")
|
val cmd =
|
||||||
fileList.writeText(segmentFiles.joinToString("\n") { "file '${it.absolutePath}'" })
|
"-i \"concat:${segmentFiles.joinToString("|")}\" -c copy \"${targetFile.absolutePath}\""
|
||||||
|
|
||||||
val cmd = "-f concat -safe 0 -i \"${fileList.absolutePath}\" -c copy \"${targetFile.absolutePath}\""
|
|
||||||
|
|
||||||
val statisticsCallback = StatisticsCallback { _ ->
|
val statisticsCallback = StatisticsCallback { _ ->
|
||||||
//TODO: Show progress?
|
//TODO: Show progress?
|
||||||
@@ -597,7 +663,6 @@ class VideoDownload {
|
|||||||
val session = FFmpegKit.executeAsync(cmd,
|
val session = FFmpegKit.executeAsync(cmd,
|
||||||
{ session ->
|
{ session ->
|
||||||
if (ReturnCode.isSuccess(session.returnCode)) {
|
if (ReturnCode.isSuccess(session.returnCode)) {
|
||||||
fileList.delete()
|
|
||||||
continuation.resumeWith(Result.success(Unit))
|
continuation.resumeWith(Result.success(Unit))
|
||||||
} else {
|
} else {
|
||||||
val errorMessage = if (ReturnCode.isCancel(session.returnCode)) {
|
val errorMessage = if (ReturnCode.isCancel(session.returnCode)) {
|
||||||
@@ -605,7 +670,6 @@ class VideoDownload {
|
|||||||
} else {
|
} else {
|
||||||
"Command failed with state '${session.state}' and return code ${session.returnCode}, stack trace ${session.failStackTrace}"
|
"Command failed with state '${session.state}' and return code ${session.returnCode}, stack trace ${session.failStackTrace}"
|
||||||
}
|
}
|
||||||
fileList.delete()
|
|
||||||
continuation.resumeWithException(RuntimeException(errorMessage))
|
continuation.resumeWithException(RuntimeException(errorMessage))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -725,7 +789,7 @@ class VideoDownload {
|
|||||||
else {
|
else {
|
||||||
Logger.i(TAG, "Download $name Sequential");
|
Logger.i(TAG, "Download $name Sequential");
|
||||||
try {
|
try {
|
||||||
sourceLength = downloadSource_Sequential(client, fileStream, videoUrl, onProgress);
|
sourceLength = downloadSource_Sequential(client, fileStream, videoUrl, null, 0, onProgress);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.w(TAG, "Failed to download sequentially (url = $videoUrl)")
|
Logger.w(TAG, "Failed to download sequentially (url = $videoUrl)")
|
||||||
throw e
|
throw e
|
||||||
@@ -752,7 +816,31 @@ class VideoDownload {
|
|||||||
}
|
}
|
||||||
return sourceLength!!;
|
return sourceLength!!;
|
||||||
}
|
}
|
||||||
private fun downloadSource_Sequential(client: ManagedHttpClient, fileStream: FileOutputStream, url: String, onProgress: (Long, Long, Long) -> Unit): Long {
|
|
||||||
|
data class DecryptionInfo(
|
||||||
|
val key: ByteArray,
|
||||||
|
val iv: ByteArray?
|
||||||
|
) {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as DecryptionInfo
|
||||||
|
|
||||||
|
if (!key.contentEquals(other.key)) return false
|
||||||
|
if (!iv.contentEquals(other.iv)) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = key.contentHashCode()
|
||||||
|
result = 31 * result + iv.contentHashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun downloadSource_Sequential(client: ManagedHttpClient, fileStream: FileOutputStream, url: String, decryptionInfo: DecryptionInfo?, index: Int, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||||
val progressRate: Int = 4096 * 5;
|
val progressRate: Int = 4096 * 5;
|
||||||
var lastProgressCount: Int = 0;
|
var lastProgressCount: Int = 0;
|
||||||
val speedRate: Int = 4096 * 5;
|
val speedRate: Int = 4096 * 5;
|
||||||
@@ -772,6 +860,8 @@ class VideoDownload {
|
|||||||
val sourceLength = result.body.contentLength();
|
val sourceLength = result.body.contentLength();
|
||||||
val sourceStream = result.body.byteStream();
|
val sourceStream = result.body.byteStream();
|
||||||
|
|
||||||
|
val segmentBuffer = ByteArrayOutputStream()
|
||||||
|
|
||||||
var totalRead: Long = 0;
|
var totalRead: Long = 0;
|
||||||
try {
|
try {
|
||||||
var read: Int;
|
var read: Int;
|
||||||
@@ -782,7 +872,7 @@ class VideoDownload {
|
|||||||
if (read < 0)
|
if (read < 0)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
fileStream.write(buffer, 0, read);
|
segmentBuffer.write(buffer, 0, read);
|
||||||
|
|
||||||
totalRead += read;
|
totalRead += read;
|
||||||
|
|
||||||
@@ -808,6 +898,21 @@ class VideoDownload {
|
|||||||
result.body.close()
|
result.body.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (decryptionInfo != null) {
|
||||||
|
var iv = decryptionInfo.iv
|
||||||
|
if (iv == null) {
|
||||||
|
iv = ByteBuffer.allocate(16)
|
||||||
|
.putLong(0L)
|
||||||
|
.putLong(index.toLong())
|
||||||
|
.array()
|
||||||
|
}
|
||||||
|
|
||||||
|
val decryptedData = decryptSegment(segmentBuffer.toByteArray(), decryptionInfo.key, iv!!)
|
||||||
|
fileStream.write(decryptedData)
|
||||||
|
} else {
|
||||||
|
fileStream.write(segmentBuffer.toByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
onProgress(sourceLength, totalRead, 0);
|
onProgress(sourceLength, totalRead, 0);
|
||||||
return sourceLength;
|
return sourceLength;
|
||||||
}
|
}
|
||||||
@@ -1026,8 +1131,8 @@ class VideoDownload {
|
|||||||
fun complete() {
|
fun complete() {
|
||||||
Logger.i(TAG, "VideoDownload Complete [${name}]");
|
Logger.i(TAG, "VideoDownload Complete [${name}]");
|
||||||
val existing = StateDownloads.instance.getCachedVideo(id);
|
val existing = StateDownloads.instance.getCachedVideo(id);
|
||||||
val localVideoSource = videoFilePath?.let { LocalVideoSource.fromSource(videoSourceToUse!!, it, videoFileSize ?: 0) };
|
val localVideoSource = videoFilePath?.let { LocalVideoSource.fromSource(videoSourceToUse!!, it, videoFileSize ?: 0, videoOverrideContainer) };
|
||||||
val localAudioSource = audioFilePath?.let { LocalAudioSource.fromSource(audioSourceToUse!!, it, audioFileSize ?: 0) };
|
val localAudioSource = audioFilePath?.let { LocalAudioSource.fromSource(audioSourceToUse!!, it, audioFileSize ?: 0, audioOverrideContainer) };
|
||||||
val localSubtitleSource = subtitleFilePath?.let { LocalSubtitleSource.fromSource(subtitleSource!!, it) };
|
val localSubtitleSource = subtitleFilePath?.let { LocalSubtitleSource.fromSource(subtitleSource!!, it) };
|
||||||
|
|
||||||
if(localVideoSource != null && videoSourceToUse != null && videoSourceToUse is IStreamMetaDataSource)
|
if(localVideoSource != null && videoSourceToUse != null && videoSourceToUse is IStreamMetaDataSource)
|
||||||
@@ -1056,7 +1161,7 @@ class VideoDownload {
|
|||||||
StateDownloads.instance.updateCachedVideo(existing);
|
StateDownloads.instance.updateCachedVideo(existing);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
val newVideo = VideoLocal(videoDetails!!);
|
val newVideo = VideoLocal(videoDetails!!, OffsetDateTime.now());
|
||||||
if(localVideoSource != null)
|
if(localVideoSource != null)
|
||||||
newVideo.videoSource.add(localVideoSource);
|
newVideo.videoSource.add(localVideoSource);
|
||||||
if(localAudioSource != null)
|
if(localAudioSource != null)
|
||||||
@@ -1108,22 +1213,24 @@ class VideoDownload {
|
|||||||
else if (container.contains("video/x-matroska"))
|
else if (container.contains("video/x-matroska"))
|
||||||
return "mkv";
|
return "mkv";
|
||||||
else
|
else
|
||||||
return "video";
|
return "video";//throw IllegalStateException("Unknown container: " + container)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun audioContainerToExtension(container: String): String {
|
fun audioContainerToExtension(container: String): String {
|
||||||
if (container.contains("audio/mp4"))
|
if (container.contains("audio/mp4"))
|
||||||
return "mp4a";
|
return "mp4a";
|
||||||
|
else if (container.contains("video/mp4"))
|
||||||
|
return "mp4";
|
||||||
else if (container.contains("audio/mpeg"))
|
else if (container.contains("audio/mpeg"))
|
||||||
return "mpga";
|
return "mpga";
|
||||||
else if (container.contains("audio/mp3"))
|
else if (container.contains("audio/mp3"))
|
||||||
return "mp3";
|
return "mp3";
|
||||||
else if (container.contains("audio/webm"))
|
else if (container.contains("audio/webm"))
|
||||||
return "webma";
|
return "webm";
|
||||||
else if (container == "application/vnd.apple.mpegurl")
|
else if (container == "application/vnd.apple.mpegurl")
|
||||||
return "mp4";
|
return "m4a";
|
||||||
else
|
else
|
||||||
return "audio";
|
return "audio";// throw IllegalStateException("Unknown container: " + container)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun subtitleContainerToExtension(container: String?): String {
|
fun subtitleContainerToExtension(container: String?): String {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class VideoExport {
|
|||||||
this.subtitleSource = subtitleSource;
|
this.subtitleSource = subtitleSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun export(context: Context, onProgress: ((Double) -> Unit)? = null): DocumentFile = coroutineScope {
|
suspend fun export(context: Context, onProgress: ((Double) -> Unit)? = null, documentRoot: DocumentFile? = null): DocumentFile = coroutineScope {
|
||||||
val v = videoSource;
|
val v = videoSource;
|
||||||
val a = audioSource;
|
val a = audioSource;
|
||||||
val s = subtitleSource;
|
val s = subtitleSource;
|
||||||
@@ -50,7 +50,7 @@ class VideoExport {
|
|||||||
if (s != null) sourceCount++;
|
if (s != null) sourceCount++;
|
||||||
|
|
||||||
val outputFile: DocumentFile?;
|
val outputFile: DocumentFile?;
|
||||||
val downloadRoot = StateApp.instance.getExternalDownloadDirectory(context) ?: throw Exception("External download directory is not set");
|
val downloadRoot = documentRoot ?: StateApp.instance.getExternalDownloadDirectory(context) ?: throw Exception("External download directory is not set");
|
||||||
if (sourceCount > 1) {
|
if (sourceCount > 1) {
|
||||||
val outputFileName = videoLocal.name.sanitizeFileName(true) + ".mp4"// + VideoDownload.videoContainerToExtension(v.container);
|
val outputFileName = videoLocal.name.sanitizeFileName(true) + ".mp4"// + VideoDownload.videoContainerToExtension(v.container);
|
||||||
val f = downloadRoot.createFile("video/mp4", outputFileName)
|
val f = downloadRoot.createFile("video/mp4", outputFileName)
|
||||||
@@ -69,7 +69,7 @@ class VideoExport {
|
|||||||
outputFile = f;
|
outputFile = f;
|
||||||
} else if (v != null) {
|
} else if (v != null) {
|
||||||
val outputFileName = videoLocal.name.sanitizeFileName(true) + "." + VideoDownload.videoContainerToExtension(v.container);
|
val outputFileName = videoLocal.name.sanitizeFileName(true) + "." + VideoDownload.videoContainerToExtension(v.container);
|
||||||
val f = downloadRoot.createFile(v.container, outputFileName)
|
val f = downloadRoot.createFile(if (v.container == "application/vnd.apple.mpegurl") "video/mp4" else v.container, outputFileName)
|
||||||
?: throw Exception("Failed to create file in external directory.");
|
?: throw Exception("Failed to create file in external directory.");
|
||||||
|
|
||||||
Logger.i(TAG, "Copying video.");
|
Logger.i(TAG, "Copying video.");
|
||||||
@@ -81,8 +81,8 @@ class VideoExport {
|
|||||||
outputFile = f;
|
outputFile = f;
|
||||||
} else if (a != null) {
|
} else if (a != null) {
|
||||||
val outputFileName = videoLocal.name.sanitizeFileName(true) + "." + VideoDownload.audioContainerToExtension(a.container);
|
val outputFileName = videoLocal.name.sanitizeFileName(true) + "." + VideoDownload.audioContainerToExtension(a.container);
|
||||||
val f = downloadRoot.createFile(a.container, outputFileName)
|
val f = downloadRoot.createFile(if (a.container == "application/vnd.apple.mpegurl") "video/mp4" else a.container, outputFileName)
|
||||||
?: throw Exception("Failed to create file in external directory.");
|
?: throw Exception("Failed to create file in external directory.");
|
||||||
|
|
||||||
Logger.i(TAG, "Copying audio.");
|
Logger.i(TAG, "Copying audio.");
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
|||||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||||
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.LocalVideoMuxedSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.DownloadedVideoMuxedSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.LocalVideoUnMuxedSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.LocalVideoUnMuxedSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
||||||
@@ -23,6 +23,7 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
|||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideoDetails
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||||
import com.futo.platformplayer.stores.v2.IStoreItem
|
import com.futo.platformplayer.stores.v2.IStoreItem
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
@@ -56,7 +57,7 @@ class VideoLocal: IPlatformVideoDetails, IStoreItem {
|
|||||||
override val video: IVideoSourceDescriptor get() = if(audioSource.isNotEmpty())
|
override val video: IVideoSourceDescriptor get() = if(audioSource.isNotEmpty())
|
||||||
LocalVideoUnMuxedSourceDescriptor(this)
|
LocalVideoUnMuxedSourceDescriptor(this)
|
||||||
else
|
else
|
||||||
LocalVideoMuxedSourceDescriptor(this);
|
DownloadedVideoMuxedSourceDescriptor(this);
|
||||||
override val preview: IVideoSourceDescriptor? get() = videoSerialized.preview;
|
override val preview: IVideoSourceDescriptor? get() = videoSerialized.preview;
|
||||||
|
|
||||||
override val live: IVideoSource? get() = videoSerialized.live;
|
override val live: IVideoSource? get() = videoSerialized.live;
|
||||||
@@ -70,14 +71,21 @@ class VideoLocal: IPlatformVideoDetails, IStoreItem {
|
|||||||
|
|
||||||
override val isLive: Boolean get() = videoSerialized.isLive;
|
override val isLive: Boolean get() = videoSerialized.isLive;
|
||||||
|
|
||||||
|
override val isShort: Boolean get() = videoSerialized.isShort;
|
||||||
|
|
||||||
//TODO: Offline subtitles
|
//TODO: Offline subtitles
|
||||||
override val subtitles: List<ISubtitleSource> = listOf();
|
override val subtitles: List<ISubtitleSource> = listOf();
|
||||||
|
|
||||||
constructor(video: SerializedPlatformVideoDetails) {
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||||
|
var downloadDate: OffsetDateTime? = null;
|
||||||
|
|
||||||
|
constructor(video: SerializedPlatformVideoDetails, downloadDate: OffsetDateTime? = null) {
|
||||||
this.videoSerialized = video;
|
this.videoSerialized = video;
|
||||||
|
this.downloadDate = downloadDate;
|
||||||
}
|
}
|
||||||
constructor(video: IPlatformVideoDetails, subtitleSources: List<SubtitleRawSource>) {
|
constructor(video: IPlatformVideoDetails, subtitleSources: List<SubtitleRawSource>) {
|
||||||
this.videoSerialized = SerializedPlatformVideoDetails.fromVideo(video, subtitleSources);
|
this.videoSerialized = SerializedPlatformVideoDetails.fromVideo(video, subtitleSources);
|
||||||
|
downloadDate = OffsetDateTime.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;
|
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import com.futo.platformplayer.engine.internal.V8Converter
|
|||||||
import com.futo.platformplayer.engine.packages.PackageBridge
|
import com.futo.platformplayer.engine.packages.PackageBridge
|
||||||
import com.futo.platformplayer.engine.packages.PackageDOMParser
|
import com.futo.platformplayer.engine.packages.PackageDOMParser
|
||||||
import com.futo.platformplayer.engine.packages.PackageHttp
|
import com.futo.platformplayer.engine.packages.PackageHttp
|
||||||
|
import com.futo.platformplayer.engine.packages.PackageJSDOM
|
||||||
import com.futo.platformplayer.engine.packages.PackageUtilities
|
import com.futo.platformplayer.engine.packages.PackageUtilities
|
||||||
import com.futo.platformplayer.engine.packages.V8Package
|
import com.futo.platformplayer.engine.packages.V8Package
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
@@ -94,7 +95,11 @@ class V8Plugin {
|
|||||||
withDependency(PackageBridge(this, config));
|
withDependency(PackageBridge(this, config));
|
||||||
|
|
||||||
for(pack in config.packages)
|
for(pack in config.packages)
|
||||||
withDependency(getPackage(pack));
|
withDependency(getPackage(pack)!!);
|
||||||
|
for(pack in config.packagesOptional)
|
||||||
|
getPackage(pack, true)?.let {
|
||||||
|
withDependency(it);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun changeAllowDevSubmit(isAllowed: Boolean) {
|
fun changeAllowDevSubmit(isAllowed: Boolean) {
|
||||||
@@ -183,6 +188,14 @@ class V8Plugin {
|
|||||||
whenNotBusy {
|
whenNotBusy {
|
||||||
synchronized(_runtimeLock) {
|
synchronized(_runtimeLock) {
|
||||||
isStopped = true;
|
isStopped = true;
|
||||||
|
|
||||||
|
//Cleanup http
|
||||||
|
for(pack in _depsPackages) {
|
||||||
|
if(pack is PackageHttp) {
|
||||||
|
pack.cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_runtime?.let {
|
_runtime?.let {
|
||||||
_runtime = null;
|
_runtime = null;
|
||||||
if(!it.isClosed && !it.isDead) {
|
if(!it.isClosed && !it.isDead) {
|
||||||
@@ -254,13 +267,14 @@ class V8Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPackage(packageName: String): V8Package {
|
private fun getPackage(packageName: String, allowNull: Boolean = false): V8Package? {
|
||||||
//TODO: Auto get all package types?
|
//TODO: Auto get all package types?
|
||||||
return when(packageName) {
|
return when(packageName) {
|
||||||
"DOMParser" -> PackageDOMParser(this)
|
"DOMParser" -> PackageDOMParser(this)
|
||||||
"Http" -> PackageHttp(this, config)
|
"Http" -> PackageHttp(this, config)
|
||||||
"Utilities" -> PackageUtilities(this, config)
|
"Utilities" -> PackageUtilities(this, config)
|
||||||
else -> throw ScriptCompilationException(config, "Unknown package [${packageName}] required for plugin ${config.name}");
|
"JSDOM" -> PackageJSDOM(this, config)
|
||||||
|
else -> if(allowNull) null else throw ScriptCompilationException(config, "Unknown package [${packageName}] required for plugin ${config.name}");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ interface IV8PluginConfig {
|
|||||||
val allowEval: Boolean;
|
val allowEval: Boolean;
|
||||||
val allowUrls: List<String>;
|
val allowUrls: List<String>;
|
||||||
val packages: List<String>;
|
val packages: List<String>;
|
||||||
|
val packagesOptional: List<String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
@@ -13,17 +14,20 @@ class V8PluginConfig : IV8PluginConfig {
|
|||||||
override val allowEval: Boolean;
|
override val allowEval: Boolean;
|
||||||
override val allowUrls: List<String>;
|
override val allowUrls: List<String>;
|
||||||
override val packages: List<String>;
|
override val packages: List<String>;
|
||||||
|
override val packagesOptional: List<String>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
name = "Unknown";
|
name = "Unknown";
|
||||||
allowEval = false;
|
allowEval = false;
|
||||||
allowUrls = listOf();
|
allowUrls = listOf();
|
||||||
packages = listOf();
|
packages = listOf();
|
||||||
|
packagesOptional = listOf();
|
||||||
}
|
}
|
||||||
constructor(name: String, allowEval: Boolean, allowUrls: List<String>, packages: List<String> = listOf()) {
|
constructor(name: String, allowEval: Boolean, allowUrls: List<String>, packages: List<String> = listOf(), packagesOptional: List<String> = listOf()) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.allowEval = allowEval;
|
this.allowEval = allowEval;
|
||||||
this.allowUrls = allowUrls;
|
this.allowUrls = allowUrls;
|
||||||
this.packages = packages;
|
this.packages = packages;
|
||||||
|
this.packagesOptional = packagesOptional;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,18 @@
|
|||||||
package com.futo.platformplayer.engine.packages
|
package com.futo.platformplayer.engine.packages
|
||||||
|
|
||||||
|
import android.media.MediaCodec
|
||||||
|
import android.media.MediaCodecList
|
||||||
import com.caoccao.javet.annotations.V8Function
|
import com.caoccao.javet.annotations.V8Function
|
||||||
import com.caoccao.javet.annotations.V8Property
|
import com.caoccao.javet.annotations.V8Property
|
||||||
|
import com.caoccao.javet.utils.JavetResourceUtils
|
||||||
import com.caoccao.javet.values.V8Value
|
import com.caoccao.javet.values.V8Value
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueFunction
|
||||||
import com.futo.platformplayer.BuildConfig
|
import com.futo.platformplayer.BuildConfig
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateDeveloper
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClientConstants
|
import com.futo.platformplayer.api.media.platforms.js.JSClientConstants
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
@@ -16,6 +21,7 @@ import com.futo.platformplayer.engine.IV8PluginConfig
|
|||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
@@ -37,6 +43,18 @@ class PackageBridge : V8Package {
|
|||||||
_config = config;
|
_config = config;
|
||||||
_client = plugin.httpClient;
|
_client = plugin.httpClient;
|
||||||
_clientAuth = plugin.httpClientAuth;
|
_clientAuth = plugin.httpClientAuth;
|
||||||
|
|
||||||
|
withScript("""
|
||||||
|
function setTimeout(func, delay) {
|
||||||
|
let args = Array.prototype.slice.call(arguments, 2);
|
||||||
|
return bridge.setTimeout(func.bind(globalThis, ...args), delay || 0);
|
||||||
|
}
|
||||||
|
""".trimIndent());
|
||||||
|
withScript("""
|
||||||
|
function clearTimeout(id) {
|
||||||
|
bridge.clearTimeout(id);
|
||||||
|
}
|
||||||
|
""".trimIndent());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -55,6 +73,26 @@ class PackageBridge : V8Package {
|
|||||||
fun buildSpecVersion(): Int {
|
fun buildSpecVersion(): Int {
|
||||||
return JSClientConstants.PLUGIN_SPEC_VERSION;
|
return JSClientConstants.PLUGIN_SPEC_VERSION;
|
||||||
}
|
}
|
||||||
|
@V8Property
|
||||||
|
fun buildPlatform(): String {
|
||||||
|
return "android";
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Property
|
||||||
|
fun supportedContent(): Array<Int> {
|
||||||
|
return arrayOf(
|
||||||
|
ContentType.MEDIA.value,
|
||||||
|
ContentType.POST.value,
|
||||||
|
ContentType.PLAYLIST.value,
|
||||||
|
ContentType.WEB.value,
|
||||||
|
ContentType.URL.value,
|
||||||
|
ContentType.NESTED_VIDEO.value,
|
||||||
|
ContentType.CHANNEL.value,
|
||||||
|
ContentType.LOCKED.value,
|
||||||
|
ContentType.PLACEHOLDER.value,
|
||||||
|
ContentType.DEFERRED.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun dispose(value: V8Value) {
|
fun dispose(value: V8Value) {
|
||||||
@@ -62,6 +100,48 @@ class PackageBridge : V8Package {
|
|||||||
value.close();
|
value.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var timeoutCounter = 0;
|
||||||
|
var timeoutMap = HashSet<Int>();
|
||||||
|
@V8Function
|
||||||
|
fun setTimeout(func: V8ValueFunction, timeout: Long): Int {
|
||||||
|
val id = timeoutCounter++;
|
||||||
|
|
||||||
|
val funcClone = func.toClone<V8ValueFunction>()
|
||||||
|
|
||||||
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
|
delay(timeout);
|
||||||
|
synchronized(timeoutMap) {
|
||||||
|
if(!timeoutMap.contains(id)) {
|
||||||
|
JavetResourceUtils.safeClose(funcClone);
|
||||||
|
return@launch;
|
||||||
|
}
|
||||||
|
timeoutMap.remove(id);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
_plugin.whenNotBusy {
|
||||||
|
funcClone.callVoid(null, arrayOf<Any>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed timeout callback", ex);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
JavetResourceUtils.safeClose(funcClone);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
synchronized(timeoutMap) {
|
||||||
|
timeoutMap.add(id);
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
@V8Function
|
||||||
|
fun clearTimeout(id: Int) {
|
||||||
|
synchronized(timeoutMap) {
|
||||||
|
if(timeoutMap.contains(id))
|
||||||
|
timeoutMap.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun toast(str: String) {
|
fun toast(str: String) {
|
||||||
Logger.i(TAG, "Plugin toast [${_config.name}]: ${str}");
|
Logger.i(TAG, "Plugin toast [${_config.name}]: ${str}");
|
||||||
@@ -130,7 +210,44 @@ class PackageBridge : V8Package {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun getHardwareCodecs(): List<String>{
|
||||||
|
return getSupportedHardwareMediaCodecs();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "PackageBridge";
|
private const val TAG = "PackageBridge";
|
||||||
|
|
||||||
|
private var _mediaCodecList: MutableList<String> = mutableListOf();
|
||||||
|
private var _mediaCodecListHardware: MutableList<String> = mutableListOf();
|
||||||
|
|
||||||
|
fun getSupportedMediaCodecs(): List<String>{
|
||||||
|
synchronized(_mediaCodecList) {
|
||||||
|
if(_mediaCodecList.size <= 0)
|
||||||
|
updateMediaCodecList();
|
||||||
|
return _mediaCodecList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun getSupportedHardwareMediaCodecs(): List<String>{
|
||||||
|
synchronized(_mediaCodecList) {
|
||||||
|
if(_mediaCodecList.size <= 0)
|
||||||
|
updateMediaCodecList();
|
||||||
|
return _mediaCodecListHardware;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private fun updateMediaCodecList() {
|
||||||
|
_mediaCodecList.clear();
|
||||||
|
_mediaCodecListHardware.clear();
|
||||||
|
for(codec in MediaCodecList(MediaCodecList.ALL_CODECS).codecInfos) {
|
||||||
|
if(!codec.isEncoder) {
|
||||||
|
_mediaCodecList.add(codec.canonicalName);
|
||||||
|
if (codec.isHardwareAccelerated)
|
||||||
|
_mediaCodecListHardware.add(codec.canonicalName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,9 +8,7 @@ import com.caoccao.javet.enums.V8ProxyMode
|
|||||||
import com.caoccao.javet.interop.V8Runtime
|
import com.caoccao.javet.interop.V8Runtime
|
||||||
import com.caoccao.javet.values.V8Value
|
import com.caoccao.javet.values.V8Value
|
||||||
import com.caoccao.javet.values.primitive.V8ValueString
|
import com.caoccao.javet.values.primitive.V8ValueString
|
||||||
import com.caoccao.javet.values.reference.V8ValueArrayBuffer
|
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.caoccao.javet.values.reference.V8ValueSharedArrayBuffer
|
|
||||||
import com.caoccao.javet.values.reference.V8ValueTypedArray
|
import com.caoccao.javet.values.reference.V8ValueTypedArray
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
@@ -20,11 +18,9 @@ import com.futo.platformplayer.engine.V8Plugin
|
|||||||
import com.futo.platformplayer.engine.internal.IV8Convertable
|
import com.futo.platformplayer.engine.internal.IV8Convertable
|
||||||
import com.futo.platformplayer.engine.internal.V8BindObject
|
import com.futo.platformplayer.engine.internal.V8BindObject
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateApp
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.net.SocketTimeoutException
|
import java.net.SocketTimeoutException
|
||||||
import kotlin.streams.asSequence
|
import java.util.concurrent.ForkJoinPool
|
||||||
|
import java.util.concurrent.ForkJoinTask
|
||||||
|
|
||||||
class PackageHttp: V8Package {
|
class PackageHttp: V8Package {
|
||||||
@Transient
|
@Transient
|
||||||
@@ -42,6 +38,12 @@ class PackageHttp: V8Package {
|
|||||||
override val name: String get() = "Http";
|
override val name: String get() = "Http";
|
||||||
override val variableName: String get() = "http";
|
override val variableName: String get() = "http";
|
||||||
|
|
||||||
|
private var _batchPoolLock: Any = Any();
|
||||||
|
private var _batchPool: ForkJoinPool? = null;
|
||||||
|
|
||||||
|
private val aliveSockets = mutableListOf<SocketResult>();
|
||||||
|
private var _cleanedUp = false;
|
||||||
|
|
||||||
|
|
||||||
constructor(plugin: V8Plugin, config: IV8PluginConfig): super(plugin) {
|
constructor(plugin: V8Plugin, config: IV8PluginConfig): super(plugin) {
|
||||||
_config = config;
|
_config = config;
|
||||||
@@ -51,6 +53,58 @@ class PackageHttp: V8Package {
|
|||||||
_packageClientAuth = PackageHttpClient(this, _clientAuth);
|
_packageClientAuth = PackageHttpClient(this, _clientAuth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun cleanup(){
|
||||||
|
Logger.w(TAG, "PackageHttp Cleaning up")
|
||||||
|
val sockets = synchronized(aliveSockets) { aliveSockets.toList() }
|
||||||
|
_cleanedUp = true;
|
||||||
|
for(socket in sockets){
|
||||||
|
try {
|
||||||
|
Logger.w(TAG, "PackageHttp Socket Cleaned Up");
|
||||||
|
socket.close(1001, "Cleanup");
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to close socket", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(sockets.size > 0) {
|
||||||
|
//Thread.sleep(100); //Give sockets a bit
|
||||||
|
}
|
||||||
|
synchronized(aliveSockets) {
|
||||||
|
aliveSockets.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
Automatically adjusting threadpool dedicated per PackageHttp for batch requests.
|
||||||
|
*/
|
||||||
|
private fun <T, R> autoParallelPool(data: List<T>, parallelism: Int, handle: (T)->R): List<Pair<R?, Throwable?>> {
|
||||||
|
synchronized(_batchPoolLock) {
|
||||||
|
val threadsToUse = if (parallelism <= 0) data.size else Math.min(parallelism, data.size);
|
||||||
|
if(_batchPool == null)
|
||||||
|
_batchPool = ForkJoinPool(threadsToUse);
|
||||||
|
var pool = _batchPool ?: return listOf();
|
||||||
|
if(pool.poolSize < threadsToUse) { //Resize pool
|
||||||
|
pool.shutdown();
|
||||||
|
_batchPool = ForkJoinPool(threadsToUse);
|
||||||
|
pool = _batchPool ?: return listOf();
|
||||||
|
}
|
||||||
|
|
||||||
|
val resultTasks = mutableListOf<ForkJoinTask<Pair<R?, Throwable?>>>();
|
||||||
|
for(item in data){
|
||||||
|
resultTasks.add(pool.submit<Pair<R?, Throwable?>> {
|
||||||
|
try {
|
||||||
|
return@submit Pair<R?, Throwable?>(handle(item), null);
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
return@submit Pair<R?, Throwable?>(null, ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return resultTasks.map { it.join() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun newClient(withAuth: Boolean): PackageHttpClient {
|
fun newClient(withAuth: Boolean): PackageHttpClient {
|
||||||
val httpClient = if(withAuth) _clientAuth.clone() else _client.clone();
|
val httpClient = if(withAuth) _clientAuth.clone() else _client.clone();
|
||||||
@@ -73,24 +127,24 @@ class PackageHttp: V8Package {
|
|||||||
@V8Function
|
@V8Function
|
||||||
fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, bytesResult: Boolean = false) : IBridgeHttpResponse {
|
fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, bytesResult: Boolean = false) : IBridgeHttpResponse {
|
||||||
return if(useAuth)
|
return if(useAuth)
|
||||||
_packageClientAuth.request(method, url, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING)
|
_packageClientAuth.requestInternal(method, url, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING)
|
||||||
else
|
else
|
||||||
_packageClient.request(method, url, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING);
|
_packageClient.requestInternal(method, url, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING);
|
||||||
}
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, bytesResult: Boolean = false) : IBridgeHttpResponse {
|
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, bytesResult: Boolean = false) : IBridgeHttpResponse {
|
||||||
return if(useAuth)
|
return if(useAuth)
|
||||||
_packageClientAuth.requestWithBody(method, url, body, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING)
|
_packageClientAuth.requestWithBodyInternal(method, url, body, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING)
|
||||||
else
|
else
|
||||||
_packageClient.requestWithBody(method, url, body, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING);
|
_packageClient.requestWithBodyInternal(method, url, body, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING);
|
||||||
}
|
}
|
||||||
@V8Function
|
@V8Function
|
||||||
fun GET(url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, useByteResponse: Boolean = false) : IBridgeHttpResponse {
|
fun GET(url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, useByteResponse: Boolean = false) : IBridgeHttpResponse {
|
||||||
return if(useAuth)
|
return if(useAuth)
|
||||||
_packageClientAuth.GET(url, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING)
|
_packageClientAuth.GETInternal(url, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING)
|
||||||
else
|
else
|
||||||
_packageClient.GET(url, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
_packageClient.GETInternal(url, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
||||||
}
|
}
|
||||||
@V8Function
|
@V8Function
|
||||||
fun POST(url: String, body: Any, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, useByteResponse: Boolean = false) : IBridgeHttpResponse {
|
fun POST(url: String, body: Any, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, useByteResponse: Boolean = false) : IBridgeHttpResponse {
|
||||||
@@ -98,15 +152,15 @@ class PackageHttp: V8Package {
|
|||||||
val client = if(useAuth) _packageClientAuth else _packageClient;
|
val client = if(useAuth) _packageClientAuth else _packageClient;
|
||||||
|
|
||||||
if(body is V8ValueString)
|
if(body is V8ValueString)
|
||||||
return client.POST(url, body.value, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
return client.POSTInternal(url, body.value, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
||||||
else if(body is String)
|
else if(body is String)
|
||||||
return client.POST(url, body, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
return client.POSTInternal(url, body, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
||||||
else if(body is V8ValueTypedArray)
|
else if(body is V8ValueTypedArray)
|
||||||
return client.POST(url, body.toBytes(), headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
return client.POSTInternal(url, body.toBytes(), headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
||||||
else if(body is ByteArray)
|
else if(body is ByteArray)
|
||||||
return client.POST(url, body, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
return client.POSTInternal(url, body, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
||||||
else if(body is ArrayList<*>) //Avoid this case, used purely for testing
|
else if(body is ArrayList<*>) //Avoid this case, used purely for testing
|
||||||
return client.POST(url, body.map { (it as Double).toInt().toByte() }.toByteArray(), headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
return client.POSTInternal(url, body.map { (it as Double).toInt().toByte() }.toByteArray(), headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
||||||
else
|
else
|
||||||
throw NotImplementedError("Body type " + body?.javaClass?.name?.toString() + " not implemented for POST");
|
throw NotImplementedError("Body type " + body?.javaClass?.name?.toString() + " not implemented for POST");
|
||||||
}
|
}
|
||||||
@@ -176,8 +230,6 @@ class PackageHttp: V8Package {
|
|||||||
obj.set("url", url);
|
obj.set("url", url);
|
||||||
obj.set("code", code);
|
obj.set("code", code);
|
||||||
if(body != null) {
|
if(body != null) {
|
||||||
val buffer = runtime.createV8ValueArrayBuffer(body.size);
|
|
||||||
buffer.fromBytes(body);
|
|
||||||
obj.set("body", body);
|
obj.set("body", body);
|
||||||
}
|
}
|
||||||
obj.set("headers", headers);
|
obj.set("headers", headers);
|
||||||
@@ -236,16 +288,19 @@ class PackageHttp: V8Package {
|
|||||||
//Finalizer
|
//Finalizer
|
||||||
@V8Function
|
@V8Function
|
||||||
fun execute(): List<IBridgeHttpResponse?> {
|
fun execute(): List<IBridgeHttpResponse?> {
|
||||||
return _reqs.parallelStream().map {
|
return _package.autoParallelPool(_reqs, -1) {
|
||||||
if(it.second.method == "DUMMY")
|
if(it.second.method == "DUMMY")
|
||||||
return@map null;
|
return@autoParallelPool null;
|
||||||
if(it.second.body != null)
|
if(it.second.body != null)
|
||||||
return@map it.first.requestWithBody(it.second.method, it.second.url, it.second.body!!, it.second.headers, it.second.respType);
|
return@autoParallelPool it.first.requestWithBodyInternal(it.second.method, it.second.url, it.second.body!!, it.second.headers, it.second.respType);
|
||||||
else
|
else
|
||||||
return@map it.first.request(it.second.method, it.second.url, it.second.headers, it.second.respType);
|
return@autoParallelPool it.first.requestInternal(it.second.method, it.second.url, it.second.headers, it.second.respType);
|
||||||
}
|
}.map {
|
||||||
.asSequence()
|
if(it.second != null)
|
||||||
.toList();
|
throw it.second!!;
|
||||||
|
else
|
||||||
|
return@map it.first;
|
||||||
|
}.toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,7 +361,9 @@ class PackageHttp: V8Package {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType) : IBridgeHttpResponse {
|
fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), useBytes: Boolean = false) : IBridgeHttpResponse
|
||||||
|
= requestInternal(method, url, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING);
|
||||||
|
fun requestInternal(method: String, url: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType) : IBridgeHttpResponse {
|
||||||
applyDefaultHeaders(headers);
|
applyDefaultHeaders(headers);
|
||||||
return logExceptions {
|
return logExceptions {
|
||||||
return@logExceptions catchHttp {
|
return@logExceptions catchHttp {
|
||||||
@@ -325,7 +382,9 @@ class PackageHttp: V8Package {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
@V8Function
|
@V8Function
|
||||||
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType) : IBridgeHttpResponse {
|
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), useBytes: Boolean = false) : IBridgeHttpResponse
|
||||||
|
= requestWithBodyInternal(method, url, body, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING)
|
||||||
|
fun requestWithBodyInternal(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType) : IBridgeHttpResponse {
|
||||||
applyDefaultHeaders(headers);
|
applyDefaultHeaders(headers);
|
||||||
return logExceptions {
|
return logExceptions {
|
||||||
catchHttp {
|
catchHttp {
|
||||||
@@ -346,7 +405,9 @@ class PackageHttp: V8Package {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun GET(url: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
|
fun GET(url: String, headers: MutableMap<String, String> = HashMap(), useBytes: Boolean = false) : IBridgeHttpResponse
|
||||||
|
= GETInternal(url, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING)
|
||||||
|
fun GETInternal(url: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
|
||||||
applyDefaultHeaders(headers);
|
applyDefaultHeaders(headers);
|
||||||
return logExceptions {
|
return logExceptions {
|
||||||
catchHttp {
|
catchHttp {
|
||||||
@@ -368,7 +429,9 @@ class PackageHttp: V8Package {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
@V8Function
|
@V8Function
|
||||||
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
|
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), useBytes: Boolean = false) : IBridgeHttpResponse
|
||||||
|
= POSTInternal(url, body, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING)
|
||||||
|
fun POSTInternal(url: String, body: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
|
||||||
applyDefaultHeaders(headers);
|
applyDefaultHeaders(headers);
|
||||||
return logExceptions {
|
return logExceptions {
|
||||||
catchHttp {
|
catchHttp {
|
||||||
@@ -390,7 +453,9 @@ class PackageHttp: V8Package {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
@V8Function
|
@V8Function
|
||||||
fun POST(url: String, body: ByteArray, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
|
fun POST(url: String, body: ByteArray, headers: MutableMap<String, String> = HashMap(), useBytes: Boolean = false) : IBridgeHttpResponse
|
||||||
|
= POSTInternal(url, body, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING)
|
||||||
|
fun POSTInternal(url: String, body: ByteArray, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
|
||||||
applyDefaultHeaders(headers);
|
applyDefaultHeaders(headers);
|
||||||
return logExceptions {
|
return logExceptions {
|
||||||
catchHttp {
|
catchHttp {
|
||||||
@@ -414,9 +479,16 @@ class PackageHttp: V8Package {
|
|||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun socket(url: String, headers: Map<String, String>? = null): SocketResult {
|
fun socket(url: String, headers: Map<String, String>? = null): SocketResult {
|
||||||
|
if(_package._cleanedUp)
|
||||||
|
throw IllegalStateException("Plugin shutdown");
|
||||||
val socketHeaders = headers?.toMutableMap() ?: HashMap();
|
val socketHeaders = headers?.toMutableMap() ?: HashMap();
|
||||||
applyDefaultHeaders(socketHeaders);
|
applyDefaultHeaders(socketHeaders);
|
||||||
return SocketResult(this, _client, url, socketHeaders);
|
val socket = SocketResult(_package, this, _client, url, socketHeaders);
|
||||||
|
Logger.w(TAG, "PackageHttp Socket opened");
|
||||||
|
synchronized(_package.aliveSockets) {
|
||||||
|
_package.aliveSockets.add(socket);
|
||||||
|
}
|
||||||
|
return socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun applyDefaultHeaders(headerMap: MutableMap<String, String>) {
|
private fun applyDefaultHeaders(headerMap: MutableMap<String, String>) {
|
||||||
@@ -439,11 +511,8 @@ class PackageHttp: V8Package {
|
|||||||
else {
|
else {
|
||||||
headers?.forEach { (header, values) ->
|
headers?.forEach { (header, values) ->
|
||||||
val lowerCaseHeader = header.lowercase()
|
val lowerCaseHeader = header.lowercase()
|
||||||
if(lowerCaseHeader == "set-cookie") {
|
if(lowerCaseHeader == "set-cookie" && !values.any { it.lowercase().contains("httponly") })
|
||||||
result[lowerCaseHeader] = values.filter{
|
result[lowerCaseHeader] = values;
|
||||||
!it.lowercase().contains("httponly")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
result[lowerCaseHeader] = values;
|
result[lowerCaseHeader] = values;
|
||||||
}
|
}
|
||||||
@@ -525,13 +594,15 @@ class PackageHttp: V8Package {
|
|||||||
|
|
||||||
private var _listeners: V8ValueObject? = null;
|
private var _listeners: V8ValueObject? = null;
|
||||||
|
|
||||||
|
private val _package: PackageHttp;
|
||||||
private val _packageClient: PackageHttpClient;
|
private val _packageClient: PackageHttpClient;
|
||||||
private val _client: ManagedHttpClient;
|
private val _client: ManagedHttpClient;
|
||||||
private val _url: String;
|
private val _url: String;
|
||||||
private val _headers: Map<String, String>;
|
private val _headers: Map<String, String>;
|
||||||
|
|
||||||
constructor(pack: PackageHttpClient, client: ManagedHttpClient, url: String, headers: Map<String,String>) {
|
constructor(parent: PackageHttp, pack: PackageHttpClient, client: ManagedHttpClient, url: String, headers: Map<String,String>) {
|
||||||
_packageClient = pack;
|
_packageClient = pack;
|
||||||
|
_package = parent;
|
||||||
_client = client;
|
_client = client;
|
||||||
_url = url;
|
_url = url;
|
||||||
_headers = headers;
|
_headers = headers;
|
||||||
@@ -557,7 +628,7 @@ class PackageHttp: V8Package {
|
|||||||
override fun open() {
|
override fun open() {
|
||||||
Logger.i(TAG, "Websocket opened: " + _url);
|
Logger.i(TAG, "Websocket opened: " + _url);
|
||||||
_isOpen = true;
|
_isOpen = true;
|
||||||
if(hasOpen) {
|
if(hasOpen && _listeners?.isClosed != true) {
|
||||||
try {
|
try {
|
||||||
_listeners?.invokeVoid("open", arrayOf<Any>());
|
_listeners?.invokeVoid("open", arrayOf<Any>());
|
||||||
}
|
}
|
||||||
@@ -567,7 +638,7 @@ class PackageHttp: V8Package {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
override fun message(msg: String) {
|
override fun message(msg: String) {
|
||||||
if(hasMessage) {
|
if(hasMessage && _listeners?.isClosed != true) {
|
||||||
try {
|
try {
|
||||||
_listeners?.invokeVoid("message", msg);
|
_listeners?.invokeVoid("message", msg);
|
||||||
}
|
}
|
||||||
@@ -575,7 +646,7 @@ class PackageHttp: V8Package {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
override fun closing(code: Int, reason: String) {
|
override fun closing(code: Int, reason: String) {
|
||||||
if(hasClosing)
|
if(hasClosing && _listeners?.isClosed != true)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
_listeners?.invokeVoid("closing", code, reason);
|
_listeners?.invokeVoid("closing", code, reason);
|
||||||
@@ -587,7 +658,7 @@ class PackageHttp: V8Package {
|
|||||||
}
|
}
|
||||||
override fun closed(code: Int, reason: String) {
|
override fun closed(code: Int, reason: String) {
|
||||||
_isOpen = false;
|
_isOpen = false;
|
||||||
if(hasClosed) {
|
if(hasClosed && _listeners?.isClosed != true) {
|
||||||
try {
|
try {
|
||||||
_listeners?.invokeVoid("closed", code, reason);
|
_listeners?.invokeVoid("closed", code, reason);
|
||||||
}
|
}
|
||||||
@@ -595,11 +666,15 @@ class PackageHttp: V8Package {
|
|||||||
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex);
|
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Logger.w(TAG, "PackageHttp Socket removed");
|
||||||
|
synchronized(_package.aliveSockets) {
|
||||||
|
_package.aliveSockets.remove(this@SocketResult);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
override fun failure(exception: Throwable) {
|
override fun failure(exception: Throwable) {
|
||||||
_isOpen = false;
|
_isOpen = false;
|
||||||
Logger.e(TAG, "Websocket failure: ${exception.message} (${_url})", exception);
|
Logger.e(TAG, "Websocket failure: ${exception.message} (${_url})", exception);
|
||||||
if(hasFailure) {
|
if(hasFailure && _listeners?.isClosed != true) {
|
||||||
try {
|
try {
|
||||||
_listeners?.invokeVoid("failure", exception.message);
|
_listeners?.invokeVoid("failure", exception.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.futo.platformplayer.engine.packages
|
||||||
|
|
||||||
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
|
||||||
|
|
||||||
|
class PackageJSDOM : V8Package {
|
||||||
|
@Transient
|
||||||
|
private val _config: IV8PluginConfig;
|
||||||
|
|
||||||
|
override val name: String get() = "JSDOM";
|
||||||
|
override val variableName: String get() = "packageJSDOM";
|
||||||
|
|
||||||
|
constructor(plugin: V8Plugin, config: IV8PluginConfig): super(plugin) {
|
||||||
|
_config = config;
|
||||||
|
plugin.withDependency(StateApp.instance.contextOrNull ?: return, "scripts/JSDOM.js");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+2
-4
@@ -13,7 +13,6 @@ import com.futo.platformplayer.R
|
|||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
import com.futo.platformplayer.fixHtmlLinks
|
import com.futo.platformplayer.fixHtmlLinks
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.resolveChannelUrl
|
import com.futo.platformplayer.resolveChannelUrl
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
@@ -21,6 +20,7 @@ import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
|
|||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.toHumanNumber
|
import com.futo.platformplayer.toHumanNumber
|
||||||
import com.futo.platformplayer.views.platform.PlatformLinkView
|
import com.futo.platformplayer.views.platform.PlatformLinkView
|
||||||
|
import com.futo.polycentric.core.PolycentricProfile
|
||||||
import com.futo.polycentric.core.toName
|
import com.futo.polycentric.core.toName
|
||||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
|
|
||||||
@@ -134,9 +134,7 @@ class ChannelAboutFragment : Fragment, IChannelTabFragment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(!map.containsKey("Harbor"))
|
if(!map.containsKey("Harbor"))
|
||||||
this.context?.let {
|
map.set("Harbor", polycentricProfile.getHarborUrl());
|
||||||
map.set("Harbor", polycentricProfile.getHarborUrl(it));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (map.isNotEmpty())
|
if (map.isNotEmpty())
|
||||||
setLinks(map, if (polycentricProfile.systemState.username.isNotBlank()) polycentricProfile.systemState.username else _lastChannel?.name ?: "")
|
setLinks(map, if (polycentricProfile.systemState.username.isNotBlank()) polycentricProfile.systemState.username else _lastChannel?.name ?: "")
|
||||||
|
|||||||
+85
-17
@@ -1,12 +1,14 @@
|
|||||||
package com.futo.platformplayer.fragment.channel.tab
|
package com.futo.platformplayer.fragment.channel.tab
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.FrameLayout
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
@@ -15,7 +17,6 @@ import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
|||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSPager
|
import com.futo.platformplayer.api.media.platforms.js.models.JSPager
|
||||||
import com.futo.platformplayer.api.media.structures.IAsyncPager
|
import com.futo.platformplayer.api.media.structures.IAsyncPager
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
@@ -25,26 +26,30 @@ import com.futo.platformplayer.api.media.structures.MultiPager
|
|||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.constructs.Event2
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
|
import com.futo.platformplayer.dp
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
import com.futo.platformplayer.exceptions.ChannelException
|
import com.futo.platformplayer.exceptions.ChannelException
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.FeedView
|
import com.futo.platformplayer.fragment.mainactivity.main.FeedView
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateCache
|
import com.futo.platformplayer.states.StateCache
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.platformplayer.states.StateSubscriptions
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
|
import com.futo.platformplayer.views.SearchView
|
||||||
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
||||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
|
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
|
||||||
|
import com.futo.polycentric.core.PolycentricProfile
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
class ChannelContentsFragment(private val subType: String? = null) : Fragment(), IChannelTabFragment {
|
||||||
private var _recyclerResults: RecyclerView? = null;
|
private var _recyclerResults: RecyclerView? = null;
|
||||||
private var _llmVideo: LinearLayoutManager? = null;
|
private var _glmVideo: GridLayoutManager? = null;
|
||||||
private var _loading = false;
|
private var _loading = false;
|
||||||
private var _pager_parent: IPager<IPlatformContent>? = null;
|
private var _pager_parent: IPager<IPlatformContent>? = null;
|
||||||
private var _pager: IPager<IPlatformContent>? = null;
|
private var _pager: IPager<IPlatformContent>? = null;
|
||||||
@@ -53,6 +58,8 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||||||
private var _results: ArrayList<IPlatformContent> = arrayListOf();
|
private var _results: ArrayList<IPlatformContent> = arrayListOf();
|
||||||
private var _adapterResults: InsertedViewAdapterWithLoader<ContentPreviewViewHolder>? = null;
|
private var _adapterResults: InsertedViewAdapterWithLoader<ContentPreviewViewHolder>? = null;
|
||||||
private var _lastPolycentricProfile: PolycentricProfile? = null;
|
private var _lastPolycentricProfile: PolycentricProfile? = null;
|
||||||
|
private var _query: String? = null
|
||||||
|
private var _searchView: SearchView? = null
|
||||||
|
|
||||||
val onContentClicked = Event2<IPlatformContent, Long>();
|
val onContentClicked = Event2<IPlatformContent, Long>();
|
||||||
val onContentUrlClicked = Event2<String, ContentType>();
|
val onContentUrlClicked = Event2<String, ContentType>();
|
||||||
@@ -67,14 +74,33 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||||||
private fun getContentPager(channel: IPlatformChannel): IPager<IPlatformContent> {
|
private fun getContentPager(channel: IPlatformChannel): IPager<IPlatformContent> {
|
||||||
Logger.i(TAG, "getContentPager");
|
Logger.i(TAG, "getContentPager");
|
||||||
|
|
||||||
val lastPolycentricProfile = _lastPolycentricProfile;
|
var pager: IPager<IPlatformContent>? = null
|
||||||
var pager: IPager<IPlatformContent>? = null;
|
val query = _query
|
||||||
if (lastPolycentricProfile != null)
|
if (!query.isNullOrBlank()) {
|
||||||
pager= StatePolycentric.instance.getChannelContent(lifecycleScope, lastPolycentricProfile);
|
if(subType != null) {
|
||||||
|
Logger.i(TAG, "StatePlatform.instance.searchChannel(channel.url = ${channel.url}, query = ${query}, subType = ${subType})")
|
||||||
if(pager == null)
|
pager = StatePlatform.instance.searchChannel(channel.url, query, subType);
|
||||||
pager = StatePlatform.instance.getChannelContent(channel.url);
|
} else {
|
||||||
|
Logger.i(TAG, "StatePlatform.instance.searchChannel(channel.url = ${channel.url}, query = ${query})")
|
||||||
|
pager = StatePlatform.instance.searchChannel(channel.url, query);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val lastPolycentricProfile = _lastPolycentricProfile;
|
||||||
|
if (lastPolycentricProfile != null && StatePolycentric.instance.enabled) {
|
||||||
|
pager = StatePolycentric.instance.getChannelContent(lifecycleScope, lastPolycentricProfile, type = subType);
|
||||||
|
Logger.i(TAG, "StatePolycentric.instance.getChannelContent(lifecycleScope, lastPolycentricProfile, type = ${subType})")
|
||||||
|
}
|
||||||
|
|
||||||
|
if(pager == null) {
|
||||||
|
if(subType != null) {
|
||||||
|
pager = StatePlatform.instance.getChannelContent(channel.url, subType);
|
||||||
|
Logger.i(TAG, "StatePlatform.instance.getChannelContent(channel.url = ${channel.url}, subType = ${subType})")
|
||||||
|
} else {
|
||||||
|
pager = StatePlatform.instance.getChannelContent(channel.url);
|
||||||
|
Logger.i(TAG, "StatePlatform.instance.getChannelContent(channel.url = ${channel.url})")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return pager;
|
return pager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +144,7 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||||||
super.onScrolled(recyclerView, dx, dy);
|
super.onScrolled(recyclerView, dx, dy);
|
||||||
|
|
||||||
val recyclerResults = _recyclerResults ?: return;
|
val recyclerResults = _recyclerResults ?: return;
|
||||||
val llmVideo = _llmVideo ?: return;
|
val llmVideo = _glmVideo ?: return;
|
||||||
|
|
||||||
val visibleItemCount = recyclerResults.childCount;
|
val visibleItemCount = recyclerResults.childCount;
|
||||||
val firstVisibleItem = llmVideo.findFirstVisibleItemPosition();
|
val firstVisibleItem = llmVideo.findFirstVisibleItemPosition();
|
||||||
@@ -140,19 +166,49 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||||||
|
|
||||||
_taskLoadVideos.cancel();
|
_taskLoadVideos.cancel();
|
||||||
|
|
||||||
|
_query = null
|
||||||
_channel = channel;
|
_channel = channel;
|
||||||
|
updateSearchViewVisibility()
|
||||||
_results.clear();
|
_results.clear();
|
||||||
_adapterResults?.notifyDataSetChanged();
|
_adapterResults?.notifyDataSetChanged();
|
||||||
|
|
||||||
loadInitial();
|
loadInitial();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateSearchViewVisibility() {
|
||||||
|
if (subType != null) {
|
||||||
|
_searchView?.visibility = View.GONE
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val client = _channel?.id?.pluginId?.let { StatePlatform.instance.getClientOrNull(it) }
|
||||||
|
Logger.i(TAG, "_searchView.visible = ${client?.capabilities?.hasSearchChannelContents == true}")
|
||||||
|
_searchView?.visibility = if (client?.capabilities?.hasSearchChannelContents == true) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setQuery(query: String) {
|
||||||
|
_query = query
|
||||||
|
_taskLoadVideos.cancel()
|
||||||
|
_results.clear()
|
||||||
|
_adapterResults?.notifyDataSetChanged()
|
||||||
|
loadInitial()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
val view = inflater.inflate(R.layout.fragment_channel_videos, container, false);
|
val view = inflater.inflate(R.layout.fragment_channel_videos, container, false);
|
||||||
|
|
||||||
|
_query = null
|
||||||
_recyclerResults = view.findViewById(R.id.recycler_videos);
|
_recyclerResults = view.findViewById(R.id.recycler_videos);
|
||||||
|
|
||||||
_adapterResults = PreviewContentListAdapter(view.context, FeedStyle.THUMBNAIL, _results, null, Settings.instance.channel.progressBar).apply {
|
val searchView = SearchView(requireContext()).apply { layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT) }.apply {
|
||||||
|
onEnter.subscribe {
|
||||||
|
setQuery(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_searchView = searchView
|
||||||
|
updateSearchViewVisibility()
|
||||||
|
|
||||||
|
_adapterResults = PreviewContentListAdapter(view.context, FeedStyle.THUMBNAIL, _results, null, Settings.instance.channel.progressBar, viewsToPrepend = arrayListOf(searchView)).apply {
|
||||||
this.onContentUrlClicked.subscribe(this@ChannelContentsFragment.onContentUrlClicked::emit);
|
this.onContentUrlClicked.subscribe(this@ChannelContentsFragment.onContentUrlClicked::emit);
|
||||||
this.onUrlClicked.subscribe(this@ChannelContentsFragment.onUrlClicked::emit);
|
this.onUrlClicked.subscribe(this@ChannelContentsFragment.onUrlClicked::emit);
|
||||||
this.onContentClicked.subscribe(this@ChannelContentsFragment.onContentClicked::emit);
|
this.onContentClicked.subscribe(this@ChannelContentsFragment.onContentClicked::emit);
|
||||||
@@ -163,11 +219,13 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||||||
this.onLongPress.subscribe(this@ChannelContentsFragment.onLongPress::emit);
|
this.onLongPress.subscribe(this@ChannelContentsFragment.onLongPress::emit);
|
||||||
}
|
}
|
||||||
|
|
||||||
_llmVideo = LinearLayoutManager(view.context);
|
val numColumns = max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
|
||||||
|
_glmVideo = GridLayoutManager(view.context, numColumns);
|
||||||
_recyclerResults?.adapter = _adapterResults;
|
_recyclerResults?.adapter = _adapterResults;
|
||||||
_recyclerResults?.layoutManager = _llmVideo;
|
_recyclerResults?.layoutManager = _glmVideo;
|
||||||
_recyclerResults?.addOnScrollListener(_scrollListener);
|
_recyclerResults?.addOnScrollListener(_scrollListener);
|
||||||
|
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,11 +234,20 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||||||
_recyclerResults?.removeOnScrollListener(_scrollListener);
|
_recyclerResults?.removeOnScrollListener(_scrollListener);
|
||||||
_recyclerResults = null;
|
_recyclerResults = null;
|
||||||
_pager = null;
|
_pager = null;
|
||||||
|
_query = null
|
||||||
|
_searchView = null
|
||||||
|
|
||||||
_taskLoadVideos.cancel();
|
_taskLoadVideos.cancel();
|
||||||
_nextPageHandler.cancel();
|
_nextPageHandler.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
|
||||||
|
_glmVideo?.spanCount =
|
||||||
|
max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
private fun setPager(pager: IPager<IPlatformContent>, cache: FeedFragment.ItemCache<IPlatformContent>? = null) {
|
private fun setPager(pager: IPager<IPlatformContent>, cache: FeedFragment.ItemCache<IPlatformContent>? = null) {
|
||||||
if (_pager_parent != null && _pager_parent is IRefreshPager<*>) {
|
if (_pager_parent != null && _pager_parent is IRefreshPager<*>) {
|
||||||
@@ -291,6 +358,7 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun loadInitial() {
|
private fun loadInitial() {
|
||||||
|
Logger.i(TAG, "loadInitial")
|
||||||
val channel: IPlatformChannel = _channel ?: return;
|
val channel: IPlatformChannel = _channel ?: return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
_taskLoadVideos.run(channel);
|
_taskLoadVideos.run(channel);
|
||||||
@@ -358,6 +426,6 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TAG = "VideoListFragment";
|
val TAG = "VideoListFragment";
|
||||||
fun newInstance() = ChannelContentsFragment().apply { }
|
fun newInstance(subType: String? = null) = ChannelContentsFragment(subType).apply { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -16,12 +16,12 @@ import com.futo.platformplayer.constructs.Event1
|
|||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.resolveChannelUrl
|
import com.futo.platformplayer.resolveChannelUrl
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
import com.futo.platformplayer.views.adapters.viewholders.CreatorViewHolder
|
import com.futo.platformplayer.views.adapters.viewholders.CreatorViewHolder
|
||||||
|
import com.futo.polycentric.core.PolycentricProfile
|
||||||
|
|
||||||
class ChannelListFragment : Fragment, IChannelTabFragment {
|
class ChannelListFragment : Fragment, IChannelTabFragment {
|
||||||
private var _channels: ArrayList<IPlatformChannel> = arrayListOf();
|
private var _channels: ArrayList<IPlatformChannel> = arrayListOf();
|
||||||
|
|||||||
+1
-1
@@ -8,8 +8,8 @@ import android.widget.TextView
|
|||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
|
||||||
import com.futo.platformplayer.views.SupportView
|
import com.futo.platformplayer.views.SupportView
|
||||||
|
import com.futo.polycentric.core.PolycentricProfile
|
||||||
|
|
||||||
|
|
||||||
class ChannelMonetizationFragment : Fragment, IChannelTabFragment {
|
class ChannelMonetizationFragment : Fragment, IChannelTabFragment {
|
||||||
|
|||||||
+15
-5
@@ -1,12 +1,13 @@
|
|||||||
package com.futo.platformplayer.fragment.channel.tab
|
package com.futo.platformplayer.fragment.channel.tab
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
@@ -36,10 +37,11 @@ import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
|||||||
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
|
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
|
class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
|
||||||
private var _recyclerResults: RecyclerView? = null
|
private var _recyclerResults: RecyclerView? = null
|
||||||
private var _llmPlaylist: LinearLayoutManager? = null
|
private var _glmPlaylist: GridLayoutManager? = null
|
||||||
private var _loading = false
|
private var _loading = false
|
||||||
private var _pagerParent: IPager<IPlatformPlaylist>? = null
|
private var _pagerParent: IPager<IPlatformPlaylist>? = null
|
||||||
private var _pager: IPager<IPlatformPlaylist>? = null
|
private var _pager: IPager<IPlatformPlaylist>? = null
|
||||||
@@ -109,7 +111,7 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
|
|||||||
super.onScrolled(recyclerView, dx, dy)
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
|
|
||||||
val recyclerResults = _recyclerResults ?: return
|
val recyclerResults = _recyclerResults ?: return
|
||||||
val llmPlaylist = _llmPlaylist ?: return
|
val llmPlaylist = _glmPlaylist ?: return
|
||||||
|
|
||||||
val visibleItemCount = recyclerResults.childCount
|
val visibleItemCount = recyclerResults.childCount
|
||||||
val firstVisibleItem = llmPlaylist.findFirstVisibleItemPosition()
|
val firstVisibleItem = llmPlaylist.findFirstVisibleItemPosition()
|
||||||
@@ -158,9 +160,10 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
|
|||||||
this.onLongPress.subscribe(this@ChannelPlaylistsFragment.onLongPress::emit)
|
this.onLongPress.subscribe(this@ChannelPlaylistsFragment.onLongPress::emit)
|
||||||
}
|
}
|
||||||
|
|
||||||
_llmPlaylist = LinearLayoutManager(view.context)
|
val numColumns = max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
|
||||||
|
_glmPlaylist = GridLayoutManager(view.context, numColumns)
|
||||||
_recyclerResults?.adapter = _adapterResults
|
_recyclerResults?.adapter = _adapterResults
|
||||||
_recyclerResults?.layoutManager = _llmPlaylist
|
_recyclerResults?.layoutManager = _glmPlaylist
|
||||||
_recyclerResults?.addOnScrollListener(_scrollListener)
|
_recyclerResults?.addOnScrollListener(_scrollListener)
|
||||||
|
|
||||||
return view
|
return view
|
||||||
@@ -176,6 +179,13 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
|
|||||||
_nextPageHandler.cancel()
|
_nextPageHandler.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
|
||||||
|
_glmPlaylist?.spanCount =
|
||||||
|
max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
private fun setPager(
|
private fun setPager(
|
||||||
pager: IPager<IPlatformPlaylist>
|
pager: IPager<IPlatformPlaylist>
|
||||||
) {
|
) {
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
package com.futo.platformplayer.fragment.channel.tab
|
package com.futo.platformplayer.fragment.channel.tab
|
||||||
|
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
import com.futo.polycentric.core.PolycentricProfile
|
||||||
|
|
||||||
interface IChannelTabFragment {
|
interface IChannelTabFragment {
|
||||||
fun setChannel(channel: IPlatformChannel)
|
fun setChannel(channel: IPlatformChannel)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user