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

Compare commits

...

609 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
Tobias Groza
1670751c94 Merge pull request #2312 from TeamNewPipe/release_v0.16.2
Release v0.16.2
2019-05-31 23:30:39 +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
Tobias Groza
0c354c4fdb Remove old strings
Remove strings which have been deleted from the English strings file during development, but were translated via Weblate, which failed to pull and push our upstream repo.
2019-05-27 00:11:37 +02:00
Tobias Groza
bd7413119a Merge branch 'weblate' into release_v0.16.2
Update translations
2019-05-26 22:58:53 +02:00
Davide Palma
f26915aab6 Translated using Weblate (Italian)
Currently translated at 99.8% (442 of 443 strings)
2019-05-25 04:49:13 +02:00
Tobias Groza
f6068dc69d Translated using Weblate (German)
Currently translated at 100.0% (443 of 443 strings)
2019-05-25 04:49:11 +02:00
Marc Riera
bd2c65cd94 Translated using Weblate (Catalan)
Currently translated at 96.8% (429 of 443 strings)
2019-05-15 11:49:29 +02:00
Tobias Groza
4777910644 Update changelog for 0.16.2 2019-05-14 23:17:04 +02:00
Tobias Groza
66b26e52ce Update extractor version 2019-05-14 23:16:43 +02:00
Tolstovka
a758267d72 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (443 of 443 strings)
2019-05-13 10:53:20 +02:00
Tolstovka
72eae64698 Translated using Weblate (Russian)
Currently translated at 100.0% (443 of 443 strings)
2019-05-13 10:53:18 +02:00
84436
d63c18f7f0 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (443 of 443 strings)
2019-05-10 10:19:35 +02:00
Eduardo Caron
b05e3ca8d8 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (443 of 443 strings)
2019-05-10 10:17:50 +02:00
gabriellluz
0a88100b67 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (443 of 443 strings)
2019-05-08 16:49:07 +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
abvgeej
55d6825f63 Translated using Weblate (Russian)
Currently translated at 99.5% (441 of 443 strings)
2019-05-06 14:48:53 +02:00
Jonathan Tavares
e4f9af7076 Translated using Weblate (Portuguese (Brazil))
Currently translated at 98.6% (437 of 443 strings)
2019-05-06 14:48:49 +02:00
Tobias Groza
ff52fe4884 Add changelog for NewPipe 0.16.2 2019-05-03 07:58:46 +02:00
Snow K1ng
0ab29b7c1f Translated using Weblate (Romanian)
Currently translated at 75.8% (336 of 443 strings)
2019-04-29 13:48:19 +02:00
yunna
d742ed7b65 Translated using Weblate (Japanese)
Currently translated at 100.0% (443 of 443 strings)
2019-04-29 13:46:50 +02:00
Jesper Hertel
a684e380b7 Translated using Weblate (Danish)
Currently translated at 98.9% (438 of 443 strings)
2019-04-29 13:45:08 +02:00
AB
c90feaf3db Translated using Weblate (Ukrainian)
Currently translated at 100.0% (443 of 443 strings)
2019-04-28 21:49:00 +02:00
Chandra Mohan Jha
460610f672 Translated using Weblate (Hindi)
Currently translated at 77.2% (342 of 443 strings)
2019-04-28 21:49:00 +02:00
Jesper Hertel
46511533aa Translated using Weblate (Danish)
Currently translated at 88.7% (393 of 443 strings)
2019-04-28 21:48:59 +02:00
Tobias Groza
40efed6580 Update version code and name to NewPipe 0.16.2 2019-04-28 03:34:47 +02:00
Tobias Groza
78b1c1eb4a Merge remote-tracking branch 'TeamNewPipe/master' into dev 2019-04-28 03:34:38 +02:00
Tobias Groza
5a59a3dd50 Merge pull request #2311 from Redirion/FixSubs
Readd CustomTrackSelector
2019-04-28 02:29:20 +02:00
Robin
0763280196 Readd CustomTrackSelector 2019-04-28 01:45:19 +02: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
Chandra Mohan Jha
e7d0685ebc Translated using Weblate (Hindi)
Currently translated at 74.5% (330 of 443 strings)
2019-04-27 09:58:33 +02:00
sXp
aadbfe1eed Translated using Weblate (Hindi)
Currently translated at 74.5% (330 of 443 strings)
2019-04-27 09:58:31 +02:00
Chandra Mohan Jha
4a54fbb872 Translated using Weblate (Hindi)
Currently translated at 74.5% (330 of 443 strings)
2019-04-27 09:58:09 +02:00
sXp
8b1836d699 Translated using Weblate (Hindi)
Currently translated at 74.5% (330 of 443 strings)
2019-04-27 09:58:08 +02:00
Chandra Mohan Jha
c47d4fd35a Translated using Weblate (Hindi)
Currently translated at 74.5% (330 of 443 strings)
2019-04-27 09:58:08 +02:00
pjammo
a58af1275c Translated using Weblate (Italian)
Currently translated at 99.5% (441 of 443 strings)
2019-04-26 19:48:45 +02:00
Marc Riera
1d63b39553 Translated using Weblate (Catalan)
Currently translated at 91.6% (406 of 443 strings)
2019-04-26 19:48:44 +02:00
Jesper Hertel
c300d52b29 Translated using Weblate (Danish)
Currently translated at 87.1% (386 of 443 strings)
2019-04-25 14:48:56 +02:00
Sören Strecke
5314e275bc Translated using Weblate (German)
Currently translated at 100.0% (443 of 443 strings)
2019-04-24 11:37:07 +02:00
yunna
08f8b9770a Translated using Weblate (Japanese)
Currently translated at 100.0% (443 of 443 strings)
2019-04-23 22:48:54 +02:00
YONGLE
a9c64b2fec Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (443 of 443 strings)
2019-04-23 22:48:50 +02:00
sherlock
1edfa78a05 removed the gena strings. 2019-04-17 16:45:40 +05:30
Kristjan Räts
1df8af35d4 Translated using Weblate (Estonian)
Currently translated at 100.0% (443 of 443 strings)
2019-04-16 21:05:04 +02:00
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
Tobias Groza
049cd2d236 Merge pull request #1969 from nv95/close_button
Show close button when playing completed
2019-04-14 11:47:33 +02:00
Tobias Groza
e99714eba6 Merge remote-tracking branch 'TeamNewPipe/dev' into close_button 2019-04-13 18:18:17 +02:00
Vasiliy
4e1423d224 Implement playback state management 2019-04-13 13:34:36 +03:00
Lambda Monad
4751075e87 Translated using Weblate (Indonesian)
Currently translated at 99.3% (440 of 443 strings)
2019-04-12 21:04:47 +02:00
Tobias Groza
416e0fb609 Merge pull request #2271 from Stypox/dev
Add "share at current time" button to the main video player
2019-04-11 19:18:52 +02:00
minsk21
f96a371464 Translated using Weblate (Belarusian)
Currently translated at 74.5% (330 of 443 strings)
2019-04-10 04:04:47 +02:00
___
e423192265 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (443 of 443 strings)
2019-04-09 03:16:34 +02:00
minsk21
e9444e058c Translated using Weblate (Belarusian)
Currently translated at 74.3% (329 of 443 strings)
2019-04-09 03:16:28 +02:00
Yaron Shahrabani
037632fbf0 Translated using Weblate (Hebrew)
Currently translated at 100.0% (443 of 443 strings)
2019-04-07 15:59:53 +02:00
Yoav
6cdea85a49 Translated using Weblate (Hebrew)
Currently translated at 100.0% (443 of 443 strings)
2019-04-07 15:59:49 +02:00
Yaron Shahrabani
f86d755890 Translated using Weblate (Hebrew)
Currently translated at 100.0% (443 of 443 strings)
2019-04-07 15:59:49 +02:00
Stypox
98cc97251a Merge branch 'dev' into dev 2019-04-07 08:18:39 +02:00
Tobias Groza
562f414e3e Merge pull request #2237 from epitron/dev
Fixed English translations for Play/Enqueue commands
2019-04-06 23:32:05 +02:00
Tobias Groza
1afc301432 Merge branch 'dev' into dev 2019-04-06 23:23:15 +02:00
Yaron Shahrabani
115b44585b Translated using Weblate (Hebrew)
Currently translated at 100.0% (443 of 443 strings)
2019-04-06 21:10:05 +02:00
artik banana
3ff47623d5 Translated using Weblate (Hebrew)
Currently translated at 100.0% (443 of 443 strings)
2019-04-06 21:10:03 +02:00
Yaron Shahrabani
943e03f9d8 Translated using Weblate (Hebrew)
Currently translated at 100.0% (443 of 443 strings)
2019-04-06 21:10:03 +02:00
AB
f7a534a0d0 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (443 of 443 strings)
2019-04-06 20:59:53 +02:00
Yaron Shahrabani
72e30d8e40 Translated using Weblate (Hebrew)
Currently translated at 100.0% (443 of 443 strings)
2019-04-06 20:59:50 +02:00
Yehuda Levy
eb265300fc Translated using Weblate (Hebrew)
Currently translated at 100.0% (443 of 443 strings)
2019-04-06 20:59:48 +02:00
artik banana
704b8f61dd Translated using Weblate (Hebrew)
Currently translated at 100.0% (443 of 443 strings)
2019-04-06 20:59:47 +02:00
Stypox
40957c445f Implemented share button in MainVideoPlayer
Android Studio also decided to change the indentation of some lines
2019-04-06 20:27:13 +02:00
Stypox
8eead9fda2 Add share button to main player layout
Placed under "more options"
2019-04-06 20:21:32 +02:00
Stypox
aadc8168be Remove share utilities from BaseStateFragment
Replaced by ShareUtils
2019-04-06 20:17:04 +02:00
Stypox
cb33f04bfc Add ShareUtils class to share videos or open urls in browser. 2019-04-06 20:11:23 +02:00
Florian
82cb71bf3f Translated using Weblate (French)
Currently translated at 99.8% (442 of 443 strings)
2019-04-06 09:53:58 +02:00
Stjepan
c37b88a239 Translated using Weblate (Croatian)
Currently translated at 99.3% (440 of 443 strings)
2019-04-06 09:53:57 +02:00
___
de59bf695d Translated using Weblate (Ukrainian)
Currently translated at 99.8% (442 of 443 strings)
2019-04-06 09:53:55 +02:00
Tobias Groza
592627b013 Merge pull request #2247 from yausername/preferredTabState
preferred tab based on selected state, fixes #2238
2019-04-04 16:16:51 +02:00
Tobias Groza
9ed1fb2588 Merge branch 'dev' into preferredTabState 2019-04-04 15:22:57 +02:00
Tobias Groza
8232a92653 Translated using Weblate (Romanian)
Currently translated at 75.6% (335 of 443 strings)
2019-04-03 17:04:43 +02:00
Tobias Groza
09eeaa92d1 Merge pull request #2207 from yausername/timestampClickFix
seek on clicking timestamp links in comments
2019-04-03 16:04:31 +02:00
Igor Nedoboy
1e4b1a2c70 Translated using Weblate (Russian)
Currently translated at 100.0% (443 of 443 strings)
2019-03-31 22:58:10 +02:00
Marco Ieni
0a1e7a7c86 Translated using Weblate (Italian)
Currently translated at 99.3% (440 of 443 strings)
2019-03-31 22:16:38 +02:00
nautilusx
e5f3b2bf6e Translated using Weblate (German)
Currently translated at 100.0% (443 of 443 strings)
2019-03-31 22:16:37 +02:00
Florian
29183c10ff Translated using Weblate (French)
Currently translated at 99.1% (439 of 443 strings)
2019-03-31 22:16:37 +02:00
Stjepan
46b8bdace7 Translated using Weblate (Croatian)
Currently translated at 99.3% (440 of 443 strings)
2019-03-31 22:16:36 +02:00
Igor Nedoboy
9b6924ec9f Translated using Weblate (Russian)
Currently translated at 100.0% (443 of 443 strings)
2019-03-31 22:16:19 +02:00
Stjepan
c69de107e5 Translated using Weblate (Croatian)
Currently translated at 68.8% (305 of 443 strings)
2019-03-29 00:36:30 +01:00
Igor Nedoboy
c4d451e420 Translated using Weblate (Russian)
Currently translated at 100.0% (443 of 443 strings)
2019-03-29 00:36:25 +01:00
sherlockbeard
be6bce5771 Translated using Weblate (Hindi)
Currently translated at 74.3% (329 of 443 strings)
2019-03-27 21:04:26 +01:00
TobiGr
9bcccc87e6 Update README
Add supported services section
Move comment support
2019-03-26 23:36:12 +01:00
Tobias Groza
bf845be727 Merge branch 'dev' into timestampClickFix 2019-03-26 23:20:55 +01:00
Ahanaf Taskin Ar-Rafee
e92a5414d1 Translated using Weblate (Bengali (Bangladesh))
Currently translated at 30.5% (135 of 443 strings)
2019-03-25 06:38:50 +01:00
yausername
657125f43c save selected tab sate in stream detail fragment, fixes #2238 2019-03-24 06:31:28 +05:30
Christian Schabesberger
e98f68e93b Merge pull request #2242 from yausername/fixMissingAuthorName
fix empty author endpoint
2019-03-23 23:43:05 +01:00
Florian
bef84e9eec Translated using Weblate (French)
Currently translated at 97.3% (431 of 443 strings)
2019-03-23 20:38:52 +01:00
Le Poisson Libre
6a5f2402c7 Translated using Weblate (French)
Currently translated at 97.3% (431 of 443 strings)
2019-03-23 20:38:49 +01:00
yausername
d6cc6ba144 fix empty author endpoint 2019-03-23 00:22:59 +05:30
yausername
07f8dcb3ca use ellipsis character 2019-03-22 05:56:56 +05:30
yausername
a026143a84 linkify optimizations 2019-03-22 04:57:33 +05:30
Andrea Gelmini
73a5b6738d Translated using Weblate (Italian)
Currently translated at 99.1% (439 of 443 strings)
2019-03-21 18:03:59 +01:00
epitron
3e2b12ae4a Fixed English translations for Play/Enqueue commands 2019-03-21 11:24:52 -04:00
naofum
8073364b7a Translated using Weblate (Japanese)
Currently translated at 100.0% (443 of 443 strings)
2019-03-20 03:14:37 +01:00
Osoitz
b141d96e7c Translated using Weblate (Basque)
Currently translated at 100.0% (443 of 443 strings)
2019-03-20 03:14:36 +01:00
Serdar Sağlam
a86e8b98fe Translated using Weblate (Turkish)
Currently translated at 100.0% (443 of 443 strings)
2019-03-20 03:14:34 +01:00
WaldiS
a7d77716f3 Translated using Weblate (Polish)
Currently translated at 100.0% (443 of 443 strings)
2019-03-18 09:39:23 +01:00
Igor Nedoboy
b017e439b1 Translated using Weblate (Russian)
Currently translated at 100.0% (443 of 443 strings)
2019-03-17 21:06:17 +01:00
ssantos
a7156f665a Translated using Weblate (Portuguese)
Currently translated at 100.0% (443 of 443 strings)
2019-03-16 12:54:18 +01:00
WaldiS
44e34d084e Translated using Weblate (Polish)
Currently translated at 99.8% (442 of 443 strings)
2019-03-16 12:54:18 +01:00
C. Rüdinger
065820ffa4 Translated using Weblate (German)
Currently translated at 100.0% (443 of 443 strings)
2019-03-16 12:54:17 +01:00
Jeff Huang
26991928ae Translated using Weblate (Chinese (Traditional))
Currently translated at 99.8% (442 of 443 strings)
2019-03-16 12:54:16 +01:00
ssantos
f4fa68c390 Translated using Weblate (German)
Currently translated at 100.0% (443 of 443 strings)
2019-03-16 12:54:09 +01:00
Gontzal Manuel Pujana Onaindia
8b86f9ea6d Translated using Weblate (Basque)
Currently translated at 100.0% (443 of 443 strings)
2019-03-14 19:00:35 +01:00
Christian Schabesberger
48f966e7db add changelog for 730 2019-03-14 09:24:02 +01:00
Christian Schabesberger
d8746dc592 hotfix decrypt function and move on to version v0.16.1 2019-03-14 09:20:37 +01:00
Ritvik Saraf
6421d3017e seek on clicking timestamp links in comments 2019-03-13 07:01:24 +05:30
Tobias Groza
f9e771f8f7 Merge pull request #2198 from Redirion/directOnBackground
Directplay on Background
2019-03-12 13:42:43 +01:00
Tobias Groza
09456ce421 Merge branch 'dev' into directOnBackground 2019-03-12 13:36:14 +01:00
dextro67
716f7e722b Translated using Weblate (Hindi)
Currently translated at 73.6% (326 of 443 strings)
2019-03-12 04:03:54 +01:00
Der_Floh
ffa4b1483f Translated using Weblate (German)
Currently translated at 100.0% (443 of 443 strings)
2019-03-12 04:03:53 +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
Christian Schabesberger
6651aa924f Merge pull request #2184 from Redirion/exoplayerupdate
Update ExoPlayer to 2.9.6
2019-03-11 20:43:28 +01:00
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
Olexandr Nesterenko
70238fd773 Translated using Weblate (Ukrainian)
Currently translated at 98.4% (436 of 443 strings)
2019-03-10 20:09:04 +01:00
WaldiS
fc4e007cc4 Translated using Weblate (Polish)
Currently translated at 100.0% (443 of 443 strings)
2019-03-10 20:09:00 +01:00
Nathan Follens
b7667ce97a Translated using Weblate (Flemish)
Currently translated at 100.0% (443 of 443 strings)
2019-03-10 20:08:59 +01:00
ssantos
1315da0da7 Translated using Weblate (German)
Currently translated at 100.0% (443 of 443 strings)
2019-03-10 20:08:57 +01:00
84436
c4d8eae547 Translated using Weblate (Vietnamese)
Currently translated at 99.3% (440 of 443 strings)
2019-03-09 17:04:15 +01:00
Pj Pj
b01fc1be62 Translated using Weblate (Telugu)
Currently translated at 28.7% (127 of 443 strings)
2019-03-09 17:04:13 +01:00
marciozomb13
c46e3cf5cb Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.5% (441 of 443 strings)
2019-03-09 17:04:12 +01:00
WaldiS
1a6b915112 Translated using Weblate (Polish)
Currently translated at 100.0% (443 of 443 strings)
2019-03-09 17:04:11 +01:00
Terry Louwers
2f38943488 Translated using Weblate (Dutch)
Currently translated at 100.0% (443 of 443 strings)
2019-03-09 17:04:09 +01:00
Marco Szeto
06711dc6c3 Translated using Weblate (Chinese (Hong Kong))
Currently translated at 31.8% (141 of 443 strings)
2019-03-09 17:04:08 +01:00
Robin
cc7e342ab7 Merge remote-tracking branch 'upstream/dev' into directOnBackground 2019-03-08 23:02:47 +01:00
Robin
5b64743987 Directplay on Background 2019-03-08 22:52:17 +01:00
Robin
a84ad031d9 Merge remote-tracking branch 'upstream/dev' into exoplayerupdate 2019-03-07 16:06:02 +01:00
Tobias Groza
c4c2fe2a9c Merge pull request #1963 from Klearchos-K/dev
Issue #1951 - Search shared text
2019-03-07 15:50:51 +01:00
Tobias Groza
8ccaef454c Merge branch 'dev' into dev 2019-03-07 15:20:42 +01:00
Marian Hanzel
27579dff37 Translated using Weblate (Slovak)
Currently translated at 88.3% (391 of 443 strings)
2019-03-06 22:13:04 +01:00
Nikola Perović
389d08c233 Translated using Weblate (Serbian)
Currently translated at 47.4% (210 of 443 strings)
2019-03-06 22:12:59 +01:00
Michalis Nikolaidis
5412a087fe Translated using Weblate (Greek)
Currently translated at 100.0% (443 of 443 strings)
2019-03-06 22:12:59 +01:00
Konstantinos Giannopoulos
dd0f3ac651 Translated using Weblate (Greek)
Currently translated at 100.0% (443 of 443 strings)
2019-03-06 22:12:58 +01:00
YONGLE
4d4d776e4d Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (443 of 443 strings)
2019-03-06 22:12:51 +01:00
Robin
7877b107c1 Merge branch 'exoplayerupdate' of https://github.com/Redirion/NewPipe into exoplayerupdate 2019-03-06 09:38:17 +01:00
Robin
a2aa0aa9a8 Fix for wrong case after language normalization 2019-03-06 09:37:55 +01:00
Redirion
b3475d30c0 Merge branch 'dev' into exoplayerupdate 2019-03-05 21:44:27 +01:00
Tobias Groza
6484c8d636 Merge pull request #2183 from Redirion/Cleanup
delete unused files
2019-03-05 21:38:19 +01:00
Tobias Groza
587cf554f2 Merge branch 'dev' into Cleanup 2019-03-05 21:30:48 +01:00
Tobias Groza
1d4e4eb6b3 Merge pull request #2178 from Redirion/patch-1
Cache duration String to improve performance of background player
2019-03-05 21:06:22 +01:00
Tobias Groza
31c4ed7d0e Update app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java
Co-Authored-By: Redirion <redirion@web.de>
2019-03-05 20:57:05 +01:00
Robin
7f246b2d3d Removed unused import 2019-03-05 19:54:37 +01:00
Robin
7d68cff700 NOTE for legacy version: Removed Lint markers and completely dropped Jelly Bean workarounds 2019-03-05 19:48:39 +01:00
Robin
4d80bdcc9f Update ExoPlayer to 2.9.6, including httook dependency and deprecations 2019-03-05 18:05:44 +01:00
Robin
8b4a94e5aa delete unused files 2019-03-05 18:01:11 +01:00
Redirion
111ad14ad3 Merge branch 'dev' into patch-1 2019-03-05 17:57:52 +01:00
Tobias Groza
aec3f19d23 Merge pull request #2179 from Redirion/patch-2
Improve performance of getTimeString
2019-03-04 19:16:43 +01:00
Redirion
d8b80f961a Improved performance of getTimeString
This pull requests complements pull request  #2178 by reducing general computational time for the method getTimeString.

On my local machine (Desktop PC with Java) my tests with a sample size of 10000 calls to the method with param 86400001 showed a performance improvement of about 50%.

See sample code below to reproduce:

    private static final StringBuilder stringBuilder = new StringBuilder();
    private static final Formatter stringFormatter = new Formatter(stringBuilder, Locale.getDefault());
    
    public static String getTimeString(int milliSeconds) {
        int seconds = (milliSeconds % 60000) / 1000;
        int minutes = (milliSeconds % 3600000) / 60000;
        int hours = (milliSeconds % 86400000) / 3600000;
        int days = (milliSeconds % (86400000 * 7)) / 86400000;

        stringBuilder.setLength(0);
        return days > 0 ? stringFormatter.format("%d:%02d:%02d:%02d", days, hours, minutes, seconds).toString()
                : hours > 0 ? stringFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString()
                : stringFormatter.format("%02d:%02d", minutes, seconds).toString();
    }
    
    public static String getTimeStringL(int milliSeconds) {
        long seconds = (milliSeconds % 60000L) / 1000L;
        long minutes = (milliSeconds % 3600000L) / 60000L;
        long hours = (milliSeconds % 86400000L) / 3600000L;
        long days = (milliSeconds % (86400000L * 7L)) / 86400000L;

        stringBuilder.setLength(0);
        return days > 0 ? stringFormatter.format("%d:%02d:%02d:%02d", days, hours, minutes, seconds).toString()
                : hours > 0 ? stringFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString()
                : stringFormatter.format("%02d:%02d", minutes, seconds).toString();
    }
    
	public static void main(String[] args) throws Exception {
		final int SAMPLE_SIZE = 25000;
		long[] results = new long[SAMPLE_SIZE];
		for(int i = 0; i < SAMPLE_SIZE; i++) {
			long now = System.nanoTime();
			getTimeString(86400001);
			results[i] = System.nanoTime() - now;
		}
		long sum = 0;
		for(int i = 0; i < SAMPLE_SIZE; i++) {
			sum += results[i];
		}
		System.out.println("Average execution time: " + (sum/SAMPLE_SIZE));
		results = new long[SAMPLE_SIZE];
		for(int i = 0; i < SAMPLE_SIZE; i++) {
			long now = System.nanoTime();
			getTimeStringL(86400001);
			results[i] = System.nanoTime() - now;
		}
		sum = 0;
		for(int i = 0; i < SAMPLE_SIZE; i++) {
			sum += results[i];
		}
		System.out.println("Average execution time: " + (sum/SAMPLE_SIZE));
2019-03-04 15:45:59 +01:00
rimasx
891bb7fa40 Translated using Weblate (Estonian)
Currently translated at 100.0% (443 of 443 strings)
2019-03-04 11:58:31 +01:00
Rex_sa
0e3af45466 Translated using Weblate (Arabic)
Currently translated at 100.0% (443 of 443 strings)
2019-03-04 11:58:24 +01:00
Redirion
6aebbc3109 Cache duration String to improve performance
In VideoPlayer the Duration String is cached effectively by setting it to the playbackSeekBar. As the playbackSeekBar doesn't exist in BackgroundPlayer, using two addition variables will reduce performance impact of notification updates by almost 50% and thus perform similar to VideoPlayer.

This addresses issue #2170
2019-03-04 10:24:08 +01:00
Christian Schabesberger
15eb7f3186 Merge pull request #2023 from dotvirus/enqueue-playlist
Add playlist to queue when long clicking on 'Background'
2019-03-03 20:59:15 +01:00
Christian Schabesberger
fb4cd98014 Merge branch 'dev' into enqueue-playlist 2019-03-03 20:53:17 +01:00
Christian Schabesberger
eb692dea59 Merge pull request #2168 from yausername/commentSizeAndLinks
make links in comments clickable, increase text size
2019-03-03 20:52:11 +01:00
Christian Schabesberger
d8039fb542 Merge branch 'dev' into enqueue-playlist 2019-03-03 20:50:00 +01:00
Christian Schabesberger
5e06d19d77 Merge branch 'dev' into commentSizeAndLinks 2019-03-03 20:46:03 +01:00
Christian Schabesberger
963f390336 Merge pull request #2176 from yausername/fixScrollOnBlank
fixed scroll w/ comments and related streams disabled
2019-03-03 20:41:07 +01:00
Ritvik Saraf
2309e15261 fixed scroll w/ comments and related streams disabled 2019-03-03 18:20:15 +05:30
Ritvik Saraf
8491035b2f updated extractor 2019-03-03 04:34:50 +05:30
Ritvik Saraf
4d4107aefc Merge remote-tracking branch 'upstream/dev' into commentSizeAndLinks 2019-03-03 04:32:19 +05:30
Ritvik Saraf
67d2b9131e handling timestamp links in comments 2019-03-02 05:12:06 +05:30
Christian Schabesberger
da8644168c Merge branch 'master' into dev 2019-03-01 09:53:43 +01:00
Ritvik Saraf
c0004e988a make links in comments clickable, increase text size 2019-03-01 13:28:32 +05:30
Javi
b9187445e0 Translated using Weblate (Spanish)
Currently translated at 100.0% (443 of 443 strings)
2019-02-27 22:28:31 +01:00
Christian Schabesberger
554ebf7ab9 Merge pull request #2161 from Redirion/patch-1
Fixed CheckForNewAppVersionTask being executed when it shouldn't
2019-02-27 22:14:53 +01:00
Redirion
3e54cd7284 Update CheckForNewAppVersionTask.java 2019-02-26 19:33:01 +01:00
Redirion
a7afc23a9a Fixed Asynctask being executed when it shouldn't
#1 check if cancel was called in onPrepare
#2 if we currently don't have a Connection, don't show crash report dialogue to user
2019-02-26 19:23:54 +01:00
abvgeej
fa3a047519 Translated using Weblate (Russian)
Currently translated at 100.0% (443 of 443 strings)
2019-02-25 23:18:20 +01:00
Christian Schabesberger
f24fab0fa2 fix brake when selecting a mediaccc channel form subscription page 2019-02-25 12:24:48 +01:00
Christian Schabesberger
261620fd13 Merge branch 'master' into release_v0.16.0 2019-02-24 23:06:06 +01:00
Christian Schabesberger
9af6effad9 move on to version 0.16.0 2019-02-24 22:52:07 +01:00
Christian Schabesberger
92602916dd merge weblate 2019-02-24 22:51:08 +01:00
Christian Schabesberger
f4d215664e Merge pull request #2117 from nv95/patch1_ui
Small ux improve
2019-02-24 22:34:12 +01:00
Christian Schabesberger
84894a557a Merge branch 'dev' into patch1_ui 2019-02-24 22:27:06 +01:00
Christian Schabesberger
ffad6e4c61 Merge pull request #2148 from yausername/commentsAndRelatedUx
added top padding for comments and related videos
2019-02-24 22:26:46 +01:00
Christian Schabesberger
93b266e6be Merge branch 'dev' into commentsAndRelatedUx 2019-02-24 22:19:47 +01:00
Christian Schabesberger
f92ea28581 Merge pull request #2151 from nv95/fix_leak
Fix AudioManager memory leak
2019-02-24 22:19:27 +01:00
Christian Schabesberger
85213e4b6c Merge branch 'dev' into patch1_ui 2019-02-24 22:18:06 +01:00
Christian Schabesberger
4e46119e18 Merge branch 'dev' into commentsAndRelatedUx 2019-02-24 22:17:24 +01:00
Christian Schabesberger
fdc6e9f1c3 Merge branch 'dev' into fix_leak 2019-02-24 22:12:46 +01:00
Christian Schabesberger
cb20f1e212 Merge pull request #2153 from TeamNewPipe/fix_url
fix invalid channel url for commends
2019-02-24 22:10:40 +01:00
Christian Schabesberger
e3fccd7125 fix invalid channel url for commends 2019-02-24 22:10:16 +01:00
Vasiliy
15142c1ec3 Fix AudioManager memory leak 2019-02-24 10:51:30 +02:00
Nathan Follens
6c7b90e1c3 Translated using Weblate (Flemish)
Currently translated at 100.0% (443 of 443 strings)
2019-02-24 06:18:27 +01:00
Nathan Follens
d8e57144f7 Translated using Weblate (Dutch)
Currently translated at 100.0% (443 of 443 strings)
2019-02-24 06:18:27 +01:00
Ritvik Saraf
49fe8a427a added top padding for comments and related videos 2019-02-24 02:21:11 +05:30
Vasiliy
4587428d13 Merge branch 'dev' into close_button 2019-02-23 13:19:09 +02:00
Vasiliy
5318e77035 Merge branch 'dev' into patch1_ui 2019-02-23 13:18:14 +02:00
Christian Schabesberger
e69c45a246 Merge pull request #1650 from yausername/dev
comments support
2019-02-21 15:50:21 +01:00
Ritvik Saraf
3f87a6d714 Merge remote-tracking branch 'origin/dev' into dev 2019-02-20 05:29:34 +05:30
Ritvik Saraf
841124b1f3 updated extractor, fixed settings padding 2019-02-20 05:24:33 +05:30
Hosted Weblate
257a878ef4 Merge branch 'origin/dev' into Weblate. 2019-02-19 19:18:29 +01:00
84436
07d3f82912 Translated using Weblate (Vietnamese)
Currently translated at 99.3% (440 of 443 strings)
2019-02-19 19:18:26 +01:00
Ali Demirtas
7d3eb4f5a6 Translated using Weblate (Turkish)
Currently translated at 100.0% (443 of 443 strings)
2019-02-19 19:18:12 +01:00
srkrishna
0efcc55373 Translated using Weblate (Tamil)
Currently translated at 34.5% (153 of 443 strings)
2019-02-19 19:18:12 +01:00
Christian Schabesberger
eafceb8a6c Merge branch 'dev' into dev 2019-02-19 17:35:49 +01:00
Christian Schabesberger
5ac8318e71 Merge pull request #2136 from TeamNewPipe/db_doc
add database doc in dia
2019-02-19 16:06:11 +01:00
Christian Schabesberger
7981f9dc91 add database doc in dia 2019-02-19 16:03:10 +01:00
Christian Schabesberger
a9bf04cbc6 Merge pull request #2135 from TeamNewPipe/firetv
add support for search in amazon firetv
2019-02-19 15:00:22 +01:00
Christian Schabesberger
4b5591d884 move firetv utils into utils package 2019-02-19 14:57:49 +01:00
Christian Schabesberger
c08197f025 Merge branch 'dev' into feature/amazonfiretv-search-support 2019-02-19 14:54:48 +01:00
Christian Schabesberger
5d22eabfa3 Merge pull request #2092 from connectety/updated-urls
support for youtube-nocookie.com
2019-02-19 14:30:56 +01:00
Christian Schabesberger
c3a38e384a Merge branch 'dev' into updated-urls 2019-02-19 14:30:34 +01:00
Christian Schabesberger
0d3dd94559 Merge pull request #2120 from Redirion/patch-1
Fix delayed ducking of Audio
2019-02-19 14:30:11 +01:00
Christian Schabesberger
9cdaa37519 Merge branch 'dev' into patch-1 2019-02-19 14:29:34 +01:00
Christian Schabesberger
202a319d69 Merge pull request #2129 from TeamNewPipe/remove-old-player
Remove old player from the manifest
2019-02-19 14:29:02 +01:00
Christian Schabesberger
198384b2ed Merge branch 'dev' into remove-old-player 2019-02-19 14:28:53 +01:00
Christian Schabesberger
9d8b070f1e Merge pull request #2133 from kapodamy/popup-player-fixup
Fix random Popup player crash
2019-02-19 14:28:04 +01:00
dimqua
28ba2d5008 Translated using Weblate (Russian)
Currently translated at 100.0% (443 of 443 strings)
2019-02-19 14:07:27 +01:00
Arthur
37ddd63d27 Translated using Weblate (Russian)
Currently translated at 100.0% (443 of 443 strings)
2019-02-19 14:07:26 +01:00
dimqua
d7f8b8c1e0 Translated using Weblate (Russian)
Currently translated at 97.5% (432 of 443 strings)
2019-02-18 13:28:20 +01:00
Arthur
9e70c5bbea Translated using Weblate (Russian)
Currently translated at 97.5% (432 of 443 strings)
2019-02-18 13:28:19 +01:00
kapodamy
4dd572063e fix crash while switching from popup to fullscreen player, or closing the popup player. 2019-02-17 16:59:35 -03:00
TobiGr
db9cf95648 Remove old player from the manifest
See https://github.com/TeamNewPipe/NewPipe/pull/1884 for more info
2019-02-17 09:52:05 +01:00
Ritvik Saraf
df6bae4712 merged upstream/dev 2019-02-16 02:06:18 +05:30
Ritvik Saraf
56cb8209b8 refactored comments capability 2019-02-16 01:23:26 +05:30
WaldiS
aba9c2e113 Translated using Weblate (Polish)
Currently translated at 100.0% (443 of 443 strings)
2019-02-15 17:24:20 +01:00
Ali Demirtas
3d25008739 Translated using Weblate (Turkish)
Currently translated at 97.5% (432 of 443 strings)
2019-02-14 10:11:03 +01:00
Redirion
9437f057d0 Fix delayed ducking of Audio
Scenario: listening to a video on NewPipe over Bluetooth and a Notification Sound causes audio focus event AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK.

Problem: With the current implementation animateAudio would cause the audio to reach the target volume AFTER the notification sound is played, which is irritating and annoying.

Solution: animateAudio should just be used on focusGain where it is sensible to increase audio gradually. On ducking event the reaction should be immediate.

This very simple fix does this. Please approve.
2019-02-14 09:52:46 +01:00
Emanuele Petriglia
917d6089a7 Translated using Weblate (Italian)
Currently translated at 97.5% (432 of 443 strings)
2019-02-13 19:10:46 +01:00
toanpv
f19e99e3ad Translated using Weblate (Vietnamese)
Currently translated at 74.9% (332 of 443 strings)
2019-02-13 12:11:48 +01:00
Allan Nordhøy
2d0fb05fa6 Translated using Weblate (Norwegian Bokmål)
Currently translated at 96.8% (429 of 443 strings)
2019-02-13 12:11:46 +01:00
Chris
c617f2dfad Translated using Weblate (Malay)
Currently translated at 100.0% (443 of 443 strings)
2019-02-13 12:11:46 +01:00
naofum
747c5fc89a Translated using Weblate (Japanese)
Currently translated at 96.6% (428 of 443 strings)
2019-02-13 12:11:14 +01:00
Chris
4f662ef203 Translated using Weblate (Indonesian)
Currently translated at 100.0% (443 of 443 strings)
2019-02-13 12:11:13 +01:00
Le Poisson Libre
eb274ad88f Translated using Weblate (French)
Currently translated at 91.9% (407 of 443 strings)
2019-02-13 12:11:09 +01:00
Chris
bd7b41be9b Translated using Weblate (English)
Currently translated at 99.1% (439 of 443 strings)
2019-02-13 12:11:07 +01:00
Chris
076720bfff Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (443 of 443 strings)
2019-02-13 12:11:06 +01:00
Osoitz
2aadae407e Translated using Weblate (Basque)
Currently translated at 100.0% (443 of 443 strings)
2019-02-13 12:11:05 +01:00
Rex_sa
b16bb07774 Translated using Weblate (Arabic)
Currently translated at 100.0% (443 of 443 strings)
2019-02-13 12:11:00 +01:00
naofum
81dd083388 Translated using Weblate (Japanese)
Currently translated at 98.4% (436 of 443 strings)
2019-02-12 16:20:23 +01:00
Chris
92d4cef1e2 Translated using Weblate (Japanese)
Currently translated at 98.4% (436 of 443 strings)
2019-02-12 16:20:22 +01:00
Vasiliy
0cb5197ccf Merge remote-tracking branch 'upstream/dev' into patch1_ui 2019-02-12 10:21:03 +02:00
C. Rüdinger
d2cd79dcf5 Translated using Weblate (German)
Currently translated at 99.8% (442 of 443 strings)
2019-02-10 23:34:08 +01:00
Hosted Weblate
0ef8b391e4 Merge branch 'origin/dev' into Weblate. 2019-02-09 13:57:02 +01:00
Eduardo Caron
0fbd105977 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (443 of 443 strings)
2019-02-09 13:57:02 +01:00
ssantos
5c23af541e Translated using Weblate (Portuguese)
Currently translated at 100.0% (443 of 443 strings)
2019-02-09 13:56:57 +01:00
WaldiS
db021ff78b Translated using Weblate (Polish)
Currently translated at 100.0% (443 of 443 strings)
2019-02-09 13:56:57 +01:00
Allan Nordhøy
31294c6f54 Translated using Weblate (Norwegian Bokmål)
Currently translated at 96.8% (429 of 443 strings)
2019-02-09 13:56:56 +01:00
JS Ahn
03d3495759 Translated using Weblate (Korean)
Currently translated at 78.6% (348 of 443 strings)
2019-02-09 13:56:56 +01:00
ssantos
4644e105a8 Translated using Weblate (German)
Currently translated at 100.0% (443 of 443 strings)
2019-02-09 13:56:53 +01:00
Tobias Groza
af5002508d Translated using Weblate (German)
Currently translated at 100.0% (443 of 443 strings)
2019-02-09 13:56:52 +01:00
Nathan Follens
9416d2b9c7 Translated using Weblate (Flemish)
Currently translated at 100.0% (443 of 443 strings)
2019-02-09 13:56:52 +01:00
Nathan Follens
f60f5d5a6c Translated using Weblate (Dutch)
Currently translated at 100.0% (443 of 443 strings)
2019-02-09 13:56:46 +01:00
ezjerry liao
26d00f87a8 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (443 of 443 strings)
2019-02-09 13:56:44 +01:00
Nathan Follens
6c33ea423c Translated using Weblate (Norwegian Bokmål)
Currently translated at 96.6% (428 of 443 strings)
2019-02-09 13:56:40 +01:00
Connectety-W
ad354aca4e added support for youtube-nocookie.com 2019-02-09 12:30:14 +01:00
Christian Schabesberger
ce6a5ebf0a Merge pull request #2107 from chin123/fix_padding_settings
Fix padding in settings layout (Fixes #1866)
2019-02-09 09:08:20 +01:00
Yaron Shahrabani
d66ef38e8d Translated using Weblate (Hebrew)
Currently translated at 100.0% (443 of 443 strings)
2019-02-09 03:38:29 +01:00
Chinmaya Krishnan Mahesh
1b6c49f621 Fix padding in settings layout (Fixes #1866) 2019-02-08 17:33:35 -06:00
Allan Nordhøy
0262c83815 Translated using Weblate (English)
Currently translated at 98.9% (438 of 443 strings)
2019-02-08 21:38:01 +01:00
Allan Nordhøy
c212934130 Translated using Weblate (Norwegian Bokmål)
Currently translated at 96.8% (429 of 443 strings)
2019-02-07 15:12:26 +01:00
Yaron Shahrabani
ac30e47e59 Translated using Weblate (Hebrew)
Currently translated at 98.2% (435 of 443 strings)
2019-02-06 20:36:39 +01:00
Yehuda Levy
d640057453 Translated using Weblate (Hebrew)
Currently translated at 90.1% (399 of 443 strings)
2019-02-06 09:17:45 +01:00
Yaron Shahrabani
0cea136d4d Translated using Weblate (Hebrew)
Currently translated at 90.1% (399 of 443 strings)
2019-02-06 09:17:44 +01:00
Hosted Weblate
9b3d93e734 Merge branch 'origin/dev' into Weblate. 2019-02-06 08:40:24 +01:00
Emin Tufan Çetin
ca3782ac62 Translated using Weblate (Turkish)
Currently translated at 100.0% (441 of 441 strings)
2019-02-06 08:40:24 +01:00
dimqua
dc9ffb7fd5 Translated using Weblate (Russian)
Currently translated at 96.8% (427 of 441 strings)
2019-02-06 08:40:23 +01:00
WaldiS
b21b3a55fe Translated using Weblate (Polish)
Currently translated at 100.0% (441 of 441 strings)
2019-02-06 08:40:21 +01:00
Mostafa Ahangarha
e62bd85a7a Translated using Weblate (Persian)
Currently translated at 57.8% (255 of 441 strings)
2019-02-06 08:40:19 +01:00
Michael Moroni
fed221b008 Translated using Weblate (Italian)
Currently translated at 100.0% (441 of 441 strings)
2019-02-06 08:40:18 +01:00
Terry Louwers
935298d159 Translated using Weblate (Dutch)
Currently translated at 100.0% (441 of 441 strings)
2019-02-06 08:40:17 +01:00
Nathan Follens
909919250c Translated using Weblate (Dutch)
Currently translated at 100.0% (441 of 441 strings)
2019-02-06 08:40:14 +01:00
Emil Tang Kristensen
d24f278f7c Translated using Weblate (Danish)
Currently translated at 85.7% (378 of 441 strings)
2019-02-06 08:40:14 +01:00
赖文胜
ca408a495c Translated using Weblate (Chinese (Simplified))
Currently translated at 97.1% (428 of 441 strings)
2019-02-06 08:39:42 +01:00
Sérgio Marques
ee99719137 Translated using Weblate (Portuguese)
Currently translated at 100.0% (441 of 441 strings)
2019-02-06 08:39:36 +01:00
Christian Schabesberger
5fb0729eae Merge pull request #2083 from TeamNewPipe/ccc
Add MediaCCC support
2019-02-05 18:28:22 +01:00
Christian Schabesberger
a1db3187cd Merge branch 'dev' into ccc 2019-02-05 18:28:17 +01:00
Christian Schabesberger
488a3cf1ff Merge pull request #2084 from TeamNewPipe/splash
add splash screen
2019-02-05 18:27:15 +01:00
Christian Schabesberger
1253a620a4 Merge pull request #2094 from def14nt/add-instable-warning
Added a warning about instability
2019-02-05 18:27:02 +01:00
Christian Schabesberger
64547fc4a7 remove splash for older android 2019-02-05 18:24:34 +01:00
Emil Tang Kristensen
d643d140cf Added translation using Weblate (Danish) 2019-02-04 20:57:57 +01:00
Ali Demirtas
222e2e3242 Translated using Weblate (Turkish)
Currently translated at 100.0% (441 of 441 strings)
2019-02-04 12:37:19 +01:00
Ali Demirtas
ea7b6daf26 Translated using Weblate (Turkish)
Currently translated at 100.0% (441 of 441 strings)
2019-02-04 02:10:01 +01:00
DefiantDog66
14cfab495d Added a warning about instability
This is a follow up to TeamNewPipe#2069. I've removed the "WARNING" from the second one as it looks a bit silly to have the same thing twice, when the user is already attracted to the first one, which means they will most probably read the second warning.

I can revert this if you don't like it.
2019-02-03 19:47:51 +00:00
Alec Holmes
ed4b4a1a3c Updated search fragment to be amazon fire tv friendly 2019-02-01 13:02:28 +00:00
Hosted Weblate
511c552188 Merge branch 'origin/dev' into Weblate. 2019-02-01 09:37:22 +01:00
ssantos
8c650219eb Translated using Weblate (Portuguese)
Currently translated at 100.0% (441 of 441 strings)
2019-02-01 09:37:22 +01:00
WaldiS
189b779abe Translated using Weblate (Polish)
Currently translated at 100.0% (441 of 441 strings)
2019-02-01 09:37:18 +01:00
Michael Moroni
c26074b184 Translated using Weblate (Italian)
Currently translated at 100.0% (441 of 441 strings)
2019-02-01 09:37:16 +01:00
Emanuele Petriglia
6c70779ff1 Translated using Weblate (Italian)
Currently translated at 100.0% (441 of 441 strings)
2019-02-01 09:37:15 +01:00
Yaron Shahrabani
73897481ac Translated using Weblate (Hebrew)
Currently translated at 83.7% (369 of 441 strings)
2019-02-01 09:37:11 +01:00
C. Rüdinger
a62c523ed5 Translated using Weblate (German)
Currently translated at 100.0% (441 of 441 strings)
2019-02-01 09:37:08 +01:00
Florian
0f46f57b52 Translated using Weblate (French)
Currently translated at 89.8% (396 of 441 strings)
2019-02-01 09:37:08 +01:00
Nathan Follens
bc4d65a20c Translated using Weblate (Dutch)
Currently translated at 86.4% (381 of 441 strings)
2019-02-01 09:37:07 +01:00
Vojtěch Šamla
f1e9a44ad9 Translated using Weblate (Czech)
Currently translated at 88.4% (390 of 441 strings)
2019-02-01 09:37:06 +01:00
Marc Riera
61972c4719 Translated using Weblate (Catalan)
Currently translated at 92.1% (406 of 441 strings)
2019-02-01 09:37:05 +01:00
Almin Agic
9183c9c220 Translated using Weblate (Bosnian (latin))
Currently translated at 4.3% (19 of 441 strings)
2019-02-01 09:37:03 +01:00
Rex_sa
1ce59f13ff Translated using Weblate (Arabic)
Currently translated at 87.8% (387 of 441 strings)
2019-02-01 09:37:00 +01:00
Christian Schabesberger
1bac5db6d5 add splash 2019-01-31 18:13:00 +01:00
Christian Schabesberger
58f2c4d8e6 Merge branch 'dev' into ccc 2019-01-31 16:48:22 +01:00
Robin
2c2c61b2fc Add Artist and Duration to MediaDescription 2019-01-31 16:47:33 +01:00
Christian Schabesberger
98b04a4a83 Merge branch 'dev' into ccc 2019-01-31 14:28:10 +01:00
Christian Schabesberger
3ac64c44f4 Merge pull request #1900 from anoymouserver/patch-1
support for invidio.us links
2019-01-31 14:10:10 +01:00
Christian Schabesberger
3375847302 Merge branch 'dev' into patch-1 2019-01-31 14:02:24 +01:00
Christian Schabesberger
14043c86f5 fix backstack issue with mediaccc 2019-01-31 13:24:02 +01:00
Jeff Huang
030117780a Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (441 of 441 strings)
2019-01-31 08:57:07 +01:00
Benedikt Freisen
971c9fe5a1 Translated using Weblate (German)
Currently translated at 100.0% (441 of 441 strings)
2019-01-30 08:49:53 +01:00
Ritvik Saraf
77c6d3d576 merged upstream/dev 2019-01-29 22:32:58 +05:30
Christian Schabesberger
6edbfe2a6f add content filter to mediaccc 2019-01-29 17:20:30 +01:00
Christian Schabesberger
d0a3125df4 fox ogg 2019-01-29 16:13:46 +01:00
Christian Schabesberger
d8c76d4c21 add conferences 2019-01-29 15:39:18 +01:00
Christian Schabesberger
a1cc0897df make frontend not crash on scrolling on ccc search 2019-01-29 15:39:18 +01:00
Christian Schabesberger
e88a90f242 add theming to mediaccc 2019-01-29 15:39:18 +01:00
Christian Schabesberger
1ae54f6f8c further compatiblity fix for meadic.ccc 2019-01-29 15:39:18 +01:00
Hosted Weblate
8a4cb484fa Merge branch 'origin/dev' into Weblate. 2019-01-29 14:17:45 +01:00
Licaon Kter
93defbf341 Translated using Weblate (Romanian)
Currently translated at 84.1% (322 of 383 strings)
2019-01-29 14:17:45 +01:00
Allan Nordhøy
ca1089da9a Translated using Weblate (Norwegian Bokmål)
Currently translated at 97.7% (374 of 383 strings)
2019-01-29 14:17:44 +01:00
Rex_sa
dd07c7db0e Translated using Weblate (Arabic)
Currently translated at 100.0% (383 of 383 strings)
2019-01-29 14:17:42 +01:00
Almin Agic
dfe932e37a Added translation using Weblate (Bosnian (latin)) 2019-01-29 14:17:38 +01:00
anoy
3ac4899b96 Merge branch 'dev' into patch-1 2019-01-28 21:19:21 +01:00
Ritvik Saraf
86d619be30 merged upstream/dev 2019-01-19 15:48:05 +05:30
Ritvik Saraf
fec7672598 updated extractor for url decryption fix 2019-01-19 15:26:31 +05:30
dotvirus
c0aca723da Update PlaylistFragment.java 2019-01-18 23:58:24 +01:00
dotvirus
28e5ee51ec Add playlist to queue when long click on 'Background' 2019-01-18 23:16:02 +01:00
Vasily
7ab10aeb80 Remove search history items using swipe 2018-12-29 20:55:24 +02:00
Vasily
9316962e47 Clear history option menu item 2018-12-29 20:55:10 +02:00
Vasily
505c528194 Show close button when playing completed 2018-12-27 16:51:48 +02:00
Ritvik Saraf
559c397b2f more NPE fix 2018-12-25 20:17:56 +05:30
Ritvik Saraf
646698f1ed fixed NPE in soundcloud 2018-12-25 19:59:03 +05:30
Ritvik Saraf
c9b938ae55 readded animations 2018-12-25 15:36:15 +05:30
Klearchos-K
e4409e8ea4 Update RouterActivity.java
Add dunction handleText() for #1951 issue
2018-12-24 20:11:21 +02:00
Ritvik Saraf
990c220fa0 updated extractor 2018-12-23 09:44:42 +05:30
Ritvik Saraf
f19cfb75e6 merged upstream/dev 2018-12-20 08:51:44 +05:30
Ritvik Saraf
ceaacc771d removed jerky animations 2018-12-19 10:58:59 +05:30
Ritvik Saraf
48067e3285 up next text alignment 2018-12-17 10:03:04 +05:30
Ritvik Saraf
222c8fdb62 tablet ui support for comments 2018-12-09 03:21:55 +05:30
Ritvik Saraf
1a62b9a161 removed dislike button, added comment published time 2018-12-08 20:32:28 +05:30
Ritvik Saraf
ccb9bceecc removed unused imports 2018-12-07 08:42:05 +05:30
Ritvik Saraf
c1a67ff1f8 minor scrolling fix and ellipsize fix 2018-12-07 06:45:33 +05:30
Ritvik Saraf
ff90f257cc removed useless log statement 2018-12-04 23:37:02 +05:30
Ritvik Saraf
9b84046865 merged upstream/dev 2018-12-04 23:19:57 +05:30
anoy
5ac17ee6b8 added support for invidio.us links 2018-11-23 19:18:34 +01:00
Ritvik Saraf
fa5896ee5b fixed screen rotation for viewpager 2018-10-19 18:44:03 +05:30
Ritvik Saraf
9fc38b5bb8 improved fling behavior, added tab indicator dots, added next video in related videos 2018-10-17 00:23:02 +05:30
Ritvik Saraf
cf3e53eb71 update notify on dataset change 2018-10-02 21:30:11 +05:30
Ritvik Saraf
b8865e925d added content setting to disable comments 2018-10-02 20:56:14 +05:30
Ritvik Saraf
2e9a860aaa added viewpager. changed from parallaxscrollview to coordinate layout 2018-10-02 20:39:16 +05:30
Ritvik Saraf
e4bef056e6 merged upstream/dev 2018-09-29 15:46:47 +05:30
Ritvik Saraf
803b8eab28 updated extractor version 2018-09-28 05:04:36 +05:30
Ritvik Saraf
08bbfc50ee updated extractor 2018-09-27 23:27:52 +05:30
Ritvik Saraf
4e6722f201 updated extractor and downloader 2018-09-27 04:20:57 +05:30
Ritvik Saraf
a29e2116a7 handling error while loading comments 2018-09-27 00:53:36 +05:30
Ritvik Saraf
515be677a9 no comments 2018-09-24 14:53:43 +05:30
Ritvik Saraf
d694c5f511 smoother transition to comments fragment 2018-09-23 19:45:26 +05:30
Ritvik Saraf
66c753f3a3 changed comments fragment loading animation 2018-09-23 15:22:45 +05:30
Ritvik Saraf
7047b62442 added comments fragment 2018-09-23 07:02:19 +05:30
Ritvik Saraf
5cc73555cd updated extractor version 2018-09-19 05:41:01 +05:30
Ritvik Saraf
219922cd82 added commentsInfo in streamInfo 2018-09-19 05:13:55 +05:30
Ritvik Saraf
08127e5806 added basic/crappy comments support 2018-09-15 17:15:44 +05:30
269 changed files with 17280 additions and 10347 deletions

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

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

View File

@@ -16,7 +16,9 @@
<p align="center"><a href="https://newpipe.schabi.org">Website</a> &bull; <a href="https://newpipe.schabi.org/blog/">Blog</a> &bull; <a href="https://newpipe.schabi.org/press/">Press</a></p>
<hr>
<b>WARNING: PUTTING NEWPIPE OR ANY FORK OF IT INTO GOOGLE PLAYSTORE VIOLATES THEIR TERMS OF CONDITIONS.</b>
<b>WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY.</b>
<b>PUTTING NEWPIPE OR ANY FORK OF IT INTO GOOGLE PLAYSTORE VIOLATES THEIR TERMS OF CONDITIONS.</b>
## Screenshots
@@ -64,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 71
versionName "0.15.1"
versionCode 770
versionName "0.17.2"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
@@ -44,10 +44,10 @@ android {
ext {
supportLibVersion = '28.0.0'
exoPlayerLibVersion = '2.8.4' //2.9.0
exoPlayerLibVersion = '2.9.6'
roomDbLibVersion = '1.1.1'
leakCanaryLibVersion = '1.5.4' //1.6.1
okHttpLibVersion = '3.11.0'
okHttpLibVersion = '3.12.1'
icepickLibVersion = '3.2.0'
stethoLibVersion = '1.5.0'
}
@@ -57,7 +57,7 @@ dependencies {
exclude module: 'support-annotations'
})
implementation 'com.github.TeamNewPipe:NewPipeExtractor:f7c7b9df1a'
implementation 'com.github.teamnewpipe:NewPipeExtractor:ec3554a2ea6aa20'
testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:2.23.0'

View File

@@ -35,12 +35,6 @@
</intent-filter>
</receiver>
<activity
android:name=".player.old.PlayVideoActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:theme="@style/OldVideoPlayerTheme"
tools:ignore="UnusedAttribute"/>
<service
android:name=".player.BackgroundPlayer"
android:exported="false">
@@ -183,6 +177,19 @@
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:host="www.youtube-nocookie.com"/>
<data android:pathPrefix="/embed/"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="vnd.youtube"/>
<data android:scheme="vnd.youtube.launch"/>
</intent-filter>
@@ -209,6 +216,29 @@
<data android:pathPrefix="/user/"/>
</intent-filter>
<!-- Invidious filter -->
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:host="invidio.us"/>
<data android:host="www.invidio.us"/>
<!-- video prefix -->
<data android:pathPrefix="/embed/"/>
<data android:pathPrefix="/watch"/>
<!-- channel prefix -->
<data android:pathPrefix="/channel/"/>
<data android:pathPrefix="/user/"/>
<!-- playlist prefix -->
<data android:pathPrefix="/playlist"/>
</intent-filter>
<!-- Soundcloud filter -->
<intent-filter>
<action android:name="android.intent.action.VIEW"/>

View File

@@ -0,0 +1,116 @@
package android.support.design.widget;
import android.animation.ValueAnimator;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.design.animation.AnimationUtils;
import android.util.AttributeSet;
import android.view.View;
// check this https://github.com/ToDou/appbarlayout-spring-behavior/blob/master/appbarspring/src/main/java/android/support/design/widget/AppBarFlingFixBehavior.java
public final class FlingBehavior extends AppBarLayout.Behavior {
private ValueAnimator mOffsetAnimator;
private static final int MAX_OFFSET_ANIMATION_DURATION = 600; // ms
public FlingBehavior() {
}
public FlingBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed, int type) {
if (dy != 0) {
int val = child.getBottom();
if (val != 0) {
int min, max;
if (dy < 0) {
// We're scrolling down
} else {
// We're scrolling up
if (mOffsetAnimator != null && mOffsetAnimator.isRunning()) {
mOffsetAnimator.cancel();
}
min = -child.getUpNestedPreScrollRange();
max = 0;
consumed[1] = scroll(coordinatorLayout, child, dy, min, max);
}
}
}
}
@Override
public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull AppBarLayout child, @NonNull View target, float velocityX, float velocityY) {
if (velocityY != 0) {
if (velocityY < 0) {
// We're flinging down
int val = child.getBottom();
if (val != 0) {
final int targetScroll =
+child.getDownNestedPreScrollRange();
animateOffsetTo(coordinatorLayout, child, targetScroll, velocityY);
}
} else {
// We're flinging up
int val = child.getBottom();
if (val != 0) {
final int targetScroll = -child.getUpNestedPreScrollRange();
if (getTopBottomOffsetForScrollingSibling() > targetScroll) {
animateOffsetTo(coordinatorLayout, child, targetScroll, velocityY);
}
}
}
}
return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
}
private void animateOffsetTo(final CoordinatorLayout coordinatorLayout,
final AppBarLayout child, final int offset, float velocity) {
final int distance = Math.abs(getTopBottomOffsetForScrollingSibling() - offset);
final int duration;
velocity = Math.abs(velocity);
if (velocity > 0) {
duration = 3 * Math.round(1000 * (distance / velocity));
} else {
final float distanceRatio = (float) distance / child.getHeight();
duration = (int) ((distanceRatio + 1) * 150);
}
animateOffsetWithDuration(coordinatorLayout, child, offset, duration);
}
private void animateOffsetWithDuration(final CoordinatorLayout coordinatorLayout,
final AppBarLayout child, final int offset, final int duration) {
final int currentOffset = getTopBottomOffsetForScrollingSibling();
if (currentOffset == offset) {
if (mOffsetAnimator != null && mOffsetAnimator.isRunning()) {
mOffsetAnimator.cancel();
}
return;
}
if (mOffsetAnimator == null) {
mOffsetAnimator = new ValueAnimator();
mOffsetAnimator.setInterpolator(AnimationUtils.DECELERATE_INTERPOLATOR);
mOffsetAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animator) {
setHeaderTopBottomOffset(coordinatorLayout, child,
(Integer) animator.getAnimatedValue());
}
});
} else {
mOffsetAnimator.cancel();
}
mOffsetAnimator.setDuration(Math.min(duration, MAX_OFFSET_ANIMATION_DURATION));
mOffsetAnimator.setIntValues(currentOffset, offset);
mOffsetAnimator.start();
}
}

View File

@@ -2,16 +2,19 @@ package org.schabi.newpipe;
import android.app.Application;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.net.ConnectivityManager;
import android.net.Uri;
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;
@@ -45,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";
@@ -68,6 +73,8 @@ public class CheckForNewAppVersionTask extends AsyncTask<Void, Void, String> {
@Override
protected String doInBackground(Void... voids) {
if(isCancelled() || !isConnected()) return null;
// Make a network request to get latest NewPipe data.
if (client == null) {
@@ -86,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;
@@ -113,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));
}
}
}
@@ -227,4 +232,12 @@ public class CheckForNewAppVersionTask extends AsyncTask<Void, Void, String> {
return getCertificateSHA1Fingerprint().equals(GITHUB_APK_SHA1);
}
private boolean isConnected() {
ConnectivityManager cm =
(ConnectivityManager) app.getSystemService(Context.CONNECTIVITY_SERVICE);
return cm.getActiveNetworkInfo() != null
&& cm.getActiveNetworkInfo().isConnected();
}
}

View File

@@ -3,18 +3,24 @@ package org.schabi.newpipe;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import org.schabi.newpipe.extractor.DownloadRequest;
import org.schabi.newpipe.extractor.DownloadResponse;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.utils.Localization;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
@@ -139,13 +145,16 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
private ResponseBody getBody(String siteUrl, Map<String, String> customProperties) throws IOException, ReCaptchaException {
final Request.Builder requestBuilder = new Request.Builder()
.method("GET", null).url(siteUrl)
.addHeader("User-Agent", USER_AGENT);
.method("GET", null).url(siteUrl);
for (Map.Entry<String, String> header : customProperties.entrySet()) {
requestBuilder.addHeader(header.getKey(), header.getValue());
}
if (!customProperties.containsKey("User-Agent")) {
requestBuilder.header("User-Agent", USER_AGENT);
}
if (!TextUtils.isEmpty(mCookies)) {
requestBuilder.addHeader("Cookie", mCookies);
}
@@ -155,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) {
@@ -177,4 +186,96 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
public String download(String siteUrl) throws IOException, ReCaptchaException {
return download(siteUrl, Collections.emptyMap());
}
}
@Override
public DownloadResponse get(String siteUrl, DownloadRequest request) throws IOException, ReCaptchaException {
final Request.Builder requestBuilder = new Request.Builder()
.method("GET", null).url(siteUrl);
Map<String, List<String>> requestHeaders = request.getRequestHeaders();
// set custom headers in request
for (Map.Entry<String, List<String>> pair : requestHeaders.entrySet()) {
for(String value : pair.getValue()){
requestBuilder.addHeader(pair.getKey(), value);
}
}
if (!requestHeaders.containsKey("User-Agent")) {
requestBuilder.header("User-Agent", USER_AGENT);
}
if (!TextUtils.isEmpty(mCookies)) {
requestBuilder.addHeader("Cookie", mCookies);
}
final Request okRequest = requestBuilder.build();
final Response response = client.newCall(okRequest).execute();
final ResponseBody body = response.body();
if (response.code() == 429) {
throw new ReCaptchaException("reCaptcha Challenge requested", siteUrl);
}
if (body == null) {
response.close();
return null;
}
return new DownloadResponse(body.string(), response.headers().toMultimap());
}
@Override
public DownloadResponse get(String siteUrl) throws IOException, ReCaptchaException {
return get(siteUrl, DownloadRequest.emptyRequest);
}
@Override
public DownloadResponse post(String siteUrl, DownloadRequest request) throws IOException, ReCaptchaException {
Map<String, List<String>> requestHeaders = request.getRequestHeaders();
if(null == requestHeaders.get("Content-Type") || requestHeaders.get("Content-Type").isEmpty()){
// content type header is required. maybe throw an exception here
return null;
}
String contentType = requestHeaders.get("Content-Type").get(0);
RequestBody okRequestBody = null;
if(null != request.getRequestBody()){
okRequestBody = RequestBody.create(MediaType.parse(contentType), request.getRequestBody());
}
final Request.Builder requestBuilder = new Request.Builder()
.method("POST", okRequestBody).url(siteUrl);
// set custom headers in request
for (Map.Entry<String, List<String>> pair : requestHeaders.entrySet()) {
for(String value : pair.getValue()){
requestBuilder.addHeader(pair.getKey(), value);
}
}
if (!requestHeaders.containsKey("User-Agent")) {
requestBuilder.header("User-Agent", USER_AGENT);
}
if (!TextUtils.isEmpty(mCookies)) {
requestBuilder.addHeader("Cookie", mCookies);
}
final Request okRequest = requestBuilder.build();
final Response response = client.newCall(okRequest).execute();
final ResponseBody body = response.body();
if (response.code() == 429) {
throw new ReCaptchaException("reCaptcha Challenge requested", siteUrl);
}
if (body == null) {
response.close();
return null;
}
return new DownloadResponse(body.string(), response.headers().toMultimap());
}
}

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

@@ -36,12 +36,12 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
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.UserAction;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper;
@@ -74,17 +74,23 @@ 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;
protected boolean internalRoute = false;
protected final CompositeDisposable disposables = new CompositeDisposable();
private boolean selectionIsDownload = false;
public static final String internalRouteKey = "internalRoute";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -94,11 +100,13 @@ public class RouterActivity extends AppCompatActivity {
currentUrl = getUrl(getIntent());
if (TextUtils.isEmpty(currentUrl)) {
Toast.makeText(this, R.string.invalid_url_toast, Toast.LENGTH_LONG).show();
handleText();
finish();
}
}
internalRoute = getIntent().getBooleanExtra(internalRouteKey, false);
setTheme(ThemeHelper.isLightThemeSelected(this)
? R.style.RouterActivityThemeLight : R.style.RouterActivityThemeDark);
}
@@ -112,7 +120,7 @@ public class RouterActivity extends AppCompatActivity {
@Override
protected void onStart() {
super.onStart();
handleUrl(currentUrl);
}
@@ -252,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();
@@ -353,6 +361,15 @@ public class RouterActivity extends AppCompatActivity {
positiveButton.setEnabled(state);
}
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);
}
private void handleChoice(final String selectedChoiceKey) {
final List<String> validChoicesList = Arrays.asList(getResources().getStringArray(R.array.preferred_open_action_values_list));
if (validChoicesList.contains(selectedChoiceKey)) {
@@ -383,8 +400,10 @@ public class RouterActivity extends AppCompatActivity {
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(intent -> {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
if (!internalRoute) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
}
startActivity(intent);
finish();
@@ -431,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;
}
@@ -444,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;
@@ -542,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;
@@ -558,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)) {
@@ -571,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

@@ -47,7 +47,7 @@ public class DownloadActivity extends AppCompatActivity {
@Override
public void onGlobalLayout() {
updateFragments();
getWindow().getDecorView().getViewTreeObserver().removeGlobalOnLayoutListener(this);
getWindow().getDecorView().getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});
}
@@ -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);
}
@@ -230,21 +232,4 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
ErrorActivity.reportError(getContext(), exception, MainActivity.class, rootView,
ErrorActivity.ErrorInfo.make(userAction, serviceName, request, errorId));
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
protected void openUrlInBrowser(String url) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
startActivity(Intent.createChooser(intent, activity.getString(R.string.share_dialog_title)));
}
protected void shareUrl(String subject, String url) {
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_SUBJECT, subject);
intent.putExtra(Intent.EXTRA_TEXT, url);
startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title)));
}
}

View File

@@ -0,0 +1,17 @@
package org.schabi.newpipe.fragments;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R;
public class EmptyFragment extends BaseFragment {
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_empty, container, 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

@@ -0,0 +1,86 @@
package org.schabi.newpipe.fragments.detail;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
public class TabAdaptor extends FragmentPagerAdapter {
private final List<Fragment> mFragmentList = new ArrayList<>();
private final List<String> mFragmentTitleList = new ArrayList<>();
private final FragmentManager fragmentManager;
public TabAdaptor(FragmentManager fm) {
super(fm);
this.fragmentManager = fm;
}
@Override
public Fragment getItem(int position) {
return mFragmentList.get(position);
}
@Override
public int getCount() {
return mFragmentList.size();
}
public void addFragment(Fragment fragment, String title) {
mFragmentList.add(fragment);
mFragmentTitleList.add(title);
}
public void clearAllItems() {
mFragmentList.clear();
mFragmentTitleList.clear();
}
public void removeItem(int position){
mFragmentList.remove(position == 0 ? 0 : position - 1);
mFragmentTitleList.remove(position == 0 ? 0 : position - 1);
}
public void updateItem(int position, Fragment fragment){
mFragmentList.set(position, fragment);
}
public void updateItem(String title, Fragment fragment){
int index = mFragmentTitleList.indexOf(title);
if(index != -1){
updateItem(index, fragment);
}
}
@Override
public int getItemPosition(Object object) {
if (mFragmentList.contains(object)) return mFragmentList.indexOf(object);
else return POSITION_NONE;
}
public int getItemPositionByTitle(String title) {
return mFragmentTitleList.indexOf(title);
}
@Nullable
public String getItemTitle(int position) {
if (position < 0 || position >= mFragmentTitleList.size()) {
return null;
}
return mFragmentTitleList.get(position);
}
public void notifyDataSetUpdate(){
notifyDataSetChanged();
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
fragmentManager.beginTransaction().remove((Fragment) object).commitNowAllowingStateLoss();
}
}

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;
@@ -22,20 +21,20 @@ import android.view.View;
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.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.StateSaver;
import org.schabi.newpipe.util.StreamDialogEntry;
import java.util.Collections;
import java.util.List;
import java.util.Queue;
@@ -63,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);
@@ -220,6 +224,13 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
}
});
infoListAdapter.setOnCommentsSelectedListener(new OnClickGesture<CommentsInfoItem>() {
@Override
public void selected(CommentsInfoItem selectedItem) {
onItemSelected(selectedItem);
}
});
itemsList.clearOnScrollListeners();
itemsList.addOnScrollListener(new OnScrollBelowItemsListener() {
@Override
@@ -241,41 +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.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.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item));
break;
case 1:
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item));
break;
case 2:
if (getFragmentManager() != null) {
PlaylistAppendDialog.fromStreamInfoItems(Collections.singletonList(item))
.show(getFragmentManager(), TAG);
}
break;
case 3:
shareUrl(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,21 +32,18 @@ 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;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
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;
@@ -149,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:
shareUrl(item.getName(), item.getUrl());
break;
default:
break;
}
};
new InfoItemDialog(getActivity(), item, commands, actions).show();
}
/*//////////////////////////////////////////////////////////////////////////
// Menu
//////////////////////////////////////////////////////////////////////////*/
@@ -233,10 +178,10 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
openRssFeed();
break;
case R.id.menu_item_openInBrowser:
openUrlInBrowser(url);
ShareUtils.openUrlInBrowser(this.getContext(), currentInfo.getOriginalUrl());
break;
case R.id.menu_item_share:
shareUrl(name, url);
ShareUtils.shareUrl(this.getContext(), name, currentInfo.getOriginalUrl());
break;
default:
return super.onOptionsItemSelected(item);
@@ -439,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

@@ -0,0 +1,149 @@
package org.schabi.newpipe.fragments.list.comments;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.comments.CommentsInfo;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.ExtractorHelper;
import io.reactivex.Single;
import io.reactivex.disposables.CompositeDisposable;
public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
private CompositeDisposable disposables = new CompositeDisposable();
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
private boolean mIsVisibleToUser = false;
public static CommentsFragment getInstance(int serviceId, String url, String name) {
CommentsFragment instance = new CommentsFragment();
instance.setInitialData(serviceId, url, name);
return instance;
}
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
mIsVisibleToUser = isVisibleToUser;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_comments, container, false);
}
@Override
public void onDestroy() {
super.onDestroy();
if (disposables != null) disposables.clear();
}
/*//////////////////////////////////////////////////////////////////////////
// Load and handle
//////////////////////////////////////////////////////////////////////////*/
@Override
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
return ExtractorHelper.getMoreCommentItems(serviceId, currentInfo, currentNextPageUrl);
}
@Override
protected Single<CommentsInfo> loadResult(boolean forceLoad) {
return ExtractorHelper.getCommentsInfo(serviceId, url, forceLoad);
}
/*//////////////////////////////////////////////////////////////////////////
// Contract
//////////////////////////////////////////////////////////////////////////*/
@Override
public void showLoading() {
super.showLoading();
}
@Override
public void handleResult(@NonNull CommentsInfo result) {
super.handleResult(result);
AnimationUtils.slideUp(getView(),120, 96, 0.06f);
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.REQUESTED_COMMENTS, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
}
if (disposables != null) disposables.clear();
}
@Override
public void handleNextItems(ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(),
UserAction.REQUESTED_COMMENTS,
NewPipe.getNameOfService(serviceId),
"Get next page of: " + url,
R.string.general_error);
}
}
/*//////////////////////////////////////////////////////////////////////////
// OnError
//////////////////////////////////////////////////////////////////////////*/
@Override
protected boolean onError(Throwable exception) {
if (super.onError(exception)) return true;
hideLoading();
showSnackBarError(exception, UserAction.REQUESTED_COMMENTS, NewPipe.getNameOfService(serviceId), url, R.string.error_unable_to_load_comments);
return true;
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
@Override
public void setTitle(String title) {
return;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
return;
}
@Override
protected boolean isGridLayout() {
return false;
}
}

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,17 +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;
@@ -134,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:
shareUrl(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
@@ -230,10 +224,10 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_item_openInBrowser:
openUrlInBrowser(url);
ShareUtils.openUrlInBrowser(this.getContext(), url);
break;
case R.id.menu_item_share:
shareUrl(name, url);
ShareUtils.shareUrl(this.getContext(), name, url);
break;
case R.id.menu_item_bookmark:
onBookmarkClicked();
@@ -300,11 +294,21 @@ 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(), true);
return true;
});
headerBackgroundButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true);
return true;
});
}
private PlayQueue getPlayQueue() {

View File

@@ -12,6 +12,7 @@ import android.support.v7.app.ActionBar;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.TooltipCompat;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
@@ -39,15 +40,15 @@ import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.search.SearchExtractor;
import org.schabi.newpipe.extractor.search.SearchInfo;
import org.schabi.newpipe.util.FireTvUtils;
import org.schabi.newpipe.fragments.BackPressable;
import org.schabi.newpipe.fragments.list.BaseListFragment;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.LayoutManagerSmoothScroller;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ServiceHelper;
@@ -72,8 +73,8 @@ import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.PublishSubject;
import static android.support.v7.widget.helper.ItemTouchHelper.Callback.makeMovementFlags;
import static java.util.Arrays.asList;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public class SearchFragment
@@ -104,8 +105,13 @@ public class SearchFragment
// this three represet the current search query
@State
protected String searchString;
/**
* No content filter should add like contentfilter = all
* be aware of this when implementing an extractor.
*/
@State
protected String[] contentFilter;
protected String[] contentFilter = new String[0];
@State
protected String sortFilter;
@@ -292,7 +298,23 @@ public class SearchFragment
suggestionsPanel = rootView.findViewById(R.id.suggestions_panel);
suggestionsRecyclerView = rootView.findViewById(R.id.suggestions_list);
suggestionsRecyclerView.setAdapter(suggestionListAdapter);
suggestionsRecyclerView.setLayoutManager(new LayoutManagerSmoothScroller(activity));
new ItemTouchHelper(new ItemTouchHelper.Callback() {
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
return getSuggestionMovementFlags(recyclerView, viewHolder);
}
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder,
@NonNull RecyclerView.ViewHolder viewHolder1) {
return false;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int i) {
onSuggestionItemSwiped(viewHolder, i);
}
}).attachToRecyclerView(suggestionsRecyclerView);
searchToolbarContainer = activity.findViewById(R.id.toolbar_search_container);
searchEditText = searchToolbarContainer.findViewById(R.id.toolbar_search_edit_text);
@@ -335,7 +357,7 @@ public class SearchFragment
|| (searchEditText != null && !TextUtils.isEmpty(searchEditText.getText()))) {
search(!TextUtils.isEmpty(searchString)
? searchString
: searchEditText.getText().toString(), new String[0], "");
: searchEditText.getText().toString(), this.contentFilter, "");
} else {
if (searchEditText != null) {
searchEditText.setText("");
@@ -449,6 +471,9 @@ public class SearchFragment
if (isSuggestionsEnabled && errorPanelRoot.getVisibility() != View.VISIBLE) {
showSuggestionsPanel();
}
if(FireTvUtils.isFireTv()){
showKeyboardSearch();
}
});
searchEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
@@ -499,7 +524,9 @@ public class SearchFragment
if (DEBUG) {
Log.d(TAG, "onEditorAction() called with: v = [" + v + "], actionId = [" + actionId + "], event = [" + event + "]");
}
if (event != null
if(actionId == EditorInfo.IME_ACTION_PREVIOUS){
hideKeyboardSearch();
} else if (event != null
&& (event.getKeyCode() == KeyEvent.KEYCODE_ENTER
|| event.getAction() == EditorInfo.IME_ACTION_SEARCH)) {
search(searchEditText.getText().toString(), new String[0], "");
@@ -541,7 +568,7 @@ public class SearchFragment
if (searchEditText.requestFocus()) {
InputMethodManager imm = (InputMethodManager) activity.getSystemService(
Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(searchEditText, InputMethodManager.SHOW_IMPLICIT);
imm.showSoftInput(searchEditText, InputMethodManager.SHOW_FORCED);
}
}
@@ -551,8 +578,7 @@ public class SearchFragment
InputMethodManager imm = (InputMethodManager) activity.getSystemService(
Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(searchEditText.getWindowToken(),
InputMethodManager.HIDE_NOT_ALWAYS);
imm.hideSoftInputFromWindow(searchEditText.getWindowToken(), InputMethodManager.RESULT_UNCHANGED_SHOWN);
searchEditText.clearFocus();
}
@@ -736,6 +762,7 @@ public class SearchFragment
@Override
protected void loadMoreItems() {
if(nextPageUrl == null || nextPageUrl.isEmpty()) return;
isLoading.set(true);
showListFooter(true);
if (searchDisposable != null) searchDisposable.dispose();
@@ -890,4 +917,28 @@ public class SearchFragment
return true;
}
/*//////////////////////////////////////////////////////////////////////////
// Suggestion item touch helper
//////////////////////////////////////////////////////////////////////////*/
public int getSuggestionMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
final int position = viewHolder.getAdapterPosition();
final SuggestionItem item = suggestionListAdapter.getItem(position);
return item.fromHistory ? makeMovementFlags(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) : 0;
}
public void onSuggestionItemSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int i) {
final int position = viewHolder.getAdapterPosition();
final String query = suggestionListAdapter.getItem(position).query;
final Disposable onDelete = historyRecordManager.deleteSearchHistory(query)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
howManyDeleted -> suggestionPublisher
.onNext(searchEditText.getText().toString()),
throwable -> showSnackBarError(throwable,
UserAction.DELETE_FROM_HISTORY, "none",
"Deleting item failed", R.string.general_error));
disposables.add(onDelete);
}
}

View File

@@ -75,7 +75,7 @@ public class SuggestionListAdapter extends RecyclerView.Adapter<SuggestionListAd
});
}
private SuggestionItem getItem(int position) {
SuggestionItem getItem(int position) {
return items.get(position);
}

View File

@@ -0,0 +1,208 @@
package org.schabi.newpipe.fragments.list.videos;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CompoundButton;
import android.widget.Switch;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.RelatedStreamInfo;
import java.io.Serializable;
import io.reactivex.Single;
import io.reactivex.disposables.CompositeDisposable;
public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInfo> implements SharedPreferences.OnSharedPreferenceChangeListener{
private CompositeDisposable disposables = new CompositeDisposable();
private RelatedStreamInfo relatedStreamInfo;
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
private View headerRootLayout;
private Switch aSwitch;
private boolean mIsVisibleToUser = false;
public static RelatedVideosFragment getInstance(StreamInfo info) {
RelatedVideosFragment instance = new RelatedVideosFragment();
instance.setInitialData(info);
return instance;
}
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
mIsVisibleToUser = isVisibleToUser;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_related_streams, container, false);
}
@Override
public void onDestroy() {
super.onDestroy();
if (disposables != null) disposables.clear();
}
protected View getListHeader(){
if(relatedStreamInfo != null && relatedStreamInfo.getNextStream() != null){
headerRootLayout = activity.getLayoutInflater().inflate(R.layout.related_streams_header, itemsList, false);
aSwitch = headerRootLayout.findViewById(R.id.autoplay_switch);
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getContext());
Boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
aSwitch.setChecked(autoplay);
aSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
SharedPreferences.Editor prefEdit = PreferenceManager.getDefaultSharedPreferences(getContext()).edit();
prefEdit.putBoolean(getString(R.string.auto_queue_key), b);
prefEdit.apply();
}
});
return headerRootLayout;
}else{
return null;
}
}
@Override
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
return Single.fromCallable(() -> ListExtractor.InfoItemsPage.emptyPage());
}
@Override
protected Single<RelatedStreamInfo> loadResult(boolean forceLoad) {
return Single.fromCallable(() -> relatedStreamInfo);
}
/*//////////////////////////////////////////////////////////////////////////
// Contract
//////////////////////////////////////////////////////////////////////////*/
@Override
public void showLoading() {
super.showLoading();
if(null != headerRootLayout) headerRootLayout.setVisibility(View.INVISIBLE);
}
@Override
public void handleResult(@NonNull RelatedStreamInfo result) {
super.handleResult(result);
if(null != headerRootLayout) headerRootLayout.setVisibility(View.VISIBLE);
AnimationUtils.slideUp(getView(),120, 96, 0.06f);
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
}
if (disposables != null) disposables.clear();
}
@Override
public void handleNextItems(ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(),
UserAction.REQUESTED_STREAM,
NewPipe.getNameOfService(serviceId),
"Get next page of: " + url,
R.string.general_error);
}
}
/*//////////////////////////////////////////////////////////////////////////
// OnError
//////////////////////////////////////////////////////////////////////////*/
@Override
protected boolean onError(Throwable exception) {
if (super.onError(exception)) return true;
hideLoading();
showSnackBarError(exception, UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(serviceId), url, R.string.general_error);
return true;
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
@Override
public void setTitle(String title) {
return;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
return;
}
private void setInitialData(StreamInfo info) {
super.setInitialData(info.getServiceId(), info.getUrl(), info.getName());
if(this.relatedStreamInfo == null) this.relatedStreamInfo = RelatedStreamInfo.getInfo(info);
}
private static final String INFO_KEY = "related_info_key";
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable(INFO_KEY, relatedStreamInfo);
}
@Override
protected void onRestoreInstanceState(@NonNull Bundle savedState) {
super.onRestoreInstanceState(savedState);
if (savedState != null) {
Serializable serializable = savedState.getSerializable(INFO_KEY);
if(serializable instanceof RelatedStreamInfo){
this.relatedStreamInfo = (RelatedStreamInfo) serializable;
}
}
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) {
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getContext());
Boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
if(null != aSwitch) aSwitch.setChecked(autoplay);
}
@Override
protected boolean isGridLayout() {
return false;
}
}

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;
@@ -10,15 +9,19 @@ import com.nostra13.universalimageloader.core.ImageLoader;
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.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.InfoItemHolder;
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;
/*
@@ -50,18 +53,20 @@ public class InfoItemBuilder {
private OnClickGesture<StreamInfoItem> onStreamSelectedListener;
private OnClickGesture<ChannelInfoItem> onChannelSelectedListener;
private OnClickGesture<PlaylistInfoItem> onPlaylistSelectedListener;
private OnClickGesture<CommentsInfoItem> onCommentsSelectedListener;
public InfoItemBuilder(Context context) {
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;
}
@@ -73,8 +78,9 @@ public class InfoItemBuilder {
return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent) : new ChannelInfoItemHolder(this, parent);
case PLAYLIST:
return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent) : new PlaylistInfoItemHolder(this, parent);
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());
}
}
@@ -111,4 +117,12 @@ public class InfoItemBuilder {
this.onPlaylistSelectedListener = listener;
}
public OnClickGesture<CommentsInfoItem> getOnCommentsSelectedListener() {
return onCommentsSelectedListener;
}
public void setOnCommentsSelectedListener(OnClickGesture<CommentsInfoItem> onCommentsSelectedListener) {
this.onCommentsSelectedListener = onCommentsSelectedListener;
}
}

View File

@@ -1,19 +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.ChannelGridInfoItemHolder;
import org.schabi.newpipe.info_list.holder.CommentsInfoItemHolder;
import org.schabi.newpipe.info_list.holder.CommentsMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistGridInfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
@@ -21,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;
@@ -63,9 +70,13 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
private static final int MINI_PLAYLIST_HOLDER_TYPE = 0x300;
private static final int PLAYLIST_HOLDER_TYPE = 0x301;
private static final int GRID_PLAYLIST_HOLDER_TYPE = 0x302;
private static final int MINI_COMMENT_HOLDER_TYPE = 0x400;
private static final int COMMENT_HOLDER_TYPE = 0x401;
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;
@@ -81,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<>();
}
@@ -98,6 +110,10 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
infoItemBuilder.setOnPlaylistSelectedListener(listener);
}
public void setOnCommentsSelectedListener(OnClickGesture<CommentsInfoItem> listener) {
infoItemBuilder.setOnCommentsSelectedListener(listener);
}
public void useMiniItemVariants(boolean useMiniVariant) {
this.useMiniVariant = useMiniVariant;
}
@@ -106,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);
}
}
@@ -223,15 +242,18 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
return useGridVariant ? GRID_CHANNEL_HOLDER_TYPE : useMiniVariant ? MINI_CHANNEL_HOLDER_TYPE : CHANNEL_HOLDER_TYPE;
case PLAYLIST:
return useGridVariant ? GRID_PLAYLIST_HOLDER_TYPE : useMiniVariant ? MINI_PLAYLIST_HOLDER_TYPE : PLAYLIST_HOLDER_TYPE;
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) {
if (DEBUG) Log.d(TAG, "onCreateViewHolder() called with: parent = [" + parent + "], type = [" + type + "]");
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int type) {
if (DEBUG)
Log.d(TAG, "onCreateViewHolder() called with: parent = [" + parent + "], type = [" + type + "]");
switch (type) {
case HEADER_TYPE:
return new HFHolder(header);
@@ -255,20 +277,23 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
return new PlaylistInfoItemHolder(infoItemBuilder, parent);
case GRID_PLAYLIST_HOLDER_TYPE:
return new PlaylistGridInfoItemHolder(infoItemBuilder, parent);
case MINI_COMMENT_HOLDER_TYPE:
return new CommentsMiniInfoItemHolder(infoItemBuilder, parent);
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) {
@@ -276,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

@@ -0,0 +1,52 @@
package org.schabi.newpipe.info_list.holder;
import android.view.ViewGroup;
import android.widget.TextView;
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;
/*
* Created by Christian Schabesberger on 12.02.17.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* ChannelInfoItemHolder .java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class CommentsInfoItemHolder extends CommentsMiniInfoItemHolder {
public final TextView itemTitleView;
public CommentsInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
super(infoItemBuilder, R.layout.list_comments_item, parent);
itemTitleView = itemView.findViewById(R.id.itemTitleView);
}
@Override
public void updateFromItem(final InfoItem infoItem, final HistoryRecordManager historyRecordManager) {
super.updateFromItem(infoItem, historyRecordManager);
if (!(infoItem instanceof CommentsInfoItem)) return;
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
itemTitleView.setText(item.getAuthorName());
}
}

View File

@@ -0,0 +1,147 @@
package org.schabi.newpipe.info_list.holder;
import android.support.v7.app.AppCompatActivity;
import android.text.util.Linkify;
import android.view.ViewGroup;
import android.widget.TextView;
import org.jsoup.helper.StringUtil;
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;
import org.schabi.newpipe.util.NavigationHelper;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import de.hdodenhof.circleimageview.CircleImageView;
public class CommentsMiniInfoItemHolder extends InfoItemHolder {
public final CircleImageView itemThumbnailView;
private final TextView itemContentView;
private final TextView itemLikesCountView;
private final TextView itemDislikesCountView;
private final TextView itemPublishedTime;
private static final int commentDefaultLines = 2;
private static final int commentExpandedLines = 1000;
private String commentText;
private String streamUrl;
private static final Pattern pattern = Pattern.compile("(\\d+:)?(\\d+)?:(\\d+)");
private final Linkify.TransformFilter timestampLink = new Linkify.TransformFilter() {
@Override
public String transformUrl(Matcher match, String url) {
int timestamp = 0;
String hours = match.group(1);
String minutes = match.group(2);
String seconds = match.group(3);
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=" + timestamp);
}
};
CommentsMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
super(infoItemBuilder, layoutId, parent);
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
itemLikesCountView = itemView.findViewById(R.id.detail_thumbs_up_count_view);
itemDislikesCountView = itemView.findViewById(R.id.detail_thumbs_down_count_view);
itemPublishedTime = itemView.findViewById(R.id.itemPublishedTime);
itemContentView = itemView.findViewById(R.id.itemCommentContentView);
}
public CommentsMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
this(infoItemBuilder, R.layout.list_comments_mini_item, parent);
}
@Override
public void updateFromItem(final InfoItem infoItem, final HistoryRecordManager historyRecordManager) {
if (!(infoItem instanceof CommentsInfoItem)) return;
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
itemBuilder.getImageLoader()
.displayImage(item.getAuthorThumbnail(),
itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
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);
}
});
streamUrl = item.getUrl();
itemContentView.setLines(commentDefaultLines);
commentText = item.getCommentText();
itemContentView.setText(commentText);
itemContentView.setOnTouchListener(CommentTextOnTouchListener.INSTANCE);
if (itemContentView.getLineCount() == 0) {
itemContentView.post(this::ellipsize);
} else {
ellipsize();
}
if (null != item.getLikeCount()) {
itemLikesCountView.setText(String.valueOf(item.getLikeCount()));
}
itemPublishedTime.setText(item.getPublishedTime());
itemView.setOnClickListener(view -> {
toggleEllipsize();
if (itemBuilder.getOnCommentsSelectedListener() != null) {
itemBuilder.getOnCommentsSelectedListener().selected(item);
}
});
}
private void ellipsize() {
if (itemContentView.getLineCount() > commentDefaultLines){
int endOfLastLine = itemContentView.getLayout().getLineEnd(commentDefaultLines - 1);
int end = itemContentView.getText().toString().lastIndexOf(' ', endOfLastLine -2);
if(end == -1) end = Math.max(endOfLastLine -2, 0);
String newVal = itemContentView.getText().subSequence(0, end) + "";
itemContentView.setText(newVal);
}
linkify();
}
private void toggleEllipsize() {
if (itemContentView.getText().toString().equals(commentText)) {
if (itemContentView.getLineCount() > commentDefaultLines) ellipsize();
} else {
expand();
}
}
private void expand() {
itemContentView.setMaxLines(commentExpandedLines);
itemContentView.setText(commentText);
linkify();
}
private void linkify(){
Linkify.addLinks(itemContentView, Linkify.WEB_URLS);
Linkify.addLinks(itemContentView, pattern, null, null, timestampLink);
itemContentView.setMovementMethod(null);
}
}

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,13 +2,16 @@ 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;
import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AlertDialog;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
@@ -19,15 +22,20 @@ 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.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.ErrorActivity;
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.StreamDialogEntry;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.ArrayList;
@@ -104,6 +112,12 @@ public class StatisticsPlaylistFragment
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.menu_history, menu);
}
///////////////////////////////////////////////////////////////////////////
// Fragment LifeCycle - Views
///////////////////////////////////////////////////////////////////////////
@@ -155,6 +169,53 @@ public class StatisticsPlaylistFragment
});
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_history_clear:
new AlertDialog.Builder(activity)
.setTitle(R.string.delete_view_history_alert)
.setNegativeButton(R.string.cancel, ((dialog, which) -> dialog.dismiss()))
.setPositiveButton(R.string.delete, ((dialog, which) -> {
final Disposable onDelete = recordManager.deleteWholeStreamHistory()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
howManyDeleted -> Toast.makeText(getContext(),
R.string.watch_history_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 onClearOrphans = recordManager.removeOrphanedRecords()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
howManyDeleted -> {},
throwable -> ErrorActivity.reportError(getContext(),
throwable,
SettingsActivity.class, null,
ErrorActivity.ErrorInfo.make(
UserAction.DELETE_FROM_HISTORY,
"none",
"Delete search history",
R.string.general_error)));
disposables.add(onClearOrphans);
disposables.add(onDelete);
}))
.create()
.show();
break;
default:
return super.onOptionsItemSelected(item);
}
return true;
}
///////////////////////////////////////////////////////////////////////////
// Fragment LifeCycle - Loading
///////////////////////////////////////////////////////////////////////////
@@ -250,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();
@@ -297,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:
shareUrl(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,14 +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.StreamDialogEntry;
import java.util.ArrayList;
import java.util.Collections;
@@ -318,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();
}
@@ -510,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:
shareUrl(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,15 +47,14 @@ 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;
import org.schabi.newpipe.util.OnClickGesture;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.ShareUtils;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.views.CollapsibleView;
@@ -130,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) {
@@ -376,7 +379,6 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
});
//noinspection ConstantConditions
whatsNewItemListHeader.setOnClickListener(v -> {
FragmentManager fragmentManager = getFM();
NavigationHelper.openWhatsNewFragment(fragmentManager);
@@ -390,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;
@@ -424,12 +426,12 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
}
private void shareChannel (ChannelInfoItem selectedItem) {
shareUrl(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()
@@ -441,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

@@ -147,11 +147,16 @@ public class SubscriptionService {
}
private boolean isSubscriptionUpToDate(final ChannelInfo info, final SubscriptionEntity entity) {
return info.getUrl().equals(entity.getUrl()) &&
return equalsAndNotNull(info.getUrl(), entity.getUrl()) &&
info.getServiceId() == entity.getServiceId() &&
info.getName().equals(entity.getName()) &&
info.getAvatarUrl().equals(entity.getAvatarUrl()) &&
info.getDescription().equals(entity.getDescription()) &&
equalsAndNotNull(info.getAvatarUrl(), entity.getAvatarUrl()) &&
equalsAndNotNull(info.getDescription(), entity.getDescription()) &&
info.getSubscriberCount() == entity.getSubscriberCount();
}
private boolean equalsAndNotNull(final Object o1, final Object o2) {
return (o1 != null && o2 != null)
&& o1.equals(o2);
}
}

View File

@@ -0,0 +1,30 @@
package org.schabi.newpipe.player;
import android.content.Context;
import android.content.ContextWrapper;
/**
* Fixes a leak caused by AudioManager using an Activity context.
* Tracked at https://android-review.googlesource.com/#/c/140481/1 and
* https://github.com/square/leakcanary/issues/205
* Source:
* https://gist.github.com/jankovd/891d96f476f7a9ce24e2
*/
public class AudioServiceLeakFix extends ContextWrapper {
AudioServiceLeakFix(Context base) {
super(base);
}
public static ContextWrapper preventLeakOf(Context base) {
return new AudioServiceLeakFix(base);
}
@Override
public Object getSystemService(String name) {
if (Context.AUDIO_SERVICE.equals(name)) {
return getApplicationContext().getSystemService(name);
}
return super.getSystemService(name);
}
}

View File

@@ -130,6 +130,11 @@ public final class BackgroundPlayer extends Service {
onClose();
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base));
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
@@ -145,6 +150,7 @@ public final class BackgroundPlayer extends Service {
lockManager.releaseWifiAndCpu();
}
if (basePlayerImpl != null) {
basePlayerImpl.savePlaybackState();
basePlayerImpl.stopActivityBinding();
basePlayerImpl.destroy();
}
@@ -270,6 +276,8 @@ public final class BackgroundPlayer extends Service {
protected class BasePlayerImpl extends BasePlayer {
@NonNull final private AudioPlaybackResolver resolver;
private int cachedDuration;
private String cachedDurationString;
BasePlayerImpl(Context context) {
super(context);
@@ -344,10 +352,14 @@ public final class BackgroundPlayer extends Service {
if (!shouldUpdateOnProgress) return;
resetNotification();
if(Build.VERSION.SDK_INT >= 26 /*Oreo*/) updateNotificationThumbnail();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O /*Oreo*/) updateNotificationThumbnail();
if (bigNotRemoteView != null) {
if(cachedDuration != duration) {
cachedDuration = duration;
cachedDurationString = getTimeString(duration);
}
bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false);
bigNotRemoteView.setTextViewText(R.id.notificationTime, getTimeString(currentProgress) + " / " + getTimeString(duration));
bigNotRemoteView.setTextViewText(R.id.notificationTime, getTimeString(currentProgress) + " / " + cachedDurationString);
}
if (notRemoteView != null) {
notRemoteView.setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false);

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;
//////////////////////////////////////////////////////////////////////////*/
@@ -207,8 +212,7 @@ public abstract class BasePlayer implements
final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
this.dataSource = new PlayerDataSource(context, userAgent, bandwidthMeter);
final TrackSelection.Factory trackSelectionFactory =
PlayerHelper.getQualitySelector(context, bandwidthMeter);
final TrackSelection.Factory trackSelectionFactory = PlayerHelper.getQualitySelector(context);
this.trackSelector = new CustomTrackSelector(trackSelectionFactory);
this.loadControl = new LoadController(context);
@@ -225,7 +229,7 @@ public abstract class BasePlayer implements
public void initPlayer(final boolean playOnReady) {
if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]");
simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(renderFactory, trackSelector, loadControl);
simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderFactory, trackSelector, loadControl);
simpleExoPlayer.addListener(this);
simpleExoPlayer.setPlayWhenReady(playOnReady);
simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context));
@@ -270,6 +274,34 @@ public abstract class BasePlayer implements
final boolean playbackSkipSilence = intent.getBooleanExtra(PLAYBACK_SKIP_SILENCE,
getPlaybackSkipSilence());
// seek to timestamp if stream is already playing
if (simpleExoPlayer != null
&& queue.size() == 1
&& playQueue != null
&& playQueue.getItem() != null
&& queue.getItem().getUrl().equals(playQueue.getItem().getUrl())
&& queue.getItem().getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET
) {
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);
@@ -307,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();
@@ -604,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;
}
@@ -710,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;
@@ -739,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
@@ -1006,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() {
@@ -1214,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;
@@ -46,6 +48,7 @@ import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.PopupMenu;
@@ -75,6 +78,7 @@ import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.ShareUtils;
import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.ThemeHelper;
@@ -111,6 +115,8 @@ public final class MainVideoPlayer extends AppCompatActivity
private boolean isInMultiWindow;
private boolean isBackPressed;
private ContentObserver rotationObserver;
/*//////////////////////////////////////////////////////////////////////////
// Activity LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@@ -145,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
@@ -237,10 +260,24 @@ public final class MainVideoPlayer extends AppCompatActivity
playerState = createPlayerState();
playerImpl.destroy();
if (rotationObserver != null)
getContentResolver().unregisterContentObserver(rotationObserver);
isInMultiWindow = false;
isBackPressed = false;
}
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(newBase));
}
@Override
protected void onPause() {
playerImpl.savePlaybackState();
super.onPause();
}
/*//////////////////////////////////////////////////////////////////////////
// State Saving
//////////////////////////////////////////////////////////////////////////*/
@@ -277,14 +314,9 @@ public final class MainVideoPlayer extends AppCompatActivity
if (DEBUG) Log.d(TAG, "showSystemUi() called");
if (playerImpl != null && playerImpl.queueVisible) return;
final int visibility;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
} else {
visibility = View.STATUS_BAR_VISIBLE;
}
final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
@ColorInt final int systenUiColor =
@@ -353,11 +385,7 @@ public final class MainVideoPlayer extends AppCompatActivity
protected void setShuffleButton(final ImageButton shuffleButton, final boolean shuffled) {
final int shuffleAlpha = shuffled ? 255 : 77;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
shuffleButton.setImageAlpha(shuffleAlpha);
} else {
shuffleButton.setAlpha(shuffleAlpha);
}
shuffleButton.setImageAlpha(shuffleAlpha);
}
private boolean isInMultiWindow() {
@@ -397,6 +425,7 @@ public final class MainVideoPlayer extends AppCompatActivity
private ImageButton playPauseButton;
private ImageButton playPreviousButton;
private ImageButton playNextButton;
private Button closeButton;
private RelativeLayout queueLayout;
private ImageButton itemsListCloseButton;
@@ -406,6 +435,7 @@ public final class MainVideoPlayer extends AppCompatActivity
private boolean queueVisible;
private ImageButton moreOptionsButton;
private ImageButton shareButton;
private ImageButton toggleOrientationButton;
private ImageButton switchPopupButton;
private ImageButton switchBackgroundButton;
@@ -437,9 +467,11 @@ public final class MainVideoPlayer extends AppCompatActivity
this.playPauseButton = rootView.findViewById(R.id.playPauseButton);
this.playPreviousButton = rootView.findViewById(R.id.playPreviousButton);
this.playNextButton = rootView.findViewById(R.id.playNextButton);
this.closeButton = rootView.findViewById(R.id.closeButton);
this.moreOptionsButton = rootView.findViewById(R.id.moreOptionsButton);
this.secondaryControls = rootView.findViewById(R.id.secondaryControls);
this.shareButton = rootView.findViewById(R.id.share);
this.toggleOrientationButton = rootView.findViewById(R.id.toggleOrientation);
this.switchBackgroundButton = rootView.findViewById(R.id.switchBackground);
this.switchPopupButton = rootView.findViewById(R.id.switchPopup);
@@ -483,8 +515,10 @@ public final class MainVideoPlayer extends AppCompatActivity
playPauseButton.setOnClickListener(this);
playPreviousButton.setOnClickListener(this);
playNextButton.setOnClickListener(this);
closeButton.setOnClickListener(this);
moreOptionsButton.setOnClickListener(this);
shareButton.setOnClickListener(this);
toggleOrientationButton.setOnClickListener(this);
switchBackgroundButton.setOnClickListener(this);
switchPopupButton.setOnClickListener(this);
@@ -579,7 +613,8 @@ public final class MainVideoPlayer extends AppCompatActivity
this.getPlaybackSpeed(),
this.getPlaybackPitch(),
this.getPlaybackSkipSilence(),
this.getPlaybackQuality()
this.getPlaybackQuality(),
false
);
context.startService(intent);
@@ -601,7 +636,8 @@ public final class MainVideoPlayer extends AppCompatActivity
this.getPlaybackSpeed(),
this.getPlaybackPitch(),
this.getPlaybackSkipSilence(),
this.getPlaybackQuality()
this.getPlaybackQuality(),
false
);
context.startService(intent);
@@ -635,6 +671,9 @@ public final class MainVideoPlayer extends AppCompatActivity
} else if (v.getId() == moreOptionsButton.getId()) {
onMoreOptionsClicked();
} else if (v.getId() == shareButton.getId()) {
onShareClicked();
} else if (v.getId() == toggleOrientationButton.getId()) {
onScreenRotationClicked();
@@ -644,6 +683,9 @@ public final class MainVideoPlayer extends AppCompatActivity
} else if (v.getId() == switchBackgroundButton.getId()) {
onPlayBackgroundButtonClicked();
} else if (v.getId() == closeButton.getId()) {
onPlaybackShutdown();
return;
}
if (getCurrentState() != STATE_COMPLETED) {
@@ -688,6 +730,13 @@ public final class MainVideoPlayer extends AppCompatActivity
showControls(DEFAULT_CONTROLS_DURATION);
}
private void onShareClicked() {
// share video at the current time (youtube.com/watch?v=ID&t=SECONDS)
ShareUtils.shareUrl(MainVideoPlayer.this,
playerImpl.getVideoTitle(),
playerImpl.getVideoUrl() + "&t=" + String.valueOf(playerImpl.getPlaybackSeekBar().getProgress()/1000));
}
private void onScreenRotationClicked() {
if (DEBUG) Log.d(TAG, "onScreenRotationClicked() called");
toggleOrientation();
@@ -770,6 +819,7 @@ public final class MainVideoPlayer extends AppCompatActivity
super.onBlocked();
playPauseButton.setImageResource(R.drawable.ic_pause_white);
animatePlayButtons(false, 100);
animateView(closeButton, false, DEFAULT_CONTROLS_DURATION);
getRootView().setKeepScreenOn(true);
}
@@ -785,6 +835,7 @@ public final class MainVideoPlayer extends AppCompatActivity
animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 80, 0, () -> {
playPauseButton.setImageResource(R.drawable.ic_pause_white);
animatePlayButtons(true, 200);
animateView(closeButton, false, DEFAULT_CONTROLS_DURATION);
});
getRootView().setKeepScreenOn(true);
@@ -796,6 +847,7 @@ public final class MainVideoPlayer extends AppCompatActivity
animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 80, 0, () -> {
playPauseButton.setImageResource(R.drawable.ic_play_arrow_white);
animatePlayButtons(true, 200);
animateView(closeButton, false, DEFAULT_CONTROLS_DURATION);
});
showSystemUi();
@@ -815,8 +867,8 @@ public final class MainVideoPlayer extends AppCompatActivity
animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 0, 0, () -> {
playPauseButton.setImageResource(R.drawable.ic_replay_white);
animatePlayButtons(true, DEFAULT_CONTROLS_DURATION);
animateView(closeButton, true, DEFAULT_CONTROLS_DURATION);
});
getRootView().setKeepScreenOn(false);
super.onCompleted();
}
@@ -851,8 +903,8 @@ public final class MainVideoPlayer extends AppCompatActivity
if (DEBUG) Log.d(TAG, "hideControls() called with: delay = [" + delay + "]");
getControlsVisibilityHandler().removeCallbacksAndMessages(null);
getControlsVisibilityHandler().postDelayed(() ->
animateView(getControlsRoot(), false, duration, 0,
MainVideoPlayer.this::hideSystemUi),
animateView(getControlsRoot(), false, duration, 0,
MainVideoPlayer.this::hideSystemUi),
/*delayMillis=*/delay
);
}
@@ -1056,9 +1108,9 @@ public final class MainVideoPlayer extends AppCompatActivity
final int resId =
currentProgressPercent <= 0 ? R.drawable.ic_volume_off_white_72dp
: currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute_white_72dp
: currentProgressPercent < 0.75 ? R.drawable.ic_volume_down_white_72dp
: R.drawable.ic_volume_up_white_72dp;
: currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute_white_72dp
: currentProgressPercent < 0.75 ? R.drawable.ic_volume_down_white_72dp
: R.drawable.ic_volume_up_white_72dp;
playerImpl.getVolumeImageView().setImageDrawable(
AppCompatResources.getDrawable(getApplicationContext(), resId)
@@ -1082,8 +1134,8 @@ public final class MainVideoPlayer extends AppCompatActivity
final int resId =
currentProgressPercent < 0.25 ? R.drawable.ic_brightness_low_white_72dp
: currentProgressPercent < 0.75 ? R.drawable.ic_brightness_medium_white_72dp
: R.drawable.ic_brightness_high_white_72dp;
: currentProgressPercent < 0.75 ? R.drawable.ic_brightness_medium_white_72dp
: R.drawable.ic_brightness_high_white_72dp;
playerImpl.getBrightnessImageView().setImageDrawable(
AppCompatResources.getDrawable(getApplicationContext(), resId)

View File

@@ -181,6 +181,11 @@ public final class PopupVideoPlayer extends Service {
closePopup();
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base));
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
@@ -320,6 +325,7 @@ public final class PopupVideoPlayer extends Service {
isPopupClosing = true;
if (playerImpl != null) {
playerImpl.savePlaybackState();
if (playerImpl.getRootView() != null) {
windowManager.removeView(playerImpl.getRootView());
}
@@ -560,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);
@@ -626,6 +633,7 @@ public final class PopupVideoPlayer extends Service {
@Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
super.onLoadingComplete(imageUri, view, loadedImage);
if (playerImpl == null) return;
// rebuild notification here since remote view does not release bitmaps,
// causing memory leaks
resetNotification();

View File

@@ -3,7 +3,6 @@ package org.schabi.newpipe.player;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.provider.Settings;
@@ -189,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);
}
@@ -653,11 +653,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
}
final int shuffleAlpha = shuffled ? 255 : 77;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
shuffleButton.setImageAlpha(shuffleAlpha);
} else {
shuffleButton.setAlpha(shuffleAlpha);
}
shuffleButton.setImageAlpha(shuffleAlpha);
}
private void onPlaybackParameterChanged(final PlaybackParameters parameters) {

View File

@@ -212,7 +212,6 @@ public abstract class VideoPlayer extends BasePlayer
@Override
public void initListeners() {
super.initListeners();
playbackSeekBar.setOnSeekBarChangeListener(this);
playbackSpeedTextView.setOnClickListener(this);
qualityTextView.setOnClickListener(this);
@@ -316,7 +315,6 @@ public abstract class VideoPlayer extends BasePlayer
captionPopupMenu.setOnDismissListener(this);
}
private void updateStreamRelatedViews() {
if (getCurrentMetadata() == null) return;
@@ -510,11 +508,11 @@ public abstract class VideoPlayer extends BasePlayer
// Normalize mismatching language strings
final String preferredLanguage = trackSelector.getPreferredTextLanguage();
// Build UI
buildCaptionMenu(availableLanguages);
if (trackSelector.getParameters().getRendererDisabled(textRenderer) ||
preferredLanguage == null || !availableLanguages.contains(preferredLanguage)) {
preferredLanguage == null || (!availableLanguages.contains(preferredLanguage)
&& !containsCaseInsensitive(availableLanguages, preferredLanguage))) {
captionTextView.setText(R.string.caption_none);
} else {
captionTextView.setText(preferredLanguage);
@@ -522,6 +520,15 @@ public abstract class VideoPlayer extends BasePlayer
captionTextView.setVisibility(availableLanguages.isEmpty() ? View.GONE : View.VISIBLE);
}
// workaround to match normalized captions like english to English or deutsch to Deutsch
private static boolean containsCaseInsensitive(List<String> list, String toFind) {
for(String i : list){
if(i.equalsIgnoreCase(toFind))
return true;
}
return false;
}
/*//////////////////////////////////////////////////////////////////////////
// General Player
//////////////////////////////////////////////////////////////////////////*/
@@ -535,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

@@ -12,13 +12,11 @@ import android.os.Build;
import android.support.annotation.NonNull;
import android.util.Log;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.analytics.AnalyticsListener;
public class AudioReactor implements AudioManager.OnAudioFocusChangeListener,
AudioRendererEventListener {
AnalyticsListener {
private static final String TAG = "AudioFocusReactor";
@@ -42,7 +40,7 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener,
this.player = player;
this.context = context;
this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
player.addAudioDebugListener(this);
player.addAnalyticsListener(this);
if (SHOULD_BUILD_FOCUS_REQUEST) {
request = new AudioFocusRequest.Builder(FOCUS_GAIN_TYPE)
@@ -57,7 +55,7 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener,
public void dispose() {
abandonAudioFocus();
player.removeAudioDebugListener(this);
player.removeAnalyticsListener(this);
}
/*//////////////////////////////////////////////////////////////////////////
@@ -131,7 +129,7 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener,
private void onAudioFocusLossCanDuck() {
Log.d(TAG, "onAudioFocusLossCanDuck() called");
// Set the volume to 1/10 on ducking
animateAudio(player.getVolume(), DUCK_AUDIO_TO);
player.setVolume(DUCK_AUDIO_TO);
}
private void animateAudio(final float from, final float to) {
@@ -164,29 +162,12 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener,
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onAudioSessionId(int i) {
public void onAudioSessionId(EventTime eventTime, int audioSessionId) {
if (!PlayerHelper.isUsingDSP(context)) return;
final Intent intent = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION);
intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, i);
intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId);
intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.getPackageName());
context.sendBroadcast(intent);
}
@Override
public void onAudioEnabled(DecoderCounters decoderCounters) {}
@Override
public void onAudioDecoderInitialized(String s, long l, long l1) {}
@Override
public void onAudioInputFormatChanged(Format format) {}
@Override
public void onAudioSinkUnderrun(int bufferSize,
long bufferSizeMs,
long elapsedSinceLastFeedMs) {}
@Override
public void onAudioDisabled(DecoderCounters decoderCounters) {}
}

View File

@@ -33,14 +33,14 @@ import java.io.File;
public CacheFactory(@NonNull final Context context,
@NonNull final String userAgent,
@NonNull final TransferListener<? super DataSource> transferListener) {
@NonNull final TransferListener transferListener) {
this(context, userAgent, transferListener, PlayerHelper.getPreferredCacheSize(context),
PlayerHelper.getPreferredFileSize(context));
}
private CacheFactory(@NonNull final Context context,
@NonNull final String userAgent,
@NonNull final TransferListener<? super DataSource> transferListener,
@NonNull final TransferListener transferListener,
final long maxCacheSize,
final long maxFileSize) {
this.maxFileSize = maxFileSize;

View File

@@ -2,17 +2,12 @@ package org.schabi.newpipe.player.helper;
import android.content.Context;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultLoadControl;
import com.google.android.exoplayer2.LoadControl;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DefaultAllocator;
import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS;
import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_TARGET_BUFFER_BYTES;
public class LoadController implements LoadControl {
@@ -36,15 +31,10 @@ public class LoadController implements LoadControl {
final int optimalPlaybackBufferMs) {
this.initialPlaybackBufferUs = initialPlaybackBufferMs * 1000;
final DefaultAllocator allocator = new DefaultAllocator(true,
C.DEFAULT_BUFFER_SEGMENT_SIZE);
internalLoadControl = new DefaultLoadControl(allocator,
/*minBufferMs=*/minimumPlaybackbufferMs,
/*maxBufferMs=*/optimalPlaybackBufferMs,
/*bufferForPlaybackMs=*/initialPlaybackBufferMs,
/*bufferForPlaybackAfterRebufferMs=*/initialPlaybackBufferMs,
DEFAULT_TARGET_BUFFER_BYTES, DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS);
DefaultLoadControl.Builder builder = new DefaultLoadControl.Builder();
builder.setBufferDurationsMs(minimumPlaybackbufferMs, optimalPlaybackBufferMs,
initialPlaybackBufferMs, initialPlaybackBufferMs);
internalLoadControl = builder.createDefaultLoadControl();
}
/*//////////////////////////////////////////////////////////////////////////

View File

@@ -12,6 +12,7 @@ import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.TransferListener;
public class PlayerDataSource {
@@ -24,7 +25,7 @@ public class PlayerDataSource {
public PlayerDataSource(@NonNull final Context context,
@NonNull final String userAgent,
@NonNull final TransferListener<? super DataSource> transferListener) {
@NonNull final TransferListener transferListener) {
cacheDataSourceFactory = new CacheFactory(context, userAgent, transferListener);
cachelessDataSourceFactory = new DefaultDataSourceFactory(context, userAgent, transferListener);
}
@@ -32,21 +33,21 @@ public class PlayerDataSource {
public SsMediaSource.Factory getLiveSsMediaSourceFactory() {
return new SsMediaSource.Factory(new DefaultSsChunkSource.Factory(
cachelessDataSourceFactory), cachelessDataSourceFactory)
.setMinLoadableRetryCount(MANIFEST_MINIMUM_RETRY)
.setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY))
.setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS);
}
public HlsMediaSource.Factory getLiveHlsMediaSourceFactory() {
return new HlsMediaSource.Factory(cachelessDataSourceFactory)
.setAllowChunklessPreparation(true)
.setMinLoadableRetryCount(MANIFEST_MINIMUM_RETRY);
.setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY));
}
public DashMediaSource.Factory getLiveDashMediaSourceFactory() {
return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory(
cachelessDataSourceFactory), cachelessDataSourceFactory)
.setMinLoadableRetryCount(MANIFEST_MINIMUM_RETRY)
.setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS);
.setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY))
.setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS, true);
}
public SsMediaSource.Factory getSsMediaSourceFactory() {
@@ -65,7 +66,7 @@ public class PlayerDataSource {
public ExtractorMediaSource.Factory getExtractorMediaSourceFactory() {
return new ExtractorMediaSource.Factory(cacheDataSourceFactory)
.setMinLoadableRetryCount(EXTRACTOR_MINIMUM_RETRY);
.setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(EXTRACTOR_MINIMUM_RETRY));
}
public ExtractorMediaSource.Factory getExtractorMediaSourceFactory(@NonNull final String key) {

View File

@@ -14,7 +14,6 @@ import com.google.android.exoplayer2.text.CaptionStyleCompat;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.util.MimeTypes;
import org.schabi.newpipe.R;
@@ -45,7 +44,9 @@ import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MOD
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT;
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.*;
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND;
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE;
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP;
public class PlayerHelper {
private PlayerHelper() {}
@@ -68,10 +69,10 @@ public class PlayerHelper {
////////////////////////////////////////////////////////////////////////////
public static String getTimeString(int milliSeconds) {
long seconds = (milliSeconds % 60000L) / 1000L;
long minutes = (milliSeconds % 3600000L) / 60000L;
long hours = (milliSeconds % 86400000L) / 3600000L;
long days = (milliSeconds % (86400000L * 7L)) / 86400000L;
int seconds = (milliSeconds % 60000) / 1000;
int minutes = (milliSeconds % 3600000) / 60000;
int hours = (milliSeconds % 86400000) / 3600000;
int days = (milliSeconds % (86400000 * 7)) / 86400000;
stringBuilder.setLength(0);
return days > 0 ? stringFormatter.format("%d:%02d:%02d:%02d", days, hours, minutes, seconds).toString()
@@ -238,9 +239,8 @@ public class PlayerHelper {
return 60000;
}
public static TrackSelection.Factory getQualitySelector(@NonNull final Context context,
@NonNull final BandwidthMeter meter) {
return new AdaptiveTrackSelection.Factory(meter,
public static TrackSelection.Factory getQualitySelector(@NonNull final Context context) {
return new AdaptiveTrackSelection.Factory(
/*bufferDurationRequiredForQualityIncrease=*/1000,
AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,

View File

@@ -1,12 +1,13 @@
package org.schabi.newpipe.player.mediasource;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.source.BaseMediaSource;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.TransferListener;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
@@ -79,7 +80,7 @@ public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSo
}
@Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
return null;
}
@@ -88,7 +89,7 @@ public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSo
@Override
protected void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) {
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
Log.e(TAG, "Loading failed source: ", error);
}

View File

@@ -2,12 +2,13 @@ package org.schabi.newpipe.player.mediasource;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSourceEventListener;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.TransferListener;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
@@ -36,9 +37,8 @@ public class LoadedMediaSource implements ManagedMediaSource {
}
@Override
public void prepareSource(ExoPlayer player, boolean isTopLevelSource,
SourceInfoRefreshListener listener) {
source.prepareSource(player, isTopLevelSource, listener);
public void prepareSource(SourceInfoRefreshListener listener, @Nullable TransferListener mediaTransferListener) {
source.prepareSource(listener, mediaTransferListener);
}
@Override
@@ -47,8 +47,8 @@ public class LoadedMediaSource implements ManagedMediaSource {
}
@Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
return source.createPeriod(id, allocator);
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
return source.createPeriod(id, allocator, startPositionUs);
}
@Override

View File

@@ -1,5 +1,5 @@
package org.schabi.newpipe.player.mediasource;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@@ -86,21 +86,22 @@ public class ManagedMediaSourcePlaylist {
/**
* Invalidates the {@link ManagedMediaSource} at the given index by replacing it
* with a {@link PlaceholderMediaSource}.
* @see #update(int, ManagedMediaSource, Runnable)
* @see #update(int, ManagedMediaSource, Handler, Runnable)
* */
public synchronized void invalidate(final int index,
@Nullable final Handler handler,
@Nullable final Runnable finalizingAction) {
if (get(index) instanceof PlaceholderMediaSource) return;
update(index, new PlaceholderMediaSource(), finalizingAction);
update(index, new PlaceholderMediaSource(), handler, finalizingAction);
}
/**
* Updates the {@link ManagedMediaSource} in {@link ConcatenatingMediaSource}
* at the given index with a given {@link ManagedMediaSource}.
* @see #update(int, ManagedMediaSource, Runnable)
* @see #update(int, ManagedMediaSource, Handler, Runnable)
* */
public synchronized void update(final int index, @NonNull final ManagedMediaSource source) {
update(index, source, /*doNothing=*/null);
update(index, source, null, /*doNothing=*/null);
}
/**
@@ -108,9 +109,10 @@ public class ManagedMediaSourcePlaylist {
* at the given index with a given {@link ManagedMediaSource}. If the index is out of bound,
* then the replacement is ignored.
* @see ConcatenatingMediaSource#addMediaSource
* @see ConcatenatingMediaSource#removeMediaSource(int, Runnable)
* @see ConcatenatingMediaSource#removeMediaSource(int, Handler, Runnable)
* */
public synchronized void update(final int index, @NonNull final ManagedMediaSource source,
@Nullable final Handler handler,
@Nullable final Runnable finalizingAction) {
if (index < 0 || index >= internalSource.getSize()) return;
@@ -126,6 +128,6 @@ public class ManagedMediaSourcePlaylist {
// Because of the above race condition, it is thus only safe to synchronize the player
// in the finalizing action AFTER the removal is complete and the timeline has changed.
internalSource.removeMediaSource(index, finalizingAction);
internalSource.removeMediaSource(index, handler, finalizingAction);
}
}

View File

@@ -1,20 +1,21 @@
package org.schabi.newpipe.player.mediasource;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.source.BaseMediaSource;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.TransferListener;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
public class PlaceholderMediaSource extends BaseMediaSource implements ManagedMediaSource {
// Do nothing, so this will stall the playback
@Override public void maybeThrowSourceInfoRefreshError() {}
@Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { return null; }
@Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { return null; }
@Override public void releasePeriod(MediaPeriod mediaPeriod) {}
@Override protected void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) {}
@Override protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {}
@Override protected void releaseSourceInternal() {}
@Override

View File

@@ -1,7 +1,9 @@
package org.schabi.newpipe.player.playback;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.MediaMetadataCompat;
import org.schabi.newpipe.player.BasePlayer;
import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
@@ -54,6 +56,15 @@ public class BasePlayerMediaSession implements MediaSessionCallback {
.setTitle(item.getTitle())
.setSubtitle(item.getUploader());
// 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() * 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());
if (thumbnailUri != null) descriptionBuilder.setIconUri(thumbnailUri);

View File

@@ -2,6 +2,7 @@ package org.schabi.newpipe.player.playback;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.util.Pair;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
@@ -17,7 +18,7 @@ import com.google.android.exoplayer2.util.Assertions;
* is mostly a copy-paste from {@link DefaultTrackSelector}.
*
* This is a hack and should be removed once ExoPlayer fixes language normalization to accept
* a broader set of languages.
* a broader set of languages.
* */
public class CustomTrackSelector extends DefaultTrackSelector {
private static final int WITHIN_RENDERER_CAPABILITIES_BONUS = 1000;
@@ -52,8 +53,8 @@ public class CustomTrackSelector extends DefaultTrackSelector {
/** @see DefaultTrackSelector#selectTextTrack(TrackGroupArray, int[][], Parameters) */
@Override
protected TrackSelection selectTextTrack(TrackGroupArray groups, int[][] formatSupport,
Parameters params) {
protected Pair<TrackSelection, Integer> selectTextTrack(TrackGroupArray groups, int[][] formatSupport,
Parameters params) {
TrackGroup selectedGroup = null;
int selectedTrackIndex = 0;
int selectedTrackScore = 0;
@@ -106,7 +107,9 @@ public class CustomTrackSelector extends DefaultTrackSelector {
}
}
}
return selectedGroup == null ? null
: new FixedTrackSelection(selectedGroup, selectedTrackIndex);
return selectedGroup == null
? null
: Pair.create(
new FixedTrackSelection(selectedGroup, selectedTrackIndex), selectedTrackScore);
}
}
}

View File

@@ -1,5 +1,5 @@
package org.schabi.newpipe.player.playback;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.util.ArraySet;
@@ -103,6 +103,8 @@ public class MediaSourceManager {
@NonNull private ManagedMediaSourcePlaylist playlist;
private Handler removeMediaSourceHandler = new Handler();
public MediaSourceManager(@NonNull final PlaybackListener listener,
@NonNull final PlayQueue playQueue) {
this(listener, playQueue, /*loadDebounceMillis=*/400L,
@@ -156,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();
@@ -395,7 +397,7 @@ public class MediaSourceManager {
if (isCorrectionNeeded(item)) {
if (DEBUG) Log.d(TAG, "MediaSource - Updating index=[" + itemIndex + "] with " +
"title=[" + item.getTitle() + "] at url=[" + item.getUrl() + "]");
playlist.update(itemIndex, mediaSource, this::maybeSynchronizePlayer);
playlist.update(itemIndex, mediaSource, removeMediaSourceHandler, this::maybeSynchronizePlayer);
}
}
@@ -441,7 +443,7 @@ public class MediaSourceManager {
if (DEBUG) Log.d(TAG, "MediaSource - Reloading currently playing, " +
"index=[" + currentIndex + "], item=[" + currentItem.getTitle() + "]");
playlist.invalidate(currentIndex, this::loadImmediate);
playlist.invalidate(currentIndex, removeMediaSourceHandler, this::loadImmediate);
}
private void maybeClearLoaders() {

View File

@@ -16,6 +16,11 @@ public final class SinglePlayQueue extends PlayQueue {
super(0, Collections.singletonList(new PlayQueueItem(info)));
}
public SinglePlayQueue(final StreamInfo info, final long startPosition) {
super(0, Collections.singletonList(new PlayQueueItem(info)));
getItem().setRecoveryPosition(startPosition);
}
public SinglePlayQueue(final List<StreamInfoItem> items, final int index) {
super(index, playQueueItemsOf(items));
}

View File

@@ -13,6 +13,7 @@ import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
import org.schabi.newpipe.extractor.stream.AudioStream;
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.player.helper.PlayerDataSource;
import org.schabi.newpipe.player.helper.PlayerHelper;
@@ -93,15 +94,17 @@ public class VideoPlaybackResolver implements PlaybackResolver {
// Below are auxiliary media sources
// Create subtitle sources
for (final SubtitlesStream subtitle : info.getSubtitles()) {
final String mimeType = PlayerHelper.subtitleMimeTypesOf(subtitle.getFormat());
if (mimeType == null) continue;
if(info.getSubtitles() != null) {
for (final SubtitlesStream subtitle : info.getSubtitles()) {
final String mimeType = PlayerHelper.subtitleMimeTypesOf(subtitle.getFormat());
if (mimeType == null) continue;
final Format textFormat = Format.createTextSampleFormat(null, mimeType,
SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle));
final MediaSource textSource = dataSource.getSampleMediaSourceFactory()
.createMediaSource(Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET);
mediaSources.add(textSource);
final Format textFormat = Format.createTextSampleFormat(null, mimeType,
SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle));
final MediaSource textSource = dataSource.getSampleMediaSourceFactory()
.createMediaSource(Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET);
mediaSources.add(textSource);
}
}
if (mediaSources.size() == 1) {

View File

@@ -15,8 +15,11 @@ public enum UserAction {
REQUESTED_CHANNEL("requested channel"),
REQUESTED_PLAYLIST("requested playlist"),
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

@@ -25,6 +25,7 @@ import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.content.res.ColorStateList;
import android.support.annotation.ColorInt;
import android.support.annotation.FloatRange;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.util.Log;
@@ -363,4 +364,24 @@ public class AnimationUtils {
}).start();
}
}
public static void slideUp(final View view,
long duration,
long delay,
@FloatRange(from = 0.0f, to = 1.0f) float translationPercent) {
int translationY = (int) (view.getResources().getDisplayMetrics().heightPixels *
(translationPercent));
view.animate().setListener(null).cancel();
view.setAlpha(0f);
view.setTranslationY(translationY);
view.setVisibility(View.VISIBLE);
view.animate()
.alpha(1f)
.translationY(0)
.setStartDelay(delay)
.setDuration(duration)
.setInterpolator(new FastOutSlowInInterpolator())
.start();
}
}

View File

@@ -0,0 +1,131 @@
package org.schabi.newpipe.util;
import android.content.Context;
import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
import android.text.Spanned;
import android.text.style.ClickableSpan;
import android.text.style.URLSpan;
import android.view.MotionEvent;
import android.view.View;
import android.widget.TextView;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.LinkHandlerFactory;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
public class CommentTextOnTouchListener implements View.OnTouchListener {
public static final CommentTextOnTouchListener INSTANCE = new CommentTextOnTouchListener();
private static final Pattern timestampPattern = Pattern.compile("(.*)#timestamp=(\\d+)");
@Override
public boolean onTouch(View v, MotionEvent event) {
if(!(v instanceof TextView)){
return false;
}
TextView widget = (TextView) v;
Object text = widget.getText();
if (text instanceof Spanned) {
Spannable buffer = (Spannable) text;
int action = event.getAction();
if (action == MotionEvent.ACTION_UP
|| action == MotionEvent.ACTION_DOWN) {
int x = (int) event.getX();
int y = (int) event.getY();
x -= widget.getTotalPaddingLeft();
y -= widget.getTotalPaddingTop();
x += widget.getScrollX();
y += widget.getScrollY();
Layout layout = widget.getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
ClickableSpan[] link = buffer.getSpans(off, off,
ClickableSpan.class);
if (link.length != 0) {
if (action == MotionEvent.ACTION_UP) {
boolean handled = false;
if(link[0] instanceof URLSpan){
handled = handleUrl(v.getContext(), (URLSpan) link[0]);
}
if(!handled) link[0].onClick(widget);
} else if (action == MotionEvent.ACTION_DOWN) {
Selection.setSelection(buffer,
buffer.getSpanStart(link[0]),
buffer.getSpanEnd(link[0]));
}
return true;
}
}
}
return false;
}
private boolean handleUrl(Context context, URLSpan urlSpan) {
String url = urlSpan.getURL();
int seconds = -1;
Matcher matcher = timestampPattern.matcher(url);
if(matcher.matches()){
url = matcher.group(1);
seconds = Integer.parseInt(matcher.group(2));
}
StreamingService service;
StreamingService.LinkType linkType;
try {
service = NewPipe.getServiceByUrl(url);
linkType = service.getLinkTypeByUrl(url);
} catch (ExtractionException e) {
return false;
}
if(linkType == StreamingService.LinkType.NONE){
return false;
}
if(linkType == StreamingService.LinkType.STREAM && seconds != -1){
return playOnPopup(context, url, service, seconds);
}else{
NavigationHelper.openRouterActivity(context, url);
return true;
}
}
private boolean playOnPopup(Context context, String url, StreamingService service, int seconds) {
LinkHandlerFactory factory = service.getStreamLHFactory();
String cleanUrl = null;
try {
cleanUrl = factory.getUrl(factory.getId(url));
} catch (ParsingException e) {
return false;
}
Single single = ExtractorHelper.getStreamInfo(service.getServiceId(), cleanUrl, false);
single.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(info -> {
PlayQueue playQueue = new SinglePlayQueue((StreamInfo) info, seconds*1000);
NavigationHelper.playOnPopupPlayer(context, playQueue, false);
});
return true;
}
}

View File

@@ -29,10 +29,12 @@ import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.extractor.Info;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.SuggestionExtractor;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfo;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
@@ -46,6 +48,7 @@ import org.schabi.newpipe.report.UserAction;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.util.Collections;
import java.util.List;
import io.reactivex.Maybe;
@@ -60,7 +63,7 @@ public final class ExtractorHelper {
}
private static void checkServiceId(int serviceId) {
if(serviceId == Constants.NO_SERVICE_ID) {
if (serviceId == Constants.NO_SERVICE_ID) {
throw new IllegalArgumentException("serviceId is NO_SERVICE_ID");
}
}
@@ -95,17 +98,20 @@ public final class ExtractorHelper {
public static Single<List<String>> suggestionsFor(final int serviceId,
final String query) {
checkServiceId(serviceId);
return Single.fromCallable(() ->
NewPipe.getService(serviceId)
.getSuggestionExtractor()
.suggestionList(query));
return Single.fromCallable(() -> {
SuggestionExtractor extractor = NewPipe.getService(serviceId)
.getSuggestionExtractor();
return extractor != null
? extractor.suggestionList(query)
: Collections.emptyList();
});
}
public static Single<StreamInfo> getStreamInfo(final int serviceId,
final String url,
boolean forceLoad) {
checkServiceId(serviceId);
return checkCache(forceLoad, serviceId, url, Single.fromCallable(() ->
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.STREAM, Single.fromCallable(() ->
StreamInfo.getInfo(NewPipe.getService(serviceId), url)));
}
@@ -113,29 +119,45 @@ public final class ExtractorHelper {
final String url,
boolean forceLoad) {
checkServiceId(serviceId);
return checkCache(forceLoad, serviceId, url, Single.fromCallable(() ->
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.CHANNEL, Single.fromCallable(() ->
ChannelInfo.getInfo(NewPipe.getService(serviceId), url)));
}
public static Single<InfoItemsPage> getMoreChannelItems(final int serviceId,
final String url,
final String nextStreamsUrl) {
final String url,
final String nextStreamsUrl) {
checkServiceId(serviceId);
return Single.fromCallable(() ->
ChannelInfo.getMoreItems(NewPipe.getService(serviceId), url, nextStreamsUrl));
}
public static Single<CommentsInfo> getCommentsInfo(final int serviceId,
final String url,
boolean forceLoad) {
checkServiceId(serviceId);
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.COMMENT, Single.fromCallable(() ->
CommentsInfo.getInfo(NewPipe.getService(serviceId), url)));
}
public static Single<InfoItemsPage> getMoreCommentItems(final int serviceId,
final CommentsInfo info,
final String nextPageUrl) {
checkServiceId(serviceId);
return Single.fromCallable(() ->
CommentsInfo.getMoreItems(NewPipe.getService(serviceId), info, nextPageUrl));
}
public static Single<PlaylistInfo> getPlaylistInfo(final int serviceId,
final String url,
boolean forceLoad) {
checkServiceId(serviceId);
return checkCache(forceLoad, serviceId, url, Single.fromCallable(() ->
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.PLAYLIST, Single.fromCallable(() ->
PlaylistInfo.getInfo(NewPipe.getService(serviceId), url)));
}
public static Single<InfoItemsPage> getMorePlaylistItems(final int serviceId,
final String url,
final String nextStreamsUrl) {
final String url,
final String nextStreamsUrl) {
checkServiceId(serviceId);
return Single.fromCallable(() ->
PlaylistInfo.getMoreItems(NewPipe.getService(serviceId), url, nextStreamsUrl));
@@ -144,7 +166,7 @@ public final class ExtractorHelper {
public static Single<KioskInfo> getKioskInfo(final int serviceId,
final String url,
boolean forceLoad) {
return checkCache(forceLoad, serviceId, url, Single.fromCallable(() ->
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.PLAYLIST, Single.fromCallable(() ->
KioskInfo.getInfo(NewPipe.getService(serviceId), url)));
}
@@ -168,16 +190,17 @@ public final class ExtractorHelper {
private static <I extends Info> Single<I> checkCache(boolean forceLoad,
int serviceId,
String url,
InfoItem.InfoType infoType,
Single<I> loadFromNetwork) {
checkServiceId(serviceId);
loadFromNetwork = loadFromNetwork.doOnSuccess(info -> cache.putInfo(serviceId, url, info));
loadFromNetwork = loadFromNetwork.doOnSuccess(info -> cache.putInfo(serviceId, url, info, infoType));
Single<I> load;
if (forceLoad) {
cache.removeInfo(serviceId, url);
cache.removeInfo(serviceId, url, infoType);
load = loadFromNetwork;
} else {
load = Maybe.concat(ExtractorHelper.loadFromCache(serviceId, url),
load = Maybe.concat(ExtractorHelper.loadFromCache(serviceId, url, infoType),
loadFromNetwork.toMaybe())
.firstElement() //Take the first valid
.toSingle();
@@ -189,20 +212,24 @@ public final class ExtractorHelper {
/**
* Default implementation uses the {@link InfoCache} to get cached results
*/
public static <I extends Info> Maybe<I> loadFromCache(final int serviceId, final String url) {
public static <I extends Info> Maybe<I> loadFromCache(final int serviceId, final String url, InfoItem.InfoType infoType) {
checkServiceId(serviceId);
return Maybe.defer(() -> {
//noinspection unchecked
I info = (I) cache.getFromKey(serviceId, url);
if (MainActivity.DEBUG) Log.d(TAG, "loadFromCache() called, info > " + info);
//noinspection unchecked
I info = (I) cache.getFromKey(serviceId, url, infoType);
if (MainActivity.DEBUG) Log.d(TAG, "loadFromCache() called, info > " + info);
// Only return info if it's not null (it is cached)
if (info != null) {
return Maybe.just(info);
}
// Only return info if it's not null (it is cached)
if (info != null) {
return Maybe.just(info);
}
return Maybe.empty();
});
return Maybe.empty();
});
}
public static boolean isCached(final int serviceId, final String url, InfoItem.InfoType infoType) {
return null != loadFromCache(serviceId, url, infoType).blockingGet();
}
/**
@@ -220,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

@@ -0,0 +1,10 @@
package org.schabi.newpipe.util;
import org.schabi.newpipe.App;
public class FireTvUtils {
public static boolean isFireTv(){
final String AMAZON_FEATURE_FIRE_TV = "amazon.hardware.fire_tv";
return App.getApp().getPackageManager().hasSystemFeature(AMAZON_FEATURE_FIRE_TV);
}
}

View File

@@ -26,6 +26,7 @@ import android.util.Log;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.extractor.Info;
import org.schabi.newpipe.extractor.InfoItem;
import java.util.Map;
@@ -52,27 +53,27 @@ public final class InfoCache {
}
@Nullable
public Info getFromKey(int serviceId, @NonNull String url) {
public Info getFromKey(int serviceId, @NonNull String url, @NonNull InfoItem.InfoType infoType) {
if (DEBUG) Log.d(TAG, "getFromKey() called with: serviceId = [" + serviceId + "], url = [" + url + "]");
synchronized (lruCache) {
return getInfo(keyOf(serviceId, url));
return getInfo(keyOf(serviceId, url, infoType));
}
}
public void putInfo(int serviceId, @NonNull String url, @NonNull Info info) {
public void putInfo(int serviceId, @NonNull String url, @NonNull Info info, @NonNull InfoItem.InfoType infoType) {
if (DEBUG) Log.d(TAG, "putInfo() called with: info = [" + info + "]");
final long expirationMillis = ServiceHelper.getCacheExpirationMillis(info.getServiceId());
synchronized (lruCache) {
final CacheData data = new CacheData(info, expirationMillis);
lruCache.put(keyOf(serviceId, url), data);
lruCache.put(keyOf(serviceId, url, infoType), data);
}
}
public void removeInfo(int serviceId, @NonNull String url) {
public void removeInfo(int serviceId, @NonNull String url, @NonNull InfoItem.InfoType infoType) {
if (DEBUG) Log.d(TAG, "removeInfo() called with: serviceId = [" + serviceId + "], url = [" + url + "]");
synchronized (lruCache) {
lruCache.remove(keyOf(serviceId, url));
lruCache.remove(keyOf(serviceId, url, infoType));
}
}
@@ -98,8 +99,8 @@ public final class InfoCache {
}
@NonNull
private static String keyOf(final int serviceId, @NonNull final String url) {
return serviceId + url;
private static String keyOf(final int serviceId, @NonNull final String url, @NonNull InfoItem.InfoType infoType) {
return serviceId + url + infoType.toString();
}
private static void removeStaleCache() {

View File

@@ -31,6 +31,8 @@ public class KioskTranslator {
return c.getString(R.string.top_50);
case "New & hot":
return c.getString(R.string.new_and_hot);
case "conferences":
return c.getString(R.string.conferences);
default:
return kioskId;
}
@@ -44,6 +46,8 @@ public class KioskTranslator {
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot);
case "New & hot":
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot);
case "conferences":
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot);
default:
return 0;
}

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();
}
}

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