1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2026-01-13 18:22:41 +00:00

Compare commits

..

478 Commits

Author SHA1 Message Date
TobiGr
2152375227 Bump version to 0.18.2 [hotfix release] 2020-01-24 02:30:08 +01:00
TobiGr
10d57afaac Update extractor version 2020-01-24 02:29:21 +01:00
Tobias Groza
42ff60ce85 Merge pull request #2956 from TeamNewPipe/release_v0.18.1
Release v0.18.1
2020-01-19 09:54:05 +01:00
TobiGr
fd62411b35 Bump version to 0.18.1 and version code to 810 2020-01-18 00:09:40 +01:00
TobiGr
134850aa04 Add changelog for 0.18.1 2020-01-18 00:09:27 +01:00
TobiGr
0795135f2f Merge remote-tracking branch 'Weblate/dev' into dev 2020-01-18 00:01:55 +01:00
Tobias Groza
85eb1dc436 Merge pull request #2966 from B0pol/c_links_support
[YouTube] /c/ channel links support & update extractor version
2020-01-17 23:59:41 +01:00
B0pol
2f2b8784f9 update extractor version 2020-01-17 23:07:45 +01:00
bopol
181658e5a4 support for opening /c/ channel links 2020-01-17 22:59:51 +01:00
WaldiS
d949894511 Translated using Weblate (Polish)
Currently translated at 100.0% (527 of 527 strings)
2020-01-17 10:02:16 +01:00
Igor Nedoboy
235ead9222 Translated using Weblate (Russian)
Currently translated at 100.0% (527 of 527 strings)
2020-01-16 20:57:01 +01:00
nautilusx
3ee6788753 Translated using Weblate (German)
Currently translated at 100.0% (527 of 527 strings)
2020-01-16 14:15:09 +01:00
Deleted User
e3dfab5078 Translated using Weblate (Norwegian Bokmål)
Currently translated at 96.4% (508 of 527 strings)
2020-01-16 06:28:38 +01:00
Allan Nordhøy
5f232a059d Translated using Weblate (Norwegian Bokmål)
Currently translated at 96.4% (508 of 527 strings)
2020-01-16 06:28:38 +01:00
Loís B
1b708d261d Translated using Weblate (Occitan)
Currently translated at 18.8% (99 of 527 strings)
2020-01-15 20:36:11 +01:00
Yaron Shahrabani
3341742f66 Translated using Weblate (Hebrew)
Currently translated at 100.0% (527 of 527 strings)
2020-01-15 20:36:11 +01:00
Oğuz Ersen
b731c79339 Translated using Weblate (Turkish)
Currently translated at 100.0% (527 of 527 strings)
2020-01-15 20:36:10 +01:00
Igor Nedoboy
29b12c2f84 Translated using Weblate (Russian)
Currently translated at 100.0% (527 of 527 strings)
2020-01-15 20:36:09 +01:00
chr56
ba53f6611c Translated using Weblate (Chinese (Simplified))
Currently translated at 97.3% (513 of 527 strings)
2020-01-15 10:10:31 +01:00
Hosted Weblate
6eeed50418 Merge branch 'origin/dev' into Weblate. 2020-01-15 09:42:51 +01:00
Loís B
8caf9f87a1 Translated using Weblate (Occitan)
Currently translated at 17.9% (94 of 525 strings)
2020-01-15 09:42:49 +01:00
B0pol
2e6089088b Translated using Weblate (Urdu)
Currently translated at 95.0% (499 of 525 strings)
2020-01-15 09:42:46 +01:00
Matsuri
40eaa166ae Translated using Weblate (Chinese (Simplified))
Currently translated at 97.3% (511 of 525 strings)
2020-01-15 09:42:45 +01:00
B0pol
41e2e5f951 Translated using Weblate (Esperanto)
Currently translated at 100.0% (525 of 525 strings)
2020-01-15 09:42:44 +01:00
B0pol
ac3938d529 Translated using Weblate (French)
Currently translated at 100.0% (525 of 525 strings)
2020-01-15 09:42:43 +01:00
DodoLeDev
515ec4d66d Translated using Weblate (French)
Currently translated at 100.0% (525 of 525 strings)
2020-01-15 09:42:43 +01:00
Nehemias Feliz
5adc27ea2b Translated using Weblate (Spanish)
Currently translated at 100.0% (525 of 525 strings)
2020-01-15 09:42:42 +01:00
C. Rüdinger
ab089a5f93 Translated using Weblate (German)
Currently translated at 100.0% (525 of 525 strings)
2020-01-15 09:42:41 +01:00
TobiGr
9f47a274a8 Update extractor version
TeamNewPipe/NewPipeExtractor@bdbfa26835
2020-01-13 20:40:11 +01:00
TobiGr
1f8c0a9e5e Merge remote-tracking branch 'Weblate/dev' into dev 2020-01-13 20:33:24 +01:00
Tobias Groza
cef9ccd937 Merge pull request #2717 from kszczek/delete-finished-downloads
Add option to delete files when clearing finished downloads
2020-01-13 20:32:18 +01:00
TobiGr
3d93ecd6ec Use Integer value directly for formatted string
a
2020-01-13 20:25:32 +01:00
Loís B
36e38e50e9 Translated using Weblate (Occitan)
Currently translated at 10.1% (53 of 525 strings)
2020-01-13 15:49:01 +01:00
Software In Interlingua
1a8be2bbf5 Translated using Weblate (Interlingua)
Currently translated at 11.4% (60 of 525 strings)
2020-01-13 15:48:59 +01:00
Allan Nordhøy
92b1fa5743 Translated using Weblate (Norwegian Bokmål)
Currently translated at 96.2% (505 of 525 strings)
2020-01-13 15:48:58 +01:00
MohammedSR Vevo
335e5c05db Translated using Weblate (Kurdish)
Currently translated at 100.0% (525 of 525 strings)
2020-01-13 15:48:58 +01:00
Yaron Shahrabani
bfead79c07 Translated using Weblate (Hebrew)
Currently translated at 100.0% (525 of 525 strings)
2020-01-13 15:48:57 +01:00
Jeff Huang
bd8014bcbd Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (525 of 525 strings)
2020-01-13 15:48:57 +01:00
Matsuri
948d57d3d1 Translated using Weblate (Chinese (Simplified))
Currently translated at 97.3% (511 of 525 strings)
2020-01-13 15:48:56 +01:00
Daniele Lira Mereb
31b830d6d0 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (525 of 525 strings)
2020-01-13 15:48:56 +01:00
WaldiS
2038df976c Translated using Weblate (Polish)
Currently translated at 100.0% (525 of 525 strings)
2020-01-13 15:48:54 +01:00
Oğuz Ersen
46cc215120 Translated using Weblate (Turkish)
Currently translated at 100.0% (525 of 525 strings)
2020-01-13 15:48:53 +01:00
zmni
3afce82aa7 Translated using Weblate (Indonesian)
Currently translated at 99.6% (523 of 525 strings)
2020-01-13 15:48:52 +01:00
B0pol
88e5be237e Translated using Weblate (Esperanto)
Currently translated at 100.0% (525 of 525 strings)
2020-01-13 15:48:51 +01:00
Petros Grammatikopoulos
7c4b9d8843 Translated using Weblate (Greek)
Currently translated at 97.7% (513 of 525 strings)
2020-01-13 15:48:50 +01:00
Osoitz
b83e1716fe Translated using Weblate (Basque)
Currently translated at 99.4% (522 of 525 strings)
2020-01-13 15:48:49 +01:00
ssantos
c3e41e2427 Translated using Weblate (Portuguese)
Currently translated at 100.0% (525 of 525 strings)
2020-01-13 15:48:49 +01:00
B0pol
3f67b3b73c Translated using Weblate (French)
Currently translated at 100.0% (525 of 525 strings)
2020-01-13 15:48:48 +01:00
nautilusx
78c9e4e1ad Translated using Weblate (German)
Currently translated at 100.0% (525 of 525 strings)
2020-01-13 15:48:48 +01:00
C. Rüdinger
69c090b5a1 Translated using Weblate (German)
Currently translated at 100.0% (525 of 525 strings)
2020-01-13 15:48:47 +01:00
Kamil Szczęk
2c8222fd55 Style corrected 2020-01-11 15:19:24 +01:00
Kamil Szczęk
d071891b2a Add a snackbar to allow user to undo file deletion 2020-01-11 15:09:01 +01:00
Kamil Szczęk
986acc5fc5 Reorder buttons in clear downloads dialog 2020-01-11 15:09:01 +01:00
Kamil Szczęk
e4295fb3fa Use a dialog option instead of a checkbox 2020-01-11 15:09:01 +01:00
Kamil Szczęk
cfad3fb5de Fix inconsistent style 2020-01-11 15:09:01 +01:00
Kamil Szczęk
b18236a27e Put call to forget finished downloads in an else statement to prevent potential bugs 2020-01-11 15:09:01 +01:00
Kamil Szczęk
f6bbc69cf9 Remove unnecessary import 2020-01-11 15:09:01 +01:00
Kamil Szczęk
707e4f7167 Add option to remove downloaded files when clearing finished downloads 2020-01-11 15:09:01 +01:00
TobiGr
99cdaec40e Translated using Weblate (German)
Currently translated at 99.0% (520 of 525 strings)
2020-01-09 17:23:02 +01:00
Igor Nedoboy
bff5371e41 Translated using Weblate (Russian)
Currently translated at 100.0% (525 of 525 strings)
2020-01-09 02:51:05 +01:00
Hosted Weblate
a76398efd0 Merge branch 'origin/dev' into Weblate. 2020-01-08 20:51:08 +01:00
Allan Nordhøy
99bcd8d043 Translated using Weblate (Norwegian Bokmål)
Currently translated at 96.0% (501 of 522 strings)
2020-01-08 20:51:04 +01:00
Software In Interlingua
1602ecbaf9 Translated using Weblate (Interlingua)
Currently translated at 11.1% (58 of 522 strings)
2020-01-08 20:51:04 +01:00
Tobias Groza
7e17bdf369 Merge pull request #2935 from comradekingu/patch-9
Spelling: Could not, PeerTube, HTTPS, URL
2020-01-08 19:09:43 +01:00
Allan Nordhøy
d316bbad44 Select your favorite
Co-Authored-By: Stypox <stypox@pm.me>
2020-01-08 18:22:17 +01:00
Tobias Groza
72151c8c0c Merge pull request #2906 from kapodamy/opus-fixup
add opus file extension
2020-01-08 18:19:23 +01:00
Allan Nordhøy
e2e0a9bfa2 Spelling: Could not, PeerTube, HTTPS, URL 2020-01-08 17:51:35 +01:00
kapodamy
8d53b07167 fixup
* [DownloadDialog.java] use *.opus extension instead of *.webm (bad change from 844f80a5f1)
* [StreamItemAdapter.java] show "opus" in format label instead of "WebM Opus"
2020-01-08 12:42:34 -03:00
Tobias Groza
1df852171d Merge pull request #2837 from decarvalhobo/dev
Playlist thumbnail updates
2020-01-07 23:02:42 +01:00
TobiGr
399e2626fb Fix code style and improve imports 2020-01-07 22:48:35 +01:00
decarvalhobo
752a76eb44 Usage of drawable instead of remote image + refactor the append to an empty playlist by just updating the thumbnail before adding the item in it. 2020-01-07 22:33:45 +01:00
De Carvalho Marcio Antonio
8feee05eec remove comments 2020-01-07 22:33:45 +01:00
De Carvalho Marcio Antonio
e9a4caaf0b remove comments 2020-01-07 22:33:45 +01:00
decarvalhobo
8de367e03f fix issue: thumbnail update when element deleted + thumbnail update when element added and no thumbnail 2020-01-07 22:33:45 +01:00
TobiGr
dad88b83fb Fix Arabic translation 2020-01-07 22:33:30 +01:00
TobiGr
846f7f2f05 Merge remote-tracking branch 'Weblate/dev' into dev 2020-01-07 22:28:15 +01:00
pjammo
41e18ae694 Translated using Weblate (Italian)
Currently translated at 100.0% (522 of 522 strings)
2020-01-07 17:21:22 +01:00
Daniele Lira Mereb
deeac118a1 Translated using Weblate (Portuguese (Brazil))
Currently translated at 97.3% (508 of 522 strings)
2020-01-07 17:21:21 +01:00
MohammedSR Vevo
594d77e713 Translated using Weblate (Kurdish)
Currently translated at 100.0% (522 of 522 strings)
2020-01-06 00:21:31 +01:00
JoC
2ea404659b Translated using Weblate (Spanish)
Currently translated at 100.0% (522 of 522 strings)
2020-01-06 00:21:28 +01:00
Software In Interlingua
e2ec95e6ff Translated using Weblate (Interlingua)
Currently translated at 10.9% (57 of 522 strings)
2020-01-06 00:21:28 +01:00
pietrasagh
db87df743d Translated using Weblate (Polish)
Currently translated at 100.0% (522 of 522 strings)
2020-01-06 00:21:27 +01:00
ssantos
23f9ffdab7 Translated using Weblate (Portuguese)
Currently translated at 100.0% (522 of 522 strings)
2020-01-06 00:21:26 +01:00
Enol P
28063c35c2 Translated using Weblate (Asturian)
Currently translated at 49.2% (257 of 522 strings)
2020-01-06 00:21:25 +01:00
Иван
21a39b06e7 Translated using Weblate (Ukrainian)
Currently translated at 98.1% (512 of 522 strings)
2020-01-06 00:21:23 +01:00
Osoitz
8fb29ae6c2 Translated using Weblate (Basque)
Currently translated at 99.8% (521 of 522 strings)
2020-01-06 00:21:22 +01:00
B0pol
21895caa3a Translated using Weblate (Esperanto)
Currently translated at 100.0% (522 of 522 strings)
2020-01-06 00:21:20 +01:00
THANOS SIOURDAKIS
f17b92512c Translated using Weblate (Greek)
Currently translated at 97.9% (511 of 522 strings)
2020-01-06 00:21:19 +01:00
ssantos
014682664d Translated using Weblate (German)
Currently translated at 100.0% (522 of 522 strings)
2020-01-06 00:21:18 +01:00
Tobias Groza
5bf1df9f14 Merge pull request #2690 from K1rakishou/(#1570)-lock-screen-video-thumbnail
Show video thumbnail on the lock screen
2020-01-05 10:36:38 +01:00
k1rakishou
eb5fb42da9 Couple more code review changes 2020-01-03 16:29:04 +03:00
k1rakishou
c46a0f7b2e Code-review changes 2020-01-03 13:00:53 +03:00
k1rakishou
835476870b Merge remote-tracking branch 'push_here/(#1570)-lock-screen-video-thumbnail' into (#1570)-lock-screen-video-thumbnail
# Conflicts:
#	app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java
#	app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java
#	app/src/main/java/org/schabi/newpipe/util/BitmapUtils.java
#	app/src/main/res/xml/video_audio_settings.xml
2020-01-03 12:56:01 +03:00
TobiGr
718acb5059 Code improvements 2020-01-02 15:00:31 +01:00
TobiGr
1aa763e86c Enable lockscreen video thumbnail by default 2020-01-02 15:00:31 +01:00
k1rakishou
0395dc6e9e Add a setting for the lock screen thumbnail feature 2020-01-02 15:00:31 +01:00
k1rakishou
96de70b71e Rebase onto the latest dev, update appcompat dependencies to use androidx 2020-01-02 15:00:31 +01:00
k1rakishou
f44883e79f Show video thumbnail on the lock screen 2020-01-02 15:00:31 +01:00
Tobias Groza
c56fb8cec2 Merge pull request #2871 from atpamat/main-window-tabs
make main page tabs scrollable and hide when there is only a single tab
2020-01-02 13:22:01 +01:00
Paweł Matuszewski
3625a38a23 improve code consistency in ScrollableTabLayout 2020-01-02 13:02:14 +01:00
Paweł Matuszewski
1393d3ad7f fix ScrollableTabLayout content width calculation
fix bug where only minimum width requested by tab was counted even if actual content was wider
2020-01-02 13:02:14 +01:00
Paweł Matuszewski
b674cfec24 simplify ScrollableTabLayout tabs width checking 2020-01-02 13:02:14 +01:00
Paweł Matuszewski
f0f0c43b72 hide main page tab selector with single tab 2020-01-02 13:02:14 +01:00
Paweł Matuszewski
33caad4690 make main page tabs scrollable 2020-01-02 13:02:14 +01:00
Tobias Groza
0afc8005d0 Merge pull request #2771 from atpamat/background-player-notif-lag
Limit amount of notification thumbnail updates in background player
2020-01-01 23:01:11 +01:00
Yaron Shahrabani
f04d2e76fa Translated using Weblate (Hebrew)
Currently translated at 100.0% (522 of 522 strings)
2020-01-01 19:12:04 +01:00
Igor Nedoboy
8e1d7f162d Translated using Weblate (Russian)
Currently translated at 100.0% (522 of 522 strings)
2020-01-01 19:12:01 +01:00
Paweł Matuszewski
ee65e89230 limit amount of notification thumbnail updates
limits amount of calls to updateNotificationThumbnail in background player
2020-01-01 16:38:46 +01:00
Matsuri
a8e26238a8 Translated using Weblate (Chinese (Simplified))
Currently translated at 98.7% (515 of 522 strings)
2020-01-01 13:26:15 +01:00
MohammedSR Vevo
a3dc95bef1 Translated using Weblate (Kurdish)
Currently translated at 99.8% (521 of 522 strings)
2020-01-01 13:26:15 +01:00
Software In Interlingua
a29df9a2dd Translated using Weblate (Interlingua)
Currently translated at 9.4% (49 of 522 strings)
2020-01-01 13:26:14 +01:00
Yaron Shahrabani
cc17d268fc Translated using Weblate (Hebrew)
Currently translated at 100.0% (522 of 522 strings)
2020-01-01 13:26:11 +01:00
Ariel Shulman
22a9a06b87 Translated using Weblate (Hebrew)
Currently translated at 100.0% (522 of 522 strings)
2020-01-01 13:26:09 +01:00
chr56
d063d39dbc Translated using Weblate (Chinese (Simplified))
Currently translated at 98.7% (515 of 522 strings)
2019-12-31 19:01:41 +01:00
Jeff Huang
87e29dbd84 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (522 of 522 strings)
2019-12-31 19:01:40 +01:00
MohammedSR Vevo
2227a7a6bd Translated using Weblate (Kurdish)
Currently translated at 99.8% (521 of 522 strings)
2019-12-31 19:01:39 +01:00
ButterflyOfFire
867f633d16 Translated using Weblate (French)
Currently translated at 99.4% (519 of 522 strings)
2019-12-31 19:01:34 +01:00
ButterflyOfFire
b9de3c202a Translated using Weblate (Arabic)
Currently translated at 100.0% (522 of 522 strings)
2019-12-31 19:01:33 +01:00
pjammo
56364c4a2c Translated using Weblate (Italian)
Currently translated at 100.0% (522 of 522 strings)
2019-12-31 19:01:32 +01:00
WaldiS
b1fd2c007d Translated using Weblate (Polish)
Currently translated at 99.2% (518 of 522 strings)
2019-12-31 19:01:27 +01:00
Yaron Shahrabani
741a872c39 Translated using Weblate (Hebrew)
Currently translated at 100.0% (522 of 522 strings)
2019-12-31 19:01:26 +01:00
Igor Nedoboy
e1e2add616 Translated using Weblate (Russian)
Currently translated at 100.0% (522 of 522 strings)
2019-12-31 19:01:25 +01:00
C. Rüdinger
0c664e346a Translated using Weblate (German)
Currently translated at 99.2% (518 of 522 strings)
2019-12-31 19:01:25 +01:00
Oğuz Ersen
7ddb856ccd Translated using Weblate (Turkish)
Currently translated at 100.0% (522 of 522 strings)
2019-12-31 19:01:24 +01:00
Software In Interlingua
17c0b981d1 Added translation using Weblate (Interlingua) 2019-12-31 19:01:20 +01:00
Tobias Groza
7f0a9904ff Merge pull request #2913 from yausername/fixTranslatableUrl
made instance list url non translatable
2019-12-31 15:51:27 +01:00
yausername
2b4190d85d made instance list url non translatable 2019-12-31 20:10:51 +05:30
Tobias Groza
57e89babf1 Merge pull request #2879 from Louis-Berlic/dev
Add support for Occitan language
2019-12-31 11:02:14 +01:00
Tobias Groza
209dc5ace9 Merge pull request #2912 from mauriciocolli/fix-wrong-thread-access
Fix for player access out of its creation thread
2019-12-31 10:55:12 +01:00
Mauricio Colli
1e7e8d4121 Fix for player access out of its creation thread 2019-12-31 02:52:16 -03:00
Tobias Groza
04b75ef05f Merge pull request #2636 from dotvirus/longpress-on-custom-playlists
Add local playlist to queue when long clicking on 'Background/Popup'
2019-12-31 01:50:52 +01:00
dotvirus
de19421de1 Update LocalPlaylistFragment.java 2019-12-31 01:42:41 +01:00
Tobias Groza
61f64a7349 Merge pull request #2875 from Nico-late/Issue#2838
Fixed issues #2838 #2797 #2773
2019-12-31 01:23:24 +01:00
Nico-late
6fb16bad85 Update app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
Space added for more clarity

Co-Authored-By: Tobias Groza <TobiGr@users.noreply.github.com>
2019-12-31 00:52:01 +01:00
Nico-late
694813ac90 Fixed issue #2838 2019-12-31 00:52:01 +01:00
C. Rüdinger
6f3fd50ed8 Translated using Weblate (German)
Currently translated at 99.2% (518 of 522 strings)
2019-12-30 14:06:08 +01:00
TobiGr
c1e1c191d0 Translated using Weblate (German)
Currently translated at 99.2% (518 of 522 strings)
2019-12-30 14:06:08 +01:00
Igor Nedoboy
f4c8fdaf07 Translated using Weblate (Russian)
Currently translated at 100.0% (522 of 522 strings)
2019-12-29 23:20:25 +01:00
MohammedSR Vevo
457ebe3aa2 Translated using Weblate (Kurdish)
Currently translated at 78.5% (410 of 522 strings)
2019-12-29 22:55:19 +01:00
MohammedSR Vevo
8da8ce0a0a Translated using Weblate (Kurdish)
Currently translated at 70.9% (360 of 508 strings)
2019-12-29 13:59:23 +01:00
Igor Nedoboy
cc869b98a3 Translated using Weblate (Russian)
Currently translated at 100.0% (508 of 508 strings)
2019-12-29 13:59:01 +01:00
winqooq
f9e7873e54 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (508 of 508 strings)
2019-12-27 17:26:55 +01:00
Hosted Weblate
c4cba8aa37 Merge branch 'origin/dev' into Weblate. 2019-12-27 09:21:15 +01:00
Jaewoi
22e4ef4034 Translated using Weblate (Korean)
Currently translated at 98.6% (501 of 508 strings)
2019-12-27 09:21:12 +01:00
thami simo
708cdc4c62 Translated using Weblate (Arabic)
Currently translated at 100.0% (522 of 522 strings)
2019-12-24 08:21:11 +01:00
Hosted Weblate
94931df60b Merge branch 'origin/dev' into Weblate. 2019-12-22 23:21:13 +01:00
Éfrit
b3605fe6d4 Translated using Weblate (French)
Currently translated at 100.0% (508 of 508 strings)
2019-12-22 23:21:11 +01:00
justanidea
11e0ed7c4f Translated using Weblate (French)
Currently translated at 100.0% (508 of 508 strings)
2019-12-22 23:21:11 +01:00
WaldiS
34e89448b1 Translated using Weblate (Polish)
Currently translated at 100.0% (508 of 508 strings)
2019-12-22 23:21:09 +01:00
TobiGr
9a6e936996 Add SoundCloud audio stream extraction fix to changelog 2019-12-22 00:59:39 +01:00
TobiGr
5ed58b8609 Update extractor to fix SoundCloud audio stream extraction 2019-12-22 00:50:16 +01:00
Tobias Groza
0d7d610127 Merge pull request #2862 from TeamNewPipe/release_0.18.0
Release 0.18.0
2019-12-21 19:47:54 +01:00
TobiGr
92a42235a1 Add changelog 2019-12-21 01:14:11 +01:00
TobiGr
3f52938f08 Bump version to 0.18.0 and version code to 800 2019-12-21 01:14:11 +01:00
Louis-Berlic
a08cd4ce6a Move to values-oc 2019-12-19 16:11:17 +01:00
Louis-Berlic
46b12ed819 Added initial strings.xml for Occitan language 2019-12-19 16:10:09 +01:00
TobiGr
e7ef193da6 Merge remote-tracking branch 'Weblate/dev' into dev 2019-12-18 23:12:31 +01:00
WaldiS
2ebe5aa878 Translated using Weblate (Polish)
Currently translated at 100.0% (508 of 508 strings)
2019-12-18 14:21:09 +01:00
Adolfo Jayme Barrientos
f58f6639f8 Translated using Weblate (Spanish)
Currently translated at 99.8% (507 of 508 strings)
2019-12-17 08:31:15 +01:00
Tobias Groza
f995ba115c Merge pull request #2868 from yausername/fix-add-instance-dialog
fix add instance dialog
2019-12-15 11:01:13 +01:00
Tobias Groza
755dff343d Merge pull request #2792 from mqus/dev
Enabling TLS1.1/1.2 on Android 4.4 devices (API 19/KitKat)
2019-12-15 00:17:37 +01:00
yausername
c8c7d23971 fix add instance dialog 2019-12-15 01:30:47 +05:30
Markus
559bcfc6a5 Remove commented-out code and hide stacktraces in release mode 2019-12-13 21:46:19 +01:00
Markus
23c2f748d6 Add trying out some more cipher suites which may be supported on non-standard Android 4.4.2 devices 2019-12-13 21:46:12 +01:00
Markus
3e409b9cc1 Fix formatting and remove unused code 2019-12-13 21:43:03 +01:00
Markus Richter
c0453065e4 Enable TLS v1.1/1.2 for KitKat devices
This enables modern TLS versions in the collection browser, the Downloader and the Player.
This is neccessary because media.ccc.de rejects all older TLS connection attempts, see issue #2777.
2019-12-13 21:42:58 +01:00
Tobias Groza
88a6e5981f Merge pull request #2744 from XiangRongLin/seek
Adjustable seek duration
2019-12-13 08:47:02 +01:00
Xiang Rong Lin
8970a663ec Rename "seek_duration_default_key" and use it in BasePlayer 2019-12-13 07:14:17 +01:00
Xiang Rong Lin
334437137e Remove local variable for seek duration 2019-12-13 07:14:17 +01:00
Xiang Rong Lin
949c01b37f Extract getting of seek duration into a function 2019-12-13 07:14:17 +01:00
Xiang Rong Lin
17146c2c13 Rename adjustable seek duration setting
Change from "Seek duration" to "Fast-forward/-rewind seek duration"
2019-12-13 07:14:17 +01:00
Xiang Rong Lin
dcd35b038e Adjust BasePlayer to use seek duration of preferences.
Changes behaviour when double-tapping in video and clicking fast forward/rewind in background mode.
2019-12-13 07:14:17 +01:00
Xiang Rong Lin
550364906d Add new preference for seek duration.
Add new ListPreference under settings/ video & audio for the duration of a seek.
With options for 5, 10, 15, 20, 25, 30 seconds.
2019-12-13 07:14:17 +01:00
Tobias Groza
fa8483bbb6 Merge pull request #2860 from spk/fix-cannot-resolv-symbol
Migrate annotation to androidx
2019-12-12 21:44:49 +01:00
TobiGr
b976e40439 Delete view_history_deleted translation 2019-12-12 21:32:38 +01:00
TobiGr
6fcae39fe2 Delete toggle_leak_canary translation 2019-12-12 21:32:03 +01:00
Laurent Arnoud
ec1de9824a Migrate annotation to androidx
release build is failing without this on android studio 3.5.3
2019-12-12 21:31:23 +01:00
TobiGr
8f83c210ad Delete overwrite_warning translations 2019-12-12 21:31:19 +01:00
TobiGr
d68009ff7e Delete msg_pending_downloads translations 2019-12-12 21:30:19 +01:00
TobiGr
bac2045111 Delete error_http_requested_range_not_satisfiable translations 2019-12-12 21:29:37 +01:00
TobiGr
bbf3c37978 Delete direct_on_background translations 2019-12-12 21:29:22 +01:00
TobiGr
901c63d1f2 Rename default_kiosk_page_sumatry to default_kiosk_page_summary
Remove duplicates
2019-12-12 21:25:31 +01:00
TobiGr
13306b5c1b Merge remote-tracking branch 'Weblate/dev' into dev 2019-12-12 20:58:22 +01:00
TobiGr
b883ad1657 Update extractor to TeamNewPipe/NewPipeExtractor@8cb3250
Fix soundcloud client id extraction
Improve handling of missing uploader in playlist
2019-12-12 18:47:26 +01:00
Tobias Groza
6029e18980 Merge pull request #2569 from marcovr/dev
Black navigation bar for black theme
2019-12-12 18:32:49 +01:00
Mitesh Sanjay Mutha
a81b791ee3 Translated using Weblate (Hindi)
Currently translated at 100.0% (508 of 508 strings)
2019-12-11 22:59:56 +01:00
THANOS SIOURDAKIS
7e5aaabadf Translated using Weblate (Greek)
Currently translated at 99.8% (507 of 508 strings)
2019-12-11 22:59:55 +01:00
chr56
e01e0d5aed Translated using Weblate (Chinese (Simplified))
Currently translated at 98.8% (502 of 508 strings)
2019-12-11 13:55:38 +01:00
pjammo
db07b81c3c Translated using Weblate (Italian)
Currently translated at 100.0% (508 of 508 strings)
2019-12-11 13:55:27 +01:00
Helios
d660e4ac9d Translated using Weblate (Tamil)
Currently translated at 43.5% (221 of 508 strings)
2019-12-11 13:55:26 +01:00
Marco vR
d90588ce6b Fix style version 2019-12-11 10:04:57 +01:00
Marco vR
7e78197b37 Avoid duplicated code & dark navbar for DarkTheme 2019-12-11 10:04:57 +01:00
Marco vR
06af26f1f2 Black navigation bar for black theme
See: https://github.com/TeamNewPipe/NewPipe/issues/1494
2019-12-11 10:04:57 +01:00
Tobias Groza
d0e07276dd Merge pull request #2856 from TeamNewPipe/Stypox-patch-1
Add PeerTube to README
2019-12-10 22:34:15 +01:00
Stypox
59c19614ea Add PeerTube to README.md 2019-12-10 22:19:54 +01:00
Stypox
0f04dd6dae Merge pull request #2724 from PeterHindes/dev
[Bug Fixed] Playlists With No Uploader No Longer Crash The App When Added To Users Library
2019-12-10 22:12:47 +01:00
Peter Hindes
b365973ac6 fix last recomendation. syntax and imports 2019-12-10 12:18:49 -07:00
Peter Hindes
19fb8cfbfe Update app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java
Co-Authored-By: Redirion <redirion@web.de>
2019-12-10 12:13:04 -07:00
Peter Hindes
d8e6a5cb33 Merge branch 'dev' into dev 2019-12-10 09:59:12 -07:00
Peter Hindes
f4c6a49339 Revert "Merge branch 'dev' of https://github.com/PeterHindes/NewPipe into dev"
This reverts commit 23ee22566d, reversing
changes made to ea70a1f334.
2019-12-10 09:50:53 -07:00
Peter Hindes
23ee22566d Merge branch 'dev' of https://github.com/PeterHindes/NewPipe into dev 2019-12-10 09:50:47 -07:00
Peter Hindes
ea70a1f334 no change 2019-12-10 09:50:33 -07:00
Peter Hindes
68a1407314 Dont update this 2019-12-10 09:48:16 -07:00
Peter Hindes
881efb9be8 Merge branch 'dev' of https://github.com/PeterHindes/NewPipe into dev 2019-12-10 09:44:21 -07:00
Peter Hindes
7844547e4f not used 2019-12-10 09:44:02 -07:00
Peter Hindes
054279d553 Update app/src/main/res/values/strings.xml
Co-Authored-By: Stypox <stypox@pm.me>
2019-12-10 09:37:57 -07:00
Tobias Groza
b2bbdc656a Merge pull request #2201 from yausername/peertube-ui
PeerTube support
2019-12-10 14:39:38 +01:00
yausername
a0151f2a68 more grammar fix 2019-12-10 12:36:56 +05:30
yausername
fd5f4d9840 merged upstream/dev 2019-12-10 01:37:33 +05:30
Peter Hindes
98d7e6bcc6 Merge branch 'dev' into dev 2019-12-09 09:13:18 -07:00
Tobias Groza
e92ca5e572 Merge pull request #2820 from Redirion/remember-subtitles
Remember caption option in player, closes #2811
2019-12-09 17:01:30 +01:00
Tobias Groza
27ca9ed8b8 Merge branch 'dev' into remember-subtitles 2019-12-09 16:55:37 +01:00
Tobias Groza
c2b1d45fc4 Merge pull request #2679 from kapodamy/dl-last-features
more dl features
2019-12-07 09:10:53 +01:00
kapodamy
03939555ac add missing change after updating NPE
use +webm_opus instead of +opus
2019-12-07 00:16:01 -03:00
kapodamy
5a2cd93d13 remove netbeans editor-fold comments 2019-12-06 16:30:07 -03:00
Peter Hindes
ef69625cd2 Merge branch 'dev' into dev 2019-12-06 08:57:43 -07:00
Peter Hindes
ae88b4c697 remove unused code pt2 2019-12-06 08:38:15 -07:00
Peter Hindes
693756bdd6 Removed redundant. Related to last merge 2019-12-06 08:36:57 -07:00
Peter Hindes
c05633979c Update app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java
Co-Authored-By: Redirion <redirion@web.de>
2019-12-06 08:35:14 -07:00
Peter Hindes
7d80d04f34 Remove unused code pt1 2019-12-06 08:32:45 -07:00
dgarciabad
3ff2da3b20 Translated using Weblate (Spanish)
Currently translated at 99.8% (507 of 508 strings)
2019-12-06 03:58:06 +01:00
Rex_sa
eb15bc97a7 Translated using Weblate (Arabic)
Currently translated at 99.4% (505 of 508 strings)
2019-12-06 03:58:05 +01:00
dgarciabad
c6cd2dd854 Translated using Weblate (Basque)
Currently translated at 99.8% (507 of 508 strings)
2019-12-06 03:58:04 +01:00
kapodamy
aae8865bdd remove unused imports 2019-12-05 14:04:48 -03:00
ozyc
c15cead9e2 Translated using Weblate (Esperanto)
Currently translated at 100.0% (508 of 508 strings)
2019-12-05 06:05:18 +01:00
Daniele Lira Mereb
0ccd30b12e Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (508 of 508 strings)
2019-12-05 06:05:17 +01:00
yausername
d2a59ecc62 grammar fix 2019-12-05 05:11:05 +05:30
yausername
7a67d192c3 updated extractor 2019-12-03 07:22:14 +05:30
yausername
d32ad36f3d reorder peertube settings entry 2019-12-03 07:16:25 +05:30
Allan Nordhøy
f587d79cd8 Translated using Weblate (Norwegian Bokmål)
Currently translated at 98.8% (502 of 508 strings)
2019-12-03 01:05:20 +01:00
Osoitz
9e290ce91a Translated using Weblate (Basque)
Currently translated at 98.2% (499 of 508 strings)
2019-12-03 01:05:19 +01:00
yausername
0c40a45075 use plurals 2019-12-03 02:08:59 +05:30
yausername
17c5e73994 null check on share 2019-12-03 02:04:52 +05:30
yausername
890d1cb50b update extractor, kiosk names and icons 2019-12-03 01:50:23 +05:30
yausername
4c89d1a6e5 Merge remote-tracking branch 'upstream/dev' into peertube-ui 2019-12-03 01:21:25 +05:30
Rex_sa
77d3a1ef45 Translated using Weblate (Arabic)
Currently translated at 99.4% (505 of 508 strings)
2019-11-30 19:22:13 +01:00
Hosted Weblate
6e289b44c7 Merge branch 'origin/dev' into Weblate. 2019-11-29 20:05:27 +01:00
Jeff Huang
8039055a87 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (508 of 508 strings)
2019-11-29 20:05:25 +01:00
Florian
6c0f5bef21 Translated using Weblate (French)
Currently translated at 100.0% (508 of 508 strings)
2019-11-29 20:05:24 +01:00
Lucas Galello
7dd7ea1a32 Translated using Weblate (Spanish)
Currently translated at 99.6% (506 of 508 strings)
2019-11-29 20:05:24 +01:00
JoC
3bce9a8ead Translated using Weblate (Spanish)
Currently translated at 99.6% (506 of 508 strings)
2019-11-29 20:05:23 +01:00
narayaan
da82e3f5d1 Translated using Weblate (Dutch)
Currently translated at 90.7% (461 of 508 strings)
2019-11-29 20:05:23 +01:00
Stefano Panzeri
0df5d7a934 Translated using Weblate (Italian)
Currently translated at 100.0% (508 of 508 strings)
2019-11-29 20:05:22 +01:00
WaldiS
27f38f329f Translated using Weblate (Polish)
Currently translated at 100.0% (508 of 508 strings)
2019-11-29 20:05:21 +01:00
zeritti
c4707978c4 Translated using Weblate (Czech)
Currently translated at 100.0% (508 of 508 strings)
2019-11-29 20:05:20 +01:00
BennyBeat
2ad0792581 Translated using Weblate (Catalan)
Currently translated at 100.0% (508 of 508 strings)
2019-11-29 20:05:16 +01:00
naofum
8741856234 Translated using Weblate (Japanese)
Currently translated at 100.0% (508 of 508 strings)
2019-11-29 20:05:00 +01:00
ssantos
e37a86efc2 Translated using Weblate (German)
Currently translated at 100.0% (508 of 508 strings)
2019-11-29 20:04:59 +01:00
Geoflly Adonias
baee238a2c Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (508 of 508 strings)
2019-11-29 20:04:59 +01:00
k1rakishou
e8437052d8 Add a setting for the lock screen thumbnail feature 2019-11-28 21:47:15 +03:00
k1rakishou
cf13f5ca56 Rebase onto the latest dev, update appcompat dependencies to use androidx 2019-11-28 21:47:15 +03:00
k1rakishou
52f82ed228 Show video thumbnail on the lock screen 2019-11-28 21:47:15 +03:00
kapodamy
84ec320df4 commit
* rebase fixup, add null check
* better ETA string
* drop connection read timeout, for HSDPA networks
* bump NPE version
2019-11-26 13:41:16 -03:00
kapodamy
0033843bc2 Merge branch 'dl-last-features' of https://github.com/kapodamy/NewPipe into dl-last-features 2019-11-26 10:46:59 -03:00
kapodamy
3ca461413e Merge branch 'dev' into dl-last-features 2019-11-26 10:46:32 -03:00
kapodamy
e6d9d8e26d code cleanup
* migrate few annotations to androidx
* mission recovery: better error handling (except StreamExtractor.getErrorMessage() method always returns an error)
* post-processing: more detailed progress

[file specific changes]

DownloadMission.java
* remove redundant/boilerplate code (again)
* make few variables volatile
* better file "length" approximation
* use "done" variable to count the amount of bytes downloaded (simplify percent calc in UI code)

Postprocessing.java
* if case of error use "ERROR_POSTPROCESSING" instead of "ERROR_UNKNOWN_EXCEPTION"
* simplify source stream init

DownloadManager.java
* move all "service message sending" code to DownloadMission
* remove not implemented method "notifyUserPendingDownloads()" also his unused strings

DownloadManagerService.java
* use START_STICKY instead of START_NOT_STICKY
* simplify addMissionEventListener()/removeMissionEventListener() methods (always are called from the main thread)

Deleter.java
* better method definition

MissionAdapter.java
* better method definition
* code cleanup
* the UI is now refreshed every 750ms
* simplify download progress calculation
* indicates if the download is actually recovering
* smooth download speed measure
* show estimated remain time

MainFragment.java:
* check if viewPager is null (issued by "Apply changes" feature of Android Studio)
2019-11-26 10:46:31 -03:00
kapodamy
763995d4c9 update DownloadDialog.java
keep *.opus extension
2019-11-26 10:46:29 -03:00
kapodamy
8a992d4c47 update WebMWriter.java
fix wrong cue generation
2019-11-26 10:46:29 -03:00
kapodamy
da052df106 update DownloadManager.java
* check if the directory pending_downloads was created
2019-11-26 10:46:29 -03:00
kapodamy
60d4c8a55d fallback for pending downloads directory 2019-11-26 10:46:29 -03:00
kapodamy
4292ca94ff misc changes
* OggFromWebMWriter: rewrite (again), reduce iterations over the input. Works as-is (video streams are not supported)
* WebMReader: use int for SimpleBlock.dataSize instead of long
* Download Recovery: allow recovering uninitialized downloads
* check range-requests using HEAD method instead of GET
* DownloadRunnableFallback: add workaround for 32kB/s issue, unknown issue origin, wont fix
* reporting downloads errors now include the source url with the selected quality and format
2019-11-26 10:46:28 -03:00
kapodamy
570738190d Mp4FromDashWriter fixes
* correct calculation of "co64" box and usage of 64bits offsets
* generate one chunk for audio streams like ffmpeg does, attempt to fix cut-off audio
* misc. cleanup
2019-11-26 10:46:26 -03:00
kapodamy
86dafdd92b long-term downloads resume
* recovery infrastructure
* bump serialVersionUID of DownloadMission
* misc cleanup in DownloadMission.java
* remove unused/redundant from strings.xml
2019-11-26 10:46:26 -03:00
kapodamy
dab53450c1 rewrite OggFromWebMWriter
* reduce the number of iterations over the output file (less seeking)
* fix audio samples with size of 255 do not handled correctly in the segment table (allows writing audio streams with 70kbps and 160kbps bitrate)
* add support for VORBIS codec metadata
* write packets based on the timestamp
2019-11-26 10:46:26 -03:00
kapodamy
773aa1eff0 implement webm to ogg demuxer
* used for opus audio stream
* update WebMReader and WebMWriter
* new post-processing algorithm
2019-11-26 10:46:26 -03:00
kapodamy
14de2f289f Merge branch 'dev' into dl-last-features 2019-11-26 10:38:47 -03:00
Tobias Groza
eeeeeef3a7 Merge pull request #2720 from haggaie/hebrew-translation
Fix Hebrew translation on some devices
2019-11-24 21:26:52 +01:00
kapodamy
ea1be11a80 Merge branch 'dev' into dl-last-features 2019-11-24 14:03:34 -03:00
yausername
309fd3fb7d white space changes 2019-11-24 21:42:05 +05:30
yausername
6a24dcec73 Merge remote-tracking branch 'upstream/dev' into peertube-ui 2019-11-24 21:20:45 +05:30
yausername
3e2dba2fd5 merged origin/peertube-ui 2019-11-24 21:19:23 +05:30
yausername
527c38adf9 easily switch between multiple peertube instances 2019-11-24 21:08:06 +05:30
kapodamy
f62a7919a5 code cleanup
* migrate few annotations to androidx
* mission recovery: better error handling (except StreamExtractor.getErrorMessage() method always returns an error)
* post-processing: more detailed progress

[file specific changes]

DownloadMission.java
* remove redundant/boilerplate code (again)
* make few variables volatile
* better file "length" approximation
* use "done" variable to count the amount of bytes downloaded (simplify percent calc in UI code)

Postprocessing.java
* if case of error use "ERROR_POSTPROCESSING" instead of "ERROR_UNKNOWN_EXCEPTION"
* simplify source stream init

DownloadManager.java
* move all "service message sending" code to DownloadMission
* remove not implemented method "notifyUserPendingDownloads()" also his unused strings

DownloadManagerService.java
* use START_STICKY instead of START_NOT_STICKY
* simplify addMissionEventListener()/removeMissionEventListener() methods (always are called from the main thread)

Deleter.java
* better method definition

MissionAdapter.java
* better method definition
* code cleanup
* the UI is now refreshed every 750ms
* simplify download progress calculation
* indicates if the download is actually recovering
* smooth download speed measure
* show estimated remain time

MainFragment.java:
* check if viewPager is null (issued by "Apply changes" feature of Android Studio)
2019-11-24 12:27:16 -03:00
kapodamy
844f80a5f1 update DownloadDialog.java
keep *.opus extension
2019-11-24 12:24:59 -03:00
kapodamy
94e23142a5 update WebMWriter.java
fix wrong cue generation
2019-11-24 12:24:59 -03:00
kapodamy
9339fc80b4 update DownloadManager.java
* check if the directory pending_downloads was created
2019-11-24 12:24:58 -03:00
kapodamy
d092e39c56 fallback for pending downloads directory 2019-11-24 12:24:57 -03:00
kapodamy
160a33e8c8 misc changes
* OggFromWebMWriter: rewrite (again), reduce iterations over the input. Works as-is (video streams are not supported)
* WebMReader: use int for SimpleBlock.dataSize instead of long
* Download Recovery: allow recovering uninitialized downloads
* check range-requests using HEAD method instead of GET
* DownloadRunnableFallback: add workaround for 32kB/s issue, unknown issue origin, wont fix
* reporting downloads errors now include the source url with the selected quality and format
2019-11-24 12:24:57 -03:00
kapodamy
429ee7eb93 Mp4FromDashWriter fixes
* correct calculation of "co64" box and usage of 64bits offsets
* generate one chunk for audio streams like ffmpeg does, attempt to fix cut-off audio
* misc. cleanup
2019-11-24 12:24:54 -03:00
kapodamy
c891f2f1ed long-term downloads resume
* recovery infrastructure
* bump serialVersionUID of DownloadMission
* misc cleanup in DownloadMission.java
* remove unused/redundant from strings.xml
2019-11-24 12:24:53 -03:00
Robin
3108c903dd squashed commit 2019-11-24 16:24:39 +01:00
kapodamy
0cdfa6e377 rewrite OggFromWebMWriter
* reduce the number of iterations over the output file (less seeking)
* fix audio samples with size of 255 do not handled correctly in the segment table (allows writing audio streams with 70kbps and 160kbps bitrate)
* add support for VORBIS codec metadata
* write packets based on the timestamp
2019-11-24 12:21:02 -03:00
kapodamy
52a21e4a24 implement webm to ogg demuxer
* used for opus audio stream
* update WebMReader and WebMWriter
* new post-processing algorithm
2019-11-24 12:21:01 -03:00
Robin
8e152df46d Remember caption option in player, closes #2811 2019-11-24 11:10:50 +01:00
Igor Nedoboy
a68c8ceebe Translated using Weblate (Russian)
Currently translated at 100.0% (508 of 508 strings)
2019-11-24 01:56:56 +01:00
Igor Nedoboy
1ce44b31e2 Translated using Weblate (Russian)
Currently translated at 100.0% (508 of 508 strings)
2019-11-24 01:50:24 +01:00
Hosted Weblate
8f52f918db Merge branch 'origin/dev' into Weblate. 2019-11-23 20:19:07 +01:00
chr56
55f5f76275 Translated using Weblate (Chinese (Simplified))
Currently translated at 95.9% (487 of 508 strings)
2019-11-23 20:19:07 +01:00
Yaron Shahrabani
f122d73754 Translated using Weblate (Hebrew)
Currently translated at 100.0% (508 of 508 strings)
2019-11-23 20:19:06 +01:00
ssantos
8227d85feb Translated using Weblate (Portuguese)
Currently translated at 100.0% (508 of 508 strings)
2019-11-23 20:19:06 +01:00
Oğuz Ersen
46e2f4e579 Translated using Weblate (Turkish)
Currently translated at 100.0% (508 of 508 strings)
2019-11-23 20:19:05 +01:00
Igor Nedoboy
0c65f73180 Translated using Weblate (Russian)
Currently translated at 100.0% (508 of 508 strings)
2019-11-23 20:19:02 +01:00
TobiGr
0fb7eab2f9 Fix code formatting 2019-11-23 20:04:40 +01:00
Tobias Groza
2b2de8811e Merge pull request #2772 from mitosagi/popup-player-gestures
Fix popup player gestures
2019-11-23 19:41:55 +01:00
chr56
5e87463125 Translated using Weblate (Chinese (Simplified))
Currently translated at 95.9% (487 of 508 strings)
2019-11-23 06:26:16 +01:00
Hosted Weblate
424d3fdcd7 Merge branch 'origin/dev' into Weblate. 2019-11-23 06:19:04 +01:00
narayaan
6452c7e08c Translated using Weblate (Flemish)
Currently translated at 90.4% (459 of 508 strings)
2019-11-23 06:19:03 +01:00
Mattias Münster
e21257b786 Translated using Weblate (Swedish)
Currently translated at 91.3% (464 of 508 strings)
2019-11-23 06:19:02 +01:00
narayaan
0f70aeb910 Translated using Weblate (Dutch)
Currently translated at 90.2% (458 of 508 strings)
2019-11-23 06:19:02 +01:00
Igor Nedoboy
cedfbf5f67 Translated using Weblate (Russian)
Currently translated at 100.0% (508 of 508 strings)
2019-11-23 06:19:01 +01:00
chr56
31fab60701 Translated using Weblate (Chinese (Simplified))
Currently translated at 95.1% (483 of 508 strings)
2019-11-23 06:18:58 +01:00
yausername
afef8d8d0b removed extra white spaces 2019-11-23 00:53:14 +05:30
yausername
ac2543d0a1 validate peertube instance. changed peertube color 2019-11-23 00:31:35 +05:30
yausername
81658de08f merged upstream/dev 2019-11-22 22:41:59 +05:30
mitosagi
2ad0d47f61 Fix popup player gestures 2019-11-22 11:41:10 +01:00
Tobias Groza
b9d6d55aa4 Merge pull request #2742 from mauriciocolli/fix-mess-tabs
Fix tab handling and enable ignored tests again
2019-11-21 22:23:40 +01:00
Jeff Huang
719d8651b3 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (508 of 508 strings)
2019-11-21 15:37:20 +01:00
ozyc
f3988c37b6 Translated using Weblate (French)
Currently translated at 99.8% (507 of 508 strings)
2019-11-21 15:37:19 +01:00
zmni
912f09c83e Translated using Weblate (Indonesian)
Currently translated at 99.8% (507 of 508 strings)
2019-11-21 15:37:19 +01:00
WaldiS
27330951aa Translated using Weblate (Polish)
Currently translated at 100.0% (508 of 508 strings)
2019-11-21 15:37:18 +01:00
Yaron Shahrabani
84089453e7 Translated using Weblate (Hebrew)
Currently translated at 100.0% (508 of 508 strings)
2019-11-21 15:37:17 +01:00
naofum
59f76ef304 Translated using Weblate (Japanese)
Currently translated at 100.0% (508 of 508 strings)
2019-11-21 15:37:17 +01:00
ozyc
4061145933 Translated using Weblate (Esperanto)
Currently translated at 99.8% (507 of 508 strings)
2019-11-21 15:37:16 +01:00
nautilusx
e900a69a26 Translated using Weblate (German)
Currently translated at 100.0% (508 of 508 strings)
2019-11-21 15:37:16 +01:00
Oğuz Ersen
9cdec5de50 Translated using Weblate (Turkish)
Currently translated at 100.0% (508 of 508 strings)
2019-11-21 15:37:15 +01:00
yausername
ceabfd1a8b updated extractor 2019-11-21 05:41:14 +05:30
Mauricio Colli
bc283bce4e Make the KioskFragment aware of changes in the preferred content country 2019-11-20 00:23:35 -03:00
Mauricio Colli
544cae4fb4 Use tab position from parameters instead of relying on the view pager 2019-11-20 00:23:34 -03:00
Mauricio Colli
38a0395d45 Enable toolbar title visibility when setting a new one 2019-11-20 00:23:33 -03:00
Mauricio Colli
a5b7666188 Clear the item list when starting loading 2019-11-20 00:23:31 -03:00
Mauricio Colli
58a626dedb Fix broken view pager tabs implementation
- Fragments were being recreated from scratch (losing their state) every
time some configuration change occurred (e.g. screen rotation).
- Use `FragmentStatePagerAdapter` instead, as it is built to work with
them and manage their states.
2019-11-20 00:23:30 -03:00
Mauricio Colli
7e311e5567 Fix mess with tab handling and enable ignored tests again
- Fix typo in a string resource
- Reorder tabs so the default kiosk is on top of the others
2019-11-20 00:23:29 -03:00
yausername
596005c69e merged upstream/dev 2019-11-20 03:48:56 +05:30
Hosted Weblate
14ee7d53d7 Merge branch 'origin/dev' into Weblate. 2019-11-19 21:58:36 +01:00
Allan Nordhøy
2f575c13f1 Translated using Weblate (Norwegian Bokmål)
Currently translated at 99.0% (503 of 508 strings)
2019-11-19 21:58:36 +01:00
ozyc
82738e23ce Translated using Weblate (French)
Currently translated at 100.0% (508 of 508 strings)
2019-11-19 21:58:36 +01:00
ssantos
93e99d096a Translated using Weblate (Portuguese)
Currently translated at 99.8% (507 of 508 strings)
2019-11-19 21:58:35 +01:00
Marc Riera
784b9cf207 Translated using Weblate (Catalan)
Currently translated at 95.5% (485 of 508 strings)
2019-11-19 21:58:34 +01:00
Александр
7b56244c8b Translated using Weblate (Russian)
Currently translated at 99.8% (507 of 508 strings)
2019-11-19 21:58:34 +01:00
Allan Nordhøy
44192d6e49 Translated using Weblate (English)
Currently translated at 100.0% (508 of 508 strings)
2019-11-19 21:58:34 +01:00
ozyc
4ded3adadb Translated using Weblate (Esperanto)
Currently translated at 100.0% (508 of 508 strings)
2019-11-19 21:58:33 +01:00
ozyc
3798d5228c Translated using Weblate (German)
Currently translated at 99.8% (507 of 508 strings)
2019-11-19 21:58:33 +01:00
Oğuz Ersen
0491c4af9c Translated using Weblate (Turkish)
Currently translated at 99.8% (507 of 508 strings)
2019-11-19 21:58:32 +01:00
nautilusx
69799613aa Translated using Weblate (German)
Currently translated at 99.6% (506 of 508 strings)
2019-11-19 21:58:29 +01:00
TobiGr
c0cbec700c Merge remote-tracking branch 'Weblate/dev' into dev 2019-11-19 21:49:14 +01:00
Tobias Groza
e9c9dfcd8c Merge pull request #2799 from Ozyc/patch-1
Make reCAPTCHA string untranslatable
2019-11-19 21:16:25 +01:00
Tobias Groza
c9cbb1e6f1 Merge pull request #2816 from Redirion/patch-3
Bump ExoPlayer to 2.10.8
2019-11-19 18:11:38 +01:00
Hosted Weblate
3ea8841e3a Merge branch 'origin/dev' into Weblate. 2019-11-19 15:04:49 +01:00
chr56
8b26e5b106 Translated using Weblate (Chinese (Simplified))
Currently translated at 32.9% (165 of 501 strings)
2019-11-19 15:04:47 +01:00
Éfrit
495fa170a9 Translated using Weblate (French)
Currently translated at 100.0% (501 of 501 strings)
2019-11-19 15:04:37 +01:00
oskamuelller4fs
406d02fd28 Translated using Weblate (German)
Currently translated at 100.0% (501 of 501 strings)
2019-11-19 15:04:34 +01:00
Geoflly Adonias
3ad8883999 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (501 of 501 strings)
2019-11-19 15:04:33 +01:00
Redirion
dcd7055a9d Bump ExoPlayer to 2.10.8
Here we go again.
Minor update.
ExoPlayer Changelog: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md
2019-11-19 14:25:40 +01:00
Tobias Groza
a8e326ceea Merge pull request #2713 from mauriciocolli/compatibility-extractor
Use new Localization and Downloader implementations from extractor
2019-11-18 10:05:45 +01:00
Mauricio Colli
b125ff702a Show parsed relative times instead of whatever the service gives us
Before, the direct value was given to the user, now it uses the parsed
date so we can match the device's language.

To get the relative time from the parsed dates, we use the PrettyTime
library.

Also introduces a debug option to check the service's original value.
2019-11-17 23:58:55 -03:00
Mauricio Colli
6e546703a9 Show proper text for live streams watching/listening count 2019-11-17 23:58:54 -03:00
Mauricio Colli
71f1bbdcc1 Use new Localization and Downloader implementations from extractor 2019-11-17 23:58:52 -03:00
chr56
bdb86a7fde Translated using Weblate (Chinese (Simplified))
Currently translated at 19.2% (96 of 501 strings)
2019-11-16 23:06:09 +01:00
Pekka Ristola
8ac8258400 Translated using Weblate (Finnish)
Currently translated at 67.7% (339 of 501 strings)
2019-11-16 23:06:01 +01:00
Mattias Münster
0338ff8d51 Translated using Weblate (Swedish)
Currently translated at 91.2% (457 of 501 strings)
2019-11-16 23:06:00 +01:00
Éfrit
d694e61006 Translated using Weblate (French)
Currently translated at 100.0% (501 of 501 strings)
2019-11-16 23:05:53 +01:00
yausername
b6be586766 merged upstream/dev 2019-11-16 04:37:14 +05:30
thami simo
9915287a5a Translated using Weblate (Arabic)
Currently translated at 100.0% (501 of 501 strings)
2019-11-15 20:04:27 +01:00
WaldiS
cb19f792da Translated using Weblate (Polish)
Currently translated at 100.0% (501 of 501 strings)
2019-11-15 20:04:26 +01:00
winqooq
f449aee901 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (501 of 501 strings)
2019-11-15 20:04:26 +01:00
Jeff Huang
b665122c3c Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (501 of 501 strings)
2019-11-14 01:03:30 +01:00
Koo Hyomin
f4dbd5c9bd Translated using Weblate (Korean)
Currently translated at 100.0% (501 of 501 strings)
2019-11-14 01:03:30 +01:00
Pekka Ristola
cb6d7e6dd7 Translated using Weblate (Finnish)
Currently translated at 67.3% (337 of 501 strings)
2019-11-14 01:03:27 +01:00
ozyc
4d4a86f889 Translated using Weblate (French)
Currently translated at 100.0% (501 of 501 strings)
2019-11-14 01:03:26 +01:00
Adolfo Jayme Barrientos
e3829303f9 Translated using Weblate (Spanish)
Currently translated at 99.8% (500 of 501 strings)
2019-11-14 01:03:26 +01:00
Mitesh Sanjay Mutha
86d4cf3dfc Translated using Weblate (Hindi)
Currently translated at 100.0% (501 of 501 strings)
2019-11-14 01:03:25 +01:00
naofum
ed4196f732 Translated using Weblate (Japanese)
Currently translated at 100.0% (501 of 501 strings)
2019-11-14 01:03:23 +01:00
ozyc
af70fdd7a6 Translated using Weblate (Esperanto)
Currently translated at 100.0% (501 of 501 strings)
2019-11-14 01:03:22 +01:00
Oğuz Ersen
0ee6c1e47e Translated using Weblate (Turkish)
Currently translated at 100.0% (501 of 501 strings)
2019-11-14 01:02:57 +01:00
Ozyc
0886c6b216 Make reCAPTCHA string untranslatable
I updated reCaptchaActivity string and made it untranslatable, because it's causing "Failed check: Unchanged translation" on Weblate.
2019-11-11 15:24:06 +00:00
Hosted Weblate
754fe3adfb Merge branch 'origin/dev' into Weblate. 2019-11-10 00:04:35 +01:00
Deleted User
b5058f99ce Translated using Weblate (Norwegian Bokmål)
Currently translated at 99.2% (497 of 501 strings)
2019-11-10 00:04:32 +01:00
Jeff Huang
f7be693470 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (501 of 501 strings)
2019-11-10 00:04:31 +01:00
Deleted User
b772afeeab Translated using Weblate (Slovak)
Currently translated at 88.2% (442 of 501 strings)
2019-11-10 00:04:31 +01:00
Adolfo Jayme Barrientos
f0c7cb9f94 Translated using Weblate (Spanish)
Currently translated at 100.0% (501 of 501 strings)
2019-11-10 00:04:29 +01:00
zmni
1561c210fb Translated using Weblate (Indonesian)
Currently translated at 99.8% (500 of 501 strings)
2019-11-10 00:04:28 +01:00
thami simo
727fb27ad0 Translated using Weblate (Arabic)
Currently translated at 100.0% (501 of 501 strings)
2019-11-10 00:04:27 +01:00
gold.ris90
ceb1d70551 Translated using Weblate (Macedonian)
Currently translated at 75.6% (379 of 501 strings)
2019-11-10 00:04:25 +01:00
Yaron Shahrabani
720bff02aa Translated using Weblate (Hebrew)
Currently translated at 100.0% (501 of 501 strings)
2019-11-10 00:04:16 +01:00
ssantos
f82dd3e152 Translated using Weblate (Portuguese)
Currently translated at 100.0% (501 of 501 strings)
2019-11-10 00:04:15 +01:00
naofum
a3dff2c608 Translated using Weblate (Japanese)
Currently translated at 100.0% (501 of 501 strings)
2019-11-10 00:04:15 +01:00
ssantos
28f87dcb2b Translated using Weblate (German)
Currently translated at 100.0% (501 of 501 strings)
2019-11-10 00:04:14 +01:00
Oguz Ersen
4daae95979 Translated using Weblate (Turkish)
Currently translated at 100.0% (501 of 501 strings)
2019-11-10 00:04:13 +01:00
Tobias Groza
577bfab366 Merge pull request #2791 from Redirion/patch-2
Bump ExoPlayer to 2.10.7
2019-11-09 11:44:50 +01:00
Igor Nedoboy
655993d8f2 Translated using Weblate (Russian)
Currently translated at 100.0% (501 of 501 strings)
2019-11-08 23:18:51 +01:00
Redirion
3e079a6858 Bump ExoPlayer to 2.10.7
Little dependency update.
ExoPlayer Changelog: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md
2019-11-07 16:36:45 +01:00
Thomas Johansen
e5afffdced Translated using Weblate (Norwegian Bokmål)
Currently translated at 97.6% (489 of 501 strings)
2019-11-06 23:26:56 +01:00
Allan Nordhøy
a43c434376 Translated using Weblate (Norwegian Bokmål)
Currently translated at 97.6% (489 of 501 strings)
2019-11-06 23:26:55 +01:00
Laura Arjona Reina
225647cf65 Translated using Weblate (Spanish)
Currently translated at 100.0% (501 of 501 strings)
2019-11-06 11:30:36 +01:00
Swyter
fdab92c92b Translated using Weblate (Spanish)
Currently translated at 100.0% (501 of 501 strings)
2019-11-06 11:30:35 +01:00
Igor Nedoboy
72adb7a53b Translated using Weblate (Russian)
Currently translated at 100.0% (501 of 501 strings)
2019-11-05 21:47:34 +01:00
Igor Nedoboy
f84907e2c9 Translated using Weblate (Russian)
Currently translated at 100.0% (501 of 501 strings)
2019-11-05 13:15:15 +01:00
Igor Nedoboy
ab3b8d0a14 Translated using Weblate (Russian)
Currently translated at 100.0% (501 of 501 strings)
2019-11-05 09:07:26 +01:00
Hosted Weblate
5e7b635006 Merge branch 'origin/dev' into Weblate. 2019-11-04 17:52:01 +01:00
gold.ris90
58181ee37f Translated using Weblate (Macedonian)
Currently translated at 65.9% (315 of 478 strings)
2019-11-04 17:52:01 +01:00
Igor Nedoboy
4db4b3af86 Translated using Weblate (Russian)
Currently translated at 100.0% (478 of 478 strings)
2019-11-04 17:52:00 +01:00
Adolfo Jayme Barrientos
504f45efa6 Translated using Weblate (Spanish)
Currently translated at 100.0% (478 of 478 strings)
2019-11-04 17:51:57 +01:00
TobiGr
114a7ccdd4 Make "Default Kiosk" string translatable
Closes #2778
2019-11-04 11:21:41 +01:00
Tobias Groza
1a9b3c9d7c Merge pull request #2714 from mauriciocolli/introduce-kotlin
Update Gradle and introduce Kotlin to NewPipe
2019-11-03 13:48:10 +01:00
Mauricio Colli
137fabee0e Update Gradle and introduce Kotlin 2019-11-03 13:33:13 +01:00
TobiGr
2d41022ee4 Merge remote-tracking branch 'Weblate/dev' into dev 2019-11-03 13:01:55 +01:00
Allan Nordhøy
2ce0caa943 Translated using Weblate (Spanish)
Currently translated at 100.0% (478 of 478 strings)
2019-11-02 17:50:03 +01:00
Joseph Kim
2839154b68 Translated using Weblate (Korean)
Currently translated at 100.0% (478 of 478 strings)
2019-11-02 15:17:46 +01:00
Éfrit
e25c5544ba Translated using Weblate (French)
Currently translated at 100.0% (478 of 478 strings)
2019-11-02 15:17:45 +01:00
Adolfo Jayme Barrientos
7c70033f19 Translated using Weblate (Spanish)
Currently translated at 100.0% (478 of 478 strings)
2019-11-02 15:17:45 +01:00
WaldiS
59f90fcdfe Translated using Weblate (Polish)
Currently translated at 99.8% (477 of 478 strings)
2019-11-02 15:17:41 +01:00
Allan Nordhøy
e05def491f Translated using Weblate (Hindi)
Currently translated at 92.3% (441 of 478 strings)
2019-11-02 15:17:41 +01:00
Mitosagi
19738c0c1b Translated using Weblate (Japanese)
Currently translated at 100.0% (478 of 478 strings)
2019-11-02 15:17:41 +01:00
Enol P
0790a43aa2 Translated using Weblate (Asturian)
Currently translated at 47.1% (225 of 478 strings)
2019-11-02 15:17:40 +01:00
Igor Nedoboy
fd4a4d979a Translated using Weblate (Russian)
Currently translated at 100.0% (478 of 478 strings)
2019-11-02 15:17:36 +01:00
Adolfo Jayme Barrientos
6011dec272 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (478 of 478 strings)
2019-11-02 15:17:36 +01:00
Mitesh Sanjay Mutha
98ae78eb06 Translated using Weblate (Hindi)
Currently translated at 92.3% (441 of 478 strings)
2019-11-02 15:17:33 +01:00
Igor Nedoboy
0a66f54487 Translated using Weblate (Russian)
Currently translated at 100.0% (478 of 478 strings)
2019-10-29 22:27:46 +01:00
ssantos
5e2f373a57 Translated using Weblate (Portuguese)
Currently translated at 100.0% (478 of 478 strings)
2019-10-29 22:08:59 +01:00
Oguz Ersen
ae8f47500f Translated using Weblate (Turkish)
Currently translated at 100.0% (478 of 478 strings)
2019-10-29 22:08:58 +01:00
Igor Nedoboy
36c1e19ef0 Translated using Weblate (Russian)
Currently translated at 100.0% (478 of 478 strings)
2019-10-29 22:08:55 +01:00
Allan Nordhøy
dc933a5e66 Translated using Weblate (Norwegian Bokmål)
Currently translated at 97.5% (466 of 478 strings)
2019-10-28 16:31:50 +01:00
Jeff Huang
c7699a5a3b Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (478 of 478 strings)
2019-10-28 16:31:49 +01:00
naofum
0f656aa7a9 Translated using Weblate (Japanese)
Currently translated at 100.0% (478 of 478 strings)
2019-10-28 16:31:48 +01:00
Allan Nordhøy
1af8481fff Translated using Weblate (English)
Currently translated at 99.8% (477 of 478 strings)
2019-10-28 16:31:48 +01:00
Deleted User
3787c6ec70 Translated using Weblate (Norwegian Bokmål)
Currently translated at 97.7% (467 of 478 strings)
2019-10-27 06:12:50 +01:00
Enol P
3e7ff96445 Translated using Weblate (Asturian)
Currently translated at 40.6% (194 of 478 strings)
2019-10-25 21:29:55 +02:00
Hosted Weblate
558e3b1032 Merge branch 'origin/dev' into Weblate. 2019-10-22 03:31:44 +02:00
Allan Nordhøy
65279b3364 Translated using Weblate (Norwegian Bokmål)
Currently translated at 95.0% (454 of 478 strings)
2019-10-22 03:31:42 +02:00
Muhammad Sharjeel
d3ca184570 Translated using Weblate (Urdu)
Currently translated at 100.0% (478 of 478 strings)
2019-10-22 03:31:42 +02:00
01QueN10
40a2322e09 Translated using Weblate (Korean)
Currently translated at 99.0% (473 of 478 strings)
2019-10-22 03:31:08 +02:00
Mattias Münster
0b593b343e Translated using Weblate (Swedish)
Currently translated at 79.7% (381 of 478 strings)
2019-10-22 03:31:06 +02:00
ozyc
83dc62239f Translated using Weblate (French)
Currently translated at 100.0% (478 of 478 strings)
2019-10-22 03:31:05 +02:00
Igor Nedoboy
34cb44f2be Translated using Weblate (Russian)
Currently translated at 100.0% (478 of 478 strings)
2019-10-22 03:31:04 +02:00
ozyc
55a82b631e Translated using Weblate (Esperanto)
Currently translated at 58.4% (279 of 478 strings)
2019-10-22 03:31:03 +02:00
Igor Nedoboy
b358231b20 Translated using Weblate (Russian)
Currently translated at 100.0% (478 of 478 strings)
2019-10-19 00:23:12 +02:00
Igor Nedoboy
4579fa52ac Translated using Weblate (Russian)
Currently translated at 100.0% (478 of 478 strings)
2019-10-19 00:11:46 +02:00
Igor Nedoboy
b4449e4998 Translated using Weblate (Russian)
Currently translated at 100.0% (478 of 478 strings)
2019-10-19 00:07:21 +02:00
Goudarz Jafari
d5b1dceb66 Translated using Weblate (Persian)
Currently translated at 98.5% (471 of 478 strings)
2019-10-18 20:52:50 +02:00
Peter Hindes
8f46757c0d New no uploader, still needs a way to programaticly switch
preferably they can both be the same file and modified at runtime
2019-10-16 20:32:12 -06:00
Peter Hindes
87378fc79c Fixed library showing null 2019-10-16 19:56:31 -06:00
朱程宇
95f44cbe9a Translated using Weblate (Chinese (Simplified))
Currently translated at 1.7% (8 of 478 strings)
2019-10-16 05:20:38 +02:00
Jeff Huang
8849ddab81 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (478 of 478 strings)
2019-10-16 05:20:35 +02:00
Nathan
9a47714645 Translated using Weblate (French)
Currently translated at 100.0% (478 of 478 strings)
2019-10-16 05:20:34 +02:00
zmni
b1396b98a3 Translated using Weblate (Indonesian)
Currently translated at 100.0% (478 of 478 strings)
2019-10-16 05:20:33 +02:00
WaldiS
c3433baf0c Translated using Weblate (Polish)
Currently translated at 99.8% (477 of 478 strings)
2019-10-16 05:20:32 +02:00
zeritti
fab692c386 Translated using Weblate (Czech)
Currently translated at 100.0% (478 of 478 strings)
2019-10-16 05:20:32 +02:00
naofum
834f599fb1 Translated using Weblate (Japanese)
Currently translated at 100.0% (478 of 478 strings)
2019-10-16 05:20:31 +02:00
Enol P
701207dcc5 Translated using Weblate (Asturian)
Currently translated at 38.9% (186 of 478 strings)
2019-10-16 05:20:31 +02:00
Daniele Lira Mereb
19ba37dfad Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (478 of 478 strings)
2019-10-16 05:20:29 +02:00
Oguz Ersen
284228ef16 Translated using Weblate (Turkish)
Currently translated at 100.0% (478 of 478 strings)
2019-10-16 05:20:27 +02:00
Peter Hindes
ad2de3a828 only use TextUtils.equals (fixes more crashes) 2019-10-15 17:22:17 -06:00
Peter Hindes
797e1a105d Comment out debuging code 2019-10-15 14:02:37 -06:00
Peter Hindes
9c00e7f45c Using Strings
still need to find out why its null on the library
2019-10-15 12:49:24 -06:00
Tushar Pandey
26a1b1377d Added translation using Weblate (Pirate) 2019-10-15 17:13:04 +02:00
Peter Hindes
df2bb228c5 Much Simpler Fix 2019-10-15 09:11:04 -06:00
Peter Hindes
49db47c12c describe what "Auto-Generated" Means 2019-10-15 09:01:30 -06:00
Peter Hindes
cc1e5edaec Revert "Merge branch 'dev-all-changes' into dev"
This reverts commit f6060261a1, reversing
changes made to 8c73253a52.
2019-10-15 08:52:51 -06:00
Peter Hindes
f6060261a1 Merge branch 'dev-all-changes' into dev 2019-10-15 08:51:30 -06:00
Peter Hindes
8c73253a52 follow stye on debug 2019-10-15 08:48:36 -06:00
Peter Hindes
4106645d6e Polished Results 2019-10-15 08:40:51 -06:00
Peter Hindes
c68c35e084 Better FIx, Improved Comment Style 2019-10-15 07:19:11 -06:00
Peter Hindes
fd34b1a291 Fixes Improved 2019-10-15 07:18:06 -06:00
Peter Hindes
bfc987f81b Revert "Revert "Added a restart song button to signle track expanded notification""
This reverts commit 646e327ed2.
2019-10-14 18:01:15 -06:00
Peter Hindes
c93c52a58c Wrap in multiline comment so it colapses 2019-10-14 16:55:16 -06:00
Peter Hindes
e72c6eed24 Explain What we did in the code itself 2019-10-14 16:48:44 -06:00
Peter Hindes
646e327ed2 Revert "Added a restart song button to signle track expanded notification"
This reverts commit cb5c219ffe.
2019-10-14 16:41:38 -06:00
Peter Hindes
cb5c219ffe Added a restart song button to signle track expanded notification
... and some comments to the code
2019-10-14 16:37:58 -06:00
Peter Hindes
3794002c7b much simpler, "uploader" will never change on a playlist 2019-10-14 14:56:04 -06:00
Peter Hindes
bd2b32bfbc Fixed Playlists With No Uploader Crashing The App 2019-10-14 11:55:55 -06:00
Haggai Eran
884528927e Fix Hebrew translation on some devices
Hebrew on android uses two locale codes (iw and he).
See details here: https://stackoverflow.com/a/8470980

For example, Nexus 7 (2013) uses iw, so it shows the English UI even when configuring the tablet to use Hebrew.

Add a symbolic link from values-iw to values-he so both use the same strings.
2019-10-12 12:21:01 +03:00
Igor Nedoboy
4688b1fe23 Translated using Weblate (Russian)
Currently translated at 100.0% (478 of 478 strings)
2019-10-12 07:08:12 +02:00
Hosted Weblate
dfd558f848 Merge branch 'origin/dev' into Weblate. 2019-10-11 16:00:20 +02:00
ssantos
d2e065d273 Translated using Weblate (Portuguese)
Currently translated at 100.0% (478 of 478 strings)
2019-10-11 16:00:20 +02:00
AntonAkovP
84a52342ab Translated using Weblate (Bulgarian)
Currently translated at 80.5% (385 of 478 strings)
2019-10-11 16:00:19 +02:00
Igor Nedoboy
6f3d5e9fb8 Translated using Weblate (Russian)
Currently translated at 100.0% (478 of 478 strings)
2019-10-11 16:00:18 +02:00
朱程宇
7d18eb8b02 Translated using Weblate (Chinese (Hong Kong))
Currently translated at 28.0% (134 of 478 strings)
2019-10-11 16:00:17 +02:00
朱程宇
7e3eb5e14d Added translation using Weblate (Chinese (Simplified)) 2019-10-11 16:00:09 +02:00
yausername
039a8e0b87 reordered services 2019-03-23 19:49:37 +05:30
Ritvik Saraf
a8b5534838 darker color for peertube 2019-03-10 19:08:08 +05:30
Ritvik Saraf
2581fa4176 init services in app onCreate 2019-03-10 17:30:21 +05:30
Ritvik Saraf
d90b1ca5be merged upstream/dev, changes for peertube support 2019-03-10 01:02:25 +05:30
Ritvik Saraf
845663f80f added themeing for peertube, change peertube instance 2018-12-29 23:06:39 +05:30
Ritvik Saraf
9530af95f4 Merge branch 'dev' into peertube 2018-12-25 20:18:39 +05:30
Ritvik Saraf
c1efa78820 Merge branch 'dev' into peertube 2018-12-25 20:00:56 +05:30
Ritvik Saraf
d61797fa3a merged dev 2018-12-25 19:31:25 +05:30
Ritvik Saraf
cc6989b4f9 updated extractor 2018-10-12 13:16:16 +05:30
Ritvik Saraf
fc31458cc4 added peertube 2018-10-12 02:34:30 +05:30
170 changed files with 7876 additions and 2363 deletions

View File

@@ -81,6 +81,7 @@ NewPipe supports multiple services. Our [docs](https://teamnewpipe.github.io/doc
* YouTube
* SoundCloud \[beta\]
* media.ccc.de \[beta\]
* PeerTube instances \[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:

View File

@@ -1,4 +1,7 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 28
@@ -8,8 +11,8 @@ android {
applicationId "org.schabi.newpipe"
minSdkVersion 19
targetSdkVersion 28
versionCode 790
versionName "0.17.4"
versionCode 820
versionName "0.18.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
@@ -44,7 +47,7 @@ android {
ext {
androidxLibVersion = '1.0.0'
exoPlayerLibVersion = '2.10.6'
exoPlayerLibVersion = '2.10.8'
roomDbLibVersion = '2.1.0'
leakCanaryLibVersion = '1.5.4' //1.6.1
okHttpLibVersion = '3.12.6'
@@ -53,11 +56,13 @@ ext {
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0', {
exclude module: 'support-annotations'
})
implementation 'com.github.teamnewpipe:NewPipeExtractor:v0.17.4'
implementation 'com.github.TeamNewPipe:NewPipeExtractor:ff61e284'
testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:2.23.0'
@@ -89,13 +94,14 @@ dependencies {
implementation 'io.reactivex.rxjava2:rxjava:2.2.2'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1'
implementation 'org.ocpsoft.prettytime:prettytime:4.0.1.Final'
implementation "androidx.room:room-runtime:${roomDbLibVersion}"
implementation "androidx.room:room-rxjava2:${roomDbLibVersion}"
annotationProcessor "androidx.room:room-compiler:${roomDbLibVersion}"
kapt "androidx.room:room-compiler:${roomDbLibVersion}"
implementation "frankiesardo:icepick:${icepickLibVersion}"
annotationProcessor "frankiesardo:icepick-processor:${icepickLibVersion}"
kapt "frankiesardo:icepick-processor:${icepickLibVersion}"
debugImplementation "com.squareup.leakcanary:leakcanary-android:${leakCanaryLibVersion}"
releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:${leakCanaryLibVersion}"

View File

@@ -17,6 +17,9 @@
#}
-dontobfuscate
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
-keep class org.ocpsoft.prettytime.i18n.** { *; }
-keep class org.mozilla.javascript.** { *; }
-keep class org.mozilla.classfile.ClassFileWriter

View File

@@ -15,7 +15,7 @@ import com.squareup.leakcanary.LeakCanary;
import com.squareup.leakcanary.LeakDirectoryProvider;
import com.squareup.leakcanary.RefWatcher;
import org.schabi.newpipe.extractor.Downloader;
import org.schabi.newpipe.extractor.downloader.Downloader;
import java.io.File;
import java.util.concurrent.TimeUnit;
@@ -39,7 +39,7 @@ public class DebugApp extends App {
@Override
protected Downloader getDownloader() {
return org.schabi.newpipe.Downloader.init(new OkHttpClient.Builder()
return DownloaderImpl.init(new OkHttpClient.Builder()
.addNetworkInterceptor(new StethoInterceptor()));
}

View File

@@ -153,6 +153,7 @@
<!-- channel prefix -->
<data android:pathPrefix="/channel/"/>
<data android:pathPrefix="/user/"/>
<data android:pathPrefix="/c/"/>
<!-- playlist prefix -->
<data android:pathPrefix="/playlist"/>
</intent-filter>

View File

@@ -6,9 +6,9 @@ import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.os.Build;
import android.util.Log;
import androidx.annotation.Nullable;
import android.util.Log;
import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache;
import com.nostra13.universalimageloader.core.ImageLoader;
@@ -21,13 +21,15 @@ import org.acra.config.ACRAConfiguration;
import org.acra.config.ACRAConfigurationException;
import org.acra.config.ConfigurationBuilder;
import org.acra.sender.ReportSenderFactory;
import org.schabi.newpipe.extractor.Downloader;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.report.AcraReportSenderFactory;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.StateSaver;
import java.io.IOException;
@@ -95,10 +97,15 @@ public class App extends Application {
SettingsActivity.initSettings(this);
NewPipe.init(getDownloader(),
org.schabi.newpipe.util.Localization.getPreferredExtractorLocal(this));
Localization.getPreferredLocalization(this),
Localization.getPreferredContentCountry(this));
Localization.init();
StateSaver.init(this);
initNotificationChannel();
ServiceHelper.initServices(this);
// Initialize image loader
ImageLoader.getInstance().init(getImageLoaderConfigurations(10, 50));
@@ -109,7 +116,7 @@ public class App extends Application {
}
protected Downloader getDownloader() {
return org.schabi.newpipe.Downloader.init(null);
return DownloaderImpl.init(null);
}
private void configureRxJavaErrorHandler() {

View File

@@ -107,6 +107,7 @@ public abstract class BaseFragment extends Fragment {
if (DEBUG) Log.d(TAG, "setTitle() called with: title = [" + title + "]");
if((!useAsFrontPage || mIsVisibleToUser)
&& (activity != null && activity.getSupportActionBar() != null)) {
activity.getSupportActionBar().setDisplayShowTitleEnabled(true);
activity.getSupportActionBar().setTitle(title);
}
}

View File

@@ -1,296 +0,0 @@
package org.schabi.newpipe;
import androidx.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.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;
/*
* Created by Christian Schabesberger on 28.01.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* Downloader.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 Downloader implements org.schabi.newpipe.extractor.Downloader {
public static final String USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0";
private static Downloader instance;
private String mCookies;
private final OkHttpClient client;
private Downloader(OkHttpClient.Builder builder) {
this.client = builder
.readTimeout(30, TimeUnit.SECONDS)
//.cache(new Cache(new File(context.getExternalCacheDir(), "okhttp"), 16 * 1024 * 1024))
.build();
}
/**
* It's recommended to call exactly once in the entire lifetime of the application.
*
* @param builder if null, default builder will be used
*/
public static Downloader init(@Nullable OkHttpClient.Builder builder) {
return instance = new Downloader(builder != null ? builder : new OkHttpClient.Builder());
}
public static Downloader getInstance() {
return instance;
}
public String getCookies() {
return mCookies;
}
public void setCookies(String cookies) {
mCookies = cookies;
}
/**
* Get the size of the content that the url is pointing by firing a HEAD request.
*
* @param url an url pointing to the content
* @return the size of the content, in bytes
*/
public long getContentLength(String url) throws IOException {
Response response = null;
try {
final Request request = new Request.Builder()
.head().url(url)
.addHeader("User-Agent", USER_AGENT)
.build();
response = client.newCall(request).execute();
String contentLength = response.header("Content-Length");
return contentLength == null ? -1 : Long.parseLong(contentLength);
} catch (NumberFormatException e) {
throw new IOException("Invalid content length", e);
} finally {
if (response != null) {
response.close();
}
}
}
/**
* Download the text file at the supplied URL as in download(String),
* but set the HTTP header field "Accept-Language" to the supplied string.
*
* @param siteUrl the URL of the text file to return the contents of
* @param localization the language and country (usually a 2-character code) to set
* @return the contents of the specified text file
*/
@Override
public String download(String siteUrl, Localization localization) throws IOException, ReCaptchaException {
Map<String, String> requestProperties = new HashMap<>();
requestProperties.put("Accept-Language", localization.getLanguage());
return download(siteUrl, requestProperties);
}
/**
* Download the text file at the supplied URL as in download(String),
* but set the HTTP headers included in the customProperties map.
*
* @param siteUrl the URL of the text file to return the contents of
* @param customProperties set request header properties
* @return the contents of the specified text file
* @throws IOException
*/
@Override
public String download(String siteUrl, Map<String, String> customProperties) throws IOException, ReCaptchaException {
return getBody(siteUrl, customProperties).string();
}
public InputStream stream(String siteUrl) throws IOException {
try {
return getBody(siteUrl, Collections.emptyMap()).byteStream();
} catch (ReCaptchaException e) {
throw new IOException(e.getMessage(), e.getCause());
}
}
private ResponseBody getBody(String siteUrl, Map<String, String> customProperties) throws IOException, ReCaptchaException {
final Request.Builder requestBuilder = new Request.Builder()
.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);
}
final Request request = requestBuilder.build();
final Response response = client.newCall(request).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 body;
}
/**
* Download (via HTTP) the text file located at the supplied URL, and return its contents.
* Primarily intended for downloading web pages.
*
* @param siteUrl the URL of the text file to download
* @return the contents of the specified text file
*/
@Override
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(response.code(), 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(response.code(), body.string(), response.headers().toMultimap());
}
@Override
public DownloadResponse head(String siteUrl) throws IOException, ReCaptchaException {
final Request request = new Request.Builder()
.head().url(siteUrl)
.addHeader("User-Agent", USER_AGENT)
.build();
final Response response = client.newCall(request).execute();
if (response.code() == 429) {
throw new ReCaptchaException("reCaptcha Challenge requested", siteUrl);
}
return new DownloadResponse(response.code(), null, response.headers().toMultimap());
}
}

View File

@@ -0,0 +1,219 @@
package org.schabi.newpipe;
import android.os.Build;
import android.text.TextUtils;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.downloader.Request;
import org.schabi.newpipe.extractor.downloader.Response;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.util.TLSSocketFactoryCompat;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import okhttp3.CipherSuite;
import okhttp3.ConnectionSpec;
import okhttp3.OkHttpClient;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import static org.schabi.newpipe.MainActivity.DEBUG;
public class DownloaderImpl extends Downloader {
public static final String USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0";
private static DownloaderImpl instance;
private String mCookies;
private OkHttpClient client;
private DownloaderImpl(OkHttpClient.Builder builder) {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
enableModernTLS(builder);
}
this.client = builder
.readTimeout(30, TimeUnit.SECONDS)
//.cache(new Cache(new File(context.getExternalCacheDir(), "okhttp"), 16 * 1024 * 1024))
.build();
}
/**
* It's recommended to call exactly once in the entire lifetime of the application.
*
* @param builder if null, default builder will be used
*/
public static DownloaderImpl init(@Nullable OkHttpClient.Builder builder) {
return instance = new DownloaderImpl(builder != null ? builder : new OkHttpClient.Builder());
}
public static DownloaderImpl getInstance() {
return instance;
}
public String getCookies() {
return mCookies;
}
public void setCookies(String cookies) {
mCookies = cookies;
}
/**
* Get the size of the content that the url is pointing by firing a HEAD request.
*
* @param url an url pointing to the content
* @return the size of the content, in bytes
*/
public long getContentLength(String url) throws IOException {
try {
final Response response = head(url);
return Long.parseLong(response.getHeader("Content-Length"));
} catch (NumberFormatException e) {
throw new IOException("Invalid content length", e);
} catch (ReCaptchaException e) {
throw new IOException(e);
}
}
public InputStream stream(String siteUrl) throws IOException {
try {
final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder()
.method("GET", null).url(siteUrl)
.addHeader("User-Agent", USER_AGENT);
if (!TextUtils.isEmpty(mCookies)) {
requestBuilder.addHeader("Cookie", mCookies);
}
final okhttp3.Request request = requestBuilder.build();
final okhttp3.Response response = client.newCall(request).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 body.byteStream();
} catch (ReCaptchaException e) {
throw new IOException(e.getMessage(), e.getCause());
}
}
@Override
public Response execute(@NonNull Request request) throws IOException, ReCaptchaException {
final String httpMethod = request.httpMethod();
final String url = request.url();
final Map<String, List<String>> headers = request.headers();
final byte[] dataToSend = request.dataToSend();
RequestBody requestBody = null;
if (dataToSend != null) {
requestBody = RequestBody.create(null, dataToSend);
}
final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder()
.method(httpMethod, requestBody).url(url)
.addHeader("User-Agent", USER_AGENT);
if (!TextUtils.isEmpty(mCookies)) {
requestBuilder.addHeader("Cookie", mCookies);
}
for (Map.Entry<String, List<String>> pair : headers.entrySet()) {
final String headerName = pair.getKey();
final List<String> headerValueList = pair.getValue();
if (headerValueList.size() > 1) {
requestBuilder.removeHeader(headerName);
for (String headerValue : headerValueList) {
requestBuilder.addHeader(headerName, headerValue);
}
} else if (headerValueList.size() == 1) {
requestBuilder.header(headerName, headerValueList.get(0));
}
}
final okhttp3.Response response = client.newCall(requestBuilder.build()).execute();
if (response.code() == 429) {
response.close();
throw new ReCaptchaException("reCaptcha Challenge requested", url);
}
final ResponseBody body = response.body();
String responseBodyToReturn = null;
if (body != null) {
responseBodyToReturn = body.string();
}
return new Response(response.code(), response.message(), response.headers().toMultimap(), responseBodyToReturn);
}
/**
* Enable TLS 1.2 and 1.1 on Android Kitkat. This function is mostly taken from the documentation of
* OkHttpClient.Builder.sslSocketFactory(_,_)
* <p>
* If there is an error, the function will safely fall back to doing nothing and printing the error to the console.
*
* @param builder The HTTPClient Builder on which TLS is enabled on (will be modified in-place)
*/
private static void enableModernTLS(OkHttpClient.Builder builder) {
try {
// get the default TrustManager
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore) null);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
throw new IllegalStateException("Unexpected default trust managers:"
+ Arrays.toString(trustManagers));
}
X509TrustManager trustManager = (X509TrustManager) trustManagers[0];
// insert our own TLSSocketFactory
SSLSocketFactory sslSocketFactory = TLSSocketFactoryCompat.getInstance();
builder.sslSocketFactory(sslSocketFactory, trustManager);
// This will try to enable all modern CipherSuites(+2 more) that are supported on the device.
// Necessary because some servers (e.g. Framatube.org) don't support the old cipher suites.
// https://github.com/square/okhttp/issues/4053#issuecomment-402579554
List<CipherSuite> cipherSuites = new ArrayList<>();
cipherSuites.addAll(ConnectionSpec.MODERN_TLS.cipherSuites());
cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA);
cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA);
ConnectionSpec legacyTLS = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.cipherSuites(cipherSuites.toArray(new CipherSuite[0]))
.build();
builder.connectionSpecs(Arrays.asList(legacyTLS, ConnectionSpec.CLEARTEXT));
} catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
if (DEBUG) e.printStackTrace();
}
}
}

View File

@@ -40,7 +40,7 @@ public class ImageDownloader extends BaseImageDownloader {
}
protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException {
final Downloader downloader = (Downloader) NewPipe.getDownloader();
final DownloaderImpl downloader = (DownloaderImpl) NewPipe.getDownloader();
return downloader.stream(imageUri);
}
}

View File

@@ -29,14 +29,18 @@ import android.os.Handler;
import android.os.Looper;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.Spinner;
import android.widget.TextView;
import androidx.annotation.NonNull;
@@ -47,12 +51,15 @@ import androidx.appcompat.widget.Toolbar;
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import com.google.android.material.navigation.NavigationView;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
import org.schabi.newpipe.fragments.BackPressable;
import org.schabi.newpipe.fragments.MainFragment;
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
@@ -61,11 +68,16 @@ import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.KioskTranslator;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PeertubeHelper;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.TLSSocketFactoryCompat;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
@@ -97,6 +109,11 @@ public class MainActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
// enable TLS1.1/1.2 for kitkat devices, to fix download and play for mediaCCC sources
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
TLSSocketFactoryCompat.setAsDefault();
}
ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this));
super.onCreate(savedInstanceState);
@@ -300,13 +317,57 @@ public class MainActivity extends AppCompatActivity {
final String title = s.getServiceInfo().getName() +
(ServiceHelper.isBeta(s) ? " (beta)" : "");
drawerItems.getMenu()
MenuItem menuItem = drawerItems.getMenu()
.add(R.id.menu_services_group, s.getServiceId(), ORDER, title)
.setIcon(ServiceHelper.getIcon(s.getServiceId()));
// peertube specifics
if(s.getServiceId() == 3){
enhancePeertubeMenu(s, menuItem);
}
}
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true);
}
private void enhancePeertubeMenu(StreamingService s, MenuItem menuItem) {
PeertubeInstance currentInstace = PeertubeHelper.getCurrentInstance();
menuItem.setTitle(currentInstace.getName() + (ServiceHelper.isBeta(s) ? " (beta)" : ""));
Spinner spinner = (Spinner) LayoutInflater.from(this).inflate(R.layout.instance_spinner_layout, null);
List<PeertubeInstance> instances = PeertubeHelper.getInstanceList(this);
List<String> items = new ArrayList<>();
int defaultSelect = 0;
for(PeertubeInstance instance: instances){
items.add(instance.getName());
if(instance.getUrl().equals(currentInstace.getUrl())){
defaultSelect = items.size()-1;
}
}
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, R.layout.instance_spinner_item, items);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter);
spinner.setSelection(defaultSelect, false);
spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
PeertubeInstance newInstance = instances.get(position);
if(newInstance.getUrl().equals(PeertubeHelper.getCurrentInstance().getUrl())) return;
PeertubeHelper.selectInstance(newInstance, getApplicationContext());
changeService(menuItem);
drawer.closeDrawers();
new Handler(Looper.getMainLooper()).postDelayed(() -> {
getSupportFragmentManager().popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
recreate();
}, 300);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
menuItem.setActionView(spinner);
}
private void showTabs() throws ExtractionException {
serviceArrow.setImageResource(R.drawable.ic_arrow_down_white);
@@ -367,6 +428,7 @@ public class MainActivity extends AppCompatActivity {
String selectedServiceName = NewPipe.getService(
ServiceHelper.getSelectedServiceId(this)).getServiceInfo().getName();
headerServiceView.setText(selectedServiceName);
headerServiceView.post(() -> headerServiceView.setSelected(true));
toggleServiceButton.setContentDescription(
getString(R.string.drawer_header_description) + selectedServiceName);
} catch (Exception e) {

View File

@@ -112,7 +112,7 @@ public class ReCaptchaActivity extends AppCompatActivity {
// find cookies : s_gl & goojf and Add cookies to Downloader
if (find_access_cookies(cookies)) {
// Give cookies to Downloader class
Downloader.getInstance().setCookies(mCookies);
DownloaderImpl.getInstance().setCookies(mCookies);
// Closing activity and return to parent
setResult(RESULT_OK);

View File

@@ -1,5 +1,7 @@
package org.schabi.newpipe.database.playlist.model;
import android.text.TextUtils;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.Ignore;
@@ -72,10 +74,16 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
@Ignore
public boolean isIdenticalTo(final PlaylistInfo info) {
return getServiceId() == info.getServiceId() && getName().equals(info.getName()) &&
getStreamCount() == info.getStreamCount() && getUrl().equals(info.getUrl()) &&
getThumbnailUrl().equals(info.getThumbnailUrl()) &&
getUploader().equals(info.getUploaderName());
/*
* Returns boolean comparing the online playlist and the local copy.
* (False if info changed such as playlist name or track count)
*/
return getServiceId() == info.getServiceId()
&& getStreamCount() == info.getStreamCount()
&& TextUtils.equals(getName(), info.getName())
&& TextUtils.equals(getUrl(), info.getUrl())
&& TextUtils.equals(getThumbnailUrl(), info.getThumbnailUrl())
&& TextUtils.equals(getUploader(), info.getUploaderName());
}
public long getUid() {

View File

@@ -38,14 +38,15 @@ import com.nononsenseapps.filepicker.Utils;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.RouterActivity;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.localization.Localization;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.Stream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.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;
@@ -68,6 +69,7 @@ import java.util.Locale;
import icepick.Icepick;
import icepick.State;
import io.reactivex.disposables.CompositeDisposable;
import us.shandian.giga.get.MissionRecoveryInfo;
import us.shandian.giga.io.StoredDirectoryHelper;
import us.shandian.giga.io.StoredFileHelper;
import us.shandian.giga.postprocessing.Postprocessing;
@@ -367,6 +369,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
toolbar.setOnMenuItemClickListener(item -> {
if (item.getItemId() == R.id.okay) {
prepareSelectedDownload();
if (getActivity() instanceof RouterActivity) {
getActivity().finish();
}
return true;
}
return false;
@@ -488,35 +493,24 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
}
private int getSubtitleIndexBy(List<SubtitlesStream> streams) {
Localization loc = NewPipe.getPreferredLocalization();
final Localization preferredLocalization = NewPipe.getPreferredLocalization();
int candidate = 0;
for (int i = 0; i < streams.size(); i++) {
Locale streamLocale = streams.get(i).getLocale();
String tag = streamLocale.getLanguage().concat("-").concat(streamLocale.getCountry());
if (tag.equalsIgnoreCase(loc.getLanguage())) {
return i;
final Locale streamLocale = streams.get(i).getLocale();
final boolean languageEquals = streamLocale.getLanguage() != null && preferredLocalization.getLanguageCode() != null &&
streamLocale.getLanguage().equals(new Locale(preferredLocalization.getLanguageCode()).getLanguage());
final boolean countryEquals = streamLocale.getCountry() != null && streamLocale.getCountry().equals(preferredLocalization.getCountryCode());
if (languageEquals) {
if (countryEquals) return i;
candidate = i;
}
}
// fallback
// 1st loop match country & language
// 2nd loop match language only
int index = loc.getLanguage().indexOf("-");
String lang = index > 0 ? loc.getLanguage().substring(0, index) : loc.getLanguage();
for (int j = 0; j < 2; j++) {
for (int i = 0; i < streams.size(); i++) {
Locale streamLocale = streams.get(i).getLocale();
if (streamLocale.getLanguage().equalsIgnoreCase(lang)) {
if (j > 0 || streamLocale.getCountry().equalsIgnoreCase(loc.getCountry())) {
return i;
}
}
}
}
return 0;
return candidate;
}
StoredDirectoryHelper mainStorageAudio = null;
@@ -565,8 +559,16 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
case R.id.audio_button:
mainStorage = mainStorageAudio;
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
mime = format.mimeType;
filename += format.suffix;
switch(format) {
case WEBMA_OPUS:
mime = "audio/ogg";
filename += "opus";
break;
default:
mime = format.mimeType;
filename += format.suffix;
break;
}
break;
case R.id.video_button:
mainStorage = mainStorageVideo;
@@ -773,12 +775,13 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
}
Stream selectedStream;
Stream secondaryStream = null;
char kind;
int threads = threadsSeekBar.getProgress() + 1;
String[] urls;
MissionRecoveryInfo[] recoveryInfo;
String psName = null;
String[] psArgs = null;
String secondaryStreamUrl = null;
long nearLength = 0;
// more download logic: select muxer, subtitle converter, etc.
@@ -789,18 +792,20 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
if (selectedStream.getFormat() == MediaFormat.M4A) {
psName = Postprocessing.ALGORITHM_M4A_NO_DASH;
} else if (selectedStream.getFormat() == MediaFormat.WEBMA_OPUS) {
psName = Postprocessing.ALGORITHM_OGG_FROM_WEBM_DEMUXER;
}
break;
case R.id.video_button:
kind = 'v';
selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex);
SecondaryStreamHelper<AudioStream> secondaryStream = videoStreamsAdapter
SecondaryStreamHelper<AudioStream> secondary = videoStreamsAdapter
.getAllSecondary()
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
if (secondaryStream != null) {
secondaryStreamUrl = secondaryStream.getStream().getUrl();
if (secondary != null) {
secondaryStream = secondary.getStream();
if (selectedStream.getFormat() == MediaFormat.MPEG_4)
psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER;
@@ -812,8 +817,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
// 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;
if (secondary.getSizeInBytes() > 0 && videoSize > 0) {
nearLength = secondary.getSizeInBytes() + videoSize;
}
}
break;
@@ -835,13 +840,25 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
return;
}
if (secondaryStreamUrl == null) {
urls = new String[]{selectedStream.getUrl()};
if (secondaryStream == null) {
urls = new String[]{
selectedStream.getUrl()
};
recoveryInfo = new MissionRecoveryInfo[]{
new MissionRecoveryInfo(selectedStream)
};
} else {
urls = new String[]{selectedStream.getUrl(), secondaryStreamUrl};
urls = new String[]{
selectedStream.getUrl(), secondaryStream.getUrl()
};
recoveryInfo = new MissionRecoveryInfo[]{
new MissionRecoveryInfo(selectedStream), new MissionRecoveryInfo(secondaryStream)
};
}
DownloadManagerService.startMission(context, urls, storage, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength);
DownloadManagerService.startMission(
context, urls, storage, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength, recoveryInfo
);
dismiss();
}

View File

@@ -1,5 +1,6 @@
package org.schabi.newpipe.fragments;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
@@ -15,7 +16,7 @@ import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayout;
@@ -29,6 +30,7 @@ import org.schabi.newpipe.settings.tabs.Tab;
import org.schabi.newpipe.settings.tabs.TabsManager;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.views.ScrollableTabLayout;
import java.util.ArrayList;
import java.util.List;
@@ -36,7 +38,7 @@ import java.util.List;
public class MainFragment extends BaseFragment implements TabLayout.OnTabSelectedListener {
private ViewPager viewPager;
private SelectedTabsPagerAdapter pagerAdapter;
private TabLayout tabLayout;
private ScrollableTabLayout tabLayout;
private List<Tab> tabsList = new ArrayList<>();
private TabsManager tabsManager;
@@ -52,32 +54,19 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
destroyOldFragments();
tabsManager = TabsManager.getManager(activity);
tabsManager.setSavedTabsListener(() -> {
if (DEBUG) {
Log.d(TAG, "TabsManager.SavedTabsChangeListener: onTabsChanged called, isResumed = " + isResumed());
}
if (isResumed()) {
updateTabs();
setupTabs();
} else {
hasTabsChanged = true;
}
});
}
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);
@@ -90,23 +79,17 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
tabLayout = rootView.findViewById(R.id.main_tab_layout);
viewPager = rootView.findViewById(R.id.pager);
/* Nested fragment, use child fragment here to maintain backstack in view pager. */
pagerAdapter = new SelectedTabsPagerAdapter(getChildFragmentManager());
viewPager.setAdapter(pagerAdapter);
tabLayout.setupWithViewPager(viewPager);
tabLayout.addOnTabSelectedListener(this);
updateTabs();
setupTabs();
}
@Override
public void onResume() {
super.onResume();
if (hasTabsChanged) {
hasTabsChanged = false;
updateTabs();
}
if (hasTabsChanged) setupTabs();
}
@Override
@@ -153,45 +136,42 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
// Tabs
//////////////////////////////////////////////////////////////////////////*/
public void updateTabs() {
public void setupTabs() {
tabsList.clear();
tabsList.addAll(tabsManager.getTabs());
pagerAdapter.notifyDataSetChanged();
viewPager.setOffscreenPageLimit(pagerAdapter.getCount());
updateTabsIcon();
updateTabsContentDescription();
updateCurrentTitle();
if (pagerAdapter == null || !pagerAdapter.sameTabs(tabsList)) {
pagerAdapter = new SelectedTabsPagerAdapter(requireContext(), getChildFragmentManager(), tabsList);
}
// Clear previous tabs/fragments and set new adapter
viewPager.setAdapter(pagerAdapter);
viewPager.setOffscreenPageLimit(tabsList.size());
updateTabsIconAndDescription();
updateTitleForTab(viewPager.getCurrentItem());
hasTabsChanged = false;
}
private void updateTabsIcon() {
private void updateTabsIconAndDescription() {
for (int i = 0; i < tabsList.size(); i++) {
final TabLayout.Tab tabToSet = tabLayout.getTabAt(i);
if (tabToSet != null) {
tabToSet.setIcon(tabsList.get(i).getTabIconRes(activity));
final Tab tab = tabsList.get(i);
tabToSet.setIcon(tab.getTabIconRes(requireContext()));
tabToSet.setContentDescription(tab.getTabName(requireContext()));
}
}
}
private void updateTabsContentDescription() {
for (int i = 0; i < tabsList.size(); i++) {
final TabLayout.Tab tabToSet = tabLayout.getTabAt(i);
if (tabToSet != null) {
final Tab t = tabsList.get(i);
tabToSet.setIcon(t.getTabIconRes(activity));
tabToSet.setContentDescription(t.getTabName(activity));
}
}
}
private void updateCurrentTitle() {
setTitle(tabsList.get(viewPager.getCurrentItem()).getTabName(requireContext()));
private void updateTitleForTab(int tabPosition) {
setTitle(tabsList.get(tabPosition).getTabName(requireContext()));
}
@Override
public void onTabSelected(TabLayout.Tab selectedTab) {
if (DEBUG) Log.d(TAG, "onTabSelected() called with: selectedTab = [" + selectedTab + "]");
updateCurrentTitle();
updateTitleForTab(selectedTab.getPosition());
}
@Override
@@ -201,29 +181,33 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
@Override
public void onTabReselected(TabLayout.Tab tab) {
if (DEBUG) Log.d(TAG, "onTabReselected() called with: tab = [" + tab + "]");
updateCurrentTitle();
updateTitleForTab(tab.getPosition());
}
private class SelectedTabsPagerAdapter extends FragmentPagerAdapter {
private static class SelectedTabsPagerAdapter extends FragmentStatePagerAdapter {
private final Context context;
private final List<Tab> internalTabsList;
private SelectedTabsPagerAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
private SelectedTabsPagerAdapter(Context context, FragmentManager fragmentManager, List<Tab> tabsList) {
super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
this.context = context;
this.internalTabsList = new ArrayList<>(tabsList);
}
@Override
public Fragment getItem(int position) {
final Tab tab = tabsList.get(position);
final Tab tab = internalTabsList.get(position);
Throwable throwable = null;
Fragment fragment = null;
try {
fragment = tab.getFragment();
fragment = tab.getFragment(context);
} catch (ExtractionException e) {
throwable = e;
}
if (throwable != null) {
ErrorActivity.reportError(activity, throwable, activity.getClass(), null,
ErrorActivity.reportError(context, throwable, null, null,
ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash));
return new BlankFragment();
}
@@ -244,15 +228,11 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
@Override
public int getCount() {
return tabsList.size();
return internalTabsList.size();
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
getChildFragmentManager()
.beginTransaction()
.remove((Fragment) object)
.commitNowAllowingStateLoss();
public boolean sameTabs(List<Tab> tabsToCompare) {
return internalTabsList.equals(tabsToCompare);
}
}
}

View File

@@ -1067,7 +1067,13 @@ public class VideoDetailFragment
uploaderThumb.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.buddy));
if (info.getViewCount() >= 0) {
videoCountView.setText(Localization.localizeViewCount(activity, info.getViewCount()));
if (info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) {
videoCountView.setText(Localization.listeningCount(activity, info.getViewCount()));
} else if (info.getStreamType().equals(StreamType.LIVE_STREAM)) {
videoCountView.setText(Localization.watchingCount(activity, info.getViewCount()));
} else {
videoCountView.setText(Localization.localizeViewCount(activity, info.getViewCount()));
}
videoCountView.setVisibility(View.VISIBLE);
} else {
videoCountView.setVisibility(View.GONE);
@@ -1120,9 +1126,15 @@ public class VideoDetailFragment
videoTitleToggleArrow.setVisibility(View.VISIBLE);
videoTitleToggleArrow.setImageResource(R.drawable.arrow_down);
videoDescriptionRootLayout.setVisibility(View.GONE);
if (!TextUtils.isEmpty(info.getUploadDate())) {
videoUploadDateView.setText(Localization.localizeDate(activity, info.getUploadDate()));
if (info.getUploadDate() != null) {
videoUploadDateView.setText(Localization.localizeUploadDate(activity, info.getUploadDate().date().getTime()));
videoUploadDateView.setVisibility(View.VISIBLE);
} else {
videoUploadDateView.setText(null);
videoUploadDateView.setVisibility(View.GONE);
}
prepareDescription(info.getDescription());
updateProgressInfo(info);

View File

@@ -111,6 +111,8 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
super.startLoading(forceLoad);
showListFooter(false);
infoListAdapter.clearStreamItemList();
currentInfo = null;
if (currentWorker != null) currentWorker.dispose();
currentWorker = loadResult(forceLoad)

View File

@@ -29,6 +29,7 @@ import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
@@ -98,7 +99,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if(activity != null
if (activity != null
&& useAsFrontPage
&& isVisibleToUser) {
setTitle(currentInfo != null ? currentInfo.getName() : name);
@@ -152,7 +153,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
ActionBar supportActionBar = activity.getSupportActionBar();
if(useAsFrontPage && supportActionBar != null) {
if (useAsFrontPage && supportActionBar != null) {
supportActionBar.setDisplayHomeAsUpEnabled(false);
} else {
inflater.inflate(R.menu.menu_channel, menu);
@@ -165,7 +166,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
private void openRssFeed() {
final ChannelInfo info = currentInfo;
if(info != null) {
if (info != null) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(info.getFeedUrl()));
startActivity(intent);
}
@@ -178,10 +179,14 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
openRssFeed();
break;
case R.id.menu_item_openInBrowser:
ShareUtils.openUrlInBrowser(this.getContext(), currentInfo.getOriginalUrl());
if (currentInfo != null) {
ShareUtils.openUrlInBrowser(this.getContext(), currentInfo.getOriginalUrl());
}
break;
case R.id.menu_item_share:
ShareUtils.shareUrl(this.getContext(), name, currentInfo.getOriginalUrl());
if (currentInfo != null) {
ShareUtils.shareUrl(this.getContext(), name, currentInfo.getOriginalUrl());
}
break;
default:
return super.onOptionsItemSelected(item);
@@ -218,7 +223,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
.debounce(100, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe((List<SubscriptionEntity> subscriptionEntities) ->
updateSubscribeButton(!subscriptionEntities.isEmpty())
updateSubscribeButton(!subscriptionEntities.isEmpty())
, onError));
}
@@ -359,9 +364,9 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
headerRootLayout.setVisibility(View.VISIBLE);
imageLoader.displayImage(result.getBannerUrl(), headerChannelBanner,
ImageDisplayConstants.DISPLAY_BANNER_OPTIONS);
ImageDisplayConstants.DISPLAY_BANNER_OPTIONS);
imageLoader.displayImage(result.getAvatarUrl(), headerAvatarView,
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
headerSubscribersTextView.setVisibility(View.VISIBLE);
if (result.getSubscriberCount() >= 0) {
@@ -397,8 +402,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
private PlayQueue getPlayQueue(final int index) {
final List<StreamInfoItem> streamItems = new ArrayList<>();
for(InfoItem i : infoListAdapter.getItemsList()) {
if(i instanceof StreamInfoItem) {
for (InfoItem i : infoListAdapter.getItemsList()) {
if (i instanceof StreamInfoItem) {
streamItems.add((StreamInfoItem) i);
}
}
@@ -432,12 +437,16 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
protected boolean onError(Throwable exception) {
if (super.onError(exception)) return true;
int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error;
onUnrecoverableError(exception,
UserAction.REQUESTED_CHANNEL,
NewPipe.getNameOfService(serviceId),
url,
errorId);
if (exception instanceof ContentNotAvailableException) {
showError(getString(R.string.content_not_available), false);
} else {
int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error;
onUnrecoverableError(exception,
UserAction.REQUESTED_CHANNEL,
NewPipe.getNameOfService(serviceId),
url,
errorId);
}
return true;
}

View File

@@ -0,0 +1,51 @@
package org.schabi.newpipe.fragments.list.kiosk;
import android.os.Bundle;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.kiosk.KioskList;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.KioskTranslator;
import org.schabi.newpipe.util.ServiceHelper;
public class DefaultKioskFragment extends KioskFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (serviceId < 0) {
updateSelectedDefaultKiosk();
}
}
@Override
public void onResume() {
super.onResume();
if (serviceId != ServiceHelper.getSelectedServiceId(requireContext())) {
if (currentWorker != null) currentWorker.dispose();
updateSelectedDefaultKiosk();
reloadContent();
}
}
private void updateSelectedDefaultKiosk() {
try {
serviceId = ServiceHelper.getSelectedServiceId(requireContext());
final KioskList kioskList = NewPipe.getService(serviceId).getKioskList();
kioskId = kioskList.getDefaultKioskId();
url = kioskList.getListLinkHandlerFactoryByType(kioskId).fromId(kioskId).getUrl();
kioskTranslatedName = KioskTranslator.getTranslatedKioskName(kioskId, requireContext());
name = kioskTranslatedName;
currentInfo = null;
currentNextPageUrl = null;
} catch (ExtractionException e) {
onUnrecoverableError(e, UserAction.REQUESTED_KIOSK, "none", "Loading default kiosk from selected service", 0);
}
}
}

View File

@@ -4,6 +4,8 @@ import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import android.preference.PreferenceManager;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -17,10 +19,12 @@ import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.kiosk.KioskInfo;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
import org.schabi.newpipe.extractor.localization.ContentCountry;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.KioskTranslator;
import org.schabi.newpipe.util.Localization;
import icepick.State;
import io.reactivex.Single;
@@ -52,6 +56,8 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
@State
protected String kioskId = "";
protected String kioskTranslatedName;
@State
protected ContentCountry contentCountry;
/*//////////////////////////////////////////////////////////////////////////
@@ -87,6 +93,7 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
kioskTranslatedName = KioskTranslator.getTranslatedKioskName(kioskId, activity);
name = kioskTranslatedName;
contentCountry = Localization.getPreferredContentCountry(requireContext());
}
@Override
@@ -108,6 +115,15 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
return inflater.inflate(R.layout.fragment_kiosk, container, false);
}
@Override
public void onResume() {
super.onResume();
if (!Localization.getPreferredContentCountry(requireContext()).equals(contentCountry)) {
reloadContent();
}
}
/*//////////////////////////////////////////////////////////////////////////
// Menu
//////////////////////////////////////////////////////////////////////////*/
@@ -127,6 +143,7 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
@Override
public Single<KioskInfo> loadResult(boolean forceReload) {
contentCountry = Localization.getPreferredContentCountry(requireContext());
return ExtractorHelper.getKioskInfo(serviceId,
url,
forceReload);

View File

@@ -259,7 +259,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
animateView(headerRootLayout, true, 100);
animateView(headerUploaderLayout, true, 300);
headerUploaderLayout.setOnClickListener(null);
if (!TextUtils.isEmpty(result.getUploaderName())) {
if (!TextUtils.isEmpty(result.getUploaderName())) { // If we have an uploader : Put them into the ui
headerUploaderName.setText(result.getUploaderName());
if (!TextUtils.isEmpty(result.getUploaderUrl())) {
headerUploaderLayout.setOnClickListener(v -> {
@@ -273,6 +273,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
}
});
}
} else { // Else : say we have no uploader
headerUploaderName.setText(R.string.playlist_no_uploader);
}
playlistCtrl.setVisibility(View.VISIBLE);
@@ -444,4 +446,4 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
playlistBookmarkButton.setIcon(ThemeHelper.resolveResourceIdFromAttr(activity, iconAttr));
playlistBookmarkButton.setTitle(titleRes);
}
}
}

View File

@@ -14,6 +14,7 @@ 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.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import java.util.regex.Matcher;
@@ -101,10 +102,17 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
ellipsize();
}
if (null != item.getLikeCount()) {
if (item.getLikeCount() >= 0) {
itemLikesCountView.setText(String.valueOf(item.getLikeCount()));
} else {
itemLikesCountView.setText("-");
}
if (item.getPublishedTime() != null) {
itemPublishedTime.setText(Localization.relativeTime(item.getPublishedTime().date()));
} else {
itemPublishedTime.setText(item.getTextualPublishedTime());
}
itemPublishedTime.setText(item.getPublishedTime());
itemView.setOnClickListener(view -> {
toggleEllipsize();

View File

@@ -1,5 +1,6 @@
package org.schabi.newpipe.info_list.holder;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.view.ViewGroup;
import android.widget.TextView;
@@ -7,10 +8,13 @@ import android.widget.TextView;
import org.schabi.newpipe.R;
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.Localization;
import static org.schabi.newpipe.MainActivity.DEBUG;
/*
* Created by Christian Schabesberger on 01.08.16.
* <p>
@@ -53,15 +57,38 @@ public class StreamInfoItemHolder extends StreamMiniInfoItemHolder {
private String getStreamInfoDetailLine(final StreamInfoItem infoItem) {
String viewsAndDate = "";
if (infoItem.getViewCount() >= 0) {
viewsAndDate = Localization.shortViewCount(itemBuilder.getContext(), infoItem.getViewCount());
}
if (!TextUtils.isEmpty(infoItem.getUploadDate())) {
if (viewsAndDate.isEmpty()) {
viewsAndDate = infoItem.getUploadDate();
if (infoItem.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) {
viewsAndDate = Localization.listeningCount(itemBuilder.getContext(), infoItem.getViewCount());
} else if (infoItem.getStreamType().equals(StreamType.LIVE_STREAM)) {
viewsAndDate = Localization.watchingCount(itemBuilder.getContext(), infoItem.getViewCount());
} else {
viewsAndDate += "" + infoItem.getUploadDate();
viewsAndDate = Localization.shortViewCount(itemBuilder.getContext(), infoItem.getViewCount());
}
}
final String uploadDate = getFormattedRelativeUploadDate(infoItem);
if (!TextUtils.isEmpty(uploadDate)) {
if (viewsAndDate.isEmpty()) {
return uploadDate;
}
return Localization.concatenateStrings(viewsAndDate, uploadDate);
}
return viewsAndDate;
}
private String getFormattedRelativeUploadDate(final StreamInfoItem infoItem) {
if (infoItem.getUploadDate() != null) {
String formattedRelativeTime = Localization.relativeTime(infoItem.getUploadDate().date());
if (DEBUG && PreferenceManager.getDefaultSharedPreferences(itemBuilder.getContext())
.getBoolean(itemBuilder.getContext().getString(R.string.show_original_time_ago_key), false)) {
formattedRelativeTime += " (" + infoItem.getTextualUploadDate() + ")";
}
return formattedRelativeTime;
} else {
return infoItem.getTextualUploadDate();
}
}
}

View File

@@ -1,15 +1,16 @@
package org.schabi.newpipe.local.dialog;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem;
@@ -152,6 +153,12 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
final Toast successToast = Toast.makeText(getContext(),
R.string.playlist_add_stream_success, Toast.LENGTH_SHORT);
if (playlist.thumbnailUrl.equals("drawable://" + R.drawable.dummy_thumbnail_playlist)) {
playlistDisposables.add(manager.changePlaylistThumbnail(playlist.uid, streams.get(0).getThumbnailUrl())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> successToast.show()));
}
playlistDisposables.add(manager.appendToPlaylist(playlist.uid, streams)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> successToast.show()));

View File

@@ -10,6 +10,8 @@ import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
import android.text.TextUtils;
import java.text.DateFormat;
public class RemotePlaylistItemHolder extends PlaylistItemHolder {
@@ -28,8 +30,14 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
itemTitleView.setText(item.getName());
itemStreamCountView.setText(String.valueOf(item.getStreamCount()));
itemUploaderView.setText(Localization.concatenateStrings(item.getUploader(),
// Here is where the uploader name is set in the bookmarked playlists library
if (!TextUtils.isEmpty(item.getUploader())) {
itemUploaderView.setText(Localization.concatenateStrings(item.getUploader(),
NewPipe.getNameOfService(item.getServiceId())));
} else {
itemUploaderView.setText(NewPipe.getNameOfService(item.getServiceId()));
}
itemBuilder.displayImage(item.getThumbnailUrl(), itemThumbnailView,
ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS);

View File

@@ -4,11 +4,6 @@ import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.ItemTouchHelper;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
@@ -18,6 +13,12 @@ import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.schabi.newpipe.NewPipeDatabase;
@@ -325,6 +326,16 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
headerBackgroundButton.setOnClickListener(view ->
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;
});
hideLoading();
}
@@ -403,10 +414,25 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
disposables.add(disposable);
}
private void updateThumbnailUrl() {
String newThumbnailUrl;
if (!itemListAdapter.getItemsList().isEmpty()) {
newThumbnailUrl = ((PlaylistStreamEntry) itemListAdapter.getItemsList().get(0)).thumbnailUrl;
} else {
newThumbnailUrl = "drawable://" + R.drawable.dummy_thumbnail_playlist;
}
changeThumbnailUrl(newThumbnailUrl);
}
private void deleteItem(final PlaylistStreamEntry item) {
if (itemListAdapter == null) return;
itemListAdapter.removeItem(item);
if (playlistManager.getPlaylistThumbnail(playlistId).equals(item.thumbnailUrl))
updateThumbnailUrl();
setVideoCount(itemListAdapter.getItemsList().size());
saveChanges();
}

View File

@@ -103,6 +103,10 @@ public class LocalPlaylistManager {
return modifyPlaylist(playlistId, null, thumbnailUrl);
}
public String getPlaylistThumbnail(final long playlistId) {
return playlistTable.getPlaylist(playlistId).blockingFirst().get(0).getThumbnailUrl();
}
private Maybe<Integer> modifyPlaylist(final long playlistId,
@Nullable final String name,
@Nullable final String thumbnailUrl) {

View File

@@ -25,12 +25,17 @@ import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.os.Build;
import android.os.IBinder;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.View;
import android.widget.RemoteViews;
@@ -48,6 +53,7 @@ import org.schabi.newpipe.player.helper.LockManager;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.resolver.AudioPlaybackResolver;
import org.schabi.newpipe.player.resolver.MediaSourceTag;
import org.schabi.newpipe.util.BitmapUtils;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ThemeHelper;
@@ -75,6 +81,7 @@ public final class BackgroundPlayer extends Service {
private BasePlayerImpl basePlayerImpl;
private LockManager lockManager;
private SharedPreferences sharedPreferences;
/*//////////////////////////////////////////////////////////////////////////
// Service-Activity Binder
@@ -95,6 +102,9 @@ public final class BackgroundPlayer extends Service {
private boolean shouldUpdateOnProgress;
private static final int NOTIFICATION_UPDATES_BEFORE_RESET = 60;
private int timesNotificationUpdated;
/*//////////////////////////////////////////////////////////////////////////
// Service's LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@@ -104,6 +114,7 @@ public final class BackgroundPlayer extends Service {
if (DEBUG) Log.d(TAG, "onCreate() called");
notificationManager = ((NotificationManager) getSystemService(NOTIFICATION_SERVICE));
lockManager = new LockManager(this);
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
ThemeHelper.setTheme(this);
basePlayerImpl = new BasePlayerImpl(this);
@@ -180,6 +191,7 @@ public final class BackgroundPlayer extends Service {
private void resetNotification() {
notBuilder = createNotification();
timesNotificationUpdated = 0;
}
private NotificationCompat.Builder createNotification() {
@@ -195,12 +207,45 @@ public final class BackgroundPlayer extends Service {
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setCustomContentView(notRemoteView)
.setCustomBigContentView(bigNotRemoteView);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setLockScreenThumbnail(builder);
}
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
builder.setPriority(NotificationCompat.PRIORITY_MAX);
}
return builder;
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void setLockScreenThumbnail(NotificationCompat.Builder builder) {
boolean isLockScreenThumbnailEnabled = sharedPreferences.getBoolean(
getString(R.string.enable_lock_screen_video_thumbnail_key),
true
);
if (isLockScreenThumbnailEnabled) {
basePlayerImpl.mediaSessionManager.setLockScreenArt(
builder,
getCenteredThumbnailBitmap()
);
} else {
basePlayerImpl.mediaSessionManager.clearLockScreenArt(builder);
}
}
@Nullable
private Bitmap getCenteredThumbnailBitmap() {
int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels;
int screenHeight = Resources.getSystem().getDisplayMetrics().heightPixels;
return BitmapUtils.centerCrop(
basePlayerImpl.getThumbnail(),
screenWidth,
screenHeight);
}
private void setupNotification(RemoteViews remoteViews) {
if (basePlayerImpl == null) return;
@@ -248,10 +293,13 @@ public final class BackgroundPlayer extends Service {
//if (DEBUG) Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]");
if (notBuilder == null) return;
if (drawableId != -1) {
if (notRemoteView != null) notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId);
if (bigNotRemoteView != null) bigNotRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId);
if (notRemoteView != null)
notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId);
if (bigNotRemoteView != null)
bigNotRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId);
}
notificationManager.notify(NOTIFICATION_ID, notBuilder.build());
timesNotificationUpdated++;
}
/*//////////////////////////////////////////////////////////////////////////
@@ -275,7 +323,8 @@ public final class BackgroundPlayer extends Service {
protected class BasePlayerImpl extends BasePlayer {
@NonNull final private AudioPlaybackResolver resolver;
@NonNull
final private AudioPlaybackResolver resolver;
private int cachedDuration;
private String cachedDurationString;
@@ -294,8 +343,10 @@ public final class BackgroundPlayer extends Service {
super.handleIntent(intent);
resetNotification();
if (bigNotRemoteView != null) bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 0, false);
if (notRemoteView != null) notRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 0, false);
if (bigNotRemoteView != null)
bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 0, false);
if (notRemoteView != null)
notRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 0, false);
startForeground(NOTIFICATION_ID, notBuilder.build());
}
@@ -330,6 +381,7 @@ public final class BackgroundPlayer extends Service {
updateNotificationThumbnail();
updateNotification(-1);
}
/*//////////////////////////////////////////////////////////////////////////
// States Implementation
//////////////////////////////////////////////////////////////////////////*/
@@ -351,10 +403,15 @@ public final class BackgroundPlayer extends Service {
updateProgress(currentProgress, duration, bufferPercent);
if (!shouldUpdateOnProgress) return;
resetNotification();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O /*Oreo*/) updateNotificationThumbnail();
if (timesNotificationUpdated > NOTIFICATION_UPDATES_BEFORE_RESET) {
resetNotification();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O /*Oreo*/) {
updateNotificationThumbnail();
}
}
if (bigNotRemoteView != null) {
if(cachedDuration != duration) {
if (cachedDuration != duration) {
cachedDuration = duration;
cachedDurationString = getTimeString(duration);
}
@@ -382,8 +439,10 @@ public final class BackgroundPlayer extends Service {
@Override
public void destroy() {
super.destroy();
if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, null);
if (bigNotRemoteView != null) bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, null);
if (notRemoteView != null)
notRemoteView.setImageViewBitmap(R.id.notificationCover, null);
if (bigNotRemoteView != null)
bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, null);
}
/*//////////////////////////////////////////////////////////////////////////

View File

@@ -55,7 +55,7 @@ import com.nostra13.universalimageloader.core.assist.FailReason;
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.Downloader;
import org.schabi.newpipe.DownloaderImpl;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.local.history.HistoryRecordManager;
@@ -178,7 +178,6 @@ public abstract class BasePlayer implements
// Player
//////////////////////////////////////////////////////////////////////////*/
protected final static int FAST_FORWARD_REWIND_AMOUNT_MILLIS = 10000; // 10 Seconds
protected final static int PLAY_PREV_ACTIVATION_LIMIT_MILLIS = 5000; // 5 seconds
protected final static int PROGRESS_LOOP_INTERVAL_MILLIS = 500;
protected final static int RECOVERY_SKIP_THRESHOLD_MILLIS = 3000; // 3 seconds
@@ -209,7 +208,7 @@ public abstract class BasePlayer implements
this.progressUpdateReactor = new SerialDisposable();
this.databaseUpdateReactor = new CompositeDisposable();
final String userAgent = Downloader.USER_AGENT;
final String userAgent = DownloaderImpl.USER_AGENT;
final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter.Builder(context).build();
this.dataSource = new PlayerDataSource(context, userAgent, bandwidthMeter);
@@ -954,12 +953,19 @@ public abstract class BasePlayer implements
public void onFastRewind() {
if (DEBUG) Log.d(TAG, "onFastRewind() called");
seekBy(-FAST_FORWARD_REWIND_AMOUNT_MILLIS);
seekBy(-getSeekDuration());
}
public void onFastForward() {
if (DEBUG) Log.d(TAG, "onFastForward() called");
seekBy(FAST_FORWARD_REWIND_AMOUNT_MILLIS);
seekBy(getSeekDuration());
}
private int getSeekDuration() {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
final String key = context.getString(R.string.seek_duration_key);
final String value = prefs.getString(key, context.getString(R.string.seek_duration_default_value));
return Integer.parseInt(value);
}
public void onPlayPrevious() {

View File

@@ -1036,7 +1036,7 @@ public final class PopupVideoPlayer extends Service {
public boolean onTouch(View v, MotionEvent event) {
popupGestureDetector.onTouchEvent(event);
if (playerImpl == null) return false;
if (event.getPointerCount() == 2 && !isResizing) {
if (event.getPointerCount() == 2 && !isMoving && !isResizing) {
if (DEBUG) Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing.");
playerImpl.showAndAnimateControl(-1, true);
playerImpl.getLoadingPanel().setVisibility(View.GONE);

View File

@@ -26,14 +26,13 @@ import android.animation.PropertyValuesHolder;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.os.Build;
import android.os.Handler;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
@@ -45,6 +44,10 @@ import android.widget.ProgressBar;
import android.widget.SeekBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
@@ -285,6 +288,17 @@ public abstract class VideoPlayer extends BasePlayer
if (captionPopupMenu == null) return;
captionPopupMenu.getMenu().removeGroup(captionPopupMenuGroupId);
String userPreferredLanguage = PreferenceManager.getDefaultSharedPreferences(context)
.getString(context.getString(R.string.caption_user_set_key), null);
/*
* only search for autogenerated cc as fallback
* if "(auto-generated)" was not already selected
* we are only looking for "(" instead of "(auto-generated)" to hopefully get all
* internationalized variants such as "(automatisch-erzeugt)" and so on
*/
boolean searchForAutogenerated = userPreferredLanguage != null &&
!userPreferredLanguage.contains("(");
// Add option for turning off caption
MenuItem captionOffItem = captionPopupMenu.getMenu().add(captionPopupMenuGroupId,
0, Menu.NONE, R.string.caption_none);
@@ -294,6 +308,8 @@ public abstract class VideoPlayer extends BasePlayer
trackSelector.setParameters(trackSelector.buildUponParameters()
.setRendererDisabled(textRendererIndex, true));
}
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
prefs.edit().remove(context.getString(R.string.caption_user_set_key)).commit();
return true;
});
@@ -308,9 +324,26 @@ public abstract class VideoPlayer extends BasePlayer
trackSelector.setPreferredTextLanguage(captionLanguage);
trackSelector.setParameters(trackSelector.buildUponParameters()
.setRendererDisabled(textRendererIndex, false));
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
prefs.edit().putString(context.getString(R.string.caption_user_set_key),
captionLanguage).commit();
}
return true;
});
// apply caption language from previous user preference
if (userPreferredLanguage != null && (captionLanguage.equals(userPreferredLanguage) ||
searchForAutogenerated && captionLanguage.startsWith(userPreferredLanguage) ||
userPreferredLanguage.contains("(") &&
captionLanguage.startsWith(userPreferredLanguage.substring(0,
userPreferredLanguage.indexOf('('))))) {
final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT);
if (textRendererIndex != RENDERER_UNAVAILABLE) {
trackSelector.setPreferredTextLanguage(captionLanguage);
trackSelector.setParameters(trackSelector.buildUponParameters()
.setRendererDisabled(textRendererIndex, false));
}
searchForAutogenerated = false;
}
}
captionPopupMenu.setOnDismissListener(this);
}

View File

@@ -2,12 +2,19 @@ package org.schabi.newpipe.player.helper;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.media.MediaMetadata;
import android.os.Build;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.view.KeyEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;
import androidx.media.session.MediaButtonReceiver;
import androidx.media.app.NotificationCompat.MediaStyle;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
@@ -19,8 +26,10 @@ import org.schabi.newpipe.player.mediasession.PlayQueuePlaybackController;
public class MediaSessionManager {
private static final String TAG = "MediaSessionManager";
@NonNull private final MediaSessionCompat mediaSession;
@NonNull private final MediaSessionConnector sessionConnector;
@NonNull
private final MediaSessionCompat mediaSession;
@NonNull
private final MediaSessionConnector sessionConnector;
public MediaSessionManager(@NonNull final Context context,
@NonNull final Player player,
@@ -40,13 +49,45 @@ public class MediaSessionManager {
return MediaButtonReceiver.handleIntent(mediaSession, intent);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public void setLockScreenArt(NotificationCompat.Builder builder, @Nullable Bitmap thumbnailBitmap) {
if (thumbnailBitmap == null || !mediaSession.isActive()) {
return;
}
mediaSession.setMetadata(
new MediaMetadataCompat.Builder()
.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, thumbnailBitmap)
.build()
);
MediaStyle mediaStyle = new MediaStyle()
.setMediaSession(mediaSession.getSessionToken());
builder.setStyle(mediaStyle);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public void clearLockScreenArt(NotificationCompat.Builder builder) {
mediaSession.setMetadata(
new MediaMetadataCompat.Builder()
.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, null)
.build()
);
MediaStyle mediaStyle = new MediaStyle()
.setMediaSession(mediaSession.getSessionToken());
builder.setStyle(mediaStyle);
}
/**
* Should be called on player destruction to prevent leakage.
* */
*/
public void dispose() {
this.sessionConnector.setPlayer(null);
this.sessionConnector.setQueueNavigator(null);
this.mediaSession.setActive(false);
this.mediaSession.release();
}
}
}

View File

@@ -319,6 +319,7 @@ public class MediaSourceManager {
private Observable<Long> getEdgeIntervalSignal() {
return Observable.interval(progressUpdateIntervalMillis, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.filter(ignored ->
playbackListener.isApproachingPlaybackEdge(playbackNearEndGapMillis));
}

View File

@@ -18,7 +18,8 @@ import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.utils.Localization;
import org.schabi.newpipe.extractor.localization.ContentCountry;
import org.schabi.newpipe.extractor.localization.Localization;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.FilePickerActivityHelper;
@@ -53,10 +54,16 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
private String thumbnailLoadToggleKey;
private Localization initialSelectedLocalization;
private ContentCountry initialSelectedContentCountry;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
thumbnailLoadToggleKey = getString(R.string.download_thumbnail_key);
initialSelectedLocalization = org.schabi.newpipe.util.Localization.getPreferredLocalization(requireContext());
initialSelectedContentCountry = org.schabi.newpipe.util.Localization.getPreferredContentCountry(requireContext());
}
@Override
@@ -108,20 +115,23 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
startActivityForResult(i, REQUEST_EXPORT_PATH);
return true;
});
}
Preference setPreferredLanguage = findPreference(getString(R.string.content_language_key));
setPreferredLanguage.setOnPreferenceChangeListener((Preference p, Object newLanguage) -> {
Localization oldLocal = org.schabi.newpipe.util.Localization.getPreferredExtractorLocal(getActivity());
NewPipe.setLocalization(new Localization(oldLocal.getCountry(), (String) newLanguage));
return true;
});
@Override
public void onDestroy() {
super.onDestroy();
Preference setPreferredCountry = findPreference(getString(R.string.content_country_key));
setPreferredCountry.setOnPreferenceChangeListener((Preference p, Object newCountry) -> {
Localization oldLocal = org.schabi.newpipe.util.Localization.getPreferredExtractorLocal(getActivity());
NewPipe.setLocalization(new Localization((String) newCountry, oldLocal.getLanguage()));
return true;
});
final Localization selectedLocalization = org.schabi.newpipe.util.Localization
.getPreferredLocalization(requireContext());
final ContentCountry selectedContentCountry = org.schabi.newpipe.util.Localization
.getPreferredContentCountry(requireContext());
if (!selectedLocalization.equals(initialSelectedLocalization)
|| !selectedContentCountry.equals(initialSelectedContentCountry)) {
Toast.makeText(requireContext(), R.string.localization_changes_requires_app_restart, Toast.LENGTH_LONG).show();
NewPipe.setupLocalization(selectedLocalization, selectedContentCountry);
}
}
@Override

View File

@@ -0,0 +1,427 @@
package org.schabi.newpipe.settings;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.InputType;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.RadioButton;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.grack.nanojson.JsonStringWriter;
import com.grack.nanojson.JsonWriter;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.PeertubeHelper;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
public class PeertubeInstanceListFragment extends Fragment {
private List<PeertubeInstance> instanceList = new ArrayList<>();
private PeertubeInstance selectedInstance;
private String savedInstanceListKey;
public InstanceListAdapter instanceListAdapter;
private ProgressBar progressBar;
private SharedPreferences sharedPreferences;
private CompositeDisposable disposables = new CompositeDisposable();
/*//////////////////////////////////////////////////////////////////////////
// Lifecycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext());
savedInstanceListKey = getString(R.string.peertube_instance_list_key);
selectedInstance = PeertubeHelper.getCurrentInstance();
updateInstanceList();
setHasOptionsMenu(true);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_instance_list, container, false);
}
@Override
public void onViewCreated(@NonNull View rootView, @Nullable Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState);
initViews(rootView);
}
private void initViews(@NonNull View rootView) {
TextView instanceHelpTV = rootView.findViewById(R.id.instanceHelpTV);
instanceHelpTV.setText(getString(R.string.peertube_instance_url_help, getString(R.string.peertube_instance_list_url)));
initButton(rootView);
RecyclerView listInstances = rootView.findViewById(R.id.instances);
listInstances.setLayoutManager(new LinearLayoutManager(requireContext()));
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
itemTouchHelper.attachToRecyclerView(listInstances);
instanceListAdapter = new InstanceListAdapter(requireContext(), itemTouchHelper);
listInstances.setAdapter(instanceListAdapter);
progressBar = rootView.findViewById(R.id.loading_progress_bar);
}
@Override
public void onResume() {
super.onResume();
updateTitle();
}
@Override
public void onPause() {
super.onPause();
saveChanges();
}
@Override
public void onDestroy() {
super.onDestroy();
if (disposables != null) disposables.clear();
disposables = null;
}
/*//////////////////////////////////////////////////////////////////////////
// Menu
//////////////////////////////////////////////////////////////////////////*/
private final int MENU_ITEM_RESTORE_ID = 123456;
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
final MenuItem restoreItem = menu.add(Menu.NONE, MENU_ITEM_RESTORE_ID, Menu.NONE, R.string.restore_defaults);
restoreItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
final int restoreIcon = ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_restore_defaults);
restoreItem.setIcon(AppCompatResources.getDrawable(requireContext(), restoreIcon));
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == MENU_ITEM_RESTORE_ID) {
restoreDefaults();
return true;
}
return super.onOptionsItemSelected(item);
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
private void updateInstanceList() {
instanceList.clear();
instanceList.addAll(PeertubeHelper.getInstanceList(requireContext()));
}
private void selectInstance(PeertubeInstance instance) {
selectedInstance = PeertubeHelper.selectInstance(instance, requireContext());
sharedPreferences.edit().putBoolean(Constants.KEY_MAIN_PAGE_CHANGE, true).apply();
}
private void updateTitle() {
if (getActivity() instanceof AppCompatActivity) {
ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
if (actionBar != null) actionBar.setTitle(R.string.peertube_instance_url_title);
}
}
private void saveChanges() {
JsonStringWriter jsonWriter = JsonWriter.string().object().array("instances");
for (PeertubeInstance instance : instanceList) {
jsonWriter.object();
jsonWriter.value("name", instance.getName());
jsonWriter.value("url", instance.getUrl());
jsonWriter.end();
}
String jsonToSave = jsonWriter.end().end().done();
sharedPreferences.edit().putString(savedInstanceListKey, jsonToSave).apply();
}
private void restoreDefaults() {
new AlertDialog.Builder(requireContext(), ThemeHelper.getDialogTheme(requireContext()))
.setTitle(R.string.restore_defaults)
.setMessage(R.string.restore_defaults_confirmation)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.yes, (dialog, which) -> {
sharedPreferences.edit().remove(savedInstanceListKey).apply();
selectInstance(PeertubeInstance.defaultInstance);
updateInstanceList();
instanceListAdapter.notifyDataSetChanged();
})
.show();
}
private void initButton(View rootView) {
final FloatingActionButton fab = rootView.findViewById(R.id.addInstanceButton);
fab.setOnClickListener(v -> {
showAddItemDialog(requireContext());
});
}
private void showAddItemDialog(Context c) {
final EditText urlET = new EditText(c);
urlET.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
urlET.setHint(R.string.peertube_instance_add_help);
AlertDialog dialog = new AlertDialog.Builder(c)
.setTitle(R.string.peertube_instance_add_title)
.setIcon(R.drawable.place_holder_peertube)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.finish, (dialog1, which) -> {
String url = urlET.getText().toString();
addInstance(url);
})
.create();
dialog.setView(urlET, 50, 0, 50, 0);
dialog.show();
}
private void addInstance(String url) {
String cleanUrl = cleanUrl(url);
if(null == cleanUrl) return;
progressBar.setVisibility(View.VISIBLE);
Disposable disposable = Single.fromCallable(() -> {
PeertubeInstance instance = new PeertubeInstance(cleanUrl);
instance.fetchInstanceMetaData();
return instance;
}).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe((instance) -> {
progressBar.setVisibility(View.GONE);
add(instance);
}, e -> {
progressBar.setVisibility(View.GONE);
Toast.makeText(getActivity(), R.string.peertube_instance_add_fail, Toast.LENGTH_SHORT).show();
});
disposables.add(disposable);
}
@Nullable
private String cleanUrl(String url){
url = url.trim();
// if protocol not present, add https
if(!url.startsWith("http")){
url = "https://" + url;
}
// remove trailing slash
url = url.replaceAll("/$", "");
// only allow https
if (!url.startsWith("https://")) {
Toast.makeText(getActivity(), R.string.peertube_instance_add_https_only, Toast.LENGTH_SHORT).show();
return null;
}
// only allow if not already exists
for (PeertubeInstance instance : instanceList) {
if (instance.getUrl().equals(url)) {
Toast.makeText(getActivity(), R.string.peertube_instance_add_exists, Toast.LENGTH_SHORT).show();
return null;
}
}
return url;
}
private void add(final PeertubeInstance instance) {
instanceList.add(instance);
instanceListAdapter.notifyDataSetChanged();
}
/*//////////////////////////////////////////////////////////////////////////
// List Handling
//////////////////////////////////////////////////////////////////////////*/
private class InstanceListAdapter extends RecyclerView.Adapter<InstanceListAdapter.TabViewHolder> {
private ItemTouchHelper itemTouchHelper;
private final LayoutInflater inflater;
private RadioButton lastChecked;
InstanceListAdapter(Context context, ItemTouchHelper itemTouchHelper) {
this.itemTouchHelper = itemTouchHelper;
this.inflater = LayoutInflater.from(context);
}
public void swapItems(int fromPosition, int toPosition) {
Collections.swap(instanceList, fromPosition, toPosition);
notifyItemMoved(fromPosition, toPosition);
}
@NonNull
@Override
public InstanceListAdapter.TabViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = inflater.inflate(R.layout.item_instance, parent, false);
return new InstanceListAdapter.TabViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull InstanceListAdapter.TabViewHolder holder, int position) {
holder.bind(position, holder);
}
@Override
public int getItemCount() {
return instanceList.size();
}
class TabViewHolder extends RecyclerView.ViewHolder {
private AppCompatImageView instanceIconView;
private TextView instanceNameView;
private TextView instanceUrlView;
private RadioButton instanceRB;
private ImageView handle;
TabViewHolder(View itemView) {
super(itemView);
instanceIconView = itemView.findViewById(R.id.instanceIcon);
instanceNameView = itemView.findViewById(R.id.instanceName);
instanceUrlView = itemView.findViewById(R.id.instanceUrl);
instanceRB = itemView.findViewById(R.id.selectInstanceRB);
handle = itemView.findViewById(R.id.handle);
}
@SuppressLint("ClickableViewAccessibility")
void bind(int position, TabViewHolder holder) {
handle.setOnTouchListener(getOnTouchListener(holder));
final PeertubeInstance instance = instanceList.get(position);
instanceNameView.setText(instance.getName());
instanceUrlView.setText(instance.getUrl());
instanceRB.setOnCheckedChangeListener(null);
if (selectedInstance.getUrl().equals(instance.getUrl())) {
if (lastChecked != null && lastChecked != instanceRB) {
lastChecked.setChecked(false);
}
instanceRB.setChecked(true);
lastChecked = instanceRB;
}
instanceRB.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (isChecked) {
selectInstance(instance);
if (lastChecked != null && lastChecked != instanceRB) {
lastChecked.setChecked(false);
}
lastChecked = instanceRB;
}
});
instanceIconView.setImageResource(R.drawable.place_holder_peertube);
}
@SuppressLint("ClickableViewAccessibility")
private View.OnTouchListener getOnTouchListener(final RecyclerView.ViewHolder item) {
return (view, motionEvent) -> {
if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
if (itemTouchHelper != null && getItemCount() > 1) {
itemTouchHelper.startDrag(item);
return true;
}
}
return false;
};
}
}
}
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
ItemTouchHelper.START | ItemTouchHelper.END) {
@Override
public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize,
int viewSizeOutOfBounds, int totalSize,
long msSinceStartScroll) {
final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize,
viewSizeOutOfBounds, totalSize, msSinceStartScroll);
final int minimumAbsVelocity = Math.max(12,
Math.abs(standardSpeed));
return minimumAbsVelocity * (int) Math.signum(viewSizeOutOfBounds);
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source,
RecyclerView.ViewHolder target) {
if (source.getItemViewType() != target.getItemViewType() ||
instanceListAdapter == null) {
return false;
}
final int sourceIndex = source.getAdapterPosition();
final int targetIndex = target.getAdapterPosition();
instanceListAdapter.swapItems(sourceIndex, targetIndex);
return true;
}
@Override
public boolean isLongPressDragEnabled() {
return false;
}
@Override
public boolean isItemViewSwipeEnabled() {
return true;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {
int position = viewHolder.getAdapterPosition();
// do not allow swiping the selected instance
if(instanceList.get(position).getUrl().equals(selectedInstance.getUrl())) {
instanceListAdapter.notifyItemChanged(position);
return;
}
instanceList.remove(position);
instanceListAdapter.notifyItemRemoved(position);
if (instanceList.isEmpty()) {
instanceList.add(selectedInstance);
instanceListAdapter.notifyItemInserted(0);
}
}
};
}
}

View File

@@ -231,7 +231,7 @@ public class ChooseTabsFragment extends Fragment {
break;
case DEFAULT_KIOSK:
if (!tabList.contains(tab)) {
returnList.add(new ChooseTabListItem(tab.getTabId(), "Default Kiosk",
returnList.add(new ChooseTabListItem(tab.getTabId(), getString(R.string.default_kiosk_page_summary),
ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_hot)));
}
break;
@@ -305,23 +305,25 @@ public class ChooseTabsFragment extends Fragment {
return;
}
String tabName = tab.getTabName(requireContext());
final String tabName;
switch (type) {
case BLANK:
tabName = requireContext().getString(R.string.blank_page_summary);
break;
case KIOSK:
tabName = NewPipe.getNameOfService(((Tab.KioskTab) tab).getKioskServiceId()) + "/" + tabName;
break;
case CHANNEL:
tabName = NewPipe.getNameOfService(((Tab.ChannelTab) tab).getChannelServiceId()) + "/" + tabName;
tabName = getString(R.string.blank_page_summary);
break;
case DEFAULT_KIOSK:
tabName = "Default Kiosk";
tabName = getString(R.string.default_kiosk_page_summary);
break;
case KIOSK:
tabName = NewPipe.getNameOfService(((Tab.KioskTab) tab).getKioskServiceId()) + "/" + tab.getTabName(requireContext());
break;
case CHANNEL:
tabName = NewPipe.getNameOfService(((Tab.ChannelTab) tab).getChannelServiceId()) + "/" + tab.getTabName(requireContext());
break;
default:
tabName = tab.getTabName(requireContext());
break;
}
tabNameView.setText(tabName);
tabIconView.setImageResource(tab.getTabIconRes(requireContext()));
}

View File

@@ -1,6 +1,7 @@
package org.schabi.newpipe.settings.tabs;
import android.content.Context;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -9,22 +10,26 @@ import androidx.fragment.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.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.fragments.BlankFragment;
import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
import org.schabi.newpipe.fragments.list.kiosk.DefaultKioskFragment;
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
import org.schabi.newpipe.local.bookmark.BookmarkFragment;
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.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.KioskTranslator;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.Objects;
public abstract class Tab {
Tab() {
}
@@ -40,10 +45,12 @@ public abstract class Tab {
/**
* Return a instance of the fragment that this tab represent.
*/
public abstract Fragment getFragment() throws ExtractionException;
public abstract Fragment getFragment(Context context) throws ExtractionException;
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
return obj instanceof Tab && obj.getClass().equals(this.getClass())
&& ((Tab) obj).getTabId() == this.getTabId();
}
@@ -115,12 +122,6 @@ 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;
}
}
@@ -133,13 +134,13 @@ public abstract class Tab {
public enum Type {
BLANK(new BlankTab()),
DEFAULT_KIOSK(new DefaultKioskTab()),
SUBSCRIPTIONS(new SubscriptionsTab()),
FEED(new FeedTab()),
BOOKMARKS(new BookmarksTab()),
HISTORY(new HistoryTab()),
KIOSK(new KioskTab()),
CHANNEL(new ChannelTab()),
DEFAULT_KIOSK(new DefaultKioskTab());
CHANNEL(new ChannelTab());
private Tab tab;
@@ -176,7 +177,7 @@ public abstract class Tab {
}
@Override
public BlankFragment getFragment() {
public BlankFragment getFragment(Context context) {
return new BlankFragment();
}
}
@@ -201,7 +202,7 @@ public abstract class Tab {
}
@Override
public SubscriptionFragment getFragment() {
public SubscriptionFragment getFragment(Context context) {
return new SubscriptionFragment();
}
@@ -227,7 +228,7 @@ public abstract class Tab {
}
@Override
public FeedFragment getFragment() {
public FeedFragment getFragment(Context context) {
return new FeedFragment();
}
}
@@ -252,7 +253,7 @@ public abstract class Tab {
}
@Override
public BookmarkFragment getFragment() {
public BookmarkFragment getFragment(Context context) {
return new BookmarkFragment();
}
}
@@ -277,7 +278,7 @@ public abstract class Tab {
}
@Override
public StatisticsPlaylistFragment getFragment() {
public StatisticsPlaylistFragment getFragment(Context context) {
return new StatisticsPlaylistFragment();
}
}
@@ -327,7 +328,7 @@ public abstract class Tab {
}
@Override
public KioskFragment getFragment() throws ExtractionException {
public KioskFragment getFragment(Context context) throws ExtractionException {
return KioskFragment.getInstance(kioskServiceId, kioskId);
}
@@ -343,6 +344,13 @@ public abstract class Tab {
kioskId = jsonObject.getString(JSON_KIOSK_ID_KEY, "<no-id>");
}
@Override
public boolean equals(Object obj) {
return super.equals(obj) &&
kioskServiceId == ((KioskTab) obj).kioskServiceId
&& Objects.equals(kioskId, ((KioskTab) obj).kioskId);
}
public int getKioskServiceId() {
return kioskServiceId;
}
@@ -394,7 +402,7 @@ public abstract class Tab {
}
@Override
public ChannelFragment getFragment() {
public ChannelFragment getFragment(Context context) {
return ChannelFragment.getInstance(channelServiceId, channelUrl, channelName);
}
@@ -412,6 +420,14 @@ public abstract class Tab {
channelName = jsonObject.getString(JSON_CHANNEL_NAME_KEY, "<no-name>");
}
@Override
public boolean equals(Object obj) {
return super.equals(obj) &&
channelServiceId == ((ChannelTab) obj).channelServiceId
&& Objects.equals(channelUrl, ((ChannelTab) obj).channelUrl)
&& Objects.equals(channelName, ((ChannelTab) obj).channelName);
}
public int getChannelServiceId() {
return channelServiceId;
}
@@ -428,22 +444,6 @@ public abstract class Tab {
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;
@@ -451,27 +451,31 @@ public abstract class Tab {
@Override
public String getTabName(Context context) {
return KioskTranslator.getTranslatedKioskName(kioskId, context);
return KioskTranslator.getTranslatedKioskName(getDefaultKioskId(context), 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;
return KioskTranslator.getKioskIcons(getDefaultKioskId(context), context);
}
@Override
public KioskFragment getFragment() throws ExtractionException {
return KioskFragment.getInstance(kioskServiceId, kioskId);
public DefaultKioskFragment getFragment(Context context) throws ExtractionException {
return new DefaultKioskFragment();
}
public String getKioskId() {
private String getDefaultKioskId(Context context) {
final int kioskServiceId = ServiceHelper.getSelectedServiceId(context);
String kioskId = "";
try {
final StreamingService service = NewPipe.getService(kioskServiceId);
kioskId = service.getKioskList().getDefaultKioskId();
} catch (ExtractionException e) {
ErrorActivity.reportError(context, e, null, null,
ErrorActivity.ErrorInfo.make(UserAction.REQUESTED_KIOSK, "none", "Loading default kiosk from selected service", 0));
}
return kioskId;
}
}

View File

@@ -1,7 +1,5 @@
package org.schabi.newpipe.settings.tabs;
import androidx.annotation.Nullable;
import com.grack.nanojson.JsonArray;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser;
@@ -9,18 +7,25 @@ import com.grack.nanojson.JsonParserException;
import com.grack.nanojson.JsonStringWriter;
import com.grack.nanojson.JsonWriter;
import org.jsoup.helper.StringUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import androidx.annotation.Nullable;
/**
* 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";
private static final List<Tab> FALLBACK_INITIAL_TABS_LIST = Collections.unmodifiableList(Arrays.asList(
Tab.Type.DEFAULT_KIOSK.getTab(),
Tab.Type.SUBSCRIPTIONS.getTab(),
Tab.Type.BOOKMARKS.getTab()
));
public static class InvalidJsonException extends Exception {
private InvalidJsonException() {
super();
@@ -83,16 +88,6 @@ public class TabsJsonHelper {
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.
*
@@ -112,4 +107,8 @@ public class TabsJsonHelper {
jsonWriter.end();
return jsonWriter.done();
}
public static List<Tab> getDefaultTabs(){
return FALLBACK_INITIAL_TABS_LIST;
}
}

View File

@@ -15,7 +15,6 @@ import java.util.NoSuchElementException;
*/
public class Mp4DashReader {
// <editor-fold defaultState="collapsed" desc="Constants">
private static final int ATOM_MOOF = 0x6D6F6F66;
private static final int ATOM_MFHD = 0x6D666864;
private static final int ATOM_TRAF = 0x74726166;
@@ -50,7 +49,7 @@ public class Mp4DashReader {
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;
@@ -293,7 +292,8 @@ public class Mp4DashReader {
return null;
}
// <editor-fold defaultState="collapsed" desc="Utils">
private long readUint() throws IOException {
return stream.readInt() & 0xffffffffL;
}
@@ -392,9 +392,7 @@ public class Mp4DashReader {
return readBox();
}
// </editor-fold>
// <editor-fold defaultState="collapsed" desc="Box readers">
private Moof parse_moof(Box ref, int trackId) throws IOException {
Moof obj = new Moof();
@@ -795,9 +793,8 @@ public class Mp4DashReader {
return readFullBox(b);
}
// </editor-fold>
// <editor-fold defaultState="collapsed" desc="Helper classes">
class Box {
int type;
@@ -1013,5 +1010,5 @@ public class Mp4DashReader {
public TrunEntry info;
public byte[] data;
}
//</editor-fold>
}

View File

@@ -6,6 +6,7 @@ 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.Mp4DashReader.TrackKind;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
@@ -22,6 +23,7 @@ public class Mp4FromDashWriter {
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 static short SINGLE_CHUNK_SAMPLE_BUFFER = 256;
private final long time;
@@ -145,7 +147,7 @@ public class Mp4FromDashWriter {
// not allowed for very short tracks (less than 0.5 seconds)
//
outStream = output;
int read = 8;// mdat box header size
long read = 8;// mdat box header size
long totalSampleSize = 0;
int[] sampleExtra = new int[readers.length];
int[] defaultMediaTime = new int[readers.length];
@@ -157,7 +159,9 @@ public class Mp4FromDashWriter {
tablesInfo[i] = new TablesInfo();
}
//<editor-fold defaultstate="expanded" desc="calculate stbl sample tables size and required moov values">
boolean singleChunk = tracks.length == 1 && tracks[0].kind == TrackKind.Audio;
for (int i = 0; i < readers.length; i++) {
int samplesSize = 0;
int sampleSizeChanges = 0;
@@ -210,14 +214,21 @@ public class Mp4FromDashWriter {
tablesInfo[i].stco = (tmp / SAMPLES_PER_CHUNK) + 1;// +1 for samples in first chunk
tmp = tmp % SAMPLES_PER_CHUNK;
if (tmp == 0) {
if (singleChunk) {
// avoid split audio streams in chunks
tablesInfo[i].stsc = 1;
tablesInfo[i].stsc_bEntries = new int[]{
1, tablesInfo[i].stsz, 1
};
tablesInfo[i].stco = 1;
} else 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 = 3;// first chunk (init) and successive chunks and remain chunk
tablesInfo[i].stsc_bEntries = new int[]{
1, SAMPLES_PER_CHUNK_INIT, 1,
2, SAMPLES_PER_CHUNK, 1,
@@ -244,7 +255,7 @@ public class Mp4FromDashWriter {
tracks[i].trak.tkhd.duration = sampleExtra[i];// this never should happen
}
}
//</editor-fold>
boolean is64 = read > THRESHOLD_FOR_CO64;
@@ -268,10 +279,10 @@ public class Mp4FromDashWriter {
} else {*/
if (auxSize > 0) {
int length = auxSize;
byte[] buffer = new byte[8 * 1024];// 8 KiB
byte[] buffer = new byte[64 * 1024];// 64 KiB
while (length > 0) {
int count = Math.min(length, buffer.length);
outWrite(buffer, 0, count);
outWrite(buffer, count);
length -= count;
}
}
@@ -280,7 +291,7 @@ public class Mp4FromDashWriter {
outSeek(ftyp_size);
}
// tablesInfo contais row counts
// tablesInfo contains row counts
// and after returning from make_moov() will contain table offsets
make_moov(defaultMediaTime, tablesInfo, is64);
@@ -291,7 +302,7 @@ public class Mp4FromDashWriter {
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
sampleCount[i] = 1;// the index is not base zero
sampleExtra[i] = -1;
}
}
@@ -303,8 +314,8 @@ public class Mp4FromDashWriter {
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[] sizes = new int[singleChunk ? SINGLE_CHUNK_SAMPLE_BUFFER : SAMPLES_PER_CHUNK];
int[] sync = new int[singleChunk ? SINGLE_CHUNK_SAMPLE_BUFFER : SAMPLES_PER_CHUNK];
int written = readers.length;
while (written > 0) {
@@ -317,7 +328,12 @@ public class Mp4FromDashWriter {
long chunkOffset = writeOffset;
int syncCount = 0;
int limit = sampleIndex[i] == 0 ? SAMPLES_PER_CHUNK_INIT : SAMPLES_PER_CHUNK;
int limit;
if (singleChunk) {
limit = SINGLE_CHUNK_SAMPLE_BUFFER;
} else {
limit = sampleIndex[i] == 0 ? SAMPLES_PER_CHUNK_INIT : SAMPLES_PER_CHUNK;
}
int j = 0;
for (; j < limit; j++) {
@@ -354,7 +370,7 @@ public class Mp4FromDashWriter {
sizes[j] = sample.data.length;
}
outWrite(sample.data, 0, sample.data.length);
outWrite(sample.data, sample.data.length);
}
if (j > 0) {
@@ -368,10 +384,16 @@ public class Mp4FromDashWriter {
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);
if (tablesInfo[i].stco > 0) {
if (is64) {
tablesInfo[i].stco = writeEntry64(tablesInfo[i].stco, chunkOffset);
} else {
tablesInfo[i].stco = writeEntryArray(tablesInfo[i].stco, 1, (int) chunkOffset);
}
if (singleChunk) {
tablesInfo[i].stco = -1;
}
}
outRestore();
@@ -404,7 +426,7 @@ public class Mp4FromDashWriter {
}
}
// <editor-fold defaultstate="expanded" desc="Stbl handling">
private int writeEntry64(int offset, long value) throws IOException {
outBackup();
@@ -447,16 +469,16 @@ public class Mp4FromDashWriter {
lastWriteOffset = -1;
}
}
// </editor-fold>
// <editor-fold defaultstate="expanded" desc="Utils">
private void outWrite(byte[] buffer) throws IOException {
outWrite(buffer, 0, buffer.length);
outWrite(buffer, buffer.length);
}
private void outWrite(byte[] buffer, int offset, int count) throws IOException {
private void outWrite(byte[] buffer, int count) throws IOException {
writeOffset += count;
outStream.write(buffer, offset, count);
outStream.write(buffer, 0, count);
}
private void outSeek(long offset) throws IOException {
@@ -509,7 +531,6 @@ public class Mp4FromDashWriter {
);
if (extra >= 0) {
//size += 4;// commented for auxiliar buffer !!!
offset += 4;
auxWrite(extra);
}
@@ -531,7 +552,7 @@ public class Mp4FromDashWriter {
if (moovSimulation) {
writeOffset += buffer.length;
} else if (auxBuffer == null) {
outWrite(buffer, 0, buffer.length);
outWrite(buffer, buffer.length);
} else {
auxBuffer.put(buffer);
}
@@ -560,9 +581,9 @@ public class Mp4FromDashWriter {
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
@@ -703,7 +724,7 @@ public class Mp4FromDashWriter {
int mediaTime;
if (tracks[index].trak.edst_elst == null) {
// is a audio track ¿is edst/elst opcional for audio tracks?
// is a audio track ¿is edst/elst optional for audio tracks?
mediaTime = 0x00;// ffmpeg set this value as zero, instead of defaultMediaTime
bMediaRate = 0x00010000;
} else {
@@ -794,17 +815,17 @@ public class Mp4FromDashWriter {
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;
int stts;
int stsc;
int[] stsc_bEntries;
int ctts;
int stsz;
int stsz_default;
int stss;
int stco;
}
}

View File

@@ -0,0 +1,431 @@
package org.schabi.newpipe.streams;
import androidx.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.Closeable;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import androidx.annotation.Nullable;
/**
* @author kapodamy
*/
public class OggFromWebMWriter implements Closeable {
private static final byte FLAG_UNSET = 0x00;
//private static final byte FLAG_CONTINUED = 0x01;
private static final byte FLAG_FIRST = 0x02;
private static final byte FLAG_LAST = 0x04;
private final static byte HEADER_CHECKSUM_OFFSET = 22;
private final static byte HEADER_SIZE = 27;
private final static int TIME_SCALE_NS = 1000000000;
private boolean done = false;
private boolean parsed = false;
private SharpStream source;
private SharpStream output;
private int sequence_count = 0;
private final int STREAM_ID;
private byte packet_flag = FLAG_FIRST;
private WebMReader webm = null;
private WebMTrack webm_track = null;
private Segment webm_segment = null;
private Cluster webm_cluster = null;
private SimpleBlock webm_block = null;
private long webm_block_last_timecode = 0;
private long webm_block_near_duration = 0;
private short segment_table_size = 0;
private final byte[] segment_table = new byte[255];
private long segment_table_next_timestamp = TIME_SCALE_NS;
private final int[] crc32_table = new int[256];
public OggFromWebMWriter(@NonNull SharpStream source, @NonNull SharpStream target) {
if (!source.canRead() || !source.canRewind()) {
throw new IllegalArgumentException("source stream must be readable and allows seeking");
}
if (!target.canWrite() || !target.canRewind()) {
throw new IllegalArgumentException("output stream must be writable and allows seeking");
}
this.source = source;
this.output = target;
this.STREAM_ID = (int) System.currentTimeMillis();
populate_crc32_table();
}
public boolean isDone() {
return done;
}
public boolean isParsed() {
return parsed;
}
public WebMTrack[] getTracksFromSource() throws IllegalStateException {
if (!parsed) {
throw new IllegalStateException("source must be parsed first");
}
return webm.getAvailableTracks();
}
public void parseSource() throws IOException, IllegalStateException {
if (done) {
throw new IllegalStateException("already done");
}
if (parsed) {
throw new IllegalStateException("already parsed");
}
try {
webm = new WebMReader(source);
webm.parse();
webm_segment = webm.getNextSegment();
} finally {
parsed = true;
}
}
public void selectTrack(int trackIndex) throws IOException {
if (!parsed) {
throw new IllegalStateException("source must be parsed first");
}
if (done) {
throw new IOException("already done");
}
if (webm_track != null) {
throw new IOException("tracks already selected");
}
switch (webm.getAvailableTracks()[trackIndex].kind) {
case Audio:
case Video:
break;
default:
throw new UnsupportedOperationException("the track must an audio or video stream");
}
try {
webm_track = webm.selectTrack(trackIndex);
} finally {
parsed = true;
}
}
@Override
public void close() throws IOException {
done = true;
parsed = true;
webm_track = null;
webm = null;
if (!output.isClosed()) {
output.flush();
}
source.close();
output.close();
}
public void build() throws IOException {
float resolution;
SimpleBlock bloq;
ByteBuffer header = ByteBuffer.allocate(27 + (255 * 255));
ByteBuffer page = ByteBuffer.allocate(64 * 1024);
header.order(ByteOrder.LITTLE_ENDIAN);
/* step 1: get the amount of frames per seconds */
switch (webm_track.kind) {
case Audio:
resolution = getSampleFrequencyFromTrack(webm_track.bMetadata);
if (resolution == 0f) {
throw new RuntimeException("cannot get the audio sample rate");
}
break;
case Video:
// WARNING: untested
if (webm_track.defaultDuration == 0) {
throw new RuntimeException("missing default frame time");
}
resolution = 1000f / ((float) webm_track.defaultDuration / webm_segment.info.timecodeScale);
break;
default:
throw new RuntimeException("not implemented");
}
/* step 2: create packet with code init data */
if (webm_track.codecPrivate != null) {
addPacketSegment(webm_track.codecPrivate.length);
make_packetHeader(0x00, header, webm_track.codecPrivate);
write(header);
output.write(webm_track.codecPrivate);
}
/* step 3: create packet with metadata */
byte[] buffer = make_metadata();
if (buffer != null) {
addPacketSegment(buffer.length);
make_packetHeader(0x00, header, buffer);
write(header);
output.write(buffer);
}
/* step 4: calculate amount of packets */
while (webm_segment != null) {
bloq = getNextBlock();
if (bloq != null && addPacketSegment(bloq)) {
int pos = page.position();
//noinspection ResultOfMethodCallIgnored
bloq.data.read(page.array(), pos, bloq.dataSize);
page.position(pos + bloq.dataSize);
continue;
}
// calculate the current packet duration using the next block
double elapsed_ns = webm_track.codecDelay;
if (bloq == null) {
packet_flag = FLAG_LAST;// note: if the flag is FLAG_CONTINUED, is changed
elapsed_ns += webm_block_last_timecode;
if (webm_track.defaultDuration > 0) {
elapsed_ns += webm_track.defaultDuration;
} else {
// hardcoded way, guess the sample duration
elapsed_ns += webm_block_near_duration;
}
} else {
elapsed_ns += bloq.absoluteTimeCodeNs;
}
// get the sample count in the page
elapsed_ns = elapsed_ns / TIME_SCALE_NS;
elapsed_ns = Math.ceil(elapsed_ns * resolution);
// create header and calculate page checksum
int checksum = make_packetHeader((long) elapsed_ns, header, null);
checksum = calc_crc32(checksum, page.array(), page.position());
header.putInt(HEADER_CHECKSUM_OFFSET, checksum);
// dump data
write(header);
write(page);
webm_block = bloq;
}
}
private int make_packetHeader(long gran_pos, @NonNull ByteBuffer buffer, byte[] immediate_page) {
short length = HEADER_SIZE;
buffer.putInt(0x5367674f);// "OggS" binary string in little-endian
buffer.put((byte) 0x00);// version
buffer.put(packet_flag);// type
buffer.putLong(gran_pos);// granulate position
buffer.putInt(STREAM_ID);// bitstream serial number
buffer.putInt(sequence_count++);// page sequence number
buffer.putInt(0x00);// page checksum
buffer.put((byte) segment_table_size);// segment table
buffer.put(segment_table, 0, segment_table_size);// segment size
length += segment_table_size;
clearSegmentTable();// clear segment table for next header
int checksum_crc32 = calc_crc32(0x00, buffer.array(), length);
if (immediate_page != null) {
checksum_crc32 = calc_crc32(checksum_crc32, immediate_page, immediate_page.length);
buffer.putInt(HEADER_CHECKSUM_OFFSET, checksum_crc32);
segment_table_next_timestamp -= TIME_SCALE_NS;
}
return checksum_crc32;
}
@Nullable
private byte[] make_metadata() {
if ("A_OPUS".equals(webm_track.codecId)) {
return new byte[]{
0x4F, 0x70, 0x75, 0x73, 0x54, 0x61, 0x67, 0x73,// "OpusTags" binary string
0x07, 0x00, 0x00, 0x00,// writting application string size
0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65,// "NewPipe" binary string
0x00, 0x00, 0x00, 0x00// additional tags count (zero means no tags)
};
} else if ("A_VORBIS".equals(webm_track.codecId)) {
return new byte[]{
0x03,// ????????
0x76, 0x6f, 0x72, 0x62, 0x69, 0x73,// "vorbis" binary string
0x07, 0x00, 0x00, 0x00,// writting application string size
0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65,// "NewPipe" binary string
0x01, 0x00, 0x00, 0x00,// additional tags count (zero means no tags)
/*
// whole file duration (not implemented)
0x44,// tag string size
0x55, 0x52, 0x41, 0x54, 0x49, 0x4F, 0x4E, 0x3D, 0x30, 0x30, 0x3A, 0x30, 0x30, 0x3A, 0x30,
0x30, 0x2E, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30
*/
0x0F,// tag string size
0x00, 0x00, 0x00, 0x45, 0x4E, 0x43, 0x4F, 0x44, 0x45, 0x52, 0x3D,// "ENCODER=" binary string
0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65,// "NewPipe" binary string
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00// ????????
};
}
// not implemented for the desired codec
return null;
}
private void write(ByteBuffer buffer) throws IOException {
output.write(buffer.array(), 0, buffer.position());
buffer.position(0);
}
@Nullable
private SimpleBlock getNextBlock() throws IOException {
SimpleBlock res;
if (webm_block != null) {
res = webm_block;
webm_block = null;
return res;
}
if (webm_segment == null) {
webm_segment = webm.getNextSegment();
if (webm_segment == null) {
return null;// no more blocks in the selected track
}
}
if (webm_cluster == null) {
webm_cluster = webm_segment.getNextCluster();
if (webm_cluster == null) {
webm_segment = null;
return getNextBlock();
}
}
res = webm_cluster.getNextSimpleBlock();
if (res == null) {
webm_cluster = null;
return getNextBlock();
}
webm_block_near_duration = res.absoluteTimeCodeNs - webm_block_last_timecode;
webm_block_last_timecode = res.absoluteTimeCodeNs;
return res;
}
private float getSampleFrequencyFromTrack(byte[] bMetadata) {
// hardcoded way
ByteBuffer buffer = ByteBuffer.wrap(bMetadata);
while (buffer.remaining() >= 6) {
int id = buffer.getShort() & 0xFFFF;
if (id == 0x0000B584) {
return buffer.getFloat();
}
}
return 0f;
}
private void clearSegmentTable() {
segment_table_next_timestamp += TIME_SCALE_NS;
packet_flag = FLAG_UNSET;
segment_table_size = 0;
}
private boolean addPacketSegment(SimpleBlock block) {
long timestamp = block.absoluteTimeCodeNs + webm_track.codecDelay;
if (timestamp >= segment_table_next_timestamp) {
return false;
}
return addPacketSegment(block.dataSize);
}
private boolean addPacketSegment(int size) {
if (size > 65025) {
throw new UnsupportedOperationException("page size cannot be larger than 65025");
}
int available = (segment_table.length - segment_table_size) * 255;
boolean extra = (size % 255) == 0;
if (extra) {
// add a zero byte entry in the table
// required to indicate the sample size is multiple of 255
available -= 255;
}
// check if possible add the segment, without overflow the table
if (available < size) {
return false;// not enough space on the page
}
for (; size > 0; size -= 255) {
segment_table[segment_table_size++] = (byte) Math.min(size, 255);
}
if (extra) {
segment_table[segment_table_size++] = 0x00;
}
return true;
}
private void populate_crc32_table() {
for (int i = 0; i < 0x100; i++) {
int crc = i << 24;
for (int j = 0; j < 8; j++) {
long b = crc >>> 31;
crc <<= 1;
crc ^= (int) (0x100000000L - b) & 0x04c11db7;
}
crc32_table[i] = crc;
}
}
private int calc_crc32(int initial_crc, byte[] buffer, int size) {
for (int i = 0; i < size; i++) {
int reg = (initial_crc >>> 24) & 0xff;
initial_crc = (initial_crc << 8) ^ crc32_table[reg ^ (buffer[i] & 0xff)];
}
return initial_crc;
}
}

View File

@@ -15,7 +15,6 @@ import java.util.NoSuchElementException;
*/
public class WebMReader {
//<editor-fold defaultState="collapsed" desc="constants">
private final static int ID_EMBL = 0x0A45DFA3;
private final static int ID_EMBLReadVersion = 0x02F7;
private final static int ID_EMBLDocType = 0x0282;
@@ -37,11 +36,14 @@ public class WebMReader {
private final static int ID_Audio = 0x61;
private final static int ID_DefaultDuration = 0x3E383;
private final static int ID_FlagLacing = 0x1C;
private final static int ID_CodecDelay = 0x16AA;
private final static int ID_Cluster = 0x0F43B675;
private final static int ID_Timecode = 0x67;
private final static int ID_SimpleBlock = 0x23;
//</editor-fold>
private final static int ID_Block = 0x21;
private final static int ID_GroupBlock = 0x20;
public enum TrackKind {
Audio/*2*/, Video/*1*/, Other
@@ -96,7 +98,7 @@ public class WebMReader {
}
ensure(segment.ref);
// WARNING: track cannot be the same or have different index in new segments
Element elem = untilElement(null, ID_Segment);
if (elem == null) {
done = true;
@@ -107,7 +109,8 @@ public class WebMReader {
return segment;
}
//<editor-fold defaultstate="collapsed" desc="utils">
private long readNumber(Element parent) throws IOException {
int length = (int) parent.contentSize;
long value = 0;
@@ -189,6 +192,9 @@ public class WebMReader {
Element elem;
while (ref == null ? stream.available() : (stream.position() < (ref.offset + ref.size))) {
elem = readElement();
if (expected.length < 1) {
return elem;
}
for (int type : expected) {
if (elem.type == type) {
return elem;
@@ -219,9 +225,9 @@ public class WebMReader {
stream.skipBytes(skip);
}
//</editor-fold>
//<editor-fold defaultState="collapsed" desc="elements readers">
private boolean readEbml(Element ref, int minReadVersion, int minDocTypeVersion) throws IOException {
Element elem = untilElement(ref, ID_EMBLReadVersion);
if (elem == null) {
@@ -300,9 +306,7 @@ public class WebMReader {
WebMTrack entry = new WebMTrack();
boolean drop = false;
Element elem;
while ((elem = untilElement(elem_trackEntry,
ID_TrackNumber, ID_TrackType, ID_CodecID, ID_CodecPrivate, ID_FlagLacing, ID_DefaultDuration, ID_Audio, ID_Video
)) != null) {
while ((elem = untilElement(elem_trackEntry)) != null) {
switch (elem.type) {
case ID_TrackNumber:
entry.trackNumber = readNumber(elem);
@@ -326,8 +330,9 @@ public class WebMReader {
case ID_FlagLacing:
drop = readNumber(elem) != lacingExpected;
break;
case ID_CodecDelay:
entry.codecDelay = readNumber(elem);
default:
System.out.println();
break;
}
ensure(elem);
@@ -360,12 +365,13 @@ public class WebMReader {
private SimpleBlock readSimpleBlock(Element ref) throws IOException {
SimpleBlock obj = new SimpleBlock(ref);
obj.dataSize = stream.position();
obj.trackNumber = readEncodedNumber();
obj.relativeTimeCode = stream.readShort();
obj.flags = (byte) stream.read();
obj.dataSize = (ref.offset + ref.size) - stream.position();
obj.dataSize = (int) ((ref.offset + ref.size) - stream.position());
obj.createdFromBlock = ref.type == ID_Block;
// NOTE: lacing is not implemented, and will be mixed with the stream data
if (obj.dataSize < 0) {
throw new IOException(String.format("Unexpected SimpleBlock element size, missing %s bytes", -obj.dataSize));
}
@@ -383,9 +389,9 @@ public class WebMReader {
return obj;
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="class helpers">
class Element {
int type;
@@ -409,6 +415,7 @@ public class WebMReader {
public byte[] bMetadata;
public TrackKind kind;
public long defaultDuration;
public long codecDelay;
}
public class Segment {
@@ -448,6 +455,7 @@ public class WebMReader {
public class SimpleBlock {
public InputStream data;
public boolean createdFromBlock;
SimpleBlock(Element ref) {
this.ref = ref;
@@ -455,8 +463,9 @@ public class WebMReader {
public long trackNumber;
public short relativeTimeCode;
public long absoluteTimeCodeNs;
public byte flags;
public long dataSize;
public int dataSize;
private final Element ref;
public boolean isKeyframe() {
@@ -468,33 +477,55 @@ public class WebMReader {
Element ref;
SimpleBlock currentSimpleBlock = null;
Element currentBlockGroup = null;
public long timecode;
Cluster(Element ref) {
this.ref = ref;
}
boolean check() {
boolean insideClusterBounds() {
return stream.position() >= (ref.offset + ref.size);
}
public SimpleBlock getNextSimpleBlock() throws IOException {
if (check()) {
if (insideClusterBounds()) {
return null;
}
if (currentSimpleBlock != null) {
if (currentBlockGroup != null) {
ensure(currentBlockGroup);
currentBlockGroup = null;
currentSimpleBlock = null;
} else if (currentSimpleBlock != null) {
ensure(currentSimpleBlock.ref);
}
while (!check()) {
Element elem = untilElement(ref, ID_SimpleBlock);
while (!insideClusterBounds()) {
Element elem = untilElement(ref, ID_SimpleBlock, ID_GroupBlock);
if (elem == null) {
return null;
}
if (elem.type == ID_GroupBlock) {
currentBlockGroup = elem;
elem = untilElement(currentBlockGroup, ID_Block);
if (elem == null) {
ensure(currentBlockGroup);
currentBlockGroup = null;
continue;
}
}
currentSimpleBlock = readSimpleBlock(elem);
if (currentSimpleBlock.trackNumber == tracks[selectedTrack].trackNumber) {
currentSimpleBlock.data = stream.getView((int) currentSimpleBlock.dataSize);
// calculate the timestamp in nanoseconds
currentSimpleBlock.absoluteTimeCodeNs = currentSimpleBlock.relativeTimeCode + this.timecode;
currentSimpleBlock.absoluteTimeCodeNs *= segment.info.timecodeScale;
return currentSimpleBlock;
}
@@ -505,5 +536,5 @@ public class WebMReader {
}
}
//</editor-fold>
}

View File

@@ -8,6 +8,7 @@ import org.schabi.newpipe.streams.WebMReader.SimpleBlock;
import org.schabi.newpipe.streams.WebMReader.WebMTrack;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
@@ -17,7 +18,7 @@ import java.util.ArrayList;
/**
* @author kapodamy
*/
public class WebMWriter {
public class WebMWriter implements Closeable {
private final static int BUFFER_SIZE = 8 * 1024;
private final static int DEFAULT_TIMECODE_SCALE = 1000000;
@@ -35,7 +36,7 @@ public class WebMWriter {
private long written = 0;
private Segment[] readersSegment;
private Cluster[] readersCluter;
private Cluster[] readersCluster;
private int[] predefinedDurations;
@@ -81,7 +82,7 @@ public class WebMWriter {
public void selectTracks(int... trackIndex) throws IOException {
try {
readersSegment = new Segment[readers.length];
readersCluter = new Cluster[readers.length];
readersCluster = new Cluster[readers.length];
predefinedDurations = new int[readers.length];
for (int i = 0; i < readers.length; i++) {
@@ -102,6 +103,7 @@ public class WebMWriter {
return parsed;
}
@Override
public void close() {
done = true;
parsed = true;
@@ -114,7 +116,7 @@ public class WebMWriter {
readers = null;
infoTracks = null;
readersSegment = null;
readersCluter = null;
readersCluster = null;
outBuffer = null;
}
@@ -247,7 +249,7 @@ public class WebMWriter {
nextCueTime += DEFAULT_CUES_EACH_MS;
}
keyFrames.add(
new KeyFrame(baseSegmentOffset, currentClusterOffset - 7, written, bTimecode.length, bloq.absoluteTimecode)
new KeyFrame(baseSegmentOffset, currentClusterOffset - 8, written, bTimecode.length, bloq.absoluteTimecode)
);
}
}
@@ -334,17 +336,17 @@ public class WebMWriter {
}
}
if (readersCluter[internalTrackId] == null) {
readersCluter[internalTrackId] = readersSegment[internalTrackId].getNextCluster();
if (readersCluter[internalTrackId] == null) {
if (readersCluster[internalTrackId] == null) {
readersCluster[internalTrackId] = readersSegment[internalTrackId].getNextCluster();
if (readersCluster[internalTrackId] == null) {
readersSegment[internalTrackId] = null;
return getNextBlockFrom(internalTrackId);
}
}
SimpleBlock res = readersCluter[internalTrackId].getNextSimpleBlock();
SimpleBlock res = readersCluster[internalTrackId].getNextSimpleBlock();
if (res == null) {
readersCluter[internalTrackId] = null;
readersCluster[internalTrackId] = null;
return new Block();// fake block to indicate the end of the cluster
}
@@ -353,16 +355,11 @@ public class WebMWriter {
bloq.dataSize = (int) res.dataSize;
bloq.trackNumber = internalTrackId;
bloq.flags = res.flags;
bloq.absoluteTimecode = convertTimecode(res.relativeTimeCode, readersSegment[internalTrackId].info.timecodeScale);
bloq.absoluteTimecode += readersCluter[internalTrackId].timecode;
bloq.absoluteTimecode = res.absoluteTimeCodeNs / DEFAULT_TIMECODE_SCALE;
return bloq;
}
private short convertTimecode(int time, long oldTimeScale) {
return (short) (time * (DEFAULT_TIMECODE_SCALE / oldTimeScale));
}
private void seekTo(SharpStream stream, long offset) throws IOException {
if (stream.canSeek()) {
stream.seek(offset);

View File

@@ -0,0 +1,43 @@
package org.schabi.newpipe.util;
import android.graphics.Bitmap;
import androidx.annotation.Nullable;
public class BitmapUtils {
@Nullable
public static Bitmap centerCrop(Bitmap inputBitmap, int newWidth, int newHeight) {
if (inputBitmap == null || inputBitmap.isRecycled()) {
return null;
}
float sourceWidth = inputBitmap.getWidth();
float sourceHeight = inputBitmap.getHeight();
float xScale = newWidth / sourceWidth;
float yScale = newHeight / sourceHeight;
float newXScale;
float newYScale;
if (yScale > xScale) {
newXScale = xScale / yScale;
newYScale = 1.0f;
} else {
newXScale = 1.0f;
newYScale = yScale / xScale;
}
float scaledWidth = newXScale * sourceWidth;
float scaledHeight = newYScale * sourceHeight;
int left = (int) ((sourceWidth - scaledWidth) / 2);
int top = (int) ((sourceHeight - scaledHeight) / 2);
int width = (int) scaledWidth;
int height = (int) scaledHeight;
return Bitmap.createBitmap(inputBitmap, left, top, width, height);
}
}

View File

@@ -32,7 +32,7 @@ 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.suggestion.SuggestionExtractor;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.comments.CommentsInfo;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;

View File

@@ -31,6 +31,12 @@ public class KioskTranslator {
return c.getString(R.string.top_50);
case "New & hot":
return c.getString(R.string.new_and_hot);
case "Local":
return c.getString(R.string.local);
case "Recently added":
return c.getString(R.string.recently_added);
case "Most liked":
return c.getString(R.string.most_liked);
case "conferences":
return c.getString(R.string.conferences);
default:
@@ -46,6 +52,12 @@ public class KioskTranslator {
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot);
case "New & hot":
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot);
case "Local":
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_kiosk_local);
case "Recently added":
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_kiosk_recent);
case "Most liked":
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.thumbs_up);
case "conferences":
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot);
default:

View File

@@ -2,24 +2,26 @@ package org.schabi.newpipe.util;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.preference.PreferenceManager;
import androidx.annotation.NonNull;
import androidx.annotation.PluralsRes;
import androidx.annotation.StringRes;
import android.text.TextUtils;
import org.ocpsoft.prettytime.PrettyTime;
import org.ocpsoft.prettytime.units.Decade;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.localization.ContentCountry;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import androidx.annotation.NonNull;
import androidx.annotation.PluralsRes;
import androidx.annotation.StringRes;
/*
* Created by chschtsch on 12/29/15.
*
@@ -42,11 +44,16 @@ import java.util.Locale;
public class Localization {
public final static String DOT_SEPARATOR = "";
private static PrettyTime prettyTime;
private static final String DOT_SEPARATOR = "";
private Localization() {
}
public static void init() {
initPrettyTime();
}
@NonNull
public static String concatenateStrings(final String... strings) {
return concatenateStrings(Arrays.asList(strings));
@@ -69,16 +76,18 @@ public class Localization {
return stringBuilder.toString();
}
public static org.schabi.newpipe.extractor.utils.Localization getPreferredExtractorLocal(Context context) {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
public static org.schabi.newpipe.extractor.localization.Localization getPreferredLocalization(final Context context) {
final String contentLanguage = PreferenceManager
.getDefaultSharedPreferences(context)
.getString(context.getString(R.string.content_language_key), context.getString(R.string.default_language_value));
return org.schabi.newpipe.extractor.localization.Localization.fromLocalizationCode(contentLanguage);
}
String languageCode = sp.getString(context.getString(R.string.content_language_key),
context.getString(R.string.default_language_value));
String countryCode = sp.getString(context.getString(R.string.content_country_key),
context.getString(R.string.default_country_value));
return new org.schabi.newpipe.extractor.utils.Localization(countryCode, languageCode);
public static ContentCountry getPreferredContentCountry(final Context context) {
final String contentCountry = PreferenceManager
.getDefaultSharedPreferences(context)
.getString(context.getString(R.string.content_country_key), context.getString(R.string.default_country_value));
return new ContentCountry(contentCountry);
}
public static Locale getPreferredLocale(Context context) {
@@ -106,27 +115,12 @@ public class Localization {
return nf.format(number);
}
private static String formatDate(Context context, String date) {
Locale locale = getPreferredLocale(context);
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
Date datum = null;
try {
datum = formatter.parse(date);
} catch (ParseException e) {
e.printStackTrace();
}
DateFormat df = DateFormat.getDateInstance(DateFormat.MEDIUM, locale);
return df.format(datum);
public static String formatDate(Date date) {
return DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.getDefault()).format(date);
}
public static String localizeDate(Context context, String date) {
Resources res = context.getResources();
String dateString = res.getString(R.string.upload_date_text);
String formattedDate = formatDate(context, date);
return String.format(dateString, formattedDate);
public static String localizeUploadDate(Context context, Date date) {
return context.getString(R.string.upload_date_text, formatDate(date));
}
public static String localizeViewCount(Context context, long viewCount) {
@@ -153,6 +147,14 @@ public class Localization {
}
}
public static String listeningCount(Context context, long listeningCount) {
return getQuantity(context, R.plurals.listening, R.string.no_one_listening, listeningCount, shortCount(context, listeningCount));
}
public static String watchingCount(Context context, long watchingCount) {
return getQuantity(context, R.plurals.watching, R.string.no_one_watching, watchingCount, shortCount(context, watchingCount));
}
public static String shortViewCount(Context context, long viewCount) {
return getQuantity(context, R.plurals.views, R.string.no_views, viewCount, shortCount(context, viewCount));
}
@@ -192,4 +194,26 @@ public class Localization {
}
return output;
}
/*//////////////////////////////////////////////////////////////////////////
// Pretty Time
//////////////////////////////////////////////////////////////////////////*/
private static void initPrettyTime() {
prettyTime = new PrettyTime(Locale.getDefault());
// Do not use decades as YouTube doesn't either.
prettyTime.removeUnit(Decade.class);
}
private static PrettyTime getPrettyTime() {
// If pretty time's Locale is different, init again with the new one.
if (!prettyTime.getLocale().equals(Locale.getDefault())) {
initPrettyTime();
}
return prettyTime;
}
public static String relativeTime(Calendar calendarTime) {
return getPrettyTime().formatUnrounded(calendarTime);
}
}

View File

@@ -0,0 +1,65 @@
package org.schabi.newpipe.util;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import com.grack.nanojson.JsonArray;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser;
import com.grack.nanojson.JsonParserException;
import com.grack.nanojson.JsonStringWriter;
import com.grack.nanojson.JsonWriter;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class PeertubeHelper {
public static List<PeertubeInstance> getInstanceList(Context context) {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
String savedInstanceListKey = context.getString(R.string.peertube_instance_list_key);
final String savedJson = sharedPreferences.getString(savedInstanceListKey, null);
if (null == savedJson) {
return Collections.singletonList(getCurrentInstance());
}
try {
JsonArray array = JsonParser.object().from(savedJson).getArray("instances");
List<PeertubeInstance> result = new ArrayList<>();
for (Object o : array) {
if (o instanceof JsonObject) {
JsonObject instance = (JsonObject) o;
String name = instance.getString("name");
String url = instance.getString("url");
result.add(new PeertubeInstance(url, name));
}
}
return result;
} catch (JsonParserException e) {
return Collections.singletonList(getCurrentInstance());
}
}
public static PeertubeInstance selectInstance(PeertubeInstance instance, Context context) {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
String selectedInstanceKey = context.getString(R.string.peertube_selected_instance_key);
JsonStringWriter jsonWriter = JsonWriter.string().object();
jsonWriter.value("name", instance.getName());
jsonWriter.value("url", instance.getUrl());
String jsonToSave = jsonWriter.end().done();
sharedPreferences.edit().putString(selectedInstanceKey, jsonToSave).apply();
ServiceList.PeerTube.setInstance(instance);
return instance;
}
public static PeertubeInstance getCurrentInstance(){
return ServiceList.PeerTube.getInstance();
}
}

View File

@@ -52,10 +52,12 @@ public class SecondaryStreamHelper<T extends Stream> {
}
}
if (m4v) return null;
// retry, but this time in reverse order
for (int i = audioStreams.size() - 1; i >= 0; i--) {
AudioStream audio = audioStreams.get(i);
if (audio.getFormat() == (m4v ? MediaFormat.MP3 : MediaFormat.OPUS)) {
if (audio.getFormat() == MediaFormat.WEBMA_OPUS) {
return audio;
}
}

View File

@@ -1,15 +1,22 @@
package org.schabi.newpipe.util;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser;
import com.grack.nanojson.JsonParserException;
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.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
import java.util.concurrent.TimeUnit;
@@ -27,13 +34,15 @@ public class ServiceHelper {
return R.drawable.place_holder_cloud;
case 2:
return R.drawable.place_holder_gadse;
case 3:
return R.drawable.place_holder_peertube;
default:
return R.drawable.place_holder_circle;
}
}
public static String getTranslatedFilterString(String filter, Context c) {
switch(filter) {
switch (filter) {
case "all": return c.getString(R.string.all);
case "videos": return c.getString(R.string.videos);
case "channels": return c.getString(R.string.channels);
@@ -126,9 +135,36 @@ public class ServiceHelper {
}
public static boolean isBeta(final StreamingService s) {
switch(s.getServiceInfo().getName()) {
switch (s.getServiceInfo().getName()) {
case "YouTube": return false;
default: return true;
}
}
public static void initService(Context context, int serviceId) {
if (serviceId == ServiceList.PeerTube.getServiceId()) {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
String json = sharedPreferences.getString(context.getString(R.string.peertube_selected_instance_key), null);
if (null == json) {
return;
}
JsonObject jsonObject = null;
try {
jsonObject = JsonParser.object().from(json);
} catch (JsonParserException e) {
return;
}
String name = jsonObject.getString("name");
String url = jsonObject.getString("url");
PeertubeInstance instance = new PeertubeInstance(url, name);
ServiceList.PeerTube.setInstance(instance);
}
}
public static void initServices(Context context) {
for (StreamingService s : ServiceList.all()) {
initService(context, s.getServiceId());
}
}
}

View File

@@ -10,7 +10,7 @@ import android.widget.ImageView;
import android.widget.Spinner;
import android.widget.TextView;
import org.schabi.newpipe.Downloader;
import org.schabi.newpipe.DownloaderImpl;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.Stream;
@@ -140,7 +140,15 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
if (stream instanceof SubtitlesStream) {
formatNameView.setText(((SubtitlesStream) stream).getLanguageTag());
} else {
formatNameView.setText(stream.getFormat().getName());
switch (stream.getFormat()) {
case WEBMA_OPUS:
// noinspection AndroidLintSetTextI18n
formatNameView.setText("opus");
break;
default:
formatNameView.setText(stream.getFormat().getName());
break;
}
}
qualityView.setText(qualityString);
@@ -182,7 +190,7 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
continue;
}
final long contentLength = Downloader.getInstance().getContentLength(stream.getUrl());
final long contentLength = DownloaderImpl.getInstance().getContentLength(stream.getUrl());
streamsWrapper.setSize(stream, contentLength);
hasChanged = true;
}

View File

@@ -0,0 +1,104 @@
package org.schabi.newpipe.util;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import static org.schabi.newpipe.MainActivity.DEBUG;
/**
* This is an extension of the SSLSocketFactory which enables TLS 1.2 and 1.1.
* Created for usage on Android 4.1-4.4 devices, which haven't enabled those by default.
*/
public class TLSSocketFactoryCompat extends SSLSocketFactory {
private static TLSSocketFactoryCompat instance = null;
private SSLSocketFactory internalSSLSocketFactory;
public static TLSSocketFactoryCompat getInstance() throws NoSuchAlgorithmException, KeyManagementException {
if (instance != null) {
return instance;
}
return instance = new TLSSocketFactoryCompat();
}
public TLSSocketFactoryCompat() throws KeyManagementException, NoSuchAlgorithmException {
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, null, null);
internalSSLSocketFactory = context.getSocketFactory();
}
public TLSSocketFactoryCompat(TrustManager[] tm) throws KeyManagementException, NoSuchAlgorithmException {
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, tm, new java.security.SecureRandom());
internalSSLSocketFactory = context.getSocketFactory();
}
public static void setAsDefault() {
try {
HttpsURLConnection.setDefaultSSLSocketFactory(getInstance());
} catch (NoSuchAlgorithmException | KeyManagementException e) {
if (DEBUG) e.printStackTrace();
}
}
@Override
public String[] getDefaultCipherSuites() {
return internalSSLSocketFactory.getDefaultCipherSuites();
}
@Override
public String[] getSupportedCipherSuites() {
return internalSSLSocketFactory.getSupportedCipherSuites();
}
@Override
public Socket createSocket() throws IOException {
return enableTLSOnSocket(internalSSLSocketFactory.createSocket());
}
@Override
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose));
}
@Override
public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port));
}
@Override
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort));
}
@Override
public Socket createSocket(InetAddress host, int port) throws IOException {
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port));
}
@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort));
}
private Socket enableTLSOnSocket(Socket socket) {
if (socket != null && (socket instanceof SSLSocket)) {
((SSLSocket) socket).setEnabledProtocols(new String[]{"TLSv1.1", "TLSv1.2"});
}
return socket;
}
}

View File

@@ -0,0 +1,128 @@
package org.schabi.newpipe.views;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import androidx.annotation.NonNull;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayout.Tab;
/**
* A TabLayout that is scrollable when tabs exceed its width.
* Hides when there are less than 2 tabs.
*/
public class ScrollableTabLayout extends TabLayout {
private static final String TAG = ScrollableTabLayout.class.getSimpleName();
private int layoutWidth = 0;
private int prevVisibility = View.GONE;
public ScrollableTabLayout(Context context) {
super(context);
}
public ScrollableTabLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ScrollableTabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
remeasureTabs();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
layoutWidth = w;
}
@Override
public void addTab(@NonNull Tab tab, int position, boolean setSelected) {
super.addTab(tab, position, setSelected);
hasMultipleTabs();
// Adding a tab won't decrease total tabs' width so tabMode won't have to change to FIXED
if (getTabMode() != MODE_SCROLLABLE) {
remeasureTabs();
}
}
@Override
public void removeTabAt(int position) {
super.removeTabAt(position);
hasMultipleTabs();
// Removing a tab won't increase total tabs' width so tabMode won't have to change to SCROLLABLE
if (getTabMode() != MODE_FIXED) {
remeasureTabs();
}
}
@Override
protected void onVisibilityChanged(View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
// Recheck content width in case some tabs have been added or removed while ScrollableTabLayout was invisible
// We don't have to check if it was GONE because then requestLayout() will be called
if (changedView == this) {
if (prevVisibility == View.INVISIBLE) {
remeasureTabs();
}
prevVisibility = visibility;
}
}
private void setMode(int mode) {
if (mode == getTabMode()) return;
setTabMode(mode);
}
/**
* Make ScrollableTabLayout not visible if there are less than two tabs
*/
private void hasMultipleTabs() {
if (getTabCount() > 1) {
setVisibility(View.VISIBLE);
} else {
setVisibility(View.GONE);
}
}
/**
* Calculate minimal width required by tabs and set tabMode accordingly
*/
private void remeasureTabs() {
if (prevVisibility != View.VISIBLE) return;
if (layoutWidth == 0) return;
final int count = getTabCount();
int contentWidth = 0;
for (int i = 0; i < count; i++) {
View child = getTabAt(i).view;
if (child.getVisibility() == View.VISIBLE) {
// Use tab's minimum requested width should actual content be too small
contentWidth += Math.max(child.getMinimumWidth(), child.getMeasuredWidth());
}
}
if (contentWidth > layoutWidth) {
setMode(TabLayout.MODE_SCROLLABLE);
} else {
setMode(TabLayout.MODE_FIXED);
}
}
}

View File

@@ -1,8 +1,10 @@
package us.shandian.giga.get;
import androidx.annotation.NonNull;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
@@ -13,6 +15,7 @@ import java.nio.channels.ClosedByInterruptException;
import us.shandian.giga.util.Utility;
import static org.schabi.newpipe.BuildConfig.DEBUG;
import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_FORBIDDEN;
public class DownloadInitializer extends Thread {
private final static String TAG = "DownloadInitializer";
@@ -28,9 +31,9 @@ public class DownloadInitializer extends Thread {
mConn = null;
}
private static void safeClose(HttpURLConnection con) {
private void dispose() {
try {
con.getInputStream().close();
mConn.getInputStream().close();
} catch (Exception e) {
// nothing to do
}
@@ -51,9 +54,9 @@ public class DownloadInitializer extends Thread {
long lowestSize = Long.MAX_VALUE;
for (int i = 0; i < mMission.urls.length && mMission.running; i++) {
mConn = mMission.openConnection(mMission.urls[i], mId, -1, -1);
mConn = mMission.openConnection(mMission.urls[i], true, -1, -1);
mMission.establishConnection(mId, mConn);
safeClose(mConn);
dispose();
if (Thread.interrupted()) return;
long length = Utility.getContentLength(mConn);
@@ -81,9 +84,9 @@ public class DownloadInitializer extends Thread {
}
} else {
// ask for the current resource length
mConn = mMission.openConnection(mId, -1, -1);
mConn = mMission.openConnection(true, -1, -1);
mMission.establishConnection(mId, mConn);
safeClose(mConn);
dispose();
if (!mMission.running || Thread.interrupted()) return;
@@ -107,9 +110,9 @@ public class DownloadInitializer extends Thread {
}
} else {
// Open again
mConn = mMission.openConnection(mId, mMission.length - 10, mMission.length);
mConn = mMission.openConnection(true, mMission.length - 10, mMission.length);
mMission.establishConnection(mId, mConn);
safeClose(mConn);
dispose();
if (!mMission.running || Thread.interrupted()) return;
@@ -151,12 +154,33 @@ public class DownloadInitializer extends Thread {
if (!mMission.running || Thread.interrupted()) return;
if (!mMission.unknownLength && mMission.recoveryInfo != null) {
String entityTag = mConn.getHeaderField("ETAG");
String lastModified = mConn.getHeaderField("Last-Modified");
MissionRecoveryInfo recovery = mMission.recoveryInfo[mMission.current];
if (!TextUtils.isEmpty(entityTag)) {
recovery.validateCondition = entityTag;
} else if (!TextUtils.isEmpty(lastModified)) {
recovery.validateCondition = lastModified;// Note: this is less precise
} else {
recovery.validateCondition = null;
}
}
mMission.running = false;
break;
} catch (InterruptedIOException | ClosedByInterruptException e) {
return;
} catch (Exception e) {
if (!mMission.running) return;
if (!mMission.running || super.isInterrupted()) return;
if (e instanceof DownloadMission.HttpError && ((DownloadMission.HttpError) e).statusCode == ERROR_HTTP_FORBIDDEN) {
// for youtube streams. The url has expired
interrupt();
mMission.doRecover(ERROR_HTTP_FORBIDDEN);
return;
}
if (e instanceof IOException && e.getMessage().contains("Permission denied")) {
mMission.notifyError(DownloadMission.ERROR_PERMISSION_DENIED, e);
@@ -179,13 +203,6 @@ public class DownloadInitializer extends Thread {
@Override
public void interrupt() {
super.interrupt();
if (mConn != null) {
try {
mConn.disconnect();
} catch (Exception e) {
// nothing to do
}
}
if (mConn != null) dispose();
}
}

View File

@@ -1,21 +1,27 @@
package us.shandian.giga.get;
import android.os.Build;
import android.os.Handler;
import android.system.ErrnoException;
import android.system.OsConstants;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.annotation.NonNull;
import org.schabi.newpipe.Downloader;
import org.schabi.newpipe.DownloaderImpl;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.Serializable;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.UnknownHostException;
import java.nio.channels.ClosedByInterruptException;
import javax.net.ssl.SSLException;
@@ -27,14 +33,11 @@ import us.shandian.giga.util.Utility;
import static org.schabi.newpipe.BuildConfig.DEBUG;
public class DownloadMission extends Mission {
private static final long serialVersionUID = 5L;// last bump: 30 june 2019
private static final long serialVersionUID = 6L;// last bump: 07 october 2019
static final int BUFFER_SIZE = 64 * 1024;
static final int BLOCK_SIZE = 512 * 1024;
@SuppressWarnings("SpellCheckingInspection")
private static final String INSUFFICIENT_STORAGE = "ENOSPC";
private static final String TAG = "DownloadMission";
public static final int ERROR_NOTHING = -1;
@@ -51,8 +54,9 @@ public class DownloadMission extends Mission {
public static final int ERROR_INSUFFICIENT_STORAGE = 1010;
public static final int ERROR_PROGRESS_LOST = 1011;
public static final int ERROR_TIMEOUT = 1012;
public static final int ERROR_RESOURCE_GONE = 1013;
public static final int ERROR_HTTP_NO_CONTENT = 204;
public static final int ERROR_HTTP_UNSUPPORTED_RANGE = 206;
static final int ERROR_HTTP_FORBIDDEN = 403;
/**
* The urls of the file to download
@@ -60,9 +64,9 @@ public class DownloadMission extends Mission {
public String[] urls;
/**
* Number of bytes downloaded
* Number of bytes downloaded and written
*/
public long done;
public volatile long done;
/**
* Indicates a file generated dynamically on the web server
@@ -118,31 +122,36 @@ public class DownloadMission extends Mission {
/**
* Download/File resume offset in fallback mode (if applicable) {@link DownloadRunnableFallback}
*/
long fallbackResumeOffset;
volatile long fallbackResumeOffset;
/**
* Maximum of download threads running, chosen by the user
*/
public int threadCount = 3;
/**
* information required to recover a download
*/
public MissionRecoveryInfo[] recoveryInfo;
private transient int finishCount;
public transient boolean running;
public transient volatile boolean running;
public boolean enqueued;
public int errCode = ERROR_NOTHING;
public Exception errObject = null;
public transient boolean recovered;
public transient Handler mHandler;
private transient boolean mWritingToFile;
private transient boolean[] blockAcquired;
private transient long writingToFileNext;
private transient volatile boolean writingToFile;
final Object LOCK = new Lock();
private transient boolean deleted;
public transient volatile Thread[] threads = new Thread[0];
private transient Thread init = null;
@NonNull
public transient Thread[] threads = new Thread[0];
public transient Thread init = null;
public DownloadMission(String[] urls, StoredFileHelper storage, char kind, Postprocessing psInstance) {
if (urls == null) throw new NullPointerException("urls is null");
@@ -197,37 +206,34 @@ public class DownloadMission extends Mission {
}
/**
* Open connection
* Opens a connection
*
* @param threadId id of the calling thread, used only for debug
* @param rangeStart range start
* @param rangeEnd range end
* @param headRequest {@code true} for use {@code HEAD} request method, otherwise, {@code GET} is used
* @param rangeStart range start
* @param rangeEnd range end
* @return a {@link java.net.URLConnection URLConnection} linking to the URL.
* @throws IOException if an I/O exception occurs.
*/
HttpURLConnection openConnection(int threadId, long rangeStart, long rangeEnd) throws IOException {
return openConnection(urls[current], threadId, rangeStart, rangeEnd);
HttpURLConnection openConnection(boolean headRequest, long rangeStart, long rangeEnd) throws IOException {
return openConnection(urls[current], headRequest, rangeStart, rangeEnd);
}
HttpURLConnection openConnection(String url, int threadId, long rangeStart, long rangeEnd) throws IOException {
HttpURLConnection openConnection(String url, boolean headRequest, long rangeStart, long rangeEnd) throws IOException {
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setInstanceFollowRedirects(true);
conn.setRequestProperty("User-Agent", Downloader.USER_AGENT);
conn.setRequestProperty("User-Agent", DownloaderImpl.USER_AGENT);
conn.setRequestProperty("Accept", "*/*");
if (headRequest) conn.setRequestMethod("HEAD");
// BUG workaround: switching between networks can freeze the download forever
conn.setConnectTimeout(30000);
conn.setReadTimeout(10000);
if (rangeStart >= 0) {
String req = "bytes=" + rangeStart + "-";
if (rangeEnd > 0) req += rangeEnd;
conn.setRequestProperty("Range", req);
if (DEBUG) {
Log.d(TAG, threadId + ":" + conn.getRequestProperty("Range"));
}
}
return conn;
@@ -240,18 +246,21 @@ public class DownloadMission extends Mission {
* @throws HttpError if the HTTP Status-Code is not satisfiable
*/
void establishConnection(int threadId, HttpURLConnection conn) throws IOException, HttpError {
conn.connect();
int statusCode = conn.getResponseCode();
if (DEBUG) {
Log.d(TAG, threadId + ":Content-Length=" + conn.getContentLength() + " Code:" + statusCode);
Log.d(TAG, threadId + ":[request] Range=" + conn.getRequestProperty("Range"));
Log.d(TAG, threadId + ":[response] Code=" + statusCode);
Log.d(TAG, threadId + ":[response] Content-Length=" + conn.getContentLength());
Log.d(TAG, threadId + ":[response] Content-Range=" + conn.getHeaderField("Content-Range"));
}
switch (statusCode) {
case 204:
case 205:
case 207:
throw new HttpError(conn.getResponseCode());
throw new HttpError(statusCode);
case 416:
return;// let the download thread handle this error
default:
@@ -268,28 +277,19 @@ public class DownloadMission extends Mission {
}
synchronized void notifyProgress(long deltaLen) {
if (!running) return;
if (recovered) {
recovered = false;
}
if (unknownLength) {
length += deltaLen;// Update length before proceeding
}
done += deltaLen;
if (done > length) {
done = length;
}
if (metadata == null) return;
if (done != length && !deleted && !mWritingToFile) {
mWritingToFile = true;
runAsync(-2, this::writeThisToFile);
if (!writingToFile && (done > writingToFileNext || deltaLen < 0)) {
writingToFile = true;
writingToFileNext = done + BLOCK_SIZE;
writeThisToFileAsync();
}
notify(DownloadManagerService.MESSAGE_PROGRESS);
}
synchronized void notifyError(Exception err) {
@@ -314,13 +314,29 @@ public class DownloadMission extends Mission {
public synchronized void notifyError(int code, Exception err) {
Log.e(TAG, "notifyError() code = " + code, err);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (err != null && err.getCause() instanceof ErrnoException) {
int errno = ((ErrnoException) err.getCause()).errno;
if (errno == OsConstants.ENOSPC) {
code = ERROR_INSUFFICIENT_STORAGE;
err = null;
} else if (errno == OsConstants.EACCES) {
code = ERROR_PERMISSION_DENIED;
err = null;
}
}
}
if (err instanceof IOException) {
if (!storage.canWrite() || err.getMessage().contains("Permission denied")) {
if (err.getMessage().contains("Permission denied")) {
code = ERROR_PERMISSION_DENIED;
err = null;
} else if (err.getMessage().contains(INSUFFICIENT_STORAGE)) {
} else if (err.getMessage().contains("ENOSPC")) {
code = ERROR_INSUFFICIENT_STORAGE;
err = null;
} else if (!storage.canWrite()) {
code = ERROR_FILE_CREATION;
err = null;
}
}
@@ -342,44 +358,42 @@ public class DownloadMission extends Mission {
notify(DownloadManagerService.MESSAGE_ERROR);
if (running) {
running = false;
recovered = true;
if (threads != null) selfPause();
}
if (running) pauseThreads();
}
synchronized void notifyFinished() {
if (errCode > ERROR_NOTHING) return;
finishCount++;
if (blocks.length < 1 || threads == null || finishCount == threads.length) {
if (errCode != ERROR_NOTHING) return;
if (current < urls.length) {
if (++finishCount < threads.length) return;
if (DEBUG) {
Log.d(TAG, "onFinish: " + (current + 1) + "/" + urls.length);
}
if ((current + 1) < urls.length) {
// prepare next sub-mission
long current_offset = offsets[current++];
offsets[current] = current_offset + length;
initializer();
return;
Log.d(TAG, "onFinish: downloaded " + (current + 1) + "/" + urls.length);
}
current++;
unknownLength = false;
if (!doPostprocessing()) return;
enqueued = false;
running = false;
deleteThisFromFile();
notify(DownloadManagerService.MESSAGE_FINISHED);
if (current < urls.length) {
// prepare next sub-mission
offsets[current] = offsets[current - 1] + length;
initializer();
return;
}
}
if (psAlgorithm != null && psState == 0) {
threads = new Thread[]{
runAsync(1, this::doPostprocessing)
};
return;
}
// this mission is fully finished
unknownLength = false;
enqueued = false;
running = false;
deleteThisFromFile();
notify(DownloadManagerService.MESSAGE_FINISHED);
}
private void notifyPostProcessing(int state) {
@@ -397,10 +411,15 @@ public class DownloadMission extends Mission {
Log.d(TAG, action + " postprocessing on " + storage.getName());
if (state == 2) {
psState = state;
return;
}
synchronized (LOCK) {
// don't return without fully write the current state
psState = state;
Utility.writeToFile(metadata, DownloadMission.this);
writeThisToFile();
}
}
@@ -409,14 +428,10 @@ public class DownloadMission extends Mission {
* Start downloading with multiple threads.
*/
public void start() {
if (running || isFinished()) return;
if (running || isFinished() || urls.length < 1) return;
// ensure that the previous state is completely paused.
joinForThread(init);
if (threads != null) {
for (Thread thread : threads) joinForThread(thread);
threads = null;
}
joinForThreads(10000);
running = true;
errCode = ERROR_NOTHING;
@@ -427,7 +442,14 @@ public class DownloadMission extends Mission {
}
if (current >= urls.length) {
runAsync(1, this::notifyFinished);
notifyFinished();
return;
}
notify(DownloadManagerService.MESSAGE_RUNNING);
if (urls[current] == null) {
doRecover(ERROR_RESOURCE_GONE);
return;
}
@@ -441,18 +463,13 @@ public class DownloadMission extends Mission {
blockAcquired = new boolean[blocks.length];
if (blocks.length < 1) {
if (unknownLength) {
done = 0;
length = 0;
}
threads = new Thread[]{runAsync(1, new DownloadRunnableFallback(this))};
} else {
int remainingBlocks = 0;
for (int block : blocks) if (block >= 0) remainingBlocks++;
if (remainingBlocks < 1) {
runAsync(1, this::notifyFinished);
notifyFinished();
return;
}
@@ -478,7 +495,7 @@ public class DownloadMission extends Mission {
}
running = false;
recovered = true;
notify(DownloadManagerService.MESSAGE_PAUSED);
if (init != null && init.isAlive()) {
// NOTE: if start() method is running ¡will no have effect!
@@ -493,29 +510,14 @@ public class DownloadMission extends Mission {
Log.w(TAG, "pausing a download that can not be resumed (range requests not allowed by the server).");
}
// check if the calling thread (alias UI thread) is interrupted
if (Thread.currentThread().isInterrupted()) {
writeThisToFile();
return;
}
// wait for all threads are suspended before save the state
if (threads != null) runAsync(-1, this::selfPause);
init = null;
pauseThreads();
}
private void selfPause() {
try {
for (Thread thread : threads) {
if (thread.isAlive()) {
thread.interrupt();
thread.join(5000);
}
}
} catch (Exception e) {
// nothing to do
} finally {
writeThisToFile();
}
private void pauseThreads() {
running = false;
joinForThreads(-1);
writeThisToFile();
}
/**
@@ -523,9 +525,10 @@ public class DownloadMission extends Mission {
*/
@Override
public boolean delete() {
deleted = true;
if (psAlgorithm != null) psAlgorithm.cleanupTemporalDir();
notify(DownloadManagerService.MESSAGE_DELETED);
boolean res = deleteThisFromFile();
if (!super.delete()) return false;
@@ -540,35 +543,37 @@ public class DownloadMission extends Mission {
* @param persistChanges {@code true} to commit changes to the metadata file, otherwise, {@code false}
*/
public void resetState(boolean rollback, boolean persistChanges, int errorCode) {
done = 0;
length = 0;
errCode = errorCode;
errObject = null;
unknownLength = false;
threads = null;
threads = new Thread[0];
fallbackResumeOffset = 0;
blocks = null;
blockAcquired = null;
if (rollback) current = 0;
if (persistChanges)
Utility.writeToFile(metadata, DownloadMission.this);
if (persistChanges) writeThisToFile();
}
private void initializer() {
init = runAsync(DownloadInitializer.mId, new DownloadInitializer(this));
}
private void writeThisToFileAsync() {
runAsync(-2, this::writeThisToFile);
}
/**
* Write this {@link DownloadMission} to the meta file asynchronously
* if no thread is already running.
*/
private void writeThisToFile() {
void writeThisToFile() {
synchronized (LOCK) {
if (deleted) return;
Utility.writeToFile(metadata, DownloadMission.this);
if (metadata == null) return;
Utility.writeToFile(metadata, this);
writingToFile = false;
}
mWritingToFile = false;
}
/**
@@ -621,11 +626,10 @@ public class DownloadMission extends Mission {
public long getLength() {
long calculated;
if (psState == 1 || psState == 3) {
calculated = length;
} else {
calculated = offsets[current < offsets.length ? current : (offsets.length - 1)] + length;
return length;
}
calculated = offsets[current < offsets.length ? current : (offsets.length - 1)] + length;
calculated -= offsets[0];// don't count reserved space
return calculated > nearLength ? calculated : nearLength;
@@ -638,7 +642,7 @@ public class DownloadMission extends Mission {
*/
public void setEnqueued(boolean queue) {
enqueued = queue;
runAsync(-2, this::writeThisToFile);
writeThisToFileAsync();
}
/**
@@ -667,24 +671,29 @@ public class DownloadMission extends Mission {
* @return {@code true} is this mission its "healthy", otherwise, {@code false}
*/
public boolean isCorrupt() {
if (urls.length < 1) return false;
return (isPsFailed() || errCode == ERROR_POSTPROCESSING_HOLD) || isFinished();
}
private boolean doPostprocessing() {
if (psAlgorithm == null || psState == 2) return true;
/**
* Indicates if mission urls has expired and there an attempt to renovate them
*
* @return {@code true} if the mission is running a recovery procedure, otherwise, {@code false}
*/
public boolean isRecovering() {
return threads.length > 0 && threads[0] instanceof DownloadMissionRecover && threads[0].isAlive();
}
private void doPostprocessing() {
errCode = ERROR_NOTHING;
errObject = null;
Thread thread = Thread.currentThread();
notifyPostProcessing(1);
notifyProgress(0);
if (DEBUG)
Thread.currentThread().setName("[" + TAG + "] ps = " +
psAlgorithm.getClass().getSimpleName() +
" filename = " + storage.getName()
);
threads = new Thread[]{Thread.currentThread()};
if (DEBUG) {
thread.setName("[" + TAG + "] ps = " + psAlgorithm + " filename = " + storage.getName());
}
Exception exception = null;
@@ -693,6 +702,11 @@ public class DownloadMission extends Mission {
} catch (Exception err) {
Log.e(TAG, "Post-processing failed. " + psAlgorithm.toString(), err);
if (err instanceof InterruptedIOException || err instanceof ClosedByInterruptException || thread.isInterrupted()) {
notifyError(DownloadMission.ERROR_POSTPROCESSING_STOPPED, null);
return;
}
if (errCode == ERROR_NOTHING) errCode = ERROR_POSTPROCESSING;
exception = err;
@@ -703,16 +717,38 @@ public class DownloadMission extends Mission {
if (errCode != ERROR_NOTHING) {
if (exception == null) exception = errObject;
notifyError(ERROR_POSTPROCESSING, exception);
return false;
return;
}
return true;
notifyFinished();
}
/**
* Attempts to recover the download
*
* @param errorCode error code which trigger the recovery procedure
*/
void doRecover(int errorCode) {
Log.i(TAG, "Attempting to recover the mission: " + storage.getName());
if (recoveryInfo == null) {
notifyError(errorCode, null);
urls = new String[0];// mark this mission as dead
return;
}
joinForThreads(0);
threads = new Thread[]{
runAsync(DownloadMissionRecover.mID, new DownloadMissionRecover(this, errorCode))
};
}
private boolean deleteThisFromFile() {
synchronized (LOCK) {
return metadata.delete();
boolean res = metadata.delete();
metadata = null;
return res;
}
}
@@ -722,8 +758,8 @@ public class DownloadMission extends Mission {
* @param id id of new thread (used for debugging only)
* @param who the Runnable whose {@code run} method is invoked.
*/
private void runAsync(int id, Runnable who) {
runAsync(id, new Thread(who));
private Thread runAsync(int id, Runnable who) {
return runAsync(id, new Thread(who));
}
/**
@@ -749,25 +785,47 @@ public class DownloadMission extends Mission {
return who;
}
private void joinForThread(Thread thread) {
if (thread == null || !thread.isAlive()) return;
if (thread == Thread.currentThread()) return;
/**
* Waits at most {@code millis} milliseconds for the thread to die
*
* @param millis the time to wait in milliseconds
*/
private void joinForThreads(int millis) {
final Thread currentThread = Thread.currentThread();
if (DEBUG) {
Log.w(TAG, "a thread is !still alive!: " + thread.getName());
if (init != null && init != currentThread && init.isAlive()) {
init.interrupt();
if (millis > 0) {
try {
init.join(millis);
} catch (InterruptedException e) {
Log.w(TAG, "Initializer thread is still running", e);
return;
}
}
}
// still alive, this should not happen.
// Possible reasons:
// if a thread is still alive, possible reasons:
// slow device
// the user is spamming start/pause buttons
// start() method called quickly after pause()
for (Thread thread : threads) {
if (!thread.isAlive() || thread == Thread.currentThread()) continue;
thread.interrupt();
}
try {
thread.join(10000);
for (Thread thread : threads) {
if (!thread.isAlive()) continue;
if (DEBUG) {
Log.w(TAG, "thread alive: " + thread.getName());
}
if (millis > 0) thread.join(millis);
}
} catch (InterruptedException e) {
Log.d(TAG, "timeout on join : " + thread.getName());
throw new RuntimeException("A thread is still running:\n" + thread.getName());
throw new RuntimeException("A download thread is still running", e);
}
}
@@ -785,9 +843,9 @@ public class DownloadMission extends Mission {
}
}
static class Block {
int position;
int done;
public static class Block {
public int position;
public int done;
}
private static class Lock implements Serializable {

View File

@@ -0,0 +1,313 @@
package us.shandian.giga.get;
import android.util.Log;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.StreamExtractor;
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
import org.schabi.newpipe.extractor.stream.VideoStream;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.HttpURLConnection;
import java.nio.channels.ClosedByInterruptException;
import java.util.List;
import us.shandian.giga.get.DownloadMission.HttpError;
import static us.shandian.giga.get.DownloadMission.ERROR_RESOURCE_GONE;
public class DownloadMissionRecover extends Thread {
private static final String TAG = "DownloadMissionRecover";
static final int mID = -3;
private final DownloadMission mMission;
private final boolean mNotInitialized;
private final int mErrCode;
private HttpURLConnection mConn;
private MissionRecoveryInfo mRecovery;
private StreamExtractor mExtractor;
DownloadMissionRecover(DownloadMission mission, int errCode) {
mMission = mission;
mNotInitialized = mission.blocks == null && mission.current == 0;
mErrCode = errCode;
}
@Override
public void run() {
if (mMission.source == null) {
mMission.notifyError(mErrCode, null);
return;
}
Exception err = null;
int attempt = 0;
while (attempt++ < mMission.maxRetry) {
try {
tryRecover();
return;
} catch (InterruptedIOException | ClosedByInterruptException e) {
return;
} catch (Exception e) {
if (!mMission.running || super.isInterrupted()) return;
err = e;
}
}
// give up
mMission.notifyError(mErrCode, err);
}
private void tryRecover() throws ExtractionException, IOException, HttpError {
if (mExtractor == null) {
try {
StreamingService svr = NewPipe.getServiceByUrl(mMission.source);
mExtractor = svr.getStreamExtractor(mMission.source);
mExtractor.fetchPage();
} catch (ExtractionException e) {
mExtractor = null;
throw e;
}
}
// maybe the following check is redundant
if (!mMission.running || super.isInterrupted()) return;
if (!mNotInitialized) {
// set the current download url to null in case if the recovery
// process is canceled. Next time start() method is called the
// recovery will be executed, saving time
mMission.urls[mMission.current] = null;
mRecovery = mMission.recoveryInfo[mMission.current];
resolveStream();
return;
}
Log.w(TAG, "mission is not fully initialized, this will take a while");
try {
for (; mMission.current < mMission.urls.length; mMission.current++) {
mRecovery = mMission.recoveryInfo[mMission.current];
if (test()) continue;
if (!mMission.running) return;
resolveStream();
if (!mMission.running) return;
// before continue, check if the current stream was resolved
if (mMission.urls[mMission.current] == null) {
break;
}
}
} finally {
mMission.current = 0;
}
mMission.writeThisToFile();
if (!mMission.running || super.isInterrupted()) return;
mMission.running = false;
mMission.start();
}
private void resolveStream() throws IOException, ExtractionException, HttpError {
// FIXME: this getErrorMessage() always returns "video is unavailable"
/*if (mExtractor.getErrorMessage() != null) {
mMission.notifyError(mErrCode, new ExtractionException(mExtractor.getErrorMessage()));
return;
}*/
String url = null;
switch (mRecovery.kind) {
case 'a':
for (AudioStream audio : mExtractor.getAudioStreams()) {
if (audio.average_bitrate == mRecovery.desiredBitrate && audio.getFormat() == mRecovery.format) {
url = audio.getUrl();
break;
}
}
break;
case 'v':
List<VideoStream> videoStreams;
if (mRecovery.desired2)
videoStreams = mExtractor.getVideoOnlyStreams();
else
videoStreams = mExtractor.getVideoStreams();
for (VideoStream video : videoStreams) {
if (video.resolution.equals(mRecovery.desired) && video.getFormat() == mRecovery.format) {
url = video.getUrl();
break;
}
}
break;
case 's':
for (SubtitlesStream subtitles : mExtractor.getSubtitles(mRecovery.format)) {
String tag = subtitles.getLanguageTag();
if (tag.equals(mRecovery.desired) && subtitles.isAutoGenerated() == mRecovery.desired2) {
url = subtitles.getURL();
break;
}
}
break;
default:
throw new RuntimeException("Unknown stream type");
}
resolve(url);
}
private void resolve(String url) throws IOException, HttpError {
if (mRecovery.validateCondition == null) {
Log.w(TAG, "validation condition not defined, the resource can be stale");
}
if (mMission.unknownLength || mRecovery.validateCondition == null) {
recover(url, false);
return;
}
///////////////////////////////////////////////////////////////////////
////// Validate the http resource doing a range request
/////////////////////
try {
mConn = mMission.openConnection(url, true, mMission.length - 10, mMission.length);
mConn.setRequestProperty("If-Range", mRecovery.validateCondition);
mMission.establishConnection(mID, mConn);
int code = mConn.getResponseCode();
switch (code) {
case 200:
case 413:
// stale
recover(url, true);
return;
case 206:
// in case of validation using the Last-Modified date, check the resource length
long[] contentRange = parseContentRange(mConn.getHeaderField("Content-Range"));
boolean lengthMismatch = contentRange[2] != -1 && contentRange[2] != mMission.length;
recover(url, lengthMismatch);
return;
}
throw new HttpError(code);
} finally {
disconnect();
}
}
private void recover(String url, boolean stale) {
Log.i(TAG,
String.format("recover() name=%s isStale=%s url=%s", mMission.storage.getName(), stale, url)
);
mMission.urls[mMission.current] = url;
if (url == null) {
mMission.urls = new String[0];
mMission.notifyError(ERROR_RESOURCE_GONE, null);
return;
}
if (mNotInitialized) return;
if (stale) {
mMission.resetState(false, false, DownloadMission.ERROR_NOTHING);
}
mMission.writeThisToFile();
if (!mMission.running || super.isInterrupted()) return;
mMission.running = false;
mMission.start();
}
private long[] parseContentRange(String value) {
long[] range = new long[3];
if (value == null) {
// this never should happen
return range;
}
try {
value = value.trim();
if (!value.startsWith("bytes")) {
return range;// unknown range type
}
int space = value.lastIndexOf(' ') + 1;
int dash = value.indexOf('-', space) + 1;
int bar = value.indexOf('/', dash);
// start
range[0] = Long.parseLong(value.substring(space, dash - 1));
// end
range[1] = Long.parseLong(value.substring(dash, bar));
// resource length
value = value.substring(bar + 1);
if (value.equals("*")) {
range[2] = -1;// unknown length received from the server but should be valid
} else {
range[2] = Long.parseLong(value);
}
} catch (Exception e) {
// nothing to do
}
return range;
}
private boolean test() {
if (mMission.urls[mMission.current] == null) return false;
try {
mConn = mMission.openConnection(mMission.urls[mMission.current], true, -1, -1);
mMission.establishConnection(mID, mConn);
if (mConn.getResponseCode() == 200) return true;
} catch (Exception e) {
// nothing to do
} finally {
disconnect();
}
return false;
}
private void disconnect() {
try {
try {
mConn.getInputStream().close();
} finally {
mConn.disconnect();
}
} catch (Exception e) {
// nothing to do
} finally {
mConn = null;
}
}
@Override
public void interrupt() {
super.interrupt();
if (mConn != null) disconnect();
}
}

View File

@@ -10,8 +10,10 @@ import java.net.HttpURLConnection;
import java.nio.channels.ClosedByInterruptException;
import us.shandian.giga.get.DownloadMission.Block;
import us.shandian.giga.get.DownloadMission.HttpError;
import static org.schabi.newpipe.BuildConfig.DEBUG;
import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_FORBIDDEN;
/**
@@ -19,7 +21,7 @@ import static org.schabi.newpipe.BuildConfig.DEBUG;
* an error occurs or the process is stopped.
*/
public class DownloadRunnable extends Thread {
private static final String TAG = DownloadRunnable.class.getSimpleName();
private static final String TAG = "DownloadRunnable";
private final DownloadMission mMission;
private final int mId;
@@ -41,13 +43,7 @@ public class DownloadRunnable extends Thread {
public void run() {
boolean retry = false;
Block block = null;
int retryCount = 0;
if (DEBUG) {
Log.d(TAG, mId + ":recovered: " + mMission.recovered);
}
SharpStream f;
try {
@@ -84,13 +80,14 @@ public class DownloadRunnable extends Thread {
}
try {
mConn = mMission.openConnection(mId, start, end);
mConn = mMission.openConnection(false, start, end);
mMission.establishConnection(mId, mConn);
// check if the download can be resumed
if (mConn.getResponseCode() == 416) {
if (block.done > 0) {
// try again from the start (of the block)
mMission.notifyProgress(-block.done);
block.done = 0;
retry = true;
mConn.disconnect();
@@ -118,7 +115,7 @@ public class DownloadRunnable extends Thread {
int len;
// use always start <= end
// fixes a deadlock in DownloadRunnable because youtube is sending one byte alone after downloading 26MiB exactly
// fixes a deadlock because in some videos, youtube is sending one byte alone
while (start <= end && mMission.running && (len = is.read(buf, 0, buf.length)) != -1) {
f.write(buf, 0, len);
start += len;
@@ -133,6 +130,17 @@ public class DownloadRunnable extends Thread {
} catch (Exception e) {
if (!mMission.running || e instanceof ClosedByInterruptException) break;
if (e instanceof HttpError && ((HttpError) e).statusCode == ERROR_HTTP_FORBIDDEN) {
// for youtube streams. The url has expired, recover
f.close();
if (mId == 1) {
// only the first thread will execute the recovery procedure
mMission.doRecover(ERROR_HTTP_FORBIDDEN);
}
return;
}
if (retryCount++ >= mMission.maxRetry) {
mMission.notifyError(e);
break;
@@ -144,11 +152,7 @@ public class DownloadRunnable extends Thread {
}
}
try {
f.close();
} catch (Exception err) {
// ¿ejected media storage? ¿file deleted? ¿storage ran out of space?
}
f.close();
if (DEBUG) {
Log.d(TAG, "thread " + mId + " exited from main download loop");

View File

@@ -1,8 +1,9 @@
package us.shandian.giga.get;
import androidx.annotation.NonNull;
import android.util.Log;
import androidx.annotation.NonNull;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
@@ -10,9 +11,11 @@ import java.io.InputStream;
import java.net.HttpURLConnection;
import java.nio.channels.ClosedByInterruptException;
import us.shandian.giga.get.DownloadMission.HttpError;
import us.shandian.giga.util.Utility;
import static org.schabi.newpipe.BuildConfig.DEBUG;
import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_FORBIDDEN;
/**
* Single-threaded fallback mode
@@ -33,7 +36,11 @@ public class DownloadRunnableFallback extends Thread {
private void dispose() {
try {
if (mIs != null) mIs.close();
try {
if (mIs != null) mIs.close();
} finally {
mConn.disconnect();
}
} catch (IOException e) {
// nothing to do
}
@@ -41,22 +48,10 @@ public class DownloadRunnableFallback extends Thread {
if (mF != null) mF.close();
}
private long loadPosition() {
synchronized (mMission.LOCK) {
return mMission.fallbackResumeOffset;
}
}
private void savePosition(long position) {
synchronized (mMission.LOCK) {
mMission.fallbackResumeOffset = position;
}
}
@Override
public void run() {
boolean done;
long start = loadPosition();
long start = mMission.fallbackResumeOffset;
if (DEBUG && !mMission.unknownLength && start > 0) {
Log.i(TAG, "Resuming a single-thread download at " + start);
@@ -66,11 +61,18 @@ public class DownloadRunnableFallback extends Thread {
long rangeStart = (mMission.unknownLength || start < 1) ? -1 : start;
int mId = 1;
mConn = mMission.openConnection(mId, rangeStart, -1);
mConn = mMission.openConnection(false, rangeStart, -1);
if (mRetryCount == 0 && rangeStart == -1) {
// workaround: bypass android connection pool
mConn.setRequestProperty("Range", "bytes=0-");
}
mMission.establishConnection(mId, mConn);
// check if the download can be resumed
if (mConn.getResponseCode() == 416 && start > 0) {
mMission.notifyProgress(-start);
start = 0;
mRetryCount--;
throw new DownloadMission.HttpError(416);
@@ -80,12 +82,17 @@ public class DownloadRunnableFallback extends Thread {
if (!mMission.unknownLength)
mMission.unknownLength = Utility.getContentLength(mConn) == -1;
if (mMission.unknownLength || mConn.getResponseCode() == 200) {
// restart amount of bytes downloaded
mMission.done = mMission.offsets[mMission.current] - mMission.offsets[0];
}
mF = mMission.storage.getStream();
mF.seek(mMission.offsets[mMission.current] + start);
mIs = mConn.getInputStream();
byte[] buf = new byte[64 * 1024];
byte[] buf = new byte[DownloadMission.BUFFER_SIZE];
int len = 0;
while (mMission.running && (len = mIs.read(buf, 0, buf.length)) != -1) {
@@ -94,15 +101,24 @@ public class DownloadRunnableFallback extends Thread {
mMission.notifyProgress(len);
}
dispose();
// if thread goes interrupted check if the last part is written. This avoid re-download the whole file
done = len == -1;
} catch (Exception e) {
dispose();
savePosition(start);
mMission.fallbackResumeOffset = start;
if (!mMission.running || e instanceof ClosedByInterruptException) return;
if (e instanceof HttpError && ((HttpError) e).statusCode == ERROR_HTTP_FORBIDDEN) {
// for youtube streams. The url has expired, recover
dispose();
mMission.doRecover(ERROR_HTTP_FORBIDDEN);
return;
}
if (mRetryCount++ >= mMission.maxRetry) {
mMission.notifyError(e);
return;
@@ -116,12 +132,10 @@ public class DownloadRunnableFallback extends Thread {
return;
}
dispose();
if (done) {
mMission.notifyFinished();
} else {
savePosition(start);
mMission.fallbackResumeOffset = start;
}
}

View File

@@ -2,17 +2,17 @@ package us.shandian.giga.get;
import androidx.annotation.NonNull;
public class FinishedMission extends Mission {
public class FinishedMission extends Mission {
public FinishedMission() {
}
public FinishedMission(@NonNull DownloadMission mission) {
source = mission.source;
length = mission.length;// ¿or mission.done?
length = mission.length;
timestamp = mission.timestamp;
kind = mission.kind;
storage = mission.storage;
}
}

View File

@@ -0,0 +1,115 @@
package us.shandian.giga.get;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.Stream;
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
import org.schabi.newpipe.extractor.stream.VideoStream;
import java.io.Serializable;
public class MissionRecoveryInfo implements Serializable, Parcelable {
private static final long serialVersionUID = 0L;
MediaFormat format;
String desired;
boolean desired2;
int desiredBitrate;
byte kind;
String validateCondition = null;
public MissionRecoveryInfo(@NonNull Stream stream) {
if (stream instanceof AudioStream) {
desiredBitrate = ((AudioStream) stream).average_bitrate;
desired2 = false;
kind = 'a';
} else if (stream instanceof VideoStream) {
desired = ((VideoStream) stream).getResolution();
desired2 = ((VideoStream) stream).isVideoOnly();
kind = 'v';
} else if (stream instanceof SubtitlesStream) {
desired = ((SubtitlesStream) stream).getLanguageTag();
desired2 = ((SubtitlesStream) stream).isAutoGenerated();
kind = 's';
} else {
throw new RuntimeException("Unknown stream kind");
}
format = stream.getFormat();
if (format == null) throw new NullPointerException("Stream format cannot be null");
}
@NonNull
@Override
public String toString() {
String info;
StringBuilder str = new StringBuilder();
str.append("{type=");
switch (kind) {
case 'a':
str.append("audio");
info = "bitrate=" + desiredBitrate;
break;
case 'v':
str.append("video");
info = "quality=" + desired + " videoOnly=" + desired2;
break;
case 's':
str.append("subtitles");
info = "language=" + desired + " autoGenerated=" + desired2;
break;
default:
info = "";
str.append("other");
}
str.append(" format=")
.append(format.getName())
.append(' ')
.append(info)
.append('}');
return str.toString();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeInt(this.format.ordinal());
parcel.writeString(this.desired);
parcel.writeInt(this.desired2 ? 0x01 : 0x00);
parcel.writeInt(this.desiredBitrate);
parcel.writeByte(this.kind);
parcel.writeString(this.validateCondition);
}
private MissionRecoveryInfo(Parcel parcel) {
this.format = MediaFormat.values()[parcel.readInt()];
this.desired = parcel.readString();
this.desired2 = parcel.readInt() != 0x00;
this.desiredBitrate = parcel.readInt();
this.kind = parcel.readByte();
this.validateCondition = parcel.readString();
}
public static final Parcelable.Creator<MissionRecoveryInfo> CREATOR = new Parcelable.Creator<MissionRecoveryInfo>() {
@Override
public MissionRecoveryInfo createFromParcel(Parcel source) {
return new MissionRecoveryInfo(source);
}
@Override
public MissionRecoveryInfo[] newArray(int size) {
return new MissionRecoveryInfo[size];
}
};
}

View File

@@ -5,21 +5,23 @@ import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
public class ChunkFileInputStream extends SharpStream {
private static final int REPORT_INTERVAL = 256 * 1024;
private SharpStream source;
private final long offset;
private final long length;
private long position;
public ChunkFileInputStream(SharpStream target, long start) throws IOException {
this(target, start, target.length());
}
private long progressReport;
private final ProgressReport onProgress;
public ChunkFileInputStream(SharpStream target, long start, long end) throws IOException {
public ChunkFileInputStream(SharpStream target, long start, long end, ProgressReport callback) throws IOException {
source = target;
offset = start;
length = end - start;
position = 0;
onProgress = callback;
progressReport = REPORT_INTERVAL;
if (length < 1) {
source.close();
@@ -60,12 +62,12 @@ public class ChunkFileInputStream extends SharpStream {
}
@Override
public int read(byte b[]) throws IOException {
public int read(byte[] b) throws IOException {
return read(b, 0, b.length);
}
@Override
public int read(byte b[], int off, int len) throws IOException {
public int read(byte[] b, int off, int len) throws IOException {
if ((position + len) > length) {
len = (int) (length - position);
}
@@ -76,6 +78,11 @@ public class ChunkFileInputStream extends SharpStream {
int res = source.read(b, off, len);
position += res;
if (onProgress != null && position > progressReport) {
onProgress.report(position);
progressReport = position + REPORT_INTERVAL;
}
return res;
}

View File

@@ -174,12 +174,12 @@ public class CircularFileWriter extends SharpStream {
}
@Override
public void write(byte b[]) throws IOException {
public void write(byte[] b) throws IOException {
write(b, 0, b.length);
}
@Override
public void write(byte b[], int off, int len) throws IOException {
public void write(byte[] b, int off, int len) throws IOException {
if (len == 0) {
return;
}
@@ -261,7 +261,7 @@ public class CircularFileWriter extends SharpStream {
@Override
public void rewind() throws IOException {
if (onProgress != null) {
onProgress.report(-out.length - aux.length);// rollback the whole progress
onProgress.report(0);// rollback the whole progress
}
seek(0);
@@ -357,16 +357,6 @@ public class CircularFileWriter extends SharpStream {
long check();
}
public interface ProgressReport {
/**
* Report the size of the new file
*
* @param progress the new size
*/
void report(long progress);
}
public interface WriteErrorHandle {
/**
@@ -381,10 +371,10 @@ public class CircularFileWriter extends SharpStream {
class BufferedFile {
protected final SharpStream target;
final SharpStream target;
private long offset;
protected long length;
long length;
private byte[] queue = new byte[QUEUE_BUFFER_SIZE];
private int queueSize;
@@ -397,16 +387,16 @@ public class CircularFileWriter extends SharpStream {
this.target = target;
}
protected long getOffset() {
long getOffset() {
return offset + queueSize;// absolute offset in the file
}
protected void close() {
void close() {
queue = null;
target.close();
}
protected void write(byte b[], int off, int len) throws IOException {
void write(byte[] b, int off, int len) throws IOException {
while (len > 0) {
// if the queue is full, the method available() will flush the queue
int read = Math.min(available(), len);
@@ -436,7 +426,7 @@ public class CircularFileWriter extends SharpStream {
target.seek(0);
}
protected int available() throws IOException {
int available() throws IOException {
if (queueSize >= queue.length) {
flush();
return queue.length;
@@ -451,7 +441,7 @@ public class CircularFileWriter extends SharpStream {
target.seek(0);
}
protected void seek(long absoluteOffset) throws IOException {
void seek(long absoluteOffset) throws IOException {
if (absoluteOffset == offset) {
return;// nothing to do
}

View File

@@ -0,0 +1,11 @@
package us.shandian.giga.io;
public interface ProgressReport {
/**
* Report the size of the new file
*
* @param progress the new size
*/
void report(long progress);
}

View File

@@ -0,0 +1,44 @@
package us.shandian.giga.postprocessing;
import androidx.annotation.NonNull;
import org.schabi.newpipe.streams.OggFromWebMWriter;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
import java.nio.ByteBuffer;
class OggFromWebmDemuxer extends Postprocessing {
OggFromWebmDemuxer() {
super(true, true, ALGORITHM_OGG_FROM_WEBM_DEMUXER);
}
@Override
boolean test(SharpStream... sources) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(4);
sources[0].read(buffer.array());
// youtube uses WebM as container, but the file extension (format suffix) is "*.opus"
// check if the file is a webm/mkv file before proceed
switch (buffer.getInt()) {
case 0x1a45dfa3:
return true;// webm/mkv
case 0x4F676753:
return false;// ogg
}
throw new UnsupportedOperationException("file not recognized, failed to demux the audio stream");
}
@Override
int process(SharpStream out, @NonNull SharpStream... sources) throws IOException {
OggFromWebMWriter demuxer = new OggFromWebMWriter(sources[0], out);
demuxer.parseSource();
demuxer.selectTrack(0);
demuxer.build();
return OK_RESULT;
}
}

View File

@@ -1,9 +1,9 @@
package us.shandian.giga.postprocessing;
import android.os.Message;
import androidx.annotation.NonNull;
import android.util.Log;
import androidx.annotation.NonNull;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.File;
@@ -14,11 +14,11 @@ import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.io.ChunkFileInputStream;
import us.shandian.giga.io.CircularFileWriter;
import us.shandian.giga.io.CircularFileWriter.OffsetChecker;
import us.shandian.giga.service.DownloadManagerService;
import us.shandian.giga.io.ProgressReport;
import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING;
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING;
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD;
import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION;
public abstract class Postprocessing implements Serializable {
@@ -28,6 +28,7 @@ public abstract class Postprocessing implements Serializable {
public transient static final String ALGORITHM_WEBM_MUXER = "webm";
public transient static final String ALGORITHM_MP4_FROM_DASH_MUXER = "mp4D-mp4";
public transient static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a";
public transient static final String ALGORITHM_OGG_FROM_WEBM_DEMUXER = "webm-ogg-d";
public static Postprocessing getAlgorithm(@NonNull String algorithmName, String[] args) {
Postprocessing instance;
@@ -45,6 +46,9 @@ public abstract class Postprocessing implements Serializable {
case ALGORITHM_M4A_NO_DASH:
instance = new M4aNoDash();
break;
case ALGORITHM_OGG_FROM_WEBM_DEMUXER:
instance = new OggFromWebmDemuxer();
break;
/*case "example-algorithm":
instance = new ExampleAlgorithm();*/
default:
@@ -59,22 +63,22 @@ public abstract class Postprocessing implements Serializable {
* Get a boolean value that indicate if the given algorithm work on the same
* file
*/
public final boolean worksOnSameFile;
public boolean worksOnSameFile;
/**
* Indicates whether the selected algorithm needs space reserved at the beginning of the file
*/
public final boolean reserveSpace;
public boolean reserveSpace;
/**
* Gets the given algorithm short name
*/
private final String name;
private String name;
private String[] args;
protected transient DownloadMission mission;
private transient DownloadMission mission;
private File tempFile;
@@ -105,16 +109,24 @@ public abstract class Postprocessing implements Serializable {
long finalLength = -1;
mission.done = 0;
mission.length = mission.storage.length();
long length = mission.storage.length() - mission.offsets[0];
mission.length = length > mission.nearLength ? length : mission.nearLength;
final ProgressReport readProgress = (long position) -> {
position -= mission.offsets[0];
if (position > mission.done) mission.done = position;
};
if (worksOnSameFile) {
ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length];
try {
int i = 0;
for (; i < sources.length - 1; i++) {
sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i], mission.offsets[i + 1]);
for (int i = 0, j = 1; i < sources.length; i++, j++) {
SharpStream source = mission.storage.getStream();
long end = j < sources.length ? mission.offsets[j] : source.length();
sources[i] = new ChunkFileInputStream(source, mission.offsets[i], end, readProgress);
}
sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i]);
if (test(sources)) {
for (SharpStream source : sources) source.rewind();
@@ -136,7 +148,7 @@ public abstract class Postprocessing implements Serializable {
};
out = new CircularFileWriter(mission.storage.getStream(), tempFile, checker);
out.onProgress = this::progressReport;
out.onProgress = (long position) -> mission.done = position;
out.onWriteError = (err) -> {
mission.psState = 3;
@@ -183,11 +195,10 @@ public abstract class Postprocessing implements Serializable {
if (result == OK_RESULT) {
if (finalLength != -1) {
mission.done = finalLength;
mission.length = finalLength;
}
} else {
mission.errCode = ERROR_UNKNOWN_EXCEPTION;
mission.errCode = ERROR_POSTPROCESSING;
mission.errObject = new RuntimeException("post-processing algorithm returned " + result);
}
@@ -212,7 +223,7 @@ public abstract class Postprocessing implements Serializable {
*
* @param out output stream
* @param sources files to be processed
* @return a error code, 0 means the operation was successful
* @return an error code, {@code OK_RESULT} means the operation was successful
* @throws IOException if an I/O error occurs.
*/
abstract int process(SharpStream out, SharpStream... sources) throws IOException;
@@ -225,23 +236,12 @@ public abstract class Postprocessing implements Serializable {
return args[index];
}
private void progressReport(long done) {
mission.done = done;
if (mission.length < mission.done) mission.length = mission.done;
Message m = new Message();
m.what = DownloadManagerService.MESSAGE_PROGRESS;
m.obj = mission;
mission.mHandler.sendMessage(m);
}
@NonNull
@Override
public String toString() {
StringBuilder str = new StringBuilder();
str.append("name=").append(name).append('[');
str.append("{ name=").append(name).append('[');
if (args != null) {
for (String arg : args) {
@@ -251,6 +251,6 @@ public abstract class Postprocessing implements Serializable {
str.delete(0, 1);
}
return str.append(']').toString();
return str.append("] }").toString();
}
}

View File

@@ -2,13 +2,11 @@ package us.shandian.giga.service;
import android.content.Context;
import android.os.Handler;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.DiffUtil;
import android.util.Log;
import android.widget.Toast;
import org.schabi.newpipe.R;
import java.io.File;
import java.io.IOException;
@@ -37,6 +35,7 @@ public class DownloadManager {
public static final String TAG_AUDIO = "audio";
public static final String TAG_VIDEO = "video";
private static final String DOWNLOADS_METADATA_FOLDER = "pending_downloads";
private final FinishedMissionStore mFinishedMissionStore;
@@ -74,25 +73,35 @@ public class DownloadManager {
mMissionsFinished = loadFinishedMissions();
mPendingMissionsDir = getPendingDir(context);
if (!Utility.mkdir(mPendingMissionsDir, false)) {
throw new RuntimeException("failed to create pending_downloads in data directory");
}
loadPendingMissions(context);
}
private static File getPendingDir(@NonNull Context context) {
//File dir = new File(ContextCompat.getDataDir(context), "pending_downloads");
File dir = context.getExternalFilesDir("pending_downloads");
File dir = context.getExternalFilesDir(DOWNLOADS_METADATA_FOLDER);
if (testDir(dir)) return dir;
if (dir == null) {
// One of the following paths are not accessible ¿unmounted internal memory?
// /storage/emulated/0/Android/data/org.schabi.newpipe[.debug]/pending_downloads
// /sdcard/Android/data/org.schabi.newpipe[.debug]/pending_downloads
Log.w(TAG, "path to pending downloads are not accessible");
dir = new File(context.getFilesDir(), DOWNLOADS_METADATA_FOLDER);
if (testDir(dir)) return dir;
throw new RuntimeException("path to pending downloads are not accessible");
}
private static boolean testDir(@Nullable File dir) {
if (dir == null) return false;
try {
if (!Utility.mkdir(dir, false)) {
Log.e(TAG, "testDir() cannot create the directory in path: " + dir.getAbsolutePath());
return false;
}
File tmp = new File(dir, ".tmp");
if (!tmp.createNewFile()) return false;
return tmp.delete();// if the file was created, SHOULD BE deleted too
} catch (Exception e) {
Log.e(TAG, "testDir() failed: " + dir.getAbsolutePath(), e);
return false;
}
return dir;
}
/**
@@ -132,6 +141,7 @@ public class DownloadManager {
for (File sub : subs) {
if (!sub.isFile()) continue;
if (sub.getName().equals(".tmp")) continue;
DownloadMission mis = Utility.readFromFile(sub);
if (mis == null || mis.isFinished()) {
@@ -140,6 +150,8 @@ public class DownloadManager {
continue;
}
mis.threads = new Thread[0];
boolean exists;
try {
mis.storage = StoredFileHelper.deserialize(mis.storage, ctx);
@@ -158,8 +170,6 @@ public class DownloadManager {
// is Java IO (avoid showing the "Save as..." dialog)
if (exists && mis.storage.isDirect() && !mis.storage.delete())
Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath());
exists = true;
}
mis.psState = 0;
@@ -177,7 +187,6 @@ public class DownloadManager {
mis.psAlgorithm.setTemporalDir(pickAvailableTemporalDir(ctx));
}
mis.recovered = exists;
mis.metadata = sub;
mis.maxRetry = mPrefMaxRetry;
mis.mHandler = mHandler;
@@ -232,7 +241,6 @@ public class DownloadManager {
boolean start = !mPrefQueueLimit || getRunningMissionsCount() < 1;
if (canDownloadInCurrentNetwork() && start) {
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
mission.start();
}
}
@@ -241,7 +249,6 @@ public class DownloadManager {
public void resumeMission(DownloadMission mission) {
if (!mission.running) {
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
mission.start();
}
}
@@ -250,7 +257,6 @@ public class DownloadManager {
if (mission.running) {
mission.setEnqueued(false);
mission.pause();
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED);
}
}
@@ -263,7 +269,6 @@ public class DownloadManager {
mFinishedMissionStore.deleteMission(mission);
}
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED);
mission.delete();
}
}
@@ -280,7 +285,6 @@ public class DownloadManager {
mFinishedMissionStore.deleteMission(mission);
}
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED);
mission.storage = null;
mission.delete();
}
@@ -363,35 +367,29 @@ public class DownloadManager {
}
public void pauseAllMissions(boolean force) {
boolean flag = false;
synchronized (this) {
for (DownloadMission mission : mMissionsPending) {
if (!mission.running || mission.isPsRunning() || mission.isFinished()) continue;
if (force) mission.threads = null;// avoid waiting for threads
if (force) {
// avoid waiting for threads
mission.init = null;
mission.threads = new Thread[0];
}
mission.pause();
flag = true;
}
}
if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED);
}
public void startAllMissions() {
boolean flag = false;
synchronized (this) {
for (DownloadMission mission : mMissionsPending) {
if (mission.running || mission.isCorrupt()) continue;
flag = true;
mission.start();
}
}
if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
}
/**
@@ -472,28 +470,18 @@ public class DownloadManager {
boolean isMetered = mPrefMeteredDownloads && mLastNetworkStatus == NetworkState.MeteredOperating;
int running = 0;
int paused = 0;
synchronized (this) {
for (DownloadMission mission : mMissionsPending) {
if (mission.isCorrupt() || mission.isPsRunning()) continue;
if (mission.running && isMetered) {
paused++;
mission.pause();
} else if (!mission.running && !isMetered && mission.enqueued) {
running++;
mission.start();
if (mPrefQueueLimit) break;
}
}
}
if (running > 0) {
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
return;
}
if (paused > 0) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED);
}
void updateMaximumAttempts() {
@@ -502,22 +490,6 @@ public class DownloadManager {
}
}
/**
* Fast check for pending downloads. If exists, the user will be notified
* TODO: call this method in somewhere
*
* @param context the application context
*/
public static void notifyUserPendingDownloads(Context context) {
int pending = getPendingDir(context).list().length;
if (pending < 1) return;
Toast.makeText(context, context.getString(
R.string.msg_pending_downloads,
String.valueOf(pending)
), Toast.LENGTH_LONG).show();
}
public MissionState checkForExistingMission(StoredFileHelper storage) {
synchronized (this) {
DownloadMission pending = getPendingMission(storage);

View File

@@ -23,15 +23,17 @@ import android.os.Handler;
import android.os.Handler.Callback;
import android.os.IBinder;
import android.os.Message;
import android.os.Parcelable;
import android.preference.PreferenceManager;
import android.util.Log;
import android.util.SparseArray;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationCompat.Builder;
import android.util.Log;
import android.util.SparseArray;
import android.widget.Toast;
import org.schabi.newpipe.R;
import org.schabi.newpipe.download.DownloadActivity;
@@ -42,6 +44,7 @@ import java.io.IOException;
import java.util.ArrayList;
import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.get.MissionRecoveryInfo;
import us.shandian.giga.io.StoredDirectoryHelper;
import us.shandian.giga.io.StoredFileHelper;
import us.shandian.giga.postprocessing.Postprocessing;
@@ -54,11 +57,11 @@ public class DownloadManagerService extends Service {
private static final String TAG = "DownloadManagerService";
public static final int MESSAGE_RUNNING = 0;
public static final int MESSAGE_PAUSED = 1;
public static final int MESSAGE_FINISHED = 2;
public static final int MESSAGE_PROGRESS = 3;
public static final int MESSAGE_ERROR = 4;
public static final int MESSAGE_DELETED = 5;
public static final int MESSAGE_ERROR = 3;
public static final int MESSAGE_DELETED = 4;
private static final int FOREGROUND_NOTIFICATION_ID = 1000;
private static final int DOWNLOADS_NOTIFICATION_ID = 1001;
@@ -73,6 +76,7 @@ public class DownloadManagerService extends Service {
private static final String EXTRA_PATH = "DownloadManagerService.extra.storagePath";
private static final String EXTRA_PARENT_PATH = "DownloadManagerService.extra.storageParentPath";
private static final String EXTRA_STORAGE_TAG = "DownloadManagerService.extra.storageTag";
private static final String EXTRA_RECOVERY_INFO = "DownloadManagerService.extra.recoveryInfo";
private static final String ACTION_RESET_DOWNLOAD_FINISHED = APPLICATION_ID + ".reset_download_finished";
private static final String ACTION_OPEN_DOWNLOADS_FINISHED = APPLICATION_ID + ".open_downloads_finished";
@@ -212,9 +216,11 @@ public class DownloadManagerService extends Service {
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
);
}
return START_NOT_STICKY;
}
}
return START_NOT_STICKY;
return START_STICKY;
}
@Override
@@ -245,6 +251,7 @@ public class DownloadManagerService extends Service {
if (icDownloadFailed != null) icDownloadFailed.recycle();
if (icLauncher != null) icLauncher.recycle();
mHandler = null;
mManager.pauseAllMissions(true);
}
@@ -269,6 +276,8 @@ public class DownloadManagerService extends Service {
}
private boolean handleMessage(@NonNull Message msg) {
if (mHandler == null) return true;
DownloadMission mission = (DownloadMission) msg.obj;
switch (msg.what) {
@@ -279,7 +288,7 @@ public class DownloadManagerService extends Service {
handleConnectivityState(false);
updateForegroundState(mManager.runMissions());
break;
case MESSAGE_PROGRESS:
case MESSAGE_RUNNING:
updateForegroundState(true);
break;
case MESSAGE_ERROR:
@@ -295,11 +304,8 @@ public class DownloadManagerService extends Service {
if (msg.what != MESSAGE_ERROR)
mFailedDownloads.delete(mFailedDownloads.indexOfValue(mission));
synchronized (mEchoObservers) {
for (Callback observer : mEchoObservers) {
observer.handleMessage(msg);
}
}
for (Callback observer : mEchoObservers)
observer.handleMessage(msg);
return true;
}
@@ -364,18 +370,20 @@ public class DownloadManagerService extends Service {
/**
* Start a new download mission
*
* @param context the activity context
* @param urls the list of urls to download
* @param storage where the file is saved
* @param kind type of file (a: audio v: video s: subtitle ?: file-extension defined)
* @param threads the number of threads maximal used to download chunks of the file.
* @param psName the name of the required post-processing algorithm, or {@code null} to ignore.
* @param source source url of the resource
* @param psArgs the arguments for the post-processing algorithm.
* @param nearLength the approximated final length of the file
* @param context the activity context
* @param urls array of urls to download
* @param storage where the file is saved
* @param kind type of file (a: audio v: video s: subtitle ?: file-extension defined)
* @param threads the number of threads maximal used to download chunks of the file.
* @param psName the name of the required post-processing algorithm, or {@code null} to ignore.
* @param source source url of the resource
* @param psArgs the arguments for the post-processing algorithm.
* @param nearLength the approximated final length of the file
* @param recoveryInfo array of MissionRecoveryInfo, in case is required recover the download
*/
public static void startMission(Context context, String[] urls, StoredFileHelper storage, char kind,
int threads, String source, String psName, String[] psArgs, long nearLength) {
public static void startMission(Context context, String[] urls, StoredFileHelper storage,
char kind, int threads, String source, String psName,
String[] psArgs, long nearLength, MissionRecoveryInfo[] recoveryInfo) {
Intent intent = new Intent(context, DownloadManagerService.class);
intent.setAction(Intent.ACTION_RUN);
intent.putExtra(EXTRA_URLS, urls);
@@ -385,6 +393,7 @@ public class DownloadManagerService extends Service {
intent.putExtra(EXTRA_POSTPROCESSING_NAME, psName);
intent.putExtra(EXTRA_POSTPROCESSING_ARGS, psArgs);
intent.putExtra(EXTRA_NEAR_LENGTH, nearLength);
intent.putExtra(EXTRA_RECOVERY_INFO, recoveryInfo);
intent.putExtra(EXTRA_PARENT_PATH, storage.getParentUri());
intent.putExtra(EXTRA_PATH, storage.getUri());
@@ -404,6 +413,7 @@ public class DownloadManagerService extends Service {
String source = intent.getStringExtra(EXTRA_SOURCE);
long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0);
String tag = intent.getStringExtra(EXTRA_STORAGE_TAG);
Parcelable[] parcelRecovery = intent.getParcelableArrayExtra(EXTRA_RECOVERY_INFO);
StoredFileHelper storage;
try {
@@ -418,10 +428,15 @@ public class DownloadManagerService extends Service {
else
ps = Postprocessing.getAlgorithm(psName, psArgs);
MissionRecoveryInfo[] recovery = new MissionRecoveryInfo[parcelRecovery.length];
for (int i = 0; i < parcelRecovery.length; i++)
recovery[i] = (MissionRecoveryInfo) parcelRecovery[i];
final DownloadMission mission = new DownloadMission(urls, storage, kind, ps);
mission.threadCount = threads;
mission.source = source;
mission.nearLength = nearLength;
mission.recoveryInfo = recovery;
if (ps != null)
ps.setTemporalDir(DownloadManager.pickAvailableTemporalDir(this));
@@ -509,16 +524,6 @@ public class DownloadManagerService extends Service {
return PendingIntent.getService(this, intent.hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
private void manageObservers(Callback handler, boolean add) {
synchronized (mEchoObservers) {
if (add) {
mEchoObservers.add(handler);
} else {
mEchoObservers.remove(handler);
}
}
}
private void manageLock(boolean acquire) {
if (acquire == mLockAcquired) return;
@@ -591,11 +596,11 @@ public class DownloadManagerService extends Service {
}
public void addMissionEventListener(Callback handler) {
manageObservers(handler, true);
mEchoObservers.add(handler);
}
public void removeMissionEventListener(Callback handler) {
manageObservers(handler, false);
mEchoObservers.remove(handler);
}
public void clearDownloadNotifications() {

View File

@@ -5,21 +5,12 @@ import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.content.FileProvider;
import androidx.core.view.ViewCompat;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.Adapter;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import android.util.Log;
import android.util.SparseArray;
import android.view.HapticFeedbackConstants;
@@ -34,8 +25,22 @@ import android.widget.PopupMenu;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.FileProvider;
import androidx.core.view.ViewCompat;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.Adapter;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import com.google.android.material.snackbar.Snackbar;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.NavigationHelper;
@@ -44,11 +49,12 @@ import java.io.File;
import java.lang.ref.WeakReference;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.get.FinishedMission;
import us.shandian.giga.get.Mission;
import us.shandian.giga.get.MissionRecoveryInfo;
import us.shandian.giga.io.StoredFileHelper;
import us.shandian.giga.service.DownloadManager;
import us.shandian.giga.service.DownloadManagerService;
@@ -62,7 +68,6 @@ import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;
import static us.shandian.giga.get.DownloadMission.ERROR_CONNECT_HOST;
import static us.shandian.giga.get.DownloadMission.ERROR_FILE_CREATION;
import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_NO_CONTENT;
import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_UNSUPPORTED_RANGE;
import static us.shandian.giga.get.DownloadMission.ERROR_INSUFFICIENT_STORAGE;
import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING;
import static us.shandian.giga.get.DownloadMission.ERROR_PATH_CREATION;
@@ -71,6 +76,7 @@ import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING;
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD;
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_STOPPED;
import static us.shandian.giga.get.DownloadMission.ERROR_PROGRESS_LOST;
import static us.shandian.giga.get.DownloadMission.ERROR_RESOURCE_GONE;
import static us.shandian.giga.get.DownloadMission.ERROR_SSL_EXCEPTION;
import static us.shandian.giga.get.DownloadMission.ERROR_TIMEOUT;
import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION;
@@ -81,6 +87,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
private static final String TAG = "MissionAdapter";
private static final String UNDEFINED_PROGRESS = "--.-%";
private static final String DEFAULT_MIME_TYPE = "*/*";
private static final String UNDEFINED_ETA = "--:--";
static {
@@ -101,11 +108,16 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
private MenuItem mPauseButton;
private View mEmptyMessage;
private RecoverHelper mRecover;
private View mView;
private ArrayList<Mission> mHidden;
private Snackbar mSnackbar;
public MissionAdapter(Context context, @NonNull DownloadManager downloadManager, View emptyMessage) {
private final Runnable rUpdater = this::updater;
private final Runnable rDelete = this::deleteFinishedDownloads;
public MissionAdapter(Context context, @NonNull DownloadManager downloadManager, View emptyMessage, View root) {
mContext = context;
mDownloadManager = downloadManager;
mDeleter = null;
mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mLayout = R.layout.mission_item;
@@ -116,7 +128,14 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
mIterator = downloadManager.getIterator();
mDeleter = new Deleter(root, mContext, this, mDownloadManager, mIterator, mHandler);
mView = root;
mHidden = new ArrayList<>();
checkEmptyMessageVisibility();
onResume();
}
@Override
@@ -141,17 +160,13 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
if (h.item.mission instanceof DownloadMission) {
mPendingDownloadsItems.remove(h);
if (mPendingDownloadsItems.size() < 1) {
setAutoRefresh(false);
checkMasterButtonsVisibility();
}
}
h.popupMenu.dismiss();
h.item = null;
h.lastTimeStamp = -1;
h.lastDone = -1;
h.lastCurrent = -1;
h.state = 0;
h.resetSpeedMeasure();
}
@Override
@@ -190,7 +205,6 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
h.size.setText(length);
h.pause.setTitle(mission.unknownLength ? R.string.stop : R.string.pause);
h.lastCurrent = mission.current;
updateProgress(h);
mPendingDownloadsItems.add(h);
} else {
@@ -215,40 +229,27 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
private void updateProgress(ViewHolderItem h) {
if (h == null || h.item == null || h.item.mission instanceof FinishedMission) return;
long now = System.currentTimeMillis();
DownloadMission mission = (DownloadMission) h.item.mission;
if (h.lastCurrent != mission.current) {
h.lastCurrent = mission.current;
h.lastTimeStamp = now;
h.lastDone = 0;
} else {
if (h.lastTimeStamp == -1) h.lastTimeStamp = now;
if (h.lastDone == -1) h.lastDone = mission.done;
}
long deltaTime = now - h.lastTimeStamp;
long deltaDone = mission.done - h.lastDone;
double done = mission.done;
long length = mission.getLength();
long now = System.currentTimeMillis();
boolean hasError = mission.errCode != ERROR_NOTHING;
// hide on error
// show if current resource length is not fetched
// show if length is unknown
h.progress.setMarquee(!hasError && (!mission.isInitialized() || mission.unknownLength));
h.progress.setMarquee(mission.isRecovering() || !hasError && (!mission.isInitialized() || mission.unknownLength));
float progress;
double progress;
if (mission.unknownLength) {
progress = Float.NaN;
progress = Double.NaN;
h.progress.setProgress(0f);
} else {
progress = (float) ((double) mission.done / mission.length);
if (mission.urls.length > 1 && mission.current < mission.urls.length) {
progress = (progress / mission.urls.length) + ((float) mission.current / mission.urls.length);
}
progress = done / length;
}
if (hasError) {
h.progress.setProgress(isNotFinite(progress) ? 1f : progress);
h.progress.setProgress(isNotFinite(progress) ? 1d : progress);
h.status.setText(R.string.msg_error);
} else if (isNotFinite(progress)) {
h.status.setText(UNDEFINED_PROGRESS);
@@ -257,59 +258,78 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
h.progress.setProgress(progress);
}
long length = mission.getLength();
@StringRes int state;
String sizeStr = Utility.formatBytes(length).concat(" ");
int state;
if (mission.isPsFailed() || mission.errCode == ERROR_POSTPROCESSING_HOLD) {
state = 0;
h.size.setText(sizeStr);
return;
} else if (!mission.running) {
state = mission.enqueued ? 1 : 2;
state = mission.enqueued ? R.string.queued : R.string.paused;
} else if (mission.isPsRunning()) {
state = 3;
state = R.string.post_processing;
} else if (mission.isRecovering()) {
state = R.string.recovering;
} else {
state = 0;
}
if (state != 0) {
// update state without download speed
if (h.state != state) {
String statusStr;
h.state = state;
h.size.setText(sizeStr.concat("(").concat(mContext.getString(state)).concat(")"));
h.resetSpeedMeasure();
return;
}
switch (state) {
case 1:
statusStr = mContext.getString(R.string.queued);
break;
case 2:
statusStr = mContext.getString(R.string.paused);
break;
case 3:
statusStr = mContext.getString(R.string.post_processing);
break;
default:
statusStr = "?";
break;
}
if (h.lastTimestamp < 0) {
h.size.setText(sizeStr);
h.lastTimestamp = now;
h.lastDone = done;
return;
}
h.size.setText(Utility.formatBytes(length).concat(" (").concat(statusStr).concat(")"));
} else if (deltaDone > 0) {
h.lastTimeStamp = now;
h.lastDone = mission.done;
}
long deltaTime = now - h.lastTimestamp;
double deltaDone = done - h.lastDone;
if (h.lastDone > done) {
h.lastDone = done;
h.size.setText(sizeStr);
return;
}
if (deltaDone > 0 && deltaTime > 0) {
float speed = (deltaDone * 1000f) / deltaTime;
float speed = (float) ((deltaDone * 1000d) / deltaTime);
float averageSpeed = speed;
String speedStr = Utility.formatSpeed(speed);
String sizeStr = Utility.formatBytes(length);
if (h.lastSpeedIdx < 0) {
for (int i = 0; i < h.lastSpeed.length; i++) {
h.lastSpeed[i] = speed;
}
h.lastSpeedIdx = 0;
} else {
for (int i = 0; i < h.lastSpeed.length; i++) {
averageSpeed += h.lastSpeed[i];
}
averageSpeed /= h.lastSpeed.length + 1f;
}
h.size.setText(sizeStr.concat(" ").concat(speedStr));
String speedStr = Utility.formatSpeed(averageSpeed);
String etaStr;
h.lastTimeStamp = now;
h.lastDone = mission.done;
if (mission.unknownLength) {
etaStr = "";
} else {
long eta = (long) Math.ceil((length - done) / averageSpeed);
etaStr = Utility.formatBytes((long) done) + "/" + Utility.stringifySeconds(eta) + " ";
}
h.size.setText(sizeStr.concat(etaStr).concat(speedStr));
h.lastTimestamp = now;
h.lastDone = done;
h.lastSpeed[h.lastSpeedIdx++] = speed;
if (h.lastSpeedIdx >= h.lastSpeed.length) h.lastSpeedIdx = 0;
}
}
@@ -388,6 +408,13 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
return true;
}
private ViewHolderItem getViewHolder(Object mission) {
for (ViewHolderItem h : mPendingDownloadsItems) {
if (h.item.mission == mission) return h;
}
return null;
}
@Override
public boolean handleMessage(@NonNull Message msg) {
if (mStartButton != null && mPauseButton != null) {
@@ -395,33 +422,28 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
}
switch (msg.what) {
case DownloadManagerService.MESSAGE_PROGRESS:
case DownloadManagerService.MESSAGE_ERROR:
case DownloadManagerService.MESSAGE_FINISHED:
case DownloadManagerService.MESSAGE_DELETED:
case DownloadManagerService.MESSAGE_PAUSED:
break;
default:
return false;
}
if (msg.what == DownloadManagerService.MESSAGE_PROGRESS) {
setAutoRefresh(true);
return true;
}
ViewHolderItem h = getViewHolder(msg.obj);
if (h == null) return false;
for (ViewHolderItem h : mPendingDownloadsItems) {
if (h.item.mission != msg.obj) continue;
if (msg.what == DownloadManagerService.MESSAGE_FINISHED) {
switch (msg.what) {
case DownloadManagerService.MESSAGE_FINISHED:
case DownloadManagerService.MESSAGE_DELETED:
// DownloadManager should mark the download as finished
applyChanges();
return true;
}
updateProgress(h);
return true;
}
return false;
updateProgress(h);
return true;
}
private void showError(@NonNull DownloadMission mission) {
@@ -430,7 +452,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
switch (mission.errCode) {
case 416:
msg = R.string.error_http_requested_range_not_satisfiable;
msg = R.string.error_http_unsupported_range;
break;
case 404:
msg = R.string.error_http_not_found;
@@ -443,9 +465,6 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
case ERROR_HTTP_NO_CONTENT:
msg = R.string.error_http_no_content;
break;
case ERROR_HTTP_UNSUPPORTED_RANGE:
msg = R.string.error_http_unsupported_range;
break;
case ERROR_PATH_CREATION:
msg = R.string.error_path_creation;
break;
@@ -466,27 +485,35 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
break;
case ERROR_POSTPROCESSING:
case ERROR_POSTPROCESSING_HOLD:
showError(mission.errObject, UserAction.DOWNLOAD_POSTPROCESSING, R.string.error_postprocessing_failed);
showError(mission, UserAction.DOWNLOAD_POSTPROCESSING, R.string.error_postprocessing_failed);
return;
case ERROR_INSUFFICIENT_STORAGE:
msg = R.string.error_insufficient_storage;
break;
case ERROR_UNKNOWN_EXCEPTION:
showError(mission.errObject, UserAction.DOWNLOAD_FAILED, R.string.general_error);
return;
if (mission.errObject != null) {
showError(mission, UserAction.DOWNLOAD_FAILED, R.string.general_error);
return;
} else {
msg = R.string.msg_error;
break;
}
case ERROR_PROGRESS_LOST:
msg = R.string.error_progress_lost;
break;
case ERROR_TIMEOUT:
msg = R.string.error_timeout;
break;
case ERROR_RESOURCE_GONE:
msg = R.string.error_download_resource_gone;
break;
default:
if (mission.errCode >= 100 && mission.errCode < 600) {
msgEx = "HTTP " + mission.errCode;
} else if (mission.errObject == null) {
msgEx = "(not_decelerated_error_code)";
} else {
showError(mission.errObject, UserAction.DOWNLOAD_FAILED, msg);
showError(mission, UserAction.DOWNLOAD_FAILED, msg);
return;
}
break;
@@ -503,7 +530,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
if (mission.errObject != null && (mission.errCode < 100 || mission.errCode >= 600)) {
@StringRes final int mMsg = msg;
builder.setPositiveButton(R.string.error_report_title, (dialog, which) ->
showError(mission.errObject, UserAction.DOWNLOAD_FAILED, mMsg)
showError(mission, UserAction.DOWNLOAD_FAILED, mMsg)
);
}
@@ -513,19 +540,79 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
.show();
}
private void showError(Exception exception, UserAction action, @StringRes int reason) {
private void showError(DownloadMission mission, UserAction action, @StringRes int reason) {
StringBuilder request = new StringBuilder(256);
request.append(mission.source);
request.append(" [");
if (mission.recoveryInfo != null) {
for (MissionRecoveryInfo recovery : mission.recoveryInfo)
request.append(' ')
.append(recovery.toString())
.append(' ');
}
request.append("]");
String service;
try {
service = NewPipe.getServiceByUrl(mission.source).getServiceInfo().getName();
} catch (Exception e) {
service = "-";
}
ErrorActivity.reportError(
mContext,
Collections.singletonList(exception),
mission.errObject,
null,
null,
ErrorActivity.ErrorInfo.make(action, "-", "-", reason)
ErrorActivity.ErrorInfo.make(action, service, request.toString(), reason)
);
}
public void clearFinishedDownloads() {
mDownloadManager.forgetFinishedDownloads();
applyChanges();
public void clearFinishedDownloads(boolean delete) {
if (delete && mIterator.hasFinishedMissions() && mHidden.isEmpty()) {
for (int i = 0; i < mIterator.getOldListSize(); i++) {
FinishedMission mission = mIterator.getItem(i).mission instanceof FinishedMission ? (FinishedMission) mIterator.getItem(i).mission : null;
if (mission != null) {
mIterator.hide(mission);
mHidden.add(mission);
}
}
applyChanges();
String msg = String.format(mContext.getString(R.string.deleted_downloads), mHidden.size());
mSnackbar = Snackbar.make(mView, msg, Snackbar.LENGTH_INDEFINITE);
mSnackbar.setAction(R.string.undo, s -> {
Iterator<Mission> i = mHidden.iterator();
while (i.hasNext()) {
mIterator.unHide(i.next());
i.remove();
}
applyChanges();
mHandler.removeCallbacks(rDelete);
});
mSnackbar.setActionTextColor(Color.YELLOW);
mSnackbar.show();
mHandler.postDelayed(rDelete, 5000);
} else if (!delete) {
mDownloadManager.forgetFinishedDownloads();
applyChanges();
}
}
private void deleteFinishedDownloads() {
if (mSnackbar != null) mSnackbar.dismiss();
Iterator<Mission> i = mHidden.iterator();
while (i.hasNext()) {
Mission mission = i.next();
if (mission != null) {
mDownloadManager.deleteMission(mission);
mContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, mission.storage.getUri()));
}
i.remove();
}
}
private boolean handlePopupItem(@NonNull ViewHolderItem h, @NonNull MenuItem option) {
@@ -538,16 +625,10 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
switch (id) {
case R.id.start:
h.status.setText(UNDEFINED_PROGRESS);
h.state = -1;
h.size.setText(Utility.formatBytes(mission.getLength()));
mDownloadManager.resumeMission(mission);
return true;
case R.id.pause:
h.state = -1;
mDownloadManager.pauseMission(mission);
updateProgress(h);
h.lastTimeStamp = -1;
h.lastDone = -1;
return true;
case R.id.error_message_view:
showError(mission);
@@ -580,12 +661,9 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
shareFile(h.item.mission);
return true;
case R.id.delete:
if (mDeleter == null) {
mDownloadManager.deleteMission(h.item.mission);
} else {
mDeleter.append(h.item.mission);
}
mDeleter.append(h.item.mission);
applyChanges();
checkMasterButtonsVisibility();
return true;
case R.id.md5:
case R.id.sha1:
@@ -621,7 +699,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
mIterator.end();
for (ViewHolderItem item : mPendingDownloadsItems) {
item.lastTimeStamp = -1;
item.resetSpeedMeasure();
}
notifyDataSetChanged();
@@ -654,6 +732,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
public void checkMasterButtonsVisibility() {
boolean[] state = mIterator.hasValidPendingMissions();
Log.d(TAG, "checkMasterButtonsVisibility() running=" + state[0] + " paused=" + state[1]);
setButtonVisible(mPauseButton, state[0]);
setButtonVisible(mStartButton, state[1]);
}
@@ -663,86 +742,57 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
button.setVisible(visible);
}
public void ensurePausedMissions() {
public void refreshMissionItems() {
for (ViewHolderItem h : mPendingDownloadsItems) {
if (((DownloadMission) h.item.mission).running) continue;
updateProgress(h);
h.lastTimeStamp = -1;
h.lastDone = -1;
h.resetSpeedMeasure();
}
}
public void deleterDispose(boolean commitChanges) {
if (mDeleter != null) mDeleter.dispose(commitChanges);
public void onDestroy() {
mDeleter.dispose();
}
public void deleterLoad(View view) {
if (mDeleter == null)
mDeleter = new Deleter(view, mContext, this, mDownloadManager, mIterator, mHandler);
public void onResume() {
mDeleter.resume();
mHandler.post(rUpdater);
}
public void deleterResume() {
if (mDeleter != null) mDeleter.resume();
}
public void recoverMission(DownloadMission mission) {
for (ViewHolderItem h : mPendingDownloadsItems) {
if (mission != h.item.mission) continue;
mission.errObject = null;
mission.resetState(true, false, DownloadMission.ERROR_NOTHING);
h.status.setText(UNDEFINED_PROGRESS);
h.state = -1;
h.size.setText(Utility.formatBytes(mission.getLength()));
h.progress.setMarquee(true);
mDownloadManager.resumeMission(mission);
return;
}
}
private boolean mUpdaterRunning = false;
private final Runnable rUpdater = this::updater;
public void onPaused() {
setAutoRefresh(false);
mDeleter.pause();
mHandler.removeCallbacks(rUpdater);
}
private void setAutoRefresh(boolean enabled) {
if (enabled && !mUpdaterRunning) {
mUpdaterRunning = true;
updater();
} else if (!enabled && mUpdaterRunning) {
mUpdaterRunning = false;
mHandler.removeCallbacks(rUpdater);
}
public void recoverMission(DownloadMission mission) {
ViewHolderItem h = getViewHolder(mission);
if (h == null) return;
mission.errObject = null;
mission.resetState(true, false, DownloadMission.ERROR_NOTHING);
h.status.setText(UNDEFINED_PROGRESS);
h.size.setText(Utility.formatBytes(mission.getLength()));
h.progress.setMarquee(true);
mDownloadManager.resumeMission(mission);
}
private void updater() {
if (!mUpdaterRunning) return;
boolean running = false;
for (ViewHolderItem h : mPendingDownloadsItems) {
// check if the mission is running first
if (!((DownloadMission) h.item.mission).running) continue;
updateProgress(h);
running = true;
}
if (running) {
mHandler.postDelayed(rUpdater, 1000);
} else {
mUpdaterRunning = false;
}
mHandler.postDelayed(rUpdater, 1000);
}
private boolean isNotFinite(Float value) {
return Float.isNaN(value) || Float.isInfinite(value);
private boolean isNotFinite(double value) {
return Double.isNaN(value) || Double.isInfinite(value);
}
public void setRecover(@NonNull RecoverHelper callback) {
@@ -771,10 +821,11 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
MenuItem source;
MenuItem checksum;
long lastTimeStamp = -1;
long lastDone = -1;
int lastCurrent = -1;
int state = 0;
long lastTimestamp = -1;
double lastDone;
int lastSpeedIdx;
float[] lastSpeed = new float[3];
String estimatedTimeArrival = UNDEFINED_ETA;
ViewHolderItem(View view) {
super(view);
@@ -859,7 +910,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
delete.setVisible(true);
boolean flag = !mission.isPsFailed();
boolean flag = !mission.isPsFailed() && mission.urls.length > 0;
start.setVisible(flag);
queue.setVisible(flag);
}
@@ -884,6 +935,12 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
return popup;
}
private void resetSpeedMeasure() {
estimatedTimeArrival = UNDEFINED_ETA;
lastTimestamp = -1;
lastSpeedIdx = -1;
}
}
class ViewHolderHeader extends RecyclerView.ViewHolder {

View File

@@ -4,9 +4,10 @@ import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.os.Handler;
import com.google.android.material.snackbar.Snackbar;
import android.view.View;
import com.google.android.material.snackbar.Snackbar;
import org.schabi.newpipe.R;
import java.util.ArrayList;
@@ -113,7 +114,7 @@ public class Deleter {
show();
}
private void pause() {
public void pause() {
running = false;
mHandler.removeCallbacks(rNext);
mHandler.removeCallbacks(rShow);
@@ -126,13 +127,11 @@ public class Deleter {
mHandler.postDelayed(rShow, DELAY_RESUME);
}
public void dispose(boolean commitChanges) {
public void dispose() {
if (items.size() < 1) return;
pause();
if (!commitChanges) return;
for (Mission mission : items) mDownloadManager.deleteMission(mission);
items = null;
}

View File

@@ -9,6 +9,7 @@ import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
@@ -35,8 +36,8 @@ public class ProgressDrawable extends Drawable {
mForegroundColor = foreground;
}
public void setProgress(float progress) {
mProgress = progress;
public void setProgress(double progress) {
mProgress = (float) progress;
invalidateSelf();
}

View File

@@ -12,18 +12,20 @@ import android.os.Bundle;
import android.os.Environment;
import android.os.IBinder;
import android.preference.PreferenceManager;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.nononsenseapps.filepicker.Utils;
import org.schabi.newpipe.R;
@@ -72,8 +74,7 @@ public class MissionsFragment extends Fragment {
mBinder = (DownloadManagerBinder) binder;
mBinder.clearDownloadNotifications();
mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty);
mAdapter.deleterLoad(getView());
mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty, getView());
mAdapter.setRecover(MissionsFragment.this::recoverMission);
@@ -132,7 +133,7 @@ public class MissionsFragment extends Fragment {
* Added in API level 23.
*/
@Override
public void onAttach(Context context) {
public void onAttach(@NonNull Context context) {
super.onAttach(context);
// Bug: in api< 23 this is never called
@@ -147,7 +148,7 @@ public class MissionsFragment extends Fragment {
*/
@SuppressWarnings("deprecation")
@Override
public void onAttach(Activity activity) {
public void onAttach(@NonNull Activity activity) {
super.onAttach(activity);
mContext = activity;
@@ -162,7 +163,7 @@ public class MissionsFragment extends Fragment {
mBinder.removeMissionEventListener(mAdapter);
mBinder.enableNotifications(true);
mContext.unbindService(mConnection);
mAdapter.deleterDispose(true);
mAdapter.onDestroy();
mBinder = null;
mAdapter = null;
@@ -189,20 +190,20 @@ public class MissionsFragment extends Fragment {
return true;
case R.id.clear_list:
AlertDialog.Builder prompt = new AlertDialog.Builder(mContext);
prompt.setTitle(R.string.clear_finished_download);
prompt.setTitle(R.string.clear_download_history);
prompt.setMessage(R.string.confirm_prompt);
prompt.setPositiveButton(android.R.string.ok, (dialog, which) -> mAdapter.clearFinishedDownloads());
prompt.setNegativeButton(R.string.cancel, null);
// Intentionally misusing button's purpose in order to achieve good order
prompt.setNegativeButton(R.string.clear_download_history, (dialog, which) -> mAdapter.clearFinishedDownloads(false));
prompt.setPositiveButton(R.string.delete_downloaded_files, (dialog, which) -> mAdapter.clearFinishedDownloads(true));
prompt.setNeutralButton(R.string.cancel, null);
prompt.create().show();
return true;
case R.id.start_downloads:
item.setVisible(false);
mBinder.getDownloadManager().startAllMissions();
return true;
case R.id.pause_downloads:
item.setVisible(false);
mBinder.getDownloadManager().pauseAllMissions(false);
mAdapter.ensurePausedMissions();// update items view
mAdapter.refreshMissionItems();// update items view
default:
return super.onOptionsItemSelected(item);
}
@@ -271,23 +272,12 @@ public class MissionsFragment extends Fragment {
}
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
if (mAdapter != null) {
mAdapter.deleterDispose(false);
mForceUpdate = true;
mBinder.removeMissionEventListener(mAdapter);
}
}
@Override
public void onResume() {
super.onResume();
if (mAdapter != null) {
mAdapter.deleterResume();
mAdapter.onResume();
if (mForceUpdate) {
mForceUpdate = false;
@@ -303,7 +293,13 @@ public class MissionsFragment extends Fragment {
@Override
public void onPause() {
super.onPause();
if (mAdapter != null) mAdapter.onPaused();
if (mAdapter != null) {
mForceUpdate = true;
mBinder.removeMissionEventListener(mAdapter);
mAdapter.onPaused();
}
if (mBinder != null) mBinder.enableNotifications(true);
}

View File

@@ -4,13 +4,14 @@ import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.os.Build;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.ColorInt;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import android.util.Log;
import android.widget.Toast;
import org.schabi.newpipe.R;
import org.schabi.newpipe.streams.io.SharpStream;
@@ -26,6 +27,7 @@ import java.io.Serializable;
import java.net.HttpURLConnection;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Locale;
import us.shandian.giga.io.StoredFileHelper;
@@ -39,26 +41,28 @@ public class Utility {
}
public static String formatBytes(long bytes) {
Locale locale = Locale.getDefault();
if (bytes < 1024) {
return String.format("%d B", bytes);
return String.format(locale, "%d B", bytes);
} else if (bytes < 1024 * 1024) {
return String.format("%.2f kB", bytes / 1024d);
return String.format(locale, "%.2f kB", bytes / 1024d);
} else if (bytes < 1024 * 1024 * 1024) {
return String.format("%.2f MB", bytes / 1024d / 1024d);
return String.format(locale, "%.2f MB", bytes / 1024d / 1024d);
} else {
return String.format("%.2f GB", bytes / 1024d / 1024d / 1024d);
return String.format(locale, "%.2f GB", bytes / 1024d / 1024d / 1024d);
}
}
public static String formatSpeed(float speed) {
public static String formatSpeed(double speed) {
Locale locale = Locale.getDefault();
if (speed < 1024) {
return String.format("%.2f B/s", speed);
return String.format(locale, "%.2f B/s", speed);
} else if (speed < 1024 * 1024) {
return String.format("%.2f kB/s", speed / 1024);
return String.format(locale, "%.2f kB/s", speed / 1024);
} else if (speed < 1024 * 1024 * 1024) {
return String.format("%.2f MB/s", speed / 1024 / 1024);
return String.format(locale, "%.2f MB/s", speed / 1024 / 1024);
} else {
return String.format("%.2f GB/s", speed / 1024 / 1024 / 1024);
return String.format(locale, "%.2f GB/s", speed / 1024 / 1024 / 1024);
}
}
@@ -188,12 +192,11 @@ public class Utility {
switch (type) {
case MUSIC:
return R.drawable.music;
default:
case VIDEO:
return R.drawable.video;
case SUBTITLE:
return R.drawable.subtitle;
default:
return R.drawable.video;
}
}
@@ -274,4 +277,25 @@ public class Utility {
return -1;
}
private static String pad(int number) {
return number < 10 ? ("0" + number) : String.valueOf(number);
}
public static String stringifySeconds(double seconds) {
int h = (int) Math.floor(seconds / 3600);
int m = (int) Math.floor((seconds - (h * 3600)) / 60);
int s = (int) (seconds - (h * 3600) - (m * 60));
String str = "";
if (h < 1 && m < 1) {
str = "00:";
} else {
if (h > 0) str = pad(h) + ":";
if (m > 0) str += pad(m) + ":";
}
return str + pad(s);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 856 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -47,15 +47,22 @@
<TextView
android:id="@+id/drawer_header_service_view"
android:layout_width="100dp"
android:layout_width="wrap_content"
android:layout_height="100dp"
android:layout_alignLeft="@id/drawer_header_np_text_view"
android:layout_alignStart="@id/drawer_header_np_text_view"
android:layout_below="@id/drawer_header_np_text_view"
android:layout_toLeftOf="@id/drawer_arrow"
android:layout_marginRight="5dp"
android:text="YouTube"
android:textSize="18sp"
android:textColor="@color/drawer_header_font_color"
android:textStyle="italic" />
android:textStyle="italic"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:marqueeRepeatLimit="marquee_forever"
android:scrollHorizontally="true"
android:singleLine="true" />
<ImageView
android:id="@+id/drawer_arrow"

View File

@@ -46,15 +46,22 @@ android:focusable="true">
<TextView
android:id="@+id/drawer_header_service_view"
android:layout_width="100dp"
android:layout_width="wrap_content"
android:layout_height="100dp"
android:text="YouTube"
android:layout_below="@id/drawer_header_np_text_view"
android:layout_alignLeft="@id/drawer_header_np_text_view"
android:layout_alignStart="@id/drawer_header_np_text_view"
android:layout_toLeftOf="@id/drawer_arrow"
android:layout_marginRight="5dp"
android:textSize="18sp"
android:textColor="@color/drawer_header_font_color"
android:textStyle="italic"/>
android:textStyle="italic"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:marqueeRepeatLimit="marquee_forever"
android:scrollHorizontally="true"
android:singleLine="true" />
<ImageView
android:id="@+id/drawer_arrow"

View File

@@ -16,7 +16,8 @@
android:layout_height="wrap_content"
app:elevation="0dp"
android:background="?attr/android:windowBackground"
app:headerLayout="@layout/drawer_header"/>
app:headerLayout="@layout/drawer_header"
android:theme="@style/NavViewTextStyle"/>
<!-- app:menu="@menu/drawer_items" -->
<LinearLayout

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/instanceHelpTV"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="15dp"
android:autoLink="web"
android:text="@string/peertube_instance_url_help"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/instances"
android:layout_below="@id/instanceHelpTV"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/item_instance" />
<!-- LOADING INDICATOR-->
<ProgressBar
android:id="@+id/loading_progress_bar"
style="@style/Widget.AppCompat.ProgressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:indeterminate="true"
android:visibility="gone"
tools:visibility="visible" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/addInstanceButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="16dp"
android:clickable="true"
android:focusable="true"
app:backgroundTint="?attr/colorPrimary"
app:fabSize="auto"
app:srcCompat="?attr/ic_add" />
</RelativeLayout>

View File

@@ -6,12 +6,13 @@
android:layout_height="match_parent">
<com.google.android.material.tabs.TabLayout
<org.schabi.newpipe.views.ScrollableTabLayout
android:id="@+id/main_tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:background="?attr/colorPrimary"
app:tabMinWidth="60dp"
app:tabGravity="fill"/>
<androidx.viewpager.widget.ViewPager

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/text1"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:maxLength="0" />

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<Spinner xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/spinner"
tools:listitem="@layout/instance_spinner_item"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="end"
android:prompt="@string/choose_instance_prompt" />

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