1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2026-01-14 02:32:40 +00:00

Compare commits

..

265 Commits

Author SHA1 Message Date
Christian Schabesberger
c29ea4693d Merge pull request #2617 from TeamNewPipe/hotfix_stream_fmt
add fixed extractor and move on to version 0.17.2
2019-09-11 22:36:17 +02:00
Christian Schabesberger
7d0df7aa80 add fixed extractor and move on to version 0.17.2 2019-09-11 22:30:41 +02:00
Christian Schabesberger
7ba71e3b37 use newerextractor 2019-08-22 11:39:32 +02:00
Christian Schabesberger
670a95a01d Merge pull request #2530 from TeamNewPipe/release_v0.17.1
Release v0.17.1
2019-08-22 11:21:06 +02:00
Tobias Groza
acea26717c Fix another %s in translation 2019-08-21 14:35:39 +02:00
TobiGr
e6bcb4628a Fix Portuguese minimize_on_exit_summary
Fix #2522
2019-08-21 14:35:39 +02:00
TobiGr
c4f08d541d Add changelog for 0.17.1 (760) 2019-08-21 14:35:39 +02:00
TobiGr
58546751dd Merge remote-tracking branch 'Weblate/dev' into dev 2019-08-21 14:35:31 +02:00
Hosted Weblate
5470c9a002 Merge branch 'origin/dev' into Weblate. 2019-08-19 20:23:29 +02:00
Xiang Xu
8885b45259 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (471 of 471 strings)
2019-08-19 20:23:28 +02:00
Westminboy
85632b24fc Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (471 of 471 strings)
2019-08-19 20:23:27 +02:00
Yaron Shahrabani
96802c7b5c Translated using Weblate (Hebrew)
Currently translated at 100.0% (471 of 471 strings)
2019-08-19 20:23:27 +02:00
ssantos
e5207f8b42 Translated using Weblate (Portuguese)
Currently translated at 100.0% (471 of 471 strings)
2019-08-19 20:23:25 +02:00
naofum
9d573e1b1d Translated using Weblate (Japanese)
Currently translated at 100.0% (471 of 471 strings)
2019-08-19 20:23:24 +02:00
Yaron Shahrabani
dd276aabc1 Translated using Weblate (English)
Currently translated at 100.0% (471 of 471 strings)
2019-08-19 20:23:22 +02:00
Eduardo Caron
e4d0635ae1 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (471 of 471 strings)
2019-08-19 20:23:21 +02:00
TobiGr
60f534d7a1 Merge remote-tracking branch 'Weblate/dev' into dev 2019-08-18 11:30:04 +02:00
Allan Nordhøy
223ddaa9bf Translated using Weblate (Norwegian Bokmål)
Currently translated at 96.8% (456 of 471 strings)
2019-08-18 07:09:54 +02:00
Westminboy
1d6c722c28 Translated using Weblate (Chinese (Simplified))
Currently translated at 99.8% (470 of 471 strings)
2019-08-18 07:09:53 +02:00
Florian
9c9dd6c7bf Translated using Weblate (French)
Currently translated at 98.3% (463 of 471 strings)
2019-08-18 07:09:51 +02:00
zmni
7ff48a6d84 Translated using Weblate (Indonesian)
Currently translated at 99.2% (467 of 471 strings)
2019-08-18 07:09:50 +02:00
thami simo
645e16fd90 Translated using Weblate (Arabic)
Currently translated at 99.6% (469 of 471 strings)
2019-08-18 07:09:49 +02:00
Trần Lê Nhật Huy
1bb58a10e2 Translated using Weblate (Vietnamese)
Currently translated at 99.6% (469 of 471 strings)
2019-08-18 07:09:47 +02:00
ΜŜβ
118788436e Translated using Weblate (Punjabi)
Currently translated at 99.6% (469 of 471 strings)
2019-08-18 07:09:45 +02:00
Marc Riera
0b0f7919a2 Translated using Weblate (Catalan)
Currently translated at 95.8% (451 of 471 strings)
2019-08-18 07:09:12 +02:00
uievawkejf
c8e23fb6ce Translated using Weblate (Ukrainian)
Currently translated at 99.4% (468 of 471 strings)
2019-08-18 07:09:11 +02:00
Osoitz
bc10717f61 Translated using Weblate (Basque)
Currently translated at 99.4% (468 of 471 strings)
2019-08-18 07:09:10 +02:00
Westminboy
e621dd3b28 Translated using Weblate (English)
Currently translated at 100.0% (471 of 471 strings)
2019-08-18 07:09:08 +02:00
Tobias Groza
20208be556 Translated using Weblate (German)
Currently translated at 99.4% (468 of 471 strings)
2019-08-18 07:09:06 +02:00
Kowith Singkornkeeree
9074733aab Translated using Weblate (Thai)
Currently translated at 54.8% (258 of 471 strings)
2019-08-18 07:09:06 +02:00
Westminboy
ae0ee61e7d Translated using Weblate (English)
Currently translated at 100.0% (471 of 471 strings)
2019-08-18 07:09:03 +02:00
Allan Nordhøy
ac797196f5 Translated using Weblate (English)
Currently translated at 100.0% (471 of 471 strings)
2019-08-18 07:09:03 +02:00
Christian Schabesberger
30aa64e9c6 Merge branch 'dev' of github.com:teamnewpipe/newpipe into dev 2019-08-18 01:01:43 +02:00
Christian Schabesberger
b992add77b move on to version 0.17.1 2019-08-18 01:00:54 +02:00
TobiGr
88ebd963f7 Merge remote-tracking branch 'Weblate/dev' into dev 2019-08-18 00:51:52 +02:00
Christian Schabesberger
96baa2978d Merge pull request #2521 from kapodamy/saf-workarround
Downloads: add switch for saf/legacy file picker
2019-08-18 00:42:59 +02:00
Christian Schabesberger
c01609b858 Merge branch 'dev' into saf-workarround 2019-08-18 00:40:43 +02:00
Christian Schabesberger
c89f0e5547 Merge pull request #2527 from Stypox/recaptcha-with-url
Fix ReCaptchaActivity
2019-08-18 00:39:30 +02:00
kapodamy
10dfcbf0b9 add manual switch in download setting fragment
switch for:
* Java I/O Api
* Storage Access Framework
2019-08-17 13:38:33 -03:00
Stypox
43446d56c5 Load the url from the exception in the ReCaptchaActivity
Sometimes YouTube introduces recaptchas only on some pages. By loading the url inside the ReCaptchaException into ReCaptchaActivity's webview, the page that originally caused the problem is shown. The user can then solve the page-specific recaptcha.
2019-08-17 09:33:51 +02:00
Stypox
e66f2ab36b Fix ReCaptchaActivity layout crash due to hidden EditText
Closes #2484
2019-08-17 09:33:20 +02:00
Stypox
63def07a0e Add url to recaptcha exceptions, after update in extractor 2019-08-17 09:25:49 +02:00
kapodamy
1ba7710af8 Merge remote-tracking branch 'origin/saf-workarround' into saf-workarround 2019-08-15 21:49:21 -03:00
kapodamy
8f13a7ec97 check if the if the content provider is disabled (the app itself) 2019-08-15 21:48:07 -03:00
Christian Schabesberger
93dff5cf7a Merge branch 'dev' into saf-workarround 2019-08-15 14:37:19 +02:00
Christian Schabesberger
6a0450b9f6 Merge pull request #2516 from rogersachan/patch-1
Add GitHub sponsors button to feature liberapay
2019-08-15 14:36:58 +02:00
nautilusx
fffeadd8ea Translated using Weblate (German)
Currently translated at 100.0% (471 of 471 strings)
2019-08-15 09:51:19 +02:00
Hosted Weblate
b697e058d9 Merge branch 'origin/dev' into Weblate. 2019-08-15 06:23:56 +02:00
thami simo
14db8b1283 Translated using Weblate (Arabic)
Currently translated at 100.0% (451 of 451 strings)
2019-08-15 06:23:54 +02:00
Kowith Singkornkeeree
45ad8621cf Translated using Weblate (Thai)
Currently translated at 59.4% (268 of 451 strings)
2019-08-15 06:23:54 +02:00
kapodamy
dee3a18ea8 misc changes
* restore permission request popup previously removed in #2486
* use legacy file picker in cases where saf file picker is not available
* fix missing file check logic in prepareSelectedDownload method (DownloadDialog.java)
2019-08-15 01:09:36 -03:00
kapodamy
950cf714d9 use legacy file picker in those cases where saf is not available 2019-08-14 22:15:42 -03:00
kapodamy
652184506b check for Storage Access Framework features
* creating files though saf
* picking folder though saf
2019-08-14 21:54:26 -03:00
Roger
6457cac797 Merge branch 'dev' into patch-1 2019-08-14 11:42:46 -04:00
Christian Schabesberger
f66c2ba171 Merge pull request #2518 from Stypox/playlist-play-from-here
Customize "start here" actions in playlist item views
2019-08-14 15:01:37 +02:00
Christian Schabesberger
6133c97f45 Merge branch 'dev' into playlist-play-from-here 2019-08-14 14:53:06 +02:00
Christian Schabesberger
0dc71ce37a Merge pull request #2517 from Stypox/list-slowdown-fix
List slowdown fix
2019-08-14 14:51:18 +02:00
Stypox
c96a05a8f9 Customize "start here" actions in playlist item views
Now those actions start playing the whole playlist from the chosen stream, instead of playing only the chosen stream.
2019-08-14 14:17:05 +02:00
Stypox
c190dc4792 Fix annotation warnings in modified files 2019-08-14 13:11:44 +02:00
Stypox
ebf91d27c7 Clean up code for addItems() on ItemListAdapters. 2019-08-14 13:11:16 +02:00
Stypox
63301ee771 Remove "Trollolo" logs
They were probably left behind by accident after a debugging session.
2019-08-14 12:57:04 +02:00
Stypox
7da827a06a Fix annotation warnings 2019-08-14 12:55:17 +02:00
Stypox
00fc5217f5 Fix potential disposable leak in PlaylistAppendDialog 2019-08-14 12:54:17 +02:00
Stypox
04e725bb50 Fix some inspection warnings in modified files 2019-08-14 11:49:37 +02:00
Stypox
e6617ff8e8 Fix slowdowns in stream list views
Now the playback state of a stream is loaded only when needed (i.e. when the stream is visible), just as it is done with thumbnails.
Removed `StateObjectsListAdapter.java`, which used to load the state of every stream at list instantiation, generating slowdowns and freezes.
2019-08-14 11:42:39 +02:00
Roger
1b0a958436 Create FUNDING.yml 2019-08-13 22:10:04 -04:00
TobiGr
5053d470f6 Do not save playback position when watch history is disabled 2019-08-13 14:39:57 +02:00
TobiGr
8de5c53485 Fix typo in HistorySettinsFragment
Rename viewsHistroyClearKey to viewsHistoryClearKey
2019-08-13 14:25:47 +02:00
TobiGr
ec3ae7c7b8 Clean up string resources 2019-08-12 17:35:36 +02:00
TobiGr
c46af7d194 Merge branch 'weblate' into dev 2019-08-12 17:25:10 +02:00
Kowith Singkornkeeree
5254e85840 Added translation using Weblate (Thai) 2019-08-12 15:02:32 +02:00
Tobias Groza
5883f6e763 Merge pull request #2487 from kapodamy/buttons-hiding-fix-on-screen-off
fixup for #2149 (missing buttons)
2019-08-12 14:16:03 +02:00
Tobias Groza
c02383d7d9 Merge branch 'dev' into buttons-hiding-fix-on-screen-off 2019-08-12 13:57:02 +02:00
Christian Schabesberger
f98e5cc22d Merge pull request #2502 from Stypox/fix-player-resume
Fix player resume
2019-08-12 10:47:21 +02:00
Christian Schabesberger
9fbb61a744 make fastlane description better suted for fdroid 2019-08-11 23:18:56 +02:00
Stypox
5191907af0 Fix player resume 2019-08-11 22:10:05 +02:00
kapodamy
35a69b4b1d update download_menu.xml
use "ifRoom" and "always" in cases where is possible
2019-08-10 15:56:59 -03:00
yausername
a64f520644 fix item addition to list 2019-08-07 22:27:58 +05:30
yausername
5aced46345 remove controls animation/flicker 2019-08-07 22:27:58 +05:30
TobiGr
3cd485069d Fix playback position not being deleted on clearing watch history 2019-08-07 14:34:49 +02:00
Christian Schabesberger
fabb07bb28 fix import settings not working 2019-08-04 17:27:56 +02:00
kapodamy
2328ea6d07 dont hardcode the buttons 2019-08-03 12:44:55 -03:00
kapodamy
0375194e7d fix start/pause buttons disappear when screen goes off
* fix start/pause buttons disappear, issued by RecyclerView re-draw
* show start/pause buttons in pair to avoid confusions
2019-08-03 12:28:58 -03:00
kapodamy
5a6a6bcc78 clean-up: remove unused method 2019-08-03 12:28:58 -03:00
TobiGr
c8f475bba1 Add changelog for 0.17.0 2019-08-03 00:35:45 +02:00
TobiGr
31f3757880 Move on to version 0.17.0 (750) 2019-08-03 00:35:45 +02:00
Igor Nedoboy
a60a9bb144 Translated using Weblate (Russian)
Currently translated at 100.0% (451 of 451 strings)
2019-08-02 23:07:42 +02:00
Tobias Groza
21a90bb7ee Merge pull request #2486 from kapodamy/32k-issue-fix
Fix slow download speed
2019-08-02 22:23:34 +02:00
kapodamy
2f66913813 drop unused popup storage permission request 2019-08-02 01:07:37 -03:00
kapodamy
d9b042d9e3 socket leak fix
* fix socket leak in "DownloadRunnable"
* in "DownloadInitializer" close the HTTP body after doing range-request checks
* in "DownloadRunnableFallback" fix typo in comment
* in "DownloadDialog" fix regression, using one thread for audios instead of subtitles
2019-08-01 22:41:09 -03:00
Marc Riera
ef9044d933 Translated using Weblate (Catalan)
Currently translated at 98.4% (444 of 451 strings)
2019-08-01 19:47:13 +02:00
Bas Conrads
12c9dbf1bf Translated using Weblate (Esperanto)
Currently translated at 18.6% (84 of 451 strings)
2019-08-01 19:47:12 +02:00
Nenad
8cc8aa8693 Translated using Weblate (Serbian)
Currently translated at 50.1% (226 of 451 strings)
2019-08-01 19:47:09 +02:00
TobiGr
e529b16956 Merge branch 'weblate' into release_v0.17.0 2019-08-01 01:44:25 +02:00
TobiGr
ffe8d4b689 Update extractor version to fix video duration parsing failure 2019-07-31 18:35:46 +02:00
Tobias Groza
4e5a20ec45 Merge pull request #2368 from Stypox/menu-consistency
Make long-press menu consistent across views
2019-07-31 13:37:49 +02:00
Stypox
7c9ef58acd Fix crash when closing a not-yet-loaded popup. 2019-07-25 12:32:56 +02:00
Stypox
d076fe72cd Optimize imports in edited files 2019-07-25 11:47:38 +02:00
Stypox
25fbbfaf94 Rename action to defaultAction in StreamDialogEntry
To improve readability
2019-07-25 01:07:51 +02:00
Stypox
9df27f43de Ensure default actions cannot be overwritten permanently in StreamDialogEntry 2019-07-25 00:53:13 +02:00
Stypox
759e9846ad Remove ugly if-else-cascade in
Common actions and labels are now in a unique enum: StreamDialogEntry
If an action is not common to every long-press menu (e.g. delete) a custom action has to be provided using e.g. delete.setAction(...)
2019-07-25 00:44:12 +02:00
Stypox
3aeba7ca8a Merge branch 'dev' into menu-consistency 2019-07-24 17:21:45 +02:00
hatsunearu
2a44a091c8 Translated using Weblate (Korean)
Currently translated at 100.0% (451 of 451 strings)
2019-07-23 03:18:37 +02:00
JS Ahn
4c92aebc3c Translated using Weblate (Korean)
Currently translated at 100.0% (451 of 451 strings)
2019-07-23 03:16:54 +02:00
JS Ahn
d4ecd0dfab Translated using Weblate (Korean)
Currently translated at 100.0% (451 of 451 strings)
2019-07-23 03:15:50 +02:00
bluepencil
3f790d01fa Translated using Weblate (Korean)
Currently translated at 100.0% (451 of 451 strings)
2019-07-23 03:15:49 +02:00
Christian Schabesberger
e7b068ed8e Merge pull request #2203 from yausername/defaultTrending
added default kiosk
2019-07-22 23:31:14 +02:00
Christian Schabesberger
bd485937c4 Merge branch 'dev' into defaultTrending 2019-07-22 22:39:47 +02:00
Stypox
8edc332a4e Fix showing popup options with audio-only streams 2019-07-22 11:58:01 +02:00
Stypox
bb5028364b Complete merge after #2288: add resumePlayback to player calls.
`resumePlayback`'s value is `false` when the video is enqueued, `true` otherwise.
Also make the use of getContext() and getActivity() more consistant.
2019-07-22 10:28:53 +02:00
Stypox
ef070a4e0e Merge branch 'dev' into menu-consistency 2019-07-22 09:10:25 +02:00
Christian Schabesberger
6787d0224c Merge pull request #2453 from m0n1ker/issue-2240
Update play queue metadata
2019-07-21 16:14:02 +02:00
Christian Schabesberger
8a43e24095 Merge branch 'dev' into issue-2240 2019-07-21 16:07:32 +02:00
Christian Schabesberger
f879f549e4 Merge pull request #2440 from kapodamy/dl-bux-fix
fix downloads stuck at 99.9%
2019-07-21 16:04:52 +02:00
Christian Schabesberger
db55484163 Merge branch 'dev' into dl-bux-fix 2019-07-21 15:38:39 +02:00
Christian Schabesberger
4d8f66f28e Merge pull request #2444 from moneytoo/rotate
Handle (auto)rotation changes during activity lifecycle
2019-07-21 15:37:55 +02:00
Christian Schabesberger
7a44061fa3 Merge branch 'dev' into rotate 2019-07-21 15:12:41 +02:00
Christian Schabesberger
5d4bb42e39 Merge branch 'dev' into dl-bux-fix 2019-07-21 15:10:57 +02:00
Christian Schabesberger
3a6c22da5c update to latest dev extractor 2019-07-21 15:08:17 +02:00
Stypox
064f0e414a Merge branch 'dev' into menu-consistency 2019-07-21 11:11:06 +02:00
Mostafa Ahangarha
77db3cb6fa Translated using Weblate (Persian)
Currently translated at 70.7% (319 of 451 strings)
2019-07-18 18:05:12 +02:00
bluepencil
b83a1fd102 Translated using Weblate (Korean)
Currently translated at 100.0% (451 of 451 strings)
2019-07-16 08:03:42 +02:00
Igor Nedoboy
99c519c065 Translated using Weblate (Russian)
Currently translated at 100.0% (451 of 451 strings)
2019-07-13 21:00:45 +02:00
Allan Nordhøy
632e52b38d Translated using Weblate (Norwegian Bokmål)
Currently translated at 97.6% (440 of 451 strings)
2019-07-11 21:01:06 +02:00
Syver Stensholt
1b66ffac6c Translated using Weblate (Norwegian Bokmål)
Currently translated at 97.6% (440 of 451 strings)
2019-07-11 21:01:04 +02:00
Alan Nelson
ee9052ad3d Add title to additional metadata object 2019-07-11 00:48:28 -05:00
John Doe
550c74da77 Translated using Weblate (Croatian)
Currently translated at 100.0% (451 of 451 strings)
2019-07-10 13:01:11 +02:00
Alan Nelson
ccdd450283 Add current and total track numbers to metadata 2019-07-09 22:37:03 -05:00
Alan Nelson
224a607bc3 Fix Bluetooth AVRCP duration metadata 2019-07-09 22:34:18 -05:00
random r
8fcd23663c Translated using Weblate (Italian)
Currently translated at 100.0% (451 of 451 strings)
2019-07-09 11:00:59 +02:00
Tobias Groza
ad79a71fbd Merge pull request #2423 from Redirion/patch-2
Silence CheckForNewAppVersionTask
2019-07-07 21:08:01 +02:00
Tobias Groza
d862a59349 Merge branch 'dev' into patch-2 2019-07-07 20:39:26 +02:00
D D
2d6362dddb Translated using Weblate (Bulgarian)
Currently translated at 85.4% (385 of 451 strings)
2019-07-06 21:01:14 +02:00
Flo - Fan
ee3ec3a4ea Translated using Weblate (French)
Currently translated at 100.0% (451 of 451 strings)
2019-07-05 17:01:08 +02:00
THANOS SIOURDAKIS
daecfd97c2 Translated using Weblate (Greek)
Currently translated at 100.0% (451 of 451 strings)
2019-07-05 17:01:07 +02:00
Khaleel Jageer
200a81d536 Translated using Weblate (Tamil)
Currently translated at 33.7% (152 of 451 strings)
2019-07-05 17:01:07 +02:00
Marcel Dopita
8059ac89d3 Handle (auto)rotation changes during activity lifecycle
Fixes #1156
2019-07-04 07:30:01 +02:00
kapodamy
60f5f07dd6 commit (3 changes)
* re-write download segmenting logic (issue #).
* clean-up download threads handling.
* fix race-condition if "pause" option in download context menu was selected, in the transition from "pending" to "finished" state.
2019-07-02 21:07:21 -03:00
Osoitz
372d5ce413 Translated using Weblate (Basque)
Currently translated at 100.0% (451 of 451 strings)
2019-07-02 12:01:06 +02:00
Mehmetali
6f97819ca7 Translated using Weblate (Turkish)
Currently translated at 100.0% (451 of 451 strings)
2019-07-02 12:01:03 +02:00
JoC
2b2ee56712 Translated using Weblate (Spanish)
Currently translated at 100.0% (451 of 451 strings)
2019-06-28 10:01:02 +02:00
thami simo
3715326034 Translated using Weblate (Arabic)
Currently translated at 100.0% (451 of 451 strings)
2019-06-28 10:01:00 +02:00
Cenk YILDIZLI
6cbb8b1753 Translated using Weblate (Turkish)
Currently translated at 99.1% (447 of 451 strings)
2019-06-28 10:00:57 +02:00
Christian Schabesberger
806896ea05 Merge pull request #2295 from sherlockbeard/removeextra
removed the gema strings.
2019-06-27 14:29:42 +02:00
Redirion
fc8746e077 Update CheckForNewAppVersionTask.java 2019-06-26 18:37:04 +02:00
Christian Schabesberger
e11df5bb49 Merge branch 'dev' into removeextra 2019-06-26 15:42:47 +02:00
Redirion
37a9e98ebc Update CheckForNewAppVersionTask.java 2019-06-25 13:53:23 +02:00
Redirion
80b4975188 Update CheckForNewAppVersionTask.java 2019-06-25 13:47:16 +02:00
Redirion
c4ef40f4dc Removed tabs 2019-06-25 13:41:08 +02:00
Redirion
6a4bb6e3e1 Update CheckForNewAppVersionTask.java 2019-06-25 13:39:47 +02:00
Redirion
05ef926a7f Update CheckForNewAppVersionTask.java 2019-06-25 13:31:26 +02:00
Redirion
0007451735 Update CheckForNewAppVersionTask.java 2019-06-25 13:22:40 +02:00
Redirion
e599de038a Silence CheckForNewAppVersionTask
Closes #2421
2019-06-25 11:49:59 +02:00
Tobias Groza
61472a995f Merge pull request #2288 from nv95/playback_state_list
Playback positions in lists
2019-06-23 19:44:13 +02:00
Vasily
2a41802f36 Merge branch 'dev' into playback_state_list 2019-06-23 20:23:29 +03:00
gkhnblt
1d1cee17c3 Translated using Weblate (Turkish)
Currently translated at 98.0% (442 of 451 strings)
2019-06-23 19:00:19 +02:00
Michael Moroni
e99266f9d8 Translated using Weblate (Italian)
Currently translated at 100.0% (451 of 451 strings)
2019-06-21 14:00:36 +02:00
Eduardo Caron
1f2cd064f7 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (451 of 451 strings)
2019-06-21 14:00:35 +02:00
minsk21
9c2cf9eef7 Translated using Weblate (Belarusian)
Currently translated at 73.4% (331 of 451 strings)
2019-06-18 21:02:47 +02:00
Tobias Groza
38b0b79644 Merge pull request #2149 from kapodamy/ps-branch
Downloader fixes
2019-06-17 22:53:11 +02:00
JoC
5252834075 Translated using Weblate (Spanish)
Currently translated at 100.0% (451 of 451 strings)
2019-06-15 19:53:20 +02:00
kapodamy
162df5eb6c Merge branch 'dev' into ps-branch 2019-06-14 12:55:49 -03:00
kapodamy
ac5e2e0532 bugs fixes
* fix storage warning dialogs created on invalid contexts
* implement mkdirs in StoredDirectoryHelper
2019-06-14 12:19:50 -03:00
Stypox
f0ba6afbdf Merge branch 'dev' into menu-consistency 2019-06-14 09:40:40 +02:00
Hosted Weblate
04a5f43472 Merge branch 'origin/dev' into Weblate. 2019-06-13 14:30:35 +02:00
Allan Nordhøy
a15ef4b7ce Translated using Weblate (Norwegian Bokmål)
Currently translated at 94.9% (428 of 451 strings)
2019-06-13 14:30:34 +02:00
Jeff Huang
f1f9147433 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (451 of 451 strings)
2019-06-13 14:30:33 +02:00
Nathan Follens
3c0d7de377 Translated using Weblate (Flemish)
Currently translated at 100.0% (451 of 451 strings)
2019-06-13 14:30:31 +02:00
Arnaud Jacquemin
2a57d74f1a Translated using Weblate (French)
Currently translated at 100.0% (451 of 451 strings)
2019-06-13 14:30:29 +02:00
Florian
79717859b3 Translated using Weblate (French)
Currently translated at 100.0% (451 of 451 strings)
2019-06-13 14:30:28 +02:00
Prashant Shahi
be423939ed Translated using Weblate (Nepali)
Currently translated at 6.0% (27 of 451 strings)
2019-06-13 14:30:27 +02:00
Nathan Follens
086cceb271 Translated using Weblate (Dutch)
Currently translated at 100.0% (451 of 451 strings)
2019-06-13 14:30:21 +02:00
zmni
a14033afb7 Translated using Weblate (Indonesian)
Currently translated at 99.8% (450 of 451 strings)
2019-06-13 14:30:20 +02:00
Rex_sa
cc89a342ed Translated using Weblate (Arabic)
Currently translated at 100.0% (451 of 451 strings)
2019-06-13 14:30:18 +02:00
ButterflyOfFire
25c3669564 Translated using Weblate (Arabic)
Currently translated at 100.0% (451 of 451 strings)
2019-06-13 14:30:17 +02:00
Michael Moroni
21eff0b2ec Translated using Weblate (Italian)
Currently translated at 98.2% (443 of 451 strings)
2019-06-13 14:30:16 +02:00
pjammo
472fd72c82 Translated using Weblate (Italian)
Currently translated at 98.2% (443 of 451 strings)
2019-06-13 14:30:15 +02:00
WaldiS
429a9a42d3 Translated using Weblate (Polish)
Currently translated at 100.0% (451 of 451 strings)
2019-06-13 14:30:13 +02:00
Yaron Shahrabani
90c525e99a Translated using Weblate (Hebrew)
Currently translated at 100.0% (451 of 451 strings)
2019-06-13 14:30:12 +02:00
Stjepan
196117998a Translated using Weblate (Croatian)
Currently translated at 99.3% (448 of 451 strings)
2019-06-13 14:30:10 +02:00
ssantos
630cbc77a8 Translated using Weblate (Portuguese)
Currently translated at 100.0% (451 of 451 strings)
2019-06-13 14:30:09 +02:00
Marc Riera
b6e4afe321 Translated using Weblate (Catalan)
Currently translated at 97.1% (438 of 451 strings)
2019-06-13 14:30:06 +02:00
naofum
59085ff8c8 Translated using Weblate (Japanese)
Currently translated at 100.0% (451 of 451 strings)
2019-06-13 14:30:04 +02:00
AB
a4274c6301 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (451 of 451 strings)
2019-06-13 14:30:02 +02:00
THANOS SIOURDAKIS
b4ef44b343 Translated using Weblate (Greek)
Currently translated at 100.0% (451 of 451 strings)
2019-06-13 14:30:01 +02:00
postsorino
08bc97582b Translated using Weblate (Greek)
Currently translated at 100.0% (451 of 451 strings)
2019-06-13 14:30:00 +02:00
gabriellluz
3952c88510 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (451 of 451 strings)
2019-06-13 14:29:59 +02:00
Lucas Ayala
b1f27b9da7 Translated using Weblate (Spanish)
Currently translated at 100.0% (451 of 451 strings)
2019-06-13 14:29:55 +02:00
Tobias Groza
171b258d5c Merge pull request #2394 from Redirion/patch-1
Fixed selected subtitle track name not being shown
2019-06-12 22:50:34 +02:00
Redirion
af971b6a19 Fixed selected subtitle stream not being shown
closes #2393
this ammends my obviously incomplete fix in PR #2311.

This is just an UI issue. Subtitle track selection works, it just shows "No Captions" unfortunately.
2019-06-12 14:44:36 +02:00
Karel S
7ca026393b Translated using Weblate (Czech)
Currently translated at 100.0% (451 of 451 strings)
2019-06-12 10:52:28 +02:00
Vojtěch Šamla
f8784ae3c7 Translated using Weblate (Czech)
Currently translated at 100.0% (451 of 451 strings)
2019-06-12 10:52:27 +02:00
ssantos
3ddc3acf4c Translated using Weblate (German)
Currently translated at 100.0% (451 of 451 strings)
2019-06-06 08:57:12 +02:00
nautilusx
ff430f5e33 Translated using Weblate (German)
Currently translated at 100.0% (451 of 451 strings)
2019-06-06 08:57:12 +02:00
Prashant Shahi
daf2890161 Added translation using Weblate (Nepali) 2019-06-05 17:16:25 +02:00
Igor Nedoboy
4ca639323d Translated using Weblate (Russian)
Currently translated at 100.0% (451 of 451 strings)
2019-06-05 00:59:48 +02:00
Igor Nedoboy
a92bf155a3 Translated using Weblate (Russian)
Currently translated at 100.0% (451 of 451 strings)
2019-06-05 00:48:25 +02:00
Igor Nedoboy
d153772eb2 Translated using Weblate (Russian)
Currently translated at 100.0% (451 of 451 strings)
2019-06-05 00:37:18 +02:00
kapodamy
cdc8fe86ce amend rebase
resolve inconsistency in string.xml files
2019-06-04 15:45:28 -03:00
Hosted Weblate
4844037ce9 Merge branch 'origin/dev' into Weblate. 2019-06-04 13:40:03 +02:00
Jeff Huang
691c1e1a37 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (443 of 443 strings)
2019-06-04 13:40:03 +02:00
Jazz
bd55b91a86 Translated using Weblate (French)
Currently translated at 99.8% (442 of 443 strings)
2019-06-04 13:40:02 +02:00
Eduardo Serra
8842f53696 Translated using Weblate (Spanish)
Currently translated at 100.0% (443 of 443 strings)
2019-06-04 13:40:02 +02:00
ButterflyOfFire
ffed9f6116 Translated using Weblate (Arabic)
Currently translated at 100.0% (443 of 443 strings)
2019-06-04 13:40:01 +02:00
pjammo
50e8f45601 Translated using Weblate (Italian)
Currently translated at 100.0% (443 of 443 strings)
2019-06-04 13:40:00 +02:00
WaldiS
8cbfe9e6cf Translated using Weblate (Polish)
Currently translated at 100.0% (443 of 443 strings)
2019-06-04 13:40:00 +02:00
Karel S
99ad3dc292 Translated using Weblate (Czech)
Currently translated at 99.8% (442 of 443 strings)
2019-06-04 13:40:00 +02:00
monolifed
6908355d38 Translated using Weblate (Turkish)
Currently translated at 100.0% (443 of 443 strings)
2019-06-04 13:39:56 +02:00
kapodamy
7b948f83c3 Space reserving tweaks for huge video resolutions
* improve space reserving, allows write better 4K/8K video data
* do not use cache dirs in the muxers, Android can force close NewPipe if the device is running out of storage. Is a aggressive cache cleaning >:/
* (for devs) webm & mkv are the same thing
* calculate the final file size inside of the mission, instead getting from the UI
* simplify ps algorithms constructors
* [missing old commit message] simplify the loading of pending downloads
2019-06-03 18:26:26 -03:00
kapodamy
34b2b96158 Simplify the storage APIs use
* use Java I/O (classic way) on older android versions
* use Storage Access Framework on newer android versions (Android Lollipop or later)
* both changes have the external SD Card write permission
* add option to ask the save path on each download
* warn the user if the save paths are not defined, this only happens on the first NewPipe run (Android Lollipop or later)
2019-06-03 18:26:24 -03:00
kapodamy
d1573a0a6e misc changes
* implement socket timeout error
* use 128k buffer size for copy
* use NewPipe HTTP user agent in the downloads
* automatically recover downloads with network errors that are queued
2019-06-03 18:25:43 -03:00
kapodamy
16d6bda85d Webm muxer fixes and strings.xml changes
* replace "In queue" to "Pending" in the downloads header to avoid confusions (all languages)
* use 29bits Clusters size to support huge video resolutions (fixes #2291) (WebmWriter.java)
* add missing changes to WebmMuxer.java (i forget select the audio track)
2019-06-03 18:24:49 -03:00
kapodamy
4b3eb2ece5 Forget the download save path if the storage API is changed 2019-06-03 18:19:20 -03:00
kapodamy
1089de6321 Add confirm dialog before clear the finished download list 2019-06-03 18:19:18 -03:00
kapodamy
d00dc798f4 more SAF implementation
* full support for Directory API (Android Lollipop or later)
* best effort to handle any kind errors (missing file, revoked permissions, etc) and recover the download
* implemented directory choosing
* fix download database version upgrading
* misc. cleanup
* do not release permission on the old save path (if the user change the download directory) under SAF api
2019-06-03 18:18:20 -03:00
kapodamy
f6b32823ba Implement Storage Access Framework
* re-work finished mission database
* re-work DownloadMission and bump it Serializable version
* keep the classic Java IO API
* SAF Tree API support on Android Lollipop or higher
* add wrapper for SAF stream opening
* implement Closeable in SharpStream to replace the dispose() method

* do required changes for this API:
** remove any file creation logic from DownloadInitializer
** make PostProcessing Serializable and reduce the number of iterations
** update all strings.xml files
** storage helpers: StoredDirectoryHelper & StoredFileHelper
** best effort to handle any kind of SAF errors/exceptions
2019-06-03 18:16:41 -03:00
kapodamy
9e34fee58c New MP4 muxer + Queue changes + Storage fixes
Main changes:
* correctly check the available space (CircularFile.java)
* misc cleanup (CircularFile.java)
* use the "Error Reporter" for non-http errors
* rewrite network state checking and add better support for API 21 (Lollipop) or higher
* implement "metered networks"
* add buttons in "Downloads" activity to start/pause all pending downloads, ignoring the queue flag or if the network is "metered"
* add workaround for VPN connections and/or network switching. Example: switching WiFi to 3G
* rewrite DataReader ¡Webm muxer is now 57% more faster!
* rewrite CircularFile, use file buffers instead of memory buffers. Less troubles in low-end devices
* fix missing offset for KaxCluster (WebMWriter.java), manifested as no thumbnails on file explorers

Download queue:
* remember queue status, unless the user pause the download (un-queue)
* semi-automatic downloads, between networks. Effective if the user create a new download or the downloads activity is starts
* allow enqueue failed downloads
* new option, queue limit, enabled by default. Used to allow one or multiple downloads at same time

Miscellaneous:
* fix crash while selecting details/error menu (mistake on MissionFragment.java)
* misc serialize changes (DownloadMission.java)
* minor UI tweaks
* allow overwrite paused downloads
* fix wrong icons for grid/list button in downloads
* add share option
* implement #2006
* correct misspelled word in strings.xml (es) (cmn)
* fix MissionAdapter crash during device shutdown

New Mp4Muxer + required changes:
* new mp4 muxer (from dash only) with this, muxing on Android 7 is possible now!!!
* re-work in SharpStream
* drop mp4 dash muxer
* misc changes: add warning in SecondaryStreamHelper.java,
* strip m4a DASH files to normal m4a format (youtube only)

Fix storage issues:
* warn to the user if is choosing a "read only" download directory (for external SD Cards), useless is rooted :)
* "write proof" allow post-processing resuming only if the device ran out of space
* implement "insufficient storage" error for downloads
2019-06-03 18:09:43 -03:00
Tobias Groza
1684a2110c Update Extractor 2019-06-03 22:06:58 +02:00
Tobias Groza
5e00e34552 Merge remote-tracking branch 'Weblate/dev' into dev 2019-06-03 22:04:36 +02:00
Yaron Shahrabani
ce204eba62 Translated using Weblate (Hebrew)
Currently translated at 100.0% (443 of 443 strings)
2019-06-02 12:48:16 +02:00
Yaron Shahrabani
c7cb652322 Translated using Weblate (Hebrew)
Currently translated at 99.8% (442 of 443 strings)
2019-06-02 12:44:09 +02:00
Yaron Shahrabani
f8ccc3128e Translated using Weblate (Hebrew)
Currently translated at 99.8% (442 of 443 strings)
2019-06-02 12:44:08 +02:00
bob mar
4a8baaef45 Translated using Weblate (Hebrew)
Currently translated at 99.8% (442 of 443 strings)
2019-06-02 12:44:08 +02:00
artik banana
a9f3939c83 Translated using Weblate (Hebrew)
Currently translated at 99.8% (442 of 443 strings)
2019-06-02 12:27:18 +02:00
Joseph Kim
d8cb950248 Translated using Weblate (Korean)
Currently translated at 100.0% (443 of 443 strings)
2019-06-02 12:27:07 +02:00
AB
aefc51db4b Translated using Weblate (Ukrainian)
Currently translated at 100.0% (443 of 443 strings)
2019-06-02 12:27:00 +02:00
artik banana
fb18ea7ff8 Translated using Weblate (Hebrew)
Currently translated at 99.8% (442 of 443 strings)
2019-06-02 12:26:58 +02:00
bob mar
407c61e212 Translated using Weblate (Hebrew)
Currently translated at 99.8% (442 of 443 strings)
2019-06-02 12:26:58 +02:00
ssantos
d8e6ad48ca Translated using Weblate (Portuguese)
Currently translated at 100.0% (443 of 443 strings)
2019-06-02 02:18:34 +02:00
yunna
bd42f4188f Translated using Weblate (Japanese)
Currently translated at 99.8% (442 of 443 strings)
2019-06-02 02:18:34 +02:00
yunna
f766f383ea Translated using Weblate (English)
Currently translated at 100.0% (443 of 443 strings)
2019-06-02 02:18:28 +02:00
ssantos
1a9922d790 Translated using Weblate (German)
Currently translated at 100.0% (443 of 443 strings)
2019-06-02 02:18:24 +02:00
Tobias Groza
6213623431 Merge pull request #2235 from TeamNewPipe/readme-services
Add supported services section to readme
2019-06-02 02:03:14 +02:00
Tobias Groza
2809ee7a3e Merge branch 'dev' into readme-services 2019-06-02 02:02:44 +02:00
Tobias Groza
281cae7a18 Merge branch 'master' into dev 2019-05-31 23:46:28 +02:00
mohammadmdp
e1ead9d2ef Translated using Weblate (Persian)
Currently translated at 58.9% (261 of 443 strings)
2019-05-31 11:24:47 +02:00
Karel S
359a9a96d6 Translated using Weblate (Czech)
Currently translated at 99.8% (442 of 443 strings)
2019-05-31 11:24:44 +02:00
Stypox
b6cfb8a3dc Remove dupliacte direct_on_background string
start_here_on_background has the same meaning

start_here_on_main is now unused, but I left it there so that if it ever becomes useful again, it is ready to be used.
2019-05-30 15:30:13 +02:00
Stypox
6f028ecb19 Remove unused imports from modified files 2019-05-29 20:45:05 +02:00
Stypox
8695466690 Make subscription long-press menu consistant in local sub list
Inverted unsubscribe with share, since share has always been put after content-specific actions.
2019-05-29 20:39:17 +02:00
Stypox
bdb1be9967 Remove useless overrides of showStreamDialog
They were exactly the same as the base class function
2019-05-29 20:25:44 +02:00
Cipisek Rumcajsu
9395df4cc3 Translated using Weblate (Czech)
Currently translated at 90.3% (400 of 443 strings)
2019-05-29 18:08:08 +02:00
Karel S
93edb333d4 Translated using Weblate (Czech)
Currently translated at 90.3% (400 of 443 strings)
2019-05-29 18:08:08 +02:00
Stypox
30eeef46c2 Removed unused showStreamDialog from VideoDetailFragment
VideoDetailFragment already borrows a consistant menu from the stream list it holds.
2019-05-29 16:25:23 +02:00
Stypox
8b584f3922 Make long-press menu consistent across views: fix #2354
Also made the code that creates the menus consistent across files.
2019-05-29 16:22:01 +02:00
sherlockbeard
43b859f778 Merge branch 'dev' into removeextra 2019-05-07 13:59:32 +05:30
sherlockbeard
d1bd7f695f Update strings.xml 2019-05-07 13:57:31 +05:30
Vasiliy
312e1378d3 Fix tablet ui 2019-05-06 19:16:39 +03:00
Vasiliy
93f2518159 Animate states changed 2019-04-27 22:27:08 +03:00
Vasiliy
273f731dd5 Refactor adapter 2019-04-27 21:23:52 +03:00
Vasiliy
c7cd9e86ac Option to disable states indicators 2019-04-27 19:04:13 +03:00
Vasiliy
41fb6f5464 Update states in lists 2019-04-27 18:12:00 +03:00
Vasiliy
03b1a8bd41 Merge branch 'dev' into playback_state_list 2019-04-27 17:37:43 +03:00
sherlock
1edfa78a05 removed the gena strings. 2019-04-17 16:45:40 +05:30
Vasiliy
f47c5e53b1 Merge branch 'playback_resume_v2' into playback_state_list 2019-04-15 22:19:54 +03:00
Vasiliy
a48cbc6971 Show streams states for local lists 2019-04-15 22:18:24 +03:00
Vasiliy
73be8cf074 Base implementation of showing playback positions in lists 2019-04-15 21:37:36 +03:00
Vasiliy
002a1412cb Fix scrolling details 2019-04-15 21:22:31 +03:00
Vasiliy
4e1423d224 Implement playback state management 2019-04-13 13:34:36 +03:00
TobiGr
9bcccc87e6 Update README
Add supported services section
Move comment support
2019-03-26 23:36:12 +01:00
Ritvik Saraf
76f7165462 Merge remote-tracking branch 'upstream/dev' into defaultTrending 2019-03-12 06:17:21 +05:30
Ritvik Saraf
fdf0d8e9c8 fixed memory leak 2019-03-12 06:07:56 +05:30
Ritvik Saraf
369fd95e2b skip tests, fix later 2019-03-11 06:27:18 +05:30
Ritvik Saraf
e242adec66 updated extractor 2019-03-11 03:16:17 +05:30
Ritvik Saraf
58e562f7d4 added default kiosk 2019-03-11 03:08:30 +05:30
203 changed files with 10426 additions and 5993 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
liberapay: TeamNewPipe

View File

@@ -66,15 +66,22 @@ NewPipe does not use any Google framework libraries, nor the YouTube API. Websit
* Enqueue videos
* Local playlists
* Subtitles
* Multi-service support (e.g. SoundCloud \[beta\])
* Livestream support
* Show comments
### Coming Features
* Cast to UPnP and Cast
* Show comments
* … and many more
### Supported Services
NewPipe supports multiple services. Our [docs](https://teamnewpipe.github.io/documentation/) provide more info on how a new service can be added to the app and the extractor. Please get in touch with us if you intend to add a new one. Currently supported services are:
* YouTube
* SoundCloud \[beta\]
* media.ccc.de \[beta\]
## Updates
When a change to the NewPipe code occurs (due to either adding features or bug fixing), eventually a release will occur. These are in the format x.xx.x . In order to get this new version, you can:
* Build a debug APK yourself. This is the fastest way to get new features on your device, but is much more complicated, so we recommend using one of the other methods.

View File

@@ -8,8 +8,8 @@ android {
applicationId "org.schabi.newpipe"
minSdkVersion 19
targetSdkVersion 28
versionCode 740
versionName "0.16.2"
versionCode 770
versionName "0.17.2"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
@@ -57,7 +57,7 @@ dependencies {
exclude module: 'support-annotations'
})
implementation 'com.github.TeamNewPipe:NewPipeExtractor:2ac713e'
implementation 'com.github.teamnewpipe:NewPipeExtractor:ec3554a2ea6aa20'
testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:2.23.0'

View File

@@ -14,6 +14,7 @@ import android.os.AsyncTask;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
@@ -47,6 +48,8 @@ import okhttp3.Response;
*/
public class CheckForNewAppVersionTask extends AsyncTask<Void, Void, String> {
private static final boolean DEBUG = MainActivity.DEBUG;
private static final String TAG = CheckForNewAppVersionTask.class.getSimpleName();
private static final Application app = App.getApp();
private static final String GITHUB_APK_SHA1 = "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15";
private static final String newPipeApiUrl = "https://newpipe.schabi.org/api/data.json";
@@ -90,9 +93,8 @@ public class CheckForNewAppVersionTask extends AsyncTask<Void, Void, String> {
Response response = client.newCall(request).execute();
return response.body().string();
} catch (IOException ex) {
ErrorActivity.reportError(app, ex, null, null,
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
"app update API fail", R.string.app_ui_crash));
// connectivity problems, do not alarm user and fail silently
if (DEBUG) Log.w(TAG, Log.getStackTraceString(ex));
}
return null;
@@ -117,9 +119,8 @@ public class CheckForNewAppVersionTask extends AsyncTask<Void, Void, String> {
compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode);
} catch (JSONException ex) {
ErrorActivity.reportError(app, ex, null, null,
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
"could not parse app update JSON data", R.string.app_ui_crash));
// connectivity problems, do not alarm user and fail silently
if (DEBUG) Log.w(TAG, Log.getStackTraceString(ex));
}
}
}

View File

@@ -164,7 +164,7 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
final ResponseBody body = response.body();
if (response.code() == 429) {
throw new ReCaptchaException("reCaptcha Challenge requested");
throw new ReCaptchaException("reCaptcha Challenge requested", siteUrl);
}
if (body == null) {
@@ -214,7 +214,7 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
final ResponseBody body = response.body();
if (response.code() == 429) {
throw new ReCaptchaException("reCaptcha Challenge requested");
throw new ReCaptchaException("reCaptcha Challenge requested", siteUrl);
}
if (body == null) {
@@ -268,7 +268,7 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
final ResponseBody body = response.body();
if (response.code() == 429) {
throw new ReCaptchaException("reCaptcha Challenge requested");
throw new ReCaptchaException("reCaptcha Challenge requested", siteUrl);
}
if (body == null) {

View File

@@ -37,15 +37,24 @@ import android.webkit.WebViewClient;
*/
public class ReCaptchaActivity extends AppCompatActivity {
public static final int RECAPTCHA_REQUEST = 10;
public static final String RECAPTCHA_URL_EXTRA = "recaptcha_url_extra";
public static final String TAG = ReCaptchaActivity.class.toString();
public static final String YT_URL = "https://www.youtube.com";
private String url;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_recaptcha);
url = getIntent().getStringExtra(RECAPTCHA_URL_EXTRA);
if (url == null || url.isEmpty()) {
url = YT_URL;
}
// Set return to Cancel by default
setResult(RESULT_CANCELED);
@@ -73,15 +82,12 @@ public class ReCaptchaActivity extends AppCompatActivity {
myWebView.clearHistory();
android.webkit.CookieManager cookieManager = CookieManager.getInstance();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
cookieManager.removeAllCookies(new ValueCallback<Boolean>() {
@Override
public void onReceiveValue(Boolean aBoolean) {}
});
cookieManager.removeAllCookies(aBoolean -> {});
} else {
cookieManager.removeAllCookie();
}
myWebView.loadUrl(YT_URL);
myWebView.loadUrl(url);
}
private class ReCaptchaWebViewClient extends WebViewClient {

View File

@@ -74,10 +74,13 @@ import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr;
*/
public class RouterActivity extends AppCompatActivity {
@State protected int currentServiceId = -1;
@State
protected int currentServiceId = -1;
private StreamingService currentService;
@State protected LinkType currentLinkType;
@State protected int selectedRadioPosition = -1;
@State
protected LinkType currentLinkType;
@State
protected int selectedRadioPosition = -1;
protected int selectedPreviously = -1;
protected String currentUrl;
@@ -257,7 +260,7 @@ public class RouterActivity extends AppCompatActivity {
.setNegativeButton(R.string.just_once, dialogButtonsClickListener)
.setPositiveButton(R.string.always, dialogButtonsClickListener)
.setOnDismissListener((dialog) -> {
if(!selectionIsDownload) finish();
if (!selectionIsDownload) finish();
})
.create();
@@ -358,13 +361,13 @@ public class RouterActivity extends AppCompatActivity {
positiveButton.setEnabled(state);
}
private void handleText(){
private void handleText() {
String searchString = getIntent().getStringExtra(Intent.EXTRA_TEXT);
int serviceId = getIntent().getIntExtra(Constants.KEY_SERVICE_ID, 0);
Intent intent = new Intent(getThemeWrapperContext(), MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
NavigationHelper.openSearch(getThemeWrapperContext(),serviceId,searchString);
NavigationHelper.openSearch(getThemeWrapperContext(), serviceId, searchString);
}
private void handleChoice(final String selectedChoiceKey) {
@@ -397,7 +400,7 @@ public class RouterActivity extends AppCompatActivity {
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(intent -> {
if(!internalRoute){
if (!internalRoute) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
}
@@ -447,8 +450,8 @@ public class RouterActivity extends AppCompatActivity {
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
for (int i: grantResults){
if (i == PackageManager.PERMISSION_DENIED){
for (int i : grantResults) {
if (i == PackageManager.PERMISSION_DENIED) {
finish();
return;
}
@@ -460,7 +463,8 @@ public class RouterActivity extends AppCompatActivity {
private static class AdapterChoiceItem {
final String description, key;
@DrawableRes final int icon;
@DrawableRes
final int icon;
AdapterChoiceItem(String key, String description, int icon) {
this.description = description;
@@ -558,7 +562,8 @@ public class RouterActivity extends AppCompatActivity {
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false);
boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false);;
boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false);
;
PlayQueue playQueue;
String playerChoice = choice.playerChoice;
@@ -574,7 +579,7 @@ public class RouterActivity extends AppCompatActivity {
playQueue = new SinglePlayQueue((StreamInfo) info);
if (playerChoice.equals(videoPlayerKey)) {
NavigationHelper.playOnMainPlayer(this, playQueue);
NavigationHelper.playOnMainPlayer(this, playQueue, true);
} else if (playerChoice.equals(backgroundPlayerKey)) {
NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true);
} else if (playerChoice.equals(popupPlayerKey)) {
@@ -587,11 +592,11 @@ public class RouterActivity extends AppCompatActivity {
playQueue = info instanceof ChannelInfo ? new ChannelPlayQueue((ChannelInfo) info) : new PlaylistPlayQueue((PlaylistInfo) info);
if (playerChoice.equals(videoPlayerKey)) {
NavigationHelper.playOnMainPlayer(this, playQueue);
NavigationHelper.playOnMainPlayer(this, playQueue, true);
} else if (playerChoice.equals(backgroundPlayerKey)) {
NavigationHelper.playOnBackgroundPlayer(this, playQueue);
NavigationHelper.playOnBackgroundPlayer(this, playQueue, true);
} else if (playerChoice.equals(popupPlayerKey)) {
NavigationHelper.playOnPopupPlayer(this, playQueue);
NavigationHelper.playOnPopupPlayer(this, playQueue, true);
}
}
};

View File

@@ -3,15 +3,24 @@ package org.schabi.newpipe.database;
import android.arch.persistence.db.SupportSQLiteDatabase;
import android.arch.persistence.room.migration.Migration;
import android.support.annotation.NonNull;
import android.util.Log;
import org.schabi.newpipe.BuildConfig;
public class Migrations {
public static final int DB_VER_11_0 = 1;
public static final int DB_VER_12_0 = 2;
public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
private static final String TAG = Migrations.class.getName();
public static final Migration MIGRATION_11_12 = new Migration(DB_VER_11_0, DB_VER_12_0) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
if(DEBUG) {
Log.d(TAG, "Start migrating database");
}
/*
* Unfortunately these queries must be hardcoded due to the possibility of
* schema and names changing at a later date, thus invalidating the older migration
@@ -56,6 +65,10 @@ public class Migrations {
"ORDER BY creation_date DESC");
database.execSQL("DROP TABLE IF EXISTS watch_history");
if(DEBUG) {
Log.d(TAG, "Stop migrating database");
}
}
};
}

View File

@@ -50,6 +50,11 @@ public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity
" ORDER BY " + STREAM_ACCESS_DATE + " DESC")
public abstract Flowable<List<StreamHistoryEntry>> getHistory();
@Query("SELECT * FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID +
" = :streamId ORDER BY " + STREAM_ACCESS_DATE + " DESC LIMIT 1")
@Nullable
public abstract StreamHistoryEntity getLatestEntry(final long streamId);
@Query("DELETE FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
public abstract int deleteStreamHistory(final long streamId);

View File

@@ -4,6 +4,9 @@ package org.schabi.newpipe.database.stream.model;
import android.arch.persistence.room.ColumnInfo;
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.ForeignKey;
import android.support.annotation.Nullable;
import java.util.concurrent.TimeUnit;
import static android.arch.persistence.room.ForeignKey.CASCADE;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
@@ -22,6 +25,12 @@ public class StreamStateEntity {
final public static String JOIN_STREAM_ID = "stream_id";
final public static String STREAM_PROGRESS_TIME = "progress_time";
/** Playback state will not be saved, if playback time less than this threshold */
private static final int PLAYBACK_SAVE_THRESHOLD_START_SECONDS = 5;
/** Playback state will not be saved, if time left less than this threshold */
private static final int PLAYBACK_SAVE_THRESHOLD_END_SECONDS = 10;
@ColumnInfo(name = JOIN_STREAM_ID)
private long streamUid;
@@ -48,4 +57,18 @@ public class StreamStateEntity {
public void setProgressTime(long progressTime) {
this.progressTime = progressTime;
}
public boolean isValid(int durationInSeconds) {
final int seconds = (int) TimeUnit.MILLISECONDS.toSeconds(progressTime);
return seconds > PLAYBACK_SAVE_THRESHOLD_START_SECONDS
&& seconds < durationInSeconds - PLAYBACK_SAVE_THRESHOLD_END_SECONDS;
}
@Override
public boolean equals(@Nullable Object obj) {
if (obj instanceof StreamStateEntity) {
return ((StreamStateEntity) obj).streamUid == streamUid
&& ((StreamStateEntity) obj).progressTime == progressTime;
} else return false;
}
}

View File

@@ -55,12 +55,13 @@ public class DownloadActivity extends AppCompatActivity {
private void updateFragments() {
MissionsFragment fragment = new MissionsFragment();
getFragmentManager().beginTransaction()
getSupportFragmentManager().beginTransaction()
.replace(R.id.frame, fragment, MISSIONS_FRAGMENT_TAG)
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.commit();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
MenuInflater inflater = getMenuInflater();
@@ -86,9 +87,4 @@ public class DownloadActivity extends AppCompatActivity {
return super.onOptionsItemSelected(item);
}
}
@Override
public void onRestoreInstanceState(Bundle inState){
super.onRestoreInstanceState(inState);
}
}

View File

@@ -1,14 +1,24 @@
package org.schabi.newpipe.download;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.annotation.IdRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.v4.app.DialogFragment;
import android.support.v4.provider.DocumentFile;
import android.support.v7.app.AlertDialog;
import android.support.v7.view.menu.ActionMenuItemView;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.util.SparseArray;
@@ -24,6 +34,8 @@ import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import com.nononsenseapps.filepicker.Utils;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.MediaFormat;
@@ -34,7 +46,10 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.extractor.utils.Localization;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.util.FilePickerActivityHelper;
import org.schabi.newpipe.util.FilenameUtils;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.PermissionHelper;
@@ -43,19 +58,28 @@ import org.schabi.newpipe.util.StreamItemAdapter;
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
import org.schabi.newpipe.util.ThemeHelper;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import icepick.Icepick;
import icepick.State;
import io.reactivex.disposables.CompositeDisposable;
import us.shandian.giga.io.StoredDirectoryHelper;
import us.shandian.giga.io.StoredFileHelper;
import us.shandian.giga.postprocessing.Postprocessing;
import us.shandian.giga.service.DownloadManager;
import us.shandian.giga.service.DownloadManagerService;
import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder;
import us.shandian.giga.service.MissionState;
public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
private static final String TAG = "DialogFragment";
private static final boolean DEBUG = MainActivity.DEBUG;
private static final int REQUEST_DOWNLOAD_SAVE_AS = 0x1230;
@State
protected StreamInfo currentInfo;
@@ -80,7 +104,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
private EditText nameEditText;
private Spinner streamsSpinner;
private RadioGroup radioVideoAudioGroup;
private RadioGroup radioStreamsGroup;
private TextView threadsCountTextView;
private SeekBar threadsSeekBar;
@@ -155,12 +179,15 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
super.onCreate(savedInstanceState);
if (DEBUG)
Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
if (!PermissionHelper.checkStoragePermissions(getActivity(), PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
getDialog().dismiss();
return;
}
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(getContext()));
context = getContext();
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
Icepick.restoreInstanceState(this, savedInstanceState);
SparseArray<SecondaryStreamHelper<AudioStream>> secondaryStreams = new SparseArray<>(4);
@@ -177,9 +204,33 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
}
}
this.videoStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedVideoStreams, secondaryStreams);
this.audioStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedAudioStreams);
this.subtitleStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedSubtitleStreams);
this.videoStreamsAdapter = new StreamItemAdapter<>(context, wrappedVideoStreams, secondaryStreams);
this.audioStreamsAdapter = new StreamItemAdapter<>(context, wrappedAudioStreams);
this.subtitleStreamsAdapter = new StreamItemAdapter<>(context, wrappedSubtitleStreams);
Intent intent = new Intent(context, DownloadManagerService.class);
context.startService(intent);
context.bindService(intent, new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName cname, IBinder service) {
DownloadManagerBinder mgr = (DownloadManagerBinder) service;
mainStorageAudio = mgr.getMainStorageAudio();
mainStorageVideo = mgr.getMainStorageVideo();
downloadManager = mgr.getDownloadManager();
askForSavePath = mgr.askForSavePath();
okButton.setEnabled(true);
context.unbindService(this);
}
@Override
public void onServiceDisconnected(ComponentName name) {
// nothing to do
}
}, Context.BIND_AUTO_CREATE);
}
@Override
@@ -204,8 +255,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
threadsCountTextView = view.findViewById(R.id.threads_count);
threadsSeekBar = view.findViewById(R.id.threads);
radioVideoAudioGroup = view.findViewById(R.id.video_audio_group);
radioVideoAudioGroup.setOnCheckedChangeListener(this);
radioStreamsGroup = view.findViewById(R.id.video_audio_group);
radioStreamsGroup.setOnCheckedChangeListener(this);
initToolbar(view.findViewById(R.id.toolbar));
setupDownloadOptions();
@@ -240,17 +291,17 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
disposables.clear();
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams).subscribe(result -> {
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.video_button) {
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.video_button) {
setupVideoSpinner();
}
}));
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams).subscribe(result -> {
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.audio_button) {
setupAudioSpinner();
}
}));
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams).subscribe(result -> {
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
setupSubtitleSpinner();
}
}));
@@ -263,22 +314,55 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
}
@Override
public void onSaveInstanceState(Bundle outState) {
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_DOWNLOAD_SAVE_AS && resultCode == Activity.RESULT_OK) {
if (data.getData() == null) {
showFailedDialog(R.string.general_error);
return;
}
if (FilePickerActivityHelper.isOwnFileUri(context, data.getData())) {
File file = Utils.getFileForUri(data.getData());
checkSelectedDownload(null, Uri.fromFile(file), file.getName(), StoredFileHelper.DEFAULT_MIME);
return;
}
DocumentFile docFile = DocumentFile.fromSingleUri(context, data.getData());
if (docFile == null) {
showFailedDialog(R.string.general_error);
return;
}
// check if the selected file was previously used
checkSelectedDownload(null, data.getData(), docFile.getName(), docFile.getType());
}
}
/*//////////////////////////////////////////////////////////////////////////
// Inits
//////////////////////////////////////////////////////////////////////////*/
private void initToolbar(Toolbar toolbar) {
if (DEBUG) Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]");
boolean isLight = ThemeHelper.isLightThemeSelected(getActivity());
toolbar.setTitle(R.string.download_dialog_title);
toolbar.setNavigationIcon(ThemeHelper.isLightThemeSelected(getActivity()) ? R.drawable.ic_arrow_back_black_24dp : R.drawable.ic_arrow_back_white_24dp);
toolbar.setNavigationIcon(isLight ? R.drawable.ic_arrow_back_black_24dp : R.drawable.ic_arrow_back_white_24dp);
toolbar.inflateMenu(R.menu.dialog_url);
toolbar.setNavigationOnClickListener(v -> getDialog().dismiss());
okButton = toolbar.findViewById(R.id.okay);
okButton.setEnabled(false);// disable until the download service connection is done
toolbar.setOnMenuItemClickListener(item -> {
if (item.getItemId() == R.id.okay) {
prepareSelectedDownload();
@@ -346,7 +430,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (DEBUG)
Log.d(TAG, "onItemSelected() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]");
switch (radioVideoAudioGroup.getCheckedRadioButtonId()) {
switch (radioStreamsGroup.getCheckedRadioButtonId()) {
case R.id.audio_button:
selectedAudioIndex = position;
break;
@@ -370,9 +454,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
protected void setupDownloadOptions() {
setRadioButtonsState(false);
final RadioButton audioButton = radioVideoAudioGroup.findViewById(R.id.audio_button);
final RadioButton videoButton = radioVideoAudioGroup.findViewById(R.id.video_button);
final RadioButton subtitleButton = radioVideoAudioGroup.findViewById(R.id.subtitle_button);
final RadioButton audioButton = radioStreamsGroup.findViewById(R.id.audio_button);
final RadioButton videoButton = radioStreamsGroup.findViewById(R.id.video_button);
final RadioButton subtitleButton = radioStreamsGroup.findViewById(R.id.subtitle_button);
final boolean isVideoStreamsAvailable = videoStreamsAdapter.getCount() > 0;
final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0;
final boolean isSubtitleStreamsAvailable = subtitleStreamsAdapter.getCount() > 0;
@@ -397,9 +481,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
}
private void setRadioButtonsState(boolean enabled) {
radioVideoAudioGroup.findViewById(R.id.audio_button).setEnabled(enabled);
radioVideoAudioGroup.findViewById(R.id.video_button).setEnabled(enabled);
radioVideoAudioGroup.findViewById(R.id.subtitle_button).setEnabled(enabled);
radioStreamsGroup.findViewById(R.id.audio_button).setEnabled(enabled);
radioStreamsGroup.findViewById(R.id.video_button).setEnabled(enabled);
radioStreamsGroup.findViewById(R.id.subtitle_button).setEnabled(enabled);
}
private int getSubtitleIndexBy(List<SubtitlesStream> streams) {
@@ -434,98 +518,320 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
return 0;
}
StoredDirectoryHelper mainStorageAudio = null;
StoredDirectoryHelper mainStorageVideo = null;
DownloadManager downloadManager = null;
ActionMenuItemView okButton = null;
Context context;
boolean askForSavePath;
private String getNameEditText() {
String str = nameEditText.getText().toString().trim();
return FilenameUtils.createFilename(context, str.isEmpty() ? currentInfo.getName() : str);
}
private void showFailedDialog(@StringRes int msg) {
new AlertDialog.Builder(context)
.setTitle(R.string.general_error)
.setMessage(msg)
.setNegativeButton(android.R.string.ok, null)
.create()
.show();
}
private void showErrorActivity(Exception e) {
ErrorActivity.reportError(
context,
Collections.singletonList(e),
null,
null,
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "-", "-", R.string.general_error)
);
}
private void prepareSelectedDownload() {
final Context context = getContext();
Stream stream;
String location;
char kind;
StoredDirectoryHelper mainStorage;
MediaFormat format;
String mime;
String fileName = nameEditText.getText().toString().trim();
if (fileName.isEmpty())
fileName = FilenameUtils.createFilename(context, currentInfo.getName());
// first, build the filename and get the output folder (if possible)
// later, run a very very very large file checking logic
switch (radioVideoAudioGroup.getCheckedRadioButtonId()) {
String filename = getNameEditText().concat(".");
switch (radioStreamsGroup.getCheckedRadioButtonId()) {
case R.id.audio_button:
stream = audioStreamsAdapter.getItem(selectedAudioIndex);
location = NewPipeSettings.getAudioDownloadPath(context);
kind = 'a';
mainStorage = mainStorageAudio;
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
mime = format.mimeType;
filename += format.suffix;
break;
case R.id.video_button:
stream = videoStreamsAdapter.getItem(selectedVideoIndex);
location = NewPipeSettings.getVideoDownloadPath(context);
kind = 'v';
mainStorage = mainStorageVideo;
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
mime = format.mimeType;
filename += format.suffix;
break;
case R.id.subtitle_button:
stream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
location = NewPipeSettings.getVideoDownloadPath(context);// assume that subtitle & video files go together
kind = 's';
mainStorage = mainStorageVideo;// subtitle & video files go together
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
mime = format.mimeType;
filename += format == MediaFormat.TTML ? MediaFormat.SRT.suffix : format.suffix;
break;
default:
throw new RuntimeException("No stream selected");
}
if (mainStorage == null || askForSavePath) {
// This part is called if with SAF preferred:
// * older android version running
// * save path not defined (via download settings)
// * the user checked the "ask where to download" option
if (!askForSavePath)
Toast.makeText(context, getString(R.string.no_available_dir), Toast.LENGTH_LONG).show();
if (NewPipeSettings.useStorageAccessFramework(context)) {
StoredFileHelper.requestSafWithFileCreation(this, REQUEST_DOWNLOAD_SAVE_AS, filename, mime);
} else {
File initialSavePath;
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.audio_button)
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MUSIC);
else
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES);
initialSavePath = new File(initialSavePath, filename);
startActivityForResult(
FilePickerActivityHelper.chooseFileToSave(context, initialSavePath.getAbsolutePath()),
REQUEST_DOWNLOAD_SAVE_AS
);
}
return;
}
// check for existing file with the same name
checkSelectedDownload(mainStorage, mainStorage.findFile(filename), filename, mime);
}
private void checkSelectedDownload(StoredDirectoryHelper mainStorage, Uri targetFile, String filename, String mime) {
StoredFileHelper storage;
try {
if (mainStorage == null) {
// using SAF on older android version
storage = new StoredFileHelper(context, null, targetFile, "");
} else if (targetFile == null) {
// the file does not exist, but it is probably used in a pending download
storage = new StoredFileHelper(mainStorage.getUri(), filename, mime, mainStorage.getTag());
} else {
// the target filename is already use, attempt to use it
storage = new StoredFileHelper(context, mainStorage.getUri(), targetFile, mainStorage.getTag());
}
} catch (Exception e) {
showErrorActivity(e);
return;
}
// check if is our file
MissionState state = downloadManager.checkForExistingMission(storage);
@StringRes int msgBtn;
@StringRes int msgBody;
switch (state) {
case Finished:
msgBtn = R.string.overwrite;
msgBody = R.string.overwrite_finished_warning;
break;
case Pending:
msgBtn = R.string.overwrite;
msgBody = R.string.download_already_pending;
break;
case PendingRunning:
msgBtn = R.string.generate_unique_name;
msgBody = R.string.download_already_running;
break;
case None:
if (mainStorage == null) {
// This part is called if:
// * using SAF on older android version
// * save path not defined
// * if the file exists overwrite it, is not necessary ask
if (!storage.existsAsFile() && !storage.create()) {
showFailedDialog(R.string.error_file_creation);
return;
}
continueSelectedDownload(storage);
return;
} else if (targetFile == null) {
// This part is called if:
// * the filename is not used in a pending/finished download
// * the file does not exists, create
if (!mainStorage.mkdirs()) {
showFailedDialog(R.string.error_path_creation);
return;
}
storage = mainStorage.createFile(filename, mime);
if (storage == null || !storage.canWrite()) {
showFailedDialog(R.string.error_file_creation);
return;
}
continueSelectedDownload(storage);
return;
}
msgBtn = R.string.overwrite;
msgBody = R.string.overwrite_unrelated_warning;
break;
default:
return;
}
int threads;
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
threads = 1;// use unique thread for subtitles due small file size
fileName += ".srt";// final subtitle format
} else {
threads = threadsSeekBar.getProgress() + 1;
fileName += "." + stream.getFormat().getSuffix();
AlertDialog.Builder askDialog = new AlertDialog.Builder(context)
.setTitle(R.string.download_dialog_title)
.setMessage(msgBody)
.setNegativeButton(android.R.string.cancel, null);
final StoredFileHelper finalStorage = storage;
if (mainStorage == null) {
// This part is called if:
// * using SAF on older android version
// * save path not defined
switch (state) {
case Pending:
case Finished:
askDialog.setPositiveButton(msgBtn, (dialog, which) -> {
dialog.dismiss();
downloadManager.forgetMission(finalStorage);
continueSelectedDownload(finalStorage);
});
break;
}
askDialog.create().show();
return;
}
final String finalFileName = fileName;
askDialog.setPositiveButton(msgBtn, (dialog, which) -> {
dialog.dismiss();
DownloadManagerService.checkForRunningMission(context, location, fileName, (listed, finished) -> {
if (listed) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(R.string.download_dialog_title)
.setMessage(finished ? R.string.overwrite_warning : R.string.download_already_running)
.setPositiveButton(
finished ? R.string.overwrite : R.string.generate_unique_name,
(dialog, which) -> downloadSelected(context, stream, location, finalFileName, kind, threads)
)
.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
dialog.cancel();
})
.create()
.show();
} else {
downloadSelected(context, stream, location, finalFileName, kind, threads);
StoredFileHelper storageNew;
switch (state) {
case Finished:
case Pending:
downloadManager.forgetMission(finalStorage);
case None:
if (targetFile == null) {
storageNew = mainStorage.createFile(filename, mime);
} else {
try {
// try take (or steal) the file
storageNew = new StoredFileHelper(context, mainStorage.getUri(), targetFile, mainStorage.getTag());
} catch (IOException e) {
Log.e(TAG, "Failed to take (or steal) the file in " + targetFile.toString());
storageNew = null;
}
}
if (storageNew != null && storageNew.canWrite())
continueSelectedDownload(storageNew);
else
showFailedDialog(R.string.error_file_creation);
break;
case PendingRunning:
storageNew = mainStorage.createUniqueFile(filename, mime);
if (storageNew == null)
showFailedDialog(R.string.error_file_creation);
else
continueSelectedDownload(storageNew);
break;
}
});
askDialog.create().show();
}
private void downloadSelected(Context context, Stream selectedStream, String location, String fileName, char kind, int threads) {
private void continueSelectedDownload(@NonNull StoredFileHelper storage) {
if (!storage.canWrite()) {
showFailedDialog(R.string.permission_denied);
return;
}
// check if the selected file has to be overwritten, by simply checking its length
try {
if (storage.length() > 0) storage.truncate();
} catch (IOException e) {
Log.e(TAG, "failed to truncate the file: " + storage.getUri().toString(), e);
showFailedDialog(R.string.overwrite_failed);
return;
}
Stream selectedStream;
char kind;
int threads = threadsSeekBar.getProgress() + 1;
String[] urls;
String psName = null;
String[] psArgs = null;
String secondaryStreamUrl = null;
long nearLength = 0;
if (selectedStream instanceof VideoStream) {
SecondaryStreamHelper<AudioStream> secondaryStream = videoStreamsAdapter
.getAllSecondary()
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
// more download logic: select muxer, subtitle converter, etc.
switch (radioStreamsGroup.getCheckedRadioButtonId()) {
case R.id.audio_button:
kind = 'a';
selectedStream = audioStreamsAdapter.getItem(selectedAudioIndex);
if (secondaryStream != null) {
secondaryStreamUrl = secondaryStream.getStream().getUrl();
psName = selectedStream.getFormat() == MediaFormat.MPEG_4 ? Postprocessing.ALGORITHM_MP4_MUXER : Postprocessing.ALGORITHM_WEBM_MUXER;
psArgs = null;
long videoSize = wrappedVideoStreams.getSizeInBytes((VideoStream) selectedStream);
// set nearLength, only, if both sizes are fetched or known. this probably does not work on slow networks
if (secondaryStream.getSizeInBytes() > 0 && videoSize > 0) {
nearLength = secondaryStream.getSizeInBytes() + videoSize;
if (selectedStream.getFormat() == MediaFormat.M4A) {
psName = Postprocessing.ALGORITHM_M4A_NO_DASH;
}
}
} else if ((selectedStream instanceof SubtitlesStream) && selectedStream.getFormat() == MediaFormat.TTML) {
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
psArgs = new String[]{
selectedStream.getFormat().getSuffix(),
"false",// ignore empty frames
"false",// detect youtube duplicate lines
};
break;
case R.id.video_button:
kind = 'v';
selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex);
SecondaryStreamHelper<AudioStream> secondaryStream = videoStreamsAdapter
.getAllSecondary()
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
if (secondaryStream != null) {
secondaryStreamUrl = secondaryStream.getStream().getUrl();
if (selectedStream.getFormat() == MediaFormat.MPEG_4)
psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER;
else
psName = Postprocessing.ALGORITHM_WEBM_MUXER;
psArgs = null;
long videoSize = wrappedVideoStreams.getSizeInBytes((VideoStream) selectedStream);
// set nearLength, only, if both sizes are fetched or known. This probably
// does not work on slow networks but is later updated in the downloader
if (secondaryStream.getSizeInBytes() > 0 && videoSize > 0) {
nearLength = secondaryStream.getSizeInBytes() + videoSize;
}
}
break;
case R.id.subtitle_button:
threads = 1;// use unique thread for subtitles due small file size
kind = 's';
selectedStream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
if (selectedStream.getFormat() == MediaFormat.TTML) {
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
psArgs = new String[]{
selectedStream.getFormat().getSuffix(),
"false",// ignore empty frames
"false",// detect youtube duplicate lines
};
}
break;
default:
return;
}
if (secondaryStreamUrl == null) {
@@ -534,8 +840,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
urls = new String[]{selectedStream.getUrl(), secondaryStreamUrl};
}
DownloadManagerService.startMission(context, urls, location, fileName, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength);
DownloadManagerService.startMission(context, urls, storage, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength);
getDialog().dismiss();
dismiss();
}
}

View File

@@ -180,7 +180,7 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
}
if (exception instanceof ReCaptchaException) {
onReCaptchaException();
onReCaptchaException((ReCaptchaException) exception);
return true;
} else if (exception instanceof IOException) {
showError(getString(R.string.network_error), true);
@@ -190,11 +190,13 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
return false;
}
public void onReCaptchaException() {
public void onReCaptchaException(ReCaptchaException exception) {
if (DEBUG) Log.d(TAG, "onReCaptchaException() called");
Toast.makeText(activity, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
// Starting ReCaptcha Challenge Activity
startActivityForResult(new Intent(activity, ReCaptchaActivity.class), ReCaptchaActivity.RECAPTCHA_REQUEST);
Intent intent = new Intent(activity, ReCaptchaActivity.class);
intent.putExtra(ReCaptchaActivity.RECAPTCHA_URL_EXTRA, exception.getUrl());
startActivityForResult(intent, ReCaptchaActivity.RECAPTCHA_REQUEST);
showError(getString(R.string.recaptcha_request_toast), false);
}

View File

@@ -50,6 +50,8 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
destroyOldFragments();
tabsManager = TabsManager.getManager(activity);
tabsManager.setSavedTabsListener(() -> {
if (DEBUG) {
@@ -63,6 +65,17 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
});
}
private void destroyOldFragments() {
for (Fragment fragment : getChildFragmentManager().getFragments()) {
if (fragment != null) {
getChildFragmentManager()
.beginTransaction()
.remove(fragment)
.commitNowAllowingStateLoss();
}
}
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_main, container, false);
@@ -98,6 +111,8 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
public void onDestroy() {
super.onDestroy();
tabsManager.unsetSavedTabsListener();
pagerAdapter = null;
viewPager.setAdapter(pagerAdapter);
}
/*//////////////////////////////////////////////////////////////////////////
@@ -177,6 +192,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
}
private class SelectedTabsPagerAdapter extends FragmentPagerAdapter {
private SelectedTabsPagerAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
}

View File

@@ -60,7 +60,6 @@ import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExt
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.Stream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.fragments.BackPressable;
@@ -68,7 +67,6 @@ import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.fragments.EmptyFragment;
import org.schabi.newpipe.fragments.list.comments.CommentsFragment;
import org.schabi.newpipe.fragments.list.videos.RelatedVideosFragment;
import org.schabi.newpipe.info_list.InfoItemDialog;
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.player.MainVideoPlayer;
@@ -77,7 +75,6 @@ import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants;
@@ -89,12 +86,13 @@ import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.ShareUtils;
import org.schabi.newpipe.util.StreamItemAdapter;
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
import org.schabi.newpipe.views.AnimatedProgressBar;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import icepick.State;
import io.reactivex.Single;
@@ -118,7 +116,7 @@ public class VideoDetailFragment
private static final int RELATED_STREAMS_UPDATE_FLAG = 0x1;
private static final int RESOLUTIONS_MENU_UPDATE_FLAG = 0x2;
private static final int TOOLBAR_ITEMS_UPDATE_FLAG = 0x4;
private static final int COMMENTS_UPDATE_FLAG = 0x4;
private static final int COMMENTS_UPDATE_FLAG = 0x8;
private boolean autoPlayEnabled;
private boolean showRelatedStreams;
@@ -136,6 +134,8 @@ public class VideoDetailFragment
private Disposable currentWorker;
@NonNull
private CompositeDisposable disposables = new CompositeDisposable();
@Nullable
private Disposable positionSubscriber = null;
private List<VideoStream> sortedVideoStreams;
private int selectedVideoStreamIndex = -1;
@@ -153,6 +153,7 @@ public class VideoDetailFragment
private View thumbnailBackgroundButton;
private ImageView thumbnailImageView;
private ImageView thumbnailPlayButton;
private AnimatedProgressBar positionView;
private View videoTitleRoot;
private TextView videoTitleTextView;
@@ -165,6 +166,7 @@ public class VideoDetailFragment
private TextView detailControlsDownload;
private TextView appendControlsDetail;
private TextView detailDurationView;
private TextView detailPositionView;
private LinearLayout videoDescriptionRootLayout;
private TextView videoUploadDateView;
@@ -259,6 +261,8 @@ public class VideoDetailFragment
// Check if it was loading when the fragment was stopped/paused,
if (wasLoading.getAndSet(false)) {
selectAndLoadVideo(serviceId, url, name);
} else if (currentInfo != null) {
updateProgressInfo(currentInfo);
}
}
@@ -268,8 +272,10 @@ public class VideoDetailFragment
PreferenceManager.getDefaultSharedPreferences(activity)
.unregisterOnSharedPreferenceChangeListener(this);
if (positionSubscriber != null) positionSubscriber.dispose();
if (currentWorker != null) currentWorker.dispose();
if (disposables != null) disposables.clear();
positionSubscriber = null;
currentWorker = null;
disposables = null;
}
@@ -462,6 +468,7 @@ public class VideoDetailFragment
videoTitleTextView = rootView.findViewById(R.id.detail_video_title_view);
videoTitleToggleArrow = rootView.findViewById(R.id.detail_toggle_description_view);
videoCountView = rootView.findViewById(R.id.detail_view_count_view);
positionView = rootView.findViewById(R.id.position_view);
detailControlsBackground = rootView.findViewById(R.id.detail_controls_background);
detailControlsPopup = rootView.findViewById(R.id.detail_controls_popup);
@@ -469,6 +476,7 @@ public class VideoDetailFragment
detailControlsDownload = rootView.findViewById(R.id.detail_controls_download);
appendControlsDetail = rootView.findViewById(R.id.touch_append_detail);
detailDurationView = rootView.findViewById(R.id.detail_duration_view);
detailPositionView = rootView.findViewById(R.id.detail_position_view);
videoDescriptionRootLayout = rootView.findViewById(R.id.detail_description_root_layout);
videoUploadDateView = rootView.findViewById(R.id.detail_upload_date_view);
@@ -522,42 +530,6 @@ public class VideoDetailFragment
detailControlsPopup.setOnTouchListener(getOnControlsTouchListener());
}
private void showStreamDialog(final StreamInfoItem item) {
final Context context = getContext();
if (context == null || context.getResources() == null || getActivity() == null) return;
final String[] commands = new String[]{
context.getResources().getString(R.string.enqueue_on_background),
context.getResources().getString(R.string.enqueue_on_popup),
context.getResources().getString(R.string.append_playlist),
context.getResources().getString(R.string.share)
};
final DialogInterface.OnClickListener actions = (DialogInterface dialogInterface, int i) -> {
switch (i) {
case 0:
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item));
break;
case 1:
NavigationHelper.enqueueOnPopupPlayer(getActivity(), new SinglePlayQueue(item));
break;
case 2:
if (getFragmentManager() != null) {
PlaylistAppendDialog.fromStreamInfoItems(Collections.singletonList(item))
.show(getFragmentManager(), TAG);
}
break;
case 3:
ShareUtils.shareUrl(this.getContext(), item.getName(), item.getUrl());
break;
default:
break;
}
};
new InfoItemDialog(getActivity(), item, commands, actions).show();
}
private View.OnTouchListener getOnControlsTouchListener() {
return (View view, MotionEvent motionEvent) -> {
if (!PreferenceManager.getDefaultSharedPreferences(activity)
@@ -890,11 +862,11 @@ public class VideoDetailFragment
final PlayQueue itemQueue = new SinglePlayQueue(currentInfo);
if (append) {
NavigationHelper.enqueueOnPopupPlayer(activity, itemQueue);
NavigationHelper.enqueueOnPopupPlayer(activity, itemQueue, false);
} else {
Toast.makeText(activity, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show();
final Intent intent = NavigationHelper.getPlayerIntent(
activity, PopupVideoPlayer.class, itemQueue, getSelectedVideoStream().resolution
activity, PopupVideoPlayer.class, itemQueue, getSelectedVideoStream().resolution, true
);
activity.startService(intent);
}
@@ -914,9 +886,9 @@ public class VideoDetailFragment
private void openNormalBackgroundPlayer(final boolean append) {
final PlayQueue itemQueue = new SinglePlayQueue(currentInfo);
if (append) {
NavigationHelper.enqueueOnBackgroundPlayer(activity, itemQueue);
NavigationHelper.enqueueOnBackgroundPlayer(activity, itemQueue, false);
} else {
NavigationHelper.playOnBackgroundPlayer(activity, itemQueue);
NavigationHelper.playOnBackgroundPlayer(activity, itemQueue, true);
}
}
@@ -926,7 +898,7 @@ public class VideoDetailFragment
mIntent = NavigationHelper.getPlayerIntent(activity,
MainVideoPlayer.class,
playQueue,
getSelectedVideoStream().getResolution());
getSelectedVideoStream().getResolution(), true);
startActivity(mIntent);
}
@@ -993,7 +965,7 @@ public class VideoDetailFragment
}
private void showContent() {
AnimationUtils.slideUp(contentRootLayoutHiding,120, 96, 0.06f);
contentRootLayoutHiding.setVisibility(View.VISIBLE);
}
protected void setInitialData(int serviceId, String url, String name) {
@@ -1026,12 +998,19 @@ public class VideoDetailFragment
@Override
public void showLoading() {
super.showLoading();
contentRootLayoutHiding.setVisibility(View.INVISIBLE);
//if data is already cached, transition from VISIBLE -> INVISIBLE -> VISIBLE is not required
if(!ExtractorHelper.isCached(serviceId, url, InfoItem.InfoType.STREAM)){
contentRootLayoutHiding.setVisibility(View.INVISIBLE);
}
animateView(spinnerToolbar, false, 200);
animateView(thumbnailPlayButton, false, 50);
animateView(detailDurationView, false, 100);
animateView(detailPositionView, false, 100);
animateView(positionView, false, 50);
videoTitleTextView.setText(name != null ? name : "");
videoTitleTextView.setMaxLines(1);
@@ -1146,6 +1125,7 @@ public class VideoDetailFragment
videoUploadDateView.setText(Localization.localizeDate(activity, info.getUploadDate()));
}
prepareDescription(info.getDescription());
updateProgressInfo(info);
animateView(spinnerToolbar, true, 500);
setupActionBar(info);
@@ -1195,7 +1175,7 @@ public class VideoDetailFragment
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
downloadDialog.setSubtitleStreams(currentInfo.getSubtitles());
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
downloadDialog.show(getActivity().getSupportFragmentManager(), "downloadDialog");
} catch (Exception e) {
ErrorActivity.ErrorInfo info = ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR,
ServiceList.all()
@@ -1220,9 +1200,7 @@ public class VideoDetailFragment
protected boolean onError(Throwable exception) {
if (super.onError(exception)) return true;
if (exception instanceof YoutubeStreamExtractor.GemaException) {
onBlockedByGemaError();
} else if (exception instanceof ContentNotAvailableException) {
else if (exception instanceof ContentNotAvailableException) {
showError(getString(R.string.content_not_available), false);
} else {
int errorId = exception instanceof YoutubeStreamExtractor.DecryptException
@@ -1240,14 +1218,36 @@ public class VideoDetailFragment
return true;
}
public void onBlockedByGemaError() {
thumbnailBackgroundButton.setOnClickListener((View v) -> {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setData(Uri.parse(getString(R.string.c3s_url)));
startActivity(intent);
});
showError(getString(R.string.blocked_by_gema), false, R.drawable.gruese_die_gema);
private void updateProgressInfo(@NonNull final StreamInfo info) {
if (positionSubscriber != null) {
positionSubscriber.dispose();
}
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
final boolean playbackResumeEnabled =
prefs.getBoolean(activity.getString(R.string.enable_watch_history_key), true)
&& prefs.getBoolean(activity.getString(R.string.enable_playback_resume_key), true);
if (!playbackResumeEnabled || info.getDuration() <= 0) {
positionView.setVisibility(View.INVISIBLE);
detailPositionView.setVisibility(View.GONE);
return;
}
final HistoryRecordManager recordManager = new HistoryRecordManager(requireContext());
positionSubscriber = recordManager.loadStreamState(info)
.subscribeOn(Schedulers.io())
.onErrorComplete()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(state -> {
final int seconds = (int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime());
positionView.setMax((int) info.getDuration());
positionView.setProgressAnimated(seconds);
detailPositionView.setText(Localization.getDurationString(seconds));
animateView(positionView, true, 500);
animateView(detailPositionView, true, 500);
}, e -> {
if (DEBUG) e.printStackTrace();
}, () -> {
animateView(positionView, false, 500);
animateView(detailPositionView, false, 500);
});
}
}

View File

@@ -2,7 +2,6 @@ package org.schabi.newpipe.fragments.list;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
@@ -25,19 +24,17 @@ import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.info_list.InfoItemDialog;
import org.schabi.newpipe.info_list.InfoListAdapter;
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
import org.schabi.newpipe.util.ShareUtils;
import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.StreamDialogEntry;
import java.util.Collections;
import java.util.List;
import java.util.Queue;
@@ -65,6 +62,11 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
infoListAdapter = new InfoListAdapter(activity);
}
@Override
public void onDetach() {
super.onDetach();
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -250,45 +252,32 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
}
}
protected void showStreamDialog(final StreamInfoItem item) {
final Context context = getContext();
final Activity activity = getActivity();
if (context == null || context.getResources() == null || getActivity() == null) return;
if (context == null || context.getResources() == null || activity == null) return;
final String[] commands = new String[]{
context.getResources().getString(R.string.direct_on_background),
context.getResources().getString(R.string.enqueue_on_background),
context.getResources().getString(R.string.enqueue_on_popup),
context.getResources().getString(R.string.append_playlist),
context.getResources().getString(R.string.share)
};
if (item.getStreamType() == StreamType.AUDIO_STREAM) {
StreamDialogEntry.setEnabledEntries(
StreamDialogEntry.enqueue_on_background,
StreamDialogEntry.start_here_on_background,
StreamDialogEntry.append_playlist,
StreamDialogEntry.share);
} else {
StreamDialogEntry.setEnabledEntries(
StreamDialogEntry.enqueue_on_background,
StreamDialogEntry.enqueue_on_popup,
StreamDialogEntry.start_here_on_background,
StreamDialogEntry.start_here_on_popup,
StreamDialogEntry.append_playlist,
StreamDialogEntry.share);
}
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
switch (i) {
case 0:
NavigationHelper.playOnBackgroundPlayer(context, new SinglePlayQueue(item));
break;
case 1:
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item));
break;
case 2:
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item));
break;
case 3:
if (getFragmentManager() != null) {
PlaylistAppendDialog.fromStreamInfoItems(Collections.singletonList(item))
.show(getFragmentManager(), TAG);
}
break;
case 4:
ShareUtils.shareUrl(this.getContext(), item.getName(), item.getUrl());
break;
default:
break;
}
};
new InfoItemDialog(getActivity(), item, commands, actions).show();
new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context), (dialog, which) ->
StreamDialogEntry.clickOn(which, this, item)).show();
}
/*//////////////////////////////////////////////////////////////////////////

View File

@@ -1,8 +1,6 @@
package org.schabi.newpipe.fragments.list.channel;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
@@ -34,12 +32,9 @@ import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.info_list.InfoItemDialog;
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.local.subscription.SubscriptionService;
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.ExtractorHelper;
@@ -49,7 +44,6 @@ import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ShareUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -150,56 +144,6 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
return headerRootLayout;
}
@Override
protected void showStreamDialog(final StreamInfoItem item) {
final Activity activity = getActivity();
final Context context = getContext();
if (context == null || context.getResources() == null || getActivity() == null) return;
final String[] commands = new String[]{
context.getResources().getString(R.string.enqueue_on_background),
context.getResources().getString(R.string.enqueue_on_popup),
context.getResources().getString(R.string.start_here_on_main),
context.getResources().getString(R.string.start_here_on_background),
context.getResources().getString(R.string.start_here_on_popup),
context.getResources().getString(R.string.append_playlist),
context.getResources().getString(R.string.share)
};
final DialogInterface.OnClickListener actions = (DialogInterface dialogInterface, int i) -> {
final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0);
switch (i) {
case 0:
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item));
break;
case 1:
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item));
break;
case 2:
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index));
break;
case 3:
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index));
break;
case 4:
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
break;
case 5:
if (getFragmentManager() != null) {
PlaylistAppendDialog.fromStreamInfoItems(Collections.singletonList(item))
.show(getFragmentManager(), TAG);
}
break;
case 6:
ShareUtils.shareUrl(this.getContext(), item.getName(), item.getUrl());
break;
default:
break;
}
};
new InfoItemDialog(getActivity(), item, commands, actions).show();
}
/*//////////////////////////////////////////////////////////////////////////
// Menu
//////////////////////////////////////////////////////////////////////////*/
@@ -440,11 +384,11 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
monitorSubscription(result);
headerPlayAllButton.setOnClickListener(
view -> NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
view -> NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false));
headerPopupButton.setOnClickListener(
view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
headerBackgroundButton.setOnClickListener(
view -> NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
view -> NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
}
private PlayQueue getPlayQueue() {

View File

@@ -1,7 +1,6 @@
package org.schabi.newpipe.fragments.list.kiosk;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.ActionBar;
@@ -155,9 +154,7 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
super.handleResult(result);
name = kioskTranslatedName;
if(!useAsFrontPage) {
setTitle(kioskTranslatedName);
}
setTitle(kioskTranslatedName);
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(),

View File

@@ -2,10 +2,10 @@ package org.schabi.newpipe.fragments.list.playlist;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.util.Log;
@@ -29,18 +29,19 @@ import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.info_list.InfoItemDialog;
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ShareUtils;
import org.schabi.newpipe.util.StreamDialogEntry;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.ArrayList;
@@ -135,48 +136,40 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
infoListAdapter.useMiniItemVariants(true);
}
private PlayQueue getPlayQueueStartingAt(StreamInfoItem infoItem) {
return getPlayQueue(Math.max(infoListAdapter.getItemsList().indexOf(infoItem), 0));
}
@Override
protected void showStreamDialog(final StreamInfoItem item) {
protected void showStreamDialog(StreamInfoItem item) {
final Context context = getContext();
final Activity activity = getActivity();
if (context == null || context.getResources() == null || getActivity() == null) return;
if (context == null || context.getResources() == null || activity == null) return;
final String[] commands = new String[]{
context.getResources().getString(R.string.enqueue_on_background),
context.getResources().getString(R.string.enqueue_on_popup),
context.getResources().getString(R.string.start_here_on_main),
context.getResources().getString(R.string.start_here_on_background),
context.getResources().getString(R.string.start_here_on_popup),
context.getResources().getString(R.string.share)
};
if (item.getStreamType() == StreamType.AUDIO_STREAM) {
StreamDialogEntry.setEnabledEntries(
StreamDialogEntry.enqueue_on_background,
StreamDialogEntry.start_here_on_background,
StreamDialogEntry.append_playlist,
StreamDialogEntry.share);
} else {
StreamDialogEntry.setEnabledEntries(
StreamDialogEntry.enqueue_on_background,
StreamDialogEntry.enqueue_on_popup,
StreamDialogEntry.start_here_on_background,
StreamDialogEntry.start_here_on_popup,
StreamDialogEntry.append_playlist,
StreamDialogEntry.share);
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0);
switch (i) {
case 0:
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item));
break;
case 1:
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item));
break;
case 2:
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index));
break;
case 3:
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index));
break;
case 4:
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
break;
case 5:
ShareUtils.shareUrl(this.getContext(), item.getName(), item.getUrl());
break;
default:
break;
}
};
StreamDialogEntry.start_here_on_popup.setCustomAction(
(fragment, infoItem) -> NavigationHelper.playOnPopupPlayer(context, getPlayQueueStartingAt(infoItem), true));
}
new InfoItemDialog(getActivity(), item, commands, actions).show();
StreamDialogEntry.start_here_on_background.setCustomAction(
(fragment, infoItem) -> NavigationHelper.playOnBackgroundPlayer(context, getPlayQueueStartingAt(infoItem), true));
new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context), (dialog, which) ->
StreamDialogEntry.clickOn(which, this, item)).show();
}
@Override
@@ -301,19 +294,19 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
.subscribe(getPlaylistBookmarkSubscriber());
headerPlayAllButton.setOnClickListener(view ->
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false));
headerPopupButton.setOnClickListener(view ->
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
headerBackgroundButton.setOnClickListener(view ->
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
headerPopupButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue());
NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true);
return true;
});
headerBackgroundButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue());
NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true);
return true;
});
}

View File

@@ -2,7 +2,6 @@ package org.schabi.newpipe.info_list;
import android.content.Context;
import android.support.annotation.NonNull;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
@@ -22,6 +21,7 @@ import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder;
import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.OnClickGesture;
/*
@@ -59,13 +59,14 @@ public class InfoItemBuilder {
this.context = context;
}
public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem) {
return buildView(parent, infoItem, false);
public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem, final HistoryRecordManager historyRecordManager) {
return buildView(parent, infoItem, historyRecordManager, false);
}
public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem, boolean useMiniVariant) {
public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem,
final HistoryRecordManager historyRecordManager, boolean useMiniVariant) {
InfoItemHolder holder = holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant);
holder.updateFromItem(infoItem);
holder.updateFromItem(infoItem, historyRecordManager);
return holder.itemView;
}
@@ -80,7 +81,6 @@ public class InfoItemBuilder {
case COMMENT:
return useMiniVariant ? new CommentsMiniInfoItemHolder(this, parent) : new CommentsInfoItemHolder(this, parent);
default:
Log.e(TAG, "Trollolo");
throw new RuntimeException("InfoType not expected = " + infoType.name());
}
}

View File

@@ -1,22 +1,25 @@
package org.schabi.newpipe.info_list;
import android.app.Activity;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder;
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.CommentsInfoItemHolder;
import org.schabi.newpipe.info_list.holder.CommentsMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder;
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistGridInfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
@@ -24,6 +27,7 @@ import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.StreamGridInfoItemHolder;
import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder;
import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.FallbackViewHolder;
import org.schabi.newpipe.util.OnClickGesture;
@@ -71,6 +75,8 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
private final InfoItemBuilder infoItemBuilder;
private final ArrayList<InfoItem> infoItemList;
private final HistoryRecordManager recordManager;
private boolean useMiniVariant = false;
private boolean useGridVariant = false;
private boolean showFooter = false;
@@ -86,8 +92,9 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
}
}
public InfoListAdapter(Activity a) {
infoItemBuilder = new InfoItemBuilder(a);
public InfoListAdapter(Context context) {
this.recordManager = new HistoryRecordManager(context);
infoItemBuilder = new InfoItemBuilder(context);
infoItemList = new ArrayList<>();
}
@@ -115,50 +122,53 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
this.useGridVariant = useGridVariant;
}
public void addInfoItemList(List<InfoItem> data) {
if (data != null) {
if (DEBUG) {
Log.d(TAG, "addInfoItemList() before > infoItemList.size() = " + infoItemList.size() + ", data.size() = " + data.size());
}
public void addInfoItemList(@Nullable final List<InfoItem> data) {
if (data == null) {
return;
}
if (DEBUG) Log.d(TAG, "addInfoItemList() before > infoItemList.size() = " +
infoItemList.size() + ", data.size() = " + data.size());
int offsetStart = sizeConsideringHeaderOffset();
infoItemList.addAll(data);
int offsetStart = sizeConsideringHeaderOffset();
infoItemList.addAll(data);
if (DEBUG) {
Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter);
}
if (DEBUG) Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart +
", infoItemList.size() = " + infoItemList.size() +
", header = " + header + ", footer = " + footer +
", showFooter = " + showFooter);
notifyItemRangeInserted(offsetStart, data.size());
notifyItemRangeInserted(offsetStart, data.size());
if (footer != null && showFooter) {
int footerNow = sizeConsideringHeaderOffset();
notifyItemMoved(offsetStart, footerNow);
if (footer != null && showFooter) {
int footerNow = sizeConsideringHeaderOffset();
notifyItemMoved(offsetStart, footerNow);
if (DEBUG) Log.d(TAG, "addInfoItemList() footer from " + offsetStart + " to " + footerNow);
}
if (DEBUG) Log.d(TAG, "addInfoItemList() footer from " + offsetStart +
" to " + footerNow);
}
}
public void addInfoItem(InfoItem data) {
if (data != null) {
if (DEBUG) {
Log.d(TAG, "addInfoItem() before > infoItemList.size() = " + infoItemList.size() + ", thread = " + Thread.currentThread());
}
public void addInfoItem(@Nullable InfoItem data) {
if (data == null) {
return;
}
if (DEBUG) Log.d(TAG, "addInfoItem() before > infoItemList.size() = " +
infoItemList.size() + ", thread = " + Thread.currentThread());
int positionInserted = sizeConsideringHeaderOffset();
infoItemList.add(data);
int positionInserted = sizeConsideringHeaderOffset();
infoItemList.add(data);
if (DEBUG) {
Log.d(TAG, "addInfoItem() after > position = " + positionInserted + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter);
}
notifyItemInserted(positionInserted);
if (DEBUG) Log.d(TAG, "addInfoItem() after > position = " + positionInserted +
", infoItemList.size() = " + infoItemList.size() +
", header = " + header + ", footer = " + footer +
", showFooter = " + showFooter);
notifyItemInserted(positionInserted);
if (footer != null && showFooter) {
int footerNow = sizeConsideringHeaderOffset();
notifyItemMoved(positionInserted, footerNow);
if (footer != null && showFooter) {
int footerNow = sizeConsideringHeaderOffset();
notifyItemMoved(positionInserted, footerNow);
if (DEBUG) Log.d(TAG, "addInfoItem() footer from " + positionInserted + " to " + footerNow);
}
if (DEBUG) Log.d(TAG, "addInfoItem() footer from " + positionInserted +
" to " + footerNow);
}
}
@@ -235,13 +245,13 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
case COMMENT:
return useMiniVariant ? MINI_COMMENT_HOLDER_TYPE : COMMENT_HOLDER_TYPE;
default:
Log.e(TAG, "Trollolo");
return -1;
}
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int type) {
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int type) {
if (DEBUG)
Log.d(TAG, "onCreateViewHolder() called with: parent = [" + parent + "], type = [" + type + "]");
switch (type) {
@@ -272,19 +282,18 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
case COMMENT_HOLDER_TYPE:
return new CommentsInfoItemHolder(infoItemBuilder, parent);
default:
Log.e(TAG, "Trollolo");
return new FallbackViewHolder(new View(parent.getContext()));
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
if (DEBUG) Log.d(TAG, "onBindViewHolder() called with: holder = [" + holder.getClass().getSimpleName() + "], position = [" + position + "]");
if (holder instanceof InfoItemHolder) {
// If header isn't null, offset the items by -1
if (header != null) position--;
((InfoItemHolder) holder).updateFromItem(infoItemList.get(position));
((InfoItemHolder) holder).updateFromItem(infoItemList.get(position), recordManager);
} else if (holder instanceof HFHolder && position == 0 && header != null) {
((HFHolder) holder).view = header;
} else if (holder instanceof HFHolder && position == sizeConsideringHeaderOffset() && footer != null && showFooter) {
@@ -292,6 +301,21 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
}
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List<Object> payloads) {
if (!payloads.isEmpty() && holder instanceof InfoItemHolder) {
for (Object payload : payloads) {
if (payload instanceof StreamStateEntity) {
((InfoItemHolder) holder).updateState(infoItemList.get(header == null ? position : position - 1), recordManager);
} else if (payload instanceof Boolean) {
((InfoItemHolder) holder).updateState(infoItemList.get(header == null ? position : position - 1), recordManager);
}
}
} else {
onBindViewHolder(holder, position);
}
}
public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final int spanCount) {
return new GridLayoutManager.SpanSizeLookup() {
@Override

View File

@@ -7,6 +7,7 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.Localization;
/*
@@ -38,8 +39,8 @@ public class ChannelInfoItemHolder extends ChannelMiniInfoItemHolder {
}
@Override
public void updateFromItem(final InfoItem infoItem) {
super.updateFromItem(infoItem);
public void updateFromItem(final InfoItem infoItem, final HistoryRecordManager historyRecordManager) {
super.updateFromItem(infoItem, historyRecordManager);
if (!(infoItem instanceof ChannelInfoItem)) return;
final ChannelInfoItem item = (ChannelInfoItem) infoItem;

View File

@@ -7,6 +7,7 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
@@ -30,7 +31,7 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
}
@Override
public void updateFromItem(final InfoItem infoItem) {
public void updateFromItem(final InfoItem infoItem, final HistoryRecordManager historyRecordManager) {
if (!(infoItem instanceof ChannelInfoItem)) return;
final ChannelInfoItem item = (ChannelInfoItem) infoItem;

View File

@@ -5,10 +5,9 @@ import android.widget.TextView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.local.history.HistoryRecordManager;
/*
* Created by Christian Schabesberger on 12.02.17.
@@ -41,8 +40,8 @@ public class CommentsInfoItemHolder extends CommentsMiniInfoItemHolder {
}
@Override
public void updateFromItem(final InfoItem infoItem) {
super.updateFromItem(infoItem);
public void updateFromItem(final InfoItem infoItem, final HistoryRecordManager historyRecordManager) {
super.updateFromItem(infoItem, historyRecordManager);
if (!(infoItem instanceof CommentsInfoItem)) return;
final CommentsInfoItem item = (CommentsInfoItem) infoItem;

View File

@@ -2,7 +2,6 @@ package org.schabi.newpipe.info_list.holder;
import android.support.v7.app.AppCompatActivity;
import android.text.util.Linkify;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
@@ -11,6 +10,7 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.util.CommentTextOnTouchListener;
import org.schabi.newpipe.util.ImageDisplayConstants;
@@ -46,7 +46,7 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
if(hours != null) timestamp += (Integer.parseInt(hours.replace(":", ""))*3600);
if(minutes != null) timestamp += (Integer.parseInt(minutes.replace(":", ""))*60);
if(seconds != null) timestamp += (Integer.parseInt(seconds));
return streamUrl + url.replace(match.group(0), "#timestamp=" + String.valueOf(timestamp));
return streamUrl + url.replace(match.group(0), "#timestamp=" + timestamp);
}
};
@@ -65,7 +65,7 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
}
@Override
public void updateFromItem(final InfoItem infoItem) {
public void updateFromItem(final InfoItem infoItem, final HistoryRecordManager historyRecordManager) {
if (!(infoItem instanceof CommentsInfoItem)) return;
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
@@ -74,20 +74,17 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
itemThumbnailView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if(StringUtil.isBlank(item.getAuthorEndpoint())) return;
try {
final AppCompatActivity activity = (AppCompatActivity) itemBuilder.getContext();
NavigationHelper.openChannelFragment(
activity.getSupportFragmentManager(),
item.getServiceId(),
item.getAuthorEndpoint(),
item.getAuthorName());
} catch (Exception e) {
ErrorActivity.reportUiError((AppCompatActivity) itemBuilder.getContext(), e);
}
itemThumbnailView.setOnClickListener(view -> {
if(StringUtil.isBlank(item.getAuthorEndpoint())) return;
try {
final AppCompatActivity activity = (AppCompatActivity) itemBuilder.getContext();
NavigationHelper.openChannelFragment(
activity.getSupportFragmentManager(),
item.getServiceId(),
item.getAuthorEndpoint(),
item.getAuthorName());
} catch (Exception e) {
ErrorActivity.reportUiError((AppCompatActivity) itemBuilder.getContext(), e);
}
});
@@ -99,7 +96,7 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
itemContentView.setOnTouchListener(CommentTextOnTouchListener.INSTANCE);
if (itemContentView.getLineCount() == 0) {
itemContentView.post(() -> ellipsize());
itemContentView.post(this::ellipsize);
} else {
ellipsize();
}

View File

@@ -6,6 +6,7 @@ import android.view.ViewGroup;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
/*
* Created by Christian Schabesberger on 12.02.17.
@@ -35,5 +36,8 @@ public abstract class InfoItemHolder extends RecyclerView.ViewHolder {
this.itemBuilder = infoItemBuilder;
}
public abstract void updateFromItem(final InfoItem infoItem);
public abstract void updateFromItem(final InfoItem infoItem, final HistoryRecordManager historyRecordManager);
public void updateState(final InfoItem infoItem, final HistoryRecordManager historyRecordManager) {
}
}

View File

@@ -8,6 +8,7 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.ImageDisplayConstants;
public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
@@ -30,7 +31,7 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
}
@Override
public void updateFromItem(final InfoItem infoItem) {
public void updateFromItem(final InfoItem infoItem, final HistoryRecordManager historyRecordManager) {
if (!(infoItem instanceof PlaylistInfoItem)) return;
final PlaylistInfoItem item = (PlaylistInfoItem) infoItem;

View File

@@ -8,6 +8,7 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.Localization;
/*
@@ -40,8 +41,8 @@ public class StreamInfoItemHolder extends StreamMiniInfoItemHolder {
}
@Override
public void updateFromItem(final InfoItem infoItem) {
super.updateFromItem(infoItem);
public void updateFromItem(final InfoItem infoItem, final HistoryRecordManager historyRecordManager) {
super.updateFromItem(infoItem, historyRecordManager);
if (!(infoItem instanceof StreamInfoItem)) return;
final StreamInfoItem item = (StreamInfoItem) infoItem;

View File

@@ -7,12 +7,18 @@ import android.widget.ImageView;
import android.widget.TextView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.views.AnimatedProgressBar;
import java.util.concurrent.TimeUnit;
public class StreamMiniInfoItemHolder extends InfoItemHolder {
@@ -20,6 +26,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
public final TextView itemVideoTitleView;
public final TextView itemUploaderView;
public final TextView itemDurationView;
public final AnimatedProgressBar itemProgressView;
StreamMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
super(infoItemBuilder, layoutId, parent);
@@ -28,6 +35,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
itemVideoTitleView = itemView.findViewById(R.id.itemVideoTitleView);
itemUploaderView = itemView.findViewById(R.id.itemUploaderView);
itemDurationView = itemView.findViewById(R.id.itemDurationView);
itemProgressView = itemView.findViewById(R.id.itemProgressView);
}
public StreamMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
@@ -35,7 +43,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
}
@Override
public void updateFromItem(final InfoItem infoItem) {
public void updateFromItem(final InfoItem infoItem, final HistoryRecordManager historyRecordManager) {
if (!(infoItem instanceof StreamInfoItem)) return;
final StreamInfoItem item = (StreamInfoItem) infoItem;
@@ -47,13 +55,24 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
R.color.duration_background_color));
itemDurationView.setVisibility(View.VISIBLE);
StreamStateEntity state2 = historyRecordManager.loadStreamState(infoItem).blockingGet()[0];
if (state2 != null) {
itemProgressView.setVisibility(View.VISIBLE);
itemProgressView.setMax((int) item.getDuration());
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state2.getProgressTime()));
} else {
itemProgressView.setVisibility(View.GONE);
}
} else if (item.getStreamType() == StreamType.LIVE_STREAM) {
itemDurationView.setText(R.string.duration_live);
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
R.color.live_duration_background_color));
itemDurationView.setVisibility(View.VISIBLE);
itemProgressView.setVisibility(View.GONE);
} else {
itemDurationView.setVisibility(View.GONE);
itemProgressView.setVisibility(View.GONE);
}
// Default thumbnail is shown on error, while loading and if the url is empty
@@ -83,6 +102,24 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
}
}
@Override
public void updateState(final InfoItem infoItem, final HistoryRecordManager historyRecordManager) {
final StreamInfoItem item = (StreamInfoItem) infoItem;
StreamStateEntity state = historyRecordManager.loadStreamState(infoItem).blockingGet()[0];
if (state != null && item.getDuration() > 0 && item.getStreamType() != StreamType.LIVE_STREAM) {
itemProgressView.setMax((int) item.getDuration());
if (itemProgressView.getVisibility() == View.VISIBLE) {
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
} else {
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
AnimationUtils.animateView(itemProgressView, true, 500);
}
} else if (itemProgressView.getVisibility() == View.VISIBLE) {
AnimationUtils.animateView(itemProgressView, false, 500);
}
}
private void enableLongClick(final StreamInfoItem item) {
itemView.setLongClickable(true);
itemView.setOnLongClickListener(view -> {
@@ -97,4 +134,4 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
itemView.setLongClickable(false);
itemView.setOnLongClickListener(null);
}
}
}

View File

@@ -1,6 +1,8 @@
package org.schabi.newpipe.local;
import android.app.Activity;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
@@ -8,6 +10,8 @@ import android.view.View;
import android.view.ViewGroup;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.local.holder.LocalItemHolder;
import org.schabi.newpipe.local.holder.LocalPlaylistGridItemHolder;
import org.schabi.newpipe.local.holder.LocalPlaylistItemHolder;
@@ -64,6 +68,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
private final LocalItemBuilder localItemBuilder;
private final ArrayList<LocalItem> localItems;
private final HistoryRecordManager recordManager;
private final DateFormat dateFormat;
private boolean showFooter = false;
@@ -71,11 +76,12 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
private View header = null;
private View footer = null;
public LocalItemListAdapter(Activity activity) {
localItemBuilder = new LocalItemBuilder(activity);
public LocalItemListAdapter(Context context) {
recordManager = new HistoryRecordManager(context);
localItemBuilder = new LocalItemBuilder(context);
localItems = new ArrayList<>();
dateFormat = DateFormat.getDateInstance(DateFormat.SHORT,
Localization.getPreferredLocale(activity));
Localization.getPreferredLocale(context));
}
public void setSelectedListener(OnClickGesture<LocalItem> listener) {
@@ -86,38 +92,33 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
localItemBuilder.setOnItemSelectedListener(null);
}
public void addItems(List<? extends LocalItem> data) {
if (data != null) {
if (DEBUG) {
Log.d(TAG, "addItems() before > localItems.size() = " +
localItems.size() + ", data.size() = " + data.size());
}
public void addItems(@Nullable List<? extends LocalItem> data) {
if (data == null) {
return;
}
if (DEBUG) Log.d(TAG, "addItems() before > localItems.size() = " +
localItems.size() + ", data.size() = " + data.size());
int offsetStart = sizeConsideringHeader();
localItems.addAll(data);
int offsetStart = sizeConsideringHeader();
localItems.addAll(data);
if (DEBUG) {
Log.d(TAG, "addItems() after > offsetStart = " + offsetStart +
", localItems.size() = " + localItems.size() +
", header = " + header + ", footer = " + footer +
", showFooter = " + showFooter);
}
if (DEBUG) Log.d(TAG, "addItems() after > offsetStart = " + offsetStart +
", localItems.size() = " + localItems.size() +
", header = " + header + ", footer = " + footer +
", showFooter = " + showFooter);
notifyItemRangeInserted(offsetStart, data.size());
notifyItemRangeInserted(offsetStart, data.size());
if (footer != null && showFooter) {
int footerNow = sizeConsideringHeader();
notifyItemMoved(offsetStart, footerNow);
if (footer != null && showFooter) {
int footerNow = sizeConsideringHeader();
notifyItemMoved(offsetStart, footerNow);
if (DEBUG) Log.d(TAG, "addItems() footer from " + offsetStart +
" to " + footerNow);
}
if (DEBUG) Log.d(TAG, "addItems() footer from " + offsetStart +
" to " + footerNow);
}
}
public void removeItem(final LocalItem data) {
final int index = localItems.indexOf(data);
localItems.remove(index);
notifyItemRemoved(index + (header != null ? 1 : 0));
}
@@ -219,8 +220,9 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
}
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int type) {
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int type) {
if (DEBUG) Log.d(TAG, "onCreateViewHolder() called with: parent = [" +
parent + "], type = [" + type + "]");
switch (type) {
@@ -251,7 +253,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
if (DEBUG) Log.d(TAG, "onBindViewHolder() called with: holder = [" +
holder.getClass().getSimpleName() + "], position = [" + position + "]");
@@ -259,7 +261,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
// If header isn't null, offset the items by -1
if (header != null) position--;
((LocalItemHolder) holder).updateFromItem(localItems.get(position), dateFormat);
((LocalItemHolder) holder).updateFromItem(localItems.get(position), recordManager, dateFormat);
} else if (holder instanceof HeaderFooterHolder && position == 0 && header != null) {
((HeaderFooterHolder) holder).view = header;
} else if (holder instanceof HeaderFooterHolder && position == sizeConsideringHeader()
@@ -268,6 +270,21 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
}
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List<Object> payloads) {
if (!payloads.isEmpty() && holder instanceof LocalItemHolder) {
for (Object payload : payloads) {
if (payload instanceof StreamStateEntity) {
((LocalItemHolder) holder).updateState(localItems.get(header == null ? position : position - 1), recordManager);
} else if (payload instanceof Boolean) {
((LocalItemHolder) holder).updateState(localItems.get(header == null ? position : position - 1), recordManager);
}
}
} else {
onBindViewHolder(holder, position);
}
}
public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final int spanCount) {
return new GridLayoutManager.SpanSizeLookup() {
@Override

View File

@@ -1,6 +1,5 @@
package org.schabi.newpipe.local.dialog;
import android.annotation.SuppressLint;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@@ -28,7 +27,7 @@ import java.util.Collections;
import java.util.List;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.disposables.CompositeDisposable;
public final class PlaylistAppendDialog extends PlaylistDialog {
private static final String TAG = PlaylistAppendDialog.class.getCanonicalName();
@@ -36,7 +35,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
private RecyclerView playlistRecyclerView;
private LocalItemListAdapter playlistAdapter;
private Disposable playlistReactor;
private CompositeDisposable playlistDisposables = new CompositeDisposable();
public static PlaylistAppendDialog fromStreamInfo(final StreamInfo info) {
PlaylistAppendDialog dialog = new PlaylistAppendDialog();
@@ -99,9 +98,9 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
final View newPlaylistButton = view.findViewById(R.id.newPlaylist);
newPlaylistButton.setOnClickListener(ignored -> openCreatePlaylistDialog());
playlistReactor = playlistManager.getPlaylists()
playlistDisposables.add(playlistManager.getPlaylists()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onPlaylistsReceived);
.subscribe(this::onPlaylistsReceived));
}
/*//////////////////////////////////////////////////////////////////////////
@@ -111,10 +110,12 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
@Override
public void onDestroyView() {
super.onDestroyView();
if (playlistReactor != null) playlistReactor.dispose();
if (playlistAdapter != null) playlistAdapter.unsetSelectedListener();
playlistDisposables.dispose();
if (playlistAdapter != null) {
playlistAdapter.unsetSelectedListener();
}
playlistReactor = null;
playlistDisposables.clear();
playlistRecyclerView = null;
playlistAdapter = null;
}
@@ -148,13 +149,12 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
@NonNull List<StreamEntity> streams) {
if (getStreams() == null) return;
@SuppressLint("ShowToast")
final Toast successToast = Toast.makeText(getContext(),
R.string.playlist_add_stream_success, Toast.LENGTH_SHORT);
manager.appendToPlaylist(playlist.uid, streams)
playlistDisposables.add(manager.appendToPlaylist(playlist.uid, streams)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> successToast.show());
.subscribe(ignored -> successToast.show()));
getDialog().dismiss();
}

View File

@@ -21,8 +21,8 @@ import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.fragments.list.BaseListFragment;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.local.subscription.SubscriptionService;
import org.schabi.newpipe.report.UserAction;
import java.util.Collections;
import java.util.HashSet;
@@ -262,7 +262,7 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi
* If chosen feed already displayed, then we request another feed from another
* subscription, until the subscription table runs out of new items.
* <p>
* This Observer is self-contained and will dispose itself when complete. However, this
* This Observer is self-contained and will close itself when complete. However, this
* does not obey the fragment lifecycle and may continue running in the background
* until it is complete. This is done due to RxJava2 no longer propagate errors once
* an observer is unsubscribed while the thread process is still running.

View File

@@ -26,25 +26,32 @@ import android.support.annotation.NonNull;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.AppDatabase;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.history.dao.SearchHistoryDAO;
import org.schabi.newpipe.database.history.dao.StreamHistoryDAO;
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
import org.schabi.newpipe.database.history.model.StreamHistoryEntry;
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
import org.schabi.newpipe.database.stream.dao.StreamDAO;
import org.schabi.newpipe.database.stream.dao.StreamStateDAO;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import io.reactivex.Completable;
import io.reactivex.Flowable;
import io.reactivex.Maybe;
import io.reactivex.Scheduler;
import io.reactivex.Single;
import io.reactivex.schedulers.Schedulers;
@@ -80,9 +87,9 @@ public class HistoryRecordManager {
final Date currentTime = new Date();
return Maybe.fromCallable(() -> database.runInTransaction(() -> {
final long streamId = streamTable.upsert(new StreamEntity(info));
StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry();
StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry(streamId);
if (latestEntry != null && latestEntry.getStreamUid() == streamId) {
if (latestEntry != null) {
streamHistoryTable.delete(latestEntry);
latestEntry.setAccessDate(currentTime);
latestEntry.setRepeatCount(latestEntry.getRepeatCount() + 1);
@@ -99,7 +106,12 @@ public class HistoryRecordManager {
}
public Single<Integer> deleteWholeStreamHistory() {
return Single.fromCallable(() -> streamHistoryTable.deleteAll())
return Single.fromCallable(streamHistoryTable::deleteAll)
.subscribeOn(Schedulers.io());
}
public Single<Integer> deleteCompelteStreamStateHistory() {
return Single.fromCallable(streamStateTable::deleteAll)
.subscribeOn(Schedulers.io());
}
@@ -159,8 +171,8 @@ public class HistoryRecordManager {
.subscribeOn(Schedulers.io());
}
public Single<Integer> deleteWholeSearchHistory() {
return Single.fromCallable(() -> searchHistoryTable.deleteAll())
public Single<Integer> deleteCompleteSearchHistory() {
return Single.fromCallable(searchHistoryTable::deleteAll)
.subscribeOn(Schedulers.io());
}
@@ -180,21 +192,104 @@ public class HistoryRecordManager {
// Stream State History
///////////////////////////////////////////////////////
@SuppressWarnings("unused")
public Maybe<StreamStateEntity> loadStreamState(final StreamInfo info) {
return Maybe.fromCallable(() -> streamTable.upsert(new StreamEntity(info)))
.flatMap(streamId -> streamStateTable.getState(streamId).firstElement())
.flatMap(states -> states.isEmpty() ? Maybe.empty() : Maybe.just(states.get(0)))
public Maybe<StreamHistoryEntity> getStreamHistory(final StreamInfo info) {
return Maybe.fromCallable(() -> {
final long streamId = streamTable.upsert(new StreamEntity(info));
return streamHistoryTable.getLatestEntry(streamId);
}).subscribeOn(Schedulers.io());
}
public Maybe<StreamStateEntity> loadStreamState(final PlayQueueItem queueItem) {
return queueItem.getStream()
.map((info) -> streamTable.upsert(new StreamEntity(info)))
.flatMapPublisher(streamStateTable::getState)
.firstElement()
.flatMap(list -> list.isEmpty() ? Maybe.empty() : Maybe.just(list.get(0)))
.filter(state -> state.isValid((int) queueItem.getDuration()))
.subscribeOn(Schedulers.io());
}
public Maybe<Long> saveStreamState(@NonNull final StreamInfo info, final long progressTime) {
return Maybe.fromCallable(() -> database.runInTransaction(() -> {
public Maybe<StreamStateEntity> loadStreamState(final StreamInfo info) {
return Single.fromCallable(() -> streamTable.upsert(new StreamEntity(info)))
.flatMapPublisher(streamStateTable::getState)
.firstElement()
.flatMap(list -> list.isEmpty() ? Maybe.empty() : Maybe.just(list.get(0)))
.filter(state -> state.isValid((int) info.getDuration()))
.subscribeOn(Schedulers.io());
}
public Completable saveStreamState(@NonNull final StreamInfo info, final long progressTime) {
return Completable.fromAction(() -> database.runInTransaction(() -> {
final long streamId = streamTable.upsert(new StreamEntity(info));
return streamStateTable.upsert(new StreamStateEntity(streamId, progressTime));
final StreamStateEntity state = new StreamStateEntity(streamId, progressTime);
if (state.isValid((int) info.getDuration())) {
streamStateTable.upsert(state);
} else {
streamStateTable.deleteState(streamId);
}
})).subscribeOn(Schedulers.io());
}
public Single<StreamStateEntity[]> loadStreamState(final InfoItem info) {
return Single.fromCallable(() -> {
final List<StreamEntity> entities = streamTable.getStream(info.getServiceId(), info.getUrl()).blockingFirst();
if (entities.isEmpty()) {
return new StreamStateEntity[]{null};
}
final List<StreamStateEntity> states = streamStateTable.getState(entities.get(0).getUid()).blockingFirst();
if (states.isEmpty()) {
return new StreamStateEntity[]{null};
}
return new StreamStateEntity[]{states.get(0)};
}).subscribeOn(Schedulers.io());
}
public Single<List<StreamStateEntity>> loadStreamStateBatch(final List<InfoItem> infos) {
return Single.fromCallable(() -> {
final List<StreamStateEntity> result = new ArrayList<>(infos.size());
for (InfoItem info : infos) {
final List<StreamEntity> entities = streamTable.getStream(info.getServiceId(), info.getUrl()).blockingFirst();
if (entities.isEmpty()) {
result.add(null);
continue;
}
final List<StreamStateEntity> states = streamStateTable.getState(entities.get(0).getUid()).blockingFirst();
if (states.isEmpty()) {
result.add(null);
continue;
}
result.add(states.get(0));
}
return result;
}).subscribeOn(Schedulers.io());
}
public Single<List<StreamStateEntity>> loadLocalStreamStateBatch(final List<? extends LocalItem> items) {
return Single.fromCallable(() -> {
final List<StreamStateEntity> result = new ArrayList<>(items.size());
for (LocalItem item : items) {
long streamId;
if (item instanceof StreamStatisticsEntry) {
streamId = ((StreamStatisticsEntry) item).streamId;
} else if (item instanceof PlaylistStreamEntity) {
streamId = ((PlaylistStreamEntity) item).getStreamUid();
} else if (item instanceof PlaylistStreamEntry) {
streamId = ((PlaylistStreamEntry) item).streamId;
} else {
result.add(null);
continue;
}
final List<StreamStateEntity> states = streamStateTable.getState(streamId).blockingFirst();
if (states.isEmpty()) {
result.add(null);
continue;
}
result.add(states.get(0));
}
return result;
}).subscribeOn(Schedulers.io());
}
///////////////////////////////////////////////////////
// Utility
///////////////////////////////////////////////////////
@@ -202,4 +297,5 @@ public class HistoryRecordManager {
public Single<Integer> removeOrphanedRecords() {
return Single.fromCallable(streamTable::deleteOrphans).subscribeOn(Schedulers.io());
}
}

View File

@@ -2,7 +2,6 @@ package org.schabi.newpipe.local.history;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
@@ -23,8 +22,10 @@ import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.info_list.InfoItemDialog;
import org.schabi.newpipe.local.BaseLocalListFragment;
import org.schabi.newpipe.player.playqueue.PlayQueue;
@@ -34,7 +35,7 @@ import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
import org.schabi.newpipe.util.ShareUtils;
import org.schabi.newpipe.util.StreamDialogEntry;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.ArrayList;
@@ -180,7 +181,7 @@ public class StatisticsPlaylistFragment
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
howManyDeleted -> Toast.makeText(getContext(),
R.string.view_history_deleted,
R.string.watch_history_deleted,
Toast.LENGTH_SHORT).show(),
throwable -> ErrorActivity.reportError(getContext(),
throwable,
@@ -310,11 +311,11 @@ public class StatisticsPlaylistFragment
}
headerPlayAllButton.setOnClickListener(view ->
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false));
headerPopupButton.setOnClickListener(view ->
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
headerBackgroundButton.setOnClickListener(view ->
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
sortButton.setOnClickListener(view -> toggleSortMode());
hideLoading();
@@ -357,52 +358,44 @@ public class StatisticsPlaylistFragment
startLoading(true);
}
private PlayQueue getPlayQueueStartingAt(StreamStatisticsEntry infoItem) {
return getPlayQueue(Math.max(itemListAdapter.getItemsList().indexOf(infoItem), 0));
}
private void showStreamDialog(final StreamStatisticsEntry item) {
final Context context = getContext();
final Activity activity = getActivity();
if (context == null || context.getResources() == null || getActivity() == null) return;
if (context == null || context.getResources() == null || activity == null) return;
final StreamInfoItem infoItem = item.toStreamInfoItem();
final String[] commands = new String[]{
context.getResources().getString(R.string.enqueue_on_background),
context.getResources().getString(R.string.enqueue_on_popup),
context.getResources().getString(R.string.start_here_on_main),
context.getResources().getString(R.string.start_here_on_background),
context.getResources().getString(R.string.start_here_on_popup),
context.getResources().getString(R.string.delete),
context.getResources().getString(R.string.share)
};
if (infoItem.getStreamType() == StreamType.AUDIO_STREAM) {
StreamDialogEntry.setEnabledEntries(
StreamDialogEntry.enqueue_on_background,
StreamDialogEntry.start_here_on_background,
StreamDialogEntry.delete,
StreamDialogEntry.append_playlist,
StreamDialogEntry.share);
} else {
StreamDialogEntry.setEnabledEntries(
StreamDialogEntry.enqueue_on_background,
StreamDialogEntry.enqueue_on_popup,
StreamDialogEntry.start_here_on_background,
StreamDialogEntry.start_here_on_popup,
StreamDialogEntry.delete,
StreamDialogEntry.append_playlist,
StreamDialogEntry.share);
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
final int index = Math.max(itemListAdapter.getItemsList().indexOf(item), 0);
switch (i) {
case 0:
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(infoItem));
break;
case 1:
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(infoItem));
break;
case 2:
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index));
break;
case 3:
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index));
break;
case 4:
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
break;
case 5:
deleteEntry(index);
break;
case 6:
ShareUtils.shareUrl(this.getContext(), item.toStreamInfoItem().getName(), item.toStreamInfoItem().getUrl());
break;
default:
break;
}
};
StreamDialogEntry.start_here_on_popup.setCustomAction(
(fragment, infoItemDuplicate) -> NavigationHelper.playOnPopupPlayer(context, getPlayQueueStartingAt(item), true));
}
new InfoItemDialog(getActivity(), infoItem, commands, actions).show();
StreamDialogEntry.start_here_on_background.setCustomAction(
(fragment, infoItemDuplicate) -> NavigationHelper.playOnBackgroundPlayer(context, getPlayQueueStartingAt(item), true));
StreamDialogEntry.delete.setCustomAction((fragment, infoItemDuplicate) ->
deleteEntry(Math.max(itemListAdapter.getItemsList().indexOf(item), 0)));
new InfoItemDialog(activity, infoItem, StreamDialogEntry.getCommands(context), (dialog, which) ->
StreamDialogEntry.clickOn(which, this, infoItem)).show();
}
private void deleteEntry(final int index) {

View File

@@ -6,6 +6,7 @@ import android.view.ViewGroup;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import java.text.DateFormat;
@@ -38,5 +39,8 @@ public abstract class LocalItemHolder extends RecyclerView.ViewHolder {
this.itemBuilder = itemBuilder;
}
public abstract void updateFromItem(final LocalItem item, final DateFormat dateFormat);
public abstract void updateFromItem(final LocalItem item, HistoryRecordManager historyRecordManager, final DateFormat dateFormat);
public void updateState(final LocalItem localItem, HistoryRecordManager historyRecordManager) {
}
}

View File

@@ -6,6 +6,7 @@ import android.view.ViewGroup;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.ImageDisplayConstants;
import java.text.DateFormat;
@@ -21,7 +22,7 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
}
@Override
public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) {
public void updateFromItem(final LocalItem localItem, HistoryRecordManager historyRecordManager, final DateFormat dateFormat) {
if (!(localItem instanceof PlaylistMetadataEntry)) return;
final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem;
@@ -32,6 +33,6 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView,
ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS);
super.updateFromItem(localItem, dateFormat);
super.updateFromItem(localItem, historyRecordManager, dateFormat);
}
}

View File

@@ -10,12 +10,18 @@ import android.widget.TextView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.views.AnimatedProgressBar;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
@@ -24,6 +30,7 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
public final TextView itemAdditionalDetailsView;
public final TextView itemDurationView;
public final View itemHandleView;
public final AnimatedProgressBar itemProgressView;
LocalPlaylistStreamItemHolder(LocalItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
super(infoItemBuilder, layoutId, parent);
@@ -33,6 +40,7 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
itemAdditionalDetailsView = itemView.findViewById(R.id.itemAdditionalDetails);
itemDurationView = itemView.findViewById(R.id.itemDurationView);
itemHandleView = itemView.findViewById(R.id.itemHandle);
itemProgressView = itemView.findViewById(R.id.itemProgressView);
}
public LocalPlaylistStreamItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) {
@@ -40,7 +48,7 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
}
@Override
public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) {
public void updateFromItem(final LocalItem localItem, HistoryRecordManager historyRecordManager, final DateFormat dateFormat) {
if (!(localItem instanceof PlaylistStreamEntry)) return;
final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem;
@@ -53,6 +61,15 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
R.color.duration_background_color));
itemDurationView.setVisibility(View.VISIBLE);
StreamStateEntity state = historyRecordManager.loadLocalStreamStateBatch(new ArrayList<LocalItem>() {{ add(localItem); }}).blockingGet().get(0);
if (state != null) {
itemProgressView.setVisibility(View.VISIBLE);
itemProgressView.setMax((int) item.duration);
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
} else {
itemProgressView.setVisibility(View.GONE);
}
} else {
itemDurationView.setVisibility(View.GONE);
}
@@ -79,6 +96,25 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
itemHandleView.setOnTouchListener(getOnTouchListener(item));
}
@Override
public void updateState(LocalItem localItem, HistoryRecordManager historyRecordManager) {
if (!(localItem instanceof PlaylistStreamEntry)) return;
final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem;
StreamStateEntity state = historyRecordManager.loadLocalStreamStateBatch(new ArrayList<LocalItem>() {{ add(localItem); }}).blockingGet().get(0);
if (state != null && item.duration > 0) {
itemProgressView.setMax((int) item.duration);
if (itemProgressView.getVisibility() == View.VISIBLE) {
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
} else {
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
AnimationUtils.animateView(itemProgressView, true, 500);
}
} else if (itemProgressView.getVisibility() == View.VISIBLE) {
AnimationUtils.animateView(itemProgressView, false, 500);
}
}
private View.OnTouchListener getOnTouchListener(final PlaylistStreamEntry item) {
return (view, motionEvent) -> {
view.performClick();

View File

@@ -10,12 +10,18 @@ import android.widget.TextView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.views.AnimatedProgressBar;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
/*
* Created by Christian Schabesberger on 01.08.16.
@@ -45,6 +51,7 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
public final TextView itemDurationView;
@Nullable
public final TextView itemAdditionalDetails;
public final AnimatedProgressBar itemProgressView;
public LocalStatisticStreamItemHolder(LocalItemBuilder itemBuilder, ViewGroup parent) {
this(itemBuilder, R.layout.list_stream_item, parent);
@@ -58,6 +65,7 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
itemUploaderView = itemView.findViewById(R.id.itemUploaderView);
itemDurationView = itemView.findViewById(R.id.itemDurationView);
itemAdditionalDetails = itemView.findViewById(R.id.itemAdditionalDetails);
itemProgressView = itemView.findViewById(R.id.itemProgressView);
}
private String getStreamInfoDetailLine(final StreamStatisticsEntry entry,
@@ -70,7 +78,7 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
}
@Override
public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) {
public void updateFromItem(final LocalItem localItem, HistoryRecordManager historyRecordManager, final DateFormat dateFormat) {
if (!(localItem instanceof StreamStatisticsEntry)) return;
final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem;
@@ -82,8 +90,18 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
R.color.duration_background_color));
itemDurationView.setVisibility(View.VISIBLE);
StreamStateEntity state = historyRecordManager.loadLocalStreamStateBatch(new ArrayList<LocalItem>() {{ add(localItem); }}).blockingGet().get(0);
if (state != null) {
itemProgressView.setVisibility(View.VISIBLE);
itemProgressView.setMax((int) item.duration);
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
} else {
itemProgressView.setVisibility(View.GONE);
}
} else {
itemDurationView.setVisibility(View.GONE);
itemProgressView.setVisibility(View.GONE);
}
if (itemAdditionalDetails != null) {
@@ -108,4 +126,23 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
return true;
});
}
@Override
public void updateState(LocalItem localItem, HistoryRecordManager historyRecordManager) {
if (!(localItem instanceof StreamStatisticsEntry)) return;
final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem;
StreamStateEntity state = historyRecordManager.loadLocalStreamStateBatch(new ArrayList<LocalItem>() {{ add(localItem); }}).blockingGet().get(0);
if (state != null && item.duration > 0) {
itemProgressView.setMax((int) item.duration);
if (itemProgressView.getVisibility() == View.VISIBLE) {
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
} else {
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
AnimationUtils.animateView(itemProgressView, true, 500);
}
} else if (itemProgressView.getVisibility() == View.VISIBLE) {
AnimationUtils.animateView(itemProgressView, false, 500);
}
}
}

View File

@@ -7,6 +7,7 @@ import android.widget.TextView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import java.text.DateFormat;
@@ -31,7 +32,7 @@ public abstract class PlaylistItemHolder extends LocalItemHolder {
}
@Override
public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) {
public void updateFromItem(final LocalItem localItem, HistoryRecordManager historyRecordManager, final DateFormat dateFormat) {
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnItemSelectedListener() != null) {
itemBuilder.getOnItemSelectedListener().selected(localItem);

View File

@@ -6,6 +6,7 @@ import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
@@ -21,7 +22,7 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
}
@Override
public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) {
public void updateFromItem(final LocalItem localItem, HistoryRecordManager historyRecordManager, final DateFormat dateFormat) {
if (!(localItem instanceof PlaylistRemoteEntity)) return;
final PlaylistRemoteEntity item = (PlaylistRemoteEntity) localItem;
@@ -33,6 +34,6 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
itemBuilder.displayImage(item.getThumbnailUrl(), itemThumbnailView,
ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS);
super.updateFromItem(localItem, dateFormat);
super.updateFromItem(localItem, historyRecordManager, dateFormat);
}
}

View File

@@ -2,7 +2,6 @@ package org.schabi.newpipe.local.playlist;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
@@ -26,15 +25,16 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.local.BaseLocalListFragment;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.info_list.InfoItemDialog;
import org.schabi.newpipe.local.BaseLocalListFragment;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
import org.schabi.newpipe.util.ShareUtils;
import org.schabi.newpipe.util.StreamDialogEntry;
import java.util.ArrayList;
import java.util.Collections;
@@ -319,11 +319,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
setVideoCount(itemListAdapter.getItemsList().size());
headerPlayAllButton.setOnClickListener(view ->
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false));
headerPopupButton.setOnClickListener(view ->
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
headerBackgroundButton.setOnClickListener(view ->
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
hideLoading();
}
@@ -511,59 +511,48 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
// Utils
//////////////////////////////////////////////////////////////////////////*/
private PlayQueue getPlayQueueStartingAt(PlaylistStreamEntry infoItem) {
return getPlayQueue(Math.max(itemListAdapter.getItemsList().indexOf(infoItem), 0));
}
protected void showStreamItemDialog(final PlaylistStreamEntry item) {
final Context context = getContext();
final Activity activity = getActivity();
if (context == null || context.getResources() == null || getActivity() == null) return;
if (context == null || context.getResources() == null || activity == null) return;
final StreamInfoItem infoItem = item.toStreamInfoItem();
final String[] commands = new String[]{
context.getResources().getString(R.string.enqueue_on_background),
context.getResources().getString(R.string.enqueue_on_popup),
context.getResources().getString(R.string.start_here_on_main),
context.getResources().getString(R.string.start_here_on_background),
context.getResources().getString(R.string.start_here_on_popup),
context.getResources().getString(R.string.set_as_playlist_thumbnail),
context.getResources().getString(R.string.delete),
context.getResources().getString(R.string.share)
};
if (infoItem.getStreamType() == StreamType.AUDIO_STREAM) {
StreamDialogEntry.setEnabledEntries(
StreamDialogEntry.enqueue_on_background,
StreamDialogEntry.start_here_on_background,
StreamDialogEntry.set_as_playlist_thumbnail,
StreamDialogEntry.delete,
StreamDialogEntry.append_playlist,
StreamDialogEntry.share);
} else {
StreamDialogEntry.setEnabledEntries(
StreamDialogEntry.enqueue_on_background,
StreamDialogEntry.enqueue_on_popup,
StreamDialogEntry.start_here_on_background,
StreamDialogEntry.start_here_on_popup,
StreamDialogEntry.set_as_playlist_thumbnail,
StreamDialogEntry.delete,
StreamDialogEntry.append_playlist,
StreamDialogEntry.share);
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
final int index = Math.max(itemListAdapter.getItemsList().indexOf(item), 0);
switch (i) {
case 0:
NavigationHelper.enqueueOnBackgroundPlayer(context,
new SinglePlayQueue(infoItem));
break;
case 1:
NavigationHelper.enqueueOnPopupPlayer(activity, new
SinglePlayQueue(infoItem));
break;
case 2:
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index));
break;
case 3:
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index));
break;
case 4:
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
break;
case 5:
changeThumbnailUrl(item.thumbnailUrl);
break;
case 6:
deleteItem(item);
break;
case 7:
ShareUtils.shareUrl(this.getContext(), item.toStreamInfoItem().getName(), item.toStreamInfoItem().getUrl());
break;
default:
break;
}
};
StreamDialogEntry.start_here_on_popup.setCustomAction(
(fragment, infoItemDuplicate) -> NavigationHelper.playOnPopupPlayer(context, getPlayQueueStartingAt(item), true));
}
new InfoItemDialog(getActivity(), infoItem, commands, actions).show();
StreamDialogEntry.start_here_on_background.setCustomAction(
(fragment, infoItemDuplicate) -> NavigationHelper.playOnBackgroundPlayer(context, getPlayQueueStartingAt(item), true));
StreamDialogEntry.set_as_playlist_thumbnail.setCustomAction(
(fragment, infoItemDuplicate) -> changeThumbnailUrl(item.thumbnailUrl));
StreamDialogEntry.delete.setCustomAction(
(fragment, infoItemDuplicate) -> deleteItem(item));
new InfoItemDialog(activity, infoItem, StreamDialogEntry.getCommands(context), (dialog, which) ->
StreamDialogEntry.clickOn(which, this, infoItem)).show();
}
private void setInitialData(long playlistId, String name) {

View File

@@ -23,7 +23,6 @@ import android.support.annotation.Nullable;
import android.support.v4.app.FragmentManager;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
@@ -48,10 +47,8 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.info_list.InfoListAdapter;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService;
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.FilePickerActivityHelper;
import org.schabi.newpipe.util.NavigationHelper;
@@ -131,6 +128,11 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
subscriptionService = SubscriptionService.getInstance(activity);
}
@Override
public void onDetach() {
super.onDetach();
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
@@ -377,7 +379,6 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
});
//noinspection ConstantConditions
whatsNewItemListHeader.setOnClickListener(v -> {
FragmentManager fragmentManager = getFM();
NavigationHelper.openWhatsNewFragment(fragmentManager);
@@ -391,17 +392,17 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
if (context == null || context.getResources() == null || getActivity() == null) return;
final String[] commands = new String[]{
context.getResources().getString(R.string.share),
context.getResources().getString(R.string.unsubscribe)
context.getResources().getString(R.string.unsubscribe),
context.getResources().getString(R.string.share)
};
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
switch (i) {
case 0:
shareChannel(selectedItem);
deleteChannel(selectedItem);
break;
case 1:
deleteChannel(selectedItem);
shareChannel(selectedItem);
break;
default:
break;
@@ -425,12 +426,12 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
}
private void shareChannel (ChannelInfoItem selectedItem) {
ShareUtils.shareUrl(this.getContext(), selectedItem.getName(), selectedItem.getUrl());
private void shareChannel(ChannelInfoItem selectedItem) {
ShareUtils.shareUrl(getContext(), selectedItem.getName(), selectedItem.getUrl());
}
@SuppressLint("CheckResult")
private void deleteChannel (ChannelInfoItem selectedItem) {
private void deleteChannel(ChannelInfoItem selectedItem) {
subscriptionService.subscriptionTable()
.getSubscription(selectedItem.getServiceId(), selectedItem.getUrl())
.toObservable()
@@ -442,7 +443,7 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
private Observer<List<SubscriptionEntity>> getDeleteObserver(){
private Observer<List<SubscriptionEntity>> getDeleteObserver() {
return new Observer<List<SubscriptionEntity>>() {
@Override
public void onSubscribe(Disposable d) {

View File

@@ -150,6 +150,7 @@ public final class BackgroundPlayer extends Service {
lockManager.releaseWifiAndCpu();
}
if (basePlayerImpl != null) {
basePlayerImpl.savePlaybackState();
basePlayerImpl.stopActivityBinding();
basePlayerImpl.destroy();
}

View File

@@ -23,9 +23,11 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.AudioManager;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
@@ -145,6 +147,8 @@ public abstract class BasePlayer implements
@NonNull
public static final String APPEND_ONLY = "append_only";
@NonNull
public static final String RESUME_PLAYBACK = "resume_playback";
@NonNull
public static final String SELECT_ON_APPEND = "select_on_append";
/*//////////////////////////////////////////////////////////////////////////
@@ -183,6 +187,7 @@ public abstract class BasePlayer implements
protected MediaSessionManager mediaSessionManager;
private boolean isPrepared = false;
private Disposable stateLoader;
//////////////////////////////////////////////////////////////////////////*/
@@ -279,8 +284,24 @@ public abstract class BasePlayer implements
) {
simpleExoPlayer.seekTo(playQueue.getIndex(), queue.getItem().getRecoveryPosition());
return;
}
} else if (intent.getBooleanExtra(RESUME_PLAYBACK, false) && isPlaybackResumeEnabled()) {
final PlayQueueItem item = queue.getItem();
if (item != null && item.getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET) {
stateLoader = recordManager.loadStreamState(item)
.observeOn(AndroidSchedulers.mainThread())
.doFinally(() -> initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence,
/*playOnInit=*/true))
.subscribe(
state -> queue.setRecovery(queue.getIndex(), state.getProgressTime()),
error -> {
if (DEBUG) error.printStackTrace();
}
);
databaseUpdateReactor.add(stateLoader);
return;
}
}
// Good to go...
initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence,
/*playOnInit=*/true);
@@ -318,6 +339,7 @@ public abstract class BasePlayer implements
if (audioReactor != null) audioReactor.dispose();
if (playbackManager != null) playbackManager.dispose();
if (mediaSessionManager != null) mediaSessionManager.dispose();
if (stateLoader != null) stateLoader.dispose();
if (playQueueAdapter != null) {
playQueueAdapter.unsetSelectedListener();
@@ -615,6 +637,9 @@ public abstract class BasePlayer implements
break;
case Player.STATE_ENDED: // 4
changeState(STATE_COMPLETED);
if (currentMetadata != null) {
resetPlaybackState(currentMetadata.getMetadata());
}
isPrepared = false;
break;
}
@@ -721,6 +746,7 @@ public abstract class BasePlayer implements
case DISCONTINUITY_REASON_SEEK_ADJUSTMENT:
case DISCONTINUITY_REASON_INTERNAL:
if (playQueue.getIndex() != newWindowIndex) {
resetPlaybackState(playQueue.getItem());
playQueue.setIndex(newWindowIndex);
}
break;
@@ -750,6 +776,9 @@ public abstract class BasePlayer implements
@Override
public void onSeekProcessed() {
if (DEBUG) Log.d(TAG, "ExoPlayer - onSeekProcessed() called");
if (isPrepared) {
savePlaybackState();
}
}
/*//////////////////////////////////////////////////////////////////////////
// Playback Listener
@@ -1017,27 +1046,46 @@ public abstract class BasePlayer implements
}
}
protected void savePlaybackState(final StreamInfo info, final long progress) {
private void savePlaybackState(final StreamInfo info, final long progress) {
if (info == null) return;
final Disposable stateSaver = recordManager.saveStreamState(info, progress)
.observeOn(AndroidSchedulers.mainThread())
.onErrorComplete()
.subscribe(
ignored -> {/* successful */},
error -> Log.e(TAG, "savePlaybackState() failure: ", error)
);
databaseUpdateReactor.add(stateSaver);
if (DEBUG) Log.d(TAG, "savePlaybackState() called");
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
if (prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) {
final Disposable stateSaver = recordManager.saveStreamState(info, progress)
.observeOn(AndroidSchedulers.mainThread())
.doOnError((e) -> {
if (DEBUG) e.printStackTrace();
})
.onErrorComplete()
.subscribe();
databaseUpdateReactor.add(stateSaver);
}
}
private void savePlaybackState() {
private void resetPlaybackState(final PlayQueueItem queueItem) {
if (queueItem == null) return;
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
if (prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) {
final Disposable stateSaver = queueItem.getStream()
.flatMapCompletable(info -> recordManager.saveStreamState(info, 0))
.observeOn(AndroidSchedulers.mainThread())
.doOnError((e) -> {
if (DEBUG) e.printStackTrace();
})
.onErrorComplete()
.subscribe();
databaseUpdateReactor.add(stateSaver);
}
}
public void resetPlaybackState(final StreamInfo info) {
savePlaybackState(info, 0);
}
public void savePlaybackState() {
if (simpleExoPlayer == null || currentMetadata == null) return;
final StreamInfo currentInfo = currentMetadata.getMetadata();
if (simpleExoPlayer.getCurrentPosition() > RECOVERY_SKIP_THRESHOLD_MILLIS &&
simpleExoPlayer.getCurrentPosition() <
simpleExoPlayer.getDuration() - RECOVERY_SKIP_THRESHOLD_MILLIS) {
savePlaybackState(currentInfo, simpleExoPlayer.getCurrentPosition());
}
savePlaybackState(currentInfo, simpleExoPlayer.getCurrentPosition());
}
private void maybeUpdateCurrentMetadata() {
@@ -1225,4 +1273,10 @@ public abstract class BasePlayer implements
public boolean gotDestroyed() {
return simpleExoPlayer == null;
}
private boolean isPlaybackResumeEnabled() {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
return prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)
&& prefs.getBoolean(context.getString(R.string.enable_playback_resume_key), true);
}
}

View File

@@ -24,11 +24,13 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.database.ContentObserver;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.media.AudioManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.support.annotation.ColorInt;
@@ -113,6 +115,8 @@ public final class MainVideoPlayer extends AppCompatActivity
private boolean isInMultiWindow;
private boolean isBackPressed;
private ContentObserver rotationObserver;
/*//////////////////////////////////////////////////////////////////////////
// Activity LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@@ -147,6 +151,23 @@ public final class MainVideoPlayer extends AppCompatActivity
Toast.makeText(this, R.string.general_error, Toast.LENGTH_SHORT).show();
finish();
}
rotationObserver = new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
if (globalScreenOrientationLocked()) {
final boolean lastOrientationWasLandscape = defaultPreferences.getBoolean(
getString(R.string.last_orientation_landscape_key), false);
setLandscape(lastOrientationWasLandscape);
} else {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
}
}
};
getContentResolver().registerContentObserver(
Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION),
false, rotationObserver);
}
@Override
@@ -239,6 +260,9 @@ public final class MainVideoPlayer extends AppCompatActivity
playerState = createPlayerState();
playerImpl.destroy();
if (rotationObserver != null)
getContentResolver().unregisterContentObserver(rotationObserver);
isInMultiWindow = false;
isBackPressed = false;
}
@@ -248,6 +272,12 @@ public final class MainVideoPlayer extends AppCompatActivity
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(newBase));
}
@Override
protected void onPause() {
playerImpl.savePlaybackState();
super.onPause();
}
/*//////////////////////////////////////////////////////////////////////////
// State Saving
//////////////////////////////////////////////////////////////////////////*/
@@ -583,7 +613,8 @@ public final class MainVideoPlayer extends AppCompatActivity
this.getPlaybackSpeed(),
this.getPlaybackPitch(),
this.getPlaybackSkipSilence(),
this.getPlaybackQuality()
this.getPlaybackQuality(),
false
);
context.startService(intent);
@@ -605,7 +636,8 @@ public final class MainVideoPlayer extends AppCompatActivity
this.getPlaybackSpeed(),
this.getPlaybackPitch(),
this.getPlaybackSkipSilence(),
this.getPlaybackQuality()
this.getPlaybackQuality(),
false
);
context.startService(intent);

View File

@@ -325,6 +325,7 @@ public final class PopupVideoPlayer extends Service {
isPopupClosing = true;
if (playerImpl != null) {
playerImpl.savePlaybackState();
if (playerImpl.getRootView() != null) {
windowManager.removeView(playerImpl.getRootView());
}
@@ -565,7 +566,8 @@ public final class PopupVideoPlayer extends Service {
this.getPlaybackSpeed(),
this.getPlaybackPitch(),
this.getPlaybackSkipSilence(),
this.getPlaybackQuality()
this.getPlaybackQuality(),
false
);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);

View File

@@ -188,7 +188,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
this.player.getPlaybackSpeed(),
this.player.getPlaybackPitch(),
this.player.getPlaybackSkipSilence(),
null
null,
false
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}

View File

@@ -297,6 +297,7 @@ public abstract class VideoPlayer extends BasePlayer
return true;
});
// Add all available captions
for (int i = 0; i < availableLanguages.size(); i++) {
final String captionLanguage = availableLanguages.get(i);
MenuItem captionItem = captionPopupMenu.getMenu().add(captionPopupMenuGroupId,
@@ -506,7 +507,7 @@ public abstract class VideoPlayer extends BasePlayer
}
// Normalize mismatching language strings
final String preferredLanguage = trackSelector.getParameters().preferredTextLanguage;
final String preferredLanguage = trackSelector.getPreferredTextLanguage();
// Build UI
buildCaptionMenu(availableLanguages);
if (trackSelector.getParameters().getRendererDisabled(textRenderer) ||
@@ -541,6 +542,11 @@ public abstract class VideoPlayer extends BasePlayer
playbackSpeedTextView.setText(formatSpeed(getPlaybackSpeed()));
super.onPrepared(playWhenReady);
if (simpleExoPlayer.getCurrentPosition() != 0 && !isControlsVisible()) {
controlsVisibilityHandler.removeCallbacksAndMessages(null);
controlsVisibilityHandler.postDelayed(this::showControlsThenHide, DEFAULT_CONTROLS_DURATION);
}
}
@Override

View File

@@ -58,8 +58,11 @@ public class BasePlayerMediaSession implements MediaSessionCallback {
// set additional metadata for A2DP/AVRCP
Bundle additionalMetadata = new Bundle();
additionalMetadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, item.getTitle());
additionalMetadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, item.getUploader());
additionalMetadata.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, item.getDuration());
additionalMetadata.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, item.getDuration() * 1000);
additionalMetadata.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, index + 1);
additionalMetadata.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, player.getPlayQueue().size());
descriptionBuilder.setExtras(additionalMetadata);
final Uri thumbnailUri = Uri.parse(item.getThumbnailUrl());

View File

@@ -158,7 +158,7 @@ public class MediaSourceManager {
* Dispose the manager and releases all message buses and loaders.
* */
public void dispose() {
if (DEBUG) Log.d(TAG, "dispose() called.");
if (DEBUG) Log.d(TAG, "close() called.");
debouncedSignal.onComplete();
debouncedLoader.dispose();

View File

@@ -17,7 +17,9 @@ public enum UserAction {
REQUESTED_KIOSK("requested kiosk"),
REQUESTED_COMMENTS("requested comments"),
DELETE_FROM_HISTORY("delete from history"),
PLAY_STREAM("Play stream");
PLAY_STREAM("Play stream"),
DOWNLOAD_POSTPROCESSING("download post-processing"),
DOWNLOAD_FAILED("download failed");
private final String message;

View File

@@ -274,7 +274,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
else if (v instanceof String)
prefEdit.putString(key, ((String) v));
}
prefEdit.apply();
prefEdit.commit();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {

View File

@@ -1,9 +1,15 @@
package org.schabi.newpipe.settings;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.v7.preference.Preference;
import android.util.Log;
@@ -12,19 +18,56 @@ import com.nononsenseapps.filepicker.Utils;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.FilePickerActivityHelper;
public class DownloadSettingsFragment extends BasePreferenceFragment {
private static final int REQUEST_DOWNLOAD_PATH = 0x1235;
private static final int REQUEST_DOWNLOAD_AUDIO_PATH = 0x1236;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
private String DOWNLOAD_PATH_PREFERENCE;
import us.shandian.giga.io.StoredDirectoryHelper;
public class DownloadSettingsFragment extends BasePreferenceFragment {
private static final int REQUEST_DOWNLOAD_VIDEO_PATH = 0x1235;
private static final int REQUEST_DOWNLOAD_AUDIO_PATH = 0x1236;
public static final boolean IGNORE_RELEASE_ON_OLD_PATH = true;
private String DOWNLOAD_PATH_VIDEO_PREFERENCE;
private String DOWNLOAD_PATH_AUDIO_PREFERENCE;
private Preference prefPathVideo;
private Preference prefPathAudio;
private Preference prefStorageAsk;
private Context ctx;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initKeys();
DOWNLOAD_PATH_VIDEO_PREFERENCE = getString(R.string.download_path_video_key);
DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key);
final String downloadStorageAsk = getString(R.string.downloads_storage_ask);
prefPathVideo = findPreference(DOWNLOAD_PATH_VIDEO_PREFERENCE);
prefPathAudio = findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE);
prefStorageAsk = findPreference(downloadStorageAsk);
updatePreferencesSummary();
updatePathPickers(!defaultPreferences.getBoolean(downloadStorageAsk, false));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
prefStorageAsk.setSummary(R.string.downloads_storage_ask_summary);
}
if (hasInvalidPath(DOWNLOAD_PATH_VIDEO_PREFERENCE) || hasInvalidPath(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
updatePreferencesSummary();
}
prefStorageAsk.setOnPreferenceChangeListener((preference, value) -> {
updatePathPickers(!(boolean) value);
return true;
});
}
@Override
@@ -32,52 +75,184 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
addPreferencesFromResource(R.xml.download_settings);
}
private void initKeys() {
DOWNLOAD_PATH_PREFERENCE = getString(R.string.download_path_key);
DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key);
@Override
public void onAttach(Context context) {
super.onAttach(context);
ctx = context;
}
@Override
public void onDetach() {
super.onDetach();
ctx = null;
prefStorageAsk.setOnPreferenceChangeListener(null);
}
private void updatePreferencesSummary() {
findPreference(DOWNLOAD_PATH_PREFERENCE).setSummary(defaultPreferences.getString(DOWNLOAD_PATH_PREFERENCE, getString(R.string.download_path_summary)));
findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE).setSummary(defaultPreferences.getString(DOWNLOAD_PATH_AUDIO_PREFERENCE, getString(R.string.download_path_audio_summary)));
showPathInSummary(DOWNLOAD_PATH_VIDEO_PREFERENCE, R.string.download_path_summary, prefPathVideo);
showPathInSummary(DOWNLOAD_PATH_AUDIO_PREFERENCE, R.string.download_path_audio_summary, prefPathAudio);
}
private void showPathInSummary(String prefKey, @StringRes int defaultString, Preference target) {
String rawUri = defaultPreferences.getString(prefKey, null);
if (rawUri == null || rawUri.isEmpty()) {
target.setSummary(getString(defaultString));
return;
}
if (rawUri.charAt(0) == File.separatorChar) {
target.setSummary(rawUri);
return;
}
if (rawUri.startsWith(ContentResolver.SCHEME_FILE)) {
target.setSummary(new File(URI.create(rawUri)).getPath());
return;
}
try {
rawUri = URLDecoder.decode(rawUri, StandardCharsets.UTF_8.name());
} catch (UnsupportedEncodingException e) {
// nothing to do
}
target.setSummary(rawUri);
}
private boolean isFileUri(String path) {
return path.charAt(0) == File.separatorChar || path.startsWith(ContentResolver.SCHEME_FILE);
}
private boolean hasInvalidPath(String prefKey) {
String value = defaultPreferences.getString(prefKey, null);
return value == null || value.isEmpty();
}
private void updatePathPickers(boolean enabled) {
prefPathVideo.setEnabled(enabled);
prefPathAudio.setEnabled(enabled);
}
// FIXME: after releasing the old path, all downloads created on the folder becomes inaccessible
private void forgetSAFTree(Context ctx, String oldPath) {
if (IGNORE_RELEASE_ON_OLD_PATH) {
return;
}
if (oldPath == null || oldPath.isEmpty() || isFileUri(oldPath)) return;
try {
Uri uri = Uri.parse(oldPath);
ctx.getContentResolver().releasePersistableUriPermission(uri, StoredDirectoryHelper.PERMISSION_FLAGS);
ctx.revokeUriPermission(uri, StoredDirectoryHelper.PERMISSION_FLAGS);
Log.i(TAG, "Revoke old path permissions success on " + oldPath);
} catch (Exception err) {
Log.e(TAG, "Error revoking old path permissions on " + oldPath, err);
}
}
private void showMessageDialog(@StringRes int title, @StringRes int message) {
AlertDialog.Builder msg = new AlertDialog.Builder(ctx);
msg.setTitle(title);
msg.setMessage(message);
msg.setPositiveButton(android.R.string.ok, null);
msg.show();
}
@Override
public boolean onPreferenceTreeClick(Preference preference) {
if (DEBUG) {
Log.d(TAG, "onPreferenceTreeClick() called with: preference = [" + preference + "]");
Log.d(TAG, "onPreferenceTreeClick() called with: preference = [" + preference + "]");
}
if (preference.getKey().equals(DOWNLOAD_PATH_PREFERENCE)
|| preference.getKey().equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
Intent i = new Intent(getActivity(), FilePickerActivityHelper.class)
String key = preference.getKey();
int request;
if (key.equals(DOWNLOAD_PATH_VIDEO_PREFERENCE)) {
request = REQUEST_DOWNLOAD_VIDEO_PATH;
} else if (key.equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
request = REQUEST_DOWNLOAD_AUDIO_PATH;
} else {
return super.onPreferenceTreeClick(preference);
}
Intent i;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && NewPipeSettings.useStorageAccessFramework(ctx)) {
i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
.putExtra("android.content.extra.SHOW_ADVANCED", true)
.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | StoredDirectoryHelper.PERMISSION_FLAGS);
} else {
i = new Intent(getActivity(), FilePickerActivityHelper.class)
.putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false)
.putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true)
.putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_DIR);
if (preference.getKey().equals(DOWNLOAD_PATH_PREFERENCE)) {
startActivityForResult(i, REQUEST_DOWNLOAD_PATH);
} else if (preference.getKey().equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
startActivityForResult(i, REQUEST_DOWNLOAD_AUDIO_PATH);
}
}
return super.onPreferenceTreeClick(preference);
startActivityForResult(i, request);
return true;
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (DEBUG) {
Log.d(TAG, "onActivityResult() called with: requestCode = [" + requestCode + "], resultCode = [" + resultCode + "], data = [" + data + "]");
Log.d(TAG, "onActivityResult() called with: requestCode = [" + requestCode + "], " +
"resultCode = [" + resultCode + "], data = [" + data + "]"
);
}
if ((requestCode == REQUEST_DOWNLOAD_PATH || requestCode == REQUEST_DOWNLOAD_AUDIO_PATH)
&& resultCode == Activity.RESULT_OK && data.getData() != null) {
String key = getString(requestCode == REQUEST_DOWNLOAD_PATH ? R.string.download_path_key : R.string.download_path_audio_key);
String path = Utils.getFileForUri(data.getData()).getAbsolutePath();
if (resultCode != Activity.RESULT_OK) return;
defaultPreferences.edit().putString(key, path).apply();
updatePreferencesSummary();
String key;
if (requestCode == REQUEST_DOWNLOAD_VIDEO_PATH)
key = DOWNLOAD_PATH_VIDEO_PREFERENCE;
else if (requestCode == REQUEST_DOWNLOAD_AUDIO_PATH)
key = DOWNLOAD_PATH_AUDIO_PREFERENCE;
else
return;
Uri uri = data.getData();
if (uri == null) {
showMessageDialog(R.string.general_error, R.string.invalid_directory);
return;
}
// revoke permissions on the old save path (required for SAF only)
final Context ctx = getContext();
if (ctx == null) throw new NullPointerException("getContext()");
forgetSAFTree(ctx, defaultPreferences.getString(key, ""));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !FilePickerActivityHelper.isOwnFileUri(ctx, uri)) {
// steps to acquire the selected path:
// 1. acquire permissions on the new save path
// 2. save the new path, if step(2) was successful
try {
ctx.grantUriPermission(ctx.getPackageName(), uri, StoredDirectoryHelper.PERMISSION_FLAGS);
StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(ctx, uri, null);
Log.i(TAG, "Acquiring tree success from " + uri.toString());
if (!mainStorage.canWrite())
throw new IOException("No write permissions on " + uri.toString());
} catch (IOException err) {
Log.e(TAG, "Error acquiring tree from " + uri.toString(), err);
showMessageDialog(R.string.general_error, R.string.no_available_dir);
return;
}
} else {
File target = Utils.getFileForUri(uri);
if (!target.canWrite()) {
showMessageDialog(R.string.download_to_sdcard_error_title, R.string.download_to_sdcard_error_message);
return;
}
uri = Uri.fromFile(target);
}
defaultPreferences.edit().putString(key, uri.toString()).apply();
updatePreferencesSummary();
}
}

View File

@@ -18,7 +18,7 @@ import io.reactivex.disposables.Disposable;
public class HistorySettingsFragment extends BasePreferenceFragment {
private String cacheWipeKey;
private String viewsHistroyClearKey;
private String viewsHistoryClearKey;
private String searchHistoryClearKey;
private HistoryRecordManager recordManager;
private CompositeDisposable disposables;
@@ -27,7 +27,7 @@ public class HistorySettingsFragment extends BasePreferenceFragment {
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
cacheWipeKey = getString(R.string.metadata_cache_wipe_key);
viewsHistroyClearKey = getString(R.string.clear_views_history_key);
viewsHistoryClearKey = getString(R.string.clear_views_history_key);
searchHistoryClearKey = getString(R.string.clear_search_history_key);
recordManager = new HistoryRecordManager(getActivity());
disposables = new CompositeDisposable();
@@ -46,16 +46,31 @@ public class HistorySettingsFragment extends BasePreferenceFragment {
Toast.LENGTH_SHORT).show();
}
if (preference.getKey().equals(viewsHistroyClearKey)) {
if (preference.getKey().equals(viewsHistoryClearKey)) {
new AlertDialog.Builder(getActivity())
.setTitle(R.string.delete_view_history_alert)
.setNegativeButton(R.string.cancel, ((dialog, which) -> dialog.dismiss()))
.setPositiveButton(R.string.delete, ((dialog, which) -> {
final Disposable onDeletePlaybackStates = recordManager.deleteCompelteStreamStateHistory()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
howManyDeleted -> Toast.makeText(getActivity(),
R.string.watch_history_states_deleted,
Toast.LENGTH_SHORT).show(),
throwable -> ErrorActivity.reportError(getContext(),
throwable,
SettingsActivity.class, null,
ErrorActivity.ErrorInfo.make(
UserAction.DELETE_FROM_HISTORY,
"none",
"Delete view history",
R.string.general_error)));
final Disposable onDelete = recordManager.deleteWholeStreamHistory()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
howManyDeleted -> Toast.makeText(getActivity(),
R.string.view_history_deleted,
R.string.watch_history_deleted,
Toast.LENGTH_SHORT).show(),
throwable -> ErrorActivity.reportError(getContext(),
throwable,
@@ -78,6 +93,7 @@ public class HistorySettingsFragment extends BasePreferenceFragment {
"none",
"Delete search history",
R.string.general_error)));
disposables.add(onDeletePlaybackStates);
disposables.add(onClearOrphans);
disposables.add(onDelete);
}))
@@ -90,7 +106,7 @@ public class HistorySettingsFragment extends BasePreferenceFragment {
.setTitle(R.string.delete_search_history_alert)
.setNegativeButton(R.string.cancel, ((dialog, which) -> dialog.dismiss()))
.setPositiveButton(R.string.delete, ((dialog, which) -> {
final Disposable onDelete = recordManager.deleteWholeSearchHistory()
final Disposable onDelete = recordManager.deleteCompleteSearchHistory()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
howManyDeleted -> Toast.makeText(getActivity(),

View File

@@ -70,57 +70,39 @@ public class NewPipeSettings {
getAudioDownloadFolder(context);
}
public static File getVideoDownloadFolder(Context context) {
return getDir(context, R.string.download_path_key, Environment.DIRECTORY_MOVIES);
private static void getVideoDownloadFolder(Context context) {
getDir(context, R.string.download_path_video_key, Environment.DIRECTORY_MOVIES);
}
public static String getVideoDownloadPath(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
final String key = context.getString(R.string.download_path_key);
return prefs.getString(key, Environment.DIRECTORY_MOVIES);
private static void getAudioDownloadFolder(Context context) {
getDir(context, R.string.download_path_audio_key, Environment.DIRECTORY_MUSIC);
}
public static File getAudioDownloadFolder(Context context) {
return getDir(context, R.string.download_path_audio_key, Environment.DIRECTORY_MUSIC);
}
public static String getAudioDownloadPath(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
final String key = context.getString(R.string.download_path_audio_key);
return prefs.getString(key, Environment.DIRECTORY_MUSIC);
}
private static File getDir(Context context, int keyID, String defaultDirectoryName) {
private static void getDir(Context context, int keyID, String defaultDirectoryName) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
final String key = context.getString(keyID);
String downloadPath = prefs.getString(key, null);
if ((downloadPath != null) && (!downloadPath.isEmpty())) return new File(downloadPath.trim());
if ((downloadPath != null) && (!downloadPath.isEmpty())) return;
final File dir = getDir(defaultDirectoryName);
SharedPreferences.Editor spEditor = prefs.edit();
spEditor.putString(key, getNewPipeChildFolderPathForDir(dir));
spEditor.apply();
return dir;
}
@NonNull
private static File getDir(String defaultDirectoryName) {
return new File(Environment.getExternalStorageDirectory(), defaultDirectoryName);
}
public static void resetDownloadFolders(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
resetDownloadFolder(prefs, context.getString(R.string.download_path_audio_key), Environment.DIRECTORY_MUSIC);
resetDownloadFolder(prefs, context.getString(R.string.download_path_key), Environment.DIRECTORY_MOVIES);
}
private static void resetDownloadFolder(SharedPreferences prefs, String key, String defaultDirectoryName) {
SharedPreferences.Editor spEditor = prefs.edit();
spEditor.putString(key, getNewPipeChildFolderPathForDir(getDir(defaultDirectoryName)));
spEditor.apply();
}
private static String getNewPipeChildFolderPathForDir(File dir) {
return new File(dir, "NewPipe").getAbsolutePath();
@NonNull
public static File getDir(String defaultDirectoryName) {
return new File(Environment.getExternalStorageDirectory(), defaultDirectoryName);
}
private static String getNewPipeChildFolderPathForDir(File dir) {
return new File(dir, "NewPipe").toURI().toString();
}
public static boolean useStorageAccessFramework(Context context) {
final String key = context.getString(R.string.storage_use_saf);
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
return prefs.getBoolean(key, false);
}
}

View File

@@ -16,7 +16,6 @@ import android.widget.TextView;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
@@ -124,9 +123,6 @@ public class SelectKioskFragment extends DialogFragment {
throws Exception {
for(StreamingService service : NewPipe.getServices()) {
//TODO: Multi-service support
if (service.getServiceId() != ServiceList.YouTube.getServiceId() && !DEBUG) continue;
for(String kioskId : service.getKioskList().getAvailableKiosks()) {
String name = String.format(getString(R.string.service_kiosk_string),
service.getServiceInfo().getName(),

View File

@@ -229,6 +229,12 @@ public class ChooseTabsFragment extends Fragment {
returnList.add(new ChooseTabListItem(tab.getTabId(), getString(R.string.channel_page_summary),
tab.getTabIconRes(context)));
break;
case DEFAULT_KIOSK:
if (!tabList.contains(tab)) {
returnList.add(new ChooseTabListItem(tab.getTabId(), "Default Kiosk",
ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_hot)));
}
break;
default:
if (!tabList.contains(tab)) {
returnList.add(new ChooseTabListItem(context, tab));
@@ -310,6 +316,9 @@ public class ChooseTabsFragment extends Fragment {
case CHANNEL:
tabName = NewPipe.getNameOfService(((Tab.ChannelTab) tab).getChannelServiceId()) + "/" + tabName;
break;
case DEFAULT_KIOSK:
tabName = "Default Kiosk";
break;
}

View File

@@ -9,7 +9,10 @@ import android.support.v4.app.Fragment;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonSink;
import org.jsoup.helper.StringUtil;
import org.schabi.newpipe.App;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.fragments.BlankFragment;
import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
@@ -19,6 +22,7 @@ import org.schabi.newpipe.local.feed.FeedFragment;
import org.schabi.newpipe.local.history.StatisticsPlaylistFragment;
import org.schabi.newpipe.local.subscription.SubscriptionFragment;
import org.schabi.newpipe.util.KioskTranslator;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.ThemeHelper;
public abstract class Tab {
@@ -111,6 +115,12 @@ public abstract class Tab {
return new KioskTab(jsonObject);
case CHANNEL:
return new ChannelTab(jsonObject);
case DEFAULT_KIOSK:
DefaultKioskTab tab = new DefaultKioskTab();
if(!StringUtil.isBlank(tab.getKioskId())){
return tab;
}
return null;
}
}
@@ -128,7 +138,8 @@ public abstract class Tab {
BOOKMARKS(new BookmarksTab()),
HISTORY(new HistoryTab()),
KIOSK(new KioskTab()),
CHANNEL(new ChannelTab());
CHANNEL(new ChannelTab()),
DEFAULT_KIOSK(new DefaultKioskTab());
private Tab tab;
@@ -413,4 +424,55 @@ public abstract class Tab {
return channelName;
}
}
public static class DefaultKioskTab extends Tab {
public static final int ID = 7;
private int kioskServiceId;
private String kioskId;
protected DefaultKioskTab() {
initKiosk();
}
public void initKiosk() {
this.kioskServiceId = ServiceHelper.getSelectedServiceId(App.getApp());
try {
this.kioskId = NewPipe.getService(this.kioskServiceId).getKioskList().getDefaultKioskId();
} catch (ExtractionException e) {
this.kioskId = "";
}
}
@Override
public int getTabId() {
return ID;
}
@Override
public String getTabName(Context context) {
return KioskTranslator.getTranslatedKioskName(kioskId, context);
}
@DrawableRes
@Override
public int getTabIconRes(Context context) {
final int kioskIcon = KioskTranslator.getKioskIcons(kioskId, context);
if (kioskIcon <= 0) {
throw new IllegalStateException("Kiosk ID is not valid: \"" + kioskId + "\"");
}
return kioskIcon;
}
@Override
public KioskFragment getFragment() throws ExtractionException {
return KioskFragment.getInstance(kioskServiceId, kioskId);
}
public String getKioskId() {
return kioskId;
}
}
}

View File

@@ -9,27 +9,18 @@ import com.grack.nanojson.JsonParserException;
import com.grack.nanojson.JsonStringWriter;
import com.grack.nanojson.JsonWriter;
import org.schabi.newpipe.settings.tabs.Tab.Type;
import org.jsoup.helper.StringUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.schabi.newpipe.extractor.ServiceList.YouTube;
/**
* Class to get a JSON representation of a list of tabs, and the other way around.
*/
public class TabsJsonHelper {
private static final String JSON_TABS_ARRAY_KEY = "tabs";
protected static final List<Tab> FALLBACK_INITIAL_TABS_LIST = Collections.unmodifiableList(Arrays.asList(
new Tab.KioskTab(YouTube.getServiceId(), "Trending"),
Type.SUBSCRIPTIONS.getTab(),
Type.BOOKMARKS.getTab()
));
public static class InvalidJsonException extends Exception {
private InvalidJsonException() {
super();
@@ -48,7 +39,7 @@ public class TabsJsonHelper {
* Try to reads the passed JSON and returns the list of tabs if no error were encountered.
* <p>
* If the JSON is null or empty, or the list of tabs that it represents is empty, the
* {@link #FALLBACK_INITIAL_TABS_LIST fallback list} will be returned.
* {@link #getDefaultTabs fallback list} will be returned.
* <p>
* Tabs with invalid ids (i.e. not in the {@link Tab.Type} enum) will be ignored.
*
@@ -58,7 +49,7 @@ public class TabsJsonHelper {
*/
public static List<Tab> getTabsFromJson(@Nullable String tabsJson) throws InvalidJsonException {
if (tabsJson == null || tabsJson.isEmpty()) {
return FALLBACK_INITIAL_TABS_LIST;
return getDefaultTabs();
}
final List<Tab> returnTabs = new ArrayList<>();
@@ -86,12 +77,22 @@ public class TabsJsonHelper {
}
if (returnTabs.isEmpty()) {
return FALLBACK_INITIAL_TABS_LIST;
return getDefaultTabs();
}
return returnTabs;
}
public static List<Tab> getDefaultTabs(){
List<Tab> tabs = new ArrayList<>();
Tab.DefaultKioskTab tab = new Tab.DefaultKioskTab();
if(!StringUtil.isBlank(tab.getKioskId())){
tabs.add(tab);
}
tabs.add(Tab.Type.SUBSCRIPTIONS.getTab());
tabs.add(Tab.Type.BOOKMARKS.getTab());
return Collections.unmodifiableList(tabs);
}
/**
* Get a JSON representation from a list of tabs.
*

View File

@@ -44,7 +44,7 @@ public class TabsManager {
}
public List<Tab> getDefaultTabs() {
return TabsJsonHelper.FALLBACK_INITIAL_TABS_LIST;
return TabsJsonHelper.getDefaultTabs();
}
/*//////////////////////////////////////////////////////////////////////////

View File

@@ -1,9 +1,10 @@
package org.schabi.newpipe.streams;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.EOFException;
import java.io.IOException;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.InputStream;
/**
* @author kapodamy
@@ -15,89 +16,239 @@ public class DataReader {
public final static int INTEGER_SIZE = 4;
public final static int FLOAT_SIZE = 4;
private long pos;
public final SharpStream stream;
private final boolean rewind;
private final static int BUFFER_SIZE = 128 * 1024;// 128 KiB
private long position = 0;
private final SharpStream stream;
private InputStream view;
private int viewSize;
public DataReader(SharpStream stream) {
this.rewind = stream.canRewind();
this.stream = stream;
this.pos = 0L;
this.readOffset = this.readBuffer.length;
}
public long position() {
return pos;
return position;
}
public final int readInt() throws IOException {
public int read() throws IOException {
if (fillBuffer()) {
return -1;
}
position++;
readCount--;
return readBuffer[readOffset++] & 0xFF;
}
public long skipBytes(long amount) throws IOException {
if (readCount < 0) {
return 0;
} else if (readCount == 0) {
amount = stream.skip(amount);
} else {
if (readCount > amount) {
readCount -= (int) amount;
readOffset += (int) amount;
} else {
amount = readCount + stream.skip(amount - readCount);
readCount = 0;
readOffset = readBuffer.length;
}
}
position += amount;
return amount;
}
public int readInt() throws IOException {
primitiveRead(INTEGER_SIZE);
return primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3];
}
public final int read() throws IOException {
int value = stream.read();
if (value == -1) {
throw new EOFException();
}
pos++;
return value;
public short readShort() throws IOException {
primitiveRead(SHORT_SIZE);
return (short) (primitive[0] << 8 | primitive[1]);
}
public final long skipBytes(long amount) throws IOException {
amount = stream.skip(amount);
pos += amount;
return amount;
}
public final long readLong() throws IOException {
public long readLong() throws IOException {
primitiveRead(LONG_SIZE);
long high = primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3];
long low = primitive[4] << 24 | primitive[5] << 16 | primitive[6] << 8 | primitive[7];
return high << 32 | low;
}
public final short readShort() throws IOException {
primitiveRead(SHORT_SIZE);
return (short) (primitive[0] << 8 | primitive[1]);
}
public final int read(byte[] buffer) throws IOException {
public int read(byte[] buffer) throws IOException {
return read(buffer, 0, buffer.length);
}
public final int read(byte[] buffer, int offset, int count) throws IOException {
int res = stream.read(buffer, offset, count);
pos += res;
public int read(byte[] buffer, int offset, int count) throws IOException {
if (readCount < 0) {
return -1;
}
int total = 0;
return res;
if (count >= readBuffer.length) {
if (readCount > 0) {
System.arraycopy(readBuffer, readOffset, buffer, offset, readCount);
readOffset += readCount;
offset += readCount;
count -= readCount;
total = readCount;
readCount = 0;
}
total += Math.max(stream.read(buffer, offset, count), 0);
} else {
while (count > 0 && !fillBuffer()) {
int read = Math.min(readCount, count);
System.arraycopy(readBuffer, readOffset, buffer, offset, read);
readOffset += read;
readCount -= read;
offset += read;
count -= read;
total += read;
}
}
position += total;
return total;
}
public final boolean available() {
return stream.available() > 0;
public boolean available() {
return readCount > 0 || stream.available() > 0;
}
public void rewind() throws IOException {
stream.rewind();
pos = 0;
if ((position - viewSize) > 0) {
viewSize = 0;// drop view
} else {
viewSize += position;
}
position = 0;
readOffset = readBuffer.length;
}
public boolean canRewind() {
return rewind;
return stream.canRewind();
}
private short[] primitive = new short[LONG_SIZE];
/**
* Wraps this instance of {@code DataReader} into {@code InputStream}
* object. Note: Any read in the {@code DataReader} will not modify
* (decrease) the view size
*
* @param size the size of the view
* @return the view
*/
public InputStream getView(int size) {
if (view == null) {
view = new InputStream() {
@Override
public int read() throws IOException {
if (viewSize < 1) {
return -1;
}
int res = DataReader.this.read();
if (res > 0) {
viewSize--;
}
return res;
}
@Override
public int read(byte[] buffer) throws IOException {
return read(buffer, 0, buffer.length);
}
@Override
public int read(byte[] buffer, int offset, int count) throws IOException {
if (viewSize < 1) {
return -1;
}
int res = DataReader.this.read(buffer, offset, Math.min(viewSize, count));
viewSize -= res;
return res;
}
@Override
public long skip(long amount) throws IOException {
if (viewSize < 1) {
return 0;
}
int res = (int) DataReader.this.skipBytes(Math.min(amount, viewSize));
viewSize -= res;
return res;
}
@Override
public int available() {
return viewSize;
}
@Override
public void close() {
viewSize = 0;
}
@Override
public boolean markSupported() {
return false;
}
};
}
viewSize = size;
return view;
}
private final short[] primitive = new short[LONG_SIZE];
private void primitiveRead(int amount) throws IOException {
byte[] buffer = new byte[amount];
int read = stream.read(buffer, 0, amount);
pos += read;
int read = read(buffer, 0, amount);
if (read != amount) {
throw new EOFException("Truncated data, missing " + String.valueOf(amount - read) + " bytes");
throw new EOFException("Truncated stream, missing " + String.valueOf(amount - read) + " bytes");
}
for (int i = 0; i < buffer.length; i++) {
primitive[i] = (short) (buffer[i] & 0xFF);// the "byte" datatype is signed and is very annoying
for (int i = 0; i < amount; i++) {
primitive[i] = (short) (buffer[i] & 0xFF);// the "byte" data type in java is signed and is very annoying
}
}
private final byte[] readBuffer = new byte[BUFFER_SIZE];
private int readOffset;
private int readCount;
private boolean fillBuffer() throws IOException {
if (readCount < 0) {
return true;
}
if (readOffset >= readBuffer.length) {
readCount = stream.read(readBuffer);
if (readCount < 1) {
readCount = -1;
return true;
}
readOffset = 0;
}
return readCount < 1;
}
}

View File

@@ -1,17 +1,15 @@
package org.schabi.newpipe.streams;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.NoSuchElementException;
import org.schabi.newpipe.streams.io.SharpStream;
/**
* @author kapodamy
*/
@@ -35,14 +33,29 @@ public class Mp4DashReader {
private static final int ATOM_TREX = 0x74726578;
private static final int ATOM_TKHD = 0x746B6864;
private static final int ATOM_MFRA = 0x6D667261;
private static final int ATOM_TFRA = 0x74667261;
private static final int ATOM_MDHD = 0x6D646864;
private static final int ATOM_EDTS = 0x65647473;
private static final int ATOM_ELST = 0x656C7374;
private static final int ATOM_HDLR = 0x68646C72;
private static final int ATOM_MINF = 0x6D696E66;
private static final int ATOM_DINF = 0x64696E66;
private static final int ATOM_STBL = 0x7374626C;
private static final int ATOM_STSD = 0x73747364;
private static final int ATOM_VMHD = 0x766D6864;
private static final int ATOM_SMHD = 0x736D6864;
private static final int BRAND_DASH = 0x64617368;
private static final int BRAND_ISO5 = 0x69736F35;
private static final int HANDLER_VIDE = 0x76696465;
private static final int HANDLER_SOUN = 0x736F756E;
private static final int HANDLER_SUBT = 0x73756274;
// </editor-fold>
private final DataReader stream;
private Mp4Track[] tracks = null;
private int[] brands = null;
private Box box;
private Moof moof;
@@ -50,9 +63,10 @@ public class Mp4DashReader {
private boolean chunkZero = false;
private int selectedTrack = -1;
private Box backupBox = null;
public enum TrackKind {
Audio, Video, Other
Audio, Video, Subtitles, Other
}
public Mp4DashReader(SharpStream source) {
@@ -65,8 +79,15 @@ public class Mp4DashReader {
}
box = readBox(ATOM_FTYP);
if (parse_ftyp() != BRAND_DASH) {
throw new NoSuchElementException("Main Brand is not dash");
brands = parse_ftyp(box);
switch (brands[0]) {
case BRAND_DASH:
case BRAND_ISO5:// ¿why not?
break;
default:
throw new NoSuchElementException(
"Not a MPEG-4 DASH container, major brand is not 'dash' or 'iso5' is " + boxName(brands[0])
);
}
Moov moov = null;
@@ -84,8 +105,6 @@ public class Mp4DashReader {
break;
case ATOM_MFRA:
break;
case ATOM_MDAT:
throw new IOException("Expected moof, found mdat");
}
}
@@ -107,15 +126,26 @@ public class Mp4DashReader {
}
}
if (moov.trak[i].tkhd.bHeight == 0 && moov.trak[i].tkhd.bWidth == 0) {
tracks[i].kind = moov.trak[i].tkhd.bVolume == 0 ? TrackKind.Other : TrackKind.Audio;
} else {
tracks[i].kind = TrackKind.Video;
switch (moov.trak[i].mdia.hdlr.subType) {
case HANDLER_VIDE:
tracks[i].kind = TrackKind.Video;
break;
case HANDLER_SOUN:
tracks[i].kind = TrackKind.Audio;
break;
case HANDLER_SUBT:
tracks[i].kind = TrackKind.Subtitles;
break;
default:
tracks[i].kind = TrackKind.Other;
break;
}
}
backupBox = box;
}
public Mp4Track selectTrack(int index) {
Mp4Track selectTrack(int index) {
selectedTrack = index;
return tracks[index];
}
@@ -126,7 +156,7 @@ public class Mp4DashReader {
* @return list with a basic info
* @throws IOException if the source stream is not seekeable
*/
public int getFragmentsCount() throws IOException {
int getFragmentsCount() throws IOException {
if (selectedTrack < 0) {
throw new IllegalStateException("track no selected");
}
@@ -136,7 +166,6 @@ public class Mp4DashReader {
Box tmp;
int count = 0;
long orig_offset = stream.position();
if (box.type == ATOM_MOOF) {
tmp = box;
@@ -162,17 +191,36 @@ public class Mp4DashReader {
ensure(tmp);
} while (stream.available() && (tmp = readBox()) != null);
stream.rewind();
stream.skipBytes((int) orig_offset);
rewind();
return count;
}
public int[] getBrands() {
if (brands == null) throw new IllegalStateException("Not parsed");
return brands;
}
public void rewind() throws IOException {
if (!stream.canRewind()) {
throw new IOException("The provided stream doesn't allow seek");
}
if (box == null) {
return;
}
box = backupBox;
chunkZero = false;
stream.rewind();
stream.skipBytes(backupBox.offset + (DataReader.INTEGER_SIZE * 2));
}
public Mp4Track[] getAvailableTracks() {
return tracks;
}
public Mp4TrackChunk getNextChunk() throws IOException {
public Mp4DashChunk getNextChunk(boolean infoOnly) throws IOException {
Mp4Track track = tracks[selectedTrack];
while (stream.available()) {
@@ -208,7 +256,7 @@ public class Mp4DashReader {
if (hasFlag(moof.traf.tfhd.bFlags, 0x10)) {
moof.traf.trun.chunkSize = moof.traf.tfhd.defaultSampleSize * moof.traf.trun.entryCount;
} else {
moof.traf.trun.chunkSize = box.size - 8;
moof.traf.trun.chunkSize = (int) (box.size - 8);
}
}
if (!hasFlag(moof.traf.trun.bFlags, 0x900) && moof.traf.trun.chunkDuration == 0) {
@@ -228,9 +276,12 @@ public class Mp4DashReader {
continue;// find another chunk
}
Mp4TrackChunk chunk = new Mp4TrackChunk();
Mp4DashChunk chunk = new Mp4DashChunk();
chunk.moof = moof;
chunk.data = new TrackDataChunk(stream, moof.traf.trun.chunkSize);
if (!infoOnly) {
chunk.data = stream.getView(moof.traf.trun.chunkSize);
}
moof = null;
stream.skipBytes(chunk.moof.traf.trun.dataOffset);
@@ -269,6 +320,10 @@ public class Mp4DashReader {
b.size = stream.readInt();
b.type = stream.readInt();
if (b.size == 1) {
b.size = stream.readLong();
}
return b;
}
@@ -280,6 +335,25 @@ public class Mp4DashReader {
return b;
}
private byte[] readFullBox(Box ref) throws IOException {
// full box reading is limited to 2 GiB, and should be enough
int size = (int) ref.size;
ByteBuffer buffer = ByteBuffer.allocate(size);
buffer.putInt(size);
buffer.putInt(ref.type);
int read = size - 8;
if (stream.read(buffer.array(), 8, read) != read) {
throw new EOFException(
String.format("EOF reached in box: type=%s offset=%s size=%s", boxName(ref.type), ref.offset, ref.size)
);
}
return buffer.array();
}
private void ensure(Box ref) throws IOException {
long skip = ref.offset + ref.size - stream.position();
@@ -310,6 +384,14 @@ public class Mp4DashReader {
return null;
}
private Box untilAnyBox(Box ref) throws IOException {
if (stream.position() >= (ref.offset + ref.size)) {
return null;
}
return readBox();
}
// </editor-fold>
// <editor-fold defaultState="collapsed" desc="Box readers">
@@ -329,7 +411,7 @@ public class Mp4DashReader {
return obj;
}
}
return obj;
}
@@ -397,14 +479,14 @@ public class Mp4DashReader {
private long parse_tfdt() throws IOException {
int version = stream.read();
stream.skipBytes(3);// flags
stream.skipBytes(3);// flags
return version == 0 ? readUint() : stream.readLong();
}
private Trun parse_trun() throws IOException {
Trun obj = new Trun();
obj.bFlags = stream.readInt();
obj.entryCount = stream.readInt();// unsigned int
obj.entryCount = stream.readInt();// unsigned int
obj.entries_rowSize = 0;
if (hasFlag(obj.bFlags, 0x0100)) {
@@ -448,11 +530,18 @@ public class Mp4DashReader {
return obj;
}
private int parse_ftyp() throws IOException {
int brand = stream.readInt();
private int[] parse_ftyp(Box ref) throws IOException {
int i = 0;
int[] list = new int[(int) ((ref.offset + ref.size - stream.position() - 4) / 4)];
list[i++] = stream.readInt();// major brand
stream.skipBytes(4);// minor version
return brand;
for (; i < list.length; i++)
list[i] = stream.readInt();// compatible brands
return list;
}
private Mvhd parse_mvhd() throws IOException {
@@ -521,32 +610,66 @@ public class Mp4DashReader {
trak.tkhd = parse_tkhd();
ensure(b);
b = untilBox(ref, ATOM_MDIA);
trak.mdia = new byte[b.size];
while ((b = untilBox(ref, ATOM_MDIA, ATOM_EDTS)) != null) {
switch (b.type) {
case ATOM_MDIA:
trak.mdia = parse_mdia(b);
break;
case ATOM_EDTS:
trak.edst_elst = parse_edts(b);
break;
}
ByteBuffer buffer = ByteBuffer.wrap(trak.mdia);
buffer.putInt(b.size);
buffer.putInt(ATOM_MDIA);
stream.read(trak.mdia, 8, b.size - 8);
trak.mdia_mdhd_timeScale = parse_mdia(buffer);
ensure(b);
}
return trak;
}
private int parse_mdia(ByteBuffer data) {
while (data.hasRemaining()) {
int end = data.position() + data.getInt();
if (data.getInt() == ATOM_MDHD) {
byte version = data.get();
data.position(data.position() + 3 + ((version == 0 ? 4 : 8) * 2));
return data.getInt();
}
private Mdia parse_mdia(Box ref) throws IOException {
Mdia obj = new Mdia();
data.position(end);
Box b;
while ((b = untilBox(ref, ATOM_MDHD, ATOM_HDLR, ATOM_MINF)) != null) {
switch (b.type) {
case ATOM_MDHD:
obj.mdhd = readFullBox(b);
// read time scale
ByteBuffer buffer = ByteBuffer.wrap(obj.mdhd);
byte version = buffer.get(8);
buffer.position(12 + ((version == 0 ? 4 : 8) * 2));
obj.mdhd_timeScale = buffer.getInt();
break;
case ATOM_HDLR:
obj.hdlr = parse_hdlr(b);
break;
case ATOM_MINF:
obj.minf = parse_minf(b);
break;
}
ensure(b);
}
return 0;// this NEVER should happen
return obj;
}
private Hdlr parse_hdlr(Box ref) throws IOException {
// version
// flags
stream.skipBytes(4);
Hdlr obj = new Hdlr();
obj.bReserved = new byte[12];
obj.type = stream.readInt();
obj.subType = stream.readInt();
stream.read(obj.bReserved);
// component name (is a ansi/ascii string)
stream.skipBytes((ref.offset + ref.size) - stream.position());
return obj;
}
private Moov parse_moov(Box ref) throws IOException {
@@ -570,7 +693,7 @@ public class Mp4DashReader {
ensure(b);
}
moov.trak = tmp.toArray(new Trak[tmp.size()]);
moov.trak = tmp.toArray(new Trak[0]);
return moov;
}
@@ -584,7 +707,7 @@ public class Mp4DashReader {
ensure(b);
}
return tmp.toArray(new Trex[tmp.size()]);
return tmp.toArray(new Trex[0]);
}
private Trex parse_trex() throws IOException {
@@ -602,74 +725,74 @@ public class Mp4DashReader {
return obj;
}
private Tfra parse_tfra() throws IOException {
int version = stream.read();
stream.skipBytes(3);// flags
Tfra tfra = new Tfra();
tfra.trackId = stream.readInt();
stream.skipBytes(3);// reserved
int bFlags = stream.read();
int size_tts = ((bFlags >> 4) & 3) + ((bFlags >> 2) & 3) + (bFlags & 3);
tfra.entries_time = new int[stream.readInt()];
for (int i = 0; i < tfra.entries_time.length; i++) {
tfra.entries_time[i] = version == 0 ? stream.readInt() : (int) stream.readLong();
stream.skipBytes(size_tts + (version == 0 ? 4 : 8));
private Elst parse_edts(Box ref) throws IOException {
Box b = untilBox(ref, ATOM_ELST);
if (b == null) {
return null;
}
return tfra;
}
private Sidx parse_sidx() throws IOException {
int version = stream.read();
Elst obj = new Elst();
boolean v1 = stream.read() == 1;
stream.skipBytes(3);// flags
Sidx obj = new Sidx();
obj.referenceId = stream.readInt();
obj.timescale = stream.readInt();
int entryCount = stream.readInt();
if (entryCount < 1) {
obj.bMediaRate = 0x00010000;// default media rate (1.0)
return obj;
}
// earliest presentation entries_time
// first offset
// reserved
stream.skipBytes((2 * (version == 0 ? 4 : 8)) + 2);
if (v1) {
stream.skipBytes(DataReader.LONG_SIZE);// segment duration
obj.MediaTime = stream.readLong();
// ignore all remain entries
stream.skipBytes((entryCount - 1) * (DataReader.LONG_SIZE * 2));
} else {
stream.skipBytes(DataReader.INTEGER_SIZE);// segment duration
obj.MediaTime = stream.readInt();
}
obj.entries_subsegmentDuration = new int[stream.readShort()];
obj.bMediaRate = stream.readInt();
for (int i = 0; i < obj.entries_subsegmentDuration.length; i++) {
// reference type
// referenced size
stream.skipBytes(4);
obj.entries_subsegmentDuration[i] = stream.readInt();// unsigned int
return obj;
}
// starts with SAP
// SAP type
// SAP delta entries_time
stream.skipBytes(4);
private Minf parse_minf(Box ref) throws IOException {
Minf obj = new Minf();
Box b;
while ((b = untilAnyBox(ref)) != null) {
switch (b.type) {
case ATOM_DINF:
obj.dinf = readFullBox(b);
break;
case ATOM_STBL:
obj.stbl_stsd = parse_stbl(b);
break;
case ATOM_VMHD:
case ATOM_SMHD:
obj.$mhd = readFullBox(b);
break;
}
ensure(b);
}
return obj;
}
private Tfra[] parse_mfra(Box ref, int trackCount) throws IOException {
ArrayList<Tfra> tmp = new ArrayList<>(trackCount);
long limit = ref.offset + ref.size;
/**
* this only read the "stsd" box inside
*/
private byte[] parse_stbl(Box ref) throws IOException {
Box b = untilBox(ref, ATOM_STSD);
while (stream.position() < limit) {
box = readBox();
if (box.type == ATOM_TFRA) {
tmp.add(parse_tfra());
}
ensure(box);
if (b == null) {
return new byte[0];// this never should happens (missing codec startup data)
}
return tmp.toArray(new Tfra[tmp.size()]);
return readFullBox(b);
}
// </editor-fold>
@@ -679,14 +802,7 @@ public class Mp4DashReader {
int type;
long offset;
int size;
}
class Sidx {
int timescale;
int referenceId;
int[] entries_subsegmentDuration;
long size;
}
public class Moof {
@@ -711,12 +827,16 @@ public class Mp4DashReader {
int defaultSampleFlags;
}
public class TrunEntry {
class TrunEntry {
int sampleDuration;
int sampleSize;
int sampleFlags;
int sampleCompositionTimeOffset;
boolean hasCompositionTimeOffset;
boolean isKeyframe;
public int sampleDuration;
public int sampleSize;
public int sampleFlags;
public int sampleCompositionTimeOffset;
}
public class Trun {
@@ -749,6 +869,31 @@ public class Mp4DashReader {
entry.sampleCompositionTimeOffset = buffer.getInt();
}
entry.hasCompositionTimeOffset = hasFlag(bFlags, 0x0800);
entry.isKeyframe = !hasFlag(entry.sampleFlags, 0x10000);
return entry;
}
public TrunEntry getAbsoluteEntry(int i, Tfhd header) {
TrunEntry entry = getEntry(i);
if (!hasFlag(bFlags, 0x0100) && hasFlag(header.bFlags, 0x20)) {
entry.sampleFlags = header.defaultSampleFlags;
}
if (!hasFlag(bFlags, 0x0200) && hasFlag(header.bFlags, 0x10)) {
entry.sampleSize = header.defaultSampleSize;
}
if (!hasFlag(bFlags, 0x0100) && hasFlag(header.bFlags, 0x08)) {
entry.sampleDuration = header.defaultSampleDuration;
}
if (i == 0 && hasFlag(bFlags, 0x0004)) {
entry.sampleFlags = bFirstSampleFlags;
}
return entry;
}
}
@@ -768,9 +913,9 @@ public class Mp4DashReader {
public class Trak {
public Tkhd tkhd;
public int mdia_mdhd_timeScale;
public Elst edst_elst;
public Mdia mdia;
byte[] mdia;
}
class Mvhd {
@@ -786,12 +931,6 @@ public class Mp4DashReader {
Trex[] mvex_trex;
}
class Tfra {
int trackId;
int[] entries_time;
}
public class Trex {
private int trackId;
@@ -801,6 +940,34 @@ public class Mp4DashReader {
int defaultSampleFlags;
}
public class Elst {
public long MediaTime;
public int bMediaRate;
}
public class Mdia {
public int mdhd_timeScale;
public byte[] mdhd;
public Hdlr hdlr;
public Minf minf;
}
public class Hdlr {
public int type;
public int subType;
public byte[] bReserved;
}
public class Minf {
public byte[] dinf;
public byte[] stbl_stsd;
public byte[] $mhd;
}
public class Mp4Track {
public TrackKind kind;
@@ -808,10 +975,43 @@ public class Mp4DashReader {
public Trex trex;
}
public class Mp4TrackChunk {
public class Mp4DashChunk {
public InputStream data;
public Moof moof;
private int i = 0;
public TrunEntry getNextSampleInfo() {
if (i >= moof.traf.trun.entryCount) {
return null;
}
return moof.traf.trun.getAbsoluteEntry(i++, moof.traf.tfhd);
}
public Mp4DashSample getNextSample() throws IOException {
if (data == null) {
throw new IllegalStateException("This chunk has info only");
}
if (i >= moof.traf.trun.entryCount) {
return null;
}
Mp4DashSample sample = new Mp4DashSample();
sample.info = moof.traf.trun.getAbsoluteEntry(i++, moof.traf.tfhd);
sample.data = new byte[sample.info.sampleSize];
if (data.read(sample.data) != sample.info.sampleSize) {
throw new EOFException("EOF reached while reading a sample");
}
return sample;
}
}
public class Mp4DashSample {
public TrunEntry info;
public byte[] data;
}
//</editor-fold>
}

View File

@@ -1,623 +0,0 @@
package org.schabi.newpipe.streams;
import org.schabi.newpipe.streams.io.SharpStream;
import org.schabi.newpipe.streams.Mp4DashReader.Mp4Track;
import org.schabi.newpipe.streams.Mp4DashReader.Mp4TrackChunk;
import org.schabi.newpipe.streams.Mp4DashReader.Trak;
import org.schabi.newpipe.streams.Mp4DashReader.Trex;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import static org.schabi.newpipe.streams.Mp4DashReader.hasFlag;
/**
*
* @author kapodamy
*/
public class Mp4DashWriter {
private final static byte DIMENSIONAL_FIVE = 5;
private final static byte DIMENSIONAL_TWO = 2;
private final static short DEFAULT_TIMESCALE = 1000;
private final static int BUFFER_SIZE = 8 * 1024;
private final static byte DEFAULT_TREX_SIZE = 32;
private final static byte[] TFRA_TTS_DEFAULT = new byte[]{0x01, 0x01, 0x01};
private final static int EPOCH_OFFSET = 2082844800;
private Mp4Track[] infoTracks;
private SharpStream[] sourceTracks;
private Mp4DashReader[] readers;
private final long time;
private boolean done = false;
private boolean parsed = false;
private long written = 0;
private ArrayList<ArrayList<Integer>> chunkTimes;
private ArrayList<Long> moofOffsets;
private ArrayList<Integer> fragSizes;
public Mp4DashWriter(SharpStream... source) {
sourceTracks = source;
readers = new Mp4DashReader[sourceTracks.length];
infoTracks = new Mp4Track[sourceTracks.length];
time = (System.currentTimeMillis() / 1000L) + EPOCH_OFFSET;
}
public Mp4Track[] getTracksFromSource(int sourceIndex) throws IllegalStateException {
if (!parsed) {
throw new IllegalStateException("All sources must be parsed first");
}
return readers[sourceIndex].getAvailableTracks();
}
public void parseSources() throws IOException, IllegalStateException {
if (done) {
throw new IllegalStateException("already done");
}
if (parsed) {
throw new IllegalStateException("already parsed");
}
try {
for (int i = 0; i < readers.length; i++) {
readers[i] = new Mp4DashReader(sourceTracks[i]);
readers[i].parse();
}
} finally {
parsed = true;
}
}
public void selectTracks(int... trackIndex) throws IOException {
if (done) {
throw new IOException("already done");
}
if (chunkTimes != null) {
throw new IOException("tracks already selected");
}
try {
chunkTimes = new ArrayList<>(readers.length);
moofOffsets = new ArrayList<>(32);
fragSizes = new ArrayList<>(32);
for (int i = 0; i < readers.length; i++) {
infoTracks[i] = readers[i].selectTrack(trackIndex[i]);
chunkTimes.add(new ArrayList<Integer>(32));
}
} finally {
parsed = true;
}
}
public long getBytesWritten() {
return written;
}
public void build(SharpStream out) throws IOException, RuntimeException {
if (done) {
throw new RuntimeException("already done");
}
if (!out.canWrite()) {
throw new IOException("the provided output is not writable");
}
long sidxOffsets = -1;
int maxFrags = 0;
for (SharpStream stream : sourceTracks) {
if (!stream.canRewind()) {
sidxOffsets = -2;// sidx not available
}
}
try {
dump(make_ftyp(), out);
dump(make_moov(), out);
if (sidxOffsets == -1 && out.canRewind()) {
//<editor-fold defaultstate="collapsed" desc="calculate sidx">
int reserved = 0;
for (Mp4DashReader reader : readers) {
int count = reader.getFragmentsCount();
if (count > maxFrags) {
maxFrags = count;
}
reserved += 12 + calcSidxBodySize(count);
}
if (maxFrags > 0xFFFF) {
sidxOffsets = -3;// TODO: to many fragments, needs a multi-sidx implementation
} else {
sidxOffsets = written;
dump(make_free(reserved), out);
}
//</editor-fold>
}
ArrayList<Mp4TrackChunk> chunks = new ArrayList<>(readers.length);
chunks.add(null);
int read;
byte[] buffer = new byte[BUFFER_SIZE];
int sequenceNumber = 1;
while (true) {
chunks.clear();
for (int i = 0; i < readers.length; i++) {
Mp4TrackChunk chunk = readers[i].getNextChunk();
if (chunk == null || chunk.moof.traf.trun.chunkSize < 1) {
continue;
}
chunk.moof.traf.tfhd.trackId = i + 1;
chunks.add(chunk);
if (sequenceNumber == 1) {
if (chunk.moof.traf.trun.entryCount > 0 && hasFlag(chunk.moof.traf.trun.bFlags, 0x0800)) {
chunkTimes.get(i).add(chunk.moof.traf.trun.getEntry(0).sampleCompositionTimeOffset);
} else {
chunkTimes.get(i).add(0);
}
}
chunkTimes.get(i).add(chunk.moof.traf.trun.chunkDuration);
}
if (chunks.size() < 1) {
break;
}
long offset = written;
moofOffsets.add(offset);
dump(make_moof(sequenceNumber++, chunks, offset), out);
dump(make_mdat(chunks), out);
for (Mp4TrackChunk chunk : chunks) {
while ((read = chunk.data.read(buffer)) > 0) {
out.write(buffer, 0, read);
written += read;
}
}
fragSizes.add((int) (written - offset));
}
dump(make_mfra(), out);
if (sidxOffsets > 0 && moofOffsets.size() == maxFrags) {
long len = written;
out.rewind();
out.skip(sidxOffsets);
written = sidxOffsets;
sidxOffsets = moofOffsets.get(0);
for (int i = 0; i < readers.length; i++) {
dump(make_sidx(i, sidxOffsets - written), out);
}
written = len;
}
} finally {
done = true;
}
}
public boolean isDone() {
return done;
}
public boolean isParsed() {
return parsed;
}
public void close() {
done = true;
parsed = true;
for (SharpStream src : sourceTracks) {
src.dispose();
}
sourceTracks = null;
readers = null;
infoTracks = null;
moofOffsets = null;
chunkTimes = null;
}
// <editor-fold defaultstate="collapsed" desc="Utils">
private void dump(byte[][] buffer, SharpStream stream) throws IOException {
for (byte[] buff : buffer) {
stream.write(buff);
written += buff.length;
}
}
private byte[][] lengthFor(byte[][] buffer) {
int length = 0;
for (byte[] buff : buffer) {
length += buff.length;
}
ByteBuffer.wrap(buffer[0]).putInt(length);
return buffer;
}
private int calcSidxBodySize(int entryCount) {
return 4 + 4 + 8 + 8 + 4 + (entryCount * 12);
}
// </editor-fold>
// <editor-fold defaultstate="collapsed" desc="Box makers">
private byte[][] make_moof(int sequence, ArrayList<Mp4TrackChunk> chunks, long referenceOffset) {
int pos = 2;
TrunExtra[] extra = new TrunExtra[chunks.size()];
byte[][] buffer = new byte[pos + (extra.length * DIMENSIONAL_FIVE)][];
buffer[0] = new byte[]{
0x00, 0x00, 0x00, 0x00, 0x6D, 0x6F, 0x6F, 0x66,// info header
0x00, 0x00, 0x00, 0x10, 0x6D, 0x66, 0x68, 0x64, 0x00, 0x00, 0x00, 0x00//mfhd
};
buffer[1] = new byte[4];
ByteBuffer.wrap(buffer[1]).putInt(sequence);
for (int i = 0; i < extra.length; i++) {
extra[i] = new TrunExtra();
for (byte[] buff : make_traf(chunks.get(i), extra[i], referenceOffset)) {
buffer[pos++] = buff;
}
}
lengthFor(buffer);
int offset = 8 + ByteBuffer.wrap(buffer[0]).getInt();
for (int i = 0; i < extra.length; i++) {
extra[i].byteBuffer.putInt(offset);
offset += chunks.get(i).moof.traf.trun.chunkSize;
}
return buffer;
}
private byte[][] make_traf(Mp4TrackChunk chunk, TrunExtra extra, long moofOffset) {
byte[][] buffer = new byte[DIMENSIONAL_FIVE][];
buffer[0] = new byte[]{
0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x61, 0x66,
0x00, 0x00, 0x00, 0x00, 0x74, 0x66, 0x68, 0x64
};
int flags = (chunk.moof.traf.tfhd.bFlags & 0x38) | 0x01;
byte tfhdBodySize = 8 + 8;
if (hasFlag(flags, 0x08)) {
tfhdBodySize += 4;
}
if (hasFlag(flags, 0x10)) {
tfhdBodySize += 4;
}
if (hasFlag(flags, 0x20)) {
tfhdBodySize += 4;
}
buffer[1] = new byte[tfhdBodySize];
ByteBuffer set = ByteBuffer.wrap(buffer[1]);
set.position(4);
set.putInt(chunk.moof.traf.tfhd.trackId);
set.putLong(moofOffset);
if (hasFlag(flags, 0x08)) {
set.putInt(chunk.moof.traf.tfhd.defaultSampleDuration);
}
if (hasFlag(flags, 0x10)) {
set.putInt(chunk.moof.traf.tfhd.defaultSampleSize);
}
if (hasFlag(flags, 0x20)) {
set.putInt(chunk.moof.traf.tfhd.defaultSampleFlags);
}
set.putInt(0, flags);
ByteBuffer.wrap(buffer[0]).putInt(8, 8 + tfhdBodySize);
buffer[2] = new byte[]{
0x00, 0x00, 0x00, 0x14,
0x74, 0x66, 0x64, 0x74,
0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00
};
ByteBuffer.wrap(buffer[2]).putLong(12, chunk.moof.traf.tfdt);
buffer[3] = new byte[]{
0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x75, 0x6E,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00
};
buffer[4] = chunk.moof.traf.trun.bEntries;
lengthFor(buffer);
set = ByteBuffer.wrap(buffer[3]);
set.putInt(buffer[3].length + buffer[4].length);
set.position(8);
set.putInt((chunk.moof.traf.trun.bFlags | 0x01) & 0x0F01);
set.putInt(chunk.moof.traf.trun.entryCount);
extra.byteBuffer = set;
return buffer;
}
private byte[][] make_mdat(ArrayList<Mp4TrackChunk> chunks) {
byte[][] buffer = new byte[][]{
{
0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x61, 0x74
}
};
int length = 0;
for (Mp4TrackChunk chunk : chunks) {
length += chunk.moof.traf.trun.chunkSize;
}
ByteBuffer.wrap(buffer[0]).putInt(length + 8);
return buffer;
}
private byte[][] make_ftyp() {
return new byte[][]{
{
0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, 0x64, 0x61, 0x73, 0x68, 0x00, 0x00, 0x00, 0x00,
0x6D, 0x70, 0x34, 0x31, 0x69, 0x73, 0x6F, 0x6D, 0x69, 0x73, 0x6F, 0x36, 0x69, 0x73, 0x6F, 0x32
}
};
}
private byte[][] make_mvhd() {
byte[][] buffer = new byte[DIMENSIONAL_FIVE][];
buffer[0] = new byte[]{
0x00, 0x00, 0x00, 0x78, 0x6D, 0x76, 0x68, 0x64, 0x01, 0x00, 0x00, 0x00
};
buffer[1] = new byte[28];
buffer[2] = new byte[]{
0x00, 0x01, 0x00, 0x00, 0x01, 0x00,// default volume and rate
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,// reserved values
// default matrix
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x40, 0x00, 0x00, 0x00
};
buffer[3] = new byte[24];// predefined
buffer[4] = ByteBuffer.allocate(4).putInt(infoTracks.length + 1).array();
long longestTrack = 0;
for (Mp4Track track : infoTracks) {
long tmp = (long) ((track.trak.tkhd.duration / (double) track.trak.mdia_mdhd_timeScale) * DEFAULT_TIMESCALE);
if (tmp > longestTrack) {
longestTrack = tmp;
}
}
ByteBuffer.wrap(buffer[1])
.putLong(time)
.putLong(time)
.putInt(DEFAULT_TIMESCALE)
.putLong(longestTrack);
return buffer;
}
private byte[][] make_trak(int trackId, Trak trak) throws RuntimeException {
if (trak.tkhd.matrix.length != 36) {
throw new RuntimeException("bad track matrix length (expected 36)");
}
byte[][] buffer = new byte[DIMENSIONAL_FIVE][];
buffer[0] = new byte[]{
0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x61, 0x6B,// trak header
0x00, 0x00, 0x00, 0x68, 0x74, 0x6B, 0x68, 0x64, 0x01, 0x00, 0x00, 0x03 // tkhd header
};
buffer[1] = new byte[48];
buffer[2] = trak.tkhd.matrix;
buffer[3] = new byte[8];
buffer[4] = trak.mdia;
ByteBuffer set = ByteBuffer.wrap(buffer[1]);
set.putLong(time);
set.putLong(time);
set.putInt(trackId);
set.position(24);
set.putLong(trak.tkhd.duration);
set.position(40);
set.putShort(trak.tkhd.bLayer);
set.putShort(trak.tkhd.bAlternateGroup);
set.putShort(trak.tkhd.bVolume);
ByteBuffer.wrap(buffer[3])
.putInt(trak.tkhd.bWidth)
.putInt(trak.tkhd.bHeight);
return lengthFor(buffer);
}
private byte[][] make_moov() throws RuntimeException {
int pos = 1;
byte[][] buffer = new byte[2 + (DIMENSIONAL_TWO * infoTracks.length) + (DIMENSIONAL_FIVE * infoTracks.length) + DIMENSIONAL_FIVE + 1][];
buffer[0] = new byte[]{
0x00, 0x00, 0x00, 0x00, 0x6D, 0x6F, 0x6F, 0x76
};
for (byte[] buff : make_mvhd()) {
buffer[pos++] = buff;
}
for (int i = 0; i < infoTracks.length; i++) {
for (byte[] buff : make_trak(i + 1, infoTracks[i].trak)) {
buffer[pos++] = buff;
}
}
buffer[pos] = new byte[]{
0x00, 0x00, 0x00, 0x00, 0x6D, 0x76, 0x65, 0x78
};
ByteBuffer.wrap(buffer[pos++]).putInt((infoTracks.length * DEFAULT_TREX_SIZE) + 8);
for (int i = 0; i < infoTracks.length; i++) {
for (byte[] buff : make_trex(i + 1, infoTracks[i].trex)) {
buffer[pos++] = buff;
}
}
// default udta
buffer[pos] = new byte[]{
0x00, 0x00, 0x00, 0x5C, 0x75, 0x64, 0x74, 0x61, 0x00, 0x00, 0x00, 0x54, 0x6D, 0x65, 0x74, 0x61,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x68, 0x64, 0x6C, 0x72, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x69, 0x72, 0x61, 0x70, 0x70, 0x6C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0x69, 0x6C, 0x73, 0x74, 0x00, 0x00, 0x00,
0x1F, (byte) 0xA9, 0x63, 0x6D, 0x74, 0x00, 0x00, 0x00, 0x17, 0x64, 0x61, 0x74, 0x61, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x00,
0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65// "NewPipe" binary string
};
return lengthFor(buffer);
}
private byte[][] make_trex(int trackId, Trex trex) {
byte[][] buffer = new byte[][]{
{
0x00, 0x00, 0x00, 0x20, 0x74, 0x72, 0x65, 0x78, 0x00, 0x00, 0x00, 0x00
},
new byte[20]
};
ByteBuffer.wrap(buffer[1])
.putInt(trackId)
.putInt(trex.defaultSampleDescriptionIndex)
.putInt(trex.defaultSampleDuration)
.putInt(trex.defaultSampleSize)
.putInt(trex.defaultSampleFlags);
return buffer;
}
private byte[][] make_tfra(int trackId, List<Integer> times, List<Long> moofOffsets) {
int entryCount = times.size() - 1;
byte[][] buffer = new byte[DIMENSIONAL_TWO][];
buffer[0] = new byte[]{
0x00, 0x00, 0x00, 0x00, 0x74, 0x66, 0x72, 0x61, 0x01, 0x00, 0x00, 0x00
};
buffer[1] = new byte[12 + ((16 + TFRA_TTS_DEFAULT.length) * entryCount)];
ByteBuffer set = ByteBuffer.wrap(buffer[1]);
set.putInt(trackId);
set.position(8);
set.putInt(entryCount);
long decodeTime = 0;
for (int i = 0; i < entryCount; i++) {
decodeTime += times.get(i);
set.putLong(decodeTime);
set.putLong(moofOffsets.get(i));
set.put(TFRA_TTS_DEFAULT);// default values: traf number/trun number/sample number
}
return lengthFor(buffer);
}
private byte[][] make_mfra() {
byte[][] buffer = new byte[2 + (DIMENSIONAL_TWO * infoTracks.length)][];
buffer[0] = new byte[]{
0x00, 0x00, 0x00, 0x00, 0x6D, 0x66, 0x72, 0x61
};
int pos = 1;
for (int i = 0; i < infoTracks.length; i++) {
for (byte[] buff : make_tfra(i + 1, chunkTimes.get(i), moofOffsets)) {
buffer[pos++] = buff;
}
}
buffer[pos] = new byte[]{// mfro
0x00, 0x00, 0x00, 0x10, 0x6D, 0x66, 0x72, 0x6F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
lengthFor(buffer);
ByteBuffer set = ByteBuffer.wrap(buffer[pos]);
set.position(12);
set.put(buffer[0], 0, 4);
return buffer;
}
private byte[][] make_sidx(int internalTrackId, long firstOffset) {
List<Integer> times = chunkTimes.get(internalTrackId);
int count = times.size() - 1;// the first item is ignored (composition time)
if (count > 65535) {
throw new OutOfMemoryError("to many fragments. sidx limit is 65535, found " + String.valueOf(count));
}
byte[][] buffer = new byte[][]{
new byte[]{
0x00, 0x00, 0x00, 0x00, 0x73, 0x69, 0x64, 0x78, 0x01, 0x00, 0x00, 0x00
},
new byte[calcSidxBodySize(count)]
};
lengthFor(buffer);
ByteBuffer set = ByteBuffer.wrap(buffer[1]);
set.putInt(internalTrackId + 1);
set.putInt(infoTracks[internalTrackId].trak.mdia_mdhd_timeScale);
set.putLong(0);
set.putLong(firstOffset - ByteBuffer.wrap(buffer[0]).getInt());
set.putInt(0xFFFF & count);// unsigned
int i = 0;
while (i < count) {
set.putInt(fragSizes.get(i) & 0x7fffffff);// default reference type is 0
set.putInt(times.get(i + 1));
set.putInt(0x90000000);// default SAP settings
i++;
}
return buffer;
}
private byte[][] make_free(int totalSize) {
return lengthFor(new byte[][]{
new byte[]{0x00, 0x00, 0x00, 0x00, 0x66, 0x72, 0x65, 0x65},
new byte[totalSize - 8]// this is waste of RAM
});
}
//</editor-fold>
class TrunExtra {
ByteBuffer byteBuffer;
}
}

View File

@@ -0,0 +1,810 @@
package org.schabi.newpipe.streams;
import org.schabi.newpipe.streams.Mp4DashReader.Hdlr;
import org.schabi.newpipe.streams.Mp4DashReader.Mdia;
import org.schabi.newpipe.streams.Mp4DashReader.Mp4DashChunk;
import org.schabi.newpipe.streams.Mp4DashReader.Mp4DashSample;
import org.schabi.newpipe.streams.Mp4DashReader.Mp4Track;
import org.schabi.newpipe.streams.Mp4DashReader.TrunEntry;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
import java.nio.ByteBuffer;
/**
* @author kapodamy
*/
public class Mp4FromDashWriter {
private final static int EPOCH_OFFSET = 2082844800;
private final static short DEFAULT_TIMESCALE = 1000;
private final static byte SAMPLES_PER_CHUNK_INIT = 2;
private final static byte SAMPLES_PER_CHUNK = 6;// ffmpeg uses 2, basic uses 1 (with 60fps uses 21 or 22). NewPipe will use 6
private final static long THRESHOLD_FOR_CO64 = 0xFFFEFFFFL;// near 3.999 GiB
private final static int THRESHOLD_MOOV_LENGTH = (256 * 1024) + (2048 * 1024); // 2.2 MiB enough for: 1080p 60fps 00h35m00s
private final long time;
private ByteBuffer auxBuffer;
private SharpStream outStream;
private long lastWriteOffset = -1;
private long writeOffset;
private boolean moovSimulation = true;
private boolean done = false;
private boolean parsed = false;
private Mp4Track[] tracks;
private SharpStream[] sourceTracks;
private Mp4DashReader[] readers;
private Mp4DashChunk[] readersChunks;
private int overrideMainBrand = 0x00;
public Mp4FromDashWriter(SharpStream... sources) throws IOException {
for (SharpStream src : sources) {
if (!src.canRewind() && !src.canRead()) {
throw new IOException("All sources must be readable and allow rewind");
}
}
sourceTracks = sources;
readers = new Mp4DashReader[sourceTracks.length];
readersChunks = new Mp4DashChunk[readers.length];
time = (System.currentTimeMillis() / 1000L) + EPOCH_OFFSET;
}
public Mp4Track[] getTracksFromSource(int sourceIndex) throws IllegalStateException {
if (!parsed) {
throw new IllegalStateException("All sources must be parsed first");
}
return readers[sourceIndex].getAvailableTracks();
}
public void parseSources() throws IOException, IllegalStateException {
if (done) {
throw new IllegalStateException("already done");
}
if (parsed) {
throw new IllegalStateException("already parsed");
}
try {
for (int i = 0; i < readers.length; i++) {
readers[i] = new Mp4DashReader(sourceTracks[i]);
readers[i].parse();
}
} finally {
parsed = true;
}
}
public void selectTracks(int... trackIndex) throws IOException {
if (done) {
throw new IOException("already done");
}
if (tracks != null) {
throw new IOException("tracks already selected");
}
try {
tracks = new Mp4Track[readers.length];
for (int i = 0; i < readers.length; i++) {
tracks[i] = readers[i].selectTrack(trackIndex[i]);
}
} finally {
parsed = true;
}
}
public void setMainBrand(int brandId) {
overrideMainBrand = brandId;
}
public boolean isDone() {
return done;
}
public boolean isParsed() {
return parsed;
}
public void close() throws IOException {
done = true;
parsed = true;
for (SharpStream src : sourceTracks) {
src.close();
}
tracks = null;
sourceTracks = null;
readers = null;
readersChunks = null;
auxBuffer = null;
outStream = null;
}
public void build(SharpStream output) throws IOException {
if (done) {
throw new RuntimeException("already done");
}
if (!output.canWrite()) {
throw new IOException("the provided output is not writable");
}
//
// WARNING: the muxer requires at least 8 samples of every track
// not allowed for very short tracks (less than 0.5 seconds)
//
outStream = output;
int read = 8;// mdat box header size
long totalSampleSize = 0;
int[] sampleExtra = new int[readers.length];
int[] defaultMediaTime = new int[readers.length];
int[] defaultSampleDuration = new int[readers.length];
int[] sampleCount = new int[readers.length];
TablesInfo[] tablesInfo = new TablesInfo[tracks.length];
for (int i = 0; i < tablesInfo.length; i++) {
tablesInfo[i] = new TablesInfo();
}
//<editor-fold defaultstate="expanded" desc="calculate stbl sample tables size and required moov values">
for (int i = 0; i < readers.length; i++) {
int samplesSize = 0;
int sampleSizeChanges = 0;
int compositionOffsetLast = -1;
Mp4DashChunk chunk;
while ((chunk = readers[i].getNextChunk(true)) != null) {
if (defaultMediaTime[i] < 1 && chunk.moof.traf.tfhd.defaultSampleDuration > 0) {
defaultMediaTime[i] = chunk.moof.traf.tfhd.defaultSampleDuration;
}
read += chunk.moof.traf.trun.chunkSize;
sampleExtra[i] += chunk.moof.traf.trun.chunkDuration;// calculate track duration
TrunEntry info;
while ((info = chunk.getNextSampleInfo()) != null) {
if (info.isKeyframe) {
tablesInfo[i].stss++;
}
if (info.sampleDuration > defaultSampleDuration[i]) {
defaultSampleDuration[i] = info.sampleDuration;
}
tablesInfo[i].stsz++;
if (samplesSize != info.sampleSize) {
samplesSize = info.sampleSize;
sampleSizeChanges++;
}
if (info.hasCompositionTimeOffset) {
if (info.sampleCompositionTimeOffset != compositionOffsetLast) {
tablesInfo[i].ctts++;
compositionOffsetLast = info.sampleCompositionTimeOffset;
}
}
totalSampleSize += info.sampleSize;
}
}
if (defaultMediaTime[i] < 1) {
defaultMediaTime[i] = defaultSampleDuration[i];
}
readers[i].rewind();
int tmp = tablesInfo[i].stsz - SAMPLES_PER_CHUNK_INIT;
tablesInfo[i].stco = (tmp / SAMPLES_PER_CHUNK) + 1;// +1 for samples in first chunk
tmp = tmp % SAMPLES_PER_CHUNK;
if (tmp == 0) {
tablesInfo[i].stsc = 2;// first chunk (init) and succesive chunks
tablesInfo[i].stsc_bEntries = new int[]{
1, SAMPLES_PER_CHUNK_INIT, 1,
2, SAMPLES_PER_CHUNK, 1
};
} else {
tablesInfo[i].stsc = 3;// first chunk (init) and succesive chunks and remain chunk
tablesInfo[i].stsc_bEntries = new int[]{
1, SAMPLES_PER_CHUNK_INIT, 1,
2, SAMPLES_PER_CHUNK, 1,
tablesInfo[i].stco + 1, tmp, 1
};
tablesInfo[i].stco++;
}
sampleCount[i] = tablesInfo[i].stsz;
if (sampleSizeChanges == 1) {
tablesInfo[i].stsz = 0;
tablesInfo[i].stsz_default = samplesSize;
} else {
tablesInfo[i].stsz_default = 0;
}
if (tablesInfo[i].stss == tablesInfo[i].stsz) {
tablesInfo[i].stss = -1;// for audio tracks (all samples are keyframes)
}
// ensure track duration
if (tracks[i].trak.tkhd.duration < 1) {
tracks[i].trak.tkhd.duration = sampleExtra[i];// this never should happen
}
}
//</editor-fold>
boolean is64 = read > THRESHOLD_FOR_CO64;
// calculate the moov size;
int auxSize = make_moov(defaultMediaTime, tablesInfo, is64);
if (auxSize < THRESHOLD_MOOV_LENGTH) {
auxBuffer = ByteBuffer.allocate(auxSize);// cache moov in the memory
}
moovSimulation = false;
writeOffset = 0;
final int ftyp_size = make_ftyp();
// reserve moov space in the output stream
/*if (outStream.canSetLength()) {
long length = writeOffset + auxSize;
outStream.setLength(length);
outSeek(length);
} else {*/
if (auxSize > 0) {
int length = auxSize;
byte[] buffer = new byte[8 * 1024];// 8 KiB
while (length > 0) {
int count = Math.min(length, buffer.length);
outWrite(buffer, 0, count);
length -= count;
}
}
if (auxBuffer == null) {
outSeek(ftyp_size);
}
// tablesInfo contais row counts
// and after returning from make_moov() will contain table offsets
make_moov(defaultMediaTime, tablesInfo, is64);
// write tables: stts stsc
// reset for ctts table: sampleCount sampleExtra
for (int i = 0; i < readers.length; i++) {
writeEntryArray(tablesInfo[i].stts, 2, sampleCount[i], defaultSampleDuration[i]);
writeEntryArray(tablesInfo[i].stsc, tablesInfo[i].stsc_bEntries.length, tablesInfo[i].stsc_bEntries);
tablesInfo[i].stsc_bEntries = null;
if (tablesInfo[i].ctts > 0) {
sampleCount[i] = 1;// index is not base zero
sampleExtra[i] = -1;
}
}
if (auxBuffer == null) {
outRestore();
}
outWrite(make_mdat(totalSampleSize, is64));
int[] sampleIndex = new int[readers.length];
int[] sizes = new int[SAMPLES_PER_CHUNK];
int[] sync = new int[SAMPLES_PER_CHUNK];
int written = readers.length;
while (written > 0) {
written = 0;
for (int i = 0; i < readers.length; i++) {
if (sampleIndex[i] < 0) {
continue;// track is done
}
long chunkOffset = writeOffset;
int syncCount = 0;
int limit = sampleIndex[i] == 0 ? SAMPLES_PER_CHUNK_INIT : SAMPLES_PER_CHUNK;
int j = 0;
for (; j < limit; j++) {
Mp4DashSample sample = getNextSample(i);
if (sample == null) {
if (tablesInfo[i].ctts > 0 && sampleExtra[i] >= 0) {
writeEntryArray(tablesInfo[i].ctts, 1, sampleCount[i], sampleExtra[i]);// flush last entries
}
sampleIndex[i] = -1;
break;
}
sampleIndex[i]++;
if (tablesInfo[i].ctts > 0) {
if (sample.info.sampleCompositionTimeOffset == sampleExtra[i]) {
sampleCount[i]++;
} else {
if (sampleExtra[i] >= 0) {
tablesInfo[i].ctts = writeEntryArray(tablesInfo[i].ctts, 2, sampleCount[i], sampleExtra[i]);
outRestore();
}
sampleCount[i] = 1;
sampleExtra[i] = sample.info.sampleCompositionTimeOffset;
}
}
if (tablesInfo[i].stss > 0 && sample.info.isKeyframe) {
sync[syncCount++] = sampleIndex[i];
}
if (tablesInfo[i].stsz > 0) {
sizes[j] = sample.data.length;
}
outWrite(sample.data, 0, sample.data.length);
}
if (j > 0) {
written++;
if (tablesInfo[i].stsz > 0) {
tablesInfo[i].stsz = writeEntryArray(tablesInfo[i].stsz, j, sizes);
}
if (syncCount > 0) {
tablesInfo[i].stss = writeEntryArray(tablesInfo[i].stss, syncCount, sync);
}
if (is64) {
tablesInfo[i].stco = writeEntry64(tablesInfo[i].stco, chunkOffset);
} else {
tablesInfo[i].stco = writeEntryArray(tablesInfo[i].stco, 1, (int) chunkOffset);
}
outRestore();
}
}
}
if (auxBuffer != null) {
// dump moov
outSeek(ftyp_size);
outStream.write(auxBuffer.array(), 0, auxBuffer.capacity());
auxBuffer = null;
}
}
private Mp4DashSample getNextSample(int track) throws IOException {
if (readersChunks[track] == null) {
readersChunks[track] = readers[track].getNextChunk(false);
if (readersChunks[track] == null) {
return null;// EOF reached
}
}
Mp4DashSample sample = readersChunks[track].getNextSample();
if (sample == null) {
readersChunks[track] = null;
return getNextSample(track);
} else {
return sample;
}
}
// <editor-fold defaultstate="expanded" desc="Stbl handling">
private int writeEntry64(int offset, long value) throws IOException {
outBackup();
auxSeek(offset);
auxWrite(ByteBuffer.allocate(8).putLong(value).array());
return offset + 8;
}
private int writeEntryArray(int offset, int count, int... values) throws IOException {
outBackup();
auxSeek(offset);
int size = count * 4;
ByteBuffer buffer = ByteBuffer.allocate(size);
for (int i = 0; i < count; i++) {
buffer.putInt(values[i]);
}
auxWrite(buffer.array());
return offset + size;
}
private void outBackup() {
if (auxBuffer == null && lastWriteOffset < 0) {
lastWriteOffset = writeOffset;
}
}
/**
* Restore to the previous position before the first call to writeEntry64()
* or writeEntryArray() methods.
*/
private void outRestore() throws IOException {
if (lastWriteOffset > 0) {
outSeek(lastWriteOffset);
lastWriteOffset = -1;
}
}
// </editor-fold>
// <editor-fold defaultstate="expanded" desc="Utils">
private void outWrite(byte[] buffer) throws IOException {
outWrite(buffer, 0, buffer.length);
}
private void outWrite(byte[] buffer, int offset, int count) throws IOException {
writeOffset += count;
outStream.write(buffer, offset, count);
}
private void outSeek(long offset) throws IOException {
if (outStream.canSeek()) {
outStream.seek(offset);
writeOffset = offset;
} else if (outStream.canRewind()) {
outStream.rewind();
writeOffset = 0;
outSkip(offset);
} else {
throw new IOException("cannot seek or rewind the output stream");
}
}
private void outSkip(long amount) throws IOException {
outStream.skip(amount);
writeOffset += amount;
}
private int lengthFor(int offset) throws IOException {
int size = auxOffset() - offset;
if (moovSimulation) {
return size;
}
auxSeek(offset);
auxWrite(size);
auxSkip(size - 4);
return size;
}
private int make(int type, int extra, int columns, int rows) throws IOException {
final byte base = 16;
int size = columns * rows * 4;
int total = size + base;
int offset = auxOffset();
if (extra >= 0) {
total += 4;
}
auxWrite(ByteBuffer.allocate(12)
.putInt(total)
.putInt(type)
.putInt(0x00)// default version & flags
.array()
);
if (extra >= 0) {
//size += 4;// commented for auxiliar buffer !!!
offset += 4;
auxWrite(extra);
}
auxWrite(rows);
auxSkip(size);
return offset + base;
}
private void auxWrite(int value) throws IOException {
auxWrite(ByteBuffer.allocate(4)
.putInt(value)
.array()
);
}
private void auxWrite(byte[] buffer) throws IOException {
if (moovSimulation) {
writeOffset += buffer.length;
} else if (auxBuffer == null) {
outWrite(buffer, 0, buffer.length);
} else {
auxBuffer.put(buffer);
}
}
private void auxSeek(int offset) throws IOException {
if (moovSimulation) {
writeOffset = offset;
} else if (auxBuffer == null) {
outSeek(offset);
} else {
auxBuffer.position(offset);
}
}
private void auxSkip(int amount) throws IOException {
if (moovSimulation) {
writeOffset += amount;
} else if (auxBuffer == null) {
outSkip(amount);
} else {
auxBuffer.position(auxBuffer.position() + amount);
}
}
private int auxOffset() {
return auxBuffer == null ? (int) writeOffset : auxBuffer.position();
}
// </editor-fold>
// <editor-fold defaultstate="expanded" desc="Box makers">
private int make_ftyp() throws IOException {
byte[] buffer = new byte[]{
0x00, 0x00, 0x00, 0x1C, 0x66, 0x74, 0x79, 0x70,// ftyp
0x6D, 0x70, 0x34, 0x32,// mayor brand (mp42)
0x00, 0x00, 0x02, 0x00,// default minor version (512)
0x6D, 0x70, 0x34, 0x31, 0x69, 0x73, 0x6F, 0x6D, 0x69, 0x73, 0x6F, 0x32// compatible brands: mp41 isom iso2
};
if (overrideMainBrand != 0)
ByteBuffer.wrap(buffer).putInt(8, overrideMainBrand);
outWrite(buffer);
return buffer.length;
}
private byte[] make_mdat(long refSize, boolean is64) {
if (is64) {
refSize += 16;
} else {
refSize += 8;
}
ByteBuffer buffer = ByteBuffer.allocate(is64 ? 16 : 8)
.putInt(is64 ? 0x01 : (int) refSize)
.putInt(0x6D646174);// mdat
if (is64) {
buffer.putLong(refSize);
}
return buffer.array();
}
private void make_mvhd(long longestTrack) throws IOException {
auxWrite(new byte[]{
0x00, 0x00, 0x00, 0x78, 0x6D, 0x76, 0x68, 0x64, 0x01, 0x00, 0x00, 0x00
});
auxWrite(ByteBuffer.allocate(28)
.putLong(time)
.putLong(time)
.putInt(DEFAULT_TIMESCALE)
.putLong(longestTrack)
.array()
);
auxWrite(new byte[]{
0x00, 0x01, 0x00, 0x00, 0x01, 0x00,// default volume and rate
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,// reserved values
// default matrix
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x40, 0x00, 0x00, 0x00
});
auxWrite(new byte[24]);// predefined
auxWrite(ByteBuffer.allocate(4)
.putInt(tracks.length + 1)
.array()
);
}
private int make_moov(int[] defaultMediaTime, TablesInfo[] tablesInfo, boolean is64) throws RuntimeException, IOException {
int start = auxOffset();
auxWrite(new byte[]{
0x00, 0x00, 0x00, 0x00, 0x6D, 0x6F, 0x6F, 0x76
});
long longestTrack = 0;
long[] durations = new long[tracks.length];
for (int i = 0; i < durations.length; i++) {
durations[i] = (long) Math.ceil(
((double) tracks[i].trak.tkhd.duration / tracks[i].trak.mdia.mdhd_timeScale) * DEFAULT_TIMESCALE
);
if (durations[i] > longestTrack) {
longestTrack = durations[i];
}
}
make_mvhd(longestTrack);
for (int i = 0; i < tracks.length; i++) {
if (tracks[i].trak.tkhd.matrix.length != 36) {
throw new RuntimeException("bad track matrix length (expected 36) in track n°" + i);
}
make_trak(i, durations[i], defaultMediaTime[i], tablesInfo[i], is64);
}
// udta/meta/ilst/©too
auxWrite(new byte[]{
0x00, 0x00, 0x00, 0x5C, 0x75, 0x64, 0x74, 0x61, 0x00, 0x00, 0x00, 0x54, 0x6D, 0x65, 0x74, 0x61,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x68, 0x64, 0x6C, 0x72, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x69, 0x72, 0x61, 0x70, 0x70, 0x6C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0x69, 0x6C, 0x73, 0x74, 0x00, 0x00, 0x00,
0x1F, (byte) 0xA9, 0x74, 0x6F, 0x6F, 0x00, 0x00, 0x00, 0x17, 0x64, 0x61, 0x74, 0x61, 0x00, 0x00,
0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65// "NewPipe" binary string
});
return lengthFor(start);
}
private void make_trak(int index, long duration, int defaultMediaTime, TablesInfo tables, boolean is64) throws IOException {
int start = auxOffset();
auxWrite(new byte[]{
0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x61, 0x6B,// trak header
0x00, 0x00, 0x00, 0x68, 0x74, 0x6B, 0x68, 0x64, 0x01, 0x00, 0x00, 0x03 // tkhd header
});
ByteBuffer buffer = ByteBuffer.allocate(48);
buffer.putLong(time);
buffer.putLong(time);
buffer.putInt(index + 1);
buffer.position(24);
buffer.putLong(duration);
buffer.position(40);
buffer.putShort(tracks[index].trak.tkhd.bLayer);
buffer.putShort(tracks[index].trak.tkhd.bAlternateGroup);
buffer.putShort(tracks[index].trak.tkhd.bVolume);
auxWrite(buffer.array());
auxWrite(tracks[index].trak.tkhd.matrix);
auxWrite(ByteBuffer.allocate(8)
.putInt(tracks[index].trak.tkhd.bWidth)
.putInt(tracks[index].trak.tkhd.bHeight)
.array()
);
auxWrite(new byte[]{
0x00, 0x00, 0x00, 0x24, 0x65, 0x64, 0x74, 0x73,// edts header
0x00, 0x00, 0x00, 0x1C, 0x65, 0x6C, 0x73, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01// elst header
});
int bMediaRate;
int mediaTime;
if (tracks[index].trak.edst_elst == null) {
// is a audio track ¿is edst/elst opcional for audio tracks?
mediaTime = 0x00;// ffmpeg set this value as zero, instead of defaultMediaTime
bMediaRate = 0x00010000;
} else {
mediaTime = (int) tracks[index].trak.edst_elst.MediaTime;
bMediaRate = tracks[index].trak.edst_elst.bMediaRate;
}
auxWrite(ByteBuffer
.allocate(12)
.putInt((int) duration)
.putInt(mediaTime)
.putInt(bMediaRate)
.array()
);
make_mdia(tracks[index].trak.mdia, tables, is64);
lengthFor(start);
}
private void make_mdia(Mdia mdia, TablesInfo tablesInfo, boolean is64) throws IOException {
int start_mdia = auxOffset();
auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x69, 0x61});// mdia
auxWrite(mdia.mdhd);
auxWrite(make_hdlr(mdia.hdlr));
int start_minf = auxOffset();
auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x6D, 0x69, 0x6E, 0x66});// minf
auxWrite(mdia.minf.$mhd);
auxWrite(mdia.minf.dinf);
int start_stbl = auxOffset();
auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x73, 0x74, 0x62, 0x6C});// stbl
auxWrite(mdia.minf.stbl_stsd);
//
// In audio tracks the following tables is not required: ssts ctts
// And stsz can be empty if has a default sample size
//
if (moovSimulation) {
make(0x73747473, -1, 2, 1);
if (tablesInfo.stss > 0) {
make(0x73747373, -1, 1, tablesInfo.stss);
}
if (tablesInfo.ctts > 0) {
make(0x63747473, -1, 2, tablesInfo.ctts);
}
make(0x73747363, -1, 3, tablesInfo.stsc);
make(0x7374737A, tablesInfo.stsz_default, 1, tablesInfo.stsz);
make(is64 ? 0x636F3634 : 0x7374636F, -1, is64 ? 2 : 1, tablesInfo.stco);
} else {
tablesInfo.stts = make(0x73747473, -1, 2, 1);
if (tablesInfo.stss > 0) {
tablesInfo.stss = make(0x73747373, -1, 1, tablesInfo.stss);
}
if (tablesInfo.ctts > 0) {
tablesInfo.ctts = make(0x63747473, -1, 2, tablesInfo.ctts);
}
tablesInfo.stsc = make(0x73747363, -1, 3, tablesInfo.stsc);
tablesInfo.stsz = make(0x7374737A, tablesInfo.stsz_default, 1, tablesInfo.stsz);
tablesInfo.stco = make(is64 ? 0x636F3634 : 0x7374636F, -1, is64 ? 2 : 1, tablesInfo.stco);
}
lengthFor(start_stbl);
lengthFor(start_minf);
lengthFor(start_mdia);
}
private byte[] make_hdlr(Hdlr hdlr) {
ByteBuffer buffer = ByteBuffer.wrap(new byte[]{
0x00, 0x00, 0x00, 0x77, 0x68, 0x64, 0x6C, 0x72,// hdlr
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// binary string "ISO Media file created in NewPipe (A libre lightweight streaming frontend for Android)."
0x49, 0x53, 0x4F, 0x20, 0x4D, 0x65, 0x64, 0x69, 0x61, 0x20, 0x66, 0x69, 0x6C, 0x65, 0x20, 0x63,
0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x20, 0x69, 0x6E, 0x20, 0x4E, 0x65, 0x77, 0x50, 0x69, 0x70,
0x65, 0x20, 0x28, 0x41, 0x20, 0x6C, 0x69, 0x62, 0x72, 0x65, 0x20, 0x6C, 0x69, 0x67, 0x68, 0x74,
0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x20, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6D, 0x69, 0x6E, 0x67,
0x20, 0x66, 0x72, 0x6F, 0x6E, 0x74, 0x65, 0x6E, 0x64, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x41, 0x6E,
0x64, 0x72, 0x6F, 0x69, 0x64, 0x29, 0x2E
});
buffer.position(12);
buffer.putInt(hdlr.type);
buffer.putInt(hdlr.subType);
buffer.put(hdlr.bReserved);// always is a zero array
return buffer.array();
}
//</editor-fold>
class TablesInfo {
public int stts;
public int stsc;
public int[] stsc_bEntries;
public int ctts;
public int stsz;
public int stsz_default;
public int stss;
public int stco;
}
}

View File

@@ -1,5 +1,6 @@
package org.schabi.newpipe.streams;
import org.schabi.newpipe.streams.io.SharpStream;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
@@ -12,8 +13,6 @@ import java.nio.charset.Charset;
import java.text.ParseException;
import java.util.Locale;
import org.schabi.newpipe.streams.io.SharpStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
@@ -27,11 +26,11 @@ public class SubtitleConverter {
public void dumpTTML(SharpStream in, final SharpStream out, final boolean ignoreEmptyFrames, final boolean detectYoutubeDuplicateLines
) throws IOException, ParseException, SAXException, ParserConfigurationException, XPathExpressionException {
final FrameWriter callback = new FrameWriter() {
int frameIndex = 0;
final Charset charset = Charset.forName("utf-8");
@Override
public void yield(SubtitleFrame frame) throws IOException {
if (ignoreEmptyFrames && frame.isEmptyText()) {
@@ -48,13 +47,13 @@ public class SubtitleConverter {
out.write(NEW_LINE.getBytes(charset));
}
};
read_xml_based(in, callback, detectYoutubeDuplicateLines,
"tt", "xmlns", "http://www.w3.org/ns/ttml",
new String[]{"timedtext", "head", "wp"},
new String[]{"body", "div", "p"},
"begin", "end", true
);
);
}
private void read_xml_based(SharpStream source, FrameWriter callback, boolean detectYoutubeDuplicateLines,
@@ -70,7 +69,7 @@ public class SubtitleConverter {
* Language parsing is not supported
*/
byte[] buffer = new byte[source.available()];
byte[] buffer = new byte[(int) source.available()];
source.read(buffer);
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
@@ -206,7 +205,7 @@ public class SubtitleConverter {
}
}
private static NodeList selectNodes(Document xml, String[] path, String namespaceUri) throws XPathExpressionException {
private static NodeList selectNodes(Document xml, String[] path, String namespaceUri) {
Element ref = xml.getDocumentElement();
for (int i = 0; i < path.length - 1; i++) {

View File

@@ -1,65 +0,0 @@
package org.schabi.newpipe.streams;
import java.io.InputStream;
import java.io.IOException;
public class TrackDataChunk extends InputStream {
private final DataReader base;
private int size;
public TrackDataChunk(DataReader base, int size) {
this.base = base;
this.size = size;
}
@Override
public int read() throws IOException {
if (size < 1) {
return -1;
}
int res = base.read();
if (res >= 0) {
size--;
}
return res;
}
@Override
public int read(byte[] buffer) throws IOException {
return read(buffer, 0, buffer.length);
}
@Override
public int read(byte[] buffer, int offset, int count) throws IOException {
count = Math.min(size, count);
int read = base.read(buffer, offset, count);
size -= count;
return read;
}
@Override
public long skip(long amount) throws IOException {
long res = base.skipBytes(Math.min(amount, size));
size -= res;
return res;
}
@Override
public int available() {
return size;
}
@Override
public void close() {
size = 0;
}
@Override
public boolean markSupported() {
return false;
}
}

View File

@@ -1,12 +1,13 @@
package org.schabi.newpipe.streams;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.NoSuchElementException;
import java.util.Objects;
import org.schabi.newpipe.streams.io.SharpStream;
/**
*
@@ -121,7 +122,7 @@ public class WebMReader {
}
private String readString(Element parent) throws IOException {
return new String(readBlob(parent), "utf-8");
return new String(readBlob(parent), StandardCharsets.UTF_8);// or use "utf-8"
}
private byte[] readBlob(Element parent) throws IOException {
@@ -193,6 +194,7 @@ public class WebMReader {
return elem;
}
}
ensure(elem);
}
@@ -306,7 +308,7 @@ public class WebMReader {
entry.trackNumber = readNumber(elem);
break;
case ID_TrackType:
entry.trackType = (int)readNumber(elem);
entry.trackType = (int) readNumber(elem);
break;
case ID_CodecID:
entry.codecId = readString(elem);
@@ -445,7 +447,7 @@ public class WebMReader {
public class SimpleBlock {
public TrackDataChunk data;
public InputStream data;
SimpleBlock(Element ref) {
this.ref = ref;
@@ -492,7 +494,7 @@ public class WebMReader {
currentSimpleBlock = readSimpleBlock(elem);
if (currentSimpleBlock.trackNumber == tracks[selectedTrack].trackNumber) {
currentSimpleBlock.data = new TrackDataChunk(stream, (int) currentSimpleBlock.dataSize);
currentSimpleBlock.data = stream.getView((int) currentSimpleBlock.dataSize);
return currentSimpleBlock;
}

View File

@@ -1,20 +1,20 @@
package org.schabi.newpipe.streams;
import android.support.annotation.NonNull;
import org.schabi.newpipe.streams.WebMReader.Cluster;
import org.schabi.newpipe.streams.WebMReader.Segment;
import org.schabi.newpipe.streams.WebMReader.SimpleBlock;
import org.schabi.newpipe.streams.WebMReader.WebMTrack;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import org.schabi.newpipe.streams.io.SharpStream;
/**
*
* @author kapodamy
*/
public class WebMWriter {
@@ -94,10 +94,6 @@ public class WebMWriter {
}
}
public long getBytesWritten() {
return written;
}
public boolean isDone() {
return done;
}
@@ -111,7 +107,7 @@ public class WebMWriter {
parsed = true;
for (SharpStream src : sourceTracks) {
src.dispose();
src.close();
}
sourceTracks = null;
@@ -138,42 +134,42 @@ public class WebMWriter {
/* segment */
listBuffer.add(new byte[]{
0x18, 0x53, (byte) 0x80, 0x67, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00// segment content size
0x18, 0x53, (byte) 0x80, 0x67, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00// segment content size
});
long baseSegmentOffset = written + listBuffer.get(0).length;
/* seek head */
listBuffer.add(new byte[]{
0x11, 0x4d, (byte) 0x9b, 0x74, (byte) 0xbe,
0x4d, (byte) 0xbb, (byte) 0x8b,
0x53, (byte) 0xab, (byte) 0x84, 0x15, 0x49, (byte) 0xa9, 0x66, 0x53,
(byte) 0xac, (byte) 0x81, /*info offset*/ 0x43,
0x4d, (byte) 0xbb, (byte) 0x8b, 0x53, (byte) 0xab,
(byte) 0x84, 0x16, 0x54, (byte) 0xae, 0x6b, 0x53, (byte) 0xac, (byte) 0x81,
/*tracks offset*/ 0x6a,
0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1f,
0x43, (byte) 0xb6, 0x75, 0x53, (byte) 0xac, (byte) 0x84, /*cluster offset [2]*/ 0x00, 0x00, 0x00, 0x00,
0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1c, 0x53,
(byte) 0xbb, 0x6b, 0x53, (byte) 0xac, (byte) 0x84, /*cues offset [7]*/ 0x00, 0x00, 0x00, 0x00
0x11, 0x4d, (byte) 0x9b, 0x74, (byte) 0xbe,
0x4d, (byte) 0xbb, (byte) 0x8b,
0x53, (byte) 0xab, (byte) 0x84, 0x15, 0x49, (byte) 0xa9, 0x66, 0x53,
(byte) 0xac, (byte) 0x81, /*info offset*/ 0x43,
0x4d, (byte) 0xbb, (byte) 0x8b, 0x53, (byte) 0xab,
(byte) 0x84, 0x16, 0x54, (byte) 0xae, 0x6b, 0x53, (byte) 0xac, (byte) 0x81,
/*tracks offset*/ 0x6a,
0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1f,
0x43, (byte) 0xb6, 0x75, 0x53, (byte) 0xac, (byte) 0x84, /*cluster offset [2]*/ 0x00, 0x00, 0x00, 0x00,
0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1c, 0x53,
(byte) 0xbb, 0x6b, 0x53, (byte) 0xac, (byte) 0x84, /*cues offset [7]*/ 0x00, 0x00, 0x00, 0x00
});
/* info */
listBuffer.add(new byte[]{
0x15, 0x49, (byte) 0xa9, 0x66, (byte) 0xa2, 0x2a, (byte) 0xd7, (byte) 0xb1
0x15, 0x49, (byte) 0xa9, 0x66, (byte) 0xa2, 0x2a, (byte) 0xd7, (byte) 0xb1
});
listBuffer.add(encode(DEFAULT_TIMECODE_SCALE, true));// this value MUST NOT exceed 4 bytes
listBuffer.add(new byte[]{0x44, (byte) 0x89, (byte) 0x84,
0x00, 0x00, 0x00, 0x00,// info.duration
/* MuxingApp */
0x4d, (byte) 0x80, (byte) 0x87, 0x4E,
0x65, 0x77, 0x50, 0x69, 0x70, 0x65, // "NewPipe" binary string
/* WritingApp */
0x57, 0x41, (byte) 0x87, 0x4E,
0x65, 0x77, 0x50, 0x69, 0x70, 0x65// "NewPipe" binary string
0x00, 0x00, 0x00, 0x00,// info.duration
/* MuxingApp */
0x4d, (byte) 0x80, (byte) 0x87, 0x4E,
0x65, 0x77, 0x50, 0x69, 0x70, 0x65, // "NewPipe" binary string
/* WritingApp */
0x57, 0x41, (byte) 0x87, 0x4E,
0x65, 0x77, 0x50, 0x69, 0x70, 0x65// "NewPipe" binary string
});
/* tracks */
@@ -200,7 +196,6 @@ public class WebMWriter {
long nextCueTime = infoTracks[cuesForTrackId].trackType == 1 ? -1 : 0;
ArrayList<KeyFrame> keyFrames = new ArrayList<>(32);
//ArrayList<Block> chunks = new ArrayList<>(readers.length);
ArrayList<Long> clusterOffsets = new ArrayList<>(32);
ArrayList<Integer> clusterSizes = new ArrayList<>(32);
@@ -283,24 +278,21 @@ public class WebMWriter {
long segmentSize = written - offsetSegmentSizeSet - 7;
// final step write offsets and sizes
out.rewind();
written = 0;
skipTo(out, offsetSegmentSizeSet);
/* ---- final step write offsets and sizes ---- */
seekTo(out, offsetSegmentSizeSet);
writeLong(out, segmentSize);
if (predefinedDurations[durationFromTrackId] > -1) {
duration += predefinedDurations[durationFromTrackId];// this value is full-filled in makeTrackEntry() method
}
skipTo(out, offsetInfoDurationSet);
seekTo(out, offsetInfoDurationSet);
writeFloat(out, duration);
firstClusterOffset -= baseSegmentOffset;
skipTo(out, offsetClusterSet);
seekTo(out, offsetClusterSet);
writeInt(out, firstClusterOffset);
skipTo(out, cueReservedOffset);
seekTo(out, cueReservedOffset);
/* Cue */
dump(new byte[]{0x1c, 0x53, (byte) 0xbb, 0x6b, 0x20, 0x00, 0x00}, out);
@@ -321,20 +313,16 @@ public class WebMWriter {
voidBuffer.putShort((short) (firstClusterOffset - written - 4));
dump(voidBuffer.array(), out);
out.rewind();
written = 0;
skipTo(out, offsetCuesSet);
seekTo(out, offsetCuesSet);
writeInt(out, (int) (cueReservedOffset - baseSegmentOffset));
skipTo(out, cueReservedOffset + 5);
seekTo(out, cueReservedOffset + 5);
writeShort(out, cueSize);
for (int i = 0; i < clusterSizes.size(); i++) {
skipTo(out, clusterOffsets.get(i));
byte[] size = ByteBuffer.allocate(4).putInt(clusterSizes.get(i) | 0x200000).array();
out.write(size, 1, 3);
written += 3;
seekTo(out, clusterOffsets.get(i));
byte[] buffer = ByteBuffer.allocate(4).putInt(clusterSizes.get(i) | 0x10000000).array();
dump(buffer, out);
}
}
@@ -365,20 +353,29 @@ public class WebMWriter {
bloq.dataSize = (int) res.dataSize;
bloq.trackNumber = internalTrackId;
bloq.flags = res.flags;
bloq.absoluteTimecode = convertTimecode(res.relativeTimeCode, readersSegment[internalTrackId].info.timecodeScale, DEFAULT_TIMECODE_SCALE);
bloq.absoluteTimecode = convertTimecode(res.relativeTimeCode, readersSegment[internalTrackId].info.timecodeScale);
bloq.absoluteTimecode += readersCluter[internalTrackId].timecode;
return bloq;
}
private short convertTimecode(int time, long oldTimeScale, int newTimeScale) {
return (short) (time * (newTimeScale / oldTimeScale));
private short convertTimecode(int time, long oldTimeScale) {
return (short) (time * (DEFAULT_TIMECODE_SCALE / oldTimeScale));
}
private void skipTo(SharpStream stream, long absoluteOffset) throws IOException {
absoluteOffset -= written;
written += absoluteOffset;
stream.skip(absoluteOffset);
private void seekTo(SharpStream stream, long offset) throws IOException {
if (stream.canSeek()) {
stream.seek(offset);
} else {
if (offset > written) {
stream.skip(offset - written);
} else {
stream.rewind();
stream.skip(offset);
}
}
written = offset;
}
private void writeLong(SharpStream stream, long number) throws IOException {
@@ -453,7 +450,7 @@ public class WebMWriter {
/* cluster */
dump(new byte[]{0x1f, 0x43, (byte) 0xb6, 0x75}, stream);
clusterOffsets.add(written);// warning: max cluster size is 256 MiB
dump(new byte[]{0x20, 0x00, 0x00}, stream);
dump(new byte[]{0x10, 0x00, 0x00, 0x00}, stream);
startOffset = written;// size for the this cluster
@@ -468,12 +465,12 @@ public class WebMWriter {
private void makeEBML(SharpStream stream) throws IOException {
// deafult values
dump(new byte[]{
0x1A, 0x45, (byte) 0xDF, (byte) 0xA3, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x1F, 0x42, (byte) 0x86, (byte) 0x81, 0x01,
0x42, (byte) 0xF7, (byte) 0x81, 0x01, 0x42, (byte) 0xF2, (byte) 0x81, 0x04,
0x42, (byte) 0xF3, (byte) 0x81, 0x08, 0x42, (byte) 0x82, (byte) 0x84, 0x77,
0x65, 0x62, 0x6D, 0x42, (byte) 0x87, (byte) 0x81, 0x02,
0x42, (byte) 0x85, (byte) 0x81, 0x02
0x1A, 0x45, (byte) 0xDF, (byte) 0xA3, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x1F, 0x42, (byte) 0x86, (byte) 0x81, 0x01,
0x42, (byte) 0xF7, (byte) 0x81, 0x01, 0x42, (byte) 0xF2, (byte) 0x81, 0x04,
0x42, (byte) 0xF3, (byte) 0x81, 0x08, 0x42, (byte) 0x82, (byte) 0x84, 0x77,
0x65, 0x62, 0x6D, 0x42, (byte) 0x87, (byte) 0x81, 0x02,
0x42, (byte) 0x85, (byte) 0x81, 0x02
}, stream);
}
@@ -618,9 +615,10 @@ public class WebMWriter {
int offset = withLength ? 1 : 0;
byte[] buffer = new byte[offset + length];
long marker = (long) Math.floor((length - 1) / 8);
long marker = (long) Math.floor((length - 1f) / 8f);
for (int i = length - 1, mul = 1; i >= 0; i--, mul *= 0x100) {
float mul = 1;
for (int i = length - 1; i >= 0; i--, mul *= 0x100) {
long b = (long) Math.floor(number / mul);
if (!withLength && i == marker) {
b = b | (0x80 >> (length - 1));
@@ -637,11 +635,7 @@ public class WebMWriter {
private ArrayList<byte[]> encode(String value) {
byte[] str;
try {
str = value.getBytes("utf-8");
} catch (UnsupportedEncodingException err) {
str = value.getBytes();
}
str = value.getBytes(StandardCharsets.UTF_8);// or use "utf-8"
ArrayList<byte[]> buffer = new ArrayList<>(2);
buffer.add(encode(str.length, false));
@@ -720,9 +714,10 @@ public class WebMWriter {
return (flags & 0x80) == 0x80;
}
@NonNull
@Override
public String toString() {
return String.format("trackNumber=%s isKeyFrame=%S absoluteTimecode=%s", trackNumber, (flags & 0x80) == 0x80, absoluteTimecode);
return String.format("trackNumber=%s isKeyFrame=%S absoluteTimecode=%s", trackNumber, isKeyframe(), absoluteTimecode);
}
}
}

View File

@@ -1,11 +1,12 @@
package org.schabi.newpipe.streams.io;
import java.io.Closeable;
import java.io.IOException;
/**
* based c#
* based on c#
*/
public abstract class SharpStream {
public abstract class SharpStream implements Closeable {
public abstract int read() throws IOException;
@@ -15,16 +16,14 @@ public abstract class SharpStream {
public abstract long skip(long amount) throws IOException;
public abstract int available();
public abstract long available();
public abstract void rewind() throws IOException;
public abstract boolean isClosed();
public abstract void dispose();
public abstract boolean isDisposed();
@Override
public abstract void close();
public abstract boolean canRewind();
@@ -32,6 +31,13 @@ public abstract class SharpStream {
public abstract boolean canWrite();
public boolean canSetLength() {
return false;
}
public boolean canSeek() {
return false;
}
public abstract void write(byte value) throws IOException;
@@ -39,9 +45,19 @@ public abstract class SharpStream {
public abstract void write(byte[] buffer, int offset, int count) throws IOException;
public abstract void flush() throws IOException;
public void flush() throws IOException {
// STUB
}
public void setLength(long length) throws IOException {
throw new IOException("Not implemented");
}
public void seek(long offset) throws IOException {
throw new IOException("Not implemented");
}
public long length() throws IOException {
throw new UnsupportedOperationException("Unsupported operation");
}
}

View File

@@ -124,7 +124,7 @@ public class CommentTextOnTouchListener implements View.OnTouchListener {
.observeOn(AndroidSchedulers.mainThread())
.subscribe(info -> {
PlayQueue playQueue = new SinglePlayQueue((StreamInfo) info, seconds*1000);
NavigationHelper.playOnPopupPlayer(context, playQueue);
NavigationHelper.playOnPopupPlayer(context, playQueue, false);
});
return true;
}

View File

@@ -228,6 +228,10 @@ public final class ExtractorHelper {
});
}
public static boolean isCached(final int serviceId, final String url, InfoItem.InfoType infoType) {
return null != loadFromCache(serviceId, url, infoType).blockingGet();
}
/**
* A simple and general error handler that show a Toast for known exceptions, and for others, opens the report error activity with the (optional) error message.
*/
@@ -243,8 +247,6 @@ public final class ExtractorHelper {
context.startActivity(intent);
} else if (exception instanceof IOException) {
Toast.makeText(context, R.string.network_error, Toast.LENGTH_LONG).show();
} else if (exception instanceof YoutubeStreamExtractor.GemaException) {
Toast.makeText(context, R.string.blocked_by_gema, Toast.LENGTH_LONG).show();
} else if (exception instanceof ContentNotAvailableException) {
Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show();
} else {

View File

@@ -2,6 +2,7 @@ package org.schabi.newpipe.util;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.support.annotation.NonNull;
@@ -29,7 +30,7 @@ public class FilePickerActivityHelper extends com.nononsenseapps.filepicker.File
@Override
public void onCreate(Bundle savedInstanceState) {
if(ThemeHelper.isLightThemeSelected(this)) {
if (ThemeHelper.isLightThemeSelected(this)) {
this.setTheme(R.style.FilePickerThemeLight);
} else {
this.setTheme(R.style.FilePickerThemeDark);
@@ -73,6 +74,11 @@ public class FilePickerActivityHelper extends com.nononsenseapps.filepicker.File
.putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_NEW_FILE);
}
public static boolean isOwnFileUri(@NonNull Context context, @NonNull Uri uri) {
if (uri.getAuthority() == null) return false;
return uri.getAuthority().startsWith(context.getPackageName());
}
/*//////////////////////////////////////////////////////////////////////////
// Internal
//////////////////////////////////////////////////////////////////////////*/

View File

@@ -10,6 +10,9 @@ import java.util.regex.Pattern;
public class FilenameUtils {
private static final String CHARSET_MOST_SPECIAL = "[\\n\\r|?*<\":\\\\>/']+";
private static final String CHARSET_ONLY_LETTERS_AND_DIGITS = "[^\\w\\d]+";
/**
* #143 #44 #42 #22: make sure that the filename does not contain illegal chars.
* @param context the context to retrieve strings and preferences from
@@ -18,11 +21,28 @@ public class FilenameUtils {
*/
public static String createFilename(Context context, String title) {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
final String key = context.getString(R.string.settings_file_charset_key);
final String value = sharedPreferences.getString(key, context.getString(R.string.default_file_charset_value));
Pattern pattern = Pattern.compile(value);
final String charset_ld = context.getString(R.string.charset_letters_and_digits_value);
final String charset_ms = context.getString(R.string.charset_most_special_value);
final String defaultCharset = context.getString(R.string.default_file_charset_value);
final String replacementChar = sharedPreferences.getString(context.getString(R.string.settings_file_replacement_character_key), "_");
String selectedCharset = sharedPreferences.getString(context.getString(R.string.settings_file_charset_key), null);
final String charset;
if (selectedCharset == null || selectedCharset.isEmpty()) selectedCharset = defaultCharset;
if (selectedCharset.equals(charset_ld)) {
charset = CHARSET_ONLY_LETTERS_AND_DIGITS;
} else if (selectedCharset.equals(charset_ms)) {
charset = CHARSET_MOST_SPECIAL;
} else {
charset = selectedCharset;// ¿is the user using a custom charset?
}
Pattern pattern = Pattern.compile(charset);
return createFilename(title, pattern, replacementChar);
}

View File

@@ -430,24 +430,26 @@ public final class ListHelper {
*/
private static String getResolutionLimit(Context context) {
String resolutionLimit = null;
if (!isWifiActive(context)) {
if (isMeteredNetwork(context)) {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
String defValue = context.getString(R.string.limit_data_usage_none_key);
String value = preferences.getString(
context.getString(R.string.limit_mobile_data_usage_key), defValue);
resolutionLimit = value.equals(defValue) ? null : value;
resolutionLimit = defValue.equals(value) ? null : value;
}
return resolutionLimit;
}
/**
* Are we connected to wifi?
* The current network is metered (like mobile data)?
* @param context App context
* @return {@code true} if connected to wifi
* @return {@code true} if connected to a metered network
*/
private static boolean isWifiActive(Context context)
private static boolean isMeteredNetwork(Context context)
{
ConnectivityManager manager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
return manager != null && manager.getActiveNetworkInfo() != null && manager.getActiveNetworkInfo().getType() == ConnectivityManager.TYPE_WIFI;
if (manager == null || manager.getActiveNetworkInfo() == null) return false;
return manager.isActiveNetworkMetered();
}
}

View File

@@ -69,12 +69,14 @@ public class NavigationHelper {
public static Intent getPlayerIntent(@NonNull final Context context,
@NonNull final Class targetClazz,
@NonNull final PlayQueue playQueue,
@Nullable final String quality) {
@Nullable final String quality,
final boolean resumePlayback) {
Intent intent = new Intent(context, targetClazz);
final String cacheKey = SerializedCache.getInstance().put(playQueue, PlayQueue.class);
if (cacheKey != null) intent.putExtra(VideoPlayer.PLAY_QUEUE_KEY, cacheKey);
if (quality != null) intent.putExtra(VideoPlayer.PLAYBACK_QUALITY, quality);
intent.putExtra(VideoPlayer.RESUME_PLAYBACK, resumePlayback);
return intent;
}
@@ -82,16 +84,18 @@ public class NavigationHelper {
@NonNull
public static Intent getPlayerIntent(@NonNull final Context context,
@NonNull final Class targetClazz,
@NonNull final PlayQueue playQueue) {
return getPlayerIntent(context, targetClazz, playQueue, null);
@NonNull final PlayQueue playQueue,
final boolean resumePlayback) {
return getPlayerIntent(context, targetClazz, playQueue, null, resumePlayback);
}
@NonNull
public static Intent getPlayerEnqueueIntent(@NonNull final Context context,
@NonNull final Class targetClazz,
@NonNull final PlayQueue playQueue,
final boolean selectOnAppend) {
return getPlayerIntent(context, targetClazz, playQueue)
final boolean selectOnAppend,
final boolean resumePlayback) {
return getPlayerIntent(context, targetClazz, playQueue, resumePlayback)
.putExtra(BasePlayer.APPEND_ONLY, true)
.putExtra(BasePlayer.SELECT_ON_APPEND, selectOnAppend);
}
@@ -104,40 +108,41 @@ public class NavigationHelper {
final float playbackSpeed,
final float playbackPitch,
final boolean playbackSkipSilence,
@Nullable final String playbackQuality) {
return getPlayerIntent(context, targetClazz, playQueue, playbackQuality)
@Nullable final String playbackQuality,
final boolean resumePlayback) {
return getPlayerIntent(context, targetClazz, playQueue, playbackQuality, resumePlayback)
.putExtra(BasePlayer.REPEAT_MODE, repeatMode)
.putExtra(BasePlayer.PLAYBACK_SPEED, playbackSpeed)
.putExtra(BasePlayer.PLAYBACK_PITCH, playbackPitch)
.putExtra(BasePlayer.PLAYBACK_SKIP_SILENCE, playbackSkipSilence);
}
public static void playOnMainPlayer(final Context context, final PlayQueue queue) {
final Intent playerIntent = getPlayerIntent(context, MainVideoPlayer.class, queue);
public static void playOnMainPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) {
final Intent playerIntent = getPlayerIntent(context, MainVideoPlayer.class, queue, resumePlayback);
playerIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(playerIntent);
}
public static void playOnPopupPlayer(final Context context, final PlayQueue queue) {
public static void playOnPopupPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) {
if (!PermissionHelper.isPopupEnabled(context)) {
PermissionHelper.showPopupEnablementToast(context);
return;
}
Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show();
startService(context, getPlayerIntent(context, PopupVideoPlayer.class, queue));
startService(context, getPlayerIntent(context, PopupVideoPlayer.class, queue, resumePlayback));
}
public static void playOnBackgroundPlayer(final Context context, final PlayQueue queue) {
public static void playOnBackgroundPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) {
Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT).show();
startService(context, getPlayerIntent(context, BackgroundPlayer.class, queue));
startService(context, getPlayerIntent(context, BackgroundPlayer.class, queue, resumePlayback));
}
public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue) {
enqueueOnPopupPlayer(context, queue, false);
public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) {
enqueueOnPopupPlayer(context, queue, false, resumePlayback);
}
public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue, boolean selectOnAppend) {
public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue, boolean selectOnAppend, final boolean resumePlayback) {
if (!PermissionHelper.isPopupEnabled(context)) {
PermissionHelper.showPopupEnablementToast(context);
return;
@@ -145,17 +150,17 @@ public class NavigationHelper {
Toast.makeText(context, R.string.popup_playing_append, Toast.LENGTH_SHORT).show();
startService(context,
getPlayerEnqueueIntent(context, PopupVideoPlayer.class, queue, selectOnAppend));
getPlayerEnqueueIntent(context, PopupVideoPlayer.class, queue, selectOnAppend, resumePlayback));
}
public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue) {
enqueueOnBackgroundPlayer(context, queue, false);
public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) {
enqueueOnBackgroundPlayer(context, queue, false, resumePlayback);
}
public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue, boolean selectOnAppend) {
public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue, boolean selectOnAppend, final boolean resumePlayback) {
Toast.makeText(context, R.string.background_player_append, Toast.LENGTH_SHORT).show();
startService(context,
getPlayerEnqueueIntent(context, BackgroundPlayer.class, queue, selectOnAppend));
getPlayerEnqueueIntent(context, BackgroundPlayer.class, queue, selectOnAppend, resumePlayback));
}
public static void startService(@NonNull final Context context, @NonNull final Intent intent) {

View File

@@ -21,10 +21,9 @@ public class PermissionHelper {
public static final int DOWNLOAD_DIALOG_REQUEST_CODE = 778;
public static final int DOWNLOADS_REQUEST_CODE = 777;
public static boolean checkStoragePermissions(Activity activity, int requestCode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
if(!checkReadStoragePermissions(activity, requestCode)) return false;
if (!checkReadStoragePermissions(activity, requestCode)) return false;
}
return checkWriteStoragePermissions(activity, requestCode);
}
@@ -92,7 +91,7 @@ public class PermissionHelper {
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(i);
return false;
}else return true;
} else return true;
}
public static boolean isPopupEnabled(Context context) {

View File

@@ -38,7 +38,7 @@ public class SecondaryStreamHelper<T extends Stream> {
public static AudioStream getAudioStreamFor(@NonNull List<AudioStream> audioStreams, @NonNull VideoStream videoStream) {
switch (videoStream.getFormat()) {
case WEBM:
case MPEG_4:
case MPEG_4:// ¿is mpeg-4 DASH?
break;
default:
return null;

View File

@@ -0,0 +1,30 @@
package org.schabi.newpipe.util;
import android.util.SparseArray;
public abstract class SparseArrayUtils {
public static <T> void shiftItemsDown(SparseArray<T> sparseArray, int lower, int upper) {
for (int i = lower + 1; i <= upper; i++) {
final T o = sparseArray.get(i);
sparseArray.put(i - 1, o);
sparseArray.remove(i);
}
}
public static <T> void shiftItemsUp(SparseArray<T> sparseArray, int lower, int upper) {
for (int i = upper - 1; i >= lower; i--) {
final T o = sparseArray.get(i);
sparseArray.put(i + 1, o);
sparseArray.remove(i);
}
}
public static <T> int[] getKeys(SparseArray<T> sparseArray) {
final int[] result = new int[sparseArray.size()];
for (int i = 0; i < result.length; i++) {
result[i] = sparseArray.keyAt(i);
}
return result;
}
}

View File

@@ -0,0 +1,109 @@
package org.schabi.newpipe.util;
import android.content.Context;
import android.support.v4.app.Fragment;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import java.util.Collections;
public enum StreamDialogEntry {
//////////////////////////////////////
// enum values with DEFAULT actions //
//////////////////////////////////////
enqueue_on_background(R.string.enqueue_on_background, (fragment, item) ->
NavigationHelper.enqueueOnBackgroundPlayer(fragment.getContext(), new SinglePlayQueue(item), false)),
enqueue_on_popup(R.string.enqueue_on_popup, (fragment, item) ->
NavigationHelper.enqueueOnPopupPlayer(fragment.getContext(), new SinglePlayQueue(item), false)),
start_here_on_background(R.string.start_here_on_background, (fragment, item) ->
NavigationHelper.playOnBackgroundPlayer(fragment.getContext(), new SinglePlayQueue(item), true)),
start_here_on_popup(R.string.start_here_on_popup, (fragment, item) ->
NavigationHelper.playOnPopupPlayer(fragment.getContext(), new SinglePlayQueue(item), true)),
set_as_playlist_thumbnail(R.string.set_as_playlist_thumbnail, (fragment, item) -> {}), // has to be set manually
delete(R.string.delete, (fragment, item) -> {}), // has to be set manually
append_playlist(R.string.append_playlist, (fragment, item) -> {
if (fragment.getFragmentManager() != null) {
PlaylistAppendDialog.fromStreamInfoItems(Collections.singletonList(item))
.show(fragment.getFragmentManager(), "StreamDialogEntry@append_playlist");
}}),
share(R.string.share, (fragment, item) ->
ShareUtils.shareUrl(fragment.getContext(), item.getName(), item.getUrl()));
///////////////
// variables //
///////////////
public interface StreamDialogEntryAction {
void onClick(Fragment fragment, final StreamInfoItem infoItem);
}
private final int resource;
private final StreamDialogEntryAction defaultAction;
private StreamDialogEntryAction customAction;
private static StreamDialogEntry[] enabledEntries;
///////////////////////////////////////////////////////
// non-static methods to initialize and edit entries //
///////////////////////////////////////////////////////
StreamDialogEntry(final int resource, StreamDialogEntryAction defaultAction) {
this.resource = resource;
this.defaultAction = defaultAction;
this.customAction = null;
}
/**
* Can be used after {@link #setEnabledEntries(StreamDialogEntry...)} has been called
*/
public void setCustomAction(StreamDialogEntryAction action) {
this.customAction = action;
}
////////////////////////////////////////////////
// static methods that act on enabled entries //
////////////////////////////////////////////////
/**
* To be called before using {@link #setCustomAction(StreamDialogEntryAction)}
*/
public static void setEnabledEntries(StreamDialogEntry... entries) {
// cleanup from last time StreamDialogEntry was used
for (StreamDialogEntry streamDialogEntry : values()) {
streamDialogEntry.customAction = null;
}
enabledEntries = entries;
}
public static String[] getCommands(Context context) {
String[] commands = new String[enabledEntries.length];
for (int i = 0; i != enabledEntries.length; ++i) {
commands[i] = context.getResources().getString(enabledEntries[i].resource);
}
return commands;
}
public static void clickOn(int which, Fragment fragment, StreamInfoItem infoItem) {
if (enabledEntries[which].customAction == null) {
enabledEntries[which].defaultAction.onClick(fragment, infoItem);
} else {
enabledEntries[which].customAction.onClick(fragment, infoItem);
}
}
}

View File

@@ -0,0 +1,64 @@
package org.schabi.newpipe.views;
import android.content.Context;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Animation;
import android.view.animation.Transformation;
import android.widget.ProgressBar;
public final class AnimatedProgressBar extends ProgressBar {
@Nullable
private ProgressBarAnimation animation = null;
public AnimatedProgressBar(Context context) {
super(context);
}
public AnimatedProgressBar(Context context, AttributeSet attrs) {
super(context, attrs);
}
public AnimatedProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public synchronized void setProgressAnimated(int progress) {
cancelAnimation();
animation = new ProgressBarAnimation(this, getProgress(), progress);
startAnimation(animation);
}
private void cancelAnimation() {
if (animation != null) {
animation.cancel();
animation = null;
}
clearAnimation();
}
private static class ProgressBarAnimation extends Animation {
private final AnimatedProgressBar progressBar;
private final float from;
private final float to;
ProgressBarAnimation(AnimatedProgressBar progressBar, float from, float to) {
super();
this.progressBar = progressBar;
this.from = from;
this.to = to;
setDuration(500);
setInterpolator(new AccelerateDecelerateInterpolator());
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
super.applyTransformation(interpolatedTime, t);
float value = from + (to - from) * interpolatedTime;
progressBar.setProgress((int) value);
}
}
}

View File

@@ -1,185 +1,191 @@
package us.shandian.giga.get;
import android.support.annotation.NonNull;
import android.util.Log;
import java.io.File;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.nio.channels.ClosedByInterruptException;
import us.shandian.giga.util.Utility;
import static org.schabi.newpipe.BuildConfig.DEBUG;
public class DownloadInitializer extends Thread {
private final static String TAG = "DownloadInitializer";
final static int mId = 0;
private DownloadMission mMission;
private HttpURLConnection mConn;
DownloadInitializer(@NonNull DownloadMission mission) {
mMission = mission;
mConn = null;
}
@Override
public void run() {
if (mMission.current > 0) mMission.resetState();
int retryCount = 0;
while (true) {
try {
mMission.currentThreadCount = mMission.threadCount;
mConn = mMission.openConnection(mId, -1, -1);
mMission.establishConnection(mId, mConn);
if (!mMission.running || Thread.interrupted()) return;
mMission.length = Utility.getContentLength(mConn);
if (mMission.length == 0) {
mMission.notifyError(DownloadMission.ERROR_HTTP_NO_CONTENT, null);
return;
}
// check for dynamic generated content
if (mMission.length == -1 && mConn.getResponseCode() == 200) {
mMission.blocks = 0;
mMission.length = 0;
mMission.fallback = true;
mMission.unknownLength = true;
mMission.currentThreadCount = 1;
if (DEBUG) {
Log.d(TAG, "falling back (unknown length)");
}
} else {
// Open again
mConn = mMission.openConnection(mId, mMission.length - 10, mMission.length);
mMission.establishConnection(mId, mConn);
if (!mMission.running || Thread.interrupted()) return;
synchronized (mMission.blockState) {
if (mConn.getResponseCode() == 206) {
if (mMission.currentThreadCount > 1) {
mMission.blocks = mMission.length / DownloadMission.BLOCK_SIZE;
if (mMission.currentThreadCount > mMission.blocks) {
mMission.currentThreadCount = (int) mMission.blocks;
}
if (mMission.currentThreadCount <= 0) {
mMission.currentThreadCount = 1;
}
if (mMission.blocks * DownloadMission.BLOCK_SIZE < mMission.length) {
mMission.blocks++;
}
} else {
// if one thread is solicited don't calculate blocks, is useless
mMission.blocks = 1;
mMission.fallback = true;
mMission.unknownLength = false;
}
if (DEBUG) {
Log.d(TAG, "http response code = " + mConn.getResponseCode());
}
} else {
// Fallback to single thread
mMission.blocks = 0;
mMission.fallback = true;
mMission.unknownLength = false;
mMission.currentThreadCount = 1;
if (DEBUG) {
Log.d(TAG, "falling back due http response code = " + mConn.getResponseCode());
}
}
for (long i = 0; i < mMission.currentThreadCount; i++) {
mMission.threadBlockPositions.add(i);
mMission.threadBytePositions.add(0L);
}
}
if (!mMission.running || Thread.interrupted()) return;
}
File file;
if (mMission.current == 0) {
file = new File(mMission.location);
if (!Utility.mkdir(file, true)) {
mMission.notifyError(DownloadMission.ERROR_PATH_CREATION, null);
return;
}
file = new File(file, mMission.name);
// if the name is used by another process, delete it
if (file.exists() && !file.isFile() && !file.delete()) {
mMission.notifyError(DownloadMission.ERROR_FILE_CREATION, null);
return;
}
if (!file.exists() && !file.createNewFile()) {
mMission.notifyError(DownloadMission.ERROR_FILE_CREATION, null);
return;
}
} else {
file = new File(mMission.location, mMission.name);
}
RandomAccessFile af = new RandomAccessFile(file, "rw");
af.setLength(mMission.offsets[mMission.current] + mMission.length);
af.seek(mMission.offsets[mMission.current]);
af.close();
if (!mMission.running || Thread.interrupted()) return;
mMission.running = false;
break;
} catch (InterruptedIOException | ClosedByInterruptException e) {
return;
} catch (Exception e) {
if (!mMission.running) return;
if (e instanceof IOException && e.getMessage().contains("Permission denied")) {
mMission.notifyError(DownloadMission.ERROR_PERMISSION_DENIED, e);
return;
}
if (retryCount++ > mMission.maxRetry) {
Log.e(TAG, "initializer failed", e);
mMission.notifyError(e);
return;
}
Log.e(TAG, "initializer failed, retrying", e);
}
}
// hide marquee in the progress bar
mMission.done++;
mMission.start();
}
@Override
public void interrupt() {
super.interrupt();
if (mConn != null) {
try {
mConn.disconnect();
} catch (Exception e) {
// nothing to do
}
}
}
}
package us.shandian.giga.get;
import android.support.annotation.NonNull;
import android.util.Log;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.HttpURLConnection;
import java.nio.channels.ClosedByInterruptException;
import us.shandian.giga.util.Utility;
import static org.schabi.newpipe.BuildConfig.DEBUG;
public class DownloadInitializer extends Thread {
private final static String TAG = "DownloadInitializer";
final static int mId = 0;
private final static int RESERVE_SPACE_DEFAULT = 5 * 1024 * 1024;// 5 MiB
private final static int RESERVE_SPACE_MAXIMUM = 150 * 1024 * 1024;// 150 MiB
private DownloadMission mMission;
private HttpURLConnection mConn;
DownloadInitializer(@NonNull DownloadMission mission) {
mMission = mission;
mConn = null;
}
private static void safeClose(HttpURLConnection con) {
try {
con.getInputStream().close();
} catch (Exception e) {
// nothing to do
}
}
@Override
public void run() {
if (mMission.current > 0) mMission.resetState(false, true, DownloadMission.ERROR_NOTHING);
int retryCount = 0;
int httpCode = 204;
while (true) {
try {
if (mMission.blocks == null && mMission.current == 0) {
// calculate the whole size of the mission
long finalLength = 0;
long lowestSize = Long.MAX_VALUE;
for (int i = 0; i < mMission.urls.length && mMission.running; i++) {
mConn = mMission.openConnection(mMission.urls[i], mId, -1, -1);
mMission.establishConnection(mId, mConn);
safeClose(mConn);
if (Thread.interrupted()) return;
long length = Utility.getContentLength(mConn);
if (i == 0) {
httpCode = mConn.getResponseCode();
mMission.length = length;
}
if (length > 0) finalLength += length;
if (length < lowestSize) lowestSize = length;
}
mMission.nearLength = finalLength;
// reserve space at the start of the file
if (mMission.psAlgorithm != null && mMission.psAlgorithm.reserveSpace) {
if (lowestSize < 1) {
// the length is unknown use the default size
mMission.offsets[0] = RESERVE_SPACE_DEFAULT;
} else {
// use the smallest resource size to download, otherwise, use the maximum
mMission.offsets[0] = lowestSize < RESERVE_SPACE_MAXIMUM ? lowestSize : RESERVE_SPACE_MAXIMUM;
}
}
} else {
// ask for the current resource length
mConn = mMission.openConnection(mId, -1, -1);
mMission.establishConnection(mId, mConn);
safeClose(mConn);
if (!mMission.running || Thread.interrupted()) return;
httpCode = mConn.getResponseCode();
mMission.length = Utility.getContentLength(mConn);
}
if (mMission.length == 0 || httpCode == 204) {
mMission.notifyError(DownloadMission.ERROR_HTTP_NO_CONTENT, null);
return;
}
// check for dynamic generated content
if (mMission.length == -1 && mConn.getResponseCode() == 200) {
mMission.blocks = new int[0];
mMission.length = 0;
mMission.unknownLength = true;
if (DEBUG) {
Log.d(TAG, "falling back (unknown length)");
}
} else {
// Open again
mConn = mMission.openConnection(mId, mMission.length - 10, mMission.length);
mMission.establishConnection(mId, mConn);
safeClose(mConn);
if (!mMission.running || Thread.interrupted()) return;
synchronized (mMission.LOCK) {
if (mConn.getResponseCode() == 206) {
if (mMission.threadCount > 1) {
int count = (int) (mMission.length / DownloadMission.BLOCK_SIZE);
if ((count * DownloadMission.BLOCK_SIZE) < mMission.length) count++;
mMission.blocks = new int[count];
} else {
// if one thread is required don't calculate blocks, is useless
mMission.blocks = new int[0];
mMission.unknownLength = false;
}
if (DEBUG) {
Log.d(TAG, "http response code = " + mConn.getResponseCode());
}
} else {
// Fallback to single thread
mMission.blocks = new int[0];
mMission.unknownLength = false;
if (DEBUG) {
Log.d(TAG, "falling back due http response code = " + mConn.getResponseCode());
}
}
}
if (!mMission.running || Thread.interrupted()) return;
}
SharpStream fs = mMission.storage.getStream();
fs.setLength(mMission.offsets[mMission.current] + mMission.length);
fs.seek(mMission.offsets[mMission.current]);
fs.close();
if (!mMission.running || Thread.interrupted()) return;
mMission.running = false;
break;
} catch (InterruptedIOException | ClosedByInterruptException e) {
return;
} catch (Exception e) {
if (!mMission.running) return;
if (e instanceof IOException && e.getMessage().contains("Permission denied")) {
mMission.notifyError(DownloadMission.ERROR_PERMISSION_DENIED, e);
return;
}
if (retryCount++ > mMission.maxRetry) {
Log.e(TAG, "initializer failed", e);
mMission.notifyError(e);
return;
}
Log.e(TAG, "initializer failed, retrying", e);
}
}
mMission.start();
}
@Override
public void interrupt() {
super.interrupt();
if (mConn != null) {
try {
mConn.disconnect();
} catch (Exception e) {
// nothing to do
}
}
}
}

View File

@@ -4,19 +4,22 @@ import android.os.Handler;
import android.os.Message;
import android.util.Log;
import org.schabi.newpipe.Downloader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Serializable;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import javax.annotation.Nullable;
import javax.net.ssl.SSLException;
import us.shandian.giga.io.StoredFileHelper;
import us.shandian.giga.postprocessing.Postprocessing;
import us.shandian.giga.service.DownloadManagerService;
import us.shandian.giga.util.Utility;
@@ -24,10 +27,13 @@ import us.shandian.giga.util.Utility;
import static org.schabi.newpipe.BuildConfig.DEBUG;
public class DownloadMission extends Mission {
private static final long serialVersionUID = 3L;// last bump: 8 november 2018
private static final long serialVersionUID = 5L;// last bump: 30 june 2019
static final int BUFFER_SIZE = 64 * 1024;
final static int BLOCK_SIZE = 512 * 1024;
static final int BLOCK_SIZE = 512 * 1024;
@SuppressWarnings("SpellCheckingInspection")
private static final String INSUFFICIENT_STORAGE = "ENOSPC";
private static final String TAG = "DownloadMission";
@@ -40,6 +46,11 @@ public class DownloadMission extends Mission {
public static final int ERROR_UNKNOWN_HOST = 1005;
public static final int ERROR_CONNECT_HOST = 1006;
public static final int ERROR_POSTPROCESSING = 1007;
public static final int ERROR_POSTPROCESSING_STOPPED = 1008;
public static final int ERROR_POSTPROCESSING_HOLD = 1009;
public static final int ERROR_INSUFFICIENT_STORAGE = 1010;
public static final int ERROR_PROGRESS_LOST = 1011;
public static final int ERROR_TIMEOUT = 1012;
public static final int ERROR_HTTP_NO_CONTENT = 204;
public static final int ERROR_HTTP_UNSUPPORTED_RANGE = 206;
@@ -48,11 +59,6 @@ public class DownloadMission extends Mission {
*/
public String[] urls;
/**
* Number of blocks the size of {@link DownloadMission#BLOCK_SIZE}
*/
long blocks = -1;
/**
* Number of bytes downloaded
*/
@@ -68,171 +74,128 @@ public class DownloadMission extends Mission {
*/
public long[] offsets;
/**
* The post-processing algorithm arguments
*/
public String[] postprocessingArgs;
/**
* The post-processing algorithm name
*/
public String postprocessingName;
/**
* Indicates if the post-processing state:
* 0: ready
* 1: running
* 2: completed
* 3: hold
*/
public int postprocessingState;
public volatile int psState;
/**
* Indicate if the post-processing algorithm works on the same file
* the post-processing algorithm instance
*/
public boolean postprocessingThis;
public Postprocessing psAlgorithm;
/**
* The current resource to download {@code urls[current]}
* The current resource to download, {@code urls[current]} and {@code offsets[current]}
*/
public int current;
/**
* Metadata where the mission state is saved
*/
public File metadata;
public transient File metadata;
/**
* maximum attempts
*/
public int maxRetry;
public transient int maxRetry;
/**
* Approximated final length, this represent the sum of all resources sizes
*/
public long nearLength;
/**
* Download blocks, the size is multiple of {@link DownloadMission#BLOCK_SIZE}.
* Every entry (block) in this array holds an offset, used to resume the download.
* An block offset can be -1 if the block was downloaded successfully.
*/
int[] blocks;
/**
* Download/File resume offset in fallback mode (if applicable) {@link DownloadRunnableFallback}
*/
long fallbackResumeOffset;
/**
* Maximum of download threads running, chosen by the user
*/
public int threadCount = 3;
boolean fallback;
private int finishCount;
private transient int finishCount;
public transient boolean running;
public transient boolean enqueued = true;
public boolean enqueued;
public int errCode = ERROR_NOTHING;
public Exception errObject = null;
public transient Exception errObject = null;
public transient boolean recovered;
public transient Handler mHandler;
private transient boolean mWritingToFile;
private transient boolean[] blockAcquired;
@SuppressWarnings("UseSparseArrays")// LongSparseArray is not serializable
final HashMap<Long, Boolean> blockState = new HashMap<>();
final List<Long> threadBlockPositions = new ArrayList<>();
final List<Long> threadBytePositions = new ArrayList<>();
final Object LOCK = new Lock();
private transient boolean deleted;
int currentThreadCount;
private transient Thread[] threads = new Thread[0];
public transient volatile Thread[] threads = new Thread[0];
private transient Thread init = null;
protected DownloadMission() {
}
public DownloadMission(String url, String name, String location, char kind) {
this(new String[]{url}, name, location, kind, null, null);
}
public DownloadMission(String[] urls, String name, String location, char kind, String postprocessingName, String[] postprocessingArgs) {
if (name == null) throw new NullPointerException("name is null");
if (name.isEmpty()) throw new IllegalArgumentException("name is empty");
public DownloadMission(String[] urls, StoredFileHelper storage, char kind, Postprocessing psInstance) {
if (urls == null) throw new NullPointerException("urls is null");
if (urls.length < 1) throw new IllegalArgumentException("urls is empty");
if (location == null) throw new NullPointerException("location is null");
if (location.isEmpty()) throw new IllegalArgumentException("location is empty");
this.urls = urls;
this.name = name;
this.location = location;
this.kind = kind;
this.offsets = new long[urls.length];
this.enqueued = true;
this.maxRetry = 3;
this.storage = storage;
this.psAlgorithm = psInstance;
if (postprocessingName != null) {
Postprocessing algorithm = Postprocessing.getAlgorithm(postprocessingName, null);
this.postprocessingThis = algorithm.worksOnSameFile;
this.offsets[0] = algorithm.recommendedReserve;
this.postprocessingName = postprocessingName;
this.postprocessingArgs = postprocessingArgs;
} else {
if (DEBUG && urls.length > 1) {
Log.w(TAG, "mission created with multiple urls ¿missing post-processing algorithm?");
if (DEBUG && psInstance == null && urls.length > 1) {
Log.w(TAG, "mission created with multiple urls ¿missing post-processing algorithm?");
}
}
/**
* Acquire a block
*
* @return the block or {@code null} if no more blocks left
*/
@Nullable
Block acquireBlock() {
synchronized (LOCK) {
for (int i = 0; i < blockAcquired.length; i++) {
if (!blockAcquired[i] && blocks[i] >= 0) {
Block block = new Block();
block.position = i;
block.done = blocks[i];
blockAcquired[i] = true;
return block;
}
}
}
return null;
}
private void checkBlock(long block) {
if (block < 0 || block >= blocks) {
throw new IllegalArgumentException("illegal block identifier");
/**
* Release an block
*
* @param position the index of the block
* @param done amount of bytes downloaded
*/
void releaseBlock(int position, int done) {
synchronized (LOCK) {
blockAcquired[position] = false;
blocks[position] = done;
}
}
/**
* Check if a block is reserved
*
* @param block the block identifier
* @return true if the block is reserved and false if otherwise
*/
boolean isBlockPreserved(long block) {
checkBlock(block);
return blockState.containsKey(block) ? blockState.get(block) : false;
}
void preserveBlock(long block) {
checkBlock(block);
synchronized (blockState) {
blockState.put(block, true);
}
}
/**
* Set the block of the file
*
* @param threadId the identifier of the thread
* @param position the block of the thread
*/
void setBlockPosition(int threadId, long position) {
threadBlockPositions.set(threadId, position);
}
/**
* Get the block of a file
*
* @param threadId the identifier of the thread
* @return the block for the thread
*/
long getBlockPosition(int threadId) {
return threadBlockPositions.get(threadId);
}
/**
* Save the position of the desired thread
*
* @param threadId the identifier of the thread
* @param position the relative position in bytes or zero
*/
void setThreadBytePosition(int threadId, long position) {
threadBytePositions.set(threadId, position);
}
/**
* Get position inside of the thread, where thread will be resumed
*
* @param threadId the identifier of the thread
* @return the relative position in bytes or zero
*/
long getThreadBytePosition(int threadId) {
return threadBytePositions.get(threadId);
}
/**
* Open connection
*
@@ -243,9 +206,18 @@ public class DownloadMission extends Mission {
* @throws IOException if an I/O exception occurs.
*/
HttpURLConnection openConnection(int threadId, long rangeStart, long rangeEnd) throws IOException {
URL url = new URL(urls[current]);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
return openConnection(urls[current], threadId, rangeStart, rangeEnd);
}
HttpURLConnection openConnection(String url, int threadId, long rangeStart, long rangeEnd) throws IOException {
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setInstanceFollowRedirects(true);
conn.setRequestProperty("User-Agent", Downloader.USER_AGENT);
conn.setRequestProperty("Accept", "*/*");
// BUG workaround: switching between networks can freeze the download forever
conn.setConnectTimeout(30000);
conn.setReadTimeout(10000);
if (rangeStart >= 0) {
String req = "bytes=" + rangeStart + "-";
@@ -337,20 +309,48 @@ public class DownloadMission extends Mission {
notifyError(ERROR_CONNECT_HOST, null);
} else if (err instanceof UnknownHostException) {
notifyError(ERROR_UNKNOWN_HOST, null);
} else if (err instanceof SocketTimeoutException) {
notifyError(ERROR_TIMEOUT, null);
} else {
notifyError(ERROR_UNKNOWN_EXCEPTION, err);
}
}
synchronized void notifyError(int code, Exception err) {
public synchronized void notifyError(int code, Exception err) {
Log.e(TAG, "notifyError() code = " + code, err);
if (err instanceof IOException) {
if (!storage.canWrite() || err.getMessage().contains("Permission denied")) {
code = ERROR_PERMISSION_DENIED;
err = null;
} else if (err.getMessage().contains(INSUFFICIENT_STORAGE)) {
code = ERROR_INSUFFICIENT_STORAGE;
err = null;
}
}
errCode = code;
errObject = err;
pause();
switch (code) {
case ERROR_SSL_EXCEPTION:
case ERROR_UNKNOWN_HOST:
case ERROR_CONNECT_HOST:
case ERROR_TIMEOUT:
// do not change the queue flag for network errors, can be
// recovered silently without the user interaction
break;
default:
// also checks for server errors
if (code < 500 || code > 599) enqueued = false;
}
notify(DownloadManagerService.MESSAGE_ERROR);
if (running) {
running = false;
recovered = true;
if (threads != null) selfPause();
}
}
synchronized void notifyFinished() {
@@ -358,11 +358,11 @@ public class DownloadMission extends Mission {
finishCount++;
if (finishCount == currentThreadCount) {
if (blocks.length < 1 || threads == null || finishCount == threads.length) {
if (errCode != ERROR_NOTHING) return;
if (DEBUG) {
Log.d(TAG, "onFinish" + (current + 1) + "/" + urls.length);
Log.d(TAG, "onFinish: " + (current + 1) + "/" + urls.length);
}
if ((current + 1) < urls.length) {
@@ -378,6 +378,7 @@ public class DownloadMission extends Mission {
if (!doPostprocessing()) return;
enqueued = false;
running = false;
deleteThisFromFile();
@@ -386,25 +387,23 @@ public class DownloadMission extends Mission {
}
private void notifyPostProcessing(int state) {
if (DEBUG) {
String action;
switch (state) {
case 1:
action = "Running";
break;
case 2:
action = "Completed";
break;
default:
action = "Failed";
}
Log.d(TAG, action + " postprocessing on " + location + File.separator + name);
String action;
switch (state) {
case 1:
action = "Running";
break;
case 2:
action = "Completed";
break;
default:
action = "Failed";
}
synchronized (blockState) {
Log.d(TAG, action + " postprocessing on " + storage.getName());
synchronized (LOCK) {
// don't return without fully write the current state
postprocessingState = state;
psState = state;
Utility.writeToFile(metadata, DownloadMission.this);
}
}
@@ -420,52 +419,52 @@ public class DownloadMission extends Mission {
if (threads != null)
for (Thread thread : threads) joinForThread(thread);
enqueued = false;
running = true;
errCode = ERROR_NOTHING;
if (current >= urls.length && postprocessingName != null) {
runAsync(1, () -> {
if (doPostprocessing()) {
running = false;
deleteThisFromFile();
notify(DownloadManagerService.MESSAGE_FINISHED);
}
});
if (current >= urls.length) {
threads = null;
runAsync(1, this::notifyFinished);
return;
}
if (blocks < 0) {
if (blocks == null) {
initializer();
return;
}
init = null;
finishCount = 0;
blockAcquired = new boolean[blocks.length];
if (threads == null || threads.length < 1) {
threads = new Thread[currentThreadCount];
}
if (fallback) {
if (blocks.length < 1) {
if (unknownLength) {
done = 0;
length = 0;
}
threads[0] = runAsync(1, new DownloadRunnableFallback(this));
threads = new Thread[]{runAsync(1, new DownloadRunnableFallback(this))};
} else {
for (int i = 0; i < currentThreadCount; i++) {
int remainingBlocks = 0;
for (int block : blocks) if (block >= 0) remainingBlocks++;
if (remainingBlocks < 1) {
runAsync(1, this::notifyFinished);
return;
}
threads = new Thread[Math.min(threadCount, remainingBlocks)];
for (int i = 0; i < threads.length; i++) {
threads[i] = runAsync(i + 1, new DownloadRunnable(this, i));
}
}
}
/**
* Pause the mission, does not affect the blocks that are being downloaded.
* Pause the mission
*/
public synchronized void pause() {
public void pause() {
if (!running) return;
if (isPsRunning()) {
@@ -477,71 +476,84 @@ public class DownloadMission extends Mission {
running = false;
recovered = true;
enqueued = false;
if (init != null && Thread.currentThread() != init && init.isAlive()) {
if (init != null && init.isAlive()) {
// NOTE: if start() method is running ¡will no have effect!
init.interrupt();
synchronized (blockState) {
resetState();
synchronized (LOCK) {
resetState(false, true, ERROR_NOTHING);
}
return;
}
if (DEBUG && blocks == 0) {
if (DEBUG && unknownLength) {
Log.w(TAG, "pausing a download that can not be resumed (range requests not allowed by the server).");
}
if (threads == null || Thread.currentThread().isInterrupted()) {
// check if the calling thread (alias UI thread) is interrupted
if (Thread.currentThread().isInterrupted()) {
writeThisToFile();
return;
}
// wait for all threads are suspended before save the state
runAsync(-1, () -> {
try {
for (Thread thread : threads) {
if (thread.isAlive()) {
thread.interrupt();
thread.join(5000);
}
if (threads != null) runAsync(-1, this::selfPause);
}
private void selfPause() {
try {
for (Thread thread : threads) {
if (thread.isAlive()) {
thread.interrupt();
thread.join(5000);
}
} catch (Exception e) {
// nothing to do
} finally {
writeThisToFile();
}
});
} catch (Exception e) {
// nothing to do
} finally {
writeThisToFile();
}
}
/**
* Removes the file and the meta file
* Removes the downloaded file and the meta file
*/
@Override
public boolean delete() {
deleted = true;
if (psAlgorithm != null) psAlgorithm.cleanupTemporalDir();
boolean res = deleteThisFromFile();
if (!super.delete()) res = false;
if (!super.delete()) return false;
return res;
}
void resetState() {
done = 0;
blocks = -1;
errCode = ERROR_NOTHING;
fallback = false;
unknownLength = false;
finishCount = 0;
threadBlockPositions.clear();
threadBytePositions.clear();
blockState.clear();
threads = new Thread[0];
Utility.writeToFile(metadata, DownloadMission.this);
/**
* Resets the mission state
*
* @param rollback {@code true} true to forget all progress, otherwise, {@code false}
* @param persistChanges {@code true} to commit changes to the metadata file, otherwise, {@code false}
*/
public void resetState(boolean rollback, boolean persistChanges, int errorCode) {
done = 0;
errCode = errorCode;
errObject = null;
unknownLength = false;
threads = null;
fallbackResumeOffset = 0;
blocks = null;
blockAcquired = null;
if (rollback) current = 0;
if (persistChanges)
Utility.writeToFile(metadata, DownloadMission.this);
}
private void initializer() {
init = runAsync(DownloadInitializer.mId, new DownloadInitializer(this));
}
/**
@@ -549,7 +561,7 @@ public class DownloadMission extends Mission {
* if no thread is already running.
*/
private void writeThisToFile() {
synchronized (blockState) {
synchronized (LOCK) {
if (deleted) return;
Utility.writeToFile(metadata, DownloadMission.this);
}
@@ -562,7 +574,7 @@ public class DownloadMission extends Mission {
* @return true, otherwise, false
*/
public boolean isFinished() {
return current >= urls.length && (postprocessingName == null || postprocessingState == 2);
return current >= urls.length && (psAlgorithm == null || psState == 2);
}
/**
@@ -571,7 +583,13 @@ public class DownloadMission extends Mission {
* @return {@code true} if this mission is unrecoverable
*/
public boolean isPsFailed() {
return postprocessingName != null && errCode == DownloadMission.ERROR_POSTPROCESSING && postprocessingThis;
switch (errCode) {
case ERROR_POSTPROCESSING:
case ERROR_POSTPROCESSING_STOPPED:
return psAlgorithm.worksOnSameFile;
}
return false;
}
/**
@@ -580,12 +598,26 @@ public class DownloadMission extends Mission {
* @return true, otherwise, false
*/
public boolean isPsRunning() {
return postprocessingName != null && postprocessingState == 1;
return psAlgorithm != null && (psState == 1 || psState == 3);
}
/**
* Indicated if the mission is ready
*
* @return true, otherwise, false
*/
public boolean isInitialized() {
return blocks != null; // DownloadMissionInitializer was executed
}
/**
* Gets the approximated final length of the file
*
* @return the length in bytes
*/
public long getLength() {
long calculated;
if (postprocessingState == 1) {
if (psState == 1 || psState == 3) {
calculated = length;
} else {
calculated = offsets[current < offsets.length ? current : (offsets.length - 1)] + length;
@@ -596,30 +628,67 @@ public class DownloadMission extends Mission {
return calculated > nearLength ? calculated : nearLength;
}
/**
* set this mission state on the queue
*
* @param queue true to add to the queue, otherwise, false
*/
public void setEnqueued(boolean queue) {
enqueued = queue;
runAsync(-2, this::writeThisToFile);
}
/**
* Attempts to continue a blocked post-processing
*
* @param recover {@code true} to retry, otherwise, {@code false} to cancel
*/
public void psContinue(boolean recover) {
psState = 1;
errCode = recover ? ERROR_NOTHING : ERROR_POSTPROCESSING;
threads[0].interrupt();
}
/**
* Indicates whatever the backed storage is invalid
*
* @return {@code true}, if storage is invalid and cannot be used
*/
public boolean hasInvalidStorage() {
return errCode == ERROR_PROGRESS_LOST || storage == null || storage.isInvalid() || !storage.existsAsFile();
}
/**
* Indicates whatever is possible to start the mission
*
* @return {@code true} is this mission its "healthy", otherwise, {@code false}
*/
public boolean isCorrupt() {
return (isPsFailed() || errCode == ERROR_POSTPROCESSING_HOLD) || isFinished() || hasInvalidStorage();
}
private boolean doPostprocessing() {
if (postprocessingName == null || postprocessingState == 2) return true;
if (psAlgorithm == null || psState == 2) return true;
errObject = null;
notifyPostProcessing(1);
notifyProgress(0);
Thread.currentThread().setName("[" + TAG + "] post-processing = " + postprocessingName + " filename = " + name);
if (DEBUG)
Thread.currentThread().setName("[" + TAG + "] ps = " +
psAlgorithm.getClass().getSimpleName() +
" filename = " + storage.getName()
);
threads = new Thread[]{Thread.currentThread()};
Exception exception = null;
try {
Postprocessing
.getAlgorithm(postprocessingName, this)
.run();
psAlgorithm.run(this);
} catch (Exception err) {
StringBuilder args = new StringBuilder(" ");
if (postprocessingArgs != null) {
for (String arg : postprocessingArgs) {
args.append(", ");
args.append(arg);
}
args.delete(0, 1);
}
Log.e(TAG, String.format("Post-processing failed. algorithm = %s args = [%s]", postprocessingName, args), err);
Log.e(TAG, "Post-processing failed. " + psAlgorithm.toString(), err);
if (errCode == ERROR_NOTHING) errCode = ERROR_POSTPROCESSING;
@@ -639,7 +708,7 @@ public class DownloadMission extends Mission {
}
private boolean deleteThisFromFile() {
synchronized (blockState) {
synchronized (LOCK) {
return metadata.delete();
}
}
@@ -669,7 +738,7 @@ public class DownloadMission extends Mission {
// >=1: any download thread
if (DEBUG) {
who.setName(String.format("%s[%s] %s", TAG, id, name));
who.setName(String.format("%s[%s] %s", TAG, id, storage.getName()));
}
who.start();
@@ -701,7 +770,7 @@ public class DownloadMission extends Mission {
static class HttpError extends Exception {
int statusCode;
final int statusCode;
HttpError(int statusCode) {
this.statusCode = statusCode;
@@ -709,7 +778,16 @@ public class DownloadMission extends Mission {
@Override
public String getMessage() {
return "HTTP " + String.valueOf(statusCode);
return "HTTP " + statusCode;
}
}
static class Block {
int position;
int done;
}
private static class Lock implements Serializable {
// java.lang.Object cannot be used because is not serializable
}
}

View File

@@ -2,14 +2,18 @@ package us.shandian.giga.get;
import android.util.Log;
import java.io.FileNotFoundException;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.nio.channels.ClosedByInterruptException;
import us.shandian.giga.get.DownloadMission.Block;
import static org.schabi.newpipe.BuildConfig.DEBUG;
/**
* Runnable to download blocks of a file until the file is completely downloaded,
* an error occurs or the process is stopped.
@@ -26,118 +30,105 @@ public class DownloadRunnable extends Thread {
if (mission == null) throw new NullPointerException("mission is null");
mMission = mission;
mId = id;
mConn = null;
}
private void releaseBlock(Block block, long remain) {
// set the block offset to -1 if it is completed
mMission.releaseBlock(block.position, remain < 0 ? -1 : block.done);
}
@Override
public void run() {
boolean retry = mMission.recovered;
long blockPosition = mMission.getBlockPosition(mId);
boolean retry = false;
Block block = null;
int retryCount = 0;
if (DEBUG) {
Log.d(TAG, mId + ":default pos " + blockPosition);
Log.d(TAG, mId + ":recovered: " + mMission.recovered);
}
RandomAccessFile f;
InputStream is = null;
SharpStream f;
try {
f = new RandomAccessFile(mMission.getDownloadedFile(), "rw");
} catch (FileNotFoundException e) {
f = mMission.storage.getStream();
} catch (IOException e) {
mMission.notifyError(e);// this never should happen
return;
}
while (mMission.running && mMission.errCode == DownloadMission.ERROR_NOTHING && blockPosition < mMission.blocks) {
if (DEBUG && retry) {
Log.d(TAG, mId + ":retry is true. Resuming at " + blockPosition);
while (mMission.running && mMission.errCode == DownloadMission.ERROR_NOTHING) {
if (!retry) {
block = mMission.acquireBlock();
}
// Wait for an unblocked position
while (!retry && blockPosition < mMission.blocks && mMission.isBlockPreserved(blockPosition)) {
if (DEBUG) {
Log.d(TAG, mId + ":position " + blockPosition + " preserved, passing");
}
blockPosition++;
}
retry = false;
if (blockPosition >= mMission.blocks) {
if (block == null) {
if (DEBUG) Log.d(TAG, mId + ":no more blocks left, exiting");
break;
}
if (DEBUG) {
Log.d(TAG, mId + ":preserving position " + blockPosition);
if (retry)
Log.d(TAG, mId + ":retry block at position=" + block.position + " from the start");
else
Log.d(TAG, mId + ":acquired block at position=" + block.position + " done=" + block.done);
}
mMission.preserveBlock(blockPosition);
mMission.setBlockPosition(mId, blockPosition);
long start = blockPosition * DownloadMission.BLOCK_SIZE;
long start = block.position * DownloadMission.BLOCK_SIZE;
long end = start + DownloadMission.BLOCK_SIZE - 1;
long offset = mMission.getThreadBytePosition(mId);
start += offset;
start += block.done;
if (end >= mMission.length) {
end = mMission.length - 1;
}
long total = 0;
try {
mConn = mMission.openConnection(mId, start, end);
mMission.establishConnection(mId, mConn);
// check if the download can be resumed
if (mConn.getResponseCode() == 416 && offset > 0) {
retryCount--;
if (mConn.getResponseCode() == 416) {
if (block.done > 0) {
// try again from the start (of the block)
block.done = 0;
retry = true;
mConn.disconnect();
continue;
}
throw new DownloadMission.HttpError(416);
}
retry = false;
// The server may be ignoring the range request
if (mConn.getResponseCode() != 206) {
mMission.notifyError(new DownloadMission.HttpError(mConn.getResponseCode()));
if (DEBUG) {
Log.e(TAG, mId + ":Unsupported " + mConn.getResponseCode());
}
mMission.notifyError(new DownloadMission.HttpError(mConn.getResponseCode()));
break;
}
f.seek(mMission.offsets[mMission.current] + start);
is = mConn.getInputStream();
try (InputStream is = mConn.getInputStream()) {
byte[] buf = new byte[DownloadMission.BUFFER_SIZE];
int len;
byte[] buf = new byte[DownloadMission.BUFFER_SIZE];
int len;
while (start < end && mMission.running && (len = is.read(buf, 0, buf.length)) != -1) {
f.write(buf, 0, len);
start += len;
total += len;
mMission.notifyProgress(len);
while (start < end && mMission.running && (len = is.read(buf, 0, buf.length)) != -1) {
f.write(buf, 0, len);
start += len;
block.done += len;
mMission.notifyProgress(len);
}
}
if (DEBUG && mMission.running) {
Log.d(TAG, mId + ":position " + blockPosition + " finished, " + total + " bytes downloaded");
Log.d(TAG, mId + ":position " + block.position + " stopped " + start + "/" + end);
}
if (mMission.running)
mMission.setThreadBytePosition(mId, 0L);// clear byte position for next block
else
mMission.setThreadBytePosition(mId, total);// download paused, save progress for this block
} catch (Exception e) {
mMission.setThreadBytePosition(mId, total);
if (!mMission.running || e instanceof ClosedByInterruptException) break;
if (retryCount++ >= mMission.maxRetry) {
@@ -145,20 +136,12 @@ public class DownloadRunnable extends Thread {
break;
}
if (DEBUG) {
Log.d(TAG, mId + ":position " + blockPosition + " retrying due exception", e);
}
retry = true;
} finally {
if (!retry) releaseBlock(block, end - start);
}
}
try {
if (is != null) is.close();
} catch (Exception err) {
// nothing to do
}
try {
f.close();
} catch (Exception err) {

View File

@@ -1,16 +1,15 @@
package us.shandian.giga.get;
import android.annotation.SuppressLint;
import android.support.annotation.NonNull;
import android.util.Log;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.nio.channels.ClosedByInterruptException;
import us.shandian.giga.util.Utility;
import static org.schabi.newpipe.BuildConfig.DEBUG;
@@ -19,21 +18,17 @@ import static org.schabi.newpipe.BuildConfig.DEBUG;
* Single-threaded fallback mode
*/
public class DownloadRunnableFallback extends Thread {
private static final String TAG = "DownloadRunnableFallback";
private static final String TAG = "DownloadRunnableFallbac";
private final DownloadMission mMission;
private final int mId = 1;
private int mRetryCount = 0;
private InputStream mIs;
private RandomAccessFile mF;
private SharpStream mF;
private HttpURLConnection mConn;
DownloadRunnableFallback(@NonNull DownloadMission mission) {
mMission = mission;
mIs = null;
mF = null;
mConn = null;
}
private void dispose() {
@@ -43,30 +38,34 @@ public class DownloadRunnableFallback extends Thread {
// nothing to do
}
try {
if (mF != null) mF.close();
} catch (IOException e) {
// ¿ejected media storage? ¿file deleted? ¿storage ran out of space?
if (mF != null) mF.close();
}
private long loadPosition() {
synchronized (mMission.LOCK) {
return mMission.fallbackResumeOffset;
}
}
private void savePosition(long position) {
synchronized (mMission.LOCK) {
mMission.fallbackResumeOffset = position;
}
}
@Override
@SuppressLint("LongLogTag")
public void run() {
boolean done;
long start = loadPosition();
long start = 0;
if (!mMission.unknownLength) {
start = mMission.getThreadBytePosition(0);
if (DEBUG && start > 0) {
Log.i(TAG, "Resuming a single-thread download at " + start);
}
if (DEBUG && !mMission.unknownLength && start > 0) {
Log.i(TAG, "Resuming a single-thread download at " + start);
}
try {
long rangeStart = (mMission.unknownLength || start < 1) ? -1 : start;
int mId = 1;
mConn = mMission.openConnection(mId, rangeStart, -1);
mMission.establishConnection(mId, mConn);
@@ -81,7 +80,7 @@ public class DownloadRunnableFallback extends Thread {
if (!mMission.unknownLength)
mMission.unknownLength = Utility.getContentLength(mConn) == -1;
mF = new RandomAccessFile(mMission.getDownloadedFile(), "rw");
mF = mMission.storage.getStream();
mF.seek(mMission.offsets[mMission.current] + start);
mIs = mConn.getInputStream();
@@ -95,13 +94,12 @@ public class DownloadRunnableFallback extends Thread {
mMission.notifyProgress(len);
}
// if thread goes interrupted check if the last part mIs written. This avoid re-download the whole file
// if thread goes interrupted check if the last part is written. This avoid re-download the whole file
done = len == -1;
} catch (Exception e) {
dispose();
// save position
mMission.setThreadBytePosition(0, start);
savePosition(start);
if (!mMission.running || e instanceof ClosedByInterruptException) return;
@@ -110,6 +108,10 @@ public class DownloadRunnableFallback extends Thread {
return;
}
if (DEBUG) {
Log.e(TAG, "got exception, retrying...", e);
}
run();// try again
return;
}
@@ -119,7 +121,7 @@ public class DownloadRunnableFallback extends Thread {
if (done) {
mMission.notifyFinished();
} else {
mMission.setThreadBytePosition(0, start);
savePosition(start);
}
}

View File

@@ -1,16 +1,18 @@
package us.shandian.giga.get;
import android.support.annotation.NonNull;
public class FinishedMission extends Mission {
public FinishedMission() {
}
public FinishedMission(DownloadMission mission) {
public FinishedMission(@NonNull DownloadMission mission) {
source = mission.source;
length = mission.length;// ¿or mission.done?
timestamp = mission.timestamp;
name = mission.name;
location = mission.location;
kind = mission.kind;
storage = mission.storage;
}
}

View File

@@ -1,12 +1,14 @@
package us.shandian.giga.get;
import java.io.File;
import android.support.annotation.NonNull;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import us.shandian.giga.io.StoredFileHelper;
public abstract class Mission implements Serializable {
private static final long serialVersionUID = 0L;// last bump: 5 october 2018
private static final long serialVersionUID = 1L;// last bump: 27 march 2019
/**
* Source url of the resource
@@ -23,33 +25,24 @@ public abstract class Mission implements Serializable {
*/
public long timestamp;
/**
* The filename
*/
public String name;
/**
* The directory to store the download
*/
public String location;
/**
* pre-defined content type
*/
public char kind;
/**
* get the target file on the storage
*
* @return File object
* The downloaded file
*/
public File getDownloadedFile() {
return new File(location, name);
}
public StoredFileHelper storage;
/**
* Delete the downloaded file
*
* @return {@code true] if and only if the file is successfully deleted, otherwise, {@code false}
*/
public boolean delete() {
deleted = true;
return getDownloadedFile().delete();
if (storage != null) return storage.delete();
return true;
}
/**
@@ -57,10 +50,11 @@ public abstract class Mission implements Serializable {
*/
public transient boolean deleted = false;
@NonNull
@Override
public String toString() {
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(timestamp);
return "[" + calendar.getTime().toString() + "] " + location + File.separator + name;
return "[" + calendar.getTime().toString() + "] " + (storage.isInvalid() ? storage.getName() : storage.getUri());
}
}

View File

@@ -1,73 +0,0 @@
package us.shandian.giga.get.sqlite;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.get.FinishedMission;
import us.shandian.giga.get.Mission;
import static us.shandian.giga.get.sqlite.DownloadMissionHelper.KEY_LOCATION;
import static us.shandian.giga.get.sqlite.DownloadMissionHelper.KEY_NAME;
import static us.shandian.giga.get.sqlite.DownloadMissionHelper.MISSIONS_TABLE_NAME;
public class DownloadDataSource {
private static final String TAG = "DownloadDataSource";
private final DownloadMissionHelper downloadMissionHelper;
public DownloadDataSource(Context context) {
downloadMissionHelper = new DownloadMissionHelper(context);
}
public ArrayList<FinishedMission> loadFinishedMissions() {
SQLiteDatabase database = downloadMissionHelper.getReadableDatabase();
Cursor cursor = database.query(MISSIONS_TABLE_NAME, null, null,
null, null, null, DownloadMissionHelper.KEY_TIMESTAMP);
int count = cursor.getCount();
if (count == 0) return new ArrayList<>(1);
ArrayList<FinishedMission> result = new ArrayList<>(count);
while (cursor.moveToNext()) {
result.add(DownloadMissionHelper.getMissionFromCursor(cursor));
}
return result;
}
public void addMission(DownloadMission downloadMission) {
if (downloadMission == null) throw new NullPointerException("downloadMission is null");
SQLiteDatabase database = downloadMissionHelper.getWritableDatabase();
ContentValues values = DownloadMissionHelper.getValuesOfMission(downloadMission);
database.insert(MISSIONS_TABLE_NAME, null, values);
}
public void deleteMission(Mission downloadMission) {
if (downloadMission == null) throw new NullPointerException("downloadMission is null");
SQLiteDatabase database = downloadMissionHelper.getWritableDatabase();
database.delete(MISSIONS_TABLE_NAME,
KEY_LOCATION + " = ? AND " +
KEY_NAME + " = ?",
new String[]{downloadMission.location, downloadMission.name});
}
public void updateMission(DownloadMission downloadMission) {
if (downloadMission == null) throw new NullPointerException("downloadMission is null");
SQLiteDatabase database = downloadMissionHelper.getWritableDatabase();
ContentValues values = DownloadMissionHelper.getValuesOfMission(downloadMission);
String whereClause = KEY_LOCATION + " = ? AND " +
KEY_NAME + " = ?";
int rowsAffected = database.update(MISSIONS_TABLE_NAME, values,
whereClause, new String[]{downloadMission.location, downloadMission.name});
if (rowsAffected != 1) {
Log.e(TAG, "Expected 1 row to be affected by update but got " + rowsAffected);
}
}
}

View File

@@ -1,112 +0,0 @@
package us.shandian.giga.get.sqlite;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.get.FinishedMission;
/**
* SQLiteHelper to store finished {@link us.shandian.giga.get.DownloadMission}'s
*/
public class DownloadMissionHelper extends SQLiteOpenHelper {
private final String TAG = "DownloadMissionHelper";
// TODO: use NewPipeSQLiteHelper ('s constants) when playlist branch is merged (?)
private static final String DATABASE_NAME = "downloads.db";
private static final int DATABASE_VERSION = 3;
/**
* The table name of download missions
*/
static final String MISSIONS_TABLE_NAME = "download_missions";
/**
* The key to the directory location of the mission
*/
static final String KEY_LOCATION = "location";
/**
* The key to the urls of a mission
*/
static final String KEY_SOURCE_URL = "url";
/**
* The key to the name of a mission
*/
static final String KEY_NAME = "name";
/**
* The key to the done.
*/
static final String KEY_DONE = "bytes_downloaded";
static final String KEY_TIMESTAMP = "timestamp";
static final String KEY_KIND = "kind";
/**
* The statement to create the table
*/
private static final String MISSIONS_CREATE_TABLE =
"CREATE TABLE " + MISSIONS_TABLE_NAME + " (" +
KEY_LOCATION + " TEXT NOT NULL, " +
KEY_NAME + " TEXT NOT NULL, " +
KEY_SOURCE_URL + " TEXT NOT NULL, " +
KEY_DONE + " INTEGER NOT NULL, " +
KEY_TIMESTAMP + " INTEGER NOT NULL, " +
KEY_KIND + " TEXT NOT NULL, " +
" UNIQUE(" + KEY_LOCATION + ", " + KEY_NAME + "));";
public DownloadMissionHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(MISSIONS_CREATE_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion == 2) {
db.execSQL("ALTER TABLE " + MISSIONS_TABLE_NAME + " ADD COLUMN " + KEY_KIND + " TEXT;");
}
}
/**
* Returns all values of the download mission as ContentValues.
*
* @param downloadMission the download mission
* @return the content values
*/
public static ContentValues getValuesOfMission(DownloadMission downloadMission) {
ContentValues values = new ContentValues();
values.put(KEY_SOURCE_URL, downloadMission.source);
values.put(KEY_LOCATION, downloadMission.location);
values.put(KEY_NAME, downloadMission.name);
values.put(KEY_DONE, downloadMission.done);
values.put(KEY_TIMESTAMP, downloadMission.timestamp);
values.put(KEY_KIND, String.valueOf(downloadMission.kind));
return values;
}
public static FinishedMission getMissionFromCursor(Cursor cursor) {
if (cursor == null) throw new NullPointerException("cursor is null");
String kind = cursor.getString(cursor.getColumnIndex(KEY_KIND));
if (kind == null || kind.isEmpty()) kind = "?";
FinishedMission mission = new FinishedMission();
mission.name = cursor.getString(cursor.getColumnIndexOrThrow(KEY_NAME));
mission.location = cursor.getString(cursor.getColumnIndexOrThrow(KEY_LOCATION));
mission.source = cursor.getString(cursor.getColumnIndexOrThrow(KEY_SOURCE_URL));;
mission.length = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_DONE));
mission.timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_TIMESTAMP));
mission.kind = kind.charAt(0);
return mission;
}
}

View File

@@ -0,0 +1,237 @@
package us.shandian.giga.get.sqlite;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.util.Log;
import java.io.File;
import java.util.ArrayList;
import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.get.FinishedMission;
import us.shandian.giga.get.Mission;
import us.shandian.giga.io.StoredFileHelper;
/**
* SQLite helper to store finished {@link us.shandian.giga.get.FinishedMission}'s
*/
public class FinishedMissionStore extends SQLiteOpenHelper {
// TODO: use NewPipeSQLiteHelper ('s constants) when playlist branch is merged (?)
private static final String DATABASE_NAME = "downloads.db";
private static final int DATABASE_VERSION = 4;
/**
* The table name of download missions (old)
*/
private static final String MISSIONS_TABLE_NAME_v2 = "download_missions";
/**
* The table name of download missions
*/
private static final String FINISHED_TABLE_NAME = "finished_missions";
/**
* The key to the urls of a mission
*/
private static final String KEY_SOURCE = "url";
/**
* The key to the done.
*/
private static final String KEY_DONE = "bytes_downloaded";
private static final String KEY_TIMESTAMP = "timestamp";
private static final String KEY_KIND = "kind";
private static final String KEY_PATH = "path";
/**
* The statement to create the table
*/
private static final String MISSIONS_CREATE_TABLE =
"CREATE TABLE " + FINISHED_TABLE_NAME + " (" +
KEY_PATH + " TEXT NOT NULL, " +
KEY_SOURCE + " TEXT NOT NULL, " +
KEY_DONE + " INTEGER NOT NULL, " +
KEY_TIMESTAMP + " INTEGER NOT NULL, " +
KEY_KIND + " TEXT NOT NULL, " +
" UNIQUE(" + KEY_TIMESTAMP + ", " + KEY_PATH + "));";
private Context context;
public FinishedMissionStore(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
this.context = context;
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(MISSIONS_CREATE_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion == 2) {
db.execSQL("ALTER TABLE " + MISSIONS_TABLE_NAME_v2 + " ADD COLUMN " + KEY_KIND + " TEXT;");
oldVersion++;
}
if (oldVersion == 3) {
final String KEY_LOCATION = "location";
final String KEY_NAME = "name";
db.execSQL(MISSIONS_CREATE_TABLE);
Cursor cursor = db.query(MISSIONS_TABLE_NAME_v2, null, null,
null, null, null, KEY_TIMESTAMP);
int count = cursor.getCount();
if (count > 0) {
db.beginTransaction();
while (cursor.moveToNext()) {
ContentValues values = new ContentValues();
values.put(KEY_SOURCE, cursor.getString(cursor.getColumnIndex(KEY_SOURCE)));
values.put(KEY_DONE, cursor.getString(cursor.getColumnIndex(KEY_DONE)));
values.put(KEY_TIMESTAMP, cursor.getLong(cursor.getColumnIndex(KEY_TIMESTAMP)));
values.put(KEY_KIND, cursor.getString(cursor.getColumnIndex(KEY_KIND)));
values.put(KEY_PATH, Uri.fromFile(
new File(
cursor.getString(cursor.getColumnIndex(KEY_LOCATION)),
cursor.getString(cursor.getColumnIndex(KEY_NAME))
)
).toString());
db.insert(FINISHED_TABLE_NAME, null, values);
}
db.setTransactionSuccessful();
db.endTransaction();
}
cursor.close();
db.execSQL("DROP TABLE " + MISSIONS_TABLE_NAME_v2);
}
}
/**
* Returns all values of the download mission as ContentValues.
*
* @param downloadMission the download mission
* @return the content values
*/
private ContentValues getValuesOfMission(@NonNull Mission downloadMission) {
ContentValues values = new ContentValues();
values.put(KEY_SOURCE, downloadMission.source);
values.put(KEY_PATH, downloadMission.storage.getUri().toString());
values.put(KEY_DONE, downloadMission.length);
values.put(KEY_TIMESTAMP, downloadMission.timestamp);
values.put(KEY_KIND, String.valueOf(downloadMission.kind));
return values;
}
private FinishedMission getMissionFromCursor(Cursor cursor) {
if (cursor == null) throw new NullPointerException("cursor is null");
String kind = cursor.getString(cursor.getColumnIndex(KEY_KIND));
if (kind == null || kind.isEmpty()) kind = "?";
String path = cursor.getString(cursor.getColumnIndexOrThrow(KEY_PATH));
FinishedMission mission = new FinishedMission();
mission.source = cursor.getString(cursor.getColumnIndexOrThrow(KEY_SOURCE));
mission.length = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_DONE));
mission.timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_TIMESTAMP));
mission.kind = kind.charAt(0);
try {
mission.storage = new StoredFileHelper(context,null, Uri.parse(path), "");
} catch (Exception e) {
Log.e("FinishedMissionStore", "failed to load the storage path of: " + path, e);
mission.storage = new StoredFileHelper(null, path, "", "");
}
return mission;
}
//////////////////////////////////
// Data source methods
///////////////////////////////////
public ArrayList<FinishedMission> loadFinishedMissions() {
SQLiteDatabase database = getReadableDatabase();
Cursor cursor = database.query(FINISHED_TABLE_NAME, null, null,
null, null, null, KEY_TIMESTAMP + " DESC");
int count = cursor.getCount();
if (count == 0) return new ArrayList<>(1);
ArrayList<FinishedMission> result = new ArrayList<>(count);
while (cursor.moveToNext()) {
result.add(getMissionFromCursor(cursor));
}
return result;
}
public void addFinishedMission(DownloadMission downloadMission) {
if (downloadMission == null) throw new NullPointerException("downloadMission is null");
SQLiteDatabase database = getWritableDatabase();
ContentValues values = getValuesOfMission(downloadMission);
database.insert(FINISHED_TABLE_NAME, null, values);
}
public void deleteMission(Mission mission) {
if (mission == null) throw new NullPointerException("mission is null");
String ts = String.valueOf(mission.timestamp);
SQLiteDatabase database = getWritableDatabase();
if (mission instanceof FinishedMission) {
if (mission.storage.isInvalid()) {
database.delete(FINISHED_TABLE_NAME, KEY_TIMESTAMP + " = ?", new String[]{ts});
} else {
database.delete(FINISHED_TABLE_NAME, KEY_TIMESTAMP + " = ? AND " + KEY_PATH + " = ?", new String[]{
ts, mission.storage.getUri().toString()
});
}
} else {
throw new UnsupportedOperationException("DownloadMission");
}
}
public void updateMission(Mission mission) {
if (mission == null) throw new NullPointerException("mission is null");
SQLiteDatabase database = getWritableDatabase();
ContentValues values = getValuesOfMission(mission);
String ts = String.valueOf(mission.timestamp);
int rowsAffected;
if (mission instanceof FinishedMission) {
if (mission.storage.isInvalid()) {
rowsAffected = database.update(FINISHED_TABLE_NAME, values, KEY_TIMESTAMP + " = ?", new String[]{ts});
} else {
rowsAffected = database.update(FINISHED_TABLE_NAME, values, KEY_PATH + " = ?", new String[]{
mission.storage.getUri().toString()
});
}
} else {
throw new UnsupportedOperationException("DownloadMission");
}
if (rowsAffected != 1) {
Log.e("FinishedMissionStore", "Expected 1 row to be affected by update but got " + rowsAffected);
}
}
}

View File

@@ -1,153 +1,148 @@
package us.shandian.giga.postprocessing.io;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
public class ChunkFileInputStream extends SharpStream {
private RandomAccessFile source;
private final long offset;
private final long length;
private long position;
public ChunkFileInputStream(File file, long start, long end, String mode) throws IOException {
source = new RandomAccessFile(file, mode);
offset = start;
length = end - start;
position = 0;
if (length < 1) {
source.close();
throw new IOException("The chunk is empty or invalid");
}
if (source.length() < end) {
try {
throw new IOException(String.format("invalid file length. expected = %s found = %s", end, source.length()));
} finally {
source.close();
}
}
source.seek(offset);
}
/**
* Get absolute position on file
*
* @return the position
*/
public long getFilePointer() {
return offset + position;
}
@Override
public int read() throws IOException {
if ((position + 1) > length) {
return 0;
}
int res = source.read();
if (res >= 0) {
position++;
}
return res;
}
@Override
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
@Override
public int read(byte b[], int off, int len) throws IOException {
if ((position + len) > length) {
len = (int) (length - position);
}
if (len == 0) {
return 0;
}
int res = source.read(b, off, len);
position += res;
return res;
}
@Override
public long skip(long pos) throws IOException {
pos = Math.min(pos + position, length);
if (pos == 0) {
return 0;
}
source.seek(offset + pos);
long oldPos = position;
position = pos;
return pos - oldPos;
}
@Override
public int available() {
return (int) (length - position);
}
@SuppressWarnings("EmptyCatchBlock")
@Override
public void dispose() {
try {
source.close();
} catch (IOException err) {
} finally {
source = null;
}
}
@Override
public boolean isDisposed() {
return source == null;
}
@Override
public void rewind() throws IOException {
position = 0;
source.seek(offset);
}
@Override
public boolean canRewind() {
return true;
}
@Override
public boolean canRead() {
return true;
}
@Override
public boolean canWrite() {
return false;
}
@Override
public void write(byte value) {
}
@Override
public void write(byte[] buffer) {
}
@Override
public void write(byte[] buffer, int offset, int count) {
}
@Override
public void flush() {
}
}
package us.shandian.giga.io;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
public class ChunkFileInputStream extends SharpStream {
private SharpStream source;
private final long offset;
private final long length;
private long position;
public ChunkFileInputStream(SharpStream target, long start) throws IOException {
this(target, start, target.length());
}
public ChunkFileInputStream(SharpStream target, long start, long end) throws IOException {
source = target;
offset = start;
length = end - start;
position = 0;
if (length < 1) {
source.close();
throw new IOException("The chunk is empty or invalid");
}
if (source.length() < end) {
try {
throw new IOException(String.format("invalid file length. expected = %s found = %s", end, source.length()));
} finally {
source.close();
}
}
source.seek(offset);
}
/**
* Get absolute position on file
*
* @return the position
*/
public long getFilePointer() {
return offset + position;
}
@Override
public int read() throws IOException {
if ((position + 1) > length) {
return 0;
}
int res = source.read();
if (res >= 0) {
position++;
}
return res;
}
@Override
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
@Override
public int read(byte b[], int off, int len) throws IOException {
if ((position + len) > length) {
len = (int) (length - position);
}
if (len == 0) {
return 0;
}
int res = source.read(b, off, len);
position += res;
return res;
}
@Override
public long skip(long pos) throws IOException {
pos = Math.min(pos + position, length);
if (pos == 0) {
return 0;
}
source.seek(offset + pos);
long oldPos = position;
position = pos;
return pos - oldPos;
}
@Override
public long available() {
return (int) (length - position);
}
@SuppressWarnings("EmptyCatchBlock")
@Override
public void close() {
source.close();
source = null;
}
@Override
public boolean isClosed() {
return source == null;
}
@Override
public void rewind() throws IOException {
position = 0;
source.seek(offset);
}
@Override
public boolean canRewind() {
return true;
}
@Override
public boolean canRead() {
return true;
}
@Override
public boolean canWrite() {
return false;
}
@Override
public void write(byte value) {
}
@Override
public void write(byte[] buffer) {
}
@Override
public void write(byte[] buffer, int offset, int count) {
}
}

View File

@@ -0,0 +1,497 @@
package us.shandian.giga.io;
import android.support.annotation.NonNull;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
public class CircularFileWriter extends SharpStream {
private final static int QUEUE_BUFFER_SIZE = 8 * 1024;// 8 KiB
private final static int COPY_BUFFER_SIZE = 128 * 1024; // 128 KiB
private final static int NOTIFY_BYTES_INTERVAL = 64 * 1024;// 64 KiB
private final static int THRESHOLD_AUX_LENGTH = 15 * 1024 * 1024;// 15 MiB
private OffsetChecker callback;
public ProgressReport onProgress;
public WriteErrorHandle onWriteError;
private long reportPosition;
private long maxLengthKnown = -1;
private BufferedFile out;
private BufferedFile aux;
public CircularFileWriter(SharpStream target, File temp, OffsetChecker checker) throws IOException {
if (checker == null) {
throw new NullPointerException("checker is null");
}
if (!temp.exists()) {
if (!temp.createNewFile()) {
throw new IOException("Cannot create a temporal file");
}
}
aux = new BufferedFile(temp);
out = new BufferedFile(target);
callback = checker;
reportPosition = NOTIFY_BYTES_INTERVAL;
}
private void flushAuxiliar(long amount) throws IOException {
if (aux.length < 1) {
return;
}
out.flush();
aux.flush();
boolean underflow = aux.offset < aux.length || out.offset < out.length;
byte[] buffer = new byte[COPY_BUFFER_SIZE];
aux.target.seek(0);
out.target.seek(out.length);
long length = amount;
while (length > 0) {
int read = (int) Math.min(length, Integer.MAX_VALUE);
read = aux.target.read(buffer, 0, Math.min(read, buffer.length));
if (read < 1) {
amount -= length;
break;
}
out.writeProof(buffer, read);
length -= read;
}
if (underflow) {
if (out.offset >= out.length) {
// calculate the aux underflow pointer
if (aux.offset < amount) {
out.offset += aux.offset;
aux.offset = 0;
out.target.seek(out.offset);
} else {
aux.offset -= amount;
out.offset = out.length + amount;
}
} else {
aux.offset = 0;
}
} else {
out.offset += amount;
aux.offset -= amount;
}
out.length += amount;
if (out.length > maxLengthKnown) {
maxLengthKnown = out.length;
}
if (amount < aux.length) {
// move the excess data to the beginning of the file
long readOffset = amount;
long writeOffset = 0;
aux.length -= amount;
length = aux.length;
while (length > 0) {
int read = (int) Math.min(length, Integer.MAX_VALUE);
read = aux.target.read(buffer, 0, Math.min(read, buffer.length));
aux.target.seek(writeOffset);
aux.writeProof(buffer, read);
writeOffset += read;
readOffset += read;
length -= read;
aux.target.seek(readOffset);
}
aux.target.setLength(aux.length);
return;
}
if (aux.length > THRESHOLD_AUX_LENGTH) {
aux.target.setLength(THRESHOLD_AUX_LENGTH);// or setLength(0);
}
aux.reset();
}
/**
* Flush any buffer and close the output file. Use this method if the
* operation is successful
*
* @return the final length of the file
* @throws IOException if an I/O error occurs
*/
public long finalizeFile() throws IOException {
flushAuxiliar(aux.length);
out.flush();
// change file length (if required)
long length = Math.max(maxLengthKnown, out.length);
if (length != out.target.length()) {
out.target.setLength(length);
}
close();
return length;
}
/**
* Close the file without flushing any buffer
*/
@Override
public void close() {
if (out != null) {
out.close();
out = null;
}
if (aux != null) {
aux.close();
aux = null;
}
}
@Override
public void write(byte b) throws IOException {
write(new byte[]{b}, 0, 1);
}
@Override
public void write(byte b[]) throws IOException {
write(b, 0, b.length);
}
@Override
public void write(byte b[], int off, int len) throws IOException {
if (len == 0) {
return;
}
long available;
long offsetOut = out.getOffset();
long offsetAux = aux.getOffset();
long end = callback.check();
if (end == -1) {
available = Integer.MAX_VALUE;
} else if (end < offsetOut) {
throw new IOException("The reported offset is invalid: " + end + "<" + offsetOut);
} else {
available = end - offsetOut;
}
boolean usingAux = aux.length > 0 && offsetOut >= out.length;
boolean underflow = offsetAux < aux.length || offsetOut < out.length;
if (usingAux) {
// before continue calculate the final length of aux
long length = offsetAux + len;
if (underflow) {
if (aux.length > length) {
length = aux.length;// the length is not changed
}
} else {
length = aux.length + len;
}
aux.write(b, off, len);
if (length >= THRESHOLD_AUX_LENGTH && length <= available) {
flushAuxiliar(available);
}
} else {
if (underflow) {
available = out.length - offsetOut;
}
int length = Math.min(len, (int) available);
out.write(b, off, length);
len -= length;
off += length;
if (len > 0) {
aux.write(b, off, len);
}
}
if (onProgress != null) {
long absoluteOffset = out.getOffset() + aux.getOffset();
if (absoluteOffset > reportPosition) {
reportPosition = absoluteOffset + NOTIFY_BYTES_INTERVAL;
onProgress.report(absoluteOffset);
}
}
}
@Override
public void flush() throws IOException {
aux.flush();
out.flush();
long total = out.length + aux.length;
if (total > maxLengthKnown) {
maxLengthKnown = total;// save the current file length in case the method {@code rewind()} is called
}
}
@Override
public long skip(long amount) throws IOException {
seek(out.getOffset() + aux.getOffset() + amount);
return amount;
}
@Override
public void rewind() throws IOException {
if (onProgress != null) {
onProgress.report(-out.length - aux.length);// rollback the whole progress
}
seek(0);
reportPosition = NOTIFY_BYTES_INTERVAL;
}
@Override
public void seek(long offset) throws IOException {
long total = out.length + aux.length;
if (offset == total) {
// do not ignore the seek offset if a underflow exists
long relativeOffset = out.getOffset() + aux.getOffset();
if (relativeOffset == total) {
return;
}
}
// flush everything, avoid any underflow
flush();
if (offset < 0 || offset > total) {
throw new IOException("desired offset is outside of range=0-" + total + " offset=" + offset);
}
if (offset > out.length) {
out.seek(out.length);
aux.seek(offset - out.length);
} else {
out.seek(offset);
aux.seek(0);
}
}
@Override
public boolean isClosed() {
return out == null;
}
@Override
public boolean canRewind() {
return true;
}
@Override
public boolean canWrite() {
return true;
}
@Override
public boolean canSeek() {
return true;
}
// <editor-fold defaultstate="collapsed" desc="stub read methods">
@Override
public boolean canRead() {
return false;
}
@Override
public int read() {
throw new UnsupportedOperationException("write-only");
}
@Override
public int read(byte[] buffer
) {
throw new UnsupportedOperationException("write-only");
}
@Override
public int read(byte[] buffer, int offset, int count
) {
throw new UnsupportedOperationException("write-only");
}
@Override
public long available() {
throw new UnsupportedOperationException("write-only");
}
//</editor-fold>
public interface OffsetChecker {
/**
* Checks the amount of available space ahead
*
* @return absolute offset in the file where no more data SHOULD NOT be
* written. If the value is -1 the whole file will be used
*/
long check();
}
public interface ProgressReport {
/**
* Report the size of the new file
*
* @param progress the new size
*/
void report(long progress);
}
public interface WriteErrorHandle {
/**
* Attempts to handle a I/O exception
*
* @param err the cause
* @return {@code true} to retry and continue, otherwise, {@code false}
* and throw the exception
*/
boolean handle(Exception err);
}
class BufferedFile {
protected final SharpStream target;
private long offset;
protected long length;
private byte[] queue = new byte[QUEUE_BUFFER_SIZE];
private int queueSize;
BufferedFile(File file) throws FileNotFoundException {
this.target = new FileStream(file);
}
BufferedFile(SharpStream target) {
this.target = target;
}
protected long getOffset() {
return offset + queueSize;// absolute offset in the file
}
protected void close() {
queue = null;
target.close();
}
protected void write(byte b[], int off, int len) throws IOException {
while (len > 0) {
// if the queue is full, the method available() will flush the queue
int read = Math.min(available(), len);
// enqueue incoming buffer
System.arraycopy(b, off, queue, queueSize, read);
queueSize += read;
len -= read;
off += read;
}
long total = offset + queueSize;
if (total > length) {
length = total;// save length
}
}
void flush() throws IOException {
writeProof(queue, queueSize);
offset += queueSize;
queueSize = 0;
}
protected void rewind() throws IOException {
offset = 0;
target.seek(0);
}
protected int available() throws IOException {
if (queueSize >= queue.length) {
flush();
return queue.length;
}
return queue.length - queueSize;
}
void reset() throws IOException {
offset = 0;
length = 0;
target.seek(0);
}
protected void seek(long absoluteOffset) throws IOException {
if (absoluteOffset == offset) {
return;// nothing to do
}
offset = absoluteOffset;
target.seek(absoluteOffset);
}
void writeProof(byte[] buffer, int length) throws IOException {
if (onWriteError == null) {
target.write(buffer, 0, length);
return;
}
while (true) {
try {
target.write(buffer, 0, length);
return;
} catch (Exception e) {
if (!onWriteError.handle(e)) {
throw e;// give up
}
}
}
}
@NonNull
@Override
public String toString() {
String absLength;
try {
absLength = Long.toString(target.length());
} catch (IOException e) {
absLength = "[" + e.getLocalizedMessage() + "]";
}
return String.format(
"offset=%s length=%s queue=%s absLength=%s",
offset, length, queueSize, absLength
);
}
}
}

View File

@@ -1,126 +1,131 @@
package us.shandian.giga.postprocessing.io;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
/**
* @author kapodamy
*/
public class FileStream extends SharpStream {
public enum Mode {
Read,
ReadWrite
}
public RandomAccessFile source;
private final Mode mode;
public FileStream(String path, Mode mode) throws IOException {
String flags;
if (mode == Mode.Read) {
flags = "r";
} else {
flags = "rw";
}
this.mode = mode;
source = new RandomAccessFile(path, flags);
}
@Override
public int read() throws IOException {
return source.read();
}
@Override
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
@Override
public int read(byte b[], int off, int len) throws IOException {
return source.read(b, off, len);
}
@Override
public long skip(long pos) throws IOException {
FileChannel fc = source.getChannel();
fc.position(fc.position() + pos);
return pos;
}
@Override
public int available() {
try {
return (int) (source.length() - source.getFilePointer());
} catch (IOException ex) {
return 0;
}
}
@SuppressWarnings("EmptyCatchBlock")
@Override
public void dispose() {
try {
source.close();
} catch (IOException err) {
} finally {
source = null;
}
}
@Override
public boolean isDisposed() {
return source == null;
}
@Override
public void rewind() throws IOException {
source.getChannel().position(0);
}
@Override
public boolean canRewind() {
return true;
}
@Override
public boolean canRead() {
return mode == Mode.Read || mode == Mode.ReadWrite;
}
@Override
public boolean canWrite() {
return mode == Mode.ReadWrite;
}
@Override
public void write(byte value) throws IOException {
source.write(value);
}
@Override
public void write(byte[] buffer) throws IOException {
source.write(buffer);
}
@Override
public void write(byte[] buffer, int offset, int count) throws IOException {
source.write(buffer, offset, count);
}
@Override
public void flush() {
}
@Override
public void setLength(long length) throws IOException {
source.setLength(length);
}
}
package us.shandian.giga.io;
import android.support.annotation.NonNull;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
/**
* @author kapodamy
*/
public class FileStream extends SharpStream {
public RandomAccessFile source;
public FileStream(@NonNull File target) throws FileNotFoundException {
this.source = new RandomAccessFile(target, "rw");
}
public FileStream(@NonNull String path) throws FileNotFoundException {
this.source = new RandomAccessFile(path, "rw");
}
@Override
public int read() throws IOException {
return source.read();
}
@Override
public int read(byte b[]) throws IOException {
return source.read(b);
}
@Override
public int read(byte b[], int off, int len) throws IOException {
return source.read(b, off, len);
}
@Override
public long skip(long pos) throws IOException {
return source.skipBytes((int) pos);
}
@Override
public long available() {
try {
return source.length() - source.getFilePointer();
} catch (IOException e) {
return 0;
}
}
@Override
public void close() {
if (source == null) return;
try {
source.close();
} catch (IOException err) {
// nothing to do
}
source = null;
}
@Override
public boolean isClosed() {
return source == null;
}
@Override
public void rewind() throws IOException {
source.seek(0);
}
@Override
public boolean canRewind() {
return true;
}
@Override
public boolean canRead() {
return true;
}
@Override
public boolean canWrite() {
return true;
}
@Override
public boolean canSeek() {
return true;
}
@Override
public boolean canSetLength() {
return true;
}
@Override
public void write(byte value) throws IOException {
source.write(value);
}
@Override
public void write(byte[] buffer) throws IOException {
source.write(buffer);
}
@Override
public void write(byte[] buffer, int offset, int count) throws IOException {
source.write(buffer, offset, count);
}
@Override
public void setLength(long length) throws IOException {
source.setLength(length);
}
@Override
public void seek(long offset) throws IOException {
source.seek(offset);
}
@Override
public long length() throws IOException {
return source.length();
}
}

View File

@@ -0,0 +1,145 @@
package us.shandian.giga.io;
import android.content.ContentResolver;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.support.annotation.NonNull;
import android.util.Log;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
public class FileStreamSAF extends SharpStream {
private final FileInputStream in;
private final FileOutputStream out;
private final FileChannel channel;
private final ParcelFileDescriptor file;
private boolean disposed;
public FileStreamSAF(@NonNull ContentResolver contentResolver, Uri fileUri) throws IOException {
// Notes:
// the file must exists first
// ¡read-write mode must allow seek!
// It is not guaranteed to work with files in the cloud (virtual files), tested in local storage devices
file = contentResolver.openFileDescriptor(fileUri, "rw");
if (file == null) {
throw new IOException("Cannot get the ParcelFileDescriptor for " + fileUri.toString());
}
in = new FileInputStream(file.getFileDescriptor());
out = new FileOutputStream(file.getFileDescriptor());
channel = out.getChannel();// or use in.getChannel()
}
@Override
public int read() throws IOException {
return in.read();
}
@Override
public int read(byte[] buffer) throws IOException {
return in.read(buffer);
}
@Override
public int read(byte[] buffer, int offset, int count) throws IOException {
return in.read(buffer, offset, count);
}
@Override
public long skip(long amount) throws IOException {
return in.skip(amount);// ¿or use channel.position(channel.position() + amount)?
}
@Override
public long available() {
try {
return in.available();
} catch (IOException e) {
return 0;// ¡but not -1!
}
}
@Override
public void rewind() throws IOException {
seek(0);
}
@Override
public void close() {
try {
disposed = true;
file.close();
in.close();
out.close();
channel.close();
} catch (IOException e) {
Log.e("FileStreamSAF", "close() error", e);
}
}
@Override
public boolean isClosed() {
return disposed;
}
@Override
public boolean canRewind() {
return true;
}
@Override
public boolean canRead() {
return true;
}
@Override
public boolean canWrite() {
return true;
}
public boolean canSetLength() {
return true;
}
public boolean canSeek() {
return true;
}
@Override
public void write(byte value) throws IOException {
out.write(value);
}
@Override
public void write(byte[] buffer) throws IOException {
out.write(buffer);
}
@Override
public void write(byte[] buffer, int offset, int count) throws IOException {
out.write(buffer, offset, count);
}
public void setLength(long length) throws IOException {
channel.truncate(length);
}
public void seek(long offset) throws IOException {
channel.position(offset);
}
@Override
public long length() throws IOException {
return channel.size();
}
}

View File

@@ -1,59 +1,61 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package us.shandian.giga.postprocessing.io;
import android.support.annotation.NonNull;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
import java.io.InputStream;
/**
* Wrapper for the classic {@link java.io.InputStream}
* @author kapodamy
*/
public class SharpInputStream extends InputStream {
private final SharpStream base;
public SharpInputStream(SharpStream base) throws IOException {
if (!base.canRead()) {
throw new IOException("The provided stream is not readable");
}
this.base = base;
}
@Override
public int read() throws IOException {
return base.read();
}
@Override
public int read(@NonNull byte[] bytes) throws IOException {
return base.read(bytes);
}
@Override
public int read(@NonNull byte[] bytes, int i, int i1) throws IOException {
return base.read(bytes, i, i1);
}
@Override
public long skip(long l) throws IOException {
return base.skip(l);
}
@Override
public int available() {
return base.available();
}
@Override
public void close() {
base.dispose();
}
}
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package us.shandian.giga.io;
import android.support.annotation.NonNull;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
import java.io.InputStream;
/**
* Wrapper for the classic {@link java.io.InputStream}
*
* @author kapodamy
*/
public class SharpInputStream extends InputStream {
private final SharpStream base;
public SharpInputStream(SharpStream base) throws IOException {
if (!base.canRead()) {
throw new IOException("The provided stream is not readable");
}
this.base = base;
}
@Override
public int read() throws IOException {
return base.read();
}
@Override
public int read(@NonNull byte[] bytes) throws IOException {
return base.read(bytes);
}
@Override
public int read(@NonNull byte[] bytes, int i, int i1) throws IOException {
return base.read(bytes, i, i1);
}
@Override
public long skip(long l) throws IOException {
return base.skip(l);
}
@Override
public int available() {
long res = base.available();
return res > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) res;
}
@Override
public void close() {
base.close();
}
}

View File

@@ -0,0 +1,289 @@
package us.shandian.giga.io;
import android.annotation.TargetApi;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.provider.DocumentsContract;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.provider.DocumentFile;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import static android.provider.DocumentsContract.Document.COLUMN_DISPLAY_NAME;
import static android.provider.DocumentsContract.Root.COLUMN_DOCUMENT_ID;
public class StoredDirectoryHelper {
public final static int PERMISSION_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
private File ioTree;
private DocumentFile docTree;
private Context context;
private String tag;
public StoredDirectoryHelper(@NonNull Context context, @NonNull Uri path, String tag) throws IOException {
this.tag = tag;
if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(path.getScheme())) {
this.ioTree = new File(URI.create(path.toString()));
return;
}
this.context = context;
try {
this.context.getContentResolver().takePersistableUriPermission(path, PERMISSION_FLAGS);
} catch (Exception e) {
throw new IOException(e);
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
throw new IOException("Storage Access Framework with Directory API is not available");
this.docTree = DocumentFile.fromTreeUri(context, path);
if (this.docTree == null)
throw new IOException("Failed to create the tree from Uri");
}
@TargetApi(Build.VERSION_CODES.KITKAT)
public StoredDirectoryHelper(@NonNull URI location, String tag) {
ioTree = new File(location);
this.tag = tag;
}
public StoredFileHelper createFile(String filename, String mime) {
return createFile(filename, mime, false);
}
public StoredFileHelper createUniqueFile(String name, String mime) {
ArrayList<String> matches = new ArrayList<>();
String[] filename = splitFilename(name);
String lcFilename = filename[0].toLowerCase();
if (docTree == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
for (File file : ioTree.listFiles())
addIfStartWith(matches, lcFilename, file.getName());
} else {
// warning: SAF file listing is very slow
Uri docTreeChildren = DocumentsContract.buildChildDocumentsUriUsingTree(
docTree.getUri(), DocumentsContract.getDocumentId(docTree.getUri())
);
String[] projection = new String[]{COLUMN_DISPLAY_NAME};
String selection = "(LOWER(" + COLUMN_DISPLAY_NAME + ") LIKE ?%";
ContentResolver cr = context.getContentResolver();
try (Cursor cursor = cr.query(docTreeChildren, projection, selection, new String[]{lcFilename}, null)) {
if (cursor != null) {
while (cursor.moveToNext())
addIfStartWith(matches, lcFilename, cursor.getString(0));
}
}
}
if (matches.size() < 1) {
return createFile(name, mime, true);
} else {
// check if the filename is in use
String lcName = name.toLowerCase();
for (String testName : matches) {
if (testName.equals(lcName)) {
lcName = null;
break;
}
}
// check if not in use
if (lcName != null) return createFile(name, mime, true);
}
Collections.sort(matches, String::compareTo);
for (int i = 1; i < 1000; i++) {
if (Collections.binarySearch(matches, makeFileName(lcFilename, i, filename[1])) < 0)
return createFile(makeFileName(filename[0], i, filename[1]), mime, true);
}
return createFile(String.valueOf(System.currentTimeMillis()).concat(filename[1]), mime, false);
}
private StoredFileHelper createFile(String filename, String mime, boolean safe) {
StoredFileHelper storage;
try {
if (docTree == null)
storage = new StoredFileHelper(ioTree, filename, mime);
else
storage = new StoredFileHelper(context, docTree, filename, mime, safe);
} catch (IOException e) {
return null;
}
storage.tag = tag;
return storage;
}
public Uri getUri() {
return docTree == null ? Uri.fromFile(ioTree) : docTree.getUri();
}
public boolean exists() {
return docTree == null ? ioTree.exists() : docTree.exists();
}
/**
* Indicates whatever if is possible access using the {@code java.io} API
*
* @return {@code true} for Java I/O API, otherwise, {@code false} for Storage Access Framework
*/
public boolean isDirect() {
return docTree == null;
}
/**
* Only using Java I/O. Creates the directory named by this abstract pathname, including any
* necessary but nonexistent parent directories. Note that if this
* operation fails it may have succeeded in creating some of the necessary
* parent directories.
*
* @return <code>true</code> if and only if the directory was created,
* along with all necessary parent directories or already exists; <code>false</code>
* otherwise
*/
public boolean mkdirs() {
if (docTree == null) {
return ioTree.exists() || ioTree.mkdirs();
}
if (docTree.exists()) return true;
try {
DocumentFile parent;
String child = docTree.getName();
while (true) {
parent = docTree.getParentFile();
if (parent == null || child == null) break;
if (parent.exists()) return true;
parent.createDirectory(child);
child = parent.getName();// for the next iteration
}
} catch (Exception e) {
// no more parent directories or unsupported by the storage provider
}
return false;
}
public String getTag() {
return tag;
}
public Uri findFile(String filename) {
if (docTree == null) {
File res = new File(ioTree, filename);
return res.exists() ? Uri.fromFile(res) : null;
}
DocumentFile res = findFileSAFHelper(context, docTree, filename);
return res == null ? null : res.getUri();
}
public boolean canWrite() {
return docTree == null ? ioTree.canWrite() : docTree.canWrite();
}
@NonNull
@Override
public String toString() {
return docTree == null ? Uri.fromFile(ioTree).toString() : docTree.getUri().toString();
}
////////////////////
// Utils
///////////////////
private static void addIfStartWith(ArrayList<String> list, @NonNull String base, String str) {
if (str == null || str.isEmpty()) return;
str = str.toLowerCase();
if (str.startsWith(base)) list.add(str);
}
private static String[] splitFilename(@NonNull String filename) {
int dotIndex = filename.lastIndexOf('.');
if (dotIndex < 0 || (dotIndex == filename.length() - 1))
return new String[]{filename, ""};
return new String[]{filename.substring(0, dotIndex), filename.substring(dotIndex)};
}
private static String makeFileName(String name, int idx, String ext) {
return name.concat(" (").concat(String.valueOf(idx)).concat(")").concat(ext);
}
/**
* Fast (but not enough) file/directory finder under the storage access framework
*
* @param context The context
* @param tree Directory where search
* @param filename Target filename
* @return A {@link android.support.v4.provider.DocumentFile} contain the reference, otherwise, null
*/
static DocumentFile findFileSAFHelper(@Nullable Context context, DocumentFile tree, String filename) {
if (context == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return tree.findFile(filename);// warning: this is very slow
}
if (!tree.canRead()) return null;// missing read permission
final int name = 0;
final int documentId = 1;
// LOWER() SQL function is not supported
String selection = COLUMN_DISPLAY_NAME + " = ?";
//String selection = COLUMN_DISPLAY_NAME + " LIKE ?%";
Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(
tree.getUri(), DocumentsContract.getDocumentId(tree.getUri())
);
String[] projection = {COLUMN_DISPLAY_NAME, COLUMN_DOCUMENT_ID};
ContentResolver contentResolver = context.getContentResolver();
filename = filename.toLowerCase();
try (Cursor cursor = contentResolver.query(childrenUri, projection, selection, new String[]{filename}, null)) {
if (cursor == null) return null;
while (cursor.moveToNext()) {
if (cursor.isNull(name) || !cursor.getString(name).toLowerCase().startsWith(filename))
continue;
return DocumentFile.fromSingleUri(
context, DocumentsContract.buildDocumentUriUsingTree(
tree.getUri(), cursor.getString(documentId)
)
);
}
}
return null;
}
}

View File

@@ -0,0 +1,381 @@
package us.shandian.giga.io;
import android.annotation.TargetApi;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.provider.DocumentsContract;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.provider.DocumentFile;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.net.URI;
public class StoredFileHelper implements Serializable {
private static final long serialVersionUID = 0L;
public static final String DEFAULT_MIME = "application/octet-stream";
private transient DocumentFile docFile;
private transient DocumentFile docTree;
private transient File ioFile;
private transient Context context;
protected String source;
private String sourceTree;
protected String tag;
private String srcName;
private String srcType;
public StoredFileHelper(@Nullable Uri parent, String filename, String mime, String tag) {
this.source = null;// this instance will be "invalid" see invalidate()/isInvalid() methods
this.srcName = filename;
this.srcType = mime == null ? DEFAULT_MIME : mime;
if (parent != null) this.sourceTree = parent.toString();
this.tag = tag;
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
StoredFileHelper(@Nullable Context context, DocumentFile tree, String filename, String mime, boolean safe) throws IOException {
this.docTree = tree;
this.context = context;
DocumentFile res;
if (safe) {
// no conflicts (the filename is not in use)
res = this.docTree.createFile(mime, filename);
if (res == null) throw new IOException("Cannot create the file");
} else {
res = createSAF(context, mime, filename);
}
this.docFile = res;
this.source = docFile.getUri().toString();
this.sourceTree = docTree.getUri().toString();
this.srcName = this.docFile.getName();
this.srcType = this.docFile.getType();
}
StoredFileHelper(File location, String filename, String mime) throws IOException {
this.ioFile = new File(location, filename);
if (this.ioFile.exists()) {
if (!this.ioFile.isFile() && !this.ioFile.delete())
throw new IOException("The filename is already in use by non-file entity and cannot overwrite it");
} else {
if (!this.ioFile.createNewFile())
throw new IOException("Cannot create the file");
}
this.source = Uri.fromFile(this.ioFile).toString();
this.sourceTree = Uri.fromFile(location).toString();
this.srcName = ioFile.getName();
this.srcType = mime;
}
@TargetApi(Build.VERSION_CODES.KITKAT)
public StoredFileHelper(Context context, @Nullable Uri parent, @NonNull Uri path, String tag) throws IOException {
this.tag = tag;
this.source = path.toString();
if (path.getScheme() == null || path.getScheme().equalsIgnoreCase(ContentResolver.SCHEME_FILE)) {
this.ioFile = new File(URI.create(this.source));
} else {
DocumentFile file = DocumentFile.fromSingleUri(context, path);
if (file == null) throw new RuntimeException("SAF not available");
this.context = context;
if (file.getName() == null) {
this.source = null;
return;
} else {
this.docFile = file;
takePermissionSAF();
}
}
if (parent != null) {
if (!ContentResolver.SCHEME_FILE.equals(parent.getScheme()))
this.docTree = DocumentFile.fromTreeUri(context, parent);
this.sourceTree = parent.toString();
}
this.srcName = getName();
this.srcType = getType();
}
public static StoredFileHelper deserialize(@NonNull StoredFileHelper storage, Context context) throws IOException {
Uri treeUri = storage.sourceTree == null ? null : Uri.parse(storage.sourceTree);
if (storage.isInvalid())
return new StoredFileHelper(treeUri, storage.srcName, storage.srcType, storage.tag);
StoredFileHelper instance = new StoredFileHelper(context, treeUri, Uri.parse(storage.source), storage.tag);
// under SAF, if the target document is deleted, conserve the filename and mime
if (instance.srcName == null) instance.srcName = storage.srcName;
if (instance.srcType == null) instance.srcType = storage.srcType;
return instance;
}
public static void requestSafWithFileCreation(@NonNull Fragment who, int requestCode, String filename, String mime) {
// SAF notes:
// ACTION_OPEN_DOCUMENT Do not let you create the file, useful for overwrite files
// ACTION_CREATE_DOCUMENT No overwrite support, useless the file provider resolve the conflict
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.setType(mime)
.putExtra(Intent.EXTRA_TITLE, filename)
.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | StoredDirectoryHelper.PERMISSION_FLAGS)
.putExtra("android.content.extra.SHOW_ADVANCED", true);// hack, show all storage disks
who.startActivityForResult(intent, requestCode);
}
public SharpStream getStream() throws IOException {
invalid();
if (docFile == null)
return new FileStream(ioFile);
else
return new FileStreamSAF(context.getContentResolver(), docFile.getUri());
}
/**
* Indicates whatever if is possible access using the {@code java.io} API
*
* @return {@code true} for Java I/O API, otherwise, {@code false} for Storage Access Framework
*/
public boolean isDirect() {
invalid();
return docFile == null;
}
public boolean isInvalid() {
return source == null;
}
public Uri getUri() {
invalid();
return docFile == null ? Uri.fromFile(ioFile) : docFile.getUri();
}
public Uri getParentUri() {
invalid();
return sourceTree == null ? null : Uri.parse(sourceTree);
}
public void truncate() throws IOException {
invalid();
try (SharpStream fs = getStream()) {
fs.setLength(0);
}
}
public boolean delete() {
if (source == null) return true;
if (docFile == null) return ioFile.delete();
boolean res = docFile.delete();
try {
int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
context.getContentResolver().releasePersistableUriPermission(docFile.getUri(), flags);
} catch (Exception ex) {
// nothing to do
}
return res;
}
public long length() {
invalid();
return docFile == null ? ioFile.length() : docFile.length();
}
public boolean canWrite() {
if (source == null) return false;
return docFile == null ? ioFile.canWrite() : docFile.canWrite();
}
public String getName() {
if (source == null)
return srcName;
else if (docFile == null)
return ioFile.getName();
String name = docFile.getName();
return name == null ? srcName : name;
}
public String getType() {
if (source == null || docFile == null)
return srcType;
String type = docFile.getType();
return type == null ? srcType : type;
}
public String getTag() {
return tag;
}
public boolean existsAsFile() {
if (source == null) return false;
boolean exists = docFile == null ? ioFile.exists() : docFile.exists();
boolean isFile = docFile == null ? ioFile.isFile() : docFile.isFile();// ¿docFile.isVirtual() means is no-physical?
return exists && isFile;
}
public boolean create() {
invalid();
boolean result;
if (docFile == null) {
try {
result = ioFile.createNewFile();
} catch (IOException e) {
return false;
}
} else if (docTree == null) {
result = false;
} else {
if (!docTree.canRead() || !docTree.canWrite()) return false;
try {
docFile = createSAF(context, srcType, srcName);
if (docFile == null || docFile.getName() == null) return false;
result = true;
} catch (IOException e) {
return false;
}
}
if (result) {
source = (docFile == null ? Uri.fromFile(ioFile) : docFile.getUri()).toString();
srcName = getName();
srcType = getType();
}
return result;
}
public void invalidate() {
if (source == null) return;
srcName = getName();
srcType = getType();
source = null;
docTree = null;
docFile = null;
ioFile = null;
context = null;
}
public boolean equals(StoredFileHelper storage) {
if (this == storage) return true;
// note: do not compare tags, files can have the same parent folder
//if (stringMismatch(this.tag, storage.tag)) return false;
if (stringMismatch(getLowerCase(this.sourceTree), getLowerCase(this.sourceTree)))
return false;
if (this.isInvalid() || storage.isInvalid()) {
return this.srcName.equalsIgnoreCase(storage.srcName) && this.srcType.equalsIgnoreCase(storage.srcType);
}
if (this.isDirect() != storage.isDirect()) return false;
if (this.isDirect())
return this.ioFile.getPath().equalsIgnoreCase(storage.ioFile.getPath());
return DocumentsContract.getDocumentId(
this.docFile.getUri()
).equalsIgnoreCase(DocumentsContract.getDocumentId(
storage.docFile.getUri()
));
}
@NonNull
@Override
public String toString() {
if (source == null)
return "[Invalid state] name=" + srcName + " type=" + srcType + " tag=" + tag;
else
return "sourceFile=" + source + " treeSource=" + (sourceTree == null ? "" : sourceTree) + " tag=" + tag;
}
private void invalid() {
if (source == null)
throw new IllegalStateException("In invalid state");
}
private void takePermissionSAF() throws IOException {
try {
context.getContentResolver().takePersistableUriPermission(docFile.getUri(), StoredDirectoryHelper.PERMISSION_FLAGS);
} catch (Exception e) {
if (docFile.getName() == null) throw new IOException(e);
}
}
private DocumentFile createSAF(@Nullable Context context, String mime, String filename) throws IOException {
DocumentFile res = StoredDirectoryHelper.findFileSAFHelper(context, docTree, filename);
if (res != null && res.exists() && res.isDirectory()) {
if (!res.delete())
throw new IOException("Directory with the same name found but cannot delete");
res = null;
}
if (res == null) {
res = this.docTree.createFile(srcType == null ? DEFAULT_MIME : mime, filename);
if (res == null) throw new IOException("Cannot create the file");
}
return res;
}
private String getLowerCase(String str) {
return str == null ? null : str.toLowerCase();
}
private boolean stringMismatch(String str1, String str2) {
if (str1 == null && str2 == null) return false;
if ((str1 == null) != (str2 == null)) return true;
return !str1.equals(str2);
}
}

View File

@@ -0,0 +1,41 @@
package us.shandian.giga.postprocessing;
import org.schabi.newpipe.streams.Mp4DashReader;
import org.schabi.newpipe.streams.Mp4FromDashWriter;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
class M4aNoDash extends Postprocessing {
M4aNoDash() {
super(false, true, ALGORITHM_M4A_NO_DASH);
}
@Override
boolean test(SharpStream... sources) throws IOException {
// check if the mp4 file is DASH (youtube)
Mp4DashReader reader = new Mp4DashReader(sources[0]);
reader.parse();
switch (reader.getBrands()[0]) {
case 0x64617368:// DASH
case 0x69736F35:// ISO5
return true;
default:
return false;
}
}
@Override
int process(SharpStream out, SharpStream... sources) throws IOException {
Mp4FromDashWriter muxer = new Mp4FromDashWriter(sources[0]);
muxer.setMainBrand(0x4D344120);// binary string "M4A "
muxer.parseSources();
muxer.selectTracks(0);
muxer.build(out);
return OK_RESULT;
}
}

View File

@@ -1,29 +1,27 @@
package us.shandian.giga.postprocessing;
import org.schabi.newpipe.streams.Mp4DashWriter;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
import us.shandian.giga.get.DownloadMission;
/**
* @author kapodamy
*/
class Mp4DashMuxer extends Postprocessing {
Mp4DashMuxer(DownloadMission mission) {
super(mission, 15360 * 1024/* 15 MiB */, true);
}
@Override
int process(SharpStream out, SharpStream... sources) throws IOException {
Mp4DashWriter muxer = new Mp4DashWriter(sources);
muxer.parseSources();
muxer.selectTracks(0, 0);
muxer.build(out);
return OK_RESULT;
}
}
package us.shandian.giga.postprocessing;
import org.schabi.newpipe.streams.Mp4FromDashWriter;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
/**
* @author kapodamy
*/
class Mp4FromDashMuxer extends Postprocessing {
Mp4FromDashMuxer() {
super(true, true, ALGORITHM_MP4_FROM_DASH_MUXER);
}
@Override
int process(SharpStream out, SharpStream... sources) throws IOException {
Mp4FromDashWriter muxer = new Mp4FromDashWriter(sources);
muxer.parseSources();
muxer.selectTracks(0, 0);
muxer.build(out);
return OK_RESULT;
}
}

View File

@@ -1,136 +0,0 @@
package us.shandian.giga.postprocessing;
import android.media.MediaCodec.BufferInfo;
import android.media.MediaExtractor;
import android.media.MediaMuxer;
import android.media.MediaMuxer.OutputFormat;
import android.util.Log;
import static org.schabi.newpipe.BuildConfig.DEBUG;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import us.shandian.giga.get.DownloadMission;
class Mp4Muxer extends Postprocessing {
private static final String TAG = "Mp4Muxer";
private static final int NOTIFY_BYTES_INTERVAL = 128 * 1024;// 128 KiB
Mp4Muxer(DownloadMission mission) {
super(mission, 0, false);
}
@Override
int process(SharpStream out, SharpStream... sources) throws IOException {
File dlFile = mission.getDownloadedFile();
File tmpFile = new File(mission.location, mission.name.concat(".tmp"));
if (tmpFile.exists())
if (!tmpFile.delete()) return DownloadMission.ERROR_FILE_CREATION;
if (!tmpFile.createNewFile()) return DownloadMission.ERROR_FILE_CREATION;
FileInputStream source = null;
MediaMuxer muxer = null;
//noinspection TryFinallyCanBeTryWithResources
try {
source = new FileInputStream(dlFile);
MediaExtractor tracks[] = {
getMediaExtractor(source, mission.offsets[0], mission.offsets[1] - mission.offsets[0]),
getMediaExtractor(source, mission.offsets[1], mission.length - mission.offsets[1])
};
muxer = new MediaMuxer(tmpFile.getAbsolutePath(), OutputFormat.MUXER_OUTPUT_MPEG_4);
int tracksIndex[] = {
muxer.addTrack(tracks[0].getTrackFormat(0)),
muxer.addTrack(tracks[1].getTrackFormat(0))
};
ByteBuffer buffer = ByteBuffer.allocate(512 * 1024);// 512 KiB
BufferInfo info = new BufferInfo();
long written = 0;
long nextReport = NOTIFY_BYTES_INTERVAL;
muxer.start();
while (true) {
int done = 0;
for (int i = 0; i < tracks.length; i++) {
if (tracksIndex[i] < 0) continue;
info.set(0,
tracks[i].readSampleData(buffer, 0),
tracks[i].getSampleTime(),
tracks[i].getSampleFlags()
);
if (info.size >= 0) {
muxer.writeSampleData(tracksIndex[i], buffer, info);
written += info.size;
done++;
}
if (!tracks[i].advance()) {
// EOF reached
tracks[i].release();
tracksIndex[i] = -1;
}
if (written > nextReport) {
nextReport = written + NOTIFY_BYTES_INTERVAL;
super.progressReport(written);
}
}
if (done < 1) break;
}
// this part should not fail
if (!dlFile.delete()) return DownloadMission.ERROR_FILE_CREATION;
if (!tmpFile.renameTo(dlFile)) return DownloadMission.ERROR_FILE_CREATION;
return OK_RESULT;
} finally {
try {
if (muxer != null) {
muxer.stop();
muxer.release();
}
} catch (Exception err) {
if (DEBUG)
Log.e(TAG, "muxer stop/release failed", err);
}
if (source != null) {
try {
source.close();
} catch (IOException e) {
// nothing to do
}
}
// if the operation fails, delete the temporal file
if (tmpFile.exists()) {
//noinspection ResultOfMethodCallIgnored
tmpFile.delete();
}
}
}
private MediaExtractor getMediaExtractor(FileInputStream source, long offset, long length) throws IOException {
MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(source.getFD(), offset, length);
extractor.selectTrack(0);
return extractor;
}
}

View File

@@ -1,164 +1,256 @@
package us.shandian.giga.postprocessing;
import android.os.Message;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.File;
import java.io.IOException;
import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.postprocessing.io.ChunkFileInputStream;
import us.shandian.giga.postprocessing.io.CircularFile;
import us.shandian.giga.service.DownloadManagerService;
public abstract class Postprocessing {
static final byte OK_RESULT = DownloadMission.ERROR_NOTHING;
public static final String ALGORITHM_TTML_CONVERTER = "ttml";
public static final String ALGORITHM_MP4_DASH_MUXER = "mp4D";
public static final String ALGORITHM_MP4_MUXER = "mp4";
public static final String ALGORITHM_WEBM_MUXER = "webm";
public static Postprocessing getAlgorithm(String algorithmName, DownloadMission mission) {
if (null == algorithmName) {
throw new NullPointerException("algorithmName");
} else switch (algorithmName) {
case ALGORITHM_TTML_CONVERTER:
return new TtmlConverter(mission);
case ALGORITHM_MP4_DASH_MUXER:
return new Mp4DashMuxer(mission);
case ALGORITHM_MP4_MUXER:
return new Mp4Muxer(mission);
case ALGORITHM_WEBM_MUXER:
return new WebMMuxer(mission);
/*case "example-algorithm":
return new ExampleAlgorithm(mission);*/
default:
throw new RuntimeException("Unimplemented post-processing algorithm: " + algorithmName);
}
}
/**
* Get a boolean value that indicate if the given algorithm work on the same
* file
*/
public boolean worksOnSameFile;
/**
* Get the recommended space to reserve for the given algorithm. The amount
* is in bytes
*/
public int recommendedReserve;
/**
* the download to post-process
*/
protected DownloadMission mission;
Postprocessing(DownloadMission mission, int recommendedReserve, boolean worksOnSameFile) {
this.mission = mission;
this.recommendedReserve = recommendedReserve;
this.worksOnSameFile = worksOnSameFile;
}
public void run() throws IOException {
File file = mission.getDownloadedFile();
CircularFile out = null;
int result;
long finalLength = -1;
mission.done = 0;
mission.length = file.length();
if (worksOnSameFile) {
ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length];
try {
int i = 0;
for (; i < sources.length - 1; i++) {
sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.offsets[i + 1], "rw");
}
sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.getDownloadedFile().length(), "rw");
int[] idx = {0};
CircularFile.OffsetChecker checker = () -> {
while (idx[0] < sources.length) {
/*
* WARNING: never use rewind() in any chunk after any writing (especially on first chunks)
* or the CircularFile can lead to unexpected results
*/
if (sources[idx[0]].isDisposed() || sources[idx[0]].available() < 1) {
idx[0]++;
continue;// the selected source is not used anymore
}
return sources[idx[0]].getFilePointer() - 1;
}
return -1;
};
out = new CircularFile(file, 0, this::progressReport, checker);
result = process(out, sources);
if (result == OK_RESULT)
finalLength = out.finalizeFile();
} finally {
for (SharpStream source : sources) {
if (source != null && !source.isDisposed()) {
source.dispose();
}
}
if (out != null) {
out.dispose();
}
}
} else {
result = process(null);
}
if (result == OK_RESULT) {
if (finalLength < 0) finalLength = file.length();
mission.done = finalLength;
mission.length = finalLength;
} else {
mission.errCode = DownloadMission.ERROR_UNKNOWN_EXCEPTION;
mission.errObject = new RuntimeException("post-processing algorithm returned " + result);
}
if (result != OK_RESULT && worksOnSameFile) {
//noinspection ResultOfMethodCallIgnored
file.delete();
}
}
/**
* Abstract method to execute the pos-processing algorithm
*
* @param out output stream
* @param sources files to be processed
* @return a error code, 0 means the operation was successful
* @throws IOException if an I/O error occurs.
*/
abstract int process(SharpStream out, SharpStream... sources) throws IOException;
String getArgumentAt(int index, String defaultValue) {
if (mission.postprocessingArgs == null || index >= mission.postprocessingArgs.length) {
return defaultValue;
}
return mission.postprocessingArgs[index];
}
void progressReport(long done) {
mission.done = done;
if (mission.length < mission.done) mission.length = mission.done;
Message m = new Message();
m.what = DownloadManagerService.MESSAGE_PROGRESS;
m.obj = mission;
mission.mHandler.sendMessage(m);
}
}
package us.shandian.giga.postprocessing;
import android.os.Message;
import android.support.annotation.NonNull;
import android.util.Log;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.io.ChunkFileInputStream;
import us.shandian.giga.io.CircularFileWriter;
import us.shandian.giga.io.CircularFileWriter.OffsetChecker;
import us.shandian.giga.service.DownloadManagerService;
import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING;
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD;
import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION;
public abstract class Postprocessing implements Serializable {
static transient final byte OK_RESULT = ERROR_NOTHING;
public transient static final String ALGORITHM_TTML_CONVERTER = "ttml";
public transient static final String ALGORITHM_WEBM_MUXER = "webm";
public transient static final String ALGORITHM_MP4_FROM_DASH_MUXER = "mp4D-mp4";
public transient static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a";
public static Postprocessing getAlgorithm(@NonNull String algorithmName, String[] args) {
Postprocessing instance;
switch (algorithmName) {
case ALGORITHM_TTML_CONVERTER:
instance = new TtmlConverter();
break;
case ALGORITHM_WEBM_MUXER:
instance = new WebMMuxer();
break;
case ALGORITHM_MP4_FROM_DASH_MUXER:
instance = new Mp4FromDashMuxer();
break;
case ALGORITHM_M4A_NO_DASH:
instance = new M4aNoDash();
break;
/*case "example-algorithm":
instance = new ExampleAlgorithm();*/
default:
throw new UnsupportedOperationException("Unimplemented post-processing algorithm: " + algorithmName);
}
instance.args = args;
return instance;
}
/**
* Get a boolean value that indicate if the given algorithm work on the same
* file
*/
public final boolean worksOnSameFile;
/**
* Indicates whether the selected algorithm needs space reserved at the beginning of the file
*/
public final boolean reserveSpace;
/**
* Gets the given algorithm short name
*/
private final String name;
private String[] args;
protected transient DownloadMission mission;
private File tempFile;
Postprocessing(boolean reserveSpace, boolean worksOnSameFile, String algorithmName) {
this.reserveSpace = reserveSpace;
this.worksOnSameFile = worksOnSameFile;
this.name = algorithmName;// for debugging only
}
public void setTemporalDir(@NonNull File directory) {
long rnd = (int) (Math.random() * 100000f);
tempFile = new File(directory, rnd + "_" + System.nanoTime() + ".tmp");
}
public void cleanupTemporalDir() {
if (tempFile != null && tempFile.exists()) {
//noinspection ResultOfMethodCallIgnored
tempFile.delete();
}
}
public void run(DownloadMission target) throws IOException {
this.mission = target;
CircularFileWriter out = null;
int result;
long finalLength = -1;
mission.done = 0;
mission.length = mission.storage.length();
if (worksOnSameFile) {
ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length];
try {
int i = 0;
for (; i < sources.length - 1; i++) {
sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i], mission.offsets[i + 1]);
}
sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i]);
if (test(sources)) {
for (SharpStream source : sources) source.rewind();
OffsetChecker checker = () -> {
for (ChunkFileInputStream source : sources) {
/*
* WARNING: never use rewind() in any chunk after any writing (especially on first chunks)
* or the CircularFileWriter can lead to unexpected results
*/
if (source.isClosed() || source.available() < 1) {
continue;// the selected source is not used anymore
}
return source.getFilePointer() - 1;
}
return -1;
};
out = new CircularFileWriter(mission.storage.getStream(), tempFile, checker);
out.onProgress = this::progressReport;
out.onWriteError = (err) -> {
mission.psState = 3;
mission.notifyError(ERROR_POSTPROCESSING_HOLD, err);
try {
synchronized (this) {
while (mission.psState == 3)
wait();
}
} catch (InterruptedException e) {
// nothing to do
Log.e(this.getClass().getSimpleName(), "got InterruptedException");
}
return mission.errCode == ERROR_NOTHING;
};
result = process(out, sources);
if (result == OK_RESULT)
finalLength = out.finalizeFile();
} else {
result = OK_RESULT;
}
} finally {
for (SharpStream source : sources) {
if (source != null && !source.isClosed()) {
source.close();
}
}
if (out != null) {
out.close();
}
if (tempFile != null) {
//noinspection ResultOfMethodCallIgnored
tempFile.delete();
tempFile = null;
}
}
} else {
result = test() ? process(null) : OK_RESULT;
}
if (result == OK_RESULT) {
if (finalLength != -1) {
mission.done = finalLength;
mission.length = finalLength;
}
} else {
mission.errCode = ERROR_UNKNOWN_EXCEPTION;
mission.errObject = new RuntimeException("post-processing algorithm returned " + result);
}
if (result != OK_RESULT && worksOnSameFile) mission.storage.delete();
this.mission = null;
}
/**
* Test if the post-processing algorithm can be skipped
*
* @param sources files to be processed
* @return {@code true} if the post-processing is required, otherwise, {@code false}
* @throws IOException if an I/O error occurs.
*/
boolean test(SharpStream... sources) throws IOException {
return true;
}
/**
* Abstract method to execute the post-processing algorithm
*
* @param out output stream
* @param sources files to be processed
* @return a error code, 0 means the operation was successful
* @throws IOException if an I/O error occurs.
*/
abstract int process(SharpStream out, SharpStream... sources) throws IOException;
String getArgumentAt(int index, String defaultValue) {
if (args == null || index >= args.length) {
return defaultValue;
}
return args[index];
}
private void progressReport(long done) {
mission.done = done;
if (mission.length < mission.done) mission.length = mission.done;
Message m = new Message();
m.what = DownloadManagerService.MESSAGE_PROGRESS;
m.obj = mission;
mission.mHandler.sendMessage(m);
}
@NonNull
@Override
public String toString() {
StringBuilder str = new StringBuilder();
str.append("name=").append(name).append('[');
if (args != null) {
for (String arg : args) {
str.append(", ");
str.append(arg);
}
str.delete(0, 1);
}
return str.append(']').toString();
}
}

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