Compare commits
423 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa06e3490d | ||
|
|
ece889b36b | ||
|
|
9848f19ce6 | ||
|
|
e59b087057 | ||
|
|
5b451d1ac7 | ||
|
|
f050c05b3c | ||
|
|
06b8edefbf | ||
|
|
f3a2a28398 | ||
|
|
9b0a1fc2ec | ||
|
|
c87ab234eb | ||
|
|
68468756a8 | ||
|
|
9016df0195 | ||
|
|
7acece9705 | ||
|
|
d1896c23c0 | ||
|
|
dae19f03e0 | ||
|
|
9dafccf0f7 | ||
|
|
5f270c41ae | ||
|
|
f9f80e9003 | ||
|
|
c5063d4269 | ||
|
|
ccbe18ec1c | ||
|
|
321a8a8b25 | ||
|
|
77a9560376 | ||
|
|
058a039a82 | ||
|
|
7c744703e4 | ||
|
|
a934cbb085 | ||
|
|
cf4158c0d0 | ||
|
|
dc56eab9b6 | ||
|
|
37d1f59132 | ||
|
|
ab0ce55411 | ||
|
|
c1d66596d1 | ||
|
|
cbfccdf0d3 | ||
|
|
9362037177 | ||
|
|
e25c93bae2 | ||
|
|
367c434010 | ||
|
|
02d8463e15 | ||
|
|
9d5a1d5c43 | ||
|
|
c8d94f541f | ||
|
|
27d06eaa6b | ||
|
|
7f32857e00 | ||
|
|
2f060f0f52 | ||
|
|
f89d405226 | ||
|
|
fd4459e570 | ||
|
|
eb0df2b101 | ||
|
|
6c178cfb7e | ||
|
|
8ced68430d | ||
|
|
0aade598ff | ||
|
|
95949fd1ab | ||
|
|
1a56382112 | ||
|
|
d610e4b19b | ||
|
|
fc44d9e36e | ||
|
|
c07686576a | ||
|
|
c2400aea4d | ||
|
|
fb4bf0dde4 | ||
|
|
e4f638d1ce | ||
|
|
5c492c01a1 | ||
|
|
f451e11f82 | ||
|
|
95b73f35f7 | ||
|
|
58147e9e12 | ||
|
|
a8830e2ede | ||
|
|
9804bb95cc | ||
|
|
0da1aef763 | ||
|
|
0a334804a3 | ||
|
|
94d2f03e9b | ||
|
|
9127f7f0c2 | ||
|
|
0bb0226bc2 | ||
|
|
b3a1a5dcc2 | ||
|
|
984dd1cc25 | ||
|
|
5663e543a4 | ||
|
|
d3879a0398 | ||
|
|
6bd2468d44 | ||
|
|
e63d43151b | ||
|
|
0265da4ae6 | ||
|
|
ef255d12ae | ||
|
|
eeb612f9a2 | ||
|
|
dfcb4edb81 | ||
|
|
d3500e9036 | ||
|
|
adcb8c6469 | ||
|
|
592eee7d3d | ||
|
|
7dadb2b26c | ||
|
|
7cbb135f28 | ||
|
|
966ac0673c | ||
|
|
d715eae0d1 | ||
|
|
ccdd13d136 | ||
|
|
efe5de4c75 | ||
|
|
2a93e9bd2e | ||
|
|
28dd53ae50 | ||
|
|
3c1e64d8dc | ||
|
|
4fe3cb2bca | ||
|
|
b31490c4e3 | ||
|
|
5533f6ba86 | ||
|
|
8aa5f87a1c | ||
|
|
6deb674377 | ||
|
|
12d1d998a3 | ||
|
|
d90162d06f | ||
|
|
97c924341c | ||
|
|
e91fc225e1 | ||
|
|
43149fd832 | ||
|
|
78df579703 | ||
|
|
f61b915894 | ||
|
|
cd3f405bff | ||
|
|
7cfdca7a81 | ||
|
|
216063dba8 | ||
|
|
b647bacd72 | ||
|
|
4f77937e3e | ||
|
|
48e299b2ac | ||
|
|
40f00af196 | ||
|
|
bd6cc22e63 | ||
|
|
8760792426 | ||
|
|
870b0bf7aa | ||
|
|
afd0bd4318 | ||
|
|
f829ac1d34 | ||
|
|
86a0177855 | ||
|
|
718d4fd0bd | ||
|
|
365137c32b | ||
|
|
e83ca0dfda | ||
|
|
6a9f6ef651 | ||
|
|
99122ccc03 | ||
|
|
97923697e1 | ||
|
|
68888b15e0 | ||
|
|
f48b26067b | ||
|
|
4bf2d5837d | ||
|
|
4eb2d09c75 | ||
|
|
a146c1c4b6 | ||
|
|
2f1ea9aa5d | ||
|
|
a90da62deb | ||
|
|
c1c3fbdf26 | ||
|
|
e07a824d82 | ||
|
|
2aff660a5b | ||
|
|
abfcbe6f0e | ||
|
|
28bf72ed75 | ||
|
|
21b054d4ca | ||
|
|
34f115b322 | ||
|
|
8b67354076 | ||
|
|
b62e0a8b40 | ||
|
|
f46d5376fe | ||
|
|
dc3640578f | ||
|
|
0a43494de5 | ||
|
|
2544e45d2d | ||
|
|
4a53e9e018 | ||
|
|
9d7dc99416 | ||
|
|
c89dc4ba5b | ||
|
|
72289ced39 | ||
|
|
3554ccde05 | ||
|
|
73e2c42931 | ||
|
|
b11778ec55 | ||
|
|
18bc937958 | ||
|
|
9f618f6678 | ||
|
|
0c3c7493de | ||
|
|
8f3f02e9f7 | ||
|
|
69903ba889 | ||
|
|
25c5f95ad9 | ||
|
|
2546d1107e | ||
|
|
fdbeaf8692 | ||
|
|
63c0316af2 | ||
|
|
f2e761c07c | ||
|
|
6a741de7d1 | ||
|
|
3e94d18fe1 | ||
|
|
95d3651e29 | ||
|
|
cd2d88781a | ||
|
|
2178e86d09 | ||
|
|
f9ad0f12d0 | ||
|
|
63b16d925d | ||
|
|
53b9ffcbc8 | ||
|
|
df01f41980 | ||
|
|
b0c40d3b09 | ||
|
|
5880dcbcfd | ||
|
|
489bbc45f5 | ||
|
|
5e67502729 | ||
|
|
842079c928 | ||
|
|
b0bab07a15 | ||
|
|
4dbb12c65d | ||
|
|
db500e9791 | ||
|
|
fd8f600fec | ||
|
|
2e2d7d02fb | ||
|
|
d068cd7f75 | ||
|
|
5a05ffcbdd | ||
|
|
d8c7f50b39 | ||
|
|
988e6e1c82 | ||
|
|
f6af19444c | ||
|
|
0581e50e0c | ||
|
|
a95da9a42d | ||
|
|
be10b9750f | ||
|
|
29a3cbc688 | ||
|
|
4f57d3a201 | ||
|
|
c9be1398b0 | ||
|
|
30eef4db12 | ||
|
|
b932dbf514 | ||
|
|
320ac82dea | ||
|
|
af1b21db23 | ||
|
|
0e892ff60e | ||
|
|
074963aee0 | ||
|
|
37d9be9095 | ||
|
|
26e36454ef | ||
|
|
78b95f67eb | ||
|
|
6c63841d0c | ||
|
|
6ec2d91d91 | ||
|
|
3299b90c20 | ||
|
|
7b6d6da9a6 | ||
|
|
7c7c61fc35 | ||
|
|
c5408fb6b8 | ||
|
|
1c49102f67 | ||
|
|
f9dd88c1cb | ||
|
|
9ed4a65fd2 | ||
|
|
10bebf8a89 | ||
|
|
36260dac18 | ||
|
|
2e4e993967 | ||
|
|
ef1bfe98f7 | ||
|
|
2e123aa617 | ||
|
|
cf2ef0f2a8 | ||
|
|
520f40d862 | ||
|
|
856bdac84b | ||
|
|
deb7e38fcf | ||
|
|
e5f47a4563 | ||
|
|
d1b465d8be | ||
|
|
5c73b2f324 | ||
|
|
7d3e992b3f | ||
|
|
296640930e | ||
|
|
1c42735e3a | ||
|
|
0800bc1790 | ||
|
|
bd76a12b90 | ||
|
|
d3daea6383 | ||
|
|
df1acd5413 | ||
|
|
0d65cc09f6 | ||
|
|
892b082b9e | ||
|
|
9069ef1325 | ||
|
|
47cc493ab5 | ||
|
|
f43d7837f8 | ||
|
|
91921ae672 | ||
|
|
eba6afc12b | ||
|
|
ff826a9eeb | ||
|
|
6c01a30af5 | ||
|
|
53f8d09d31 | ||
|
|
ae191aaafe | ||
|
|
d1694d563b | ||
|
|
75fadf79da | ||
|
|
23ce5a6b1e | ||
|
|
bb65e2b84d | ||
|
|
aa42b1d95a | ||
|
|
32d5a18198 | ||
|
|
63faefe9c3 | ||
|
|
a90c49d030 | ||
|
|
b1ef3fa4df | ||
|
|
b8cf67cba9 | ||
|
|
4780e10ade | ||
|
|
401e606fbc | ||
|
|
6b4ef8f397 | ||
|
|
7c66d07779 | ||
|
|
92aed0cc3a | ||
|
|
457b08d3cc | ||
|
|
1e5f6fd2b8 | ||
|
|
aebfeb98aa | ||
|
|
f6974e8315 | ||
|
|
2ce6313ac1 | ||
|
|
e98a113a59 | ||
|
|
ba7bed9c2c | ||
|
|
c9ea451c53 | ||
|
|
4261ff32c7 | ||
|
|
cb4b20af45 | ||
|
|
b8a27adb93 | ||
|
|
15b58128f4 | ||
|
|
237282db28 | ||
|
|
71bb59dbb8 | ||
|
|
e41c46c075 | ||
|
|
6ca9e52f2f | ||
|
|
2afee89de3 | ||
|
|
189bee3e44 | ||
|
|
6b9a4d5e0a | ||
|
|
451e2b2182 | ||
|
|
f6ff41cfb4 | ||
|
|
d6d144c927 | ||
|
|
7f86872139 | ||
|
|
dc0fc05a9e | ||
|
|
6b2c3217ab | ||
|
|
0f93a45b9d | ||
|
|
943027ffdd | ||
|
|
5d28b2400f | ||
|
|
67324bfc80 | ||
|
|
3c340e7144 | ||
|
|
c834405a92 | ||
|
|
4ee9e26847 | ||
|
|
94293ca9d9 | ||
|
|
b9cd9f8d35 | ||
|
|
812dd9282d | ||
|
|
fa1d386fcc | ||
|
|
0392bf6a02 | ||
|
|
97f771ff50 | ||
|
|
6adcc72a8a | ||
|
|
2c11bd1889 | ||
|
|
23e0196fcc | ||
|
|
91f98c125e | ||
|
|
7f01e9a4d9 | ||
|
|
320a4e2351 | ||
|
|
0829ce51fc | ||
|
|
97ec50c202 | ||
|
|
975a3e8103 | ||
|
|
658ef2ef26 | ||
|
|
bd1e531d7b | ||
|
|
e440d1d1bd | ||
|
|
2110020165 | ||
|
|
dac74dc30d | ||
|
|
fa4b971254 | ||
|
|
2d31ae0baa | ||
|
|
5eebfa132f | ||
|
|
522febef93 | ||
|
|
a421645ea5 | ||
|
|
0aac4b1347 | ||
|
|
36697825cf | ||
|
|
b0182ed604 | ||
|
|
c4ea73ca7a | ||
|
|
2aa28d6453 | ||
|
|
2d51085ccf | ||
|
|
4eae02e47e | ||
|
|
f3bf9f9e5d | ||
|
|
75d0b84bdb | ||
|
|
3643ddcf5c | ||
|
|
35500e8ef7 | ||
|
|
6badcf5391 | ||
|
|
d4898043f6 | ||
|
|
2322ba833a | ||
|
|
5aabb2917f | ||
|
|
eacbf6ce50 | ||
|
|
031300a132 | ||
|
|
f905611e69 | ||
|
|
e475f9f876 | ||
|
|
b65263349e | ||
|
|
5cb8026f6d | ||
|
|
cc7ce5cf93 | ||
|
|
af506639a9 | ||
|
|
d377d67174 | ||
|
|
cf926353d1 | ||
|
|
d686c744d0 | ||
|
|
501c60b180 | ||
|
|
da36687e25 | ||
|
|
f13f9a066a | ||
|
|
8eaa4f7654 | ||
|
|
145a7f8e0d | ||
|
|
67ba126602 | ||
|
|
c5084901b5 | ||
|
|
3bfc82f7c0 | ||
|
|
3411b53450 | ||
|
|
873564f2aa | ||
|
|
353ed90d12 | ||
|
|
799faecc5b | ||
|
|
731321b1f9 | ||
|
|
7ff43caf96 | ||
|
|
d0877c3132 | ||
|
|
c589b03dcb | ||
|
|
862b5aaef6 | ||
|
|
596443bf5e | ||
|
|
27b450f1e3 | ||
|
|
61a09e97ca | ||
|
|
224e7a8969 | ||
|
|
9e7d9ee973 | ||
|
|
586bad345c | ||
|
|
abdd7dc7d3 | ||
|
|
aee32f7a3e | ||
|
|
696760e65a | ||
|
|
200db15d4b | ||
|
|
33e332f105 | ||
|
|
bb2955e442 | ||
|
|
2fc2fa56c3 | ||
|
|
c87458590c | ||
|
|
8aff134c56 | ||
|
|
4a938b81df | ||
|
|
4def715b25 | ||
|
|
821acf12d8 | ||
|
|
fc707b6c7e | ||
|
|
85ac000479 | ||
|
|
68a0eefa20 | ||
|
|
fc32377ce7 | ||
|
|
59e512a64d | ||
|
|
c51a5a51f1 | ||
|
|
9546a276dc | ||
|
|
a18353df5f | ||
|
|
3c72113f4c | ||
|
|
7e193751c4 | ||
|
|
56c96eb712 | ||
|
|
4106a984ca | ||
|
|
10f1ab0598 | ||
|
|
bca9603440 | ||
|
|
c32c267889 | ||
|
|
627e987bda | ||
|
|
7c18e147f3 | ||
|
|
6a8fb5910d | ||
|
|
b865326d51 | ||
|
|
a2d5b0893d | ||
|
|
db0508b9ab | ||
|
|
1850dee93a | ||
|
|
7b1eb8a6dc | ||
|
|
95a9f2f5e3 | ||
|
|
ae7ed2d226 | ||
|
|
20cf82bab1 | ||
|
|
5dcb1e26b5 | ||
|
|
8076589180 | ||
|
|
edbd4003be | ||
|
|
122b089bf0 | ||
|
|
a28d917990 | ||
|
|
5b605a1100 | ||
|
|
f67158a2a7 | ||
|
|
c22c2009d4 | ||
|
|
ab4d626ea9 | ||
|
|
96709d22e9 | ||
|
|
f0bd171eee | ||
|
|
c4191077f3 | ||
|
|
321d090052 | ||
|
|
32dcb4d281 | ||
|
|
080159849e | ||
|
|
d9e690f62c | ||
|
|
8c0156dea3 | ||
|
|
038c59ce66 | ||
|
|
72e08c0447 | ||
|
|
173eaa8cf8 | ||
|
|
a04cd24e5e | ||
|
|
0e11404b3b | ||
|
|
342807e26a | ||
|
|
c068f08ff8 | ||
|
|
b7c0a77edc | ||
|
|
aab0f45890 | ||
|
|
c62ad66f11 | ||
|
|
d8bdada9db | ||
|
|
1b9f7a7654 | ||
|
|
ac710fff08 | ||
|
|
2e09492eef |
7
.gitignore
vendored
@@ -1,7 +1,10 @@
|
||||
.gitignore
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/workspace.xml
|
||||
/.idea/libraries
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
/app/app.iml
|
||||
/.idea
|
||||
/*.iml
|
||||
gradle.properties
|
||||
|
||||
1
.idea/.name
generated
@@ -1 +0,0 @@
|
||||
NewPipe
|
||||
22
.idea/compiler.xml
generated
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<resourceExtensions />
|
||||
<wildcardResourcePatterns>
|
||||
<entry name="!?*.java" />
|
||||
<entry name="!?*.form" />
|
||||
<entry name="!?*.class" />
|
||||
<entry name="!?*.groovy" />
|
||||
<entry name="!?*.scala" />
|
||||
<entry name="!?*.flex" />
|
||||
<entry name="!?*.kt" />
|
||||
<entry name="!?*.clj" />
|
||||
<entry name="!?*.aj" />
|
||||
</wildcardResourcePatterns>
|
||||
<annotationProcessing>
|
||||
<profile default="true" name="Default" enabled="false">
|
||||
<processorPath useClasspath="true" />
|
||||
</profile>
|
||||
</annotationProcessing>
|
||||
</component>
|
||||
</project>
|
||||
3
.idea/copyright/profiles_settings.xml
generated
@@ -1,3 +0,0 @@
|
||||
<component name="CopyrightManager">
|
||||
<settings default="" />
|
||||
</component>
|
||||
3
.idea/dictionaries/the_scrabi.xml
generated
@@ -1,3 +0,0 @@
|
||||
<component name="ProjectDictionaryState">
|
||||
<dictionary name="the-scrabi" />
|
||||
</component>
|
||||
19
.idea/gradle.xml
generated
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="distributionType" value="LOCAL" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleHome" value="$APPLICATION_HOME_DIR$/gradle/gradle-2.4" />
|
||||
<option name="gradleJvm" value="1.8" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
46
.idea/misc.xml
generated
@@ -1,46 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="EntryPointsManager">
|
||||
<entry_points version="2.0" />
|
||||
</component>
|
||||
<component name="NullableNotNullManager">
|
||||
<option name="myDefaultNullable" value="android.support.annotation.Nullable" />
|
||||
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
|
||||
<option name="myNullables">
|
||||
<value>
|
||||
<list size="4">
|
||||
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
|
||||
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
|
||||
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
|
||||
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
<option name="myNotNulls">
|
||||
<value>
|
||||
<list size="4">
|
||||
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
|
||||
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
|
||||
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
|
||||
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectLevelVcsManager" settingsEditedManually="false">
|
||||
<OptionsSetting value="true" id="Add" />
|
||||
<OptionsSetting value="true" id="Remove" />
|
||||
<OptionsSetting value="true" id="Checkout" />
|
||||
<OptionsSetting value="true" id="Update" />
|
||||
<OptionsSetting value="true" id="Status" />
|
||||
<OptionsSetting value="true" id="Edit" />
|
||||
<ConfirmationsSetting value="0" id="Add" />
|
||||
<ConfirmationsSetting value="0" id="Remove" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" default="true" assert-keyword="true" jdk-15="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
||||
9
.idea/modules.xml
generated
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/NewPipe.iml" filepath="$PROJECT_DIR$/NewPipe.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/app/app.iml" filepath="$PROJECT_DIR$/app/app.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
12
.idea/runConfigurations.xml
generated
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
30
.travis.yml
Normal file
@@ -0,0 +1,30 @@
|
||||
language: android
|
||||
android:
|
||||
components:
|
||||
# The BuildTools version used by NewPipe
|
||||
- tools
|
||||
- build-tools-23.0.2
|
||||
|
||||
# The SDK version used to compile NewPipe
|
||||
- android-23
|
||||
|
||||
# Additional components
|
||||
- extra-android-support
|
||||
- extra-android-m2repository
|
||||
|
||||
# Emulators
|
||||
- sys-img-armeabi-v7a-android-21
|
||||
- sys-img-armeabi-v7a-android-19
|
||||
- sys-img-armeabi-v7a-android-15
|
||||
|
||||
env:
|
||||
global:
|
||||
- ADB_INSTALL_TIMEOUT=8 # minutes (2 by default)
|
||||
matrix:
|
||||
- ANDROID_TARGET=android-19 ANDROID_ABI=armeabi-v7a
|
||||
|
||||
before_script:
|
||||
- echo no | android create avd --force -n test -t $ANDROID_TARGET --abi $ANDROID_ABI
|
||||
- emulator -avd test -no-skin -no-audio -no-window &
|
||||
- android-wait-for-emulator
|
||||
- adb shell input keyevent 82 &
|
||||
33
CONTRIBUTING.md
Normal file
@@ -0,0 +1,33 @@
|
||||
#Contribution
|
||||
|
||||
This document contains guidelines on making contributions to NewPipe.
|
||||
|
||||
## Programming
|
||||
|
||||
* Follow the [Google Style Guidelines](https://google.github.io/styleguide/javaguide.html)
|
||||
* Make a new feature on a separate branch, not on the master branch
|
||||
* Make a [pull request](https://github.com/theScrabi/NewPipe/pulls) if you're done with your changes
|
||||
* When submitting changes, you agree that your code will be GPLv3 licensed
|
||||
|
||||
## Commit messages
|
||||
|
||||
* The subject line of your commit message shouldn't be longer than 72 characters
|
||||
* Try to keep each line of your commit message 72 characters to ensure proper
|
||||
compatibility with all git tools
|
||||
* [This guide](http://chris.beams.io/posts/git-commit/) goes more in depth on what makes a good commit message
|
||||
|
||||
## Translation
|
||||
|
||||
* NewPipe can be translated on [weblate](https://hosted.weblate.org/projects/newpipe/strings/)
|
||||
|
||||
## Issue reporting
|
||||
|
||||
* Search the [existing issues](https://github.com/theScrabi/NewPipe/issues) first to make sure your issue hasn't been reported before
|
||||
* Check if this issue is already fixed in the repository
|
||||
* When making bug reports, be sure to tell which version of NewPipe you are using and the steps to reproduce the problem
|
||||
* Please include a log if you can
|
||||
|
||||
## Communication
|
||||
|
||||
* For the time being, [Slack](http://invite.chschtsch.ml/) is being used for project communication
|
||||
* Feel free to post suggestions, changes, ideas etc!
|
||||
19
NewPipe.iml
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module external.linked.project.id="NewPipe" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" external.system.module.group="" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
|
||||
<component name="FacetManager">
|
||||
<facet type="java-gradle" name="Java-Gradle">
|
||||
<configuration>
|
||||
<option name="BUILD_FOLDER_PATH" value="$MODULE_DIR$/build" />
|
||||
<option name="BUILDABLE" value="false" />
|
||||
</configuration>
|
||||
</facet>
|
||||
</component>
|
||||
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.gradle" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
71
README.md
@@ -1,7 +1,70 @@
|
||||
NewPipe
|
||||
-------
|
||||
# NewPipe
|
||||
NewPipe: A free lightweight Youtube frontend for Android.
|
||||
|
||||
[](http://dasochan.nl/newpipe/)
|
||||
|
||||
NewPipe is a lightweight youtube frontend for android. It's supposed to be used without the youtube-api and without any google play services. NewPipe only parses the youtube website in order to gain the information it needs.
|
||||
Project status:
|
||||
[](https://hosted.weblate.org/engage/NewPipe/)
|
||||
[](https://travis-ci.org/theScrabi/NewPipe)
|
||||
|
||||
This a very early version of the app, so not all functionality is implemented, and there may still be a lot of bugs. But all in all it's doing what it is supposed to do. It makes it possible to watch youtube videos. So don't be cruel to this app. It will improve...
|
||||
## Get NewPipe
|
||||
|
||||
[](https://f-droid.org/repository/browse/?fdfilter=newpipe&fdid=org.schabi.newpipe)
|
||||
|
||||
## Screenshots
|
||||
|
||||
[<img src="screenshots/screenshot_1.png" width=150>](screenshots/screenshot_1.png)
|
||||
[<img src="screenshots/screenshot_2.png" width=150>](screenshots/screenshot_2.png)
|
||||
[<img src="screenshots/screenshot_3.png" width=150>](screenshots/screenshot_3.png)
|
||||
[<img src="screenshots/screenshot_4.png" width=150>](screenshots/screenshot_4.png)
|
||||
[<img src="screenshots/screenshot_5.png" width=150>](screenshots/screenshot_5.png)
|
||||
[<img src="screenshots/screenshot_6.png" width=250>](screenshots/screenshot_6.png)
|
||||
|
||||
## Description
|
||||
|
||||
NewPipe does not use any Google framework libraries, or the YouTube API. It only parses the website in order to gain the information it needs. Therefore this app can be used on devices without Google Services installed. Also, you don't need a YouTube account to use NewPipe, and it's FLOSS.
|
||||
|
||||
### Features
|
||||
|
||||
* Search videos
|
||||
* Display general information about a video
|
||||
* Watch YouTube videos
|
||||
* Listen to YouTube videos (experimental)
|
||||
* Select the streaming player to watch the video with
|
||||
* Download videos (experimental)
|
||||
* Download audio only (experimental)
|
||||
* Open a video in Kodi
|
||||
* Show Next/Related videos
|
||||
* Search YouTube in a specific language
|
||||
* Orbot/Tor support (no streaming yet)
|
||||
|
||||
### Coming Features
|
||||
|
||||
* Improved Downloading
|
||||
* Bookmarks
|
||||
* View history
|
||||
* Search history
|
||||
* Search channels
|
||||
* Display general information about channels
|
||||
* Subscribe to channels
|
||||
* Watch videos from a channel
|
||||
* Search/Watch Playlists
|
||||
* ... and many more
|
||||
|
||||
### Multiservice support
|
||||
Although NewPipe only supports YouTube at the moment, it's designed to support many more streaming services. The plan is, that NewPipe will get such support by the version 2.0.
|
||||
|
||||
## Contribution
|
||||
Whether you have ideas, translation, design changes, code cleaning, or real heavy code changes, help is always welcome.
|
||||
The more is done the better it gets!
|
||||
|
||||
If you'd like to get involved, check our [contribution notes](CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
[](http://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
NewPipe is Free Software: You can use, study share and improve it at your
|
||||
will. Specifically you can redistribute and/or modify it under the terms of the
|
||||
[GNU General Public License](https://www.gnu.org/licenses/gpl.html) as
|
||||
published by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
2
app/.gitignore
vendored
@@ -1 +1,3 @@
|
||||
.gitignore
|
||||
/build
|
||||
app.iml
|
||||
|
||||
101
app/app.iml
@@ -1,101 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module external.linked.project.id=":app" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="NewPipe" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
|
||||
<component name="FacetManager">
|
||||
<facet type="android-gradle" name="Android-Gradle">
|
||||
<configuration>
|
||||
<option name="GRADLE_PROJECT_PATH" value=":app" />
|
||||
</configuration>
|
||||
</facet>
|
||||
<facet type="android" name="Android">
|
||||
<configuration>
|
||||
<option name="SELECTED_BUILD_VARIANT" value="debug" />
|
||||
<option name="SELECTED_TEST_ARTIFACT" value="_android_test_" />
|
||||
<option name="ASSEMBLE_TASK_NAME" value="assembleDebug" />
|
||||
<option name="COMPILE_JAVA_TASK_NAME" value="compileDebugSources" />
|
||||
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleDebugAndroidTest" />
|
||||
<option name="COMPILE_JAVA_TEST_TASK_NAME" value="compileDebugAndroidTestSources" />
|
||||
<afterSyncTasks>
|
||||
<task>generateDebugAndroidTestSources</task>
|
||||
<task>generateDebugSources</task>
|
||||
</afterSyncTasks>
|
||||
<option name="ALLOW_USER_CONFIGURATION" value="false" />
|
||||
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
|
||||
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
|
||||
<option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/src/main/res" />
|
||||
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
|
||||
</configuration>
|
||||
</facet>
|
||||
</component>
|
||||
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="false">
|
||||
<output url="file://$MODULE_DIR$/build/intermediates/classes/debug" />
|
||||
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/androidTest/debug" />
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/debug" isTestSource="false" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/debug" isTestSource="false" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/debug" isTestSource="false" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/debug" isTestSource="false" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/debug" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/debug" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/androidTest/debug" isTestSource="true" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/androidTest/debug" isTestSource="true" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/debug" isTestSource="true" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/androidTest/debug" isTestSource="true" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/debug" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/androidTest/debug" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/resources" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/assets" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/aidl" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/jni" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/assets" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/aidl" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/jni" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/aidl" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/jni" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/assets" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/bundles" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/classes" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/coverage-instrumented-classes" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dependency-cache" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex-cache" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/appcompat-v7/23.0.1/jars" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/design/23.0.1/jars" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/support-v4/23.0.1/jars" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/jacoco" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/javaResources" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/libs" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/lint" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/manifests" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/ndk" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/pre-dexed" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/proguard" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/res" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/outputs" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/tmp" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Android API 23 Platform" jdkType="Android SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" exported="" name="jsoup-1.8.3" level="project" />
|
||||
<orderEntry type="library" exported="" name="support-v4-23.0.1" level="project" />
|
||||
<orderEntry type="library" exported="" name="rhino-1.7.7" level="project" />
|
||||
<orderEntry type="library" exported="" name="design-23.0.1" level="project" />
|
||||
<orderEntry type="library" exported="" name="appcompat-v7-23.0.1" level="project" />
|
||||
<orderEntry type="library" exported="" name="support-annotations-23.0.1" level="project" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -2,14 +2,14 @@ apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 23
|
||||
buildToolsVersion "23.0.1"
|
||||
buildToolsVersion "23.0.2"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.schabi.newpipe"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 23
|
||||
versionCode 4
|
||||
versionName "0.4.1"
|
||||
versionCode 12
|
||||
versionName "0.7.3"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
@@ -17,13 +17,23 @@ android {
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
checkReleaseBuilds false
|
||||
// Or, if you prefer, you can continue to check for errors in release builds,
|
||||
// but continue the build even when errors are found:
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile fileTree(include: ['*.jar'], dir: 'libs')
|
||||
compile 'com.android.support:appcompat-v7:23.0.1'
|
||||
compile 'com.android.support:support-v4:23.0.1'
|
||||
compile 'com.android.support:appcompat-v7:23.1.1'
|
||||
compile 'com.android.support:support-v4:23.1.1'
|
||||
compile 'com.android.support:design:23.1.1'
|
||||
compile 'com.android.support:cardview-v7:23.1.1'
|
||||
compile 'com.android.support:recyclerview-v7:23.1.1'
|
||||
compile 'org.jsoup:jsoup:1.8.3'
|
||||
compile 'org.mozilla:rhino:1.7.7'
|
||||
compile 'com.android.support:design:23.0.1'
|
||||
compile 'info.guardianproject.netcipher:netcipher:1.2'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package org.schabi.newpipe.services.youtube;
|
||||
|
||||
import android.test.AndroidTestCase;
|
||||
|
||||
import org.schabi.newpipe.VideoPreviewInfo;
|
||||
import org.schabi.newpipe.services.SearchEngine;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Created by the-scrabi on 29.12.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* YoutubeSearchEngineTest.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 YoutubeSearchEngineTest extends AndroidTestCase {
|
||||
private SearchEngine.Result result;
|
||||
private ArrayList<String> suggestionReply;
|
||||
|
||||
@Override
|
||||
public void setUp() throws Exception{
|
||||
super.setUp();
|
||||
SearchEngine engine = new YoutubeSearchEngine();
|
||||
result = engine.search("https://www.youtube.com/results?search_query=bla", 0, "de");
|
||||
suggestionReply = engine.suggestionList("hello");
|
||||
}
|
||||
|
||||
public void testIfNoErrorOccur() {
|
||||
assertEquals(result.errorMessage, "");
|
||||
}
|
||||
|
||||
public void testIfListIsNotEmpty() {
|
||||
assertEquals(result.resultList.size() > 0, true);
|
||||
}
|
||||
|
||||
public void testItemsHaveTitle() {
|
||||
for(VideoPreviewInfo i : result.resultList) {
|
||||
assertEquals(i.title.isEmpty(), false);
|
||||
}
|
||||
}
|
||||
|
||||
public void testItemsHaveUploader() {
|
||||
for(VideoPreviewInfo i : result.resultList) {
|
||||
assertEquals(i.uploader.isEmpty(), false);
|
||||
}
|
||||
}
|
||||
|
||||
public void testItemsHaveRightDuration() {
|
||||
for(VideoPreviewInfo i : result.resultList) {
|
||||
assertTrue(i.duration, i.duration.contains(":"));
|
||||
}
|
||||
}
|
||||
|
||||
public void testItemsHaveRightThumbnail() {
|
||||
for (VideoPreviewInfo i : result.resultList) {
|
||||
assertTrue(i.thumbnail_url, i.thumbnail_url.contains("https://"));
|
||||
}
|
||||
}
|
||||
|
||||
public void testItemsHaveRightVideoUrl() {
|
||||
for (VideoPreviewInfo i : result.resultList) {
|
||||
assertTrue(i.webpage_url, i.webpage_url.contains("https://"));
|
||||
}
|
||||
}
|
||||
|
||||
public void testIfSuggestionsAreReplied() {
|
||||
assertEquals(suggestionReply.size() > 0, true);
|
||||
}
|
||||
|
||||
public void testIfSuggestionsAreValid() {
|
||||
for(String s : suggestionReply) {
|
||||
assertTrue(s, !s.isEmpty());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package org.schabi.newpipe.services.youtube;
|
||||
|
||||
import android.test.AndroidTestCase;
|
||||
import android.util.Log;
|
||||
|
||||
import org.schabi.newpipe.services.VideoInfo;
|
||||
|
||||
/**
|
||||
* Created by the-scrabi on 30.12.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* YoutubeVideoExtractorDefault.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 YoutubeVideoExtractorDefaultTest extends AndroidTestCase {
|
||||
private YoutubeVideoExtractor extractor;
|
||||
|
||||
public void setUp() {
|
||||
extractor = new YoutubeVideoExtractor("https://www.youtube.com/watch?v=FmG385_uUys");
|
||||
}
|
||||
|
||||
public void testGetErrorCode() {
|
||||
assertEquals(extractor.getErrorCode(), VideoInfo.NO_ERROR);
|
||||
}
|
||||
|
||||
public void testGetErrorMessage() {
|
||||
assertEquals(extractor.getErrorMessage(), "");
|
||||
}
|
||||
|
||||
public void testGetTimeStamp() {
|
||||
assertTrue(Integer.toString(extractor.getTimeStamp()),
|
||||
extractor.getTimeStamp() >= 0);
|
||||
}
|
||||
|
||||
public void testGetTitle() {
|
||||
assertTrue(!extractor.getTitle().isEmpty());
|
||||
}
|
||||
|
||||
public void testGetDescription() {
|
||||
assertTrue(extractor.getDescription() != null);
|
||||
}
|
||||
|
||||
public void testGetUploader() {
|
||||
assertTrue(!extractor.getUploader().isEmpty());
|
||||
}
|
||||
|
||||
public void testGetLength() {
|
||||
assertTrue(extractor.getLength() > 0);
|
||||
}
|
||||
|
||||
public void testGetViews() {
|
||||
assertTrue(extractor.getLength() > 0);
|
||||
}
|
||||
|
||||
public void testGetUploadDate() {
|
||||
assertTrue(extractor.getUploadDate().length() > 0);
|
||||
}
|
||||
|
||||
public void testGetThumbnailUrl() {
|
||||
assertTrue(extractor.getThumbnailUrl(),
|
||||
extractor.getThumbnailUrl().contains("https://"));
|
||||
}
|
||||
|
||||
public void testGetUploaderThumbnailUrl() {
|
||||
assertTrue(extractor.getUploaderThumbnailUrl(),
|
||||
extractor.getUploaderThumbnailUrl().contains("https://"));
|
||||
}
|
||||
|
||||
public void testGetAudioStreams() {
|
||||
for(VideoInfo.AudioStream s : extractor.getAudioStreams()) {
|
||||
assertTrue(s.url,
|
||||
s.url.contains("https://"));
|
||||
assertTrue(s.bandwidth > 0);
|
||||
assertTrue(s.samplingRate > 0);
|
||||
}
|
||||
}
|
||||
|
||||
public void testGetVideoStreams() {
|
||||
for(VideoInfo.VideoStream s : extractor.getVideoStreams()) {
|
||||
assertTrue(s.url,
|
||||
s.url.contains("https://"));
|
||||
assertTrue(s.resolution.length() > 0);
|
||||
assertTrue(Integer.toString(s.format),
|
||||
0 <= s.format && s.format <= 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.schabi.newpipe.services.youtube;
|
||||
|
||||
import android.test.AndroidTestCase;
|
||||
|
||||
import org.schabi.newpipe.services.VideoInfo;
|
||||
|
||||
/**
|
||||
* Created by the-scrabi on 30.12.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* YoutubeVideoExtractorGema.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/>.
|
||||
*/
|
||||
|
||||
|
||||
// This class only works in Germany.
|
||||
public class YoutubeVideoExtractorGemaTest extends AndroidTestCase {
|
||||
|
||||
// Deaktivate this Test Case bevore uploading it githup, otherwise CI will fail.
|
||||
private static final boolean testActive = false;
|
||||
|
||||
|
||||
private YoutubeVideoExtractor extractor;
|
||||
|
||||
public void setUp() {
|
||||
if(testActive) {
|
||||
extractor = new YoutubeVideoExtractor("https://www.youtube.com/watch?v=3O1_3zBUKM8");
|
||||
}
|
||||
}
|
||||
|
||||
public void testGetErrorCode() {
|
||||
if(testActive) {
|
||||
assertEquals(extractor.getErrorCode(), VideoInfo.ERROR_BLOCKED_BY_GEMA);
|
||||
} else {
|
||||
assertTrue(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void testGetErrorMessage() {
|
||||
if(testActive) {
|
||||
assertTrue(extractor.getErrorMessage(),
|
||||
extractor.getErrorMessage().contains("GEMA"));
|
||||
} else {
|
||||
assertTrue(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.schabi.newpipe" >
|
||||
|
||||
<uses-permission android:name= "android.permission.INTERNET" />
|
||||
<uses-permission android:name= "android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<application
|
||||
android:name=".App"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:logo="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme" >
|
||||
android:theme="@style/AppTheme"
|
||||
tools:ignore="AllowBackup">
|
||||
<activity
|
||||
android:name=".VideoItemListActivity"
|
||||
android:label="@string/app_name" >
|
||||
android:label="@string/app_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
@@ -22,57 +25,79 @@
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".VideoItemDetailActivity"
|
||||
android:label="@string/title_videoitem_detail" >
|
||||
android:label="@string/title_videoitem_detail"
|
||||
android:theme="@style/AppTheme">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".VideoItemListActivity" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="youtube.com"
|
||||
android:scheme="http"
|
||||
android:pathPrefix="/watch"/>
|
||||
<data
|
||||
android:host="youtube.com"
|
||||
android:scheme="https"
|
||||
android:pathPrefix="/watch"/>
|
||||
<data
|
||||
android:host="www.youtube.com"
|
||||
android:scheme="http"
|
||||
android:pathPrefix="/watch"/>
|
||||
<data
|
||||
android:host="www.youtube.com"
|
||||
android:scheme="https"
|
||||
android:pathPrefix="/watch"/>
|
||||
<data
|
||||
android:host="m.youtube.com"
|
||||
android:scheme="http"
|
||||
android:pathPrefix="/watch"/>
|
||||
<data
|
||||
android:host="m.youtube.com"
|
||||
android:scheme="https"
|
||||
android:pathPrefix="/watch"/>
|
||||
<data
|
||||
android:host="youtu.be"
|
||||
android:scheme="https"
|
||||
android:pathPrefix="/"/>
|
||||
<data
|
||||
android:host="youtu.be"
|
||||
android:scheme="http"
|
||||
android:pathPrefix="/"/>
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="youtube.com" />
|
||||
<data android:host="m.youtube.com" />
|
||||
<data android:host="www.youtube.com" />
|
||||
<data android:pathPrefix="/v/" />
|
||||
<data android:pathPrefix="/watch" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="youtu.be" />
|
||||
<data android:pathPrefix="/" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="vnd.youtube" />
|
||||
<data android:scheme="vnd.youtube.launch" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".PlayVideoActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:theme="@style/FullscreenTheme"
|
||||
>
|
||||
android:theme="@style/VideoPlayerTheme"
|
||||
android:parentActivityName=".VideoItemDetailActivity"
|
||||
tools:ignore="UnusedAttribute">
|
||||
</activity>
|
||||
<service
|
||||
android:name=".BackgroundPlayer"
|
||||
android:label="@string/background_player_name"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".SettingsActivity"
|
||||
android:label="@string/title_activity_settings" >
|
||||
android:label="@string/settings_activity_title" >
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".PanicResponderActivity"
|
||||
android:launchMode="singleInstance"
|
||||
android:noHistory="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter>
|
||||
<action android:name="info.guardianproject.panic.action.TRIGGER" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ExitActivity"
|
||||
android:theme="@android:style/Theme.NoDisplay" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.DownloadManager;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.view.MenuItemCompat;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
@@ -19,7 +16,8 @@ import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.ArrayAdapter;
|
||||
|
||||
import java.io.File;
|
||||
import org.schabi.newpipe.services.MediaFormat;
|
||||
import org.schabi.newpipe.services.VideoInfo;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 18.08.15.
|
||||
@@ -41,30 +39,26 @@ import java.io.File;
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class ActionBarHandler {
|
||||
|
||||
class ActionBarHandler {
|
||||
private static final String TAG = ActionBarHandler.class.toString();
|
||||
private static final String KORE_PACKET = "org.xbmc.kore";
|
||||
|
||||
private static ActionBarHandler handler = null;
|
||||
|
||||
private Context context = null;
|
||||
private String webisteUrl = "";
|
||||
private int serviceId;
|
||||
private String websiteUrl = "";
|
||||
private Bitmap videoThumbnail = null;
|
||||
private String channelName = "";
|
||||
private AppCompatActivity activity;
|
||||
private VideoInfo.VideoStream[] videoStreams = null;
|
||||
private VideoInfo.AudioStream audioStream = null;
|
||||
private int selectedStream = -1;
|
||||
private String videoTitle = "";
|
||||
|
||||
SharedPreferences defaultPreferences = null;
|
||||
private SharedPreferences defaultPreferences = null;
|
||||
private int startPosition;
|
||||
|
||||
public static ActionBarHandler getHandler() {
|
||||
if(handler == null) {
|
||||
handler = new ActionBarHandler();
|
||||
}
|
||||
return handler;
|
||||
}
|
||||
|
||||
class ForamatItemSelectListener implements ActionBar.OnNavigationListener {
|
||||
@SuppressWarnings("deprecation")
|
||||
private class FormatItemSelectListener implements ActionBar.OnNavigationListener {
|
||||
@Override
|
||||
public boolean onNavigationItemSelected(int itemPosition, long itemId) {
|
||||
selectFormatItem((int)itemId);
|
||||
@@ -72,152 +66,174 @@ public class ActionBarHandler {
|
||||
}
|
||||
}
|
||||
|
||||
public void setupNavMenu(AppCompatActivity activity) {
|
||||
public ActionBarHandler(AppCompatActivity activity) {
|
||||
this.activity = activity;
|
||||
activity.getSupportActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"deprecation", "ConstantConditions"})
|
||||
public void setupNavMenu(AppCompatActivity activity) {
|
||||
this.activity = activity;
|
||||
try {
|
||||
activity.getSupportActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
|
||||
} catch(NullPointerException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void setServiceId(int id) {
|
||||
serviceId = id;
|
||||
}
|
||||
|
||||
public void setSetVideoThumbnail(Bitmap bitmap) {
|
||||
videoThumbnail = bitmap;
|
||||
}
|
||||
|
||||
public void setChannelName(String name) {
|
||||
channelName = name;
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public void setStreams(VideoInfo.VideoStream[] videoStreams, VideoInfo.AudioStream[] audioStreams) {
|
||||
this.videoStreams = videoStreams;
|
||||
selectedStream = 0;
|
||||
defaultPreferences = PreferenceManager.getDefaultSharedPreferences(activity);
|
||||
String[] itemArray = new String[videoStreams.length];
|
||||
String defaultResolution = defaultPreferences
|
||||
.getString(context.getString(R.string.defaultResolutionPreference),
|
||||
context.getString(R.string.defaultResolutionListItem));
|
||||
.getString(activity.getString(R.string.default_resolution_key),
|
||||
activity.getString(R.string.default_resolution_value));
|
||||
int defaultResolutionPos = 0;
|
||||
|
||||
for(int i = 0; i < videoStreams.length; i++) {
|
||||
itemArray[i] = VideoInfo.getNameById(videoStreams[i].format) + " " + videoStreams[i].resolution;
|
||||
itemArray[i] = MediaFormat.getNameById(videoStreams[i].format) + " " + videoStreams[i].resolution;
|
||||
if(defaultResolution.equals(videoStreams[i].resolution)) {
|
||||
defaultResolutionPos = i;
|
||||
}
|
||||
}
|
||||
|
||||
ArrayAdapter<String> itemAdapter = new ArrayAdapter<String>(activity.getBaseContext(),
|
||||
ArrayAdapter<String> itemAdapter = new ArrayAdapter<>(activity.getBaseContext(),
|
||||
android.R.layout.simple_spinner_dropdown_item, itemArray);
|
||||
if(activity != null) {
|
||||
ActionBar ab = activity.getSupportActionBar();
|
||||
ab.setListNavigationCallbacks(itemAdapter
|
||||
,new ForamatItemSelectListener());
|
||||
assert ab != null : "Could not get actionbar";
|
||||
ab.setListNavigationCallbacks(itemAdapter
|
||||
, new FormatItemSelectListener());
|
||||
|
||||
ab.setSelectedNavigationItem(defaultResolutionPos);
|
||||
}
|
||||
|
||||
// set audioStream
|
||||
audioStream = null;
|
||||
String preferedFormat = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getString(context.getString(R.string.defaultAudioFormatPreference), "webm");
|
||||
String preferedFormat = defaultPreferences
|
||||
.getString(activity.getString(R.string.default_audio_format_key), "webm");
|
||||
if(preferedFormat.equals("webm")) {
|
||||
for(VideoInfo.AudioStream s : audioStreams) {
|
||||
if(s.format == VideoInfo.I_WEBMA) {
|
||||
if(s.format == MediaFormat.WEBMA.id) {
|
||||
audioStream = s;
|
||||
}
|
||||
}
|
||||
} else if(preferedFormat.equals("m4a")){
|
||||
for(VideoInfo.AudioStream s : audioStreams) {
|
||||
Log.d(TAG, VideoInfo.getMimeById(s.format) + " : " + Integer.toString(s.bandWidth));
|
||||
if(s.format == VideoInfo.I_M4A &&
|
||||
(audioStream == null || audioStream.bandWidth > s.bandWidth)) {
|
||||
if(s.format == MediaFormat.M4A.id &&
|
||||
(audioStream == null || audioStream.bandwidth > s.bandwidth)) {
|
||||
audioStream = s;
|
||||
Log.d(TAG, "last choosen");
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
Log.e(TAG, "FAILED to set audioStream value!");
|
||||
}
|
||||
}
|
||||
|
||||
private void selectFormatItem(int i) {
|
||||
selectedStream = i;
|
||||
}
|
||||
|
||||
public boolean setupMenu(Menu menu, MenuInflater inflater, Context context) {
|
||||
this.context = context;
|
||||
public void setupMenu(Menu menu, MenuInflater inflater) {
|
||||
// CAUTION set item properties programmatically otherwise it would not be accepted by
|
||||
// appcompat itemsinflater.inflate(R.menu.videoitem_detail, menu);
|
||||
|
||||
defaultPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
defaultPreferences = PreferenceManager.getDefaultSharedPreferences(activity);
|
||||
|
||||
inflater.inflate(R.menu.videoitem_detail, menu);
|
||||
MenuItem playItem = menu.findItem(R.id.menu_item_play);
|
||||
MenuItem shareItem = menu.findItem(R.id.menu_item_share);
|
||||
MenuItem castItem = menu.findItem(R.id.action_play_with_kodi);
|
||||
|
||||
MenuItemCompat.setShowAsAction(playItem, MenuItemCompat.SHOW_AS_ACTION_ALWAYS
|
||||
| MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT);
|
||||
MenuItemCompat.setShowAsAction(shareItem, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM
|
||||
| MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT);
|
||||
|
||||
castItem.setVisible(defaultPreferences
|
||||
.getBoolean(context.getString(R.string.showPlayWidthKodiPreference), false));
|
||||
|
||||
return true;
|
||||
.getBoolean(activity.getString(R.string.show_play_with_kodi_key), false));
|
||||
}
|
||||
|
||||
public boolean onItemSelected(MenuItem item, Context context) {
|
||||
this.context = context;
|
||||
int id = item.getItemId();
|
||||
switch(id) {
|
||||
case R.id.menu_item_play:
|
||||
playVideo();
|
||||
break;
|
||||
case R.id.menu_item_share:
|
||||
if(!videoTitle.isEmpty()) {
|
||||
public boolean onItemSelected(MenuItem item) {
|
||||
if(!videoTitle.isEmpty()) {
|
||||
int id = item.getItemId();
|
||||
switch (id) {
|
||||
case R.id.menu_item_share: {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_SEND);
|
||||
intent.putExtra(Intent.EXTRA_TEXT, webisteUrl);
|
||||
intent.putExtra(Intent.EXTRA_TEXT, websiteUrl);
|
||||
intent.setType("text/plain");
|
||||
context.startActivity(Intent.createChooser(intent, context.getString(R.string.shareDialogTitle)));
|
||||
activity.startActivity(Intent.createChooser(intent, activity.getString(R.string.share_dialog_title)));
|
||||
return true;
|
||||
}
|
||||
case R.id.menu_item_openInBrowser: {
|
||||
openInBrowser();
|
||||
}
|
||||
return true;
|
||||
case R.id.menu_item_download:
|
||||
downloadVideo();
|
||||
return true;
|
||||
case R.id.action_settings: {
|
||||
Intent intent = new Intent(activity, SettingsActivity.class);
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
break;
|
||||
case R.id.menu_item_openInBrowser: {
|
||||
openInBrowser();
|
||||
case R.id.action_play_with_kodi:
|
||||
playWithKodi();
|
||||
return true;
|
||||
case R.id.menu_item_play_audio:
|
||||
playAudio();
|
||||
return true;
|
||||
default:
|
||||
Log.e(TAG, "Menu Item not known");
|
||||
}
|
||||
break;
|
||||
case R.id.menu_item_download:
|
||||
downloadVideo();
|
||||
break;
|
||||
case R.id.action_settings: {
|
||||
Intent intent = new Intent(context, SettingsActivity.class);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
break;
|
||||
case R.id.action_play_with_kodi:
|
||||
playWithKodi();
|
||||
break;
|
||||
case R.id.menu_item_play_audio:
|
||||
playAudio();
|
||||
break;
|
||||
default:
|
||||
Log.e(TAG, "Menu Item not known");
|
||||
} else {
|
||||
// That line may not be necessary.
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setVideoInfo(String websiteUrl, String videoTitle) {
|
||||
this.webisteUrl = websiteUrl;
|
||||
this.websiteUrl = websiteUrl;
|
||||
this.videoTitle = videoTitle;
|
||||
}
|
||||
|
||||
public void playVideo() {
|
||||
// ----------- THE MAGIC MOMENT ---------------
|
||||
if(!videoTitle.isEmpty()) {
|
||||
if (PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean(context.getString(R.string.useExternalPlayer), false)) {
|
||||
if (PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.getBoolean(activity.getString(R.string.use_external_video_player_key), false)) {
|
||||
|
||||
// External Player
|
||||
Intent intent = new Intent();
|
||||
try {
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
|
||||
intent.setDataAndType(Uri.parse(videoStreams[selectedStream].url),
|
||||
VideoInfo.getMimeById(videoStreams[selectedStream].format));
|
||||
context.startActivity(intent); // HERE !!!
|
||||
MediaFormat.getMimeById(videoStreams[selectedStream].format));
|
||||
intent.putExtra(Intent.EXTRA_TITLE, videoTitle);
|
||||
intent.putExtra("title", videoTitle);
|
||||
|
||||
activity.startActivity(intent); // HERE !!!
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setMessage(R.string.noPlayerFound)
|
||||
.setPositiveButton(R.string.installStreamPlayer, new DialogInterface.OnClickListener() {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setMessage(R.string.no_player_found)
|
||||
.setPositiveButton(R.string.install, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(context.getString(R.string.fdroidVLCurl)));
|
||||
context.startActivity(intent);
|
||||
intent.setData(Uri.parse(activity.getString(R.string.fdroid_vlc_url)));
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
|
||||
@@ -229,21 +245,27 @@ public class ActionBarHandler {
|
||||
builder.create().show();
|
||||
}
|
||||
} else {
|
||||
Intent intent = new Intent(context, PlayVideoActivity.class);
|
||||
// Internal Player
|
||||
Intent intent = new Intent(activity, PlayVideoActivity.class);
|
||||
intent.putExtra(PlayVideoActivity.VIDEO_TITLE, videoTitle);
|
||||
intent.putExtra(PlayVideoActivity.STREAM_URL, videoStreams[selectedStream].url);
|
||||
intent.putExtra(PlayVideoActivity.VIDEO_URL, webisteUrl);
|
||||
context.startActivity(intent);
|
||||
intent.putExtra(PlayVideoActivity.VIDEO_URL, websiteUrl);
|
||||
intent.putExtra(PlayVideoActivity.START_POSITION, startPosition);
|
||||
activity.startActivity(intent); //also HERE !!!
|
||||
}
|
||||
}
|
||||
// --------------------------------------------
|
||||
}
|
||||
|
||||
public void downloadVideo() {
|
||||
Log.d(TAG, "bla");
|
||||
public void setStartPosition(int startPositionSeconds)
|
||||
{
|
||||
this.startPosition = startPositionSeconds;
|
||||
}
|
||||
|
||||
private void downloadVideo() {
|
||||
if(!videoTitle.isEmpty()) {
|
||||
String videoSuffix = "." + VideoInfo.getSuffixById(videoStreams[selectedStream].format);
|
||||
String audioSuffix = "." + VideoInfo.getSuffixById(audioStream.format);
|
||||
String videoSuffix = "." + MediaFormat.getSuffixById(videoStreams[selectedStream].format);
|
||||
String audioSuffix = "." + MediaFormat.getSuffixById(audioStream.format);
|
||||
Bundle args = new Bundle();
|
||||
args.putString(DownloadDialog.FILE_SUFFIX_VIDEO, videoSuffix);
|
||||
args.putString(DownloadDialog.FILE_SUFFIX_AUDIO, audioSuffix);
|
||||
@@ -256,34 +278,34 @@ public class ActionBarHandler {
|
||||
}
|
||||
}
|
||||
|
||||
public void openInBrowser() {
|
||||
private void openInBrowser() {
|
||||
if(!videoTitle.isEmpty()) {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(webisteUrl));
|
||||
intent.setData(Uri.parse(websiteUrl));
|
||||
|
||||
context.startActivity(Intent.createChooser(intent, context.getString(R.string.chooseBrowser)));
|
||||
activity.startActivity(Intent.createChooser(intent, activity.getString(R.string.choose_browser)));
|
||||
}
|
||||
}
|
||||
|
||||
public void playWithKodi() {
|
||||
private void playWithKodi() {
|
||||
if(!videoTitle.isEmpty()) {
|
||||
try {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setPackage(KORE_PACKET);
|
||||
intent.setData(Uri.parse(webisteUrl.replace("https", "http")));
|
||||
context.startActivity(intent);
|
||||
intent.setData(Uri.parse(websiteUrl.replace("https", "http")));
|
||||
activity.startActivity(intent);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setMessage(R.string.koreNotFound)
|
||||
.setPositiveButton(R.string.installeKore, new DialogInterface.OnClickListener() {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setMessage(R.string.kore_not_found)
|
||||
.setPositiveButton(R.string.install, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(context.getString(R.string.fdroidKoreUrl)));
|
||||
context.startActivity(intent);
|
||||
intent.setData(Uri.parse(activity.getString(R.string.fdroid_kore_url)));
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
|
||||
@@ -298,32 +320,62 @@ public class ActionBarHandler {
|
||||
}
|
||||
|
||||
public void playAudio() {
|
||||
Intent intent = new Intent();
|
||||
try {
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setDataAndType(Uri.parse(audioStream.url),
|
||||
VideoInfo.getMimeById(audioStream.format));
|
||||
context.startActivity(intent); // HERE !!!
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setMessage(R.string.noPlayerFound)
|
||||
.setPositiveButton(R.string.installStreamPlayer, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(context.getString(R.string.fdroidVLCurl)));
|
||||
context.startActivity(intent);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
Log.i(TAG, "You unlocked a secret unicorn.");
|
||||
}
|
||||
});
|
||||
builder.create().show();
|
||||
|
||||
boolean externalAudioPlayer = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.getBoolean(activity.getString(R.string.use_external_audio_player_key), false);
|
||||
Intent intent;
|
||||
|
||||
if (!externalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 18) {
|
||||
//internal music player: explicit intent
|
||||
if (!BackgroundPlayer.isRunning && videoThumbnail != null) {
|
||||
ActivityCommunicator.getCommunicator()
|
||||
.backgroundPlayerThumbnail = videoThumbnail;
|
||||
intent = new Intent(activity, BackgroundPlayer.class);
|
||||
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
Log.i(TAG, "audioStream is null:" + (audioStream == null));
|
||||
Log.i(TAG, "audioStream.url is null:" + (audioStream.url == null));
|
||||
intent.setDataAndType(Uri.parse(audioStream.url),
|
||||
MediaFormat.getMimeById(audioStream.format));
|
||||
intent.putExtra(BackgroundPlayer.TITLE, videoTitle);
|
||||
intent.putExtra(BackgroundPlayer.WEB_URL, websiteUrl);
|
||||
intent.putExtra(BackgroundPlayer.SERVICE_ID, serviceId);
|
||||
intent.putExtra(BackgroundPlayer.CHANNEL_NAME, channelName);
|
||||
activity.startService(intent);
|
||||
}
|
||||
} else {
|
||||
intent = new Intent();
|
||||
try {
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setDataAndType(Uri.parse(audioStream.url),
|
||||
MediaFormat.getMimeById(audioStream.format));
|
||||
intent.putExtra(Intent.EXTRA_TITLE, videoTitle);
|
||||
intent.putExtra("title", videoTitle);
|
||||
|
||||
activity.startActivity(intent); // HERE !!!
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setMessage(R.string.no_player_found)
|
||||
.setPositiveButton(R.string.install, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(activity.getString(R.string.fdroid_vlc_url)));
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
Log.i(TAG, "You unlocked a secret unicorn.");
|
||||
}
|
||||
});
|
||||
builder.create().show();
|
||||
Log.e(TAG, "Either no Streaming player for audio was installed, or something important crashed:");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 10.08.15.
|
||||
* Created by Christian Schabesberger on 24.12.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* Extractor.java is part of NewPipe.
|
||||
* ActivityCommunicator.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
|
||||
@@ -22,9 +20,23 @@ import android.graphics.Bitmap;
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
public interface Extractor {
|
||||
VideoInfo getVideoInfo(String siteUrl);
|
||||
String getVideoUrl(String videoId);
|
||||
String getVideoId(String videoUrl);
|
||||
/**
|
||||
* Singleton:
|
||||
* Used to send data between certain Activity/Services within the same process.
|
||||
* This can be considered as hack inside the Android universe. **/
|
||||
public class ActivityCommunicator {
|
||||
|
||||
private static ActivityCommunicator activityCommunicator = null;
|
||||
|
||||
public static ActivityCommunicator getCommunicator() {
|
||||
if(activityCommunicator == null) {
|
||||
activityCommunicator = new ActivityCommunicator();
|
||||
}
|
||||
return activityCommunicator;
|
||||
}
|
||||
|
||||
// Thumbnail send from VideoItemDetailFragment to BackgroundPlayer
|
||||
public volatile Bitmap backgroundPlayerThumbnail;
|
||||
}
|
||||
71
app/src/main/java/org/schabi/newpipe/App.java
Normal file
@@ -0,0 +1,71 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import info.guardianproject.netcipher.NetCipher;
|
||||
import info.guardianproject.netcipher.proxy.OrbotHelper;
|
||||
|
||||
/**
|
||||
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
||||
* App.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 App extends Application {
|
||||
|
||||
private static boolean useTor;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
if(prefs.getBoolean(getString(R.string.use_tor_key), false)) {
|
||||
OrbotHelper.requestStartTor(this);
|
||||
configureTor(true);
|
||||
} else {
|
||||
configureTor(false);
|
||||
}
|
||||
|
||||
// DO NOT REMOVE THIS FUNCTION!!!
|
||||
// Otherwise downloadPathPreference has invalid value.
|
||||
SettingsActivity.initSettings(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the proxy settings based on whether Tor should be enabled or not.
|
||||
*/
|
||||
static void configureTor(boolean enabled) {
|
||||
useTor = enabled;
|
||||
if (useTor) {
|
||||
NetCipher.useTor();
|
||||
} else {
|
||||
NetCipher.setProxy(null);
|
||||
}
|
||||
}
|
||||
|
||||
static void checkStartTor(Context context) {
|
||||
if (useTor) {
|
||||
OrbotHelper.requestStartTor(context);
|
||||
}
|
||||
}
|
||||
|
||||
static boolean isUsingTor() {
|
||||
return useTor;
|
||||
}
|
||||
}
|
||||
357
app/src/main/java/org/schabi/newpipe/BackgroundPlayer.java
Normal file
@@ -0,0 +1,357 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaPlayer;
|
||||
import android.net.wifi.WifiManager;
|
||||
import android.os.IBinder;
|
||||
import android.os.PowerManager;
|
||||
import android.support.v7.app.NotificationCompat;
|
||||
import android.util.Log;
|
||||
import android.widget.RemoteViews;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Created by Adam Howard on 08/11/15.
|
||||
* Copyright (c) Adam Howard <achdisposable1@gmail.com> 2015
|
||||
*
|
||||
* BackgroundPlayer.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/>.
|
||||
*/
|
||||
|
||||
/**Plays the audio stream of videos in the background.*/
|
||||
public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPreparedListener*/ {
|
||||
|
||||
private static final String TAG = BackgroundPlayer.class.toString();
|
||||
private static final String ACTION_STOP = TAG + ".STOP";
|
||||
private static final String ACTION_PLAYPAUSE = TAG + ".PLAYPAUSE";
|
||||
|
||||
// Extra intent arguments
|
||||
public static final String TITLE = "title";
|
||||
public static final String WEB_URL = "web_url";
|
||||
public static final String SERVICE_ID = "service_id";
|
||||
public static final String CHANNEL_NAME="channel_name";
|
||||
|
||||
private volatile String webUrl = "";
|
||||
private volatile int serviceId = -1;
|
||||
private volatile String channelName = "";
|
||||
|
||||
// Determines if the service is already running.
|
||||
// Prevents launching the service twice.
|
||||
public static volatile boolean isRunning = false;
|
||||
|
||||
public BackgroundPlayer() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
/*PendingIntent pi = PendingIntent.getActivity(this, 0,
|
||||
new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);*/
|
||||
super.onCreate();
|
||||
}
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
Toast.makeText(this, R.string.background_player_playing_toast,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
|
||||
String source = intent.getDataString();
|
||||
//Log.i(TAG, "backgroundPLayer source:"+source);
|
||||
String videoTitle = intent.getStringExtra(TITLE);
|
||||
webUrl = intent.getStringExtra(WEB_URL);
|
||||
serviceId = intent.getIntExtra(SERVICE_ID, -1);
|
||||
channelName = intent.getStringExtra(CHANNEL_NAME);
|
||||
|
||||
//do nearly everything in a separate thread
|
||||
PlayerThread player = new PlayerThread(source, videoTitle, this);
|
||||
player.start();
|
||||
|
||||
isRunning = true;
|
||||
|
||||
// If we get killed after returning here, don't restart
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
// We don't provide binding (yet?), so return null
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
//Toast.makeText(this, "service done", Toast.LENGTH_SHORT).show();
|
||||
isRunning = false;
|
||||
}
|
||||
|
||||
private class PlayerThread extends Thread {
|
||||
MediaPlayer mediaPlayer;
|
||||
private String source;
|
||||
private String title;
|
||||
private int noteID = TAG.hashCode();
|
||||
private BackgroundPlayer owner;
|
||||
private NotificationManager noteMgr;
|
||||
private WifiManager.WifiLock wifiLock;
|
||||
private Bitmap videoThumbnail = null;
|
||||
private NotificationCompat.Builder noteBuilder;
|
||||
|
||||
public PlayerThread(String src, String title, BackgroundPlayer owner) {
|
||||
this.source = src;
|
||||
this.title = title;
|
||||
this.owner = owner;
|
||||
mediaPlayer = new MediaPlayer();
|
||||
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
mediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);//cpu lock
|
||||
try {
|
||||
mediaPlayer.setDataSource(source);
|
||||
//We are already in a separate worker thread,
|
||||
//so calling the blocking prepare() method should be ok
|
||||
mediaPlayer.prepare();
|
||||
|
||||
} catch (IOException ioe) {
|
||||
ioe.printStackTrace();
|
||||
Log.e(TAG, "video source:" + source);
|
||||
Log.e(TAG, "video title:" + title);
|
||||
//can't do anything useful without a file to play; exit early
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
videoThumbnail = ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail;
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Could not get video thumbnail from ActivityCommunicator");
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
WifiManager wifiMgr = ((WifiManager)getSystemService(Context.WIFI_SERVICE));
|
||||
wifiLock = wifiMgr.createWifiLock(WifiManager.WIFI_MODE_FULL, TAG);
|
||||
|
||||
//listen for end of video
|
||||
mediaPlayer.setOnCompletionListener(new EndListener(wifiLock));
|
||||
|
||||
//get audio focus
|
||||
/*
|
||||
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
|
||||
int result = audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC,
|
||||
AudioManager.AUDIOFOCUS_GAIN);
|
||||
|
||||
if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
|
||||
// could not get audio focus.
|
||||
}*/
|
||||
wifiLock.acquire();
|
||||
mediaPlayer.start();
|
||||
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.setPriority(Integer.MAX_VALUE);
|
||||
filter.addAction(ACTION_PLAYPAUSE);
|
||||
filter.addAction(ACTION_STOP);
|
||||
registerReceiver(broadcastReceiver, filter);
|
||||
|
||||
Notification note = buildNotification();
|
||||
|
||||
startForeground(noteID, note);
|
||||
|
||||
//currently decommissioned progressbar looping update code - works, but doesn't fit inside
|
||||
//Notification.MediaStyle Notification layout.
|
||||
noteMgr = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
|
||||
/*
|
||||
//update every 2s or 4 times in the video, whichever is shorter
|
||||
int sleepTime = Math.min(2000, (int)((double)vidLength/4));
|
||||
while(mediaPlayer.isPlaying()) {
|
||||
noteBuilder.setProgress(vidLength, mediaPlayer.getCurrentPosition(), false);
|
||||
noteMgr.notify(noteID, noteBuilder.build());
|
||||
try {
|
||||
Thread.sleep(sleepTime);
|
||||
} catch (InterruptedException e) {
|
||||
Log.d(TAG, "sleep failure");
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
/**Handles button presses from the notification. */
|
||||
private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
//Log.i(TAG, "received broadcast action:"+action);
|
||||
if(action.equals(ACTION_PLAYPAUSE)) {
|
||||
if(mediaPlayer.isPlaying()) {
|
||||
mediaPlayer.pause();
|
||||
}
|
||||
else {
|
||||
//reacquire CPU lock after auto-releasing it on pause
|
||||
mediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
|
||||
mediaPlayer.start();
|
||||
}
|
||||
}
|
||||
else if(action.equals(ACTION_STOP)) {
|
||||
//this auto-releases CPU lock
|
||||
mediaPlayer.stop();
|
||||
afterPlayCleanup();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private void afterPlayCleanup() {
|
||||
//remove progress bar
|
||||
//noteBuilder.setProgress(0, 0, false);
|
||||
|
||||
//remove notification
|
||||
noteMgr.cancel(noteID);
|
||||
unregisterReceiver(broadcastReceiver);
|
||||
//release mediaPlayer's system resources
|
||||
mediaPlayer.release();
|
||||
|
||||
//release wifilock
|
||||
wifiLock.release();
|
||||
//remove foreground status of service; make BackgroundPlayer killable
|
||||
stopForeground(true);
|
||||
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
private class EndListener implements MediaPlayer.OnCompletionListener {
|
||||
private WifiManager.WifiLock wl;
|
||||
public EndListener(WifiManager.WifiLock wifiLock) {
|
||||
this.wl = wifiLock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompletion(MediaPlayer mp) {
|
||||
afterPlayCleanup();
|
||||
}
|
||||
}
|
||||
|
||||
private Notification buildNotification() {
|
||||
Notification note;
|
||||
Resources res = getApplicationContext().getResources();
|
||||
noteBuilder = new NotificationCompat.Builder(owner);
|
||||
|
||||
PendingIntent playPI = PendingIntent.getBroadcast(owner, noteID,
|
||||
new Intent(ACTION_PLAYPAUSE), PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
PendingIntent stopPI = PendingIntent.getBroadcast(owner, noteID,
|
||||
new Intent(ACTION_STOP), PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
/*
|
||||
NotificationCompat.Action pauseButton = new NotificationCompat.Action.Builder
|
||||
(R.drawable.ic_pause_white_24dp, "Pause", playPI).build();
|
||||
*/
|
||||
|
||||
//build intent to return to video, on tapping notification
|
||||
Intent openDetailView = new Intent(getApplicationContext(),
|
||||
VideoItemDetailActivity.class);
|
||||
openDetailView.putExtra(VideoItemDetailFragment.STREAMING_SERVICE, serviceId);
|
||||
openDetailView.putExtra(VideoItemDetailFragment.VIDEO_URL, webUrl);
|
||||
openDetailView.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
|
||||
noteBuilder
|
||||
.setOngoing(true)
|
||||
.setDeleteIntent(stopPI)
|
||||
//doesn't fit with Notification.MediaStyle
|
||||
//.setProgress(vidLength, 0, false)
|
||||
.setSmallIcon(R.drawable.ic_play_circle_filled_white_24dp)
|
||||
.setTicker(
|
||||
String.format(res.getString(
|
||||
R.string.background_player_time_text), title))
|
||||
.setContentIntent(PendingIntent.getActivity(getApplicationContext(),
|
||||
noteID, openDetailView,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT));
|
||||
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT < 21) {
|
||||
|
||||
NotificationCompat.Action playButton = new NotificationCompat.Action.Builder
|
||||
(R.drawable.ic_play_arrow_white_48dp,
|
||||
res.getString(R.string.play_btn_text), playPI).build();
|
||||
|
||||
noteBuilder
|
||||
.setContentTitle(title)
|
||||
//really? Id like to put something more helpful here.
|
||||
//was more of a placeholder than anything else. -medavox
|
||||
//.setContentText("NewPipe is playing in the background")
|
||||
.setContentText(channelName)
|
||||
//.setAutoCancel(!mediaPlayer.isPlaying())
|
||||
.setDeleteIntent(stopPI)
|
||||
//doesn't fit with Notification.MediaStyle
|
||||
//.setProgress(vidLength, 0, false)
|
||||
.setLargeIcon(videoThumbnail)
|
||||
.addAction(playButton);
|
||||
//.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
//.setLargeIcon(cover)
|
||||
|
||||
//is wrapping this in an SDK version check really necessary,
|
||||
// if we're using NotificationCompat?
|
||||
// the compat libraries should handle this, right? -medavox
|
||||
if (android.os.Build.VERSION.SDK_INT >= 16)
|
||||
noteBuilder.setPriority(Notification.PRIORITY_LOW);
|
||||
|
||||
noteBuilder.setStyle(new NotificationCompat.MediaStyle()
|
||||
//.setMediaSession(mMediaSession.getSessionToken())
|
||||
.setShowActionsInCompactView(new int[]{0})
|
||||
.setShowCancelButton(true)
|
||||
.setCancelButtonIntent(stopPI));
|
||||
if (videoThumbnail != null) {
|
||||
noteBuilder.setLargeIcon(videoThumbnail);
|
||||
}
|
||||
note = noteBuilder.build();
|
||||
} else {
|
||||
RemoteViews view =
|
||||
new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification);
|
||||
view.setImageViewBitmap(R.id.backgroundCover, videoThumbnail);
|
||||
view.setTextViewText(R.id.backgroundSongName, title);
|
||||
view.setTextViewText(R.id.backgroundArtist, channelName);
|
||||
view.setOnClickPendingIntent(R.id.backgroundStop, stopPI);
|
||||
view.setOnClickPendingIntent(R.id.backgroundPlayPause, playPI);
|
||||
|
||||
//possibly found the expandedView problem,
|
||||
//but can't test it as I don't have a 5.0 device. -medavox
|
||||
RemoteViews expandedView =
|
||||
new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification_expanded);
|
||||
expandedView.setImageViewBitmap(R.id.backgroundCover, videoThumbnail);
|
||||
expandedView.setTextViewText(R.id.backgroundSongName, title);
|
||||
expandedView.setTextViewText(R.id.backgroundArtist, channelName);
|
||||
expandedView.setOnClickPendingIntent(R.id.backgroundStop, stopPI);
|
||||
expandedView.setOnClickPendingIntent(R.id.backgroundPlayPause, playPI);
|
||||
|
||||
noteBuilder.setCategory(Notification.CATEGORY_TRANSPORT);
|
||||
|
||||
//Make notification appear on lockscreen
|
||||
noteBuilder.setVisibility(Notification.VISIBILITY_PUBLIC);
|
||||
|
||||
note = noteBuilder.build();
|
||||
note.contentView = view;
|
||||
|
||||
//todo: This never shows up. I was not able to figure out why:
|
||||
note.bigContentView = expandedView;
|
||||
}
|
||||
|
||||
return note;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,29 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.app.DownloadManager;
|
||||
import android.app.Notification;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 21.09.15.
|
||||
@@ -42,48 +53,102 @@ public class DownloadDialog extends DialogFragment {
|
||||
public static final String FILE_SUFFIX_VIDEO = "file_suffix_video";
|
||||
public static final String AUDIO_URL = "audio_url";
|
||||
public static final String VIDEO_URL = "video_url";
|
||||
Bundle arguments;
|
||||
private Bundle arguments;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
arguments = getArguments();
|
||||
super.onCreateDialog(savedInstanceState);
|
||||
if(ContextCompat.checkSelfPermission(this.getContext(),Manifest.permission.WRITE_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED)
|
||||
ActivityCompat.requestPermissions(getActivity(),new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},0);
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||
builder.setTitle(R.string.downloadDialogTitle)
|
||||
.setItems(R.array.downloadOptions, new DialogInterface.OnClickListener() {
|
||||
builder.setTitle(R.string.download_dialog_title)
|
||||
.setItems(R.array.download_options, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
Context context = getActivity();
|
||||
SharedPreferences defaultPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
String suffix = "";
|
||||
String title = arguments.getString(TITLE);
|
||||
String url = "";
|
||||
File downloadDir = NewPipeSettings.getDownloadFolder();
|
||||
switch(which) {
|
||||
case 0: // Video
|
||||
suffix = arguments.getString(FILE_SUFFIX_VIDEO);
|
||||
url = arguments.getString(VIDEO_URL);
|
||||
downloadDir = NewPipeSettings.getVideoDownloadFolder(context);
|
||||
break;
|
||||
case 1:
|
||||
suffix = arguments.getString(FILE_SUFFIX_AUDIO);
|
||||
url = arguments.getString(AUDIO_URL);
|
||||
downloadDir = NewPipeSettings.getAudioDownloadFolder(context);
|
||||
break;
|
||||
default:
|
||||
Log.d(TAG, "lolz");
|
||||
}
|
||||
DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
|
||||
DownloadManager.Request request = new DownloadManager.Request(
|
||||
Uri.parse(url));
|
||||
request.setDestinationUri(Uri.fromFile(new File(
|
||||
defaultPreferences.getString("download_path_preference", "/storage/emulated/0/NewPipe")
|
||||
+ "/" + title + suffix)));
|
||||
try {
|
||||
dm.enqueue(request);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
if(!downloadDir.exists()) {
|
||||
//attempt to create directory
|
||||
boolean mkdir = downloadDir.mkdirs();
|
||||
if(!mkdir && !downloadDir.isDirectory()) {
|
||||
String message = context.getString(R.string.err_dir_create,downloadDir.toString());
|
||||
Log.e(TAG, message);
|
||||
Toast.makeText(context,message , Toast.LENGTH_LONG).show();
|
||||
|
||||
return;
|
||||
}
|
||||
String message = context.getString(R.string.info_dir_created,downloadDir.toString());
|
||||
Log.e(TAG, message);
|
||||
Toast.makeText(context,message , Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
File saveFilePath = new File(downloadDir,createFileName(title) + suffix);
|
||||
|
||||
long id = 0;
|
||||
if (App.isUsingTor()) {
|
||||
// if using Tor, do not use DownloadManager because the proxy cannot be set
|
||||
Downloader.downloadFile(getContext(), url, saveFilePath, title);
|
||||
} else {
|
||||
DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
|
||||
DownloadManager.Request request = new DownloadManager.Request(
|
||||
Uri.parse(url));
|
||||
request.setDestinationUri(Uri.fromFile(saveFilePath));
|
||||
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
|
||||
|
||||
request.setTitle(title);
|
||||
request.setDescription("'" + url +
|
||||
"' => '" + saveFilePath + "'");
|
||||
request.allowScanningByMediaScanner();
|
||||
|
||||
try {
|
||||
id = dm.enqueue(request);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
Log.i(TAG,"Started downloading '" + url +
|
||||
"' => '" + saveFilePath + "' #" + id);
|
||||
}
|
||||
});
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
/**
|
||||
* #143 #44 #42 #22: make shure that the filename does not contain illegal chars.
|
||||
* This should fix some of the "cannot download" problems.
|
||||
* */
|
||||
private String createFileName(String fName) {
|
||||
// from http://eng-przemelek.blogspot.de/2009/07/how-to-create-valid-file-name.html
|
||||
|
||||
List<String> forbiddenCharsPatterns = new ArrayList<String> ();
|
||||
forbiddenCharsPatterns.add("[:]+"); // Mac OS, but it looks that also Windows XP
|
||||
forbiddenCharsPatterns.add("[\\*\"/\\\\\\[\\]\\:\\;\\|\\=\\,]+"); // Windows
|
||||
forbiddenCharsPatterns.add("[^\\w\\d\\.]+"); // last chance... only latin letters and digits
|
||||
String nameToTest = fName;
|
||||
for (String pattern : forbiddenCharsPatterns) {
|
||||
nameToTest = nameToTest.replaceAll(pattern, "_");
|
||||
}
|
||||
return nameToTest;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,30 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.AsyncTask;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
|
||||
import info.guardianproject.netcipher.NetCipher;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 14.08.15.
|
||||
@@ -25,15 +46,56 @@ import java.net.URL;
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class Downloader {
|
||||
|
||||
public class Downloader extends AsyncTask<Void, Integer, Void> {
|
||||
public static final String TAG = "Downloader";
|
||||
private static final String USER_AGENT = "Mozilla/5.0";
|
||||
public static String download(String siteUrl) {
|
||||
|
||||
StringBuffer response = new StringBuffer();
|
||||
private NotificationManager nm;
|
||||
private NotificationCompat.Builder builder;
|
||||
private int notifyId = 0x1234;
|
||||
private int fileSize = 0xffffffff;
|
||||
|
||||
private final Context context;
|
||||
private final String fileURL;
|
||||
private final File saveFilePath;
|
||||
private final String title;
|
||||
|
||||
private final String debugContext;
|
||||
|
||||
public Downloader(Context context, String fileURL, File saveFilePath, String title) {
|
||||
this.context = context;
|
||||
this.fileURL = fileURL;
|
||||
this.saveFilePath = saveFilePath;
|
||||
this.title = title;
|
||||
|
||||
this.debugContext = "'" + fileURL +
|
||||
"' => '" + saveFilePath + "'";
|
||||
}
|
||||
|
||||
/**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 language the language (usually a 2-character code) to set as the preferred language
|
||||
* @return the contents of the specified text file*/
|
||||
public static String download(String siteUrl, String language) {
|
||||
String ret = "";
|
||||
try {
|
||||
URL url = new URL(siteUrl);
|
||||
HttpURLConnection con = (HttpURLConnection) url.openConnection();
|
||||
HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
|
||||
con.setRequestProperty("Accept-Language", language);
|
||||
ret = dl(con);
|
||||
}
|
||||
catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**Common functionality between download(String url) and download(String url, String language)*/
|
||||
private static String dl(HttpsURLConnection con) throws IOException {
|
||||
StringBuilder response = new StringBuilder();
|
||||
|
||||
try {
|
||||
con.setRequestMethod("GET");
|
||||
con.setRequestProperty("User-Agent", USER_AGENT);
|
||||
|
||||
@@ -45,9 +107,130 @@ public class Downloader {
|
||||
response.append(inputLine);
|
||||
}
|
||||
in.close();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
|
||||
}
|
||||
catch(UnknownHostException uhe) {//thrown when there's no internet connection
|
||||
uhe.printStackTrace();
|
||||
//Toast.makeText(getActivity(), uhe.getMessage(), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
return response.toString();
|
||||
}
|
||||
|
||||
/**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*/
|
||||
public static String download(String siteUrl) {
|
||||
String ret = "";
|
||||
|
||||
try {
|
||||
URL url = new URL(siteUrl);
|
||||
HttpsURLConnection con = NetCipher.getHttpsURLConnection(url);
|
||||
ret = dl(con);
|
||||
}
|
||||
catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a file from a URL in the background using an {@link AsyncTask}.
|
||||
*
|
||||
* @param fileURL HTTP URL of the file to be downloaded
|
||||
* @param saveFilePath path of the directory to save the file
|
||||
* @param title
|
||||
* @throws IOException
|
||||
*/
|
||||
public static void downloadFile(final Context context, final String fileURL, final File saveFilePath, String title) {
|
||||
new Downloader(context, fileURL, saveFilePath, title).execute();
|
||||
}
|
||||
|
||||
/** AsyncTask impl: executed in gui thread */
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
super.onPreExecute();
|
||||
nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
Drawable icon = context.getResources().getDrawable(R.mipmap.ic_launcher);
|
||||
builder = new NotificationCompat.Builder(context)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
.setLargeIcon(((BitmapDrawable) icon).getBitmap())
|
||||
.setContentTitle(saveFilePath.getName())
|
||||
.setContentText(saveFilePath.getAbsolutePath())
|
||||
.setProgress(fileSize, 0, false);
|
||||
nm.notify(notifyId, builder.build());
|
||||
}
|
||||
|
||||
/** AsyncTask impl: executed in background thread does the download */
|
||||
@Override
|
||||
protected Void doInBackground(Void... voids) {
|
||||
HttpsURLConnection con = null;
|
||||
InputStream inputStream = null;
|
||||
FileOutputStream outputStream = null;
|
||||
try {
|
||||
con = NetCipher.getHttpsURLConnection(fileURL);
|
||||
int responseCode = con.getResponseCode();
|
||||
|
||||
// always check HTTP response code first
|
||||
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||
fileSize = con.getContentLength();
|
||||
inputStream = new BufferedInputStream(con.getInputStream());
|
||||
outputStream = new FileOutputStream(saveFilePath);
|
||||
|
||||
int bufferSize = 8192;
|
||||
int downloaded = 0;
|
||||
|
||||
int bytesRead = -1;
|
||||
byte[] buffer = new byte[bufferSize];
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
downloaded += bytesRead;
|
||||
if (downloaded % 50000 < bufferSize) {
|
||||
publishProgress(downloaded);
|
||||
}
|
||||
}
|
||||
|
||||
publishProgress(bufferSize);
|
||||
|
||||
} else {
|
||||
Log.i(TAG, "No file to download. Server replied HTTP code: " + responseCode);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "No file to download. Server replied HTTP code: ", e);
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
try {
|
||||
if (outputStream != null) {
|
||||
outputStream.close();
|
||||
outputStream = null;
|
||||
}
|
||||
if (inputStream != null) {
|
||||
inputStream.close();
|
||||
inputStream = null;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (con != null) {
|
||||
con.disconnect();
|
||||
con = null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onProgressUpdate(Integer... progress) {
|
||||
builder.setProgress(fileSize, progress[0], false);
|
||||
nm.notify(notifyId, builder.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void aVoid) {
|
||||
super.onPostExecute(aVoid);
|
||||
nm.cancel(notifyId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
54
app/src/main/java/org/schabi/newpipe/ExitActivity.java
Normal file
@@ -0,0 +1,54 @@
|
||||
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
||||
/**
|
||||
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
||||
* ExitActivity.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 ExitActivity extends Activity {
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
finishAndRemoveTask();
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
public static void exitAndRemoveFromRecentApps(Activity activity) {
|
||||
Intent intent = new Intent(activity, ExitActivity.class);
|
||||
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
| Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
|
||||
| Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
| Intent.FLAG_ACTIVITY_NO_ANIMATION);
|
||||
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
}
|
||||
93
app/src/main/java/org/schabi/newpipe/Localization.java
Normal file
@@ -0,0 +1,93 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.NumberFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Created by chschtsch on 12/29/15.
|
||||
*
|
||||
* Copyright (C) Gregory Arkhipov 2015
|
||||
* Localization.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 Localization {
|
||||
|
||||
public static Locale getPreferredLocale(Context context) {
|
||||
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
|
||||
String languageCode = sp.getString(String.valueOf(R.string.search_language_key),
|
||||
context.getString(R.string.default_language_value));
|
||||
|
||||
if(languageCode.length() == 2) {
|
||||
return new Locale(languageCode);
|
||||
}
|
||||
else if(languageCode.contains("_")) {
|
||||
String country = languageCode
|
||||
.substring(languageCode.indexOf("_"), languageCode.length());
|
||||
return new Locale(languageCode.substring(0, 2), country);
|
||||
}
|
||||
return Locale.getDefault();
|
||||
}
|
||||
|
||||
public static String localizeViewCount(long viewCount, Context context) {
|
||||
Locale locale = getPreferredLocale(context);
|
||||
|
||||
Resources res = context.getResources();
|
||||
String viewsString = res.getString(R.string.view_count_text);
|
||||
|
||||
NumberFormat nf = NumberFormat.getInstance(locale);
|
||||
String formattedViewCount = nf.format(viewCount);
|
||||
return String.format(viewsString, formattedViewCount);
|
||||
}
|
||||
|
||||
public static String localizeNumber(long number, Context context) {
|
||||
Locale locale = getPreferredLocale(context);
|
||||
NumberFormat nf = NumberFormat.getInstance(locale);
|
||||
return nf.format(number);
|
||||
}
|
||||
|
||||
private static String formatDate(String date, Context context) {
|
||||
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 localizeDate(String date, Context context) {
|
||||
Resources res = context.getResources();
|
||||
String dateString = res.getString(R.string.upload_date_text);
|
||||
|
||||
String formattedDate = formatDate(date, context);
|
||||
return String.format(dateString, formattedDate);
|
||||
}
|
||||
}
|
||||
72
app/src/main/java/org/schabi/newpipe/NewPipeSettings.java
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Created by k3b on 07.01.2016.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* NewPipeSettings.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/>.
|
||||
*/
|
||||
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Environment;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Helper for global settings
|
||||
*/
|
||||
public class NewPipeSettings {
|
||||
public static void initSettings(Context context) {
|
||||
PreferenceManager.setDefaultValues(context, R.xml.settings, false);
|
||||
getVideoDownloadFolder(context);
|
||||
getAudioDownloadFolder(context);
|
||||
}
|
||||
|
||||
public static File getDownloadFolder() {
|
||||
return getFolder(Environment.DIRECTORY_DOWNLOADS);
|
||||
}
|
||||
|
||||
public static File getVideoDownloadFolder(Context context) {
|
||||
return getFolder(context, R.string.download_path_key, Environment.DIRECTORY_MOVIES);
|
||||
}
|
||||
|
||||
public static File getAudioDownloadFolder(Context context) {
|
||||
return getFolder(context, R.string.download_path_audio_key, Environment.DIRECTORY_MUSIC);
|
||||
}
|
||||
|
||||
private static File getFolder(Context context, int keyID, String defaultDirectoryName) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
final String key = context.getString(keyID);
|
||||
String downloadPath = prefs.getString(key, null);
|
||||
if ((downloadPath != null) && (!downloadPath.isEmpty())) return new File(downloadPath.trim());
|
||||
|
||||
final File folder = getFolder(defaultDirectoryName);
|
||||
SharedPreferences.Editor spEditor = prefs.edit();
|
||||
spEditor.putString(key
|
||||
, new File(folder,"NewPipe").getAbsolutePath());
|
||||
spEditor.apply();
|
||||
return folder;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static File getFolder(String defaultDirectoryName) {
|
||||
return new File(Environment.getExternalStorageDirectory(),defaultDirectoryName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
||||
/**
|
||||
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
||||
* PanicResponderActivity.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 PanicResponderActivity extends Activity {
|
||||
|
||||
public static final String PANIC_TRIGGER_ACTION = "info.guardianproject.panic.action.TRIGGER";
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Intent intent = getIntent();
|
||||
if (intent != null && PANIC_TRIGGER_ACTION.equals(intent.getAction())) {
|
||||
// TODO explicitly clear the search results once they are restored when the app restarts
|
||||
// or if the app reloads the current video after being killed, that should be cleared also
|
||||
ExitActivity.exitAndRemoveFromRecentApps(this);
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
finishAndRemoveTask();
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.media.MediaPlayer;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
@@ -14,18 +15,15 @@ import android.support.v7.app.AppCompatActivity;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.Display;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.Surface;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.MediaController;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.VideoView;
|
||||
|
||||
/**
|
||||
@@ -55,9 +53,9 @@ public class PlayVideoActivity extends AppCompatActivity {
|
||||
public static final String STREAM_URL = "stream_url";
|
||||
public static final String VIDEO_TITLE = "video_title";
|
||||
private static final String POSITION = "position";
|
||||
public static final String START_POSITION = "start_position";
|
||||
|
||||
private static final long HIDING_DELAY = 3000;
|
||||
private static final long TAB_HIDING_DELAY = 100;
|
||||
|
||||
private String videoUrl = "";
|
||||
|
||||
@@ -72,6 +70,9 @@ public class PlayVideoActivity extends AppCompatActivity {
|
||||
private boolean isLandscape = true;
|
||||
private boolean hasSoftKeys = false;
|
||||
|
||||
private SharedPreferences prefs;
|
||||
private static final String PREF_IS_LANDSCAPE = "is_landscape";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -79,15 +80,41 @@ public class PlayVideoActivity extends AppCompatActivity {
|
||||
setContentView(R.layout.activity_play_video);
|
||||
|
||||
isLandscape = checkIfLandscape();
|
||||
hasSoftKeys = checkIfhasSoftKeys();
|
||||
hasSoftKeys = checkIfHasSoftKeys();
|
||||
|
||||
actionBar = getSupportActionBar();
|
||||
assert actionBar != null;
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
Intent intent = getIntent();
|
||||
if(mediaController == null) {
|
||||
mediaController = new MediaController(this);
|
||||
//prevents back button hiding media controller controls (after showing them)
|
||||
//instead of exiting video
|
||||
//see http://stackoverflow.com/questions/6051825
|
||||
//also solves https://github.com/theScrabi/NewPipe/issues/99
|
||||
mediaController = new MediaController(this) {
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
int keyCode = event.getKeyCode();
|
||||
final boolean uniqueDown = event.getRepeatCount() == 0
|
||||
&& event.getAction() == KeyEvent.ACTION_DOWN;
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
if (uniqueDown)
|
||||
{
|
||||
if (isShowing()) {
|
||||
finish();
|
||||
} else {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
position = intent.getIntExtra(START_POSITION, 0)*1000;//convert from seconds to milliseconds
|
||||
|
||||
videoView = (VideoView) findViewById(R.id.video_view);
|
||||
progressBar = (ProgressBar) findViewById(R.id.play_video_progress_bar);
|
||||
try {
|
||||
@@ -133,15 +160,16 @@ public class PlayVideoActivity extends AppCompatActivity {
|
||||
}
|
||||
});
|
||||
|
||||
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
if (android.os.Build.VERSION.SDK_INT >= 17) {
|
||||
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
|
||||
}
|
||||
|
||||
prefs = getPreferences(Context.MODE_PRIVATE);
|
||||
if(prefs.getBoolean(PREF_IS_LANDSCAPE, false) && !isLandscape) {
|
||||
toggleOrientation();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -153,17 +181,24 @@ public class PlayVideoActivity extends AppCompatActivity {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
videoView.pause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
App.checkStartTor(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
prefs = getPreferences(Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
int id = item.getItemId();
|
||||
@@ -176,17 +211,10 @@ public class PlayVideoActivity extends AppCompatActivity {
|
||||
intent.setAction(Intent.ACTION_SEND);
|
||||
intent.putExtra(Intent.EXTRA_TEXT, videoUrl);
|
||||
intent.setType("text/plain");
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.shareDialogTitle)));
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title)));
|
||||
break;
|
||||
case R.id.menu_item_screen_rotation:
|
||||
Display display = ((WindowManager) getSystemService(WINDOW_SERVICE)).getDefaultDisplay();
|
||||
if(display.getRotation() == Surface.ROTATION_0
|
||||
|| display.getRotation() == Surface.ROTATION_180) {
|
||||
setRequestedOrientation (ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
|
||||
} else if(display.getRotation() == Surface.ROTATION_90
|
||||
|| display.getRotation() == Surface.ROTATION_270) {
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
|
||||
}
|
||||
toggleOrientation();
|
||||
break;
|
||||
default:
|
||||
Log.e(TAG, "Error: MenuItem not known");
|
||||
@@ -201,10 +229,10 @@ public class PlayVideoActivity extends AppCompatActivity {
|
||||
|
||||
if (config.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
isLandscape = true;
|
||||
adjustMediaControllMetrics();
|
||||
adjustMediaControlMetrics();
|
||||
} else if (config.orientation == Configuration.ORIENTATION_PORTRAIT){
|
||||
isLandscape = false;
|
||||
adjustMediaControllMetrics();
|
||||
adjustMediaControlMetrics();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,7 +255,7 @@ public class PlayVideoActivity extends AppCompatActivity {
|
||||
uiIsHidden = false;
|
||||
mediaController.show(100000);
|
||||
actionBar.show();
|
||||
adjustMediaControllMetrics();
|
||||
adjustMediaControlMetrics();
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
Handler handler = new Handler();
|
||||
handler.postDelayed(new Runnable() {
|
||||
@@ -248,16 +276,18 @@ public class PlayVideoActivity extends AppCompatActivity {
|
||||
uiIsHidden = true;
|
||||
actionBar.hide();
|
||||
mediaController.hide();
|
||||
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
|
||||
if (android.os.Build.VERSION.SDK_INT >= 17) {
|
||||
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
|
||||
}
|
||||
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
}
|
||||
|
||||
private void adjustMediaControllMetrics() {
|
||||
private void adjustMediaControlMetrics() {
|
||||
MediaController.LayoutParams mediaControllerLayout
|
||||
= new MediaController.LayoutParams(MediaController.LayoutParams.MATCH_PARENT,
|
||||
MediaController.LayoutParams.WRAP_CONTENT);
|
||||
@@ -272,12 +302,10 @@ public class PlayVideoActivity extends AppCompatActivity {
|
||||
mediaController.setLayoutParams(mediaControllerLayout);
|
||||
}
|
||||
|
||||
private boolean checkIfhasSoftKeys(){
|
||||
if(Build.VERSION.SDK_INT >= 17) {
|
||||
return getNavigationBarHeight() != 0 || getNavigationBarWidth() != 0;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
private boolean checkIfHasSoftKeys(){
|
||||
return Build.VERSION.SDK_INT >= 17 ||
|
||||
getNavigationBarHeight() != 0 ||
|
||||
getNavigationBarWidth() != 0;
|
||||
}
|
||||
|
||||
private int getNavigationBarHeight() {
|
||||
@@ -314,9 +342,22 @@ public class PlayVideoActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean checkIfLandscape() {
|
||||
private boolean checkIfLandscape() {
|
||||
DisplayMetrics displayMetrics = new DisplayMetrics();
|
||||
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
|
||||
return displayMetrics.heightPixels < displayMetrics.widthPixels;
|
||||
}
|
||||
|
||||
private void toggleOrientation() {
|
||||
if(isLandscape) {
|
||||
isLandscape = false;
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
|
||||
} else {
|
||||
isLandscape = true;
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
|
||||
}
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.putBoolean(PREF_IS_LANDSCAPE, isLandscape);
|
||||
editor.apply();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,29 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.preference.CheckBoxPreference;
|
||||
import android.preference.EditTextPreference;
|
||||
import android.preference.ListPreference;
|
||||
import android.preference.PreferenceActivity;
|
||||
import android.preference.PreferenceFragment;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.LayoutRes;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AppCompatDelegate;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import info.guardianproject.netcipher.proxy.OrbotHelper;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 31.08.15.
|
||||
*
|
||||
@@ -38,8 +44,9 @@ import android.view.ViewGroup;
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class SettingsActivity extends PreferenceActivity {
|
||||
public class SettingsActivity extends PreferenceActivity {
|
||||
|
||||
private static final int REQUEST_INSTALL_ORBOT = 0x1234;
|
||||
private AppCompatDelegate mDelegate = null;
|
||||
|
||||
@Override
|
||||
@@ -56,12 +63,108 @@ public class SettingsActivity extends PreferenceActivity {
|
||||
|
||||
}
|
||||
|
||||
public static class SettingsFragment extends PreferenceFragment {
|
||||
public static class SettingsFragment extends PreferenceFragment{
|
||||
SharedPreferences.OnSharedPreferenceChangeListener prefListener;
|
||||
|
||||
// get keys
|
||||
String DEFAULT_RESOLUTION_PREFERENCE;
|
||||
String DEFAULT_AUDIO_FORMAT_PREFERENCE;
|
||||
String SEARCH_LANGUAGE_PREFERENCE;
|
||||
String DOWNLOAD_PATH_PREFERENCE;
|
||||
String DOWNLOAD_PATH_AUDIO_PREFERENCE;
|
||||
String USE_TOR_KEY;
|
||||
|
||||
private ListPreference defaultResolutionPreference;
|
||||
private ListPreference defaultAudioFormatPreference;
|
||||
private ListPreference searchLanguagePreference;
|
||||
private EditTextPreference downloadPathPreference;
|
||||
private EditTextPreference downloadPathAudioPreference;
|
||||
private CheckBoxPreference useTorCheckBox;
|
||||
private SharedPreferences defaultPreferences;
|
||||
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
addPreferencesFromResource(R.xml.settings_screen);
|
||||
addPreferencesFromResource(R.xml.settings);
|
||||
|
||||
final Activity activity = getActivity();
|
||||
|
||||
defaultPreferences = PreferenceManager.getDefaultSharedPreferences(activity);
|
||||
|
||||
// get keys
|
||||
DEFAULT_RESOLUTION_PREFERENCE =getString(R.string.default_resolution_key);
|
||||
DEFAULT_AUDIO_FORMAT_PREFERENCE =getString(R.string.default_audio_format_key);
|
||||
SEARCH_LANGUAGE_PREFERENCE =getString(R.string.search_language_key);
|
||||
DOWNLOAD_PATH_PREFERENCE = getString(R.string.download_path_key);
|
||||
DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key);
|
||||
USE_TOR_KEY = getString(R.string.use_tor_key);
|
||||
|
||||
// get pref objects
|
||||
defaultResolutionPreference =
|
||||
(ListPreference) findPreference(DEFAULT_RESOLUTION_PREFERENCE);
|
||||
defaultAudioFormatPreference =
|
||||
(ListPreference) findPreference(DEFAULT_AUDIO_FORMAT_PREFERENCE);
|
||||
searchLanguagePreference =
|
||||
(ListPreference) findPreference(SEARCH_LANGUAGE_PREFERENCE);
|
||||
downloadPathPreference =
|
||||
(EditTextPreference) findPreference(DOWNLOAD_PATH_PREFERENCE);
|
||||
downloadPathAudioPreference =
|
||||
(EditTextPreference) findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE);
|
||||
useTorCheckBox = (CheckBoxPreference) findPreference(USE_TOR_KEY);
|
||||
|
||||
prefListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
|
||||
String key) {
|
||||
Activity a = getActivity();
|
||||
updateSummary();
|
||||
|
||||
if (defaultPreferences.getBoolean(USE_TOR_KEY, false)) {
|
||||
if (OrbotHelper.isOrbotInstalled(a)) {
|
||||
App.configureTor(true);
|
||||
OrbotHelper.requestStartTor(a);
|
||||
} else {
|
||||
Intent intent = OrbotHelper.getOrbotInstallIntent(a);
|
||||
a.startActivityForResult(intent, REQUEST_INSTALL_ORBOT);
|
||||
}
|
||||
} else {
|
||||
App.configureTor(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
defaultPreferences.registerOnSharedPreferenceChangeListener(prefListener);
|
||||
|
||||
updateSummary();
|
||||
}
|
||||
|
||||
// This is used to show the status of some preference in the description
|
||||
private void updateSummary() {
|
||||
defaultResolutionPreference.setSummary(
|
||||
defaultPreferences.getString(DEFAULT_RESOLUTION_PREFERENCE,
|
||||
getString(R.string.default_resolution_value)));
|
||||
defaultAudioFormatPreference.setSummary(
|
||||
defaultPreferences.getString(DEFAULT_AUDIO_FORMAT_PREFERENCE,
|
||||
getString(R.string.default_audio_format_value)));
|
||||
searchLanguagePreference.setSummary(
|
||||
defaultPreferences.getString(SEARCH_LANGUAGE_PREFERENCE,
|
||||
getString(R.string.default_language_value)));
|
||||
downloadPathPreference.setSummary(
|
||||
defaultPreferences.getString(DOWNLOAD_PATH_PREFERENCE,
|
||||
getString(R.string.download_path_summary)));
|
||||
downloadPathAudioPreference.setSummary(
|
||||
defaultPreferences.getString(DOWNLOAD_PATH_AUDIO_PREFERENCE,
|
||||
getString(R.string.download_path_audio_summary)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
// try to start tor regardless of resultCode since clicking back after
|
||||
// installing the app does not necessarily return RESULT_OK
|
||||
App.configureTor(requestCode == REQUEST_INSTALL_ORBOT
|
||||
&& OrbotHelper.requestStartTor(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -70,14 +173,11 @@ public class SettingsActivity extends PreferenceActivity {
|
||||
getDelegate().onPostCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
public ActionBar getSupportActionBar() {
|
||||
private ActionBar getSupportActionBar() {
|
||||
return getDelegate().getSupportActionBar();
|
||||
}
|
||||
|
||||
public void setSupportActionBar(@Nullable Toolbar toolbar) {
|
||||
getDelegate().setSupportActionBar(toolbar);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public MenuInflater getMenuInflater() {
|
||||
return getDelegate().getMenuInflater();
|
||||
@@ -154,15 +254,6 @@ public class SettingsActivity extends PreferenceActivity {
|
||||
}
|
||||
|
||||
public static void initSettings(Context context) {
|
||||
PreferenceManager.setDefaultValues(context, R.xml.settings_screen, false);
|
||||
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
if(sp.getString(context.getString(R.string.downloadPathPreference), "").isEmpty()){
|
||||
SharedPreferences.Editor spEditor = sp.edit();
|
||||
String newPipeDownloadStorage =
|
||||
Environment.getExternalStorageDirectory().getAbsolutePath() + "/NewPipe";
|
||||
spEditor.putString(context.getString(R.string.downloadPathPreference)
|
||||
, newPipeDownloadStorage);
|
||||
spEditor.commit();
|
||||
}
|
||||
NewPipeSettings.initSettings(context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 26.08.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* VideoInfo.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/>.
|
||||
*/
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Vector;
|
||||
|
||||
public class VideoInfo {
|
||||
|
||||
private static final String TAG = VideoInfo.class.toString();
|
||||
|
||||
// format identifier
|
||||
public static final int I_MPEG_4 = 0x0;
|
||||
public static final int I_3GPP = 0x1;
|
||||
public static final int I_WEBM = 0x2;
|
||||
public static final int I_M4A = 0x3;
|
||||
public static final int I_WEBMA = 0x4;
|
||||
|
||||
// format name
|
||||
public static final String F_MPEG_4 = "MPEG-4";
|
||||
public static final String F_3GPP = "3GPP";
|
||||
public static final String F_WEBM = "WebM";
|
||||
public static final String F_M4A = "m4a";
|
||||
public static final String F_WEBMA = "WebM";
|
||||
|
||||
// file suffix
|
||||
public static final String C_MPEG_4 = "mp4";
|
||||
public static final String C_3GPP = "3gp";
|
||||
public static final String C_WEBM = "webm";
|
||||
public static final String C_M4A = "m4a";
|
||||
public static final String C_WEBMA = "webm";
|
||||
|
||||
// mimeType
|
||||
public static final String M_MPEG_4 = "video/mp4";
|
||||
public static final String M_3GPP = "video/3gpp";
|
||||
public static final String M_WEBM = "video/webm";
|
||||
public static final String M_M4A = "audio/mp4";
|
||||
public static final String M_WEBMA = "audio/webm";
|
||||
|
||||
public static final int VIDEO_AVAILABLE = 0x00;
|
||||
public static final int VIDEO_UNAVAILABLE = 0x01;
|
||||
public static final int VIDEO_UNAVAILABLE_GEMA = 0x02;
|
||||
|
||||
public static String getNameById(int id) {
|
||||
switch(id) {
|
||||
case I_MPEG_4: return F_MPEG_4;
|
||||
case I_3GPP: return F_3GPP;
|
||||
case I_WEBM: return F_WEBM;
|
||||
case I_M4A: return F_M4A;
|
||||
case I_WEBMA: return F_WEBMA;
|
||||
default: Log.e(TAG, "format not known: " +
|
||||
Integer.toString(id) + "call the programmer he messed it up.");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public static String getSuffixById(int id) {
|
||||
switch(id) {
|
||||
case I_MPEG_4: return C_MPEG_4;
|
||||
case I_3GPP: return C_3GPP;
|
||||
case I_WEBM: return C_WEBM;
|
||||
case I_M4A: return C_M4A;
|
||||
case I_WEBMA: return C_WEBMA;
|
||||
default: Log.e(TAG, "format not known: " +
|
||||
Integer.toString(id) + "call the programmer he messed it up.");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public static String getMimeById(int id) {
|
||||
switch(id) {
|
||||
case I_MPEG_4: return M_MPEG_4;
|
||||
case I_3GPP: return M_3GPP;
|
||||
case I_WEBM: return M_WEBM;
|
||||
case I_M4A: return M_M4A;
|
||||
case I_WEBMA: return M_WEBMA;
|
||||
default: Log.e(TAG, "format not known: " +
|
||||
Integer.toString(id) + "call the programmer he messed it up.");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public static class VideoStream {
|
||||
public VideoStream(String url, int format, String res) {
|
||||
this.url = url; this.format = format; resolution = res;
|
||||
}
|
||||
public String url = ""; //url of the stream
|
||||
public int format = -1;
|
||||
public String resolution = "";
|
||||
}
|
||||
|
||||
public static class AudioStream {
|
||||
public AudioStream(String url, int format, int bandWidth, int samplingRate) {
|
||||
this.url = url; this.format = format;
|
||||
this.bandWidth = bandWidth; this.samplingRate = samplingRate;
|
||||
}
|
||||
public String url = "";
|
||||
public int format = -1;
|
||||
public int bandWidth = -1;
|
||||
public int samplingRate = -1;
|
||||
|
||||
}
|
||||
|
||||
public String id = "";
|
||||
public String uploader = "";
|
||||
public String upload_date = "";
|
||||
public String uploader_thumbnail_url = "";
|
||||
public Bitmap uploader_thumbnail = null;
|
||||
public String title = "";
|
||||
public String thumbnail_url = "";
|
||||
public Bitmap thumbnail = null;
|
||||
public String description = "";
|
||||
public int duration = -1;
|
||||
public int age_limit = 0;
|
||||
public String webpage_url = "";
|
||||
public String view_count = "";
|
||||
public String like_count = "";
|
||||
public String dislike_count = "";
|
||||
public String average_rating = "";
|
||||
public VideoStream[] videoStreams = null;
|
||||
public AudioStream[] audioStreams = null;
|
||||
public VideoInfoItem nextVideo = null;
|
||||
public Vector<VideoInfoItem> relatedVideos = null;
|
||||
public int videoAvailableStatus = VIDEO_AVAILABLE;
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 26.08.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* VideoInfoItem.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 VideoInfoItem {
|
||||
public String id = "";
|
||||
public String title = "";
|
||||
public String uploader = "";
|
||||
public String duration = "";
|
||||
public String thumbnail_url = "";
|
||||
public Bitmap thumbnail = null;
|
||||
public String webpage_url = "";
|
||||
public String upload_date = "";
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 24.10.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* VideoInfoItemViewCreator.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/>.
|
||||
*/
|
||||
|
||||
class VideoInfoItemViewCreator {
|
||||
private final LayoutInflater inflater;
|
||||
|
||||
public VideoInfoItemViewCreator(LayoutInflater inflater) {
|
||||
this.inflater = inflater;
|
||||
}
|
||||
|
||||
public View getViewFromVideoInfoItem(View convertView, ViewGroup parent, VideoPreviewInfo info, Context context) {
|
||||
ViewHolder holder;
|
||||
if(convertView == null) {
|
||||
convertView = inflater.inflate(R.layout.video_item, parent, false);
|
||||
holder = new ViewHolder();
|
||||
holder.itemThumbnailView = (ImageView) convertView.findViewById(R.id.itemThumbnailView);
|
||||
holder.itemVideoTitleView = (TextView) convertView.findViewById(R.id.itemVideoTitleView);
|
||||
holder.itemUploaderView = (TextView) convertView.findViewById(R.id.itemUploaderView);
|
||||
holder.itemDurationView = (TextView) convertView.findViewById(R.id.itemDurationView);
|
||||
holder.itemUploadDateView = (TextView) convertView.findViewById(R.id.itemUploadDateView);
|
||||
convertView.setTag(holder);
|
||||
} else {
|
||||
holder = (ViewHolder) convertView.getTag();
|
||||
}
|
||||
|
||||
if(info.thumbnail == null) {
|
||||
holder.itemThumbnailView.setImageResource(R.drawable.dummy_thumbnail);
|
||||
} else {
|
||||
holder.itemThumbnailView.setImageBitmap(info.thumbnail);
|
||||
}
|
||||
holder.itemVideoTitleView.setText(info.title);
|
||||
holder.itemUploaderView.setText(info.uploader);
|
||||
holder.itemDurationView.setText(info.duration);
|
||||
if(!info.upload_date.isEmpty()) {
|
||||
holder.itemUploadDateView.setText(info.upload_date);
|
||||
} else {
|
||||
holder.itemUploadDateView.setText(Localization.localizeViewCount(info.view_count, context));
|
||||
}
|
||||
|
||||
return convertView;
|
||||
}
|
||||
|
||||
private class ViewHolder {
|
||||
public ImageView itemThumbnailView;
|
||||
public TextView itemVideoTitleView, itemUploaderView, itemDurationView, itemUploadDateView;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,22 +1,17 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.content.ContentProviderOperation;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.app.NavUtils;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.youtube.YoutubeExtractor;
|
||||
import org.schabi.newpipe.services.ServiceList;
|
||||
import org.schabi.newpipe.services.StreamingService;
|
||||
|
||||
|
||||
/**
|
||||
@@ -41,17 +36,23 @@ public class VideoItemDetailActivity extends AppCompatActivity {
|
||||
|
||||
private static final String TAG = VideoItemDetailActivity.class.toString();
|
||||
|
||||
private VideoItemDetailFragment fragment;
|
||||
|
||||
private String videoUrl;
|
||||
private int currentStreamingService = -1;
|
||||
private boolean isLandscape;
|
||||
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_videoitem_detail);
|
||||
|
||||
// Show the Up button in the action bar.
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
ActionBarHandler.getHandler().setupNavMenu(this);
|
||||
try {
|
||||
//noinspection ConstantConditions
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
} catch(Exception e) {
|
||||
Log.d(TAG, "Could not get SupportActionBar");
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
// savedInstanceState is non-null when there is fragment state
|
||||
// saved from previous configurations of this activity
|
||||
@@ -69,25 +70,26 @@ public class VideoItemDetailActivity extends AppCompatActivity {
|
||||
if (getIntent().getData() != null) {
|
||||
videoUrl = getIntent().getData().toString();
|
||||
StreamingService[] serviceList = ServiceList.getServices();
|
||||
Extractor extractor = null;
|
||||
//VideoExtractor videoExtractor = null;
|
||||
for (int i = 0; i < serviceList.length; i++) {
|
||||
if (serviceList[i].acceptUrl(videoUrl)) {
|
||||
arguments.putInt(VideoItemDetailFragment.STREAMING_SERVICE, i);
|
||||
try {
|
||||
currentStreamingService = i;
|
||||
extractor = (Extractor) ServiceList.getService(i)
|
||||
.getExtractorClass().newInstance();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
currentStreamingService = i;
|
||||
//videoExtractor = ServiceList.getService(i).getExtractorInstance();
|
||||
break;
|
||||
}
|
||||
}
|
||||
arguments.putString(VideoItemDetailFragment.VIDEO_URL,
|
||||
extractor.getVideoUrl(extractor.getVideoId(videoUrl)));
|
||||
if(currentStreamingService == -1) {
|
||||
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
}
|
||||
//arguments.putString(VideoItemDetailFragment.VIDEO_URL,
|
||||
// videoExtractor.getVideoUrl(videoExtractor.getVideoId(videoUrl)));//cleans URL
|
||||
arguments.putString(VideoItemDetailFragment.VIDEO_URL, videoUrl);
|
||||
|
||||
arguments.putBoolean(VideoItemDetailFragment.AUTO_PLAY,
|
||||
PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.getBoolean(getString(R.string.autoPlayThroughIntent), false));
|
||||
.getBoolean(getString(R.string.autoplay_through_intent_key), false));
|
||||
} else {
|
||||
videoUrl = getIntent().getStringExtra(VideoItemDetailFragment.VIDEO_URL);
|
||||
currentStreamingService = getIntent().getIntExtra(VideoItemDetailFragment.STREAMING_SERVICE, -1);
|
||||
@@ -95,14 +97,33 @@ public class VideoItemDetailActivity extends AppCompatActivity {
|
||||
arguments.putInt(VideoItemDetailFragment.STREAMING_SERVICE, currentStreamingService);
|
||||
arguments.putBoolean(VideoItemDetailFragment.AUTO_PLAY, false);
|
||||
}
|
||||
// Create the detail fragment and add it to the activity
|
||||
// using a fragment transaction.
|
||||
VideoItemDetailFragment fragment = new VideoItemDetailFragment();
|
||||
fragment.setArguments(arguments);
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.add(R.id.videoitem_detail_container, fragment)
|
||||
.commit();
|
||||
|
||||
} else {
|
||||
videoUrl = savedInstanceState.getString(VideoItemDetailFragment.VIDEO_URL);
|
||||
currentStreamingService = savedInstanceState.getInt(VideoItemDetailFragment.STREAMING_SERVICE);
|
||||
arguments = savedInstanceState;
|
||||
}
|
||||
|
||||
// Create the detail fragment and add it to the activity
|
||||
// using a fragment transaction.
|
||||
fragment = new VideoItemDetailFragment();
|
||||
fragment.setArguments(arguments);
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.add(R.id.videoitem_detail_container, fragment)
|
||||
.commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
App.checkStartTor(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
outState.putString(VideoItemDetailFragment.VIDEO_URL, videoUrl);
|
||||
outState.putInt(VideoItemDetailFragment.STREAMING_SERVICE, currentStreamingService);
|
||||
outState.putBoolean(VideoItemDetailFragment.AUTO_PLAY, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -113,25 +134,23 @@ public class VideoItemDetailActivity extends AppCompatActivity {
|
||||
// activity, the Up button is shown. Use NavUtils to allow users
|
||||
// to navigate up one level in the application structure. For
|
||||
// more details, see the Navigation pattern on Android Design:
|
||||
//
|
||||
|
||||
// http://developer.android.com/design/patterns/navigation.html#up-vs-back
|
||||
//
|
||||
|
||||
Intent intent = new Intent(this, VideoItemListActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
NavUtils.navigateUpTo(this, intent);
|
||||
return true;
|
||||
} else {
|
||||
ActionBarHandler.getHandler().onItemSelected(item, this);
|
||||
return fragment.onOptionsItemSelected(item) ||
|
||||
super.onOptionsItemSelected(item);
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreatePanelMenu(int featured, Menu menu) {
|
||||
super.onCreatePanelMenu(featured, menu);
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
ActionBarHandler.getHandler().setupMenu(menu, inflater, this);
|
||||
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
fragment.onCreateOptionsMenu(menu, getMenuInflater());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,46 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.media.Image;
|
||||
import android.graphics.Point;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.design.widget.FloatingActionButton;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.text.Html;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.GridLayout;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ListView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import android.view.MenuItem;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Vector;
|
||||
|
||||
import org.schabi.newpipe.services.VideoExtractor;
|
||||
import org.schabi.newpipe.services.ServiceList;
|
||||
import org.schabi.newpipe.services.StreamingService;
|
||||
import org.schabi.newpipe.services.VideoInfo;
|
||||
|
||||
|
||||
/**
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
@@ -55,39 +68,68 @@ public class VideoItemDetailFragment extends Fragment {
|
||||
* The fragment argument representing the item ID that this fragment
|
||||
* represents.
|
||||
*/
|
||||
public static final String ARG_ITEM_ID = "item_id";
|
||||
//public static final String ARG_ITEM_ID = "item_id";
|
||||
public static final String VIDEO_URL = "video_url";
|
||||
public static final String STREAMING_SERVICE = "streaming_service";
|
||||
public static final String AUTO_PLAY = "auto_play";
|
||||
|
||||
private boolean autoPlayEnabled = false;
|
||||
private Thread extractorThread = null;
|
||||
private AppCompatActivity activity;
|
||||
private ActionBarHandler actionBarHandler;
|
||||
|
||||
private class ExtractorRunnable implements Runnable {
|
||||
private Handler h = new Handler();
|
||||
private Class extractorClass;
|
||||
private String videoUrl;
|
||||
public ExtractorRunnable(String videoUrl, Class extractorClass, VideoItemDetailFragment f) {
|
||||
this.extractorClass = extractorClass;
|
||||
private int streamingServiceId = -1;
|
||||
|
||||
private boolean autoPlayEnabled = false;
|
||||
private VideoInfo currentVideoInfo = null;
|
||||
private boolean showNextVideoItem = false;
|
||||
|
||||
private View thumbnailWindowLayout;
|
||||
private FloatingActionButton playVideoButton;
|
||||
private final Point initialThumbnailPos = new Point(0, 0);
|
||||
|
||||
public interface OnInvokeCreateOptionsMenuListener {
|
||||
void createOptionsMenu();
|
||||
}
|
||||
|
||||
private OnInvokeCreateOptionsMenuListener onInvokeCreateOptionsMenuListener = null;
|
||||
|
||||
private class VideoExtractorRunnable implements Runnable {
|
||||
private final Handler h = new Handler();
|
||||
private VideoExtractor videoExtractor;
|
||||
private final StreamingService service;
|
||||
private final String videoUrl;
|
||||
|
||||
public VideoExtractorRunnable(String videoUrl, StreamingService service) {
|
||||
this.service = service;
|
||||
this.videoUrl = videoUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
Extractor extractor = (Extractor) extractorClass.newInstance();
|
||||
VideoInfo videoInfo = extractor.getVideoInfo(videoUrl);
|
||||
this.videoExtractor = service.getExtractorInstance(videoUrl);
|
||||
VideoInfo videoInfo = videoExtractor.getVideoInfo();
|
||||
h.post(new VideoResultReturnedRunnable(videoInfo));
|
||||
if (videoInfo.videoAvailableStatus == VideoInfo.VIDEO_AVAILABLE) {
|
||||
if (videoInfo.errorCode == VideoInfo.NO_ERROR) {
|
||||
h.post(new SetThumbnailRunnable(
|
||||
BitmapFactory.decodeStream(
|
||||
new URL(videoInfo.thumbnail_url)
|
||||
.openConnection()
|
||||
.getInputStream()), SetThumbnailRunnable.VIDEO_THUMBNAIL));
|
||||
.getInputStream()),
|
||||
SetThumbnailRunnable.VIDEO_THUMBNAIL));
|
||||
h.post(new SetThumbnailRunnable(
|
||||
BitmapFactory.decodeStream(
|
||||
new URL(videoInfo.uploader_thumbnail_url)
|
||||
.openConnection()
|
||||
.getInputStream()), SetThumbnailRunnable.CHANNEL_THUMBNAIL));
|
||||
.getInputStream()),
|
||||
SetThumbnailRunnable.CHANNEL_THUMBNAIL));
|
||||
if(showNextVideoItem) {
|
||||
h.post(new SetThumbnailRunnable(
|
||||
BitmapFactory.decodeStream(
|
||||
new URL(videoInfo.nextVideo.thumbnail_url)
|
||||
.openConnection()
|
||||
.getInputStream()),
|
||||
SetThumbnailRunnable.NEXT_VIDEO_THUMBNAIL));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
@@ -97,21 +139,24 @@ public class VideoItemDetailFragment extends Fragment {
|
||||
}
|
||||
|
||||
private class VideoResultReturnedRunnable implements Runnable {
|
||||
private VideoInfo videoInfo;
|
||||
private final VideoInfo videoInfo;
|
||||
public VideoResultReturnedRunnable(VideoInfo videoInfo) {
|
||||
this.videoInfo = videoInfo;
|
||||
}
|
||||
@Override
|
||||
public void run() {
|
||||
//todo: fix expired thread error:
|
||||
// If the thread calling this runnable is expired, the following function will crash.
|
||||
updateInfo(videoInfo);
|
||||
}
|
||||
}
|
||||
|
||||
private class SetThumbnailRunnable implements Runnable {
|
||||
public static final int CHANNEL_THUMBNAIL = 2;
|
||||
public static final int VIDEO_THUMBNAIL = 1;
|
||||
private Bitmap thumbnail;
|
||||
private int thumbnailId;
|
||||
public static final int CHANNEL_THUMBNAIL = 2;
|
||||
public static final int NEXT_VIDEO_THUMBNAIL = 3;
|
||||
private final Bitmap thumbnail;
|
||||
private final int thumbnailId;
|
||||
public SetThumbnailRunnable(Bitmap thumbnail, int id) {
|
||||
this.thumbnail = thumbnail;
|
||||
this.thumbnailId = id;
|
||||
@@ -122,78 +167,103 @@ public class VideoItemDetailFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
public void updateThumbnail(Bitmap thumbnail, int id) {
|
||||
private void updateThumbnail(Bitmap thumbnail, int id) {
|
||||
Activity a = getActivity();
|
||||
ImageView thumbnailView = null;
|
||||
ImageView thumbnailView;
|
||||
try {
|
||||
switch (id) {
|
||||
case SetThumbnailRunnable.VIDEO_THUMBNAIL:
|
||||
|
||||
thumbnailView = (ImageView) a.findViewById(R.id.detailThumbnailView);
|
||||
actionBarHandler.setSetVideoThumbnail(thumbnail);
|
||||
break;
|
||||
case SetThumbnailRunnable.CHANNEL_THUMBNAIL:
|
||||
thumbnailView = (ImageView) a.findViewById(R.id.detailUploaderThumbnailView);
|
||||
break;
|
||||
case SetThumbnailRunnable.NEXT_VIDEO_THUMBNAIL:
|
||||
FrameLayout nextVideoFrame = (FrameLayout) a.findViewById(R.id.detailNextVideoFrame);
|
||||
thumbnailView = (ImageView) nextVideoFrame.findViewById(R.id.itemThumbnailView);
|
||||
currentVideoInfo.nextVideo.thumbnail = thumbnail;
|
||||
break;
|
||||
default:
|
||||
Log.d(TAG, "Error: Thumbnail id not known");
|
||||
return;
|
||||
}
|
||||
|
||||
if (thumbnailView != null) {
|
||||
thumbnailView.setImageBitmap(thumbnail);
|
||||
}
|
||||
|
||||
} catch (java.lang.NullPointerException e) {
|
||||
// No god programm design i know. :/
|
||||
// Not good program design, I know. :/
|
||||
Log.w(TAG, "updateThumbnail(): Fragment closed before thread ended work");
|
||||
}
|
||||
}
|
||||
|
||||
public void updateInfo(VideoInfo info) {
|
||||
Activity a = getActivity();
|
||||
private void updateInfo(VideoInfo info) {
|
||||
currentVideoInfo = info;
|
||||
Resources res = activity.getResources();
|
||||
try {
|
||||
ProgressBar progressBar = (ProgressBar) a.findViewById(R.id.detailProgressBar);
|
||||
TextView videoTitleView = (TextView) a.findViewById(R.id.detailVideoTitleView);
|
||||
TextView uploaderView = (TextView) a.findViewById(R.id.detailUploaderView);
|
||||
TextView viewCountView = (TextView) a.findViewById(R.id.detailViewCountView);
|
||||
TextView thumbsUpView = (TextView) a.findViewById(R.id.detailThumbsUpCountView);
|
||||
TextView thumbsDownView = (TextView) a.findViewById(R.id.detailThumbsDownCountView);
|
||||
TextView uploadDateView = (TextView) a.findViewById(R.id.detailUploadDateView);
|
||||
TextView descriptionView = (TextView) a.findViewById(R.id.detailDescriptionView);
|
||||
ImageView thumbnailView = (ImageView) a.findViewById(R.id.detailThumbnailView);
|
||||
ImageView uploaderThumbnailView = (ImageView) a.findViewById(R.id.detailUploaderThumbnailView);
|
||||
ImageView thumbsUpPic = (ImageView) a.findViewById(R.id.detailThumbsUpImgView);
|
||||
ImageView thumbsDownPic = (ImageView) a.findViewById(R.id.detailThumbsDownImgView);
|
||||
View textSeperationLine = a.findViewById(R.id.textSeperationLine);
|
||||
VideoInfoItemViewCreator videoItemViewCreator =
|
||||
new VideoInfoItemViewCreator(LayoutInflater.from(getActivity()));
|
||||
|
||||
RelativeLayout textContentLayout = (RelativeLayout) activity.findViewById(R.id.detailTextContentLayout);
|
||||
ProgressBar progressBar = (ProgressBar) activity.findViewById(R.id.detailProgressBar);
|
||||
TextView videoTitleView = (TextView) activity.findViewById(R.id.detailVideoTitleView);
|
||||
TextView uploaderView = (TextView) activity.findViewById(R.id.detailUploaderView);
|
||||
TextView viewCountView = (TextView) activity.findViewById(R.id.detailViewCountView);
|
||||
TextView thumbsUpView = (TextView) activity.findViewById(R.id.detailThumbsUpCountView);
|
||||
TextView thumbsDownView = (TextView) activity.findViewById(R.id.detailThumbsDownCountView);
|
||||
TextView uploadDateView = (TextView) activity.findViewById(R.id.detailUploadDateView);
|
||||
TextView descriptionView = (TextView) activity.findViewById(R.id.detailDescriptionView);
|
||||
ImageView thumbnailView = (ImageView) activity.findViewById(R.id.detailThumbnailView);
|
||||
FrameLayout nextVideoFrame = (FrameLayout) activity.findViewById(R.id.detailNextVideoFrame);
|
||||
RelativeLayout nextVideoRootFrame =
|
||||
(RelativeLayout) activity.findViewById(R.id.detailNextVideoRootLayout);
|
||||
Button backgroundButton = (Button)
|
||||
activity.findViewById(R.id.detailVideoThumbnailWindowBackgroundButton);
|
||||
|
||||
if(textSeperationLine != null) {
|
||||
textSeperationLine.setVisibility(View.VISIBLE);
|
||||
}
|
||||
progressBar.setVisibility(View.GONE);
|
||||
videoTitleView.setVisibility(View.VISIBLE);
|
||||
uploaderView.setVisibility(View.VISIBLE);
|
||||
uploadDateView.setVisibility(View.VISIBLE);
|
||||
viewCountView.setVisibility(View.VISIBLE);
|
||||
thumbsUpView.setVisibility(View.VISIBLE);
|
||||
thumbsDownView.setVisibility(View.VISIBLE);
|
||||
uploadDateView.setVisibility(View.VISIBLE);
|
||||
descriptionView.setVisibility(View.VISIBLE);
|
||||
thumbnailView.setVisibility(View.VISIBLE);
|
||||
uploaderThumbnailView.setVisibility(View.VISIBLE);
|
||||
thumbsUpPic.setVisibility(View.VISIBLE);
|
||||
thumbsDownPic.setVisibility(View.VISIBLE);
|
||||
|
||||
switch (info.videoAvailableStatus) {
|
||||
case VideoInfo.VIDEO_AVAILABLE: {
|
||||
switch (info.errorCode) {
|
||||
case VideoInfo.NO_ERROR: {
|
||||
View nextVideoView = videoItemViewCreator
|
||||
.getViewFromVideoInfoItem(null, nextVideoFrame, info.nextVideo, getContext());
|
||||
nextVideoFrame.addView(nextVideoView);
|
||||
|
||||
|
||||
Button nextVideoButton = (Button) activity.findViewById(R.id.detailNextVideoButton);
|
||||
Button similarVideosButton = (Button) activity.findViewById(R.id.detailShowSimilarButton);
|
||||
|
||||
textContentLayout.setVisibility(View.VISIBLE);
|
||||
playVideoButton.setVisibility(View.VISIBLE);
|
||||
if (!showNextVideoItem) {
|
||||
nextVideoRootFrame.setVisibility(View.GONE);
|
||||
similarVideosButton.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
videoTitleView.setText(info.title);
|
||||
uploaderView.setText(info.uploader);
|
||||
viewCountView.setText(info.view_count + " " + a.getString(R.string.viewSufix));
|
||||
thumbsUpView.setText(info.like_count);
|
||||
thumbsDownView.setText(info.dislike_count);
|
||||
uploadDateView.setText(a.getString(R.string.uploadDatePrefix) + " " + info.upload_date);
|
||||
actionBarHandler.setChannelName(info.uploader);
|
||||
|
||||
String localizedViewCount = Localization.localizeViewCount(info.view_count, getContext());
|
||||
viewCountView.setText(localizedViewCount);
|
||||
|
||||
String localizedLikeCount = Localization.localizeNumber(info.like_count, getContext());
|
||||
thumbsUpView.setText(localizedLikeCount);
|
||||
|
||||
String localizedDislikeCount = Localization.localizeNumber(info.dislike_count, getContext());
|
||||
thumbsDownView.setText(localizedDislikeCount);
|
||||
|
||||
String localizedDate = Localization.localizeDate(info.upload_date, getContext());
|
||||
uploadDateView.setText(localizedDate);
|
||||
|
||||
descriptionView.setText(Html.fromHtml(info.description));
|
||||
|
||||
descriptionView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
|
||||
ActionBarHandler.getHandler().setVideoInfo(info.webpage_url, info.title);
|
||||
actionBarHandler.setServiceId(streamingServiceId);
|
||||
actionBarHandler.setVideoInfo(info.webpage_url, info.title);
|
||||
actionBarHandler.setStartPosition(info.startPosition);
|
||||
|
||||
// parse streams
|
||||
Vector<VideoInfo.VideoStream> streamsToUse = new Vector<>();
|
||||
@@ -206,21 +276,49 @@ public class VideoItemDetailFragment extends Fragment {
|
||||
for (int i = 0; i < streamList.length; i++) {
|
||||
streamList[i] = streamsToUse.get(i);
|
||||
}
|
||||
ActionBarHandler.getHandler().setStreams(streamList, info.audioStreams);
|
||||
actionBarHandler.setStreams(streamList, info.audioStreams);
|
||||
|
||||
nextVideoButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent detailIntent =
|
||||
new Intent(getActivity(), VideoItemDetailActivity.class);
|
||||
/*detailIntent.putExtra(
|
||||
VideoItemDetailFragment.ARG_ITEM_ID, currentVideoInfo.nextVideo.id); */
|
||||
detailIntent.putExtra(
|
||||
VideoItemDetailFragment.VIDEO_URL, currentVideoInfo.nextVideo.webpage_url);
|
||||
|
||||
detailIntent.putExtra(VideoItemDetailFragment.STREAMING_SERVICE, streamingServiceId);
|
||||
startActivity(detailIntent);
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case VideoInfo.VIDEO_UNAVAILABLE_GEMA:
|
||||
thumbnailView.setImageBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.gruese_die_gema_unangebracht));
|
||||
case VideoInfo.ERROR_BLOCKED_BY_GEMA:
|
||||
thumbnailView.setImageBitmap(BitmapFactory.decodeResource(
|
||||
getResources(), R.drawable.gruese_die_gema));
|
||||
backgroundButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(activity.getString(R.string.c3s_url)));
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case VideoInfo.VIDEO_UNAVAILABLE:
|
||||
thumbnailView.setImageBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.not_available_monkey));
|
||||
case VideoInfo.ERROR_NO_SPECIFIED_ERROR:
|
||||
thumbnailView.setImageBitmap(BitmapFactory.decodeResource(
|
||||
getResources(), R.drawable.not_available_monkey));
|
||||
Toast.makeText(activity, info.errorMessage, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
break;
|
||||
default:
|
||||
Log.e(TAG, "Video Availeble Status not known.");
|
||||
Log.e(TAG, "Video Available Status not known.");
|
||||
}
|
||||
|
||||
if(autoPlayEnabled) {
|
||||
ActionBarHandler.getHandler().playVideo();
|
||||
actionBarHandler.playVideo();
|
||||
}
|
||||
} catch (java.lang.NullPointerException e) {
|
||||
Log.w(TAG, "updateInfo(): Fragment closed before thread ended work... or else");
|
||||
@@ -241,59 +339,123 @@ public class VideoItemDetailFragment extends Fragment {
|
||||
* Mandatory empty constructor for the fragment manager to instantiate the
|
||||
* fragment (e.g. upon screen orientation changes).
|
||||
*/
|
||||
public VideoItemDetailFragment() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
try {
|
||||
StreamingService streamingService = ServiceList.getService(
|
||||
getArguments().getInt(STREAMING_SERVICE));
|
||||
extractorThread = new Thread(new ExtractorRunnable(
|
||||
getArguments().getString(VIDEO_URL), streamingService.getExtractorClass(), this));
|
||||
autoPlayEnabled = getArguments().getBoolean(AUTO_PLAY);
|
||||
extractorThread.start();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
activity = (AppCompatActivity) getActivity();
|
||||
showNextVideoItem = PreferenceManager.getDefaultSharedPreferences(getActivity())
|
||||
.getBoolean(activity.getString(R.string.show_next_video_key), true);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_videoitem_detail, container, false);
|
||||
actionBarHandler = new ActionBarHandler(activity);
|
||||
actionBarHandler.setupNavMenu(activity);
|
||||
if(onInvokeCreateOptionsMenuListener != null) {
|
||||
onInvokeCreateOptionsMenuListener.createOptionsMenu();
|
||||
}
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceBundle) {
|
||||
super.onActivityCreated(savedInstanceBundle);
|
||||
FloatingActionButton playVideoButton = (FloatingActionButton) getActivity().findViewById(R.id.playVideoButton);
|
||||
Activity a = getActivity();
|
||||
playVideoButton = (FloatingActionButton) a.findViewById(R.id.playVideoButton);
|
||||
thumbnailWindowLayout = a.findViewById(R.id.detailVideoThumbnailWindowLayout);
|
||||
Button backgroundButton = (Button)
|
||||
a.findViewById(R.id.detailVideoThumbnailWindowBackgroundButton);
|
||||
|
||||
if(PreferenceManager.getDefaultSharedPreferences(getActivity())
|
||||
.getBoolean(getString(R.string.leftHandLayout), false) && checkIfLandscape()) {
|
||||
RelativeLayout.LayoutParams oldLayout = (RelativeLayout.LayoutParams) playVideoButton.getLayoutParams();
|
||||
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
|
||||
RelativeLayout.LayoutParams.WRAP_CONTENT,
|
||||
RelativeLayout.LayoutParams.WRAP_CONTENT);
|
||||
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
|
||||
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
|
||||
layoutParams.setMargins(oldLayout.leftMargin, oldLayout.topMargin, oldLayout.rightMargin, oldLayout.rightMargin);
|
||||
playVideoButton.setLayoutParams(layoutParams);
|
||||
}
|
||||
// Sometimes when this fragment is not visible it still gets initiated
|
||||
// then we must not try to access objects of this fragment.
|
||||
// Otherwise the applications would crash.
|
||||
if(playVideoButton != null) {
|
||||
try {
|
||||
streamingServiceId = getArguments().getInt(STREAMING_SERVICE);
|
||||
StreamingService streamingService = ServiceList.getService(streamingServiceId);
|
||||
Thread videoExtractorThread = new Thread(new VideoExtractorRunnable(
|
||||
getArguments().getString(VIDEO_URL), streamingService));
|
||||
|
||||
playVideoButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
ActionBarHandler.getHandler().playVideo();
|
||||
autoPlayEnabled = getArguments().getBoolean(AUTO_PLAY);
|
||||
videoExtractorThread.start();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
|
||||
playVideoButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
actionBarHandler.playVideo();
|
||||
}
|
||||
});
|
||||
|
||||
backgroundButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
actionBarHandler.playVideo();
|
||||
}
|
||||
});
|
||||
|
||||
Button similarVideosButton = (Button) activity.findViewById(R.id.detailShowSimilarButton);
|
||||
similarVideosButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent intent = new Intent(activity, VideoItemListActivity.class);
|
||||
//todo: find more elegant way to do this - converting from List to ArrayList sucks
|
||||
ArrayList<VideoPreviewInfo> toParcel = new ArrayList<>(currentVideoInfo.relatedVideos);
|
||||
//why oh why does the parcelable array put method have to be so damn specific
|
||||
// about the class of its argument?
|
||||
//why not a List<? extends Parcelable>?
|
||||
intent.putParcelableArrayListExtra(VideoItemListActivity.VIDEO_INFO_ITEMS, toParcel);
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
});
|
||||
|
||||
// todo: Fix this workaround (probably with a better design), so that older android
|
||||
// versions don't have problems rendering the thumbnail right.
|
||||
if(Build.VERSION.SDK_INT >= 18) {
|
||||
ImageView thumbnailView = (ImageView) activity.findViewById(R.id.detailThumbnailView);
|
||||
thumbnailView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
|
||||
// This is used to synchronize the thumbnailWindowButton and the playVideoButton
|
||||
// inside the ScrollView with the actual size of the thumbnail.
|
||||
@Override
|
||||
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
|
||||
RelativeLayout.LayoutParams newWindowLayoutParams =
|
||||
(RelativeLayout.LayoutParams) thumbnailWindowLayout.getLayoutParams();
|
||||
newWindowLayoutParams.height = bottom - top;
|
||||
thumbnailWindowLayout.setLayoutParams(newWindowLayoutParams);
|
||||
|
||||
//noinspection SuspiciousNameCombination
|
||||
initialThumbnailPos.set(top, left);
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean checkIfLandscape() {
|
||||
private boolean checkIfLandscape() {
|
||||
DisplayMetrics displayMetrics = new DisplayMetrics();
|
||||
getActivity().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
|
||||
return displayMetrics.heightPixels < displayMetrics.widthPixels;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
actionBarHandler.setupMenu(menu, inflater);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
return actionBarHandler.onItemSelected(item);
|
||||
}
|
||||
|
||||
public void setOnInvokeCreateOptionsMenuListener(OnInvokeCreateOptionsMenuListener listener) {
|
||||
this.onInvokeCreateOptionsMenuListener = listener;
|
||||
}
|
||||
}
|
||||
@@ -2,22 +2,21 @@ package org.schabi.newpipe;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.app.NavUtils;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.support.v7.widget.SearchView;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.support.v7.widget.SearchView;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.schabi.newpipe.services.ServiceList;
|
||||
|
||||
/**
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
@@ -41,15 +40,27 @@ public class VideoItemListActivity extends AppCompatActivity
|
||||
implements VideoItemListFragment.Callbacks {
|
||||
|
||||
private static final String TAG = VideoItemListFragment.class.toString();
|
||||
|
||||
// arguments to give to this activity
|
||||
public static final String VIDEO_INFO_ITEMS = "video_info_items";
|
||||
|
||||
// savedInstanceBundle arguments
|
||||
private static final String QUERY = "query";
|
||||
private static final String STREAMING_SERVICE = "streaming_service";
|
||||
|
||||
// activity modes
|
||||
private static final int SEARCH_MODE = 0;
|
||||
private static final int PRESENT_VIDEOS_MODE = 1;
|
||||
|
||||
private int mode = SEARCH_MODE;
|
||||
private int currentStreamingServiceId = -1;
|
||||
private String searchQuery = "";
|
||||
|
||||
private VideoItemListFragment listFragment;
|
||||
private VideoItemDetailFragment videoFragment = null;
|
||||
private Menu menu = null;
|
||||
|
||||
public class SearchVideoQueryListener implements SearchView.OnQueryTextListener {
|
||||
private class SearchVideoQueryListener implements SearchView.OnQueryTextListener {
|
||||
|
||||
@Override
|
||||
public boolean onQueryTextSubmit(String query) {
|
||||
@@ -60,15 +71,20 @@ public class VideoItemListActivity extends AppCompatActivity
|
||||
// hide virtual keyboard
|
||||
InputMethodManager inputManager =
|
||||
(InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
inputManager.hideSoftInputFromWindow(
|
||||
getCurrentFocus().getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
|
||||
try {
|
||||
//noinspection ConstantConditions
|
||||
inputManager.hideSoftInputFromWindow(
|
||||
getCurrentFocus().getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
|
||||
} catch(NullPointerException e) {
|
||||
Log.e(TAG, "Could not get widget with focus");
|
||||
e.printStackTrace();
|
||||
}
|
||||
// clear focus
|
||||
// 1. to not open up the keyboard after switching back to this
|
||||
// 2. It's a workaround to a seeming bug by the Android OS it self, causing
|
||||
// onQueryTextSubmit to trigger twice when focus is not cleared.
|
||||
// See: http://stackoverflow.com/questions/17874951/searchview-onquerytextsubmit-runs-twice-while-i-pressed-once
|
||||
getCurrentFocus().clearFocus();
|
||||
hideWatermark();
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
@@ -82,13 +98,6 @@ public class VideoItemListActivity extends AppCompatActivity
|
||||
|
||||
}
|
||||
|
||||
private void hideWatermark() {
|
||||
ImageView waterMark = (ImageView) findViewById(R.id.list_view_watermark);
|
||||
if(waterMark != null) {
|
||||
waterMark.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the activity is in two-pane mode, i.e. running on a tablet
|
||||
* device.
|
||||
@@ -100,21 +109,39 @@ public class VideoItemListActivity extends AppCompatActivity
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_videoitem_list);
|
||||
|
||||
listFragment = (VideoItemListFragment)
|
||||
getSupportFragmentManager().findFragmentById(R.id.videoitem_list);
|
||||
|
||||
//-------- remove this line when multiservice support is implemented ----------
|
||||
//------ todo: remove this line when multiservice support is implemented ------
|
||||
currentStreamingServiceId = ServiceList.getIdOfService("Youtube");
|
||||
//-----------------------------------------------------------------------------
|
||||
VideoItemListFragment listFragment = (VideoItemListFragment) getSupportFragmentManager()
|
||||
|
||||
listFragment = (VideoItemListFragment) getSupportFragmentManager()
|
||||
.findFragmentById(R.id.videoitem_list);
|
||||
listFragment.setStreamingService(ServiceList.getService(currentStreamingServiceId));
|
||||
|
||||
if(savedInstanceState != null) {
|
||||
Bundle arguments = getIntent().getExtras();
|
||||
|
||||
if(arguments != null) {
|
||||
//Parcelable[] p = arguments.getParcelableArray(VIDEO_INFO_ITEMS);
|
||||
ArrayList<VideoPreviewInfo> p = arguments.getParcelableArrayList(VIDEO_INFO_ITEMS);
|
||||
if(p != null) {
|
||||
mode = PRESENT_VIDEOS_MODE;
|
||||
try {
|
||||
//noinspection ConstantConditions
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
} catch (NullPointerException e) {
|
||||
Log.e(TAG, "Could not get SupportActionBar");
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
listFragment.present(p);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(savedInstanceState != null
|
||||
&& mode != PRESENT_VIDEOS_MODE) {
|
||||
searchQuery = savedInstanceState.getString(QUERY);
|
||||
currentStreamingServiceId = savedInstanceState.getInt(STREAMING_SERVICE);
|
||||
if(!searchQuery.isEmpty()) {
|
||||
hideWatermark();
|
||||
listFragment.search(searchQuery);
|
||||
}
|
||||
}
|
||||
@@ -134,19 +161,24 @@ public class VideoItemListActivity extends AppCompatActivity
|
||||
.setActivateOnItemClick(true);
|
||||
|
||||
SearchView searchView = (SearchView)findViewById(R.id.searchViewTablet);
|
||||
// Somehow the seticonifiedbydefault property set by the layout xml is not working on
|
||||
// the support version on SearchView, so it needs to be set programmatically.
|
||||
searchView.setIconifiedByDefault(false);
|
||||
searchView.setIconified(false);
|
||||
searchView.setOnQueryTextListener(new SearchVideoQueryListener());
|
||||
|
||||
ActionBarHandler.getHandler().setupNavMenu(this);
|
||||
|
||||
if(mode != PRESENT_VIDEOS_MODE) {
|
||||
// Somehow the seticonifiedbydefault property set by the layout xml is not working on
|
||||
// the support version on SearchView, so it needs to be set programmatically.
|
||||
searchView.setIconifiedByDefault(false);
|
||||
searchView.setIconified(false);
|
||||
searchView.setOnQueryTextListener(new SearchVideoQueryListener());
|
||||
} else {
|
||||
searchView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
SettingsActivity.initSettings(this);
|
||||
PreferenceManager.setDefaultValues(this, R.xml.settings, false);
|
||||
}
|
||||
|
||||
// TODO: If exposing deep links into your app, handle intents here.
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
App.checkStartTor(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,19 +197,26 @@ public class VideoItemListActivity extends AppCompatActivity
|
||||
// adding or replacing the detail fragment using a
|
||||
// fragment transaction.
|
||||
Bundle arguments = new Bundle();
|
||||
arguments.putString(VideoItemDetailFragment.ARG_ITEM_ID, id);
|
||||
//arguments.putString(VideoItemDetailFragment.ARG_ITEM_ID, id);
|
||||
arguments.putString(VideoItemDetailFragment.VIDEO_URL, webpage_url);
|
||||
arguments.putInt(VideoItemDetailFragment.STREAMING_SERVICE, currentStreamingServiceId);
|
||||
VideoItemDetailFragment fragment = new VideoItemDetailFragment();
|
||||
fragment.setArguments(arguments);
|
||||
videoFragment = new VideoItemDetailFragment();
|
||||
videoFragment.setArguments(arguments);
|
||||
videoFragment.setOnInvokeCreateOptionsMenuListener(new VideoItemDetailFragment.OnInvokeCreateOptionsMenuListener() {
|
||||
@Override
|
||||
public void createOptionsMenu() {
|
||||
menu.clear();
|
||||
onCreateOptionsMenu(menu);
|
||||
}
|
||||
});
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.videoitem_detail_container, fragment)
|
||||
.replace(R.id.videoitem_detail_container, videoFragment)
|
||||
.commit();
|
||||
} else {
|
||||
// In single-pane mode, simply start the detail activity
|
||||
// for the selected item ID.
|
||||
Intent detailIntent = new Intent(this, VideoItemDetailActivity.class);
|
||||
detailIntent.putExtra(VideoItemDetailFragment.ARG_ITEM_ID, id);
|
||||
//detailIntent.putExtra(VideoItemDetailFragment.ARG_ITEM_ID, id);
|
||||
detailIntent.putExtra(VideoItemDetailFragment.VIDEO_URL, webpage_url);
|
||||
detailIntent.putExtra(VideoItemDetailFragment.STREAMING_SERVICE, currentStreamingServiceId);
|
||||
startActivity(detailIntent);
|
||||
@@ -185,10 +224,12 @@ public class VideoItemListActivity extends AppCompatActivity
|
||||
}
|
||||
|
||||
|
||||
public boolean onCreatePanelMenu(int featured, Menu menu) {
|
||||
super.onCreatePanelMenu(featured, menu);
|
||||
if(findViewById(R.id.videoitem_detail_container) == null) {
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
this.menu = menu;
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
if(mode != PRESENT_VIDEOS_MODE &&
|
||||
findViewById(R.id.videoitem_detail_container) == null) {
|
||||
inflater.inflate(R.menu.videoitem_list, menu);
|
||||
MenuItem searchItem = menu.findItem(R.id.action_search);
|
||||
SearchView searchView = (SearchView) searchItem.getActionView();
|
||||
@@ -196,9 +237,10 @@ public class VideoItemListActivity extends AppCompatActivity
|
||||
searchView.setOnQueryTextListener(
|
||||
new SearchVideoQueryListener());
|
||||
|
||||
} else if (videoFragment != null){
|
||||
videoFragment.onCreateOptionsMenu(menu, inflater);
|
||||
} else {
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
ActionBarHandler.getHandler().setupMenu(menu, inflater, this);
|
||||
inflater.inflate(R.menu.videoitem_two_pannel, menu);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -207,14 +249,23 @@ public class VideoItemListActivity extends AppCompatActivity
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
int id = item.getItemId();
|
||||
if(id == R.id.action_settings) {
|
||||
Intent intent = new Intent(this, SettingsActivity.class);
|
||||
startActivity(intent);
|
||||
} else {
|
||||
ActionBarHandler.getHandler().onItemSelected(item, this);
|
||||
return super.onOptionsItemSelected(item);
|
||||
|
||||
switch(id) {
|
||||
case android.R.id.home: {
|
||||
Intent intent = new Intent(this, VideoItemListActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
NavUtils.navigateUpTo(this, intent);
|
||||
return true;
|
||||
}
|
||||
case R.id.action_settings: {
|
||||
Intent intent = new Intent(this, SettingsActivity.class);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return videoFragment.onOptionsItemSelected(item) ||
|
||||
super.onOptionsItemSelected(item);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.app.ListFragment;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.AbsListView;
|
||||
import android.widget.ListView;
|
||||
@@ -16,6 +19,9 @@ import java.net.URL;
|
||||
import java.util.List;
|
||||
import java.util.Vector;
|
||||
|
||||
import org.schabi.newpipe.services.SearchEngine;
|
||||
import org.schabi.newpipe.services.StreamingService;
|
||||
|
||||
|
||||
/**
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
@@ -42,6 +48,11 @@ public class VideoItemListFragment extends ListFragment {
|
||||
private StreamingService streamingService = null;
|
||||
private VideoListAdapter videoListAdapter;
|
||||
|
||||
// activity modes
|
||||
private static final int SEARCH_MODE = 0;
|
||||
private static final int PRESENT_VIDEOS_MODE = 1;
|
||||
|
||||
private int mode = SEARCH_MODE;
|
||||
private String query = "";
|
||||
private int lastPage = 0;
|
||||
|
||||
@@ -51,48 +62,55 @@ public class VideoItemListFragment extends ListFragment {
|
||||
private LoadThumbsRunnable loadThumbsRunnable = null;
|
||||
// used to track down if results posted by threads ar still valid
|
||||
private int currentRequestId = -1;
|
||||
private ListView list;
|
||||
|
||||
private View footer;
|
||||
|
||||
// used to suppress request for loading a new page while another page is already loading.
|
||||
private boolean loadingNextPage = true;
|
||||
|
||||
private class ResultRunnable implements Runnable {
|
||||
private SearchEngine.Result result;
|
||||
private int reuqestId;
|
||||
private final SearchEngine.Result result;
|
||||
private final int requestId;
|
||||
public ResultRunnable(SearchEngine.Result result, int requestId) {
|
||||
this.result = result;
|
||||
this.reuqestId = requestId;
|
||||
this.requestId = requestId;
|
||||
}
|
||||
@Override
|
||||
public void run() {
|
||||
updateListOnResult(result, reuqestId);
|
||||
updateListOnResult(result, requestId);
|
||||
if (android.os.Build.VERSION.SDK_INT >= 19) {
|
||||
getListView().removeFooterView(footer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SearchRunnable implements Runnable {
|
||||
private Class engineClass = null;
|
||||
private String query;
|
||||
private int page;
|
||||
Handler h = new Handler();
|
||||
private volatile boolean run = true;
|
||||
private int requestId;
|
||||
public SearchRunnable(Class engineClass, String query, int page, int requestId) {
|
||||
this.engineClass = engineClass;
|
||||
private final SearchEngine engine;
|
||||
private final String query;
|
||||
private final int page;
|
||||
final Handler h = new Handler();
|
||||
private volatile boolean runs = true;
|
||||
private final int requestId;
|
||||
public SearchRunnable(SearchEngine engine, String query, int page, int requestId) {
|
||||
this.engine = engine;
|
||||
this.query = query;
|
||||
this.page = page;
|
||||
this.requestId = requestId;
|
||||
}
|
||||
void terminate() {
|
||||
run = false;
|
||||
runs = false;
|
||||
}
|
||||
@Override
|
||||
public void run() {
|
||||
SearchEngine engine = null;
|
||||
try {
|
||||
engine = (SearchEngine) engineClass.newInstance();
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
SearchEngine.Result result = engine.search(query, page);
|
||||
if(run) {
|
||||
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
String searchLanguageKey = getContext().getString(R.string.search_language_key);
|
||||
String searchLanguage = sp.getString(searchLanguageKey,
|
||||
getString(R.string.default_language_value));
|
||||
SearchEngine.Result result = engine.search(query, page, searchLanguage);
|
||||
Log.i(TAG, "language code passed:\""+searchLanguage+"\"");
|
||||
if(runs) {
|
||||
h.post(new ResultRunnable(result, requestId));
|
||||
}
|
||||
} catch(Exception e) {
|
||||
@@ -100,7 +118,8 @@ public class VideoItemListFragment extends ListFragment {
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(getActivity(), "Network Error", Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(getActivity(), getString(R.string.network_error),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -108,14 +127,14 @@ public class VideoItemListFragment extends ListFragment {
|
||||
}
|
||||
|
||||
private class LoadThumbsRunnable implements Runnable {
|
||||
private Vector<String> thumbnailUrlList = new Vector<>();
|
||||
private Vector<Boolean> downloadedList;
|
||||
Handler h = new Handler();
|
||||
private final Vector<String> thumbnailUrlList = new Vector<>();
|
||||
private final Vector<Boolean> downloadedList;
|
||||
final Handler h = new Handler();
|
||||
private volatile boolean run = true;
|
||||
private int requestId;
|
||||
public LoadThumbsRunnable(Vector<VideoInfoItem> videoList,
|
||||
private final int requestId;
|
||||
public LoadThumbsRunnable(Vector<VideoPreviewInfo> videoList,
|
||||
Vector<Boolean> downloadedList, int requestId) {
|
||||
for(VideoInfoItem item : videoList) {
|
||||
for(VideoPreviewInfo item : videoList) {
|
||||
thumbnailUrlList.add(item.thumbnail_url);
|
||||
}
|
||||
this.downloadedList = downloadedList;
|
||||
@@ -131,7 +150,7 @@ public class VideoItemListFragment extends ListFragment {
|
||||
public void run() {
|
||||
for(int i = 0; i < thumbnailUrlList.size() && run; i++) {
|
||||
if(!downloadedList.get(i)) {
|
||||
Bitmap thumbnail = null;
|
||||
Bitmap thumbnail;
|
||||
try {
|
||||
thumbnail = BitmapFactory.decodeStream(
|
||||
new URL(thumbnailUrlList.get(i)).openConnection().getInputStream());
|
||||
@@ -145,9 +164,9 @@ public class VideoItemListFragment extends ListFragment {
|
||||
}
|
||||
|
||||
private class SetThumbnailRunnable implements Runnable {
|
||||
private int index;
|
||||
private Bitmap thumbnail;
|
||||
private int requestId;
|
||||
private final int index;
|
||||
private final Bitmap thumbnail;
|
||||
private final int requestId;
|
||||
public SetThumbnailRunnable(int index, Bitmap thumbnail, int requestId) {
|
||||
this.index = index;
|
||||
this.thumbnail = thumbnail;
|
||||
@@ -156,31 +175,44 @@ public class VideoItemListFragment extends ListFragment {
|
||||
@Override
|
||||
public void run() {
|
||||
if(requestId == currentRequestId) {
|
||||
videoListAdapter.updateDownloadedThumbnailList(index, true);
|
||||
videoListAdapter.updateDownloadedThumbnailList(index);
|
||||
videoListAdapter.setThumbnail(index, thumbnail);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void present(List<VideoPreviewInfo> videoList) {
|
||||
mode = PRESENT_VIDEOS_MODE;
|
||||
setListShown(true);
|
||||
getListView().smoothScrollToPosition(0);
|
||||
|
||||
updateList(videoList);
|
||||
}
|
||||
|
||||
public void search(String query) {
|
||||
mode = SEARCH_MODE;
|
||||
this.query = query;
|
||||
this.lastPage = 1;
|
||||
videoListAdapter.clearVideoList();
|
||||
setListShown(false);
|
||||
startSearch(query, lastPage);
|
||||
//todo: Somehow this command is not working on older devices,
|
||||
// although it was introduced with API level 8. Test this and find a solution.
|
||||
getListView().smoothScrollToPosition(0);
|
||||
}
|
||||
|
||||
public void nextPage() {
|
||||
private void nextPage() {
|
||||
loadingNextPage = true;
|
||||
lastPage++;
|
||||
Log.d(TAG, getString(R.string.searchPage) + Integer.toString(lastPage));
|
||||
Log.d(TAG, getString(R.string.search_page) + Integer.toString(lastPage));
|
||||
startSearch(query, lastPage);
|
||||
}
|
||||
|
||||
private void startSearch(String query, int page) {
|
||||
currentRequestId++;
|
||||
terminateThreads();
|
||||
searchRunnable = new SearchRunnable(streamingService.getSearchEngineClass(), query, page, currentRequestId);
|
||||
searchRunnable = new SearchRunnable(streamingService.getSearchEngineInstance(),
|
||||
query, page, currentRequestId);
|
||||
searchThread = new Thread(searchRunnable);
|
||||
searchThread.start();
|
||||
}
|
||||
@@ -189,14 +221,14 @@ public class VideoItemListFragment extends ListFragment {
|
||||
this.streamingService = streamingService;
|
||||
}
|
||||
|
||||
public void updateListOnResult(SearchEngine.Result result, int requestId) {
|
||||
private void updateListOnResult(SearchEngine.Result result, int requestId) {
|
||||
if(requestId == currentRequestId) {
|
||||
setListShown(true);
|
||||
if (result.resultList.isEmpty()) {
|
||||
Toast.makeText(getActivity(), result.errorMessage, Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
if (!result.suggestion.isEmpty()) {
|
||||
Toast.makeText(getActivity(), getString(R.string.didYouMean) + result.suggestion + " ?",
|
||||
Toast.makeText(getActivity(), getString(R.string.did_you_mean) + result.suggestion + " ?",
|
||||
Toast.LENGTH_LONG).show();
|
||||
}
|
||||
updateList(result.resultList);
|
||||
@@ -204,7 +236,7 @@ public class VideoItemListFragment extends ListFragment {
|
||||
}
|
||||
}
|
||||
|
||||
private void updateList(Vector<VideoInfoItem> list) {
|
||||
private void updateList(List<VideoPreviewInfo> list) {
|
||||
try {
|
||||
videoListAdapter.addVideoList(list);
|
||||
terminateThreads();
|
||||
@@ -213,13 +245,15 @@ public class VideoItemListFragment extends ListFragment {
|
||||
loadThumbsThread = new Thread(loadThumbsRunnable);
|
||||
loadThumbsThread.start();
|
||||
} catch(java.lang.IllegalStateException e) {
|
||||
Log.w(TAG, "Trying to set value while activity is not existing anymore.");
|
||||
Log.w(TAG, "Trying to set value while activity doesn't exist anymore.");
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
loadingNextPage = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void terminateThreads() {
|
||||
private void terminateThreads() {
|
||||
if(loadThumbsRunnable != null && loadThumbsRunnable.isRunning()) {
|
||||
loadThumbsRunnable.terminate();
|
||||
try {
|
||||
@@ -230,15 +264,11 @@ public class VideoItemListFragment extends ListFragment {
|
||||
}
|
||||
if(searchThread != null) {
|
||||
searchRunnable.terminate();
|
||||
// No need to join, since we don't realy terminate the thread. We just demand
|
||||
// No need to join, since we don't really terminate the thread. We just demand
|
||||
// it to post its result runnable into the gui main loop.
|
||||
}
|
||||
}
|
||||
|
||||
void displayList() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The serialization (saved instance state) Bundle key representing the
|
||||
* activated item position. Only used on tablets.
|
||||
@@ -262,17 +292,17 @@ public class VideoItemListFragment extends ListFragment {
|
||||
void onItemSelected(String id);
|
||||
}
|
||||
|
||||
Callbacks mCallbacks = null;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
private Callbacks mCallbacks = null;
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list = getListView();
|
||||
videoListAdapter = new VideoListAdapter(getActivity(), this);
|
||||
footer = ((LayoutInflater) getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE))
|
||||
.inflate(R.layout.paginate_footer, null, false);
|
||||
|
||||
|
||||
setListAdapter(videoListAdapter);
|
||||
|
||||
// Restore the previously serialized activated item position.
|
||||
@@ -282,9 +312,8 @@ public class VideoItemListFragment extends ListFragment {
|
||||
setActivatedPosition(savedInstanceState.getInt(STATE_ACTIVATED_POSITION));
|
||||
}
|
||||
|
||||
|
||||
getListView().setOnScrollListener(new AbsListView.OnScrollListener() {
|
||||
private static final float OVERSCROLL_THRESHOLD_IN_PIXELS = 100;
|
||||
private float downY;
|
||||
long lastScrollDate = 0;
|
||||
|
||||
@Override
|
||||
@@ -292,14 +321,17 @@ public class VideoItemListFragment extends ListFragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
|
||||
ListView list = getListView();
|
||||
if (list.getChildAt(0) != null
|
||||
public void onScroll(AbsListView view, int firstVisibleItem,
|
||||
int visibleItemCount, int totalItemCount) {
|
||||
if (mode != PRESENT_VIDEOS_MODE
|
||||
&& list.getChildAt(0) != null
|
||||
&& list.getLastVisiblePosition() == list.getAdapter().getCount() - 1
|
||||
&& list.getChildAt(list.getChildCount() - 1).getBottom() <= list.getHeight()) {
|
||||
long time = System.currentTimeMillis();
|
||||
if ((time - lastScrollDate) > 200) {
|
||||
if ((time - lastScrollDate) > 200
|
||||
&& !loadingNextPage) {
|
||||
lastScrollDate = time;
|
||||
getListView().addFooterView(footer);
|
||||
nextPage();
|
||||
}
|
||||
}
|
||||
@@ -309,20 +341,15 @@ public class VideoItemListFragment extends ListFragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity) {
|
||||
super.onAttach(activity);
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
// Activities containing this fragment must implement its callbacks.
|
||||
if (!(activity instanceof Callbacks)) {
|
||||
if (!(context instanceof Callbacks)) {
|
||||
throw new IllegalStateException("Activity must implement fragment's callbacks.");
|
||||
}
|
||||
|
||||
mCallbacks = (Callbacks) activity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
super.onDetach();
|
||||
mCallbacks = (Callbacks) context;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -332,22 +359,11 @@ public class VideoItemListFragment extends ListFragment {
|
||||
mCallbacks.onItemSelected(Long.toString(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
/*
|
||||
if (mActivatedPosition != ListView.INVALID_POSITION) {
|
||||
// Serialize and persist the activated item position.
|
||||
outState.putInt(STATE_ACTIVATED_POSITION, mActivatedPosition);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns on activate-on-click mode. When this mode is on, list items will be
|
||||
* given the 'activated' state when touched.
|
||||
*/
|
||||
public void setActivateOnItemClick(boolean activateOnItemClick) {
|
||||
public void setActivateOnItemClick(@SuppressWarnings("SameParameterValue") boolean activateOnItemClick) {
|
||||
// When setting CHOICE_MODE_SINGLE, ListView will automatically
|
||||
// give items the 'activated' state when touched.
|
||||
getListView().setChoiceMode(activateOnItemClick
|
||||
|
||||
@@ -2,18 +2,18 @@ package org.schabi.newpipe;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
* Created by the-scrabi on 11.08.15.
|
||||
* Created by Christian Schabesberger on 11.08.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* VideoListAdapter.java is part of NewPipe.
|
||||
@@ -32,22 +32,22 @@ import java.util.Vector;
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class VideoListAdapter extends BaseAdapter {
|
||||
|
||||
private static final String TAG = VideoListAdapter.class.toString();
|
||||
private LayoutInflater inflater;
|
||||
private Vector<VideoInfoItem> videoList = new Vector<>();
|
||||
class VideoListAdapter extends BaseAdapter {
|
||||
private final Context context;
|
||||
private final VideoInfoItemViewCreator viewCreator;
|
||||
private Vector<VideoPreviewInfo> videoList = new Vector<>();
|
||||
private Vector<Boolean> downloadedThumbnailList = new Vector<>();
|
||||
VideoItemListFragment videoListFragment;
|
||||
ListView listView;
|
||||
private final ListView listView;
|
||||
|
||||
public VideoListAdapter(Context context, VideoItemListFragment videoListFragment) {
|
||||
inflater = LayoutInflater.from(context);
|
||||
this.videoListFragment = videoListFragment;
|
||||
viewCreator = new VideoInfoItemViewCreator(LayoutInflater.from(context));
|
||||
this.listView = videoListFragment.getListView();
|
||||
this.listView.setDivider(null);
|
||||
this.listView.setDividerHeight(0);
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public void addVideoList(Vector<VideoInfoItem> videos) {
|
||||
public void addVideoList(List<VideoPreviewInfo> videos) {
|
||||
videoList.addAll(videos);
|
||||
for(int i = 0; i < videos.size(); i++) {
|
||||
downloadedThumbnailList.add(false);
|
||||
@@ -61,12 +61,12 @@ public class VideoListAdapter extends BaseAdapter {
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public Vector<VideoInfoItem> getVideoList() {
|
||||
public Vector<VideoPreviewInfo> getVideoList() {
|
||||
return videoList;
|
||||
}
|
||||
|
||||
public void updateDownloadedThumbnailList(int index, boolean val) {
|
||||
downloadedThumbnailList.set(index, val);
|
||||
public void updateDownloadedThumbnailList(int index) {
|
||||
downloadedThumbnailList.set(index, true);
|
||||
}
|
||||
|
||||
public Vector<Boolean> getDownloadedThumbnailList() {
|
||||
@@ -96,42 +96,14 @@ public class VideoListAdapter extends BaseAdapter {
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
ViewHolder holder;
|
||||
if(convertView == null) {
|
||||
convertView = inflater.inflate(R.layout.video_item, parent, false);
|
||||
holder = new ViewHolder();
|
||||
holder.itemThumbnailView = (ImageView) convertView.findViewById(R.id.itemThumbnailView);
|
||||
holder.itemVideoTitleView = (TextView) convertView.findViewById(R.id.itemVideoTitleView);
|
||||
holder.itemUploaderView = (TextView) convertView.findViewById(R.id.itemUploaderView);
|
||||
holder.itemDurationView = (TextView) convertView.findViewById(R.id.itemDurationView);
|
||||
holder.itemUploadDateView = (TextView) convertView.findViewById(R.id.itemUploadDateView);
|
||||
convertView.setTag(holder);
|
||||
} else {
|
||||
holder = (ViewHolder) convertView.getTag();
|
||||
}
|
||||
|
||||
final Context context = parent.getContext();
|
||||
if(videoList.get(position).thumbnail == null) {
|
||||
holder.itemThumbnailView.setImageResource(R.drawable.dummi_thumbnail);
|
||||
} else {
|
||||
holder.itemThumbnailView.setImageBitmap(videoList.get(position).thumbnail);
|
||||
}
|
||||
holder.itemVideoTitleView.setText(videoList.get(position).title);
|
||||
holder.itemUploaderView.setText(videoList.get(position).uploader);
|
||||
holder.itemDurationView.setText(videoList.get(position).duration);
|
||||
holder.itemUploadDateView.setText(videoList.get(position).upload_date);
|
||||
convertView = viewCreator.getViewFromVideoInfoItem(convertView, parent, videoList.get(position), context);
|
||||
|
||||
if(listView.isItemChecked(position)) {
|
||||
convertView.setBackgroundColor(context.getResources().getColor(R.color.primaryColorYoutube));
|
||||
convertView.setBackgroundColor(ContextCompat.getColor(context,R.color.light_youtube_primary_color));
|
||||
} else {
|
||||
convertView.setBackgroundColor(0);
|
||||
}
|
||||
|
||||
return convertView;
|
||||
}
|
||||
|
||||
private class ViewHolder {
|
||||
public ImageView itemThumbnailView;
|
||||
public TextView itemVideoTitleView, itemUploaderView, itemDurationView, itemUploadDateView;
|
||||
}
|
||||
}
|
||||
79
app/src/main/java/org/schabi/newpipe/VideoPreviewInfo.java
Normal file
@@ -0,0 +1,79 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import org.schabi.newpipe.services.AbstractVideoInfo;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 26.08.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* VideoPreviewInfo.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/>.
|
||||
*/
|
||||
|
||||
/**Info object for previews of unopened videos, eg search results, related videos*/
|
||||
public class VideoPreviewInfo extends AbstractVideoInfo implements Parcelable {
|
||||
public String duration = "";
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
protected VideoPreviewInfo(Parcel in) {
|
||||
id = in.readString();
|
||||
title = in.readString();
|
||||
uploader = in.readString();
|
||||
duration = in.readString();
|
||||
thumbnail_url = in.readString();
|
||||
thumbnail = (Bitmap) in.readValue(Bitmap.class.getClassLoader());
|
||||
webpage_url = in.readString();
|
||||
upload_date = in.readString();
|
||||
view_count = in.readLong();
|
||||
}
|
||||
|
||||
public VideoPreviewInfo() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(id);
|
||||
dest.writeString(title);
|
||||
dest.writeString(uploader);
|
||||
dest.writeString(duration);
|
||||
dest.writeString(thumbnail_url);
|
||||
dest.writeValue(thumbnail);
|
||||
dest.writeString(webpage_url);
|
||||
dest.writeString(upload_date);
|
||||
dest.writeLong(view_count);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static final Parcelable.Creator<VideoPreviewInfo> CREATOR = new Parcelable.Creator<VideoPreviewInfo>() {
|
||||
@Override
|
||||
public VideoPreviewInfo createFromParcel(Parcel in) {
|
||||
return new VideoPreviewInfo(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VideoPreviewInfo[] newArray(int size) {
|
||||
return new VideoPreviewInfo[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.schabi.newpipe.services;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
/**Common properties between VideoInfo and VideoPreviewInfo.*/
|
||||
public abstract class AbstractVideoInfo {
|
||||
public String id = "";
|
||||
public String title = "";
|
||||
public String uploader = "";
|
||||
//public int duration = -1;
|
||||
public String thumbnail_url = "";
|
||||
public Bitmap thumbnail = null;
|
||||
public String webpage_url = "";
|
||||
public String upload_date = "";
|
||||
public long view_count = -1;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package org.schabi.newpipe.services;
|
||||
|
||||
/**
|
||||
* Created by Adam Howard on 08/11/15.
|
||||
*
|
||||
* Copyright (c) Christian Schabesberger <chris.schabesberger@mailbox.org>
|
||||
* and Adam Howard <achdisposable1@gmail.com> 2015
|
||||
*
|
||||
* VideoListAdapter.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/>.
|
||||
*/
|
||||
|
||||
/**Static data about various media formats support by Newpipe, eg mime type, extension*/
|
||||
|
||||
public enum MediaFormat {
|
||||
// id name suffix mime type
|
||||
MPEG_4 (0x0, "MPEG-4", "mp4", "video/mp4"),
|
||||
v3GPP (0x1, "3GPP", "3gp", "video/3gpp"),
|
||||
WEBM (0x2, "WebM", "webm", "video/webm"),
|
||||
M4A (0x3, "m4a", "m4a", "audio/mp4"),
|
||||
WEBMA (0x4, "WebM", "webm", "audio/webm");
|
||||
|
||||
public final int id;
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public final String name;
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public final String suffix;
|
||||
public final String mimeType;
|
||||
|
||||
MediaFormat(int id, String name, String suffix, String mimeType) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.suffix = suffix;
|
||||
this.mimeType = mimeType;
|
||||
}
|
||||
|
||||
/**Return the friendly name of the media format with the supplied id
|
||||
* @param ident the id of the media format. Currently an arbitrary, NewPipe-specific number.
|
||||
* @return the friendly name of the MediaFormat associated with this ids,
|
||||
* or an empty String if none match it.*/
|
||||
public static String getNameById(int ident) {
|
||||
for (MediaFormat vf : MediaFormat.values()) {
|
||||
if(vf.id == ident) return vf.name;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**Return the file extension of the media format with the supplied id
|
||||
* @param ident the id of the media format. Currently an arbitrary, NewPipe-specific number.
|
||||
* @return the file extension of the MediaFormat associated with this ids,
|
||||
* or an empty String if none match it.*/
|
||||
public static String getSuffixById(int ident) {
|
||||
for (MediaFormat vf : MediaFormat.values()) {
|
||||
if(vf.id == ident) return vf.suffix;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**Return the MIME type of the media format with the supplied id
|
||||
* @param ident the id of the media format. Currently an arbitrary, NewPipe-specific number.
|
||||
* @return the MIME type of the MediaFormat associated with this ids,
|
||||
* or an empty String if none match it.*/
|
||||
public static String getMimeById(int ident) {
|
||||
for (MediaFormat vf : MediaFormat.values()) {
|
||||
if(vf.id == ident) return vf.mimeType;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
package org.schabi.newpipe;
|
||||
package org.schabi.newpipe.services;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import org.schabi.newpipe.VideoPreviewInfo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
@@ -24,14 +25,18 @@ import java.util.Vector;
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@SuppressWarnings("ALL")
|
||||
public interface SearchEngine {
|
||||
|
||||
|
||||
class Result {
|
||||
public String errorMessage = "";
|
||||
public String suggestion = "";
|
||||
public Vector<VideoInfoItem> resultList = new Vector<>();
|
||||
public final Vector<VideoPreviewInfo> resultList = new Vector<>();
|
||||
}
|
||||
|
||||
Result search(String query, int page);
|
||||
ArrayList<String> suggestionList(String query);
|
||||
|
||||
//Result search(String query, int page);
|
||||
Result search(String query, int page, String contentCountry);
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package org.schabi.newpipe;
|
||||
package org.schabi.newpipe.services;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.schabi.newpipe.youtube.YoutubeService;
|
||||
import org.schabi.newpipe.services.youtube.YoutubeService;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 23.08.15.
|
||||
@@ -24,6 +24,10 @@ import org.schabi.newpipe.youtube.YoutubeService;
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**Provides access to the video streaming services supported by NewPipe.
|
||||
* Currently only Youtube until the API becomes more stable.*/
|
||||
|
||||
@SuppressWarnings("ALL")
|
||||
public class ServiceList {
|
||||
private static final String TAG = ServiceList.class.toString();
|
||||
private static final StreamingService[] services = {
|
||||
@@ -40,7 +44,7 @@ public class ServiceList {
|
||||
}
|
||||
public static int getIdOfService(String serviceName) {
|
||||
for(int i = 0; i < services.length; i++) {
|
||||
if(services[i].getServiceInfo().name == serviceName) {
|
||||
if(services[i].getServiceInfo().name.equals(serviceName)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe;
|
||||
package org.schabi.newpipe.services;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 23.08.15.
|
||||
@@ -25,11 +25,11 @@ public interface StreamingService {
|
||||
public String name = "";
|
||||
}
|
||||
ServiceInfo getServiceInfo();
|
||||
Class getExtractorClass();
|
||||
Class getSearchEngineClass();
|
||||
VideoExtractor getExtractorInstance(String url);
|
||||
SearchEngine getSearchEngineInstance();
|
||||
|
||||
// When a VIEW_ACTION is caught this function will test if the url delivered within the calling
|
||||
// Intent was meant to be watched with this Service.
|
||||
// Return false if this service shall not allow to be callean through ACTIONs.
|
||||
/**When a VIEW_ACTION is caught this function will test if the url delivered within the calling
|
||||
Intent was meant to be watched with this Service.
|
||||
Return false if this service shall not allow to be called through ACTIONs.*/
|
||||
boolean acceptUrl(String videoUrl);
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package org.schabi.newpipe.services;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 10.08.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* VideoExtractor.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/>.
|
||||
*/
|
||||
|
||||
/**Scrapes information from a video streaming service (eg, YouTube).*/
|
||||
|
||||
@SuppressWarnings("ALL")
|
||||
public abstract class VideoExtractor {
|
||||
protected final String pageUrl;
|
||||
protected VideoInfo videoInfo;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public VideoExtractor(String url) {
|
||||
this.pageUrl = url;
|
||||
}
|
||||
|
||||
/**Fills out the video info fields which are common to all services.
|
||||
* Probably needs to be overridden by subclasses*/
|
||||
public VideoInfo getVideoInfo()
|
||||
{
|
||||
if(videoInfo == null) {
|
||||
videoInfo = new VideoInfo();
|
||||
}
|
||||
|
||||
if(videoInfo.webpage_url.isEmpty()) {
|
||||
videoInfo.webpage_url = pageUrl;
|
||||
}
|
||||
|
||||
if(getErrorCode() == VideoInfo.NO_ERROR) {
|
||||
|
||||
if (videoInfo.title.isEmpty()) {
|
||||
videoInfo.title = getTitle();
|
||||
}
|
||||
|
||||
if (videoInfo.duration < 1) {
|
||||
videoInfo.duration = getLength();
|
||||
}
|
||||
|
||||
|
||||
if (videoInfo.uploader.isEmpty()) {
|
||||
videoInfo.uploader = getUploader();
|
||||
}
|
||||
|
||||
if (videoInfo.description.isEmpty()) {
|
||||
videoInfo.description = getDescription();
|
||||
}
|
||||
|
||||
if (videoInfo.view_count == -1) {
|
||||
videoInfo.view_count = getViews();
|
||||
}
|
||||
|
||||
if (videoInfo.upload_date.isEmpty()) {
|
||||
videoInfo.upload_date = getUploadDate();
|
||||
}
|
||||
|
||||
if (videoInfo.thumbnail_url.isEmpty()) {
|
||||
videoInfo.thumbnail_url = getThumbnailUrl();
|
||||
}
|
||||
|
||||
if (videoInfo.id.isEmpty()) {
|
||||
videoInfo.id = getVideoId(pageUrl);
|
||||
}
|
||||
|
||||
/** Load and extract audio*/
|
||||
if (videoInfo.audioStreams == null) {
|
||||
videoInfo.audioStreams = getAudioStreams();
|
||||
}
|
||||
/** Extract video stream url*/
|
||||
if (videoInfo.videoStreams == null) {
|
||||
videoInfo.videoStreams = getVideoStreams();
|
||||
}
|
||||
|
||||
if (videoInfo.uploader_thumbnail_url.isEmpty()) {
|
||||
videoInfo.uploader_thumbnail_url = getUploaderThumbnailUrl();
|
||||
}
|
||||
|
||||
if (videoInfo.startPosition < 0) {
|
||||
videoInfo.startPosition = getTimeStamp();
|
||||
}
|
||||
} else {
|
||||
videoInfo.errorCode = getErrorCode();
|
||||
videoInfo.errorMessage = getErrorMessage();
|
||||
}
|
||||
|
||||
//Bitmap thumbnail = null;
|
||||
//Bitmap uploader_thumbnail = null;
|
||||
//int videoAvailableStatus = VIDEO_AVAILABLE;
|
||||
return videoInfo;
|
||||
}
|
||||
|
||||
//todo: add licence field
|
||||
public abstract int getErrorCode();
|
||||
public abstract String getErrorMessage();
|
||||
|
||||
//todo: remove these functions, or make them static, otherwise its useles, to have them here
|
||||
public abstract String getVideoUrl(String videoId);
|
||||
public abstract String getVideoId(String siteUrl);
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
public abstract int getTimeStamp();
|
||||
public abstract String getTitle();
|
||||
public abstract String getDescription();
|
||||
public abstract String getUploader();
|
||||
public abstract int getLength();
|
||||
public abstract long getViews();
|
||||
public abstract String getUploadDate();
|
||||
public abstract String getThumbnailUrl();
|
||||
public abstract String getUploaderThumbnailUrl();
|
||||
public abstract VideoInfo.AudioStream[] getAudioStreams();
|
||||
public abstract VideoInfo.VideoStream[] getVideoStreams();
|
||||
}
|
||||
106
app/src/main/java/org/schabi/newpipe/services/VideoInfo.java
Normal file
@@ -0,0 +1,106 @@
|
||||
package org.schabi.newpipe.services;
|
||||
|
||||
import org.schabi.newpipe.VideoPreviewInfo;
|
||||
import org.schabi.newpipe.services.AbstractVideoInfo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 26.08.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* VideoInfo.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/>.
|
||||
*/
|
||||
|
||||
/**Info object for opened videos, ie the video ready to play.*/
|
||||
@SuppressWarnings("ALL")
|
||||
public class VideoInfo extends AbstractVideoInfo {
|
||||
|
||||
// If a video could not be parsed, this predefined error codes
|
||||
// will be returned AND can be parsed by the frontend of the app.
|
||||
// Error codes:
|
||||
public final static int NO_ERROR = 0x0;
|
||||
public final static int ERROR_NO_SPECIFIED_ERROR = 0x1;
|
||||
// GEMA a german music colecting society.
|
||||
public final static int ERROR_BLOCKED_BY_GEMA = 0x2;
|
||||
|
||||
public String uploader_thumbnail_url = "";
|
||||
public String description = "";
|
||||
public VideoStream[] videoStreams = null;
|
||||
public AudioStream[] audioStreams = null;
|
||||
public int errorCode = NO_ERROR;
|
||||
public String errorMessage = "";
|
||||
public int duration = -1;
|
||||
|
||||
/*YouTube-specific fields
|
||||
todo: move these to a subclass*/
|
||||
public int age_limit = 0;
|
||||
public int like_count = -1;
|
||||
public int dislike_count = -1;
|
||||
public String average_rating = "";
|
||||
public VideoPreviewInfo nextVideo = null;
|
||||
public List<VideoPreviewInfo> relatedVideos = null;
|
||||
public int startPosition = -1;//in seconds. some metadata is not passed using a VideoInfo object!
|
||||
|
||||
public VideoInfo() {}
|
||||
|
||||
|
||||
/**Creates a new VideoInfo object from an existing AbstractVideoInfo.
|
||||
* All the shared properties are copied to the new VideoInfo.*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public VideoInfo(AbstractVideoInfo avi) {
|
||||
this.id = avi.id;
|
||||
this.title = avi.title;
|
||||
this.uploader = avi.uploader;
|
||||
this.thumbnail_url = avi.thumbnail_url;
|
||||
this.thumbnail = avi.thumbnail;
|
||||
this.webpage_url = avi.webpage_url;
|
||||
this.upload_date = avi.upload_date;
|
||||
this.upload_date = avi.upload_date;
|
||||
this.view_count = avi.view_count;
|
||||
|
||||
//todo: better than this
|
||||
if(avi instanceof VideoPreviewInfo) {//shitty String to convert code
|
||||
String dur = ((VideoPreviewInfo)avi).duration;
|
||||
int minutes = Integer.parseInt(dur.substring(0, dur.indexOf(":")));
|
||||
int seconds = Integer.parseInt(dur.substring(dur.indexOf(":")+1, dur.length()));
|
||||
this.duration = (minutes*60)+seconds;
|
||||
}
|
||||
}
|
||||
|
||||
public static class VideoStream {
|
||||
public String url = ""; //url of the stream
|
||||
public int format = -1;
|
||||
public String resolution = "";
|
||||
|
||||
public VideoStream(String url, int format, String res) {
|
||||
this.url = url; this.format = format; resolution = res;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static class AudioStream {
|
||||
public String url = "";
|
||||
public int format = -1;
|
||||
public int bandwidth = -1;
|
||||
public int samplingRate = -1;
|
||||
|
||||
public AudioStream(String url, int format, int bandwidth, int samplingRate) {
|
||||
this.url = url; this.format = format;
|
||||
this.bandwidth = bandwidth; this.samplingRate = samplingRate;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,28 @@
|
||||
package org.schabi.newpipe.youtube;
|
||||
|
||||
import org.schabi.newpipe.Downloader;
|
||||
import org.schabi.newpipe.SearchEngine;
|
||||
import org.schabi.newpipe.VideoInfoItem;
|
||||
package org.schabi.newpipe.services.youtube;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.schabi.newpipe.Downloader;
|
||||
import org.schabi.newpipe.services.SearchEngine;
|
||||
import org.schabi.newpipe.VideoPreviewInfo;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.InputSource;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 09.08.15.
|
||||
@@ -39,7 +49,8 @@ public class YoutubeSearchEngine implements SearchEngine {
|
||||
private static final String TAG = YoutubeSearchEngine.class.toString();
|
||||
|
||||
@Override
|
||||
public Result search(String query, int page) {
|
||||
public Result search(String query, int page, String languageCode) {
|
||||
//String contentCountry = PreferenceManager.getDefaultSharedPreferences(this).getString(getString(R.string., "");
|
||||
Uri.Builder builder = new Uri.Builder();
|
||||
builder.scheme("https")
|
||||
.authority("www.youtube.com")
|
||||
@@ -47,9 +58,19 @@ public class YoutubeSearchEngine implements SearchEngine {
|
||||
.appendQueryParameter("search_query", query)
|
||||
.appendQueryParameter("page", Integer.toString(page))
|
||||
.appendQueryParameter("filters", "video");
|
||||
String url = builder.build().toString();
|
||||
|
||||
String site = Downloader.download(url);
|
||||
String site;
|
||||
String url = builder.build().toString();
|
||||
//if we've been passed a valid language code, append it to the URL
|
||||
if(!languageCode.isEmpty()) {
|
||||
//assert Pattern.matches("[a-z]{2}(-([A-Z]{2}|[0-9]{1,3}))?", languageCode);
|
||||
site = Downloader.download(url, languageCode);
|
||||
}
|
||||
else {
|
||||
site = Downloader.download(url);
|
||||
}
|
||||
|
||||
|
||||
Document doc = Jsoup.parse(site, url);
|
||||
Result result = new Result();
|
||||
Element list = doc.select("ol[class=\"item-section\"]").first();
|
||||
@@ -58,14 +79,14 @@ public class YoutubeSearchEngine implements SearchEngine {
|
||||
int i = 0;
|
||||
for(Element item : list.children()) {
|
||||
i++;
|
||||
/* First we need to determine witch kind of item we are working with.
|
||||
Youtube depicts fife different kinds if items at its search result page. These are
|
||||
regular videos, playlists, channels, two types of video suggestions, and a no video
|
||||
found item. Since we only want videos, we net to filter out all the others.
|
||||
/* First we need to determine which kind of item we are working with.
|
||||
Youtube depicts five different kinds of items on its search result page. These are
|
||||
regular videos, playlists, channels, two types of video suggestions, and a "no video
|
||||
found" item. Since we only want videos, we need to filter out all the others.
|
||||
An example for this can be seen here:
|
||||
https://www.youtube.com/results?search_query=asdf&page=1
|
||||
|
||||
We already applied a filter to the url, so we don't need to care about channels, and
|
||||
We already applied a filter to the url, so we don't need to care about channels and
|
||||
playlists now.
|
||||
*/
|
||||
|
||||
@@ -74,26 +95,26 @@ public class YoutubeSearchEngine implements SearchEngine {
|
||||
// both types of spell correction item
|
||||
if(!((el = item.select("div[class*=\"spell-correction\"]").first()) == null)) {
|
||||
result.suggestion = el.select("a").first().text();
|
||||
// search message item
|
||||
// search message item
|
||||
} else if(!((el = item.select("div[class*=\"search-message\"]").first()) == null)) {
|
||||
result.errorMessage = el.text();
|
||||
|
||||
// video item type
|
||||
// video item type
|
||||
} else if(!((el = item.select("div[class*=\"yt-lockup-video\"").first()) == null)) {
|
||||
VideoInfoItem resultItem = new VideoInfoItem();
|
||||
VideoPreviewInfo resultItem = new VideoPreviewInfo();
|
||||
Element dl = el.select("h3").first().select("a").first();
|
||||
resultItem.webpage_url = dl.attr("abs:href");
|
||||
try {
|
||||
Pattern p = Pattern.compile("v=([0-9a-zA-Z-]*)");
|
||||
Matcher m = p.matcher(resultItem.webpage_url);
|
||||
m.find();
|
||||
resultItem.id=m.group(1);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
//e.printStackTrace();
|
||||
}
|
||||
resultItem.title = dl.text();
|
||||
resultItem.duration = item.select("span[class=\"video-time\"]").first()
|
||||
.text();
|
||||
|
||||
resultItem.duration = item.select("span[class=\"video-time\"]").first().text();
|
||||
|
||||
resultItem.uploader = item.select("div[class=\"yt-lockup-byline\"]").first()
|
||||
.select("a").first()
|
||||
.text();
|
||||
@@ -103,18 +124,67 @@ public class YoutubeSearchEngine implements SearchEngine {
|
||||
Element te = item.select("div[class=\"yt-thumb video-thumb\"]").first()
|
||||
.select("img").first();
|
||||
resultItem.thumbnail_url = te.attr("abs:src");
|
||||
// Sometimes youtube sends links to gif files witch somehow seam to not exist
|
||||
// Sometimes youtube sends links to gif files which somehow seem to not exist
|
||||
// anymore. Items with such gif also offer a secondary image source. So we are going
|
||||
// to use that if we caught such an item.
|
||||
// to use that if we've caught such an item.
|
||||
if(resultItem.thumbnail_url.contains(".gif")) {
|
||||
resultItem.thumbnail_url = te.attr("abs:data-thumb");
|
||||
}
|
||||
result.resultList.add(resultItem);
|
||||
} else {
|
||||
Log.e(TAG, "GREAT FUCKING ERROR");
|
||||
//noinspection ConstantConditions
|
||||
Log.e(TAG, "unexpected element found:\""+el+"\"");
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ArrayList<String> suggestionList(String query) {
|
||||
|
||||
ArrayList<String> suggestions = new ArrayList<>();
|
||||
|
||||
Uri.Builder builder = new Uri.Builder();
|
||||
builder.scheme("https")
|
||||
.authority("suggestqueries.google.com")
|
||||
.appendPath("complete")
|
||||
.appendPath("search")
|
||||
.appendQueryParameter("client", "")
|
||||
.appendQueryParameter("output", "toolbar")
|
||||
.appendQueryParameter("ds", "yt")
|
||||
.appendQueryParameter("q", query);
|
||||
String url = builder.build().toString();
|
||||
|
||||
String response = Downloader.download(url);
|
||||
|
||||
//TODO: Parse xml data using Jsoup not done
|
||||
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder dBuilder;
|
||||
org.w3c.dom.Document doc = null;
|
||||
|
||||
try {
|
||||
dBuilder = dbFactory.newDocumentBuilder();
|
||||
doc = dBuilder.parse(new InputSource(new ByteArrayInputStream(response.getBytes("utf-8"))));
|
||||
doc.getDocumentElement().normalize();
|
||||
}catch (ParserConfigurationException | SAXException | IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
if(doc!=null){
|
||||
NodeList nList = doc.getElementsByTagName("CompleteSuggestion");
|
||||
for (int temp = 0; temp < nList.getLength(); temp++) {
|
||||
|
||||
NodeList nList1 = doc.getElementsByTagName("suggestion");
|
||||
Node nNode1 = nList1.item(temp);
|
||||
if (nNode1.getNodeType() == Node.ELEMENT_NODE) {
|
||||
org.w3c.dom.Element eElement = (org.w3c.dom.Element) nNode1;
|
||||
suggestions.add(eElement.getAttribute("data"));
|
||||
}
|
||||
}
|
||||
}else {
|
||||
Log.e(TAG, "GREAT FUCKING ERROR");
|
||||
}
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package org.schabi.newpipe.youtube;
|
||||
package org.schabi.newpipe.services.youtube;
|
||||
|
||||
import org.schabi.newpipe.services.StreamingService;
|
||||
import org.schabi.newpipe.services.VideoExtractor;
|
||||
import org.schabi.newpipe.services.SearchEngine;
|
||||
|
||||
import org.schabi.newpipe.StreamingService;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 23.08.15.
|
||||
@@ -30,12 +33,17 @@ public class YoutubeService implements StreamingService {
|
||||
return serviceInfo;
|
||||
}
|
||||
@Override
|
||||
public Class getExtractorClass() {
|
||||
return YoutubeExtractor.class;
|
||||
public VideoExtractor getExtractorInstance(String url) {
|
||||
if(acceptUrl(url)) {
|
||||
return new YoutubeVideoExtractor(url);
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException("supplied String is not a valid Youtube URL");
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public Class getSearchEngineClass() {
|
||||
return YoutubeSearchEngine.class;
|
||||
public SearchEngine getSearchEngineInstance() {
|
||||
return new YoutubeSearchEngine();
|
||||
}
|
||||
@Override
|
||||
public boolean acceptUrl(String videoUrl) {
|
||||
@@ -0,0 +1,644 @@
|
||||
package org.schabi.newpipe.services.youtube;
|
||||
|
||||
import android.util.Log;
|
||||
import android.util.Xml;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.jsoup.parser.Parser;
|
||||
import org.mozilla.javascript.Context;
|
||||
import org.mozilla.javascript.Function;
|
||||
import org.mozilla.javascript.ScriptableObject;
|
||||
import org.schabi.newpipe.Downloader;
|
||||
import org.schabi.newpipe.services.VideoExtractor;
|
||||
import org.schabi.newpipe.services.MediaFormat;
|
||||
import org.schabi.newpipe.services.VideoInfo;
|
||||
import org.schabi.newpipe.VideoPreviewInfo;
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
|
||||
import java.io.StringReader;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Vector;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 06.08.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* YoutubeVideoExtractor.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 YoutubeVideoExtractor extends VideoExtractor {
|
||||
|
||||
private static final String TAG = YoutubeVideoExtractor.class.toString();
|
||||
private final Document doc;
|
||||
private JSONObject jsonObj;
|
||||
private JSONObject playerArgs;
|
||||
private int errorCode = VideoInfo.NO_ERROR;
|
||||
private String errorMessage = "";
|
||||
|
||||
// static values
|
||||
private static final String DECRYPTION_FUNC_NAME="decrypt";
|
||||
|
||||
// cached values
|
||||
private static volatile String decryptionCode = "";
|
||||
|
||||
public YoutubeVideoExtractor(String pageUrl) {
|
||||
super(pageUrl);//most common videoInfo fields are now set in our superclass, for all services
|
||||
String pageContents = Downloader.download(cleanUrl(pageUrl));
|
||||
doc = Jsoup.parse(pageContents, pageUrl);
|
||||
|
||||
//attempt to load the youtube js player JSON arguments
|
||||
try {
|
||||
String jsonString = matchGroup1("ytplayer.config\\s*=\\s*(\\{.*?\\});", pageContents);
|
||||
//todo: implement this by try and catch. TESTING THE STRING AGAINST EMPTY IS CONSIDERED POOR STYLE !!!
|
||||
if(jsonString.isEmpty()) {
|
||||
errorCode = findErrorReason(doc);
|
||||
return;
|
||||
}
|
||||
|
||||
jsonObj = new JSONObject(jsonString);
|
||||
playerArgs = jsonObj.getJSONObject("args");
|
||||
|
||||
} catch (Exception e) {//if this fails, the video is most likely not available.
|
||||
// Determining why is done later.
|
||||
videoInfo.errorCode = VideoInfo.ERROR_NO_SPECIFIED_ERROR;
|
||||
Log.e(TAG, "Could not load JSON data for Youtube video \""+pageUrl+"\". This most likely means the video is unavailable");
|
||||
}
|
||||
|
||||
//----------------------------------
|
||||
// load and parse description code, if it isn't already initialised
|
||||
//----------------------------------
|
||||
if (decryptionCode.isEmpty()) {
|
||||
try {
|
||||
// The Youtube service needs to be initialized by downloading the
|
||||
// js-Youtube-player. This is done in order to get the algorithm
|
||||
// for decrypting cryptic signatures inside certain stream urls.
|
||||
JSONObject ytAssets = jsonObj.getJSONObject("assets");
|
||||
String playerUrl = ytAssets.getString("js");
|
||||
|
||||
if (playerUrl.startsWith("//")) {
|
||||
playerUrl = "https:" + playerUrl;
|
||||
}
|
||||
decryptionCode = loadDecryptionCode(playerUrl);
|
||||
} catch (Exception e){
|
||||
Log.e(TAG, "Could not load decryption code for the Youtube service.");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
try {//json player args method
|
||||
return playerArgs.getString("title");
|
||||
} catch(JSONException je) {//html <meta> method
|
||||
je.printStackTrace();
|
||||
Log.w(TAG, "failed to load title from JSON args; trying to extract it from HTML");
|
||||
} try { // fall through to fall-back
|
||||
return doc.select("meta[name=title]").attr("content");
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "failed permanently to load title.");
|
||||
e.printStackTrace();
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
try {
|
||||
return doc.select("p[id=\"eow-description\"]").first().html();
|
||||
} catch (Exception e) {//todo: add fallback method
|
||||
Log.e(TAG, "failed to load description.");
|
||||
e.printStackTrace();
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUploader() {
|
||||
try {//json player args method
|
||||
return playerArgs.getString("author");
|
||||
} catch(JSONException je) {
|
||||
je.printStackTrace();
|
||||
Log.w(TAG, "failed to load uploader name from JSON args; trying to extract it from HTML");
|
||||
} try {//fall through to fallback HTML method
|
||||
return doc.select("div.yt-user-info").first().text();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "failed permanently to load uploader name.");
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLength() {
|
||||
try {
|
||||
return playerArgs.getInt("length_seconds");
|
||||
} catch (JSONException je) {//todo: find fallback method
|
||||
Log.e(TAG, "failed to load video duration from JSON args");
|
||||
je.printStackTrace();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getViews() {
|
||||
try {
|
||||
String viewCountString = doc.select("meta[itemprop=interactionCount]").attr("content");
|
||||
return Long.parseLong(viewCountString);
|
||||
} catch (Exception e) {//todo: find fallback method
|
||||
Log.e(TAG, "failed to number of views");
|
||||
e.printStackTrace();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUploadDate() {
|
||||
try {
|
||||
return doc.select("meta[itemprop=datePublished]").attr("content");
|
||||
} catch (Exception e) {//todo: add fallback method
|
||||
Log.e(TAG, "failed to get upload date.");
|
||||
e.printStackTrace();
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getThumbnailUrl() {
|
||||
//first attempt getting a small image version
|
||||
//in the html extracting part we try to get a thumbnail with a higher resolution
|
||||
// Try to get high resolution thumbnail if it fails use low res from the player instead
|
||||
try {
|
||||
return doc.select("link[itemprop=\"thumbnailUrl\"]").first().attr("abs:href");
|
||||
} catch(Exception e) {
|
||||
Log.w(TAG, "Could not find high res Thumbnail. Using low res instead");
|
||||
//fall through to fallback
|
||||
} try {
|
||||
return playerArgs.getString("thumbnail_url");
|
||||
} catch (JSONException je) {
|
||||
je.printStackTrace();
|
||||
Log.w(TAG, "failed to extract thumbnail URL from JSON args; trying to extract it from HTML");
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUploaderThumbnailUrl() {
|
||||
try {
|
||||
return doc.select("a[class*=\"yt-user-photo\"]").first()
|
||||
.select("img").first()
|
||||
.attr("abs:data-thumb");
|
||||
} catch (Exception e) {//todo: add fallback method
|
||||
Log.e(TAG, "failed to get uploader thumbnail URL.");
|
||||
e.printStackTrace();
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public VideoInfo.AudioStream[] getAudioStreams() {
|
||||
try {
|
||||
String dashManifest = playerArgs.getString("dashmpd");
|
||||
return parseDashManifest(dashManifest, decryptionCode);
|
||||
|
||||
} catch (NullPointerException e) {
|
||||
Log.e(TAG, "Could not find \"dashmpd\" upon the player args (maybe no dash manifest available).");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return new VideoInfo.AudioStream[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public VideoInfo.VideoStream[] getVideoStreams() {
|
||||
try{
|
||||
//------------------------------------
|
||||
// extract video stream url
|
||||
//------------------------------------
|
||||
String encoded_url_map = playerArgs.getString("url_encoded_fmt_stream_map");
|
||||
Vector<VideoInfo.VideoStream> videoStreams = new Vector<>();
|
||||
for(String url_data_str : encoded_url_map.split(",")) {
|
||||
Map<String, String> tags = new HashMap<>();
|
||||
for(String raw_tag : Parser.unescapeEntities(url_data_str, true).split("&")) {
|
||||
String[] split_tag = raw_tag.split("=");
|
||||
tags.put(split_tag[0], split_tag[1]);
|
||||
}
|
||||
|
||||
int itag = Integer.parseInt(tags.get("itag"));
|
||||
String streamUrl = URLDecoder.decode(tags.get("url"), "UTF-8");
|
||||
|
||||
// if video has a signature: decrypt it and add it to the url
|
||||
if(tags.get("s") != null) {
|
||||
streamUrl = streamUrl + "&signature=" + decryptSignature(tags.get("s"), decryptionCode);
|
||||
}
|
||||
|
||||
if(resolveFormat(itag) != -1) {
|
||||
videoStreams.add(new VideoInfo.VideoStream(
|
||||
streamUrl,
|
||||
resolveFormat(itag),
|
||||
resolveResolutionString(itag)));
|
||||
}
|
||||
}
|
||||
return videoStreams.toArray(new VideoInfo.VideoStream[videoStreams.size()]);
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Failed to get video stream");
|
||||
e.printStackTrace();
|
||||
return new VideoInfo.VideoStream[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**These lists only contain itag formats that are supported by the common Android Video player.
|
||||
However if you are looking for a list showing all itag formats, look at
|
||||
https://github.com/rg3/youtube-dl/issues/1687 */
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public static int resolveFormat(int itag) {
|
||||
switch(itag) {
|
||||
// video
|
||||
case 17: return MediaFormat.v3GPP.id;
|
||||
case 18: return MediaFormat.MPEG_4.id;
|
||||
case 22: return MediaFormat.MPEG_4.id;
|
||||
case 36: return MediaFormat.v3GPP.id;
|
||||
case 37: return MediaFormat.MPEG_4.id;
|
||||
case 38: return MediaFormat.MPEG_4.id;
|
||||
case 43: return MediaFormat.WEBM.id;
|
||||
case 44: return MediaFormat.WEBM.id;
|
||||
case 45: return MediaFormat.WEBM.id;
|
||||
case 46: return MediaFormat.WEBM.id;
|
||||
default:
|
||||
//Log.i(TAG, "Itag " + Integer.toString(itag) + " not known or not supported.");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public static String resolveResolutionString(int itag) {
|
||||
switch(itag) {
|
||||
case 17: return "144p";
|
||||
case 18: return "360p";
|
||||
case 22: return "720p";
|
||||
case 36: return "240p";
|
||||
case 37: return "1080p";
|
||||
case 38: return "1080p";
|
||||
case 43: return "360p";
|
||||
case 44: return "480p";
|
||||
case 45: return "720p";
|
||||
case 46: return "1080p";
|
||||
default:
|
||||
//Log.i(TAG, "Itag " + Integer.toString(itag) + " not known or not supported.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@Override
|
||||
public String getVideoId(String url) {
|
||||
String id;
|
||||
String pat;
|
||||
|
||||
if(url.contains("youtube")) {
|
||||
pat = "youtube\\.com/watch\\?v=([\\-a-zA-Z0-9_]{11})";
|
||||
}
|
||||
else if(url.contains("youtu.be")) {
|
||||
pat = "youtu\\.be/([a-zA-Z0-9_-]{11})";
|
||||
}
|
||||
else {
|
||||
Log.e(TAG, "Error could not parse url: " + url);
|
||||
return "";
|
||||
}
|
||||
id = matchGroup1(pat, url);
|
||||
if(!id.isEmpty()){
|
||||
//Log.i(TAG, "string \""+url+"\" matches!");
|
||||
return id;
|
||||
}
|
||||
//Log.i(TAG, "string \""+url+"\" does not match.");
|
||||
return "";
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@Override
|
||||
public String getVideoUrl(String videoId) {
|
||||
return "https://www.youtube.com/watch?v=" + videoId;
|
||||
}
|
||||
|
||||
/**Attempts to parse (and return) the offset to start playing the video from.
|
||||
* @return the offset (in seconds), or 0 if no timestamp is found.*/
|
||||
@Override
|
||||
public int getTimeStamp(){
|
||||
String timeStamp = matchGroup1("((#|&)t=\\d{0,3}h?\\d{0,3}m?\\d{1,3}s?)", pageUrl);
|
||||
|
||||
//TODO: test this
|
||||
if(!timeStamp.isEmpty()) {
|
||||
String secondsString = matchGroup1("(\\d{1,3})s", timeStamp);
|
||||
String minutesString = matchGroup1("(\\d{1,3})m", timeStamp);
|
||||
String hoursString = matchGroup1("(\\d{1,3})h", timeStamp);
|
||||
|
||||
if(secondsString.isEmpty()//if nothing was got,
|
||||
&& minutesString.isEmpty()//treat as unlabelled seconds
|
||||
&& hoursString.isEmpty())
|
||||
secondsString = matchGroup1("t=(\\d{1,3})", timeStamp);
|
||||
|
||||
int seconds = (secondsString.isEmpty() ? 0 : Integer.parseInt(secondsString));
|
||||
int minutes = (minutesString.isEmpty() ? 0 : Integer.parseInt(minutesString));
|
||||
int hours = (hoursString.isEmpty() ? 0 : Integer.parseInt(hoursString));
|
||||
|
||||
int ret = seconds + (60*minutes) + (3600*hours);//don't trust BODMAS!
|
||||
//Log.d(TAG, "derived timestamp value:"+ret);
|
||||
return ret;
|
||||
//the ordering varies internationally
|
||||
}//else, return default 0
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VideoInfo getVideoInfo() {
|
||||
//todo: @medovax i like your work, but what the fuck:
|
||||
videoInfo = super.getVideoInfo();
|
||||
|
||||
if(errorCode == VideoInfo.NO_ERROR) {
|
||||
//todo: replace this with a call to getVideoId, if possible
|
||||
videoInfo.id = matchGroup1("v=([0-9a-zA-Z_-]{11})", pageUrl);
|
||||
|
||||
if (videoInfo.audioStreams == null
|
||||
|| videoInfo.audioStreams.length == 0) {
|
||||
Log.e(TAG, "uninitialised audio streams!");
|
||||
}
|
||||
|
||||
if (videoInfo.videoStreams == null
|
||||
|| videoInfo.videoStreams.length == 0) {
|
||||
Log.e(TAG, "uninitialised video streams!");
|
||||
}
|
||||
|
||||
videoInfo.age_limit = 0;
|
||||
|
||||
//average rating
|
||||
try {
|
||||
videoInfo.average_rating = playerArgs.getString("avg_rating");
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
//---------------------------------------
|
||||
// extracting information from html page
|
||||
//---------------------------------------
|
||||
|
||||
/* Code does not work here anymore.
|
||||
// Determine what went wrong when the Video is not available
|
||||
if(videoInfo.errorCode == VideoInfo.ERROR_NO_SPECIFIED_ERROR) {
|
||||
if(doc.select("h1[id=\"unavailable-message\"]").first().text().contains("GEMA")) {
|
||||
videoInfo.videoAvailableStatus = VideoInfo.VIDEO_UNAVAILABLE_GEMA;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
String likesString = "";
|
||||
String dislikesString = "";
|
||||
try {
|
||||
// likes
|
||||
likesString = doc.select("button.like-button-renderer-like-button").first()
|
||||
.select("span.yt-uix-button-content").first().text();
|
||||
videoInfo.like_count = Integer.parseInt(likesString.replaceAll("[^\\d]", ""));
|
||||
// dislikes
|
||||
dislikesString = doc.select("button.like-button-renderer-dislike-button").first()
|
||||
.select("span.yt-uix-button-content").first().text();
|
||||
|
||||
videoInfo.dislike_count = Integer.parseInt(dislikesString.replaceAll("[^\\d]", ""));
|
||||
} catch (NumberFormatException nfe) {
|
||||
Log.e(TAG, "failed to parse likesString \"" + likesString + "\" and dislikesString \"" +
|
||||
dislikesString + "\" as integers");
|
||||
} catch (Exception e) {
|
||||
// if it fails we know that the video does not offer dislikes.
|
||||
e.printStackTrace();
|
||||
videoInfo.like_count = 0;
|
||||
videoInfo.dislike_count = 0;
|
||||
}
|
||||
|
||||
// next video
|
||||
videoInfo.nextVideo = extractVideoPreviewInfo(doc.select("div[class=\"watch-sidebar-section\"]").first()
|
||||
.select("li").first());
|
||||
|
||||
// related videos
|
||||
Vector<VideoPreviewInfo> relatedVideos = new Vector<>();
|
||||
for (Element li : doc.select("ul[id=\"watch-related\"]").first().children()) {
|
||||
// first check if we have a playlist. If so leave them out
|
||||
if (li.select("a[class*=\"content-link\"]").first() != null) {
|
||||
relatedVideos.add(extractVideoPreviewInfo(li));
|
||||
}
|
||||
}
|
||||
//todo: replace conversion
|
||||
videoInfo.relatedVideos = relatedVideos;
|
||||
//videoInfo.relatedVideos = relatedVideos.toArray(new VideoPreviewInfo[relatedVideos.size()]);
|
||||
}
|
||||
return videoInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
private VideoInfo.AudioStream[] parseDashManifest(String dashManifest, String decryptoinCode) {
|
||||
if(!dashManifest.contains("/signature/")) {
|
||||
String encryptedSig = matchGroup1("/s/([a-fA-F0-9\\.]+)", dashManifest);
|
||||
String decryptedSig;
|
||||
|
||||
decryptedSig = decryptSignature(encryptedSig, decryptoinCode);
|
||||
dashManifest = dashManifest.replace("/s/" + encryptedSig, "/signature/" + decryptedSig);
|
||||
}
|
||||
String dashDoc = Downloader.download(dashManifest);
|
||||
Vector<VideoInfo.AudioStream> audioStreams = new Vector<>();
|
||||
try {
|
||||
XmlPullParser parser = Xml.newPullParser();
|
||||
parser.setInput(new StringReader(dashDoc));
|
||||
String tagName = "";
|
||||
String currentMimeType = "";
|
||||
int currentBandwidth = -1;
|
||||
int currentSamplingRate = -1;
|
||||
boolean currentTagIsBaseUrl = false;
|
||||
for(int eventType = parser.getEventType();
|
||||
eventType != XmlPullParser.END_DOCUMENT;
|
||||
eventType = parser.next() ) {
|
||||
switch(eventType) {
|
||||
case XmlPullParser.START_TAG:
|
||||
tagName = parser.getName();
|
||||
if(tagName.equals("AdaptationSet")) {
|
||||
currentMimeType = parser.getAttributeValue(XmlPullParser.NO_NAMESPACE, "mimeType");
|
||||
} else if(tagName.equals("Representation") && currentMimeType.contains("audio")) {
|
||||
currentBandwidth = Integer.parseInt(
|
||||
parser.getAttributeValue(XmlPullParser.NO_NAMESPACE, "bandwidth"));
|
||||
currentSamplingRate = Integer.parseInt(
|
||||
parser.getAttributeValue(XmlPullParser.NO_NAMESPACE, "audioSamplingRate"));
|
||||
} else if(tagName.equals("BaseURL")) {
|
||||
currentTagIsBaseUrl = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case XmlPullParser.TEXT:
|
||||
if(currentTagIsBaseUrl &&
|
||||
(currentMimeType.contains("audio"))) {
|
||||
int format = -1;
|
||||
if(currentMimeType.equals(MediaFormat.WEBMA.mimeType)) {
|
||||
format = MediaFormat.WEBMA.id;
|
||||
} else if(currentMimeType.equals(MediaFormat.M4A.mimeType)) {
|
||||
format = MediaFormat.M4A.id;
|
||||
}
|
||||
audioStreams.add(new VideoInfo.AudioStream(parser.getText(),
|
||||
format, currentBandwidth, currentSamplingRate));
|
||||
}
|
||||
//missing break here?
|
||||
case XmlPullParser.END_TAG:
|
||||
if(tagName.equals("AdaptationSet")) {
|
||||
currentMimeType = "";
|
||||
} else if(tagName.equals("BaseURL")) {
|
||||
currentTagIsBaseUrl = false;
|
||||
}//no break needed here
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return audioStreams.toArray(new VideoInfo.AudioStream[audioStreams.size()]);
|
||||
}
|
||||
/**Provides information about links to other videos on the video page, such as related videos.
|
||||
* This is encapsulated in a VideoPreviewInfo object,
|
||||
* which is a subset of the fields in a full VideoInfo.*/
|
||||
private VideoPreviewInfo extractVideoPreviewInfo(Element li) {
|
||||
VideoPreviewInfo info = new VideoPreviewInfo();
|
||||
info.webpage_url = li.select("a.content-link").first()
|
||||
.attr("abs:href");
|
||||
try {
|
||||
info.id = matchGroup1("v=([0-9a-zA-Z-]*)", info.webpage_url);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
//todo: check NullPointerException causing
|
||||
info.title = li.select("span.title").first().text();
|
||||
//this page causes the NullPointerException, after finding it by searching for "tjvg":
|
||||
//https://www.youtube.com/watch?v=Uqg0aEhLFAg
|
||||
String views = li.select("span.view-count").first().text();
|
||||
//Log.i(TAG, "title:"+info.title);
|
||||
//Log.i(TAG, "view count:"+views);
|
||||
try {
|
||||
info.view_count = Long.parseLong(li.select("span.view-count")
|
||||
.first().text().replaceAll("[^\\d]", ""));
|
||||
} catch (NullPointerException e) {//related videos sometimes have no view count
|
||||
info.view_count = 0;
|
||||
}
|
||||
info.uploader = li.select("span.g-hovercard").first().text();
|
||||
|
||||
info.duration = li.select("span.video-time").first().text();
|
||||
|
||||
Element img = li.select("img").first();
|
||||
info.thumbnail_url = img.attr("abs:src");
|
||||
// Sometimes youtube sends links to gif files which somehow seem to not exist
|
||||
// anymore. Items with such gif also offer a secondary image source. So we are going
|
||||
// to use that if we caught such an item.
|
||||
if(info.thumbnail_url.contains(".gif")) {
|
||||
info.thumbnail_url = img.attr("data-thumb");
|
||||
}
|
||||
if(info.thumbnail_url.startsWith("//")) {
|
||||
info.thumbnail_url = "https:" + info.thumbnail_url;
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
private String loadDecryptionCode(String playerUrl) {
|
||||
String playerCode = Downloader.download(playerUrl);
|
||||
String decryptionFuncName = "";
|
||||
String decryptionFunc = "";
|
||||
String helperObjectName;
|
||||
String helperObject = "";
|
||||
String callerFunc = "function " + DECRYPTION_FUNC_NAME + "(a){return %%(a);}";
|
||||
String decryptionCode;
|
||||
|
||||
try {
|
||||
decryptionFuncName = matchGroup1("\\.sig\\|\\|([a-zA-Z0-9$]+)\\(", playerCode);
|
||||
|
||||
String functionPattern = "(" + decryptionFuncName.replace("$", "\\$") +"=function\\([a-zA-Z0-9_]*\\)\\{.+?\\})";
|
||||
decryptionFunc = "var " + matchGroup1(functionPattern, playerCode) + ";";
|
||||
|
||||
helperObjectName = matchGroup1(";([A-Za-z0-9_\\$]{2})\\...\\(", decryptionFunc);
|
||||
|
||||
String helperPattern = "(var " + helperObjectName.replace("$", "\\$") + "=\\{.+?\\}\\};)";
|
||||
helperObject = matchGroup1(helperPattern, playerCode);
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
callerFunc = callerFunc.replace("%%", decryptionFuncName);
|
||||
decryptionCode = helperObject + decryptionFunc + callerFunc;
|
||||
|
||||
return decryptionCode;
|
||||
}
|
||||
|
||||
private String decryptSignature(String encryptedSig, String decryptionCode) {
|
||||
Context context = Context.enter();
|
||||
context.setOptimizationLevel(-1);
|
||||
Object result = null;
|
||||
try {
|
||||
ScriptableObject scope = context.initStandardObjects();
|
||||
context.evaluateString(scope, decryptionCode, "decryptionCode", 1, null);
|
||||
Function decryptionFunc = (Function) scope.get("decrypt", scope);
|
||||
result = decryptionFunc.call(context, scope, scope, new Object[]{encryptedSig});
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
Context.exit();
|
||||
return (result == null ? "" : result.toString());
|
||||
}
|
||||
|
||||
private String cleanUrl(String complexUrl) {
|
||||
return getVideoUrl(getVideoId(complexUrl));
|
||||
}
|
||||
|
||||
private String matchGroup1(String pattern, String input) {
|
||||
Pattern pat = Pattern.compile(pattern);
|
||||
Matcher mat = pat.matcher(input);
|
||||
boolean foundMatch = mat.find();
|
||||
if (foundMatch) {
|
||||
return mat.group(1);
|
||||
}
|
||||
else {
|
||||
Log.e(TAG, "failed to find pattern \""+pattern+"\" inside of \""+input+"\"");
|
||||
new Exception("failed to find pattern \""+pattern+"\"").printStackTrace();
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private int findErrorReason(Document doc) {
|
||||
errorMessage = doc.select("h1[id=\"unavailable-message\"]").first().text();
|
||||
if(errorMessage.contains("GEMA")) {
|
||||
return VideoInfo.ERROR_BLOCKED_BY_GEMA;
|
||||
}
|
||||
return VideoInfo.ERROR_NO_SPECIFIED_ERROR;
|
||||
}
|
||||
}
|
||||
@@ -1,498 +0,0 @@
|
||||
package org.schabi.newpipe.youtube;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.schabi.newpipe.Downloader;
|
||||
import org.schabi.newpipe.Extractor;
|
||||
import org.schabi.newpipe.VideoInfo;
|
||||
|
||||
import android.util.Log;
|
||||
import android.util.Xml;
|
||||
|
||||
import java.io.StringReader;
|
||||
import java.net.URI;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Vector;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.regex.Matcher;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.parser.Parser;
|
||||
|
||||
import org.mozilla.javascript.Context;
|
||||
import org.mozilla.javascript.Function;
|
||||
import org.mozilla.javascript.ScriptableObject;
|
||||
import org.schabi.newpipe.VideoInfoItem;
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 06.08.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* YoutubeExtractor.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 YoutubeExtractor implements Extractor {
|
||||
|
||||
|
||||
|
||||
private static final String TAG = YoutubeExtractor.class.toString();
|
||||
|
||||
// These lists only contain itag formats that are supported by the common Android Video player.
|
||||
// How ever if you are heading for a list showing all itag formats lock at
|
||||
// https://github.com/rg3/youtube-dl/issues/1687
|
||||
|
||||
public static int resolveFormat(int itag) {
|
||||
switch(itag) {
|
||||
// video
|
||||
case 17: return VideoInfo.I_3GPP;
|
||||
case 18: return VideoInfo.I_MPEG_4;
|
||||
case 22: return VideoInfo.I_MPEG_4;
|
||||
case 36: return VideoInfo.I_3GPP;
|
||||
case 37: return VideoInfo.I_MPEG_4;
|
||||
case 38: return VideoInfo.I_MPEG_4;
|
||||
case 43: return VideoInfo.I_WEBM;
|
||||
case 44: return VideoInfo.I_WEBM;
|
||||
case 45: return VideoInfo.I_WEBM;
|
||||
case 46: return VideoInfo.I_WEBM;
|
||||
default:
|
||||
//Log.i(TAG, "Itag " + Integer.toString(itag) + " not known or not supported.");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
public static String resolveResolutionString(int itag) {
|
||||
switch(itag) {
|
||||
case 17: return "144p";
|
||||
case 18: return "360p";
|
||||
case 22: return "720p";
|
||||
case 36: return "240p";
|
||||
case 37: return "1080p";
|
||||
case 38: return "1080p";
|
||||
case 43: return "360p";
|
||||
case 44: return "480p";
|
||||
case 45: return "720p";
|
||||
case 46: return "1080p";
|
||||
default:
|
||||
//Log.i(TAG, "Itag " + Integer.toString(itag) + " not known or not supported.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String decryptoinCode = "";
|
||||
private static final String DECRYPTION_FUNC_NAME="decrypt";
|
||||
|
||||
@Override
|
||||
public String getVideoId(String videoUrl) {
|
||||
try {
|
||||
URI uri = new URI(videoUrl);
|
||||
if(uri.getHost().contains("youtube")) {
|
||||
String query = uri.getQuery();
|
||||
String queryElements[] = query.split("&");
|
||||
Map<String, String> queryArguments = new HashMap<>();
|
||||
for (String e : queryElements) {
|
||||
String[] s = e.split("=");
|
||||
queryArguments.put(s[0], s[1]);
|
||||
}
|
||||
return queryArguments.get("v");
|
||||
} else if(uri.getHost().contains("youtu.be")) {
|
||||
// uri.getRawPath() does somehow not return the last character.
|
||||
// so we do a workaround instead.
|
||||
//return uri.getRawPath();
|
||||
String url[] = videoUrl.split("/");
|
||||
return url[url.length-1];
|
||||
} else {
|
||||
Log.e(TAG, "Error could not parse url: " + videoUrl);
|
||||
|
||||
}
|
||||
} catch(Exception e) {
|
||||
Log.e(TAG, "Error could not parse url: " + videoUrl);
|
||||
e.printStackTrace();
|
||||
return "";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVideoUrl(String videoId) {
|
||||
return "https://www.youtube.com/watch?v=" + videoId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VideoInfo getVideoInfo(String siteUrl) {
|
||||
String site = Downloader.download(siteUrl);
|
||||
VideoInfo videoInfo = new VideoInfo();
|
||||
|
||||
Document doc = Jsoup.parse(site, siteUrl);
|
||||
|
||||
try {
|
||||
Pattern p = Pattern.compile("v=([0-9a-zA-Z]*)");
|
||||
Matcher m = p.matcher(siteUrl);
|
||||
m.find();
|
||||
videoInfo.id = m.group(1);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
videoInfo.age_limit = 0;
|
||||
videoInfo.webpage_url = siteUrl;
|
||||
|
||||
//-------------------------------------
|
||||
// extracting form player args
|
||||
//-------------------------------------
|
||||
JSONObject playerArgs = null;
|
||||
JSONObject ytAssets = null;
|
||||
String dashManifest = "";
|
||||
{
|
||||
Pattern p = Pattern.compile("ytplayer.config\\s*=\\s*(\\{.*?\\});");
|
||||
Matcher m = p.matcher(site);
|
||||
m.find();
|
||||
|
||||
try {
|
||||
playerArgs = (new JSONObject(m.group(1)))
|
||||
.getJSONObject("args");
|
||||
ytAssets = (new JSONObject(m.group(1)))
|
||||
.getJSONObject("assets");
|
||||
}catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
// If we fail in this part the video is most likely not available.
|
||||
// Determining why is done later.
|
||||
videoInfo.videoAvailableStatus = VideoInfo.VIDEO_UNAVAILABLE;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
videoInfo.uploader = playerArgs.getString("author");
|
||||
videoInfo.title = playerArgs.getString("title");
|
||||
//first attempt gating a small image version
|
||||
//in the html extracting part we try to get a thumbnail with a higher resolution
|
||||
videoInfo.thumbnail_url = playerArgs.getString("thumbnail_url");
|
||||
videoInfo.duration = playerArgs.getInt("length_seconds");
|
||||
videoInfo.average_rating = playerArgs.getString("avg_rating");
|
||||
// View Count will be extracted from html
|
||||
dashManifest = playerArgs.getString("dashmpd");
|
||||
String playerUrl = ytAssets.getString("js");
|
||||
if(playerUrl.startsWith("//")) {
|
||||
playerUrl = "https:" + playerUrl;
|
||||
}
|
||||
if(decryptoinCode.isEmpty()) {
|
||||
decryptoinCode = loadDecryptioinCode(playerUrl);
|
||||
}
|
||||
|
||||
// extract audio
|
||||
videoInfo.audioStreams = parseDashManifest(dashManifest, decryptoinCode);
|
||||
|
||||
//------------------------------------
|
||||
// extract video stream url
|
||||
//------------------------------------
|
||||
String encoded_url_map = playerArgs.getString("url_encoded_fmt_stream_map");
|
||||
Vector<VideoInfo.VideoStream> videoStreams = new Vector<>();
|
||||
for(String url_data_str : encoded_url_map.split(",")) {
|
||||
Map<String, String> tags = new HashMap<>();
|
||||
for(String raw_tag : Parser.unescapeEntities(url_data_str, true).split("&")) {
|
||||
String[] split_tag = raw_tag.split("=");
|
||||
tags.put(split_tag[0], split_tag[1]);
|
||||
}
|
||||
|
||||
int itag = Integer.parseInt(tags.get("itag"));
|
||||
String streamUrl = terrible_unescape_workaround_fuck(tags.get("url"));
|
||||
|
||||
// if video has a signature: decrypt it and add it to the url
|
||||
if(tags.get("s") != null) {
|
||||
if(decryptoinCode.isEmpty()) {
|
||||
decryptoinCode = loadDecryptioinCode(playerUrl);
|
||||
}
|
||||
streamUrl = streamUrl + "&signature=" + decryptSignature(tags.get("s"), decryptoinCode);
|
||||
}
|
||||
|
||||
if(resolveFormat(itag) != -1) {
|
||||
videoStreams.add(new VideoInfo.VideoStream(
|
||||
streamUrl,
|
||||
resolveFormat(itag),
|
||||
resolveResolutionString(itag)));
|
||||
}
|
||||
}
|
||||
videoInfo.videoStreams = new VideoInfo.VideoStream[videoStreams.size()];
|
||||
for(int i = 0; i < videoStreams.size(); i++) {
|
||||
videoInfo.videoStreams[i] = videoStreams.get(i);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
//-------------------------------
|
||||
// extrating from html page
|
||||
//-------------------------------
|
||||
|
||||
|
||||
// Determine what went wrong when the Video is not available
|
||||
if(videoInfo.videoAvailableStatus == VideoInfo.VIDEO_UNAVAILABLE) {
|
||||
if(doc.select("h1[id=\"unavailable-message\"]").first().text().contains("GEMA")) {
|
||||
videoInfo.videoAvailableStatus = VideoInfo.VIDEO_UNAVAILABLE_GEMA;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get high resolution thumbnail if it fails use low res from the player instead
|
||||
try {
|
||||
videoInfo.thumbnail_url = doc.select("link[itemprop=\"thumbnailUrl\"]").first()
|
||||
.attr("abs:href");
|
||||
} catch(Exception e) {
|
||||
Log.i(TAG, "Could not find high res Thumbnail. Use low res instead");
|
||||
}
|
||||
|
||||
// upload date
|
||||
videoInfo.upload_date = doc.select("strong[class=\"watch-time-text\"").first()
|
||||
.text();
|
||||
// Try to only use date not the text around it
|
||||
try {
|
||||
Pattern p = Pattern.compile("([0-9.]*$)");
|
||||
Matcher m = p.matcher(videoInfo.upload_date);
|
||||
m.find();
|
||||
videoInfo.upload_date = m.group(1);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
// description
|
||||
videoInfo.description = doc.select("p[id=\"eow-description\"]").first()
|
||||
.html();
|
||||
|
||||
try {
|
||||
// likes
|
||||
videoInfo.like_count = doc.select("span[class=\"like-button-renderer \"]").first()
|
||||
.getAllElements().select("button")
|
||||
.select("span").get(0).text();
|
||||
|
||||
|
||||
// dislikes
|
||||
videoInfo.dislike_count = doc.select("span[class=\"like-button-renderer \"]").first()
|
||||
.getAllElements().select("button")
|
||||
.select("span").get(2).text();
|
||||
} catch(Exception e) {
|
||||
// if it fails we know that the video does not offer dislikes.
|
||||
videoInfo.like_count = "0";
|
||||
videoInfo.dislike_count = "0";
|
||||
}
|
||||
|
||||
// uploader thumbnail
|
||||
videoInfo.uploader_thumbnail_url = doc.select("a[class*=\"yt-user-photo\"]").first()
|
||||
.select("img").first()
|
||||
.attr("abs:data-thumb");
|
||||
|
||||
// view count
|
||||
videoInfo.view_count = doc.select("div[class=\"watch-view-count\"]").first().text();
|
||||
|
||||
/* todo finish this code
|
||||
|
||||
// next video
|
||||
videoInfo.nextVideo = extractVideoInfoItem(doc.select("div[class=\"watch-sidebar-section\"]").first()
|
||||
.select("li").first());
|
||||
|
||||
int i = 0;
|
||||
// related videos
|
||||
for(Element li : doc.select("ul[id=\"watch-related\"]").first().children()) {
|
||||
// first check if we have a playlist. If so leave them out
|
||||
if(li.select("a[class*=\"content-link\"]").first() != null) {
|
||||
//videoInfo.relatedVideos.add(extractVideoInfoItem(li));
|
||||
//i++;
|
||||
//Log.d(TAG, Integer.toString(i));
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
return videoInfo;
|
||||
}
|
||||
|
||||
private VideoInfo.AudioStream[] parseDashManifest(String dashManifest, String decryptoinCode) {
|
||||
if(!dashManifest.contains("/signature/")) {
|
||||
String encryptedSig = "";
|
||||
String decryptedSig = "";
|
||||
try {
|
||||
Pattern p = Pattern.compile("/s/([a-fA-F0-9\\.]+)");
|
||||
Matcher m = p.matcher(dashManifest);
|
||||
m.find();
|
||||
encryptedSig = m.group(1);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
decryptedSig = decryptSignature(encryptedSig, decryptoinCode);
|
||||
dashManifest = dashManifest.replace("/s/" + encryptedSig, "/signature/" + decryptedSig);
|
||||
}
|
||||
String dashDoc = Downloader.download(dashManifest);
|
||||
Vector<VideoInfo.AudioStream> audioStreams = new Vector<>();
|
||||
try {
|
||||
XmlPullParser parser = Xml.newPullParser();
|
||||
parser.setInput(new StringReader(dashDoc));
|
||||
int eventType = parser.getEventType();
|
||||
String tagName = "";
|
||||
String currentMimeType = "";
|
||||
int currentBandwidth = -1;
|
||||
int currentSamplingRate = -1;
|
||||
boolean currentTagIsBaseUrl = false;
|
||||
while(eventType != XmlPullParser.END_DOCUMENT) {
|
||||
switch(eventType) {
|
||||
case XmlPullParser.START_TAG:
|
||||
tagName = parser.getName();
|
||||
if(tagName.equals("AdaptationSet")) {
|
||||
currentMimeType = parser.getAttributeValue(XmlPullParser.NO_NAMESPACE, "mimeType");
|
||||
} else if(tagName.equals("Representation") && currentMimeType.contains("audio")) {
|
||||
currentBandwidth = Integer.parseInt(
|
||||
parser.getAttributeValue(XmlPullParser.NO_NAMESPACE, "bandwidth"));
|
||||
currentSamplingRate = Integer.parseInt(
|
||||
parser.getAttributeValue(XmlPullParser.NO_NAMESPACE, "audioSamplingRate"));
|
||||
} else if(tagName.equals("BaseURL")) {
|
||||
currentTagIsBaseUrl = true;
|
||||
}
|
||||
|
||||
break;
|
||||
case XmlPullParser.TEXT:
|
||||
if(currentTagIsBaseUrl &&
|
||||
(currentMimeType.contains("audio"))) {
|
||||
int format = -1;
|
||||
if(currentMimeType.equals(VideoInfo.M_WEBMA)) {
|
||||
format = VideoInfo.I_WEBMA;
|
||||
} else if(currentMimeType.equals(VideoInfo.M_M4A)) {
|
||||
format = VideoInfo.I_M4A;
|
||||
}
|
||||
audioStreams.add(new VideoInfo.AudioStream(parser.getText(),
|
||||
format, currentBandwidth, currentSamplingRate));
|
||||
}
|
||||
case XmlPullParser.END_TAG:
|
||||
if(tagName.equals("AdaptationSet")) {
|
||||
currentMimeType = "";
|
||||
} else if(tagName.equals("BaseURL")) {
|
||||
currentTagIsBaseUrl = false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
eventType = parser.next();
|
||||
}
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
VideoInfo.AudioStream[] output = new VideoInfo.AudioStream[audioStreams.size()];
|
||||
for(int i = 0; i < output.length; i++) {
|
||||
output[i] = audioStreams.get(i);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
private VideoInfoItem extractVideoInfoItem(Element li) {
|
||||
VideoInfoItem info = new VideoInfoItem();
|
||||
info.webpage_url = li.select("a[class*=\"content-link\"]").first()
|
||||
.attr("abs:href");
|
||||
try {
|
||||
Pattern p = Pattern.compile("v=([0-9a-zA-Z-]*)");
|
||||
Matcher m = p.matcher(info.webpage_url);
|
||||
m.find();
|
||||
info.id=m.group(1);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
info.title = li.select("span[class=\"title\"]").first()
|
||||
.text();
|
||||
|
||||
info.uploader = li.select("span[class=\"g-hovercard\"]").first().text();
|
||||
|
||||
info.duration = li.select("span[class=\"video-time\"]").first().text();
|
||||
|
||||
Element img = li.select("img").first();
|
||||
info.thumbnail_url = img.attr("abs:src");
|
||||
// Sometimes youtube sends links to gif files witch somehow seam to not exist
|
||||
// anymore. Items with such gif also offer a secondary image source. So we are going
|
||||
// to use that if we caught such an item.
|
||||
if(info.thumbnail_url.contains(".gif")) {
|
||||
info.thumbnail_url = img.attr("data-thumb");
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
private String terrible_unescape_workaround_fuck(String shit) {
|
||||
String[] splitAtEscape = shit.split("%");
|
||||
String retval = "";
|
||||
retval += splitAtEscape[0];
|
||||
for(int i = 1; i < splitAtEscape.length; i++) {
|
||||
String escNum = splitAtEscape[i].substring(0, 2);
|
||||
char c = (char) Integer.parseInt(escNum,16);
|
||||
retval += c;
|
||||
retval += splitAtEscape[i].substring(2);
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
|
||||
private String loadDecryptioinCode(String playerUrl) {
|
||||
String playerCode = Downloader.download(playerUrl);
|
||||
String decryptionFuncName = "";
|
||||
String decryptionFunc = "";
|
||||
String helperObjectName;
|
||||
String helperObject = "";
|
||||
String callerFunc = "function " + DECRYPTION_FUNC_NAME + "(a){return %%(a);}";
|
||||
String decryptionCode;
|
||||
|
||||
try {
|
||||
Pattern p = Pattern.compile("\\.sig\\|\\|([a-zA-Z0-9$]+)\\(");
|
||||
Matcher m = p.matcher(playerCode);
|
||||
m.find();
|
||||
decryptionFuncName = m.group(1);
|
||||
|
||||
String functionPattern = "(function " + decryptionFuncName.replace("$", "\\$") + "\\([a-zA-Z0-9_]*\\)\\{.+?\\})";
|
||||
p = Pattern.compile(functionPattern);
|
||||
m = p.matcher(playerCode);
|
||||
m.find();
|
||||
decryptionFunc = m.group(1);
|
||||
|
||||
p = Pattern.compile(";([A-Za-z0-9_\\$]{2})\\...\\(");
|
||||
m = p.matcher(decryptionFunc);
|
||||
m.find();
|
||||
helperObjectName = m.group(1);
|
||||
|
||||
String helperPattern = "(var " + helperObjectName.replace("$", "\\$") + "=\\{.+?\\}\\};)function";
|
||||
p = Pattern.compile(helperPattern);
|
||||
m = p.matcher(playerCode);
|
||||
m.find();
|
||||
helperObject = m.group(1);
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
callerFunc = callerFunc.replace("%%", decryptionFuncName);
|
||||
decryptionCode = helperObject + decryptionFunc + callerFunc;
|
||||
|
||||
return decryptionCode;
|
||||
}
|
||||
|
||||
private String decryptSignature(String encryptedSig, String decryptoinCode) {
|
||||
Context context = Context.enter();
|
||||
context.setOptimizationLevel(-1);
|
||||
Object result = null;
|
||||
try {
|
||||
ScriptableObject scope = context.initStandardObjects();
|
||||
context.evaluateString(scope, decryptoinCode, "decryptionCode", 1, null);
|
||||
Function decryptionFunc = (Function) scope.get("decrypt", scope);
|
||||
result = decryptionFunc.call(context, scope, scope, new Object[]{encryptedSig});
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
Context.exit();
|
||||
return result.toString();
|
||||
}
|
||||
}
|
||||
BIN
app/src/main/res/drawable-hdpi/ic_close_white_24dp.png
Normal file
|
After Width: | Height: | Size: 221 B |
BIN
app/src/main/res/drawable-hdpi/ic_pause_white_24dp.png
Normal file
|
After Width: | Height: | Size: 105 B |
BIN
app/src/main/res/drawable-hdpi/ic_play_arrow_white_48dp.png
Normal file
|
After Width: | Height: | Size: 283 B |
|
After Width: | Height: | Size: 358 B |
BIN
app/src/main/res/drawable-mdpi/ic_close_white_24dp.png
Normal file
|
After Width: | Height: | Size: 175 B |
BIN
app/src/main/res/drawable-mdpi/ic_pause_white_24dp.png
Normal file
|
After Width: | Height: | Size: 83 B |
BIN
app/src/main/res/drawable-mdpi/ic_play_arrow_white_48dp.png
Normal file
|
After Width: | Height: | Size: 220 B |
|
After Width: | Height: | Size: 255 B |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
BIN
app/src/main/res/drawable-nodpi/dummy_thumbnail_dark.png
Normal file
|
After Width: | Height: | Size: 188 B |
BIN
app/src/main/res/drawable-nodpi/gruese_die_gema.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 869 B After Width: | Height: | Size: 869 B |
BIN
app/src/main/res/drawable-nodpi/ic_file_download_black.png
Normal file
|
After Width: | Height: | Size: 209 B |
|
Before Width: | Height: | Size: 786 B After Width: | Height: | Size: 786 B |
|
Before Width: | Height: | Size: 320 B After Width: | Height: | Size: 320 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 888 B After Width: | Height: | Size: 888 B |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
BIN
app/src/main/res/drawable-nodpi/thumbs_down.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
app/src/main/res/drawable-nodpi/thumbs_up.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png
Normal file
|
After Width: | Height: | Size: 257 B |
BIN
app/src/main/res/drawable-xhdpi/ic_pause_white_24dp.png
Normal file
|
After Width: | Height: | Size: 90 B |
BIN
app/src/main/res/drawable-xhdpi/ic_play_arrow_white_48dp.png
Normal file
|
After Width: | Height: | Size: 343 B |
|
After Width: | Height: | Size: 464 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png
Normal file
|
After Width: | Height: | Size: 347 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_pause_white_24dp.png
Normal file
|
After Width: | Height: | Size: 92 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_play_arrow_white_48dp.png
Normal file
|
After Width: | Height: | Size: 461 B |
|
After Width: | Height: | Size: 666 B |
BIN
app/src/main/res/drawable-xxxhdpi/ic_close_white_24dp.png
Normal file
|
After Width: | Height: | Size: 436 B |
BIN
app/src/main/res/drawable-xxxhdpi/ic_pause_white_24dp.png
Normal file
|
After Width: | Height: | Size: 94 B |
BIN
app/src/main/res/drawable-xxxhdpi/ic_play_arrow_white_48dp.png
Normal file
|
After Width: | Height: | Size: 605 B |
|
After Width: | Height: | Size: 883 B |
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
|
||||
<solid android:color="#000000" />
|
||||
<stroke android:width="1dip" android:color="#888"/>
|
||||
</shape>
|
||||
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 236 B |
|
Before Width: | Height: | Size: 938 B |
|
Before Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 870 B |
|
Before Width: | Height: | Size: 783 B |
@@ -1,159 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
style="?android:attr/textAppearanceLarge"
|
||||
tools:context=".VideoItemDetailFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/videoitem_detail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:textIsSelectable="true">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="16dp">
|
||||
|
||||
<ProgressBar android:id="@+id/detailProgressBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="true"/>
|
||||
|
||||
<ImageView android:id="@+id/detailThumbnailView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:adjustViewBounds="true"
|
||||
android:src="@drawable/dummi_thumbnail"
|
||||
android:visibility="invisible"/>
|
||||
|
||||
<TextView android:id="@+id/detailVideoTitleView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/detailThumbnailView"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:paddingLeft="6dp"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:text="Bla blabla !!!"
|
||||
android:visibility="invisible"/>
|
||||
|
||||
<ImageView android:id="@+id/detailUploaderThumbnailView"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="100dp"
|
||||
android:paddingTop="25dp"
|
||||
android:paddingLeft="5dp"
|
||||
android:layout_below="@id/detailVideoTitleView"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:visibility="invisible"
|
||||
android:src="@drawable/budy" />
|
||||
|
||||
<TextView android:id="@+id/detailUploaderView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/detailUploaderThumbnailView"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:paddingLeft="6dp"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:visibility="invisible"
|
||||
android:text="Herr von Gurken" />
|
||||
|
||||
<View android:id="@+id/textSeperationLine"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="@android:color/darker_gray"
|
||||
android:layout_below="@id/detailUploaderView"
|
||||
android:paddingTop="20dp"
|
||||
android:visibility="invisible"
|
||||
android:layout_alignParentLeft="true" />
|
||||
|
||||
<TextView android:id="@+id/detailViewCountView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/detailVideoTitleView"
|
||||
android:layout_alignParentRight="true"
|
||||
android:paddingTop="70dp"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:visibility="invisible"
|
||||
android:text="1.000.115 views" />
|
||||
|
||||
<TextView android:id="@+id/detailThumbsDownCountView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/detailViewCountView"
|
||||
android:layout_alignParentRight="true"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:visibility="invisible"
|
||||
android:text="5.000" />
|
||||
|
||||
<ImageView android:id="@+id/detailThumbsDownImgView"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_below="@id/detailViewCountView"
|
||||
android:layout_toLeftOf="@id/detailThumbsDownCountView"
|
||||
android:visibility="invisible"
|
||||
android:src="@drawable/thumbs_down" />
|
||||
|
||||
<TextView android:id="@+id/detailThumbsUpCountView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/detailViewCountView"
|
||||
android:layout_toLeftOf="@id/detailThumbsDownImgView"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:visibility="invisible"
|
||||
android:text="111.111" />
|
||||
|
||||
<ImageView android:id="@+id/detailThumbsUpImgView"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_below="@id/detailViewCountView"
|
||||
android:layout_toLeftOf="@id/detailThumbsUpCountView"
|
||||
android:visibility="invisible"
|
||||
android:src="@drawable/thumbs_up" />
|
||||
|
||||
<TextView android:id="@+id/detailUploadDateView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/detailUploaderView"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:paddingLeft="6dp"
|
||||
android:paddingTop="20dp"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:visibility="invisible"
|
||||
android:text="Uploaded at: 45.64.1285" />
|
||||
|
||||
<TextView android:id="@+id/detailDescriptionView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/detailUploadDateView"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:paddingLeft="6dp"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:visibility="invisible"
|
||||
android:text="Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmodtempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. "
|
||||
/>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="100dp"
|
||||
android:layout_below="@id/detailDescriptionView"/>
|
||||
|
||||
</RelativeLayout>
|
||||
</ScrollView>
|
||||
|
||||
<android.support.design.widget.FloatingActionButton
|
||||
android:id="@+id/playVideoButton"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:backgroundTint="@color/primaryColorYoutube"
|
||||
android:src="@drawable/ic_play_arrow_black"
|
||||
android:layout_margin="15pt"/>
|
||||
</RelativeLayout>
|
||||
@@ -25,7 +25,8 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:iconifiedByDefault="false"
|
||||
android:focusable="false"/>
|
||||
android:focusable="false"
|
||||
tools:ignore="InconsistentLayout" />
|
||||
|
||||
<fragment android:id="@+id/videoitem_list"
|
||||
android:name="org.schabi.newpipe.VideoItemListFragment"
|
||||
@@ -42,15 +43,10 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="4">
|
||||
|
||||
<ImageView android:id="@+id/list_view_watermark"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="10dp"
|
||||
android:src="@drawable/new_pipe_watermark"/>
|
||||
|
||||
<FrameLayout android:id="@+id/videoitem_detail_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
android:layout_height="match_parent"
|
||||
tools:ignore="InconsistentLayout" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
219
app/src/main/res/layout-v18/fragment_videoitem_detail.xml
Normal file
@@ -0,0 +1,219 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
tools:context=".VideoItemDetailFragment"
|
||||
android:textIsSelectable="true"
|
||||
style="?android:attr/textAppearanceLarge"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/videoitem_detail">
|
||||
|
||||
<ImageView android:id="@+id/detailThumbnailView"
|
||||
android:contentDescription="@string/detail_thumbnail_view_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:scaleType="centerCrop"
|
||||
android:adjustViewBounds="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:background="@android:color/black"
|
||||
android:src="@drawable/dummy_thumbnail_dark"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/detailMainContent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="visible">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/detailVideoThumbnailWindowLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/video_item_detail_thumbnail_image_height"
|
||||
android:background="?attr/selectableItemBackground">
|
||||
|
||||
<ProgressBar android:id="@+id/detailProgressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:indeterminate="true"/>
|
||||
|
||||
<android.support.design.widget.FloatingActionButton
|
||||
android:id="@+id/playVideoButton"
|
||||
android:visibility="invisible"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
app:backgroundTint="@color/light_youtube_primary_color"
|
||||
android:src="@drawable/ic_play_arrow_black"
|
||||
android:layout_margin="@dimen/video_item_detail_play_fab_margin"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/detailVideoThumbnailWindowBackgroundButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/selectableItemBackground"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout android:id="@+id/detailTextContentLayout"
|
||||
android:visibility="invisible"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="@dimen/video_item_detail_info_text_padding"
|
||||
android:layout_below="@id/detailVideoThumbnailWindowLayout"
|
||||
android:background="@color/light_background_color">
|
||||
|
||||
<TextView android:id="@+id/detailVideoTitleView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:textStyle="bold"
|
||||
android:textSize="@dimen/video_item_detail_title_text_size"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<ImageView android:id="@+id/detailUploaderThumbnailView"
|
||||
android:contentDescription="@string/detail_uploader_thumbnail_view_description"
|
||||
android:layout_width="@dimen/video_item_detail_uploader_image_size"
|
||||
android:layout_height="@dimen/video_item_detail_uploader_image_size"
|
||||
android:layout_below="@id/detailVideoTitleView"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:src="@drawable/buddy"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<TextView android:id="@+id/detailUploaderView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/detailUploaderThumbnailView"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:textStyle="bold"
|
||||
android:textSize="@dimen/video_item_detail_uploader_text_size"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<TextView android:id="@+id/detailViewCountView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/detailVideoTitleView"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:textSize="@dimen/video_item_detail_views_text_size"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<TextView android:id="@+id/detailThumbsDownCountView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/detailViewCountView"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:textSize="@dimen/video_item_detail_likes_text_size"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<ImageView android:id="@+id/detailThumbsDownImgView"
|
||||
android:contentDescription="@string/detail_dislikes_img_view_description"
|
||||
android:layout_width="@dimen/video_item_detail_like_image_width"
|
||||
android:layout_height="@dimen/video_item_detail_like_image_height"
|
||||
android:layout_below="@id/detailViewCountView"
|
||||
android:layout_toLeftOf="@id/detailThumbsDownCountView"
|
||||
android:layout_toStartOf="@id/detailThumbsDownCountView"
|
||||
android:layout_marginLeft="@dimen/video_item_detail_like_margin"
|
||||
android:src="@drawable/thumbs_down"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<TextView android:id="@+id/detailThumbsUpCountView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/detailViewCountView"
|
||||
android:layout_toLeftOf="@id/detailThumbsDownImgView"
|
||||
android:layout_toStartOf="@id/detailThumbsDownImgView"
|
||||
android:textSize="@dimen/video_item_detail_likes_text_size"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<ImageView android:id="@+id/detailThumbsUpImgView"
|
||||
android:contentDescription="@string/detail_likes_img_view_description"
|
||||
android:layout_width="@dimen/video_item_detail_like_image_width"
|
||||
android:layout_height="@dimen/video_item_detail_like_image_height"
|
||||
android:layout_below="@id/detailViewCountView"
|
||||
android:layout_toLeftOf="@id/detailThumbsUpCountView"
|
||||
android:layout_toStartOf="@id/detailThumbsUpCountView"
|
||||
android:src="@drawable/thumbs_up"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<TextView android:id="@+id/detailUploadDateView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/detailUploaderView"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:textSize="@dimen/video_item_detail_upload_date_text_size"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<TextView android:id="@+id/detailDescriptionView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/detailUploadDateView"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:textSize="@dimen/video_item_detail_description_text_size"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
|
||||
<RelativeLayout android:id="@+id/detailNextVideoRootLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="@dimen/video_item_detail_info_text_padding"
|
||||
android:layout_below="@id/detailDescriptionView" >
|
||||
|
||||
<TextView android:id="@+id/detailNextVideoTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:textSize="@dimen/video_item_detail_next_text_size"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:text="@string/next_video_title"
|
||||
android:textAllCaps="true" />
|
||||
|
||||
<RelativeLayout android:id="@+id/detailNextVidButtonAndContentLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/detailNextVideoTitle">
|
||||
<FrameLayout
|
||||
android:id="@+id/detailNextVideoFrame"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
<Button
|
||||
android:id="@+id/detailNextVideoButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_alignTop="@id/detailNextVideoFrame"
|
||||
android:layout_alignBottom="@id/detailNextVideoFrame"
|
||||
android:background="?attr/selectableItemBackground"/>
|
||||
</RelativeLayout>
|
||||
<Button android:id="@+id/detailShowSimilarButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/detailNextVidButtonAndContentLayout"
|
||||
android:textSize="@dimen/video_item_detail_similar_text_size"
|
||||
android:text="@string/similar_videos_btn_text"/>
|
||||
</RelativeLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
</RelativeLayout>
|
||||
</ScrollView>
|
||||
</RelativeLayout>
|
||||
221
app/src/main/res/layout-v21/fragment_videoitem_detail.xml
Normal file
@@ -0,0 +1,221 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
tools:context=".VideoItemDetailFragment"
|
||||
android:textIsSelectable="true"
|
||||
style="?android:attr/textAppearanceLarge"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/videoitem_detail">
|
||||
|
||||
<ImageView android:id="@+id/detailThumbnailView"
|
||||
android:contentDescription="@string/detail_thumbnail_view_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:scaleType="centerCrop"
|
||||
android:adjustViewBounds="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:background="@android:color/black"
|
||||
android:src="@drawable/dummy_thumbnail_dark"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/detailMainContent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="visible">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/detailVideoThumbnailWindowLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/video_item_detail_thumbnail_image_height"
|
||||
android:background="?attr/selectableItemBackground">
|
||||
|
||||
<ProgressBar android:id="@+id/detailProgressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:indeterminate="true"
|
||||
android:indeterminateTint="@color/light_youtube_primary_color"
|
||||
android:indeterminateTintMode="src_in"/>
|
||||
|
||||
<android.support.design.widget.FloatingActionButton
|
||||
android:id="@+id/playVideoButton"
|
||||
android:visibility="invisible"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
app:backgroundTint="@color/light_youtube_primary_color"
|
||||
android:src="@drawable/ic_play_arrow_black"
|
||||
android:layout_margin="@dimen/video_item_detail_play_fab_margin"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/detailVideoThumbnailWindowBackgroundButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/selectableItemBackground"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout android:id="@+id/detailTextContentLayout"
|
||||
android:visibility="invisible"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="@dimen/video_item_detail_info_text_padding"
|
||||
android:layout_below="@id/detailVideoThumbnailWindowLayout"
|
||||
android:background="@color/light_background_color">
|
||||
|
||||
<TextView android:id="@+id/detailVideoTitleView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:textStyle="bold"
|
||||
android:textSize="@dimen/video_item_detail_title_text_size"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<ImageView android:id="@+id/detailUploaderThumbnailView"
|
||||
android:contentDescription="@string/detail_uploader_thumbnail_view_description"
|
||||
android:layout_width="@dimen/video_item_detail_uploader_image_size"
|
||||
android:layout_height="@dimen/video_item_detail_uploader_image_size"
|
||||
android:layout_below="@id/detailVideoTitleView"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:src="@drawable/buddy"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<TextView android:id="@+id/detailUploaderView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/detailUploaderThumbnailView"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:textStyle="bold"
|
||||
android:textSize="@dimen/video_item_detail_uploader_text_size"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<TextView android:id="@+id/detailViewCountView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/detailVideoTitleView"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:textSize="@dimen/video_item_detail_views_text_size"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<TextView android:id="@+id/detailThumbsDownCountView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/detailViewCountView"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:textSize="@dimen/video_item_detail_likes_text_size"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<ImageView android:id="@+id/detailThumbsDownImgView"
|
||||
android:contentDescription="@string/detail_dislikes_img_view_description"
|
||||
android:layout_width="@dimen/video_item_detail_like_image_width"
|
||||
android:layout_height="@dimen/video_item_detail_like_image_height"
|
||||
android:layout_below="@id/detailViewCountView"
|
||||
android:layout_toLeftOf="@id/detailThumbsDownCountView"
|
||||
android:layout_toStartOf="@id/detailThumbsDownCountView"
|
||||
android:layout_marginLeft="@dimen/video_item_detail_like_margin"
|
||||
android:src="@drawable/thumbs_down"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<TextView android:id="@+id/detailThumbsUpCountView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/detailViewCountView"
|
||||
android:layout_toLeftOf="@id/detailThumbsDownImgView"
|
||||
android:layout_toStartOf="@id/detailThumbsDownImgView"
|
||||
android:textSize="@dimen/video_item_detail_likes_text_size"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<ImageView android:id="@+id/detailThumbsUpImgView"
|
||||
android:contentDescription="@string/detail_likes_img_view_description"
|
||||
android:layout_width="@dimen/video_item_detail_like_image_width"
|
||||
android:layout_height="@dimen/video_item_detail_like_image_height"
|
||||
android:layout_below="@id/detailViewCountView"
|
||||
android:layout_toLeftOf="@id/detailThumbsUpCountView"
|
||||
android:layout_toStartOf="@id/detailThumbsUpCountView"
|
||||
android:src="@drawable/thumbs_up"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<TextView android:id="@+id/detailUploadDateView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/detailUploaderView"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:textSize="@dimen/video_item_detail_upload_date_text_size"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<TextView android:id="@+id/detailDescriptionView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/detailUploadDateView"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:textSize="@dimen/video_item_detail_description_text_size"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
|
||||
<RelativeLayout android:id="@+id/detailNextVideoRootLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="@dimen/video_item_detail_info_text_padding"
|
||||
android:layout_below="@id/detailDescriptionView" >
|
||||
|
||||
<TextView android:id="@+id/detailNextVideoTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:textSize="@dimen/video_item_detail_next_text_size"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:text="@string/next_video_title"
|
||||
android:textAllCaps="true" />
|
||||
|
||||
<RelativeLayout android:id="@+id/detailNextVidButtonAndContentLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/detailNextVideoTitle">
|
||||
<FrameLayout
|
||||
android:id="@+id/detailNextVideoFrame"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
<Button
|
||||
android:id="@+id/detailNextVideoButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_alignTop="@id/detailNextVideoFrame"
|
||||
android:layout_alignBottom="@id/detailNextVideoFrame"
|
||||
android:background="?attr/selectableItemBackground"/>
|
||||
</RelativeLayout>
|
||||
<Button android:id="@+id/detailShowSimilarButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/detailNextVidButtonAndContentLayout"
|
||||
android:textSize="@dimen/video_item_detail_similar_text_size"
|
||||
android:text="@string/similar_videos_btn_text"/>
|
||||
</RelativeLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
</RelativeLayout>
|
||||
</ScrollView>
|
||||
</RelativeLayout>
|
||||
@@ -1,8 +1,7 @@
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/black"
|
||||
tools:context="org.schabi.newpipe.PlayVideoActivity"
|
||||
android:gravity="center">
|
||||
|
||||
@@ -25,4 +24,4 @@
|
||||
android:layout_gravity="center"
|
||||
android:focusable="false"/>
|
||||
|
||||
</FrameLayout>
|
||||
</merge>
|
||||
|
||||
@@ -3,12 +3,6 @@
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView android:id="@+id/list_view_watermark"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="10dp"
|
||||
android:src="@drawable/new_pipe_watermark"/>
|
||||
|
||||
<fragment
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/videoitem_list"
|
||||
@@ -16,6 +10,6 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".VideoItemListActivity"
|
||||
tools:layout="@android:layout/list_content"/>
|
||||
tools:layout="@android:layout/list_content" />
|
||||
|
||||
</LinearLayout>
|
||||