Compare commits
216 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f181ce7c9 | ||
|
|
3a13d4a1de | ||
|
|
719de00e0f | ||
|
|
ddffe99f53 | ||
|
|
9ade596f06 | ||
|
|
46f413b7f2 | ||
|
|
5fe2e7b8ad | ||
|
|
3008cbb5f4 | ||
|
|
f7983960e5 | ||
|
|
bb8007bb7c | ||
|
|
01751ba97a | ||
|
|
f41475d11c | ||
|
|
d8914d9b6d | ||
|
|
94cc2ad365 | ||
|
|
e07464b81c | ||
|
|
9d231b55b5 | ||
|
|
77d8dac3c1 | ||
|
|
e160015283 | ||
|
|
aeb0cac3ee | ||
|
|
a539f94837 | ||
|
|
df70751071 | ||
|
|
e55f1dff78 | ||
|
|
47646e1c62 | ||
|
|
80e673f20c | ||
|
|
9ca8c5480c | ||
|
|
4d0d3c7ead | ||
|
|
58137aadc9 | ||
|
|
82a59ae479 | ||
|
|
affd23b14e | ||
|
|
9c7f249756 | ||
|
|
e2a0502171 | ||
|
|
3832a4b355 | ||
|
|
84f059415c | ||
|
|
bb292e3199 | ||
|
|
70541bf561 | ||
|
|
48dd8e88e3 | ||
|
|
52c2e0899d | ||
|
|
9955d5b62f | ||
|
|
8e26247fa1 | ||
|
|
72924e2692 | ||
|
|
8c2c8d630f | ||
|
|
5ccf0baa6b | ||
|
|
8fff0ccdf2 | ||
|
|
35264bfb97 | ||
|
|
4c9b0dc8e9 | ||
|
|
4deb7caa83 | ||
|
|
45f1a512b4 | ||
|
|
ab95fdc087 | ||
|
|
df608b9ded | ||
|
|
6a4c81d160 | ||
|
|
040d658540 | ||
|
|
cbc9913e9c | ||
|
|
f007cbd5ee | ||
|
|
7351591df7 | ||
|
|
5222d4cbba | ||
|
|
ef9966815e | ||
| ad73440ce9 | |||
|
|
c81cfdc455 | ||
|
|
134c3804db | ||
|
|
e355df5eda | ||
|
|
a68e0a95f4 | ||
|
|
6fd6facf72 | ||
|
|
00102d4048 | ||
|
|
adbeff11d4 | ||
|
|
a8fe329678 | ||
|
|
a4b61bf730 | ||
|
|
ee592def0c | ||
|
|
c14e117239 | ||
|
|
b6e94dde0c | ||
|
|
63a4a44d7e | ||
|
|
eb8ab5d527 | ||
|
|
b778d40d65 | ||
|
|
2f570672dc | ||
| 9290ed490f | |||
|
|
1bda812c75 | ||
|
|
ab82406c98 | ||
|
|
be763349df | ||
|
|
91764ad601 | ||
|
|
1c4031aa38 | ||
|
|
c3bc648dd4 | ||
|
|
c97d794272 | ||
|
|
8e7d2e91e9 | ||
|
|
cf0fbbbd3d | ||
|
|
7d3ede7946 | ||
|
|
1aa308eb5d | ||
| 4bfd30e34a | |||
|
|
0f46c90688 | ||
|
|
aa2173fc4a | ||
|
|
932cb1d19c | ||
|
|
18b038d8e4 | ||
|
|
2ac71c75c0 | ||
|
|
4067ba5232 | ||
|
|
ab47dd5a5b | ||
|
|
3130910307 | ||
|
|
22113439a4 | ||
|
|
2d25a9ad7a | ||
|
|
8f0a2cc2f0 | ||
|
|
4ca39258c9 | ||
|
|
d0c0238b8b | ||
|
|
f9f08e5169 | ||
|
|
cfc51b2401 | ||
|
|
54c2704cb5 | ||
|
|
9b51c946fe | ||
|
|
06f38cbcb4 | ||
|
|
f368d6b257 | ||
|
|
f4301da14d | ||
|
|
f34c09f165 | ||
|
|
6d1db56512 | ||
|
|
b70c07d004 | ||
|
|
f9f48a5eb6 | ||
|
|
14e4e73444 | ||
|
|
f2ce4d2daf | ||
|
|
468ebdda87 | ||
|
|
a1f0fb3b14 | ||
|
|
3c9f4de234 | ||
|
|
8ae411619f | ||
|
|
61e5c9121a | ||
|
|
8a3cf0d5dc | ||
|
|
ecb5df65ac | ||
| 5f1e98a0d3 | |||
|
|
3b9a477499 | ||
|
|
e5bf98a741 | ||
|
|
06fafc247e | ||
|
|
e8bb17b631 | ||
|
|
363cd07883 | ||
|
|
2b4a9286c4 | ||
|
|
34c985026f | ||
|
|
4a0aa42914 | ||
|
|
746c2a15bf | ||
|
|
9318bb5306 | ||
|
|
2a10ceb74f | ||
|
|
83f4db59e2 | ||
|
|
9f66f759ad | ||
|
|
6f015349e8 | ||
|
|
a37802c2b9 | ||
|
|
4fa3baf5e1 | ||
|
|
ef8c2c81d5 | ||
|
|
8c80d8c457 | ||
|
|
1596872c54 | ||
|
|
da8873fa78 | ||
|
|
ecd8439b3f | ||
|
|
5d178532ac | ||
|
|
08e40a013d | ||
|
|
c136f7363c | ||
|
|
a60f10d739 | ||
|
|
ae46afcb42 | ||
|
|
bffb9f6800 | ||
|
|
79aa9ad04b | ||
| ff5714f04a | |||
|
|
ce499a9766 | ||
|
|
d3bb8b7651 | ||
|
|
6d9c23c4cb | ||
|
|
b95a9332a9 | ||
|
|
609715eb5c | ||
|
|
a1266c919c | ||
|
|
a1925a0302 | ||
|
|
a7a4c03372 | ||
|
|
37201600e0 | ||
|
|
a94f40ed62 | ||
|
|
0b08cf8c76 | ||
|
|
33e29be7db | ||
|
|
bd1c7851c7 | ||
|
|
82faea5965 | ||
|
|
0bbbfd3217 | ||
|
|
fb578ecda8 | ||
|
|
e804647a65 | ||
|
|
c3c3a94593 | ||
|
|
9d55569f80 | ||
|
|
5dd8271c15 | ||
|
|
7a4a54c3ea | ||
|
|
7c9078a625 | ||
|
|
71ae342f52 | ||
|
|
b43c56085d | ||
|
|
83d2ab95e0 | ||
|
|
20a8d7372c | ||
|
|
c36ba88db7 | ||
|
|
4aa23023ee | ||
|
|
8735cf931a | ||
|
|
2473069326 | ||
|
|
03bab57a97 | ||
|
|
e3baf69533 | ||
|
|
461f747af1 | ||
|
|
028872a7d8 | ||
|
|
a1483b6c55 | ||
|
|
e406ba094c | ||
|
|
c100d15ba8 | ||
|
|
b4ea592638 | ||
|
|
16b757d9a3 | ||
|
|
2aa801a392 | ||
|
|
b838344526 | ||
|
|
233a3df222 | ||
|
|
da6661b1ea | ||
|
|
c6e120fc51 | ||
|
|
c416a1254d | ||
|
|
2aa4f6ddda | ||
|
|
129597023d | ||
|
|
02aed86b7e | ||
|
|
e44f4b5823 | ||
|
|
2f0c0f0fc2 | ||
|
|
e5ce3f3007 | ||
|
|
095a2be748 | ||
|
|
c75fe88757 | ||
|
|
a8ff4b0744 | ||
|
|
c02c511e31 | ||
|
|
b9550fb528 | ||
|
|
a37d8f083a | ||
|
|
abff1f537b | ||
|
|
483fde5e42 | ||
|
|
761b7ea57b | ||
|
|
af92631a0c | ||
|
|
ffe832d061 | ||
|
|
a08cbfcef1 | ||
|
|
d2d6ac1bf8 | ||
|
|
615ffca64b | ||
|
|
2fcf6197c5 | ||
|
|
22d31ae14f |
24
.travis.yml
@@ -5,35 +5,15 @@ android:
|
||||
components:
|
||||
# The BuildTools version used by NewPipe
|
||||
- tools
|
||||
- build-tools-23.0.3
|
||||
- build-tools-25.0.2
|
||||
|
||||
# The SDK version used to compile NewPipe
|
||||
- android-25
|
||||
|
||||
# Additional components
|
||||
- extra-android-support
|
||||
- extra-android-m2repository
|
||||
- extra-google-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)
|
||||
- GRADLE_OPTS=-Xmx512m # give gradle more memory since it seem to fail otherwise
|
||||
matrix:
|
||||
- ANDROID_TARGET=android-21 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 &
|
||||
|
||||
script: ./gradlew --info build connectedCheck
|
||||
script: ./gradlew -Dorg.gradle.jvmargs=-Xmx1536m assembleDebug lintDebug testDebugUnitTest
|
||||
|
||||
licenses:
|
||||
- '.+'
|
||||
|
||||
BIN
CCC_NewPipe_presentation.pdf
Normal file
26
README.md
@@ -19,11 +19,16 @@ Project status:
|
||||
|
||||
## 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_1.png" width=160>](screenshots/screenshot_1.png)
|
||||
[<img src="screenshots/screenshot_2.png" width=160>](screenshots/screenshot_2.png)
|
||||
[<img src="screenshots/screenshot_3.png" width=160>](screenshots/screenshot_3.png)
|
||||
[<img src="screenshots/screenshot_4.png" width=160>](screenshots/screenshot_4.png)
|
||||
[<img src="screenshots/screenshot_5.png" width=160>](screenshots/screenshot_5.png)
|
||||
[<img src="screenshots/screenshot_6.png" width=160>](screenshots/screenshot_6.png)
|
||||
[<img src="screenshots/screenshot_7.png" width=160>](screenshots/screenshot_7.png)
|
||||
[<img src="screenshots/screenshot_8.png" width=160>](screenshots/screenshot_8.png)
|
||||
[<img src="screenshots/screenshot_9.png" width=160>](screenshots/screenshot_9.png)
|
||||
|
||||
|
||||
## Description
|
||||
|
||||
@@ -35,25 +40,30 @@ NewPipe does not use any Google framework libraries, or the YouTube API. It only
|
||||
* Display general information about a video
|
||||
* Watch YouTube videos
|
||||
* Listen to YouTube videos (experimental)
|
||||
* Popup mode (floating player)
|
||||
* Select the streaming player to watch the video with
|
||||
* Download videos
|
||||
* Download audio only * Open a video in Kodi
|
||||
* Download audio only
|
||||
* Open a video in Kodi
|
||||
* Show Next/Related videos
|
||||
* Search YouTube in a specific language
|
||||
* Watch age restricted material
|
||||
* Display general information about channels
|
||||
* Search channels
|
||||
* Watch videos from a channel
|
||||
* Orbot/Tor support (not yet directly)
|
||||
* 1080p/2k/4k support
|
||||
|
||||
### Coming Features
|
||||
|
||||
* Orbot/Tor support
|
||||
* Bookmarks
|
||||
* View history
|
||||
* Search history
|
||||
* Subscribe to channels
|
||||
* Watch videos from a channel
|
||||
* Search/Watch Playlists
|
||||
* Queeing videos
|
||||
* Subtitles support
|
||||
* livestream support
|
||||
* ... and many more
|
||||
|
||||
### Multiservice support
|
||||
|
||||
@@ -2,14 +2,14 @@ apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 25
|
||||
buildToolsVersion '23.0.3'
|
||||
buildToolsVersion '25.0.2'
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.schabi.newpipe"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 25
|
||||
versionCode 25
|
||||
versionName "0.8.11"
|
||||
versionCode 33
|
||||
versionName "0.9.6"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
@@ -31,22 +31,24 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testCompile 'junit:junit:4.12'
|
||||
compile 'com.android.support:appcompat-v7:25.1.0'
|
||||
compile 'com.android.support:support-v4:25.1.0'
|
||||
compile 'com.android.support:design:25.1.0'
|
||||
compile 'com.android.support:recyclerview-v7:25.1.0'
|
||||
compile 'org.jsoup:jsoup:1.8.3'
|
||||
compile 'org.mozilla:rhino:1.7.7'
|
||||
compile 'info.guardianproject.netcipher:netcipher:1.2'
|
||||
compile 'de.hdodenhof:circleimageview:2.0.0'
|
||||
compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
|
||||
compile 'com.github.nirhart:parallaxscroll:1.0'
|
||||
compile 'com.google.android.exoplayer:exoplayer:r1.5.5'
|
||||
compile 'com.google.code.gson:gson:2.4'
|
||||
compile 'com.nononsenseapps:filepicker:3.0.0'
|
||||
compile 'ch.acra:acra:4.9.0'
|
||||
testCompile 'junit:junit:4.12'
|
||||
testCompile 'org.mockito:mockito-core:1.10.19'
|
||||
testCompile 'org.json:json:20160810'
|
||||
|
||||
compile 'com.android.support:appcompat-v7:25.3.1'
|
||||
compile 'com.android.support:support-v4:25.3.1'
|
||||
compile 'com.android.support:design:25.3.1'
|
||||
compile 'com.android.support:recyclerview-v7:25.3.1'
|
||||
|
||||
compile 'com.google.code.gson:gson:2.7'
|
||||
compile 'org.jsoup:jsoup:1.8.3'
|
||||
compile 'org.mozilla:rhino:1.7.7'
|
||||
compile 'ch.acra:acra:4.9.0'
|
||||
compile 'info.guardianproject.netcipher:netcipher:1.2'
|
||||
|
||||
compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
|
||||
compile 'de.hdodenhof:circleimageview:2.0.0'
|
||||
compile 'com.github.nirhart:parallaxscroll:1.0'
|
||||
compile 'com.nononsenseapps:filepicker:3.0.0'
|
||||
compile 'com.google.android.exoplayer:exoplayer:r2.3.1'
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<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" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
@@ -18,22 +19,15 @@
|
||||
tools:ignore="AllowBackup">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name">
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTask">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".detail.VideoItemDetailActivity"
|
||||
android:label="@string/title_videoitem_detail"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/AppTheme">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".MainActivity" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".player.PlayVideoActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
@@ -46,28 +40,11 @@
|
||||
android:label="@string/background_player_name" />
|
||||
|
||||
<activity
|
||||
android:name=".player.ExoPlayerActivity"
|
||||
android:name=".player.MainVideoPlayer"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleInstance"
|
||||
android:theme="@style/PlayerTheme">
|
||||
<intent-filter>
|
||||
<action android:name="org.schabi.newpipe.exoplayer.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:scheme="content" />
|
||||
<data android:scheme="asset" />
|
||||
<data android:scheme="file" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".player.BackgroundPlayer"
|
||||
android:exported="false"
|
||||
android:label="@string/background_player_name" />
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/PlayerTheme"/>
|
||||
|
||||
<activity
|
||||
android:name=".settings.SettingsActivity"
|
||||
@@ -103,9 +80,6 @@
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/FilePickerTheme"/>
|
||||
<activity
|
||||
android:name=".ChannelActivity"
|
||||
android:launchMode="singleTask" />
|
||||
<activity
|
||||
android:name=".ReCaptchaActivity"
|
||||
android:label="@string/reCaptchaActivity" />
|
||||
@@ -120,7 +94,9 @@
|
||||
android:resource="@xml/provider_paths" />
|
||||
</provider>
|
||||
|
||||
<activity android:name=".RouterActivity"
|
||||
<activity
|
||||
android:name=".RouterActivity"
|
||||
android:taskAffinity=""
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
@@ -137,6 +113,7 @@
|
||||
<data android:host="www.youtube.com" />
|
||||
<!-- video prefix -->
|
||||
<data android:pathPrefix="/v/" />
|
||||
<data android:pathPrefix="/embed/" />
|
||||
<data android:pathPrefix="/watch" />
|
||||
<data android:pathPrefix="/attribution_link" />
|
||||
<!-- channel prefix -->
|
||||
@@ -175,6 +152,64 @@
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".RouterPopupActivity"
|
||||
android:taskAffinity=""
|
||||
android:theme="@android:style/Theme.NoDisplay"
|
||||
android:label="@string/popup_mode_share_menu_title">
|
||||
<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="youtube.com" />
|
||||
<data android:host="m.youtube.com" />
|
||||
<data android:host="www.youtube.com" />
|
||||
<!-- video prefix -->
|
||||
<data android:pathPrefix="/v/" />
|
||||
<data android:pathPrefix="/embed/" />
|
||||
<data android:pathPrefix="/watch" />
|
||||
<data android:pathPrefix="/attribution_link" />
|
||||
</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>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service android:name=".player.PopupVideoPlayer"/>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
||||
@@ -20,10 +20,6 @@ package org.schabi.newpipe;
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Singleton:
|
||||
* Used to send data between certain Activity/Services within the same process.
|
||||
@@ -39,8 +35,5 @@ public class ActivityCommunicator {
|
||||
return activityCommunicator;
|
||||
}
|
||||
|
||||
// Thumbnail send from VideoItemDetailFragment to BackgroundPlayer
|
||||
public volatile Bitmap backgroundPlayerThumbnail;
|
||||
|
||||
public volatile Class returnActivity;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.report.AcraReportSenderFactory;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import info.guardianproject.netcipher.NetCipher;
|
||||
import info.guardianproject.netcipher.proxy.OrbotHelper;
|
||||
@@ -82,6 +83,8 @@ public class App extends Application {
|
||||
// DO NOT REMOVE THIS FUNCTION!!!
|
||||
// Otherwise downloadPathPreference has invalid value.
|
||||
SettingsActivity.initSettings(this);
|
||||
|
||||
ThemeHelper.setTheme(getApplicationContext());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,404 +0,0 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
|
||||
import org.schabi.newpipe.detail.VideoItemDetailFragment;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelExtractor;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.info_list.InfoListAdapter;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.util.NavStack;
|
||||
import java.io.IOException;
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
|
||||
/**
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* ChannelActivity.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 ChannelActivity extends AppCompatActivity {
|
||||
private static final String TAG = ChannelActivity.class.toString();
|
||||
private View rootView = null;
|
||||
|
||||
private int serviceId = -1;
|
||||
private String channelUrl = "";
|
||||
private int pageNumber = 0;
|
||||
private boolean hasNextPage = true;
|
||||
private boolean isLoading = false;
|
||||
|
||||
private ImageLoader imageLoader = ImageLoader.getInstance();
|
||||
private InfoListAdapter infoListAdapter = null;
|
||||
|
||||
private String subS = "";
|
||||
|
||||
ProgressBar progressBar = null;
|
||||
ImageView channelBanner = null;
|
||||
ImageView avatarView = null;
|
||||
TextView titleView = null;
|
||||
TextView subscirberView = null;
|
||||
Button subscriberButton = null;
|
||||
View subscriberLayout = null;
|
||||
|
||||
View header = null;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
ThemeHelper.setTheme(this, true);
|
||||
setContentView(R.layout.activity_channel);
|
||||
rootView = findViewById(android.R.id.content);
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowTitleEnabled(true);
|
||||
|
||||
infoListAdapter = new InfoListAdapter(this, rootView);
|
||||
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.channel_streams_view);
|
||||
final LinearLayoutManager layoutManager = new LinearLayoutManager(this);
|
||||
recyclerView.setLayoutManager(layoutManager);
|
||||
header = getLayoutInflater().inflate(R.layout.channel_header, recyclerView, false);
|
||||
infoListAdapter.setHeader(header);
|
||||
infoListAdapter.setFooter(
|
||||
getLayoutInflater().inflate(R.layout.pignate_footer, recyclerView, false));
|
||||
recyclerView.setAdapter(infoListAdapter);
|
||||
infoListAdapter.setOnStreamInfoItemSelectedListener(
|
||||
new InfoItemBuilder.OnInfoItemSelectedListener() {
|
||||
@Override
|
||||
public void selected(String url, int serviceId) {
|
||||
NavStack.getInstance()
|
||||
.openDetailActivity(ChannelActivity.this, url, serviceId);
|
||||
}
|
||||
});
|
||||
|
||||
// detect if list has ben scrolled to the bottom
|
||||
recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
|
||||
int pastVisiblesItems, visibleItemCount, totalItemCount;
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
if(dy > 0) //check for scroll down
|
||||
{
|
||||
visibleItemCount = layoutManager.getChildCount();
|
||||
totalItemCount = layoutManager.getItemCount();
|
||||
pastVisiblesItems = layoutManager.findFirstVisibleItemPosition();
|
||||
|
||||
if ( (visibleItemCount + pastVisiblesItems) >= totalItemCount
|
||||
&& !isLoading
|
||||
&& hasNextPage)
|
||||
{
|
||||
pageNumber++;
|
||||
requestData(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
subS = getString(R.string.subscriber);
|
||||
|
||||
progressBar = (ProgressBar) findViewById(R.id.progressBar);
|
||||
channelBanner = (ImageView) header.findViewById(R.id.channel_banner_image);
|
||||
avatarView = (ImageView) header.findViewById(R.id.channel_avatar_view);
|
||||
titleView = (TextView) header.findViewById(R.id.channel_title_view);
|
||||
subscirberView = (TextView) header.findViewById(R.id.channel_subscriber_view);
|
||||
subscriberButton = (Button) header.findViewById(R.id.channel_subscribe_button);
|
||||
subscriberLayout = header.findViewById(R.id.channel_subscriber_layout);
|
||||
|
||||
if(savedInstanceState == null) {
|
||||
handleIntent(getIntent());
|
||||
} else {
|
||||
channelUrl = savedInstanceState.getString(NavStack.URL);
|
||||
serviceId = savedInstanceState.getInt(NavStack.SERVICE_ID);
|
||||
NavStack.getInstance()
|
||||
.restoreSavedInstanceState(savedInstanceState);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
handleIntent(intent);
|
||||
}
|
||||
|
||||
private void handleIntent(Intent i) {
|
||||
channelUrl = i.getStringExtra(NavStack.URL);
|
||||
serviceId = i.getIntExtra(NavStack.SERVICE_ID, -1);
|
||||
requestData(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putString(NavStack.URL, channelUrl);
|
||||
outState.putInt(NavStack.SERVICE_ID, serviceId);
|
||||
NavStack.getInstance()
|
||||
.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
private void updateUi(final ChannelInfo info) {
|
||||
findViewById(R.id.channel_header_layout).setVisibility(View.VISIBLE);
|
||||
progressBar.setVisibility(View.GONE);
|
||||
|
||||
if(info.channel_name != null && !info.channel_name.isEmpty()) {
|
||||
getSupportActionBar().setTitle(info.channel_name);
|
||||
titleView.setText(info.channel_name);
|
||||
}
|
||||
|
||||
if(info.banner_url != null && !info.banner_url.isEmpty()) {
|
||||
imageLoader.displayImage(info.banner_url, channelBanner,
|
||||
new ImageErrorLoadingListener(this, rootView ,info.service_id));
|
||||
}
|
||||
|
||||
if(info.avatar_url != null && !info.avatar_url.isEmpty()) {
|
||||
avatarView.setVisibility(View.VISIBLE);
|
||||
imageLoader.displayImage(info.avatar_url, avatarView,
|
||||
new ImageErrorLoadingListener(this, rootView ,info.service_id));
|
||||
}
|
||||
|
||||
if(info.subscriberCount != -1) {
|
||||
subscirberView.setText(buildSubscriberString(info.subscriberCount));
|
||||
}
|
||||
|
||||
if((info.feed_url != null && !info.feed_url.isEmpty()) ||
|
||||
(info.subscriberCount != -1)) {
|
||||
subscriberLayout.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
if(info.feed_url != null && !info.feed_url.isEmpty()) {
|
||||
subscriberButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
Log.d(TAG, info.feed_url);
|
||||
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(info.feed_url));
|
||||
startActivity(i);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
subscriberButton.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void addVideos(final ChannelInfo info) {
|
||||
infoListAdapter.addInfoItemList(info.related_streams);
|
||||
}
|
||||
|
||||
private void postNewErrorToast(Handler h, final int stringResource) {
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(ChannelActivity.this,
|
||||
stringResource, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void requestData(final boolean onlyVideos) {
|
||||
// start processing
|
||||
isLoading = true;
|
||||
|
||||
if(!onlyVideos) {
|
||||
//delete already displayed content
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
infoListAdapter.clearSteamItemList();
|
||||
pageNumber = 0;
|
||||
subscriberLayout.setVisibility(View.GONE);
|
||||
titleView.setText("");
|
||||
getSupportActionBar().setTitle("");
|
||||
if (SDK_INT >= 21) {
|
||||
channelBanner.setImageDrawable(getDrawable(R.drawable.channel_banner));
|
||||
avatarView.setImageDrawable(getDrawable(R.drawable.buddy));
|
||||
}
|
||||
infoListAdapter.showFooter(false);
|
||||
}
|
||||
|
||||
Thread channelExtractorThread = new Thread(new Runnable() {
|
||||
Handler h = new Handler();
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
StreamingService service = null;
|
||||
try {
|
||||
service = NewPipe.getService(serviceId);
|
||||
ChannelExtractor extractor = service.getChannelExtractorInstance(
|
||||
channelUrl, pageNumber);
|
||||
|
||||
final ChannelInfo info = ChannelInfo.getInfo(extractor);
|
||||
|
||||
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
isLoading = false;
|
||||
if(!onlyVideos) {
|
||||
updateUi(info);
|
||||
infoListAdapter.showFooter(true);
|
||||
}
|
||||
hasNextPage = info.hasNextPage;
|
||||
if(!hasNextPage) {
|
||||
infoListAdapter.showFooter(false);
|
||||
}
|
||||
addVideos(info);
|
||||
}
|
||||
});
|
||||
|
||||
// look for non critical errors during extraction
|
||||
if(info != null &&
|
||||
!info.errors.isEmpty()) {
|
||||
Log.e(TAG, "OCCURRED ERRORS DURING EXTRACTION:");
|
||||
for (Throwable e : info.errors) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "------");
|
||||
}
|
||||
|
||||
View rootView = findViewById(android.R.id.content);
|
||||
ErrorActivity.reportError(h, ChannelActivity.this,
|
||||
info.errors, null, rootView,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_CHANNEL,
|
||||
service.getServiceInfo().name, channelUrl, 0 /* no message for the user */));
|
||||
}
|
||||
} catch(IOException ioe) {
|
||||
postNewErrorToast(h, R.string.network_error);
|
||||
ioe.printStackTrace();
|
||||
} catch(ParsingException pe) {
|
||||
ErrorActivity.reportError(h, ChannelActivity.this, pe, VideoItemDetailFragment.class, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_CHANNEL,
|
||||
service.getServiceInfo().name, channelUrl, R.string.parsing_error));
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ChannelActivity.this.finish();
|
||||
}
|
||||
});
|
||||
pe.printStackTrace();
|
||||
} catch(ExtractionException ex) {
|
||||
String name = "none";
|
||||
if(service != null) {
|
||||
name = service.getServiceInfo().name;
|
||||
}
|
||||
ErrorActivity.reportError(h, ChannelActivity.this, ex, VideoItemDetailFragment.class, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_CHANNEL,
|
||||
name, channelUrl, R.string.parsing_error));
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ChannelActivity.this.finish();
|
||||
}
|
||||
});
|
||||
ex.printStackTrace();
|
||||
} catch(Exception e) {
|
||||
ErrorActivity.reportError(h, ChannelActivity.this, e, VideoItemDetailFragment.class, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_CHANNEL,
|
||||
service.getServiceInfo().name, channelUrl, R.string.general_error));
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ChannelActivity.this.finish();
|
||||
}
|
||||
});
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
channelExtractorThread.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
try {
|
||||
NavStack.getInstance()
|
||||
.navBack(this);
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportUiError(this, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
getMenuInflater().inflate(R.menu.menu_channel, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
super.onOptionsItemSelected(item);
|
||||
switch(item.getItemId()) {
|
||||
case R.id.action_settings: {
|
||||
Intent intent = new Intent(this, SettingsActivity.class);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
case R.id.menu_item_openInBrowser: {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(channelUrl));
|
||||
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.choose_browser)));
|
||||
}
|
||||
case R.id.menu_item_share:
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_SEND);
|
||||
intent.putExtra(Intent.EXTRA_TEXT, channelUrl);
|
||||
intent.setType("text/plain");
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title)));
|
||||
case android.R.id.home:
|
||||
NavStack.getInstance().openMainActivity(this);
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
private String buildSubscriberString(long count) {
|
||||
String out = "";
|
||||
if(count >= 1000000000){
|
||||
out += Long.toString((count/1000000000)%1000)+".";
|
||||
}
|
||||
if(count>=1000000){
|
||||
out += Long.toString((count/1000000)%1000) + ".";
|
||||
}
|
||||
if(count>=1000){
|
||||
out += Long.toString((count/1000)%1000)+".";
|
||||
}
|
||||
out += Long.toString(count%1000) + " " + subS;
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.view.View;
|
||||
|
||||
import com.nostra13.universalimageloader.core.assist.FailReason;
|
||||
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
|
||||
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 01.08.16.
|
||||
@@ -33,11 +33,11 @@ import org.schabi.newpipe.extractor.NewPipe;
|
||||
public class ImageErrorLoadingListener implements ImageLoadingListener {
|
||||
|
||||
private int serviceId = -1;
|
||||
private Activity activity = null;
|
||||
private Context context = null;
|
||||
private View rootView = null;
|
||||
|
||||
public ImageErrorLoadingListener(Activity activity, View rootView, int serviceId) {
|
||||
this.activity = activity;
|
||||
public ImageErrorLoadingListener(Context context, View rootView, int serviceId) {
|
||||
this.context = context;
|
||||
this.serviceId= serviceId;
|
||||
this.rootView = rootView;
|
||||
}
|
||||
@@ -47,7 +47,7 @@ public class ImageErrorLoadingListener implements ImageLoadingListener {
|
||||
|
||||
@Override
|
||||
public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
|
||||
ErrorActivity.reportError(activity,
|
||||
ErrorActivity.reportError(context,
|
||||
failReason.getCause(), null, rootView,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.LOAD_IMAGE,
|
||||
NewPipe.getNameOfService(serviceId), imageUri,
|
||||
|
||||
@@ -1,71 +1,140 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.NavUtils;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import org.schabi.newpipe.download.DownloadActivity;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
/**
|
||||
/*
|
||||
* Created by Christian Schabesberger on 02.08.16.
|
||||
*
|
||||
* <p>
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* DownloadActivity.java is part of NewPipe.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* <p>
|
||||
* 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;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
private Fragment mainFragment = null;
|
||||
private static final String TAG = MainActivity.class.toString();
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
|
||||
import org.schabi.newpipe.download.DownloadActivity;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.fragments.MainFragment;
|
||||
import org.schabi.newpipe.fragments.OnItemSelectedListener;
|
||||
import org.schabi.newpipe.fragments.channel.ChannelFragment;
|
||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
||||
import org.schabi.newpipe.fragments.search.SearchFragment;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
public class MainActivity extends AppCompatActivity implements OnItemSelectedListener {
|
||||
private static final String TAG = "MainActivity";
|
||||
public static final boolean DEBUG = false;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Activity's LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
||||
ThemeHelper.setTheme(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
ThemeHelper.setTheme(this, true);
|
||||
setContentView(R.layout.activity_main);
|
||||
setVolumeControlStream(AudioManager.STREAM_MUSIC);
|
||||
mainFragment = getSupportFragmentManager()
|
||||
.findFragmentById(R.id.search_fragment);
|
||||
|
||||
if (getSupportFragmentManager() != null && getSupportFragmentManager().getBackStackEntryCount() == 0) {
|
||||
initFragments();
|
||||
}
|
||||
|
||||
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
if (DEBUG) Log.d(TAG, "onNewIntent() called with: intent = [" + intent + "]");
|
||||
if (intent != null) {
|
||||
// Return if launched from a launcher (e.g. Nova Launcher, Pixel Launcher ...)
|
||||
// to not destroy the already created backstack
|
||||
String action = intent.getAction();
|
||||
if ((action != null && action.equals(Intent.ACTION_MAIN)) && intent.hasCategory(Intent.CATEGORY_LAUNCHER)) return;
|
||||
}
|
||||
|
||||
super.onNewIntent(intent);
|
||||
setIntent(intent);
|
||||
handleIntent(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (DEBUG) Log.d(TAG, "onBackPressed() called");
|
||||
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
|
||||
if (fragment instanceof VideoDetailFragment) if (((VideoDetailFragment) fragment).onActivityBackPressed()) return;
|
||||
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "]");
|
||||
super.onCreateOptionsMenu(menu);
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.main_menu, menu);
|
||||
|
||||
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
|
||||
if (!(fragment instanceof VideoDetailFragment)) {
|
||||
findViewById(R.id.toolbar).findViewById(R.id.toolbar_spinner).setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (!(fragment instanceof SearchFragment)) {
|
||||
findViewById(R.id.toolbar).findViewById(R.id.toolbar_search_container).setVisibility(View.GONE);
|
||||
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.main_menu, menu);
|
||||
}
|
||||
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayShowTitleEnabled(false);
|
||||
actionBar.setDisplayHomeAsUpEnabled(false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (DEBUG) Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]");
|
||||
int id = item.getItemId();
|
||||
|
||||
switch (id) {
|
||||
case android.R.id.home: {
|
||||
Intent intent = new Intent(this, MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
NavUtils.navigateUpTo(this, intent);
|
||||
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
|
||||
if (fragment instanceof VideoDetailFragment) ((VideoDetailFragment) fragment).clearHistory();
|
||||
|
||||
NavigationHelper.openMainActivity(this);
|
||||
return true;
|
||||
}
|
||||
case R.id.action_settings: {
|
||||
@@ -86,8 +155,128 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
//ignore back
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void initFragments() {
|
||||
openMainFragment();
|
||||
if (getIntent() != null && getIntent().hasExtra(Constants.KEY_URL)) {
|
||||
handleIntent(getIntent());
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// OnItemSelectedListener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onItemSelected(StreamingService.LinkType linkType, int serviceId, String url, String name) {
|
||||
switch (linkType) {
|
||||
case STREAM:
|
||||
openVideoDetailFragment(serviceId, url, name, false);
|
||||
break;
|
||||
case CHANNEL:
|
||||
openChannelFragment(serviceId, url, name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void handleIntent(Intent intent) {
|
||||
if (intent.hasExtra(Constants.KEY_THEME_CHANGE) && intent.getBooleanExtra(Constants.KEY_THEME_CHANGE, false)) {
|
||||
this.recreate();
|
||||
Intent setI = new Intent(this, SettingsActivity.class);
|
||||
startActivity(setI);
|
||||
return;
|
||||
}
|
||||
|
||||
if (intent.hasExtra(Constants.KEY_LINK_TYPE)) {
|
||||
String url = intent.getStringExtra(Constants.KEY_URL);
|
||||
int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0);
|
||||
try {
|
||||
switch (((StreamingService.LinkType) intent.getSerializableExtra(Constants.KEY_LINK_TYPE))) {
|
||||
case STREAM:
|
||||
handleVideoDetailIntent(serviceId, url, intent);
|
||||
break;
|
||||
case CHANNEL:
|
||||
handleChannelIntent(serviceId, url, intent);
|
||||
break;
|
||||
case NONE:
|
||||
throw new Exception("Url not known to service. service=" + Integer.toString(serviceId) + " url=" + url);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else if (intent.hasExtra(Constants.KEY_OPEN_SEARCH)) {
|
||||
String searchQuery = intent.getStringExtra(Constants.KEY_QUERY);
|
||||
if (searchQuery == null) searchQuery = "";
|
||||
int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0);
|
||||
openSearchFragment(serviceId, searchQuery);
|
||||
} else {
|
||||
getSupportFragmentManager().popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
|
||||
openMainFragment();//openSearchFragment();
|
||||
}
|
||||
}
|
||||
|
||||
private void openMainFragment() {
|
||||
ImageLoader.getInstance().clearMemoryCache();
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out, android.R.anim.fade_in, android.R.anim.fade_out)
|
||||
.replace(R.id.fragment_holder, new MainFragment())
|
||||
.commit();
|
||||
}
|
||||
|
||||
private void openSearchFragment(int serviceId, String query) {
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out, android.R.anim.fade_in, android.R.anim.fade_out)
|
||||
.replace(R.id.fragment_holder, SearchFragment.getInstance(serviceId, query))
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
}
|
||||
|
||||
private void openVideoDetailFragment(int serviceId, String url, String title, boolean autoPlay) {
|
||||
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
|
||||
if (title == null) title = "";
|
||||
|
||||
if (fragment instanceof VideoDetailFragment && fragment.isVisible()) {
|
||||
VideoDetailFragment detailFragment = (VideoDetailFragment) fragment;
|
||||
detailFragment.setAutoplay(autoPlay);
|
||||
detailFragment.selectAndLoadVideo(serviceId, url, title);
|
||||
return;
|
||||
}
|
||||
|
||||
VideoDetailFragment instance = VideoDetailFragment.getInstance(serviceId, url, title);
|
||||
instance.setAutoplay(autoPlay);
|
||||
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out, android.R.anim.fade_in, android.R.anim.fade_out)
|
||||
.replace(R.id.fragment_holder, instance)
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
}
|
||||
|
||||
private void openChannelFragment(int serviceId, String url, String name) {
|
||||
if (name == null) name = "";
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out, android.R.anim.fade_in, android.R.anim.fade_out)
|
||||
.replace(R.id.fragment_holder, ChannelFragment.getInstance(serviceId, url, name))
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
}
|
||||
|
||||
private void handleVideoDetailIntent(int serviceId, String url, Intent intent) {
|
||||
boolean autoPlay = intent.getBooleanExtra(VideoDetailFragment.AUTO_PLAY, false);
|
||||
String title = intent.getStringExtra(Constants.KEY_TITLE);
|
||||
openVideoDetailFragment(serviceId, url, title, autoPlay);
|
||||
}
|
||||
|
||||
private void handleChannelIntent(int serviceId, String url, Intent intent) {
|
||||
String name = intent.getStringExtra(Constants.KEY_TITLE);
|
||||
openChannelFragment(serviceId, url, name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.os.Bundle;
|
||||
import android.support.v4.app.NavUtils;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.view.MenuItem;
|
||||
import android.webkit.CookieManager;
|
||||
import android.webkit.ValueCallback;
|
||||
@@ -48,10 +49,15 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||
// Set return to Cancel by default
|
||||
setResult(RESULT_CANCELED);
|
||||
|
||||
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
actionBar.setTitle(R.string.reCaptcha_title);
|
||||
actionBar.setDisplayShowTitleEnabled(true);
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
actionBar.setTitle(R.string.reCaptcha_title);
|
||||
actionBar.setDisplayShowTitleEnabled(true);
|
||||
}
|
||||
|
||||
WebView myWebView = (WebView) findViewById(R.id.reCaptchaWebView);
|
||||
|
||||
|
||||
@@ -2,22 +2,15 @@ package org.schabi.newpipe;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.detail.VideoItemDetailActivity;
|
||||
import org.schabi.newpipe.detail.VideoItemDetailFragment;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.util.NavStack;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
|
||||
/**
|
||||
/*
|
||||
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
|
||||
* RouterActivity .java is part of NewPipe.
|
||||
*
|
||||
@@ -40,7 +33,7 @@ import java.util.HashSet;
|
||||
* to the part of the service which can handle the url.
|
||||
*/
|
||||
public class RouterActivity extends Activity {
|
||||
private static final String TAG = RouterActivity.class.toString();
|
||||
//private static final String TAG = "RouterActivity"
|
||||
|
||||
/**
|
||||
* Removes invisible separators (\p{Z}) and punctuation characters including
|
||||
@@ -56,6 +49,25 @@ public class RouterActivity extends Activity {
|
||||
finish();
|
||||
}
|
||||
|
||||
private void handleIntent(Intent intent) {
|
||||
String videoUrl = "";
|
||||
|
||||
// first gather data and find service
|
||||
if (intent.getData() != null) {
|
||||
// this means the video was called though another app
|
||||
videoUrl = intent.getData().toString();
|
||||
} else if (intent.getStringExtra(Intent.EXTRA_TEXT) != null) {
|
||||
//this means that vidoe was called through share menu
|
||||
String extraText = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
videoUrl = getUris(extraText)[0];
|
||||
}
|
||||
|
||||
try {
|
||||
NavigationHelper.openByLink(this, videoUrl);
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
private static String removeHeadingGibberish(final String input) {
|
||||
int start = 0;
|
||||
@@ -109,50 +121,4 @@ public class RouterActivity extends Activity {
|
||||
return result.toArray(new String[result.size()]);
|
||||
}
|
||||
|
||||
private void handleIntent(Intent intent) {
|
||||
String videoUrl = "";
|
||||
StreamingService service = null;
|
||||
|
||||
// first gather data and find service
|
||||
if (intent.getData() != null) {
|
||||
// this means the video was called though another app
|
||||
videoUrl = intent.getData().toString();
|
||||
} else if(intent.getStringExtra(Intent.EXTRA_TEXT) != null) {
|
||||
//this means that vidoe was called through share menu
|
||||
String extraText = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
videoUrl = getUris(extraText)[0];
|
||||
}
|
||||
|
||||
service = NewPipe.getServiceByUrl(videoUrl);
|
||||
if(service == null) {
|
||||
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
return;
|
||||
} else {
|
||||
Intent callIntent = new Intent();
|
||||
switch(service.getLinkTypeByUrl(videoUrl)) {
|
||||
case CHANNEL:
|
||||
callIntent.setClass(this, ChannelActivity.class);
|
||||
break;
|
||||
case STREAM:
|
||||
callIntent.setClass(this, VideoItemDetailActivity.class);
|
||||
callIntent.putExtra(VideoItemDetailFragment.AUTO_PLAY,
|
||||
PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.getBoolean(
|
||||
getString(R.string.autoplay_through_intent_key), false));
|
||||
break;
|
||||
case PLAYLIST:
|
||||
Log.e(TAG, "NOT YET DEFINED");
|
||||
break;
|
||||
default:
|
||||
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
|
||||
callIntent.putExtra(NavStack.URL, videoUrl);
|
||||
callIntent.putExtra(NavStack.SERVICE_ID, service.getServiceId());
|
||||
startActivity(callIntent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
130
app/src/main/java/org/schabi/newpipe/RouterPopupActivity.java
Normal file
@@ -0,0 +1,130 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.player.PopupVideoPlayer;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
|
||||
/**
|
||||
* This activity is thought to open video streams form an external app using the popup player.
|
||||
*/
|
||||
public class RouterPopupActivity extends Activity {
|
||||
//private static final String TAG = "RouterPopupActivity";
|
||||
|
||||
/**
|
||||
* Removes invisible separators (\p{Z}) and punctuation characters including
|
||||
* brackets (\p{P}). See http://www.regular-expressions.info/unicode.html for
|
||||
* more details.
|
||||
*/
|
||||
private final static String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
handleIntent(getIntent());
|
||||
finish();
|
||||
}
|
||||
|
||||
|
||||
private void handleIntent(Intent intent) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
&& !PermissionHelper.checkSystemAlertWindowPermission(this)) {
|
||||
Toast.makeText(this, R.string.msg_popup_permission, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
String videoUrl = "";
|
||||
StreamingService service;
|
||||
|
||||
// first gather data and find service
|
||||
if (intent.getData() != null) {
|
||||
// this means the video was called though another app
|
||||
videoUrl = intent.getData().toString();
|
||||
} else if (intent.getStringExtra(Intent.EXTRA_TEXT) != null) {
|
||||
//this means that vidoe was called through share menu
|
||||
String extraText = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
videoUrl = getUris(extraText)[0];
|
||||
}
|
||||
|
||||
service = NewPipe.getServiceByUrl(videoUrl);
|
||||
if (service == null) {
|
||||
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
Intent callIntent = new Intent(this, PopupVideoPlayer.class);
|
||||
switch (service.getLinkTypeByUrl(videoUrl)) {
|
||||
case STREAM:
|
||||
break;
|
||||
default:
|
||||
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
callIntent.putExtra(Constants.KEY_URL, videoUrl);
|
||||
callIntent.putExtra(Constants.KEY_SERVICE_ID, service.getServiceId());
|
||||
startService(callIntent);
|
||||
}
|
||||
|
||||
private static String removeHeadingGibberish(final String input) {
|
||||
int start = 0;
|
||||
for (int i = input.indexOf("://") - 1; i >= 0; i--) {
|
||||
if (!input.substring(i, i + 1).matches("\\p{L}")) {
|
||||
start = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return input.substring(start, input.length());
|
||||
}
|
||||
|
||||
private static String trim(final String input) {
|
||||
if (input == null || input.length() < 1) {
|
||||
return input;
|
||||
} else {
|
||||
String output = input;
|
||||
while (output.length() > 0 && output.substring(0, 1).matches(REGEX_REMOVE_FROM_URL)) {
|
||||
output = output.substring(1);
|
||||
}
|
||||
while (output.length() > 0
|
||||
&& output.substring(output.length() - 1, output.length()).matches(REGEX_REMOVE_FROM_URL)) {
|
||||
output = output.substring(0, output.length() - 1);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all Strings which look remotely like URLs from a text.
|
||||
* Used if NewPipe was called through share menu.
|
||||
*
|
||||
* @param sharedText text to scan for URLs.
|
||||
* @return potential URLs
|
||||
*/
|
||||
private String[] getUris(final String sharedText) {
|
||||
final Collection<String> result = new HashSet<>();
|
||||
if (sharedText != null) {
|
||||
final String[] array = sharedText.split("\\p{Space}");
|
||||
for (String s : array) {
|
||||
s = trim(s);
|
||||
if (s.length() != 0) {
|
||||
if (s.matches(".+://.+")) {
|
||||
result.add(removeHeadingGibberish(s));
|
||||
} else if (s.matches(".+\\..+")) {
|
||||
result.add("http://" + s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toArray(new String[result.size()]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
|
||||
public class ThemableActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.getString("theme", getResources().getString(R.string.light_theme_title)).
|
||||
equals(getResources().getString(R.string.dark_theme_title))) {
|
||||
setTheme(R.style.DarkTheme);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.getString("theme", getResources().getString(R.string.light_theme_title)).
|
||||
equals(getResources().getString(R.string.dark_theme_title))) {
|
||||
setTheme(R.style.DarkTheme);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,234 +0,0 @@
|
||||
package org.schabi.newpipe.detail;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.ArrayAdapter;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.stream_info.VideoStream;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 18.08.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* DetailsMenuHandler.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 ActionBarHandler {
|
||||
private static final String TAG = ActionBarHandler.class.toString();
|
||||
|
||||
private AppCompatActivity activity;
|
||||
private int selectedVideoStream = -1;
|
||||
|
||||
private SharedPreferences defaultPreferences;
|
||||
|
||||
private Menu menu;
|
||||
|
||||
// Only callbacks are listed here, there are more actions which don't need a callback.
|
||||
// those are edited directly. Typically VideoItemDetailFragment will implement those callbacks.
|
||||
private OnActionListener onShareListener;
|
||||
private OnActionListener onOpenInBrowserListener;
|
||||
private OnActionListener onDownloadListener;
|
||||
private OnActionListener onPlayWithKodiListener;
|
||||
private OnActionListener onPlayAudioListener;
|
||||
|
||||
|
||||
// Triggered when a stream related action is triggered.
|
||||
public interface OnActionListener {
|
||||
void onActionSelected(int selectedStreamId);
|
||||
}
|
||||
|
||||
public ActionBarHandler(AppCompatActivity activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
@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 setupStreamList(final List<VideoStream> videoStreams) {
|
||||
if (activity != null) {
|
||||
selectedVideoStream = 0;
|
||||
|
||||
|
||||
// this array will be shown in the dropdown menu for selecting the stream/resolution.
|
||||
String[] itemArray = new String[videoStreams.size()];
|
||||
for (int i = 0; i < videoStreams.size(); i++) {
|
||||
VideoStream item = videoStreams.get(i);
|
||||
itemArray[i] = MediaFormat.getNameById(item.format) + " " + item.resolution;
|
||||
}
|
||||
int defaultResolution = getDefaultResolution(videoStreams);
|
||||
|
||||
ArrayAdapter<String> itemAdapter = new ArrayAdapter<>(activity.getBaseContext(),
|
||||
android.R.layout.simple_spinner_dropdown_item, itemArray);
|
||||
|
||||
ActionBar ab = activity.getSupportActionBar();
|
||||
//todo: make this throwsable
|
||||
assert ab != null : "Could not get actionbar";
|
||||
ab.setListNavigationCallbacks(itemAdapter
|
||||
, new ActionBar.OnNavigationListener() {
|
||||
@Override
|
||||
public boolean onNavigationItemSelected(int itemPosition, long itemId) {
|
||||
selectedVideoStream = (int) itemId;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
ab.setSelectedNavigationItem(defaultResolution);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private int getDefaultResolution(final List<VideoStream> videoStreams) {
|
||||
if (defaultPreferences == null)
|
||||
return 0;
|
||||
|
||||
String defaultResolution = defaultPreferences
|
||||
.getString(activity.getString(R.string.default_resolution_key),
|
||||
activity.getString(R.string.default_resolution_value));
|
||||
|
||||
for (int i = 0; i < videoStreams.size(); i++) {
|
||||
VideoStream item = videoStreams.get(i);
|
||||
if (defaultResolution.equals(item.resolution)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
// this is actually an error,
|
||||
// but maybe there is really no stream fitting to the default value.
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void setupMenu(Menu menu, MenuInflater inflater) {
|
||||
this.menu = menu;
|
||||
|
||||
// CAUTION set item properties programmatically otherwise it would not be accepted by
|
||||
// appcompat itemsinflater.inflate(R.menu.videoitem_detail, menu);
|
||||
|
||||
defaultPreferences = PreferenceManager.getDefaultSharedPreferences(activity);
|
||||
inflater.inflate(R.menu.videoitem_detail, menu);
|
||||
|
||||
showPlayWithKodiAction(defaultPreferences
|
||||
.getBoolean(activity.getString(R.string.show_play_with_kodi_key), false));
|
||||
}
|
||||
|
||||
public boolean onItemSelected(MenuItem item) {
|
||||
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, websiteUrl);
|
||||
intent.setType("text/plain");
|
||||
activity.startActivity(Intent.createChooser(intent, activity.getString(R.string.share_dialog_title)));
|
||||
*/
|
||||
if(onShareListener != null) {
|
||||
onShareListener.onActionSelected(selectedVideoStream);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case R.id.menu_item_openInBrowser: {
|
||||
if(onOpenInBrowserListener != null) {
|
||||
onOpenInBrowserListener.onActionSelected(selectedVideoStream);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
case R.id.menu_item_download:
|
||||
if(onDownloadListener != null) {
|
||||
onDownloadListener.onActionSelected(selectedVideoStream);
|
||||
}
|
||||
return true;
|
||||
case R.id.action_settings: {
|
||||
Intent intent = new Intent(activity, SettingsActivity.class);
|
||||
activity.startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
case R.id.action_play_with_kodi:
|
||||
if(onPlayWithKodiListener != null) {
|
||||
onPlayWithKodiListener.onActionSelected(selectedVideoStream);
|
||||
}
|
||||
return true;
|
||||
case R.id.menu_item_play_audio:
|
||||
if(onPlayAudioListener != null) {
|
||||
onPlayAudioListener.onActionSelected(selectedVideoStream);
|
||||
}
|
||||
return true;
|
||||
case R.id.menu_item_downloads: {
|
||||
Intent intent =
|
||||
new Intent(activity, org.schabi.newpipe.download.DownloadActivity.class);
|
||||
activity.startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
Log.e(TAG, "Menu Item not known");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public int getSelectedVideoStream() {
|
||||
return selectedVideoStream;
|
||||
}
|
||||
|
||||
public void setOnShareListener(OnActionListener listener) {
|
||||
onShareListener = listener;
|
||||
}
|
||||
|
||||
public void setOnOpenInBrowserListener(OnActionListener listener) {
|
||||
onOpenInBrowserListener = listener;
|
||||
}
|
||||
|
||||
public void setOnDownloadListener(OnActionListener listener) {
|
||||
onDownloadListener = listener;
|
||||
}
|
||||
|
||||
public void setOnPlayWithKodiListener(OnActionListener listener) {
|
||||
onPlayWithKodiListener = listener;
|
||||
}
|
||||
|
||||
public void setOnPlayAudioListener(OnActionListener listener) {
|
||||
onPlayAudioListener = listener;
|
||||
}
|
||||
|
||||
public void showAudioAction(boolean visible) {
|
||||
menu.findItem(R.id.menu_item_play_audio).setVisible(visible);
|
||||
}
|
||||
|
||||
public void showDownloadAction(boolean visible) {
|
||||
menu.findItem(R.id.menu_item_download).setVisible(visible);
|
||||
}
|
||||
|
||||
public void showPlayWithKodiAction(boolean visible) {
|
||||
menu.findItem(R.id.action_play_with_kodi).setVisible(visible);
|
||||
}
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
package org.schabi.newpipe.detail;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
import org.schabi.newpipe.extractor.stream_info.StreamExtractor;
|
||||
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 02.08.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* StreamInfoWorker.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 StreamInfoWorker {
|
||||
|
||||
private static final String TAG = StreamInfoWorker.class.toString();
|
||||
|
||||
public interface OnStreamInfoReceivedListener {
|
||||
void onReceive(StreamInfo info);
|
||||
void onError(int messageId);
|
||||
void onReCaptchaException();
|
||||
void onBlockedByGemaError();
|
||||
void onContentErrorWithMessage(int messageId);
|
||||
void onContentError();
|
||||
}
|
||||
|
||||
private class StreamExtractorRunnable implements Runnable {
|
||||
private final Handler h = new Handler();
|
||||
private StreamExtractor streamExtractor;
|
||||
private final int serviceId;
|
||||
private final String videoUrl;
|
||||
private Activity a;
|
||||
|
||||
public StreamExtractorRunnable(Activity a, String videoUrl, int serviceId) {
|
||||
this.serviceId = serviceId;
|
||||
this.videoUrl = videoUrl;
|
||||
this.a = a;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
StreamInfo streamInfo = null;
|
||||
StreamingService service = null;
|
||||
try {
|
||||
service = NewPipe.getService(serviceId);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
ErrorActivity.reportError(h, a, e, VideoItemDetailFragment.class, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
|
||||
"", videoUrl, R.string.could_not_get_stream));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
streamExtractor = service.getExtractorInstance(videoUrl);
|
||||
streamInfo = StreamInfo.getVideoInfo(streamExtractor);
|
||||
|
||||
final StreamInfo info = streamInfo;
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
onStreamInfoReceivedListener.onReceive(info);
|
||||
}
|
||||
});
|
||||
|
||||
// look for errors during extraction
|
||||
// this if statement only covers extra information.
|
||||
// if these are not available or caused an error, they are just not available
|
||||
// but don't render the stream information unusalbe.
|
||||
if(streamInfo != null &&
|
||||
!streamInfo.errors.isEmpty()) {
|
||||
Log.e(TAG, "OCCURRED ERRORS DURING EXTRACTION:");
|
||||
for (Throwable e : streamInfo.errors) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "------");
|
||||
}
|
||||
|
||||
View rootView = a != null ? a.findViewById(R.id.video_item_detail) : null;
|
||||
ErrorActivity.reportError(h, a,
|
||||
streamInfo.errors, null, rootView,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
|
||||
service.getServiceInfo().name, videoUrl, 0 /* no message for the user */));
|
||||
}
|
||||
|
||||
// These errors render the stream information unusable.
|
||||
} catch (ReCaptchaException e) {
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
onStreamInfoReceivedListener.onReCaptchaException();
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
onStreamInfoReceivedListener.onError(R.string.network_error);
|
||||
}
|
||||
});
|
||||
e.printStackTrace();
|
||||
} catch (YoutubeStreamExtractor.DecryptException de) {
|
||||
// custom service related exceptions
|
||||
ErrorActivity.reportError(h, a, de, VideoItemDetailFragment.class, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
|
||||
service.getServiceInfo().name, videoUrl, R.string.youtube_signature_decryption_error));
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
a.finish();
|
||||
}
|
||||
});
|
||||
de.printStackTrace();
|
||||
} catch (YoutubeStreamExtractor.GemaException ge) {
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
onStreamInfoReceivedListener.onBlockedByGemaError();
|
||||
}
|
||||
});
|
||||
} catch(YoutubeStreamExtractor.LiveStreamException e) {
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
onStreamInfoReceivedListener
|
||||
.onContentErrorWithMessage(R.string.live_streams_not_supported);
|
||||
}
|
||||
});
|
||||
}
|
||||
// ----------------------------------------
|
||||
catch(StreamExtractor.ContentNotAvailableException e) {
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
onStreamInfoReceivedListener
|
||||
.onContentError();
|
||||
}
|
||||
});
|
||||
e.printStackTrace();
|
||||
} catch(StreamInfo.StreamExctractException e) {
|
||||
if(!streamInfo.errors.isEmpty()) {
|
||||
// !!! if this case ever kicks in someone gets kicked out !!!
|
||||
ErrorActivity.reportError(h, a, e, VideoItemDetailFragment.class, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
|
||||
service.getServiceInfo().name, videoUrl, R.string.could_not_get_stream));
|
||||
} else {
|
||||
ErrorActivity.reportError(h, a, streamInfo.errors, VideoItemDetailFragment.class, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
|
||||
service.getServiceInfo().name, videoUrl, R.string.could_not_get_stream));
|
||||
}
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
a.finish();
|
||||
}
|
||||
});
|
||||
e.printStackTrace();
|
||||
} catch (ParsingException e) {
|
||||
ErrorActivity.reportError(h, a, e, VideoItemDetailFragment.class, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
|
||||
service.getServiceInfo().name, videoUrl, R.string.parsing_error));
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
a.finish();
|
||||
}
|
||||
});
|
||||
e.printStackTrace();
|
||||
} catch(Exception e) {
|
||||
ErrorActivity.reportError(h, a, e, VideoItemDetailFragment.class, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
|
||||
service.getServiceInfo().name, videoUrl, R.string.general_error));
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
a.finish();
|
||||
}
|
||||
});
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static StreamInfoWorker streamInfoWorker = null;
|
||||
private StreamExtractorRunnable runnable = null;
|
||||
private OnStreamInfoReceivedListener onStreamInfoReceivedListener = null;
|
||||
|
||||
private StreamInfoWorker() {
|
||||
|
||||
}
|
||||
|
||||
public static StreamInfoWorker getInstance() {
|
||||
return streamInfoWorker == null ? (streamInfoWorker = new StreamInfoWorker()) : streamInfoWorker;
|
||||
}
|
||||
|
||||
public void search(int serviceId, String url, Activity a) {
|
||||
runnable = new StreamExtractorRunnable(a, url, serviceId);
|
||||
Thread thread = new Thread(runnable);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
public void setOnStreamInfoReceivedListener(
|
||||
OnStreamInfoReceivedListener onStreamInfoReceivedListener) {
|
||||
this.onStreamInfoReceivedListener = onStreamInfoReceivedListener;
|
||||
}
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
package org.schabi.newpipe.detail;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.util.Log;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.util.NavStack;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
|
||||
/**
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* VideoItemDetailActivity.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 VideoItemDetailActivity extends AppCompatActivity {
|
||||
private static final String TAG = VideoItemDetailActivity.class.toString();
|
||||
|
||||
private VideoItemDetailFragment fragment;
|
||||
|
||||
private String videoUrl;
|
||||
private int currentStreamingService = -1;
|
||||
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
ThemeHelper.setTheme(this, true);
|
||||
setContentView(R.layout.activity_videoitem_detail);
|
||||
setVolumeControlStream(AudioManager.STREAM_MUSIC);
|
||||
// Show the Up button in the action bar.
|
||||
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
|
||||
// (e.g. when rotating the screen from portrait to landscape).
|
||||
// In this case, the fragment will automatically be re-added
|
||||
// to its container so we don't need to manually add it.
|
||||
// For more information, see the Fragments API guide at:
|
||||
//
|
||||
// http://developer.android.com/guide/components/fragments.html
|
||||
//
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
handleIntent(getIntent());
|
||||
} else {
|
||||
videoUrl = savedInstanceState.getString(NavStack.URL);
|
||||
currentStreamingService = savedInstanceState.getInt(NavStack.SERVICE_ID);
|
||||
NavStack.getInstance()
|
||||
.restoreSavedInstanceState(savedInstanceState);
|
||||
addFragment(savedInstanceState);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
setIntent(intent);
|
||||
handleIntent(intent);
|
||||
}
|
||||
|
||||
private void handleIntent(Intent intent) {
|
||||
Bundle arguments = new Bundle();
|
||||
boolean autoplay = false;
|
||||
|
||||
videoUrl = intent.getStringExtra(NavStack.URL);
|
||||
currentStreamingService = intent.getIntExtra(NavStack.SERVICE_ID, -1);
|
||||
if(intent.hasExtra(VideoItemDetailFragment.AUTO_PLAY)) {
|
||||
arguments.putBoolean(VideoItemDetailFragment.AUTO_PLAY,
|
||||
intent.getBooleanExtra(VideoItemDetailFragment.AUTO_PLAY, false));
|
||||
}
|
||||
arguments.putString(NavStack.URL, videoUrl);
|
||||
arguments.putInt(NavStack.SERVICE_ID, currentStreamingService);
|
||||
addFragment(arguments);
|
||||
}
|
||||
|
||||
private void addFragment(final Bundle arguments) {
|
||||
// Create the detail fragment and add it to the activity
|
||||
// using a fragment transaction.
|
||||
fragment = new VideoItemDetailFragment();
|
||||
fragment.setArguments(arguments);
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.videoitem_detail_container, fragment)
|
||||
.commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
App.checkStartTor(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putString(NavStack.URL, videoUrl);
|
||||
outState.putInt(NavStack.SERVICE_ID, currentStreamingService);
|
||||
outState.putBoolean(VideoItemDetailFragment.AUTO_PLAY, false);
|
||||
NavStack.getInstance()
|
||||
.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
super.onOptionsItemSelected(item);
|
||||
int id = item.getItemId();
|
||||
if (id == android.R.id.home) {
|
||||
// This ID represents the Home or Up button. In the case of this
|
||||
// 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
|
||||
|
||||
NavStack.getInstance()
|
||||
.openMainActivity(this);
|
||||
return true;
|
||||
} else {
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
try {
|
||||
NavStack.getInstance()
|
||||
.navBack(this);
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportUiError(this, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,836 +0,0 @@
|
||||
package org.schabi.newpipe.detail;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Point;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.design.widget.FloatingActionButton;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.text.Html;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.assist.FailReason;
|
||||
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
|
||||
|
||||
import org.schabi.newpipe.ActivityCommunicator;
|
||||
import org.schabi.newpipe.ImageErrorLoadingListener;
|
||||
import org.schabi.newpipe.Localization;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.download.DownloadDialog;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.stream_info.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream_info.VideoStream;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.player.BackgroundPlayer;
|
||||
import org.schabi.newpipe.player.ExoPlayerActivity;
|
||||
import org.schabi.newpipe.player.PlayVideoActivity;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import java.util.Vector;
|
||||
|
||||
import org.schabi.newpipe.util.NavStack;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
import static org.schabi.newpipe.ReCaptchaActivity.RECAPTCHA_REQUEST;
|
||||
|
||||
|
||||
/**
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* VideoItemDetailFragment.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 VideoItemDetailFragment extends Fragment {
|
||||
|
||||
private static final String TAG = VideoItemDetailFragment.class.toString();
|
||||
private static final String KORE_PACKET = "org.xbmc.kore";
|
||||
|
||||
/**
|
||||
* The fragment argument representing the item ID that this fragment
|
||||
* represents.
|
||||
*/
|
||||
public static final String AUTO_PLAY = "auto_play";
|
||||
|
||||
private AppCompatActivity activity;
|
||||
private ActionBarHandler actionBarHandler;
|
||||
private ProgressBar progressBar;
|
||||
|
||||
private int streamingServiceId = -1;
|
||||
|
||||
private boolean autoPlayEnabled;
|
||||
private boolean showNextStreamItem;
|
||||
|
||||
private View thumbnailWindowLayout;
|
||||
//this only remains due to downwards compatibility
|
||||
private FloatingActionButton playVideoButton;
|
||||
private final Point initialThumbnailPos = new Point(0, 0);
|
||||
private View rootView = null;
|
||||
private Bitmap streamThumbnail = null;
|
||||
|
||||
private ImageLoader imageLoader = ImageLoader.getInstance();
|
||||
private DisplayImageOptions displayImageOptions =
|
||||
new DisplayImageOptions.Builder().cacheInMemory(true).build();
|
||||
|
||||
private InfoItemBuilder infoItemBuilder = null;
|
||||
|
||||
public interface OnInvokeCreateOptionsMenuListener {
|
||||
void createOptionsMenu();
|
||||
}
|
||||
|
||||
private OnInvokeCreateOptionsMenuListener onInvokeCreateOptionsMenuListener;
|
||||
|
||||
private void updateInfo(final StreamInfo info) {
|
||||
Activity a = getActivity();
|
||||
|
||||
RelativeLayout textContentLayout =
|
||||
(RelativeLayout) activity.findViewById(R.id.detail_text_content_layout);
|
||||
final TextView videoTitleView =
|
||||
(TextView) activity.findViewById(R.id.detail_video_title_view);
|
||||
TextView uploaderView = (TextView) activity.findViewById(R.id.detail_uploader_view);
|
||||
TextView viewCountView = (TextView) activity.findViewById(R.id.detail_view_count_view);
|
||||
TextView thumbsUpView = (TextView) activity.findViewById(R.id.detail_thumbs_up_count_view);
|
||||
TextView thumbsDownView =
|
||||
(TextView) activity.findViewById(R.id.detail_thumbs_down_count_view);
|
||||
TextView uploadDateView = (TextView) activity.findViewById(R.id.detail_upload_date_view);
|
||||
TextView descriptionView = (TextView) activity.findViewById(R.id.detail_description_view);
|
||||
RecyclerView nextStreamView =
|
||||
(RecyclerView) activity.findViewById(R.id.detail_next_stream_content);
|
||||
RelativeLayout nextVideoRootFrame =
|
||||
(RelativeLayout) activity.findViewById(R.id.detail_next_stream_root_layout);
|
||||
TextView similarTitle = (TextView) activity.findViewById(R.id.detail_similar_title);
|
||||
Button backgroundButton = (Button)
|
||||
activity.findViewById(R.id.detail_stream_thumbnail_window_background_button);
|
||||
View thumbnailView = activity.findViewById(R.id.detail_thumbnail_view);
|
||||
View topView = activity.findViewById(R.id.detailTopView);
|
||||
Button channelButton = (Button) activity.findViewById(R.id.channel_button);
|
||||
|
||||
// prevents a crash if the activity/fragment was already left when the response came
|
||||
if(channelButton != null) {
|
||||
|
||||
progressBar.setVisibility(View.GONE);
|
||||
if (info.next_video != null) {
|
||||
// todo: activate this function or remove it
|
||||
nextStreamView.setVisibility(View.GONE);
|
||||
} else {
|
||||
nextStreamView.setVisibility(View.GONE);
|
||||
activity.findViewById(R.id.detail_similar_title).setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
textContentLayout.setVisibility(View.VISIBLE);
|
||||
if (android.os.Build.VERSION.SDK_INT < 18) {
|
||||
playVideoButton.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
ImageView playArrowView = (ImageView) activity.findViewById(R.id.play_arrow_view);
|
||||
playArrowView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
if (!showNextStreamItem) {
|
||||
nextVideoRootFrame.setVisibility(View.GONE);
|
||||
similarTitle.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
videoTitleView.setText(info.title);
|
||||
|
||||
topView.setOnTouchListener(new View.OnTouchListener() {
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
if (event.getAction() == android.view.MotionEvent.ACTION_UP) {
|
||||
ImageView arrow = (ImageView) activity.findViewById(R.id.toggle_description_view);
|
||||
View extra = activity.findViewById(R.id.detailExtraView);
|
||||
if (extra.getVisibility() == View.VISIBLE) {
|
||||
extra.setVisibility(View.GONE);
|
||||
arrow.setImageResource(R.drawable.arrow_down);
|
||||
} else {
|
||||
extra.setVisibility(View.VISIBLE);
|
||||
arrow.setImageResource(R.drawable.arrow_up);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// Since newpipe is designed to work even if certain information is not available,
|
||||
// the UI has to react on missing information.
|
||||
videoTitleView.setText(info.title);
|
||||
if (!info.uploader.isEmpty()) {
|
||||
uploaderView.setText(info.uploader);
|
||||
} else {
|
||||
activity.findViewById(R.id.detail_uploader_view).setVisibility(View.GONE);
|
||||
}
|
||||
if (info.view_count >= 0) {
|
||||
viewCountView.setText(Localization.localizeViewCount(info.view_count, a));
|
||||
} else {
|
||||
viewCountView.setVisibility(View.GONE);
|
||||
}
|
||||
if (info.dislike_count >= 0) {
|
||||
thumbsDownView.setText(Localization.localizeNumber(info.dislike_count, a));
|
||||
} else {
|
||||
thumbsDownView.setVisibility(View.INVISIBLE);
|
||||
activity.findViewById(R.id.detail_thumbs_down_count_view).setVisibility(View.GONE);
|
||||
}
|
||||
if (info.like_count >= 0) {
|
||||
thumbsUpView.setText(Localization.localizeNumber(info.like_count, a));
|
||||
} else {
|
||||
thumbsUpView.setVisibility(View.GONE);
|
||||
activity.findViewById(R.id.detail_thumbs_up_img_view).setVisibility(View.GONE);
|
||||
thumbsDownView.setVisibility(View.GONE);
|
||||
activity.findViewById(R.id.detail_thumbs_down_img_view).setVisibility(View.GONE);
|
||||
}
|
||||
if (!info.upload_date.isEmpty()) {
|
||||
uploadDateView.setText(Localization.localizeDate(info.upload_date, a));
|
||||
} else {
|
||||
uploadDateView.setVisibility(View.GONE);
|
||||
}
|
||||
if (!info.description.isEmpty()) {
|
||||
descriptionView.setText(Html.fromHtml(info.description));
|
||||
} else {
|
||||
descriptionView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
descriptionView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
|
||||
// parse streams
|
||||
Vector<VideoStream> streamsToUse = new Vector<>();
|
||||
for (VideoStream i : info.video_streams) {
|
||||
if (useStream(i, streamsToUse)) {
|
||||
streamsToUse.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
textContentLayout.setVisibility(View.VISIBLE);
|
||||
|
||||
if (info.next_video == null) {
|
||||
activity.findViewById(R.id.detail_next_stream_title).setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (info.related_streams != null && !info.related_streams.isEmpty()) {
|
||||
initSimilarVideos(info);
|
||||
} else {
|
||||
activity.findViewById(R.id.detail_similar_title).setVisibility(View.GONE);
|
||||
activity.findViewById(R.id.similar_streams_view).setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
setupActionBarHandler(info);
|
||||
|
||||
if (autoPlayEnabled) {
|
||||
playVideo(info);
|
||||
}
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT < 18) {
|
||||
playVideoButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
playVideo(info);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
backgroundButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
playVideo(info);
|
||||
}
|
||||
});
|
||||
|
||||
//todo: make backgroundButton handle this
|
||||
thumbnailView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
playVideo(info);
|
||||
}
|
||||
});
|
||||
|
||||
if (info.channel_url != null && info.channel_url != "") {
|
||||
channelButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
NavStack.getInstance()
|
||||
.openChannelActivity(getActivity(), info.channel_url, info.service_id);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
channelButton.setVisibility(Button.GONE);
|
||||
}
|
||||
|
||||
initThumbnailViews(info);
|
||||
}
|
||||
}
|
||||
|
||||
private void initThumbnailViews(final StreamInfo info) {
|
||||
ImageView videoThumbnailView = (ImageView) activity.findViewById(R.id.detail_thumbnail_view);
|
||||
ImageView uploaderThumb
|
||||
= (ImageView) activity.findViewById(R.id.detail_uploader_thumbnail_view);
|
||||
|
||||
if (info.thumbnail_url != null && !info.thumbnail_url.isEmpty()) {
|
||||
imageLoader.displayImage(info.thumbnail_url, videoThumbnailView,
|
||||
displayImageOptions, new ImageLoadingListener() {
|
||||
@Override
|
||||
public void onLoadingStarted(String imageUri, View view) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
|
||||
ErrorActivity.reportError(getActivity(),
|
||||
failReason.getCause(), null, rootView,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.LOAD_IMAGE,
|
||||
NewPipe.getNameOfService(info.service_id), imageUri,
|
||||
R.string.could_not_load_thumbnails));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
|
||||
streamThumbnail = loadedImage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingCancelled(String imageUri, View view) {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
videoThumbnailView.setImageResource(R.drawable.dummy_thumbnail_dark);
|
||||
}
|
||||
if (info.uploader_thumbnail_url != null && !info.uploader_thumbnail_url.isEmpty()) {
|
||||
imageLoader.displayImage(info.uploader_thumbnail_url,
|
||||
uploaderThumb, displayImageOptions,
|
||||
new ImageErrorLoadingListener(activity, rootView, info.service_id));
|
||||
}
|
||||
}
|
||||
|
||||
private void setupActionBarHandler(final StreamInfo info) {
|
||||
actionBarHandler.setupStreamList(info.video_streams);
|
||||
|
||||
actionBarHandler.setOnShareListener(new ActionBarHandler.OnActionListener() {
|
||||
@Override
|
||||
public void onActionSelected(int selectedStreamId) {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_SEND);
|
||||
intent.putExtra(Intent.EXTRA_TEXT, info.webpage_url);
|
||||
intent.setType("text/plain");
|
||||
activity.startActivity(Intent.createChooser(intent, activity.getString(R.string.share_dialog_title)));
|
||||
}
|
||||
});
|
||||
|
||||
actionBarHandler.setOnOpenInBrowserListener(new ActionBarHandler.OnActionListener() {
|
||||
@Override
|
||||
public void onActionSelected(int selectedStreamId) {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(info.webpage_url));
|
||||
|
||||
activity.startActivity(Intent.createChooser(intent, activity.getString(R.string.choose_browser)));
|
||||
}
|
||||
});
|
||||
|
||||
actionBarHandler.setOnPlayWithKodiListener(new ActionBarHandler.OnActionListener() {
|
||||
@Override
|
||||
public void onActionSelected(int selectedStreamId) {
|
||||
try {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setPackage(KORE_PACKET);
|
||||
intent.setData(Uri.parse(info.webpage_url.replace("https", "http")));
|
||||
activity.startActivity(intent);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
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(activity.getString(R.string.fdroid_kore_url)));
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
|
||||
}
|
||||
});
|
||||
builder.create().show();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
actionBarHandler.setOnDownloadListener(new ActionBarHandler.OnActionListener() {
|
||||
@Override
|
||||
public void onActionSelected(int selectedStreamId) {
|
||||
if(!PermissionHelper.checkStoragePermissions(getActivity())) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Bundle args = new Bundle();
|
||||
|
||||
// Sometimes it may be that some information is not available due to changes fo the
|
||||
// website which was crawled. Then the ui has to understand this and act right.
|
||||
|
||||
if (info.audio_streams != null) {
|
||||
AudioStream audioStream =
|
||||
info.audio_streams.get(getPreferredAudioStreamId(info));
|
||||
|
||||
String audioSuffix = "." + MediaFormat.getSuffixById(audioStream.format);
|
||||
args.putString(DownloadDialog.AUDIO_URL, audioStream.url);
|
||||
args.putString(DownloadDialog.FILE_SUFFIX_AUDIO, audioSuffix);
|
||||
}
|
||||
|
||||
if (info.video_streams != null) {
|
||||
VideoStream selectedStreamItem = info.video_streams.get(selectedStreamId);
|
||||
String videoSuffix = "." + MediaFormat.getSuffixById(selectedStreamItem.format);
|
||||
args.putString(DownloadDialog.FILE_SUFFIX_VIDEO, videoSuffix);
|
||||
args.putString(DownloadDialog.VIDEO_URL, selectedStreamItem.url);
|
||||
}
|
||||
|
||||
args.putString(DownloadDialog.TITLE, info.title);
|
||||
DownloadDialog downloadDialog = DownloadDialog.newInstance(args);
|
||||
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(VideoItemDetailFragment.this.getActivity(),
|
||||
R.string.could_not_setup_download_menu, Toast.LENGTH_LONG).show();
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (info.audio_streams == null) {
|
||||
actionBarHandler.showAudioAction(false);
|
||||
} else {
|
||||
actionBarHandler.setOnPlayAudioListener(new ActionBarHandler.OnActionListener() {
|
||||
@Override
|
||||
public void onActionSelected(int selectedStreamId) {
|
||||
boolean useExternalAudioPlayer = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.getBoolean(activity.getString(R.string.use_external_audio_player_key), false);
|
||||
Intent intent;
|
||||
AudioStream audioStream =
|
||||
info.audio_streams.get(getPreferredAudioStreamId(info));
|
||||
if (!useExternalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 18) {
|
||||
//internal music player: explicit intent
|
||||
if (!BackgroundPlayer.isRunning && streamThumbnail != null) {
|
||||
ActivityCommunicator.getCommunicator()
|
||||
.backgroundPlayerThumbnail = streamThumbnail;
|
||||
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, info.title);
|
||||
intent.putExtra(BackgroundPlayer.WEB_URL, info.webpage_url);
|
||||
intent.putExtra(BackgroundPlayer.SERVICE_ID, streamingServiceId);
|
||||
intent.putExtra(BackgroundPlayer.CHANNEL_NAME, info.uploader);
|
||||
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, info.title);
|
||||
intent.putExtra("title", info.title);
|
||||
// HERE !!!
|
||||
activity.startActivity(intent);
|
||||
} 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private int getPreferredAudioStreamId(final StreamInfo info) {
|
||||
String preferredFormatString = PreferenceManager.getDefaultSharedPreferences(getActivity())
|
||||
.getString(activity.getString(R.string.default_audio_format_key), "webm");
|
||||
|
||||
int preferredFormat = MediaFormat.WEBMA.id;
|
||||
switch(preferredFormatString) {
|
||||
case "webm":
|
||||
preferredFormat = MediaFormat.WEBMA.id;
|
||||
break;
|
||||
case "m4a":
|
||||
preferredFormat = MediaFormat.M4A.id;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
for(int i = 0; i < info.audio_streams.size(); i++) {
|
||||
if(info.audio_streams.get(i).format == preferredFormat) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
//todo: make this a proper error
|
||||
Log.e(TAG, "FAILED to set audioStream value!");
|
||||
return 0;
|
||||
}
|
||||
|
||||
private void initSimilarVideos(final StreamInfo info) {
|
||||
LinearLayout similarLayout = (LinearLayout) activity.findViewById(R.id.similar_streams_view);
|
||||
for (final InfoItem item : info.related_streams) {
|
||||
similarLayout.addView(infoItemBuilder.buildView(similarLayout, item));
|
||||
}
|
||||
infoItemBuilder.setOnStreamInfoItemSelectedListener(
|
||||
new InfoItemBuilder.OnInfoItemSelectedListener() {
|
||||
@Override
|
||||
public void selected(String url, int serviceId) {
|
||||
NavStack.getInstance()
|
||||
.openDetailActivity(getContext(), url, serviceId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onErrorBlockedByGema() {
|
||||
Button backgroundButton = (Button)
|
||||
activity.findViewById(R.id.detail_stream_thumbnail_window_background_button);
|
||||
ImageView thumbnailView = (ImageView) activity.findViewById(R.id.detail_thumbnail_view);
|
||||
|
||||
progressBar.setVisibility(View.GONE);
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
Toast.makeText(VideoItemDetailFragment.this.getActivity(),
|
||||
R.string.blocked_by_gema, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
private void onNotSpecifiedContentError() {
|
||||
ImageView thumbnailView = (ImageView) activity.findViewById(R.id.detail_thumbnail_view);
|
||||
progressBar.setVisibility(View.GONE);
|
||||
thumbnailView.setImageBitmap(BitmapFactory.decodeResource(
|
||||
getResources(), R.drawable.not_available_monkey));
|
||||
Toast.makeText(activity, R.string.content_not_available, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void onNotSpecifiedContentErrorWithMessage(int resourceId) {
|
||||
ImageView thumbnailView = (ImageView) activity.findViewById(R.id.detail_thumbnail_view);
|
||||
progressBar.setVisibility(View.GONE);
|
||||
thumbnailView.setImageBitmap(BitmapFactory.decodeResource(
|
||||
getResources(), R.drawable.not_available_monkey));
|
||||
Toast.makeText(activity, resourceId, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
}
|
||||
|
||||
private boolean useStream(VideoStream stream, Vector<VideoStream> streams) {
|
||||
for(VideoStream i : streams) {
|
||||
if(i.resolution.equals(stream.resolution)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
activity = (AppCompatActivity) getActivity();
|
||||
showNextStreamItem = PreferenceManager.getDefaultSharedPreferences(getActivity())
|
||||
.getBoolean(activity.getString(R.string.show_next_video_key), true);
|
||||
|
||||
|
||||
StreamInfoWorker siw = StreamInfoWorker.getInstance();
|
||||
siw.setOnStreamInfoReceivedListener(new StreamInfoWorker.OnStreamInfoReceivedListener() {
|
||||
@Override
|
||||
public void onReceive(StreamInfo info) {
|
||||
updateInfo(info);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(int messageId) {
|
||||
postNewErrorToast(messageId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReCaptchaException() {
|
||||
Toast.makeText(getActivity(), R.string.recaptcha_request_toast,
|
||||
Toast.LENGTH_LONG).show();
|
||||
|
||||
// Starting ReCaptcha Challenge Activity
|
||||
startActivityForResult(
|
||||
new Intent(getActivity(), ReCaptchaActivity.class),
|
||||
RECAPTCHA_REQUEST);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBlockedByGemaError() {
|
||||
onErrorBlockedByGema();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContentErrorWithMessage(int messageId) {
|
||||
onNotSpecifiedContentErrorWithMessage(messageId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContentError() {
|
||||
onNotSpecifiedContentError();
|
||||
}
|
||||
});
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
rootView = inflater.inflate(R.layout.fragment_videoitem_detail, container, false);
|
||||
progressBar = (ProgressBar) rootView.findViewById(R.id.detail_progress_bar);
|
||||
|
||||
actionBarHandler = new ActionBarHandler(activity);
|
||||
actionBarHandler.setupNavMenu(activity);
|
||||
if(onInvokeCreateOptionsMenuListener != null) {
|
||||
onInvokeCreateOptionsMenuListener.createOptionsMenu();
|
||||
}
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
Activity a = getActivity();
|
||||
infoItemBuilder = new InfoItemBuilder(a, a.findViewById(android.R.id.content));
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT < 18) {
|
||||
playVideoButton = (FloatingActionButton) a.findViewById(R.id.play_video_button);
|
||||
}
|
||||
thumbnailWindowLayout = a.findViewById(R.id.detail_stream_thumbnail_window_layout);
|
||||
Button backgroundButton = (Button)
|
||||
a.findViewById(R.id.detail_stream_thumbnail_window_background_button);
|
||||
|
||||
// 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(backgroundButton != null) {
|
||||
streamingServiceId = getArguments().getInt(NavStack.SERVICE_ID);
|
||||
String videoUrl = getArguments().getString(NavStack.URL);
|
||||
StreamInfoWorker siw = StreamInfoWorker.getInstance();
|
||||
siw.search(streamingServiceId, videoUrl, getActivity());
|
||||
|
||||
autoPlayEnabled = getArguments().getBoolean(AUTO_PLAY);
|
||||
|
||||
if(Build.VERSION.SDK_INT >= 18) {
|
||||
ImageView thumbnailView = (ImageView) activity.findViewById(R.id.detail_thumbnail_view);
|
||||
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.
|
||||
//todo: onLayoutChage sometimes not triggered
|
||||
// background buttons area seem to overlap the thumbnail view
|
||||
// So although you just clicked slightly beneath the thumbnail the action still
|
||||
// gets triggered.
|
||||
@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 void playVideo(final StreamInfo info) {
|
||||
// ----------- THE MAGIC MOMENT ---------------
|
||||
VideoStream selectedVideoStream =
|
||||
info.video_streams.get(actionBarHandler.getSelectedVideoStream());
|
||||
|
||||
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)
|
||||
.setDataAndType(Uri.parse(selectedVideoStream.url),
|
||||
MediaFormat.getMimeById(selectedVideoStream.format))
|
||||
.putExtra(Intent.EXTRA_TITLE, info.title)
|
||||
.putExtra("title", info.title);
|
||||
|
||||
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()
|
||||
.setAction(Intent.ACTION_VIEW)
|
||||
.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) {
|
||||
|
||||
}
|
||||
});
|
||||
builder.create().show();
|
||||
}
|
||||
} else {
|
||||
if (PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.getBoolean(activity.getString(R.string.use_exoplayer_key), false)) {
|
||||
|
||||
// exo player
|
||||
|
||||
if(info.dashMpdUrl != null && !info.dashMpdUrl.isEmpty()) {
|
||||
// try dash
|
||||
Intent intent = new Intent(activity, ExoPlayerActivity.class)
|
||||
.setData(Uri.parse(info.dashMpdUrl))
|
||||
.putExtra(ExoPlayerActivity.CONTENT_TYPE_EXTRA, Util.TYPE_DASH);
|
||||
startActivity(intent);
|
||||
} else if((info.audio_streams != null && !info.audio_streams.isEmpty()) &&
|
||||
(info.video_only_streams != null && !info.video_only_streams.isEmpty())) {
|
||||
// try smooth streaming
|
||||
|
||||
} else {
|
||||
//default streaming
|
||||
Intent intent = new Intent(activity, ExoPlayerActivity.class)
|
||||
.setDataAndType(Uri.parse(selectedVideoStream.url),
|
||||
MediaFormat.getMimeById(selectedVideoStream.format))
|
||||
.putExtra(ExoPlayerActivity.CONTENT_TYPE_EXTRA, Util.TYPE_OTHER);
|
||||
|
||||
activity.startActivity(intent); // HERE !!!
|
||||
}
|
||||
//-------------
|
||||
|
||||
} else {
|
||||
// Internal Player
|
||||
Intent intent = new Intent(activity, PlayVideoActivity.class)
|
||||
.putExtra(PlayVideoActivity.VIDEO_TITLE, info.title)
|
||||
.putExtra(PlayVideoActivity.STREAM_URL, selectedVideoStream.url)
|
||||
.putExtra(PlayVideoActivity.VIDEO_URL, info.webpage_url)
|
||||
.putExtra(PlayVideoActivity.START_POSITION, info.start_position);
|
||||
activity.startActivity(intent); //also HERE !!!
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
actionBarHandler.setupMenu(menu, inflater);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if(!actionBarHandler.onItemSelected(item)) {
|
||||
return super.onOptionsItemSelected(item);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public void setOnInvokeCreateOptionsMenuListener(OnInvokeCreateOptionsMenuListener listener) {
|
||||
this.onInvokeCreateOptionsMenuListener = listener;
|
||||
}
|
||||
|
||||
private void postNewErrorToast(final int stringResource) {
|
||||
Toast.makeText(VideoItemDetailFragment.this.getActivity(),
|
||||
stringResource, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
switch (requestCode) {
|
||||
case RECAPTCHA_REQUEST:
|
||||
if (resultCode == RESULT_OK) {
|
||||
String videoUrl = getArguments().getString(NavStack.URL);
|
||||
StreamInfoWorker siw = StreamInfoWorker.getInstance();
|
||||
siw.search(streamingServiceId, videoUrl, getActivity());
|
||||
} else {
|
||||
Log.d(TAG, "ReCaptcha failed");
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
Log.e(TAG, "Request code from activity not supported [" + requestCode + "]");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.app.NavUtils;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
@@ -26,13 +25,11 @@ import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Vector;
|
||||
|
||||
import us.shandian.giga.service.DownloadManagerService;
|
||||
import us.shandian.giga.ui.fragment.AllMissionsFragment;
|
||||
@@ -64,17 +61,19 @@ public class DownloadActivity extends AppCompatActivity implements AdapterView.O
|
||||
i.setClass(this, DownloadManagerService.class);
|
||||
startService(i);
|
||||
|
||||
ThemeHelper.setTheme(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
ThemeHelper.setTheme(this, true);
|
||||
setContentView(R.layout.activity_downloader);
|
||||
|
||||
//noinspection ConstantConditions
|
||||
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
// its ok if this fails, we will catch that error later, and send it as report
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
actionBar.setTitle(R.string.downloads_title);
|
||||
actionBar.setDisplayShowTitleEnabled(true);
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
actionBar.setTitle(R.string.downloads_title);
|
||||
actionBar.setDisplayShowTitleEnabled(true);
|
||||
}
|
||||
|
||||
mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
|
||||
@@ -159,7 +158,7 @@ public class DownloadActivity extends AppCompatActivity implements AdapterView.O
|
||||
name.setText(getIntent().getStringExtra("fileName"));
|
||||
|
||||
toolbar.setTitle(R.string.add);
|
||||
toolbar.setNavigationIcon(R.drawable.ic_arrow_back_black_24dp);
|
||||
toolbar.setNavigationIcon(ThemeHelper.isLightThemeSelected(this) ? R.drawable.ic_arrow_back_black_24dp : R.drawable.ic_arrow_back_white_24dp);
|
||||
toolbar.inflateMenu(R.menu.dialog_url);
|
||||
|
||||
// Show the dialog
|
||||
@@ -183,7 +182,7 @@ public class DownloadActivity extends AppCompatActivity implements AdapterView.O
|
||||
if (item.getItemId() == R.id.okay) {
|
||||
|
||||
String location;
|
||||
if(audioButton.isChecked()) {
|
||||
if (audioButton.isChecked()) {
|
||||
location = NewPipeSettings.getAudioDownloadPath(DownloadActivity.this);
|
||||
} else {
|
||||
location = NewPipeSettings.getVideoDownloadPath(DownloadActivity.this);
|
||||
@@ -201,7 +200,7 @@ public class DownloadActivity extends AppCompatActivity implements AdapterView.O
|
||||
audioButton.isChecked(), threads.getProgress() + 1);
|
||||
mFragment.notifyChange();
|
||||
|
||||
mPrefs.edit().putInt(THREADS, threads.getProgress() + 1).commit();
|
||||
mPrefs.edit().putInt(THREADS, threads.getProgress() + 1).apply();
|
||||
mPendingUrl = null;
|
||||
dialog.dismiss();
|
||||
}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
package org.schabi.newpipe.download;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
@@ -26,34 +23,32 @@ import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import us.shandian.giga.get.DownloadManager;
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
import us.shandian.giga.service.DownloadManagerService;
|
||||
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 21.09.15.
|
||||
*
|
||||
* <p>
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* DownloadDialog.java is part of NewPipe.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* <p>
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
@@ -71,8 +66,7 @@ public class DownloadDialog extends DialogFragment {
|
||||
|
||||
}
|
||||
|
||||
public static DownloadDialog newInstance(Bundle args)
|
||||
{
|
||||
public static DownloadDialog newInstance(Bundle args) {
|
||||
DownloadDialog dialog = new DownloadDialog();
|
||||
dialog.setArguments(args);
|
||||
dialog.setStyle(DialogFragment.STYLE_NO_TITLE, 0);
|
||||
@@ -100,7 +94,7 @@ public class DownloadDialog extends DialogFragment {
|
||||
final SeekBar threads = (SeekBar) view.findViewById(R.id.threads);
|
||||
|
||||
toolbar.setTitle(R.string.download_dialog_title);
|
||||
toolbar.setNavigationIcon(R.drawable.ic_arrow_back_black_24dp);
|
||||
toolbar.setNavigationIcon(ThemeHelper.isLightThemeSelected(getActivity()) ? R.drawable.ic_arrow_back_black_24dp : R.drawable.ic_arrow_back_white_24dp);
|
||||
toolbar.inflateMenu(R.menu.dialog_url);
|
||||
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
@@ -151,16 +145,16 @@ public class DownloadDialog extends DialogFragment {
|
||||
|
||||
}
|
||||
|
||||
protected void checkDownloadOptions(){
|
||||
protected void checkDownloadOptions() {
|
||||
View view = getView();
|
||||
Bundle arguments = getArguments();
|
||||
RadioButton audioButton = (RadioButton) view.findViewById(R.id.audio_button);
|
||||
RadioButton videoButton = (RadioButton) view.findViewById(R.id.video_button);
|
||||
|
||||
if(arguments.getString(AUDIO_URL) == null) {
|
||||
if (arguments.getString(AUDIO_URL) == null) {
|
||||
audioButton.setVisibility(View.GONE);
|
||||
videoButton.setChecked(true);
|
||||
} else if(arguments.getString(VIDEO_URL) == null) {
|
||||
} else if (arguments.getString(VIDEO_URL) == null) {
|
||||
videoButton.setVisibility(View.GONE);
|
||||
audioButton.setChecked(true);
|
||||
}
|
||||
@@ -169,11 +163,11 @@ public class DownloadDialog extends DialogFragment {
|
||||
/**
|
||||
* #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<> ();
|
||||
List<String> forbiddenCharsPatterns = new ArrayList<>();
|
||||
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
|
||||
@@ -186,8 +180,7 @@ public class DownloadDialog extends DialogFragment {
|
||||
|
||||
|
||||
//download audio, video or both?
|
||||
private void download()
|
||||
{
|
||||
private void download() {
|
||||
View view = getView();
|
||||
Bundle arguments = getArguments();
|
||||
final EditText name = (EditText) view.findViewById(R.id.file_name);
|
||||
@@ -199,7 +192,7 @@ public class DownloadDialog extends DialogFragment {
|
||||
|
||||
boolean isAudio = audioButton.isChecked();
|
||||
String url, location, filename;
|
||||
if(isAudio) {
|
||||
if (isAudio) {
|
||||
url = arguments.getString(AUDIO_URL);
|
||||
location = NewPipeSettings.getAudioDownloadPath(getContext());
|
||||
filename = fName + arguments.getString(FILE_SUFFIX_AUDIO);
|
||||
@@ -218,11 +211,11 @@ public class DownloadDialog extends DialogFragment {
|
||||
private void download(String url, String title,
|
||||
String fileSuffix, File downloadDir, Context context) {
|
||||
|
||||
File saveFilePath = new File(downloadDir,createFileName(title) + fileSuffix);
|
||||
File saveFilePath = new File(downloadDir, createFileName(title) + fileSuffix);
|
||||
|
||||
long id = 0;
|
||||
|
||||
Log.i(TAG,"Started downloading '" + url +
|
||||
Log.i(TAG, "Started downloading '" + url +
|
||||
"' => '" + saveFilePath + "' #" + id);
|
||||
|
||||
if (App.isUsingTor()) {
|
||||
|
||||
179
app/src/main/java/org/schabi/newpipe/fragments/BaseFragment.java
Normal file
@@ -0,0 +1,179 @@
|
||||
package org.schabi.newpipe.fragments;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.AttrRes;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
public abstract class BaseFragment extends Fragment {
|
||||
protected final String TAG = "BaseFragment@" + Integer.toHexString(hashCode());
|
||||
protected static final boolean DEBUG = MainActivity.DEBUG;
|
||||
|
||||
protected AppCompatActivity activity;
|
||||
protected OnItemSelectedListener onItemSelectedListener;
|
||||
|
||||
protected AtomicBoolean isLoading = new AtomicBoolean(false);
|
||||
protected AtomicBoolean wasLoading = new AtomicBoolean(false);
|
||||
|
||||
protected static final ImageLoader imageLoader = ImageLoader.getInstance();
|
||||
protected static final DisplayImageOptions displayImageOptions =
|
||||
new DisplayImageOptions.Builder().displayer(new FadeInBitmapDisplayer(400)).cacheInMemory(false).build();
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected Toolbar toolbar;
|
||||
|
||||
protected View errorPanel;
|
||||
protected Button errorButtonRetry;
|
||||
protected TextView errorTextView;
|
||||
protected ProgressBar loadingProgressBar;
|
||||
//protected SwipeRefreshLayout swipeRefreshLayout;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Fragment's Lifecycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
if (DEBUG) Log.d(TAG, "onAttach() called with: context = [" + context + "]");
|
||||
|
||||
activity = (AppCompatActivity) context;
|
||||
onItemSelectedListener = (OnItemSelectedListener) context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
||||
|
||||
isLoading.set(false);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View rootView, Bundle savedInstanceState) {
|
||||
if (DEBUG) Log.d(TAG, "onViewCreated() called with: rootView = [" + rootView + "], savedInstanceState = [" + savedInstanceState + "]");
|
||||
initViews(rootView, savedInstanceState);
|
||||
initListeners();
|
||||
wasLoading.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
if (DEBUG) Log.d(TAG, "onDestroyView() called");
|
||||
toolbar = null;
|
||||
|
||||
errorPanel = null;
|
||||
errorButtonRetry = null;
|
||||
errorTextView = null;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
toolbar = (Toolbar) activity.findViewById(R.id.toolbar);
|
||||
|
||||
loadingProgressBar = (ProgressBar) rootView.findViewById(R.id.loading_progress_bar);
|
||||
//swipeRefreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.swipe_refresh);
|
||||
|
||||
errorPanel = rootView.findViewById(R.id.error_panel);
|
||||
errorButtonRetry = (Button) rootView.findViewById(R.id.error_button_retry);
|
||||
errorTextView = (TextView) rootView.findViewById(R.id.error_message_view);
|
||||
}
|
||||
|
||||
protected void initListeners() {
|
||||
errorButtonRetry.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
onRetryButtonClicked();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected abstract void reloadContent();
|
||||
|
||||
protected void onRetryButtonClicked() {
|
||||
if (DEBUG) Log.d(TAG, "onRetryButtonClicked() called");
|
||||
reloadContent();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected void setErrorMessage(String message, boolean showRetryButton) {
|
||||
if (errorTextView == null || activity == null) return;
|
||||
|
||||
errorTextView.setText(message);
|
||||
if (showRetryButton) animateView(errorButtonRetry, true, 300);
|
||||
else animateView(errorButtonRetry, false, 0);
|
||||
|
||||
animateView(errorPanel, true, 300);
|
||||
isLoading.set(false);
|
||||
|
||||
animateView(loadingProgressBar, false, 200);
|
||||
}
|
||||
|
||||
protected int getResourceIdFromAttr(@AttrRes int attr) {
|
||||
TypedArray a = activity.getTheme().obtainStyledAttributes(new int[]{attr});
|
||||
int attributeResourceId = a.getResourceId(0, 0);
|
||||
a.recycle();
|
||||
return attributeResourceId;
|
||||
}
|
||||
|
||||
public static void showMenuTooltip(View v, String message) {
|
||||
final int[] screenPos = new int[2];
|
||||
final Rect displayFrame = new Rect();
|
||||
v.getLocationOnScreen(screenPos);
|
||||
v.getWindowVisibleDisplayFrame(displayFrame);
|
||||
|
||||
final Context context = v.getContext();
|
||||
final int width = v.getWidth();
|
||||
final int height = v.getHeight();
|
||||
final int midy = screenPos[1] + height / 2;
|
||||
int referenceX = screenPos[0] + width / 2;
|
||||
if (ViewCompat.getLayoutDirection(v) == View.LAYOUT_DIRECTION_LTR) {
|
||||
final int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
|
||||
referenceX = screenWidth - referenceX; // mirror
|
||||
}
|
||||
Toast cheatSheet = Toast.makeText(context, message, Toast.LENGTH_SHORT);
|
||||
if (midy < displayFrame.height()) {
|
||||
// Show along the top; follow action buttons
|
||||
cheatSheet.setGravity(Gravity.TOP | Gravity.END, referenceX,
|
||||
screenPos[1] + height - displayFrame.top);
|
||||
} else {
|
||||
// Show along the bottom center
|
||||
cheatSheet.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, height);
|
||||
}
|
||||
cheatSheet.show();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package org.schabi.newpipe.fragments;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
|
||||
public class MainFragment extends Fragment {
|
||||
private final String TAG = "MainFragment@" + Integer.toHexString(hashCode());
|
||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||
|
||||
private AppCompatActivity activity;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Fragment's LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
if (DEBUG) Log.d(TAG, "onAttach() called with: context = [" + context + "]");
|
||||
activity = ((AppCompatActivity) context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
if (DEBUG) Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]");
|
||||
return inflater.inflate(R.layout.fragment_main, container, false);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||
inflater.inflate(R.menu.main_fragment_menu, menu);
|
||||
|
||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if (supportActionBar != null) {
|
||||
supportActionBar.setDisplayShowTitleEnabled(false);
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_search:
|
||||
NavigationHelper.openSearch(activity, 0, "");
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.schabi.newpipe.fragments;
|
||||
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
|
||||
/**
|
||||
* Interface for communication purposes between activity and fragment
|
||||
*/
|
||||
public interface OnItemSelectedListener {
|
||||
void onItemSelected(StreamingService.LinkType linkType, int serviceId, String url, String name);
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
package org.schabi.newpipe.fragments.channel;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.ImageErrorLoadingListener;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.fragments.BaseFragment;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.info_list.InfoListAdapter;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.workers.ChannelExtractorWorker;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
public class ChannelFragment extends BaseFragment implements ChannelExtractorWorker.OnChannelInfoReceive {
|
||||
private final String TAG = "ChannelFragment@" + Integer.toHexString(hashCode());
|
||||
|
||||
private static final String INFO_LIST_KEY = "info_list_key";
|
||||
private static final String CHANNEL_INFO_KEY = "channel_info_key";
|
||||
private static final String PAGE_NUMBER_KEY = "page_number_key";
|
||||
|
||||
private InfoListAdapter infoListAdapter;
|
||||
|
||||
private ChannelExtractorWorker currentChannelWorker;
|
||||
private ChannelInfo currentChannelInfo;
|
||||
private int serviceId = -1;
|
||||
private String channelName = "";
|
||||
private String channelUrl = "";
|
||||
private int pageNumber = 0;
|
||||
private boolean hasNextPage = true;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private RecyclerView channelVideosList;
|
||||
|
||||
private View headerRootLayout;
|
||||
private ImageView headerChannelBanner;
|
||||
private ImageView headerAvatarView;
|
||||
private TextView headerTitleView;
|
||||
private TextView headerSubscribersTextView;
|
||||
private Button headerRssButton;
|
||||
|
||||
/*////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public ChannelFragment() {
|
||||
}
|
||||
|
||||
public static Fragment getInstance(int serviceId, String channelUrl, String name) {
|
||||
ChannelFragment instance = new ChannelFragment();
|
||||
instance.setChannel(serviceId, channelUrl, name);
|
||||
return instance;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Fragment's LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
if (savedInstanceState != null) {
|
||||
channelUrl = savedInstanceState.getString(Constants.KEY_URL);
|
||||
channelName = savedInstanceState.getString(Constants.KEY_TITLE);
|
||||
serviceId = savedInstanceState.getInt(Constants.KEY_SERVICE_ID, -1);
|
||||
|
||||
pageNumber = savedInstanceState.getInt(PAGE_NUMBER_KEY, 0);
|
||||
Serializable serializable = savedInstanceState.getSerializable(CHANNEL_INFO_KEY);
|
||||
if (serializable instanceof ChannelInfo) currentChannelInfo = (ChannelInfo) serializable;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
if (DEBUG) Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]");
|
||||
return inflater.inflate(R.layout.fragment_channel, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
if (currentChannelInfo == null) loadPage(0);
|
||||
else handleChannelInfo(currentChannelInfo, false, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
if (DEBUG) Log.d(TAG, "onDestroyView() called");
|
||||
headerAvatarView.setImageBitmap(null);
|
||||
headerChannelBanner.setImageBitmap(null);
|
||||
channelVideosList.removeAllViews();
|
||||
|
||||
channelVideosList = null;
|
||||
headerRootLayout = null;
|
||||
headerChannelBanner = null;
|
||||
headerAvatarView = null;
|
||||
headerTitleView = null;
|
||||
headerSubscribersTextView = null;
|
||||
headerRssButton = null;
|
||||
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
if (DEBUG) Log.d(TAG, "onResume() called");
|
||||
super.onResume();
|
||||
if (wasLoading.getAndSet(false) && (currentChannelWorker == null || !currentChannelWorker.isRunning())) {
|
||||
loadPage(pageNumber);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
if (DEBUG) Log.d(TAG, "onStop() called");
|
||||
super.onStop();
|
||||
wasLoading.set(currentChannelWorker != null && currentChannelWorker.isRunning());
|
||||
if (currentChannelWorker != null && currentChannelWorker.isRunning()) currentChannelWorker.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
if (DEBUG) Log.d(TAG, "onSaveInstanceState() called with: outState = [" + outState + "]");
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putString(Constants.KEY_URL, channelUrl);
|
||||
outState.putString(Constants.KEY_TITLE, channelName);
|
||||
outState.putInt(Constants.KEY_SERVICE_ID, serviceId);
|
||||
|
||||
outState.putSerializable(INFO_LIST_KEY, ((ArrayList<InfoItem>) infoListAdapter.getItemsList()));
|
||||
outState.putSerializable(CHANNEL_INFO_KEY, currentChannelInfo);
|
||||
outState.putInt(PAGE_NUMBER_KEY, pageNumber);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
inflater.inflate(R.menu.menu_channel, menu);
|
||||
|
||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if (supportActionBar != null) {
|
||||
supportActionBar.setDisplayShowTitleEnabled(true);
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (DEBUG) Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]");
|
||||
super.onOptionsItemSelected(item);
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_item_openInBrowser: {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(channelUrl));
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.choose_browser)));
|
||||
return true;
|
||||
}
|
||||
case R.id.menu_item_share:
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_SEND);
|
||||
intent.putExtra(Intent.EXTRA_TEXT, channelUrl);
|
||||
intent.setType("text/plain");
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title)));
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init's
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
channelVideosList = (RecyclerView) rootView.findViewById(R.id.channel_streams_view);
|
||||
|
||||
channelVideosList.setLayoutManager(new LinearLayoutManager(activity));
|
||||
if (infoListAdapter == null) {
|
||||
infoListAdapter = new InfoListAdapter(activity, rootView);
|
||||
if (savedInstanceState != null) {
|
||||
//noinspection unchecked
|
||||
ArrayList<InfoItem> serializable = (ArrayList<InfoItem>) savedInstanceState.getSerializable(INFO_LIST_KEY);
|
||||
infoListAdapter.addInfoItemList(serializable);
|
||||
}
|
||||
}
|
||||
|
||||
channelVideosList.setAdapter(infoListAdapter);
|
||||
headerRootLayout = activity.getLayoutInflater().inflate(R.layout.channel_header, channelVideosList, false);
|
||||
infoListAdapter.setHeader(headerRootLayout);
|
||||
infoListAdapter.setFooter(activity.getLayoutInflater().inflate(R.layout.pignate_footer, channelVideosList, false));
|
||||
|
||||
headerChannelBanner = (ImageView) headerRootLayout.findViewById(R.id.channel_banner_image);
|
||||
headerAvatarView = (ImageView) headerRootLayout.findViewById(R.id.channel_avatar_view);
|
||||
headerTitleView = (TextView) headerRootLayout.findViewById(R.id.channel_title_view);
|
||||
headerSubscribersTextView = (TextView) headerRootLayout.findViewById(R.id.channel_subscriber_view);
|
||||
headerRssButton = (Button) headerRootLayout.findViewById(R.id.channel_rss_button);
|
||||
}
|
||||
|
||||
protected void initListeners() {
|
||||
super.initListeners();
|
||||
|
||||
infoListAdapter.setOnStreamInfoItemSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() {
|
||||
@Override
|
||||
public void selected(int serviceId, String url, String title) {
|
||||
if (DEBUG) Log.d(TAG, "selected() called with: serviceId = [" + serviceId + "], url = [" + url + "], title = [" + title + "]");
|
||||
NavigationHelper.openVideoDetail(onItemSelectedListener, serviceId, url, title);
|
||||
}
|
||||
});
|
||||
|
||||
channelVideosList.clearOnScrollListeners();
|
||||
channelVideosList.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
|
||||
int pastVisiblesItems, visibleItemCount, totalItemCount;
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
//check for scroll down
|
||||
if (dy > 0) {
|
||||
LinearLayoutManager layoutManager = (LinearLayoutManager) channelVideosList.getLayoutManager();
|
||||
|
||||
visibleItemCount = layoutManager.getChildCount();
|
||||
totalItemCount = layoutManager.getItemCount();
|
||||
pastVisiblesItems = layoutManager.findFirstVisibleItemPosition();
|
||||
|
||||
if ((visibleItemCount + pastVisiblesItems) >= totalItemCount && (currentChannelWorker == null || !currentChannelWorker.isRunning()) && hasNextPage && !isLoading.get()) {
|
||||
pageNumber++;
|
||||
loadMoreVideos();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
headerRssButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (DEBUG) Log.d(TAG, "onClick() called with: view = [" + view + "] feed url > " + currentChannelInfo.feed_url);
|
||||
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(currentChannelInfo.feed_url));
|
||||
startActivity(i);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void reloadContent() {
|
||||
if (DEBUG) Log.d(TAG, "reloadContent() called");
|
||||
currentChannelInfo = null;
|
||||
infoListAdapter.clearStreamItemList();
|
||||
loadPage(0);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private String buildSubscriberString(long count) {
|
||||
String out = NumberFormat.getNumberInstance().format(count);
|
||||
out += " " + getString(count > 1 ? R.string.subscriber_plural : R.string.subscriber);
|
||||
return out;
|
||||
}
|
||||
|
||||
private void loadPage(int page) {
|
||||
if (DEBUG) Log.d(TAG, "loadPage() called with: page = [" + page + "]");
|
||||
if (currentChannelWorker != null && currentChannelWorker.isRunning()) currentChannelWorker.cancel();
|
||||
isLoading.set(true);
|
||||
pageNumber = page;
|
||||
infoListAdapter.showFooter(false);
|
||||
|
||||
animateView(loadingProgressBar, true, 200);
|
||||
animateView(errorPanel, false, 200);
|
||||
|
||||
imageLoader.cancelDisplayTask(headerChannelBanner);
|
||||
imageLoader.cancelDisplayTask(headerAvatarView);
|
||||
|
||||
headerRssButton.setVisibility(View.GONE);
|
||||
headerSubscribersTextView.setVisibility(View.GONE);
|
||||
|
||||
headerTitleView.setText(channelName != null ? channelName : "");
|
||||
headerChannelBanner.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.channel_banner));
|
||||
headerAvatarView.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.buddy));
|
||||
if (activity.getSupportActionBar() != null) activity.getSupportActionBar().setTitle(channelName != null ? channelName : "");
|
||||
|
||||
currentChannelWorker = new ChannelExtractorWorker(activity, serviceId, channelUrl, page, false, this);
|
||||
currentChannelWorker.start();
|
||||
}
|
||||
|
||||
private void loadMoreVideos() {
|
||||
if (DEBUG) Log.d(TAG, "loadMoreVideos() called");
|
||||
if (currentChannelWorker != null && currentChannelWorker.isRunning()) currentChannelWorker.cancel();
|
||||
isLoading.set(true);
|
||||
currentChannelWorker = new ChannelExtractorWorker(activity, serviceId, channelUrl, pageNumber, true, this);
|
||||
currentChannelWorker.start();
|
||||
}
|
||||
|
||||
private void setChannel(int serviceId, String channelUrl, String name) {
|
||||
this.serviceId = serviceId;
|
||||
this.channelUrl = channelUrl;
|
||||
this.channelName = name;
|
||||
}
|
||||
|
||||
private void handleChannelInfo(ChannelInfo info, boolean onlyVideos, boolean addVideos) {
|
||||
currentChannelInfo = info;
|
||||
|
||||
animateView(errorPanel, false, 300);
|
||||
animateView(channelVideosList, true, 200);
|
||||
animateView(loadingProgressBar, false, 200);
|
||||
|
||||
if (!onlyVideos) {
|
||||
headerRootLayout.setVisibility(View.VISIBLE);
|
||||
//animateView(loadingProgressBar, false, 200, null);
|
||||
|
||||
if (!TextUtils.isEmpty(info.channel_name)) {
|
||||
if (activity.getSupportActionBar() != null) activity.getSupportActionBar().setTitle(info.channel_name);
|
||||
headerTitleView.setText(info.channel_name);
|
||||
channelName = info.channel_name;
|
||||
} else channelName = "";
|
||||
|
||||
if (!TextUtils.isEmpty(info.banner_url)) {
|
||||
imageLoader.displayImage(info.banner_url, headerChannelBanner, displayImageOptions, new ImageErrorLoadingListener(activity, getView(), info.service_id));
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(info.avatar_url)) {
|
||||
headerAvatarView.setVisibility(View.VISIBLE);
|
||||
imageLoader.displayImage(info.avatar_url, headerAvatarView, displayImageOptions, new ImageErrorLoadingListener(activity, getView(), info.service_id));
|
||||
}
|
||||
|
||||
if (info.subscriberCount != -1) {
|
||||
headerSubscribersTextView.setText(buildSubscriberString(info.subscriberCount));
|
||||
headerSubscribersTextView.setVisibility(View.VISIBLE);
|
||||
} else headerSubscribersTextView.setVisibility(View.GONE);
|
||||
|
||||
if (!TextUtils.isEmpty(info.feed_url)) headerRssButton.setVisibility(View.VISIBLE);
|
||||
else headerRssButton.setVisibility(View.INVISIBLE);
|
||||
|
||||
infoListAdapter.showFooter(true);
|
||||
}
|
||||
|
||||
hasNextPage = info.hasNextPage;
|
||||
if (!hasNextPage) infoListAdapter.showFooter(false);
|
||||
|
||||
//if (!listRestored) {
|
||||
if (addVideos) infoListAdapter.addInfoItemList(info.related_streams);
|
||||
//}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setErrorMessage(String message, boolean showRetryButton) {
|
||||
super.setErrorMessage(message, showRetryButton);
|
||||
|
||||
animateView(channelVideosList, false, 200);
|
||||
currentChannelInfo = null;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// OnChannelInfoReceiveListener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onReceive(ChannelInfo info, boolean onlyVideos) {
|
||||
if (DEBUG) Log.d(TAG, "onReceive() called with: info = [" + info + "]");
|
||||
if (info == null || isRemoving() || !isVisible()) return;
|
||||
|
||||
handleChannelInfo(info, onlyVideos, true);
|
||||
isLoading.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(int messageId) {
|
||||
if (DEBUG) Log.d(TAG, "onError() called with: messageId = [" + messageId + "]");
|
||||
setErrorMessage(getString(messageId), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnrecoverableError(Exception exception) {
|
||||
if (DEBUG) Log.d(TAG, "onUnrecoverableError() called with: exception = [" + exception + "]");
|
||||
activity.finish();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package org.schabi.newpipe.fragments.detail;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
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.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Spinner;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.stream_info.VideoStream;
|
||||
import org.schabi.newpipe.util.Utils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 18.08.15.
|
||||
* <p>
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* DetailsMenuHandler.java is part of NewPipe.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
class ActionBarHandler {
|
||||
private static final String TAG = "ActionBarHandler";
|
||||
|
||||
private AppCompatActivity activity;
|
||||
private int selectedVideoStream = -1;
|
||||
|
||||
private SharedPreferences defaultPreferences;
|
||||
|
||||
private Menu menu;
|
||||
|
||||
// Only callbacks are listed here, there are more actions which don't need a callback.
|
||||
// those are edited directly. Typically VideoDetailFragment will implement those callbacks.
|
||||
private OnActionListener onShareListener;
|
||||
private OnActionListener onOpenInBrowserListener;
|
||||
private OnActionListener onDownloadListener;
|
||||
private OnActionListener onPlayWithKodiListener;
|
||||
|
||||
// Triggered when a stream related action is triggered.
|
||||
public interface OnActionListener {
|
||||
void onActionSelected(int selectedStreamId);
|
||||
}
|
||||
|
||||
public ActionBarHandler(AppCompatActivity activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
public void setupStreamList(final List<VideoStream> videoStreams, Spinner toolbarSpinner) {
|
||||
if (activity == null) return;
|
||||
selectedVideoStream = 0;
|
||||
|
||||
// this array will be shown in the dropdown menu for selecting the stream/resolution.
|
||||
String[] itemArray = new String[videoStreams.size()];
|
||||
for (int i = 0; i < videoStreams.size(); i++) {
|
||||
VideoStream item = videoStreams.get(i);
|
||||
itemArray[i] = MediaFormat.getNameById(item.format) + " " + item.resolution;
|
||||
}
|
||||
|
||||
int defaultResolutionIndex = Utils.getDefaultResolution(activity, videoStreams);
|
||||
ArrayAdapter<String> itemAdapter = new ArrayAdapter<>(activity.getBaseContext(), android.R.layout.simple_spinner_dropdown_item, itemArray);
|
||||
toolbarSpinner.setAdapter(itemAdapter);
|
||||
toolbarSpinner.setSelection(defaultResolutionIndex);
|
||||
toolbarSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
selectedVideoStream = position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public void setupMenu(Menu menu, MenuInflater inflater) {
|
||||
this.menu = menu;
|
||||
|
||||
// CAUTION set item properties programmatically otherwise it would not be accepted by
|
||||
// appcompat itemsinflater.inflate(R.menu.videoitem_detail, menu);
|
||||
|
||||
defaultPreferences = PreferenceManager.getDefaultSharedPreferences(activity);
|
||||
inflater.inflate(R.menu.video_detail_menu, menu);
|
||||
|
||||
updateItemsVisibility();
|
||||
}
|
||||
|
||||
public void updateItemsVisibility(){
|
||||
showPlayWithKodiAction(defaultPreferences.getBoolean(activity.getString(R.string.show_play_with_kodi_key), false));
|
||||
}
|
||||
|
||||
public boolean onItemSelected(MenuItem item) {
|
||||
int id = item.getItemId();
|
||||
switch (id) {
|
||||
case R.id.menu_item_share: {
|
||||
if (onShareListener != null) {
|
||||
onShareListener.onActionSelected(selectedVideoStream);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case R.id.menu_item_openInBrowser: {
|
||||
if (onOpenInBrowserListener != null) {
|
||||
onOpenInBrowserListener.onActionSelected(selectedVideoStream);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case R.id.menu_item_download:
|
||||
if (onDownloadListener != null) {
|
||||
onDownloadListener.onActionSelected(selectedVideoStream);
|
||||
}
|
||||
return true;
|
||||
case R.id.action_play_with_kodi:
|
||||
if (onPlayWithKodiListener != null) {
|
||||
onPlayWithKodiListener.onActionSelected(selectedVideoStream);
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
Log.e(TAG, "Menu Item not known");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public int getSelectedVideoStream() {
|
||||
return selectedVideoStream;
|
||||
}
|
||||
|
||||
public void setOnShareListener(OnActionListener listener) {
|
||||
onShareListener = listener;
|
||||
}
|
||||
|
||||
public void setOnOpenInBrowserListener(OnActionListener listener) {
|
||||
onOpenInBrowserListener = listener;
|
||||
}
|
||||
|
||||
public void setOnDownloadListener(OnActionListener listener) {
|
||||
onDownloadListener = listener;
|
||||
}
|
||||
|
||||
public void setOnPlayWithKodiListener(OnActionListener listener) {
|
||||
onPlayWithKodiListener = listener;
|
||||
}
|
||||
|
||||
public void showDownloadAction(boolean visible) {
|
||||
menu.findItem(R.id.menu_item_download).setVisible(visible);
|
||||
}
|
||||
|
||||
public void showPlayWithKodiAction(boolean visible) {
|
||||
menu.findItem(R.id.action_play_with_kodi).setVisible(visible);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package org.schabi.newpipe.fragments.detail;
|
||||
|
||||
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class StackItem implements Serializable {
|
||||
private String title, url;
|
||||
private StreamInfo info;
|
||||
|
||||
public StackItem(String url, String title) {
|
||||
this.title = title;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setInfo(StreamInfo info) {
|
||||
this.info = info;
|
||||
}
|
||||
|
||||
public StreamInfo getInfo() {
|
||||
return info;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getUrl() + " > " + getTitle();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package org.schabi.newpipe.fragments.detail;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class StreamInfoCache {
|
||||
private static String TAG = "StreamInfoCache@";
|
||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||
private static final StreamInfoCache instance = new StreamInfoCache();
|
||||
private static final int MAX_ITEMS_ON_CACHE = 20;
|
||||
|
||||
private final LinkedHashMap<String, StreamInfo> myCache = new LinkedHashMap<>();
|
||||
|
||||
private StreamInfoCache() {
|
||||
TAG += "" + Integer.toHexString(hashCode());
|
||||
}
|
||||
|
||||
public static StreamInfoCache getInstance() {
|
||||
if (DEBUG) Log.d(TAG, "getInstance() called");
|
||||
return instance;
|
||||
}
|
||||
|
||||
public boolean hasKey(@NonNull String url) {
|
||||
if (DEBUG) Log.d(TAG, "hasKey() called with: url = [" + url + "]");
|
||||
return !TextUtils.isEmpty(url) && myCache.containsKey(url) && myCache.get(url) != null;
|
||||
}
|
||||
|
||||
public StreamInfo getFromKey(@NonNull String url) {
|
||||
if (DEBUG) Log.d(TAG, "getFromKey() called with: url = [" + url + "]");
|
||||
return myCache.get(url);
|
||||
}
|
||||
|
||||
public void putInfo(@NonNull StreamInfo info) {
|
||||
if (DEBUG) Log.d(TAG, "putInfo() called with: info = [" + info + "]");
|
||||
putInfo(info.webpage_url, info);
|
||||
}
|
||||
|
||||
public void putInfo(@NonNull String url, @NonNull StreamInfo info) {
|
||||
if (DEBUG) Log.d(TAG, "putInfo() called with: url = [" + url + "], info = [" + info + "]");
|
||||
myCache.put(url, info);
|
||||
}
|
||||
|
||||
public void removeInfo(@NonNull StreamInfo info) {
|
||||
if (DEBUG) Log.d(TAG, "removeInfo() called with: info = [" + info + "]");
|
||||
myCache.remove(info.webpage_url);
|
||||
}
|
||||
|
||||
public void removeInfo(@NonNull String url) {
|
||||
if (DEBUG) Log.d(TAG, "removeInfo() called with: url = [" + url + "]");
|
||||
myCache.remove(url);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public void clearCache() {
|
||||
if (DEBUG) Log.d(TAG, "clearCache() called");
|
||||
myCache.clear();
|
||||
}
|
||||
|
||||
public void removeOldEntries() {
|
||||
if (DEBUG) Log.d(TAG, "removeOldEntries() called , size = " + getSize());
|
||||
if (getSize() > MAX_ITEMS_ON_CACHE) {
|
||||
Iterator<String> iterator = myCache.keySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
iterator.next();
|
||||
iterator.remove();
|
||||
if (DEBUG) Log.d(TAG, "getSize() = " + getSize());
|
||||
if (getSize() <= MAX_ITEMS_ON_CACHE) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int getSize() {
|
||||
return myCache.size();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,615 @@
|
||||
package org.schabi.newpipe.fragments.search;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AutoCompleteTextView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.search.SearchEngine;
|
||||
import org.schabi.newpipe.extractor.search.SearchResult;
|
||||
import org.schabi.newpipe.fragments.BaseFragment;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.info_list.InfoListAdapter;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.workers.SearchWorker;
|
||||
import org.schabi.newpipe.workers.SuggestionWorker;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
public class SearchFragment extends BaseFragment implements SuggestionWorker.OnSuggestionResult, SearchWorker.OnSearchResult {
|
||||
private final String TAG = "SearchFragment@" + Integer.toHexString(hashCode());
|
||||
// savedInstanceBundle arguments
|
||||
private static final String QUERY_KEY = "query_key";
|
||||
private static final String PAGE_NUMBER_KEY = "page_number_key";
|
||||
private static final String INFO_LIST_KEY = "info_list_key";
|
||||
private static final String WAS_LOADING_KEY = "was_loading_key";
|
||||
private static final String ERROR_KEY = "error_key";
|
||||
private static final String FILTER_CHECKED_ID_KEY = "filter_checked_id_key";
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Search
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private int filterItemCheckedId = -1;
|
||||
private EnumSet<SearchEngine.Filter> filter = EnumSet.of(SearchEngine.Filter.CHANNEL, SearchEngine.Filter.STREAM);
|
||||
|
||||
private int serviceId = -1;
|
||||
private String searchQuery = "";
|
||||
private int pageNumber = 0;
|
||||
|
||||
private SearchWorker curSearchWorker;
|
||||
private SuggestionWorker curSuggestionWorker;
|
||||
private SuggestionListAdapter suggestionListAdapter;
|
||||
private InfoListAdapter infoListAdapter;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private View searchToolbarContainer;
|
||||
private AutoCompleteTextView searchEditText;
|
||||
private View searchClear;
|
||||
|
||||
private RecyclerView resultRecyclerView;
|
||||
|
||||
/*////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public static SearchFragment getInstance(int serviceId, String query) {
|
||||
SearchFragment searchFragment = new SearchFragment();
|
||||
searchFragment.setQuery(serviceId, query);
|
||||
return searchFragment;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Fragment's LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
||||
setHasOptionsMenu(true);
|
||||
if (savedInstanceState != null) {
|
||||
searchQuery = savedInstanceState.getString(QUERY_KEY);
|
||||
serviceId = savedInstanceState.getInt(Constants.KEY_SERVICE_ID, 0);
|
||||
pageNumber = savedInstanceState.getInt(PAGE_NUMBER_KEY, 0);
|
||||
wasLoading.set(savedInstanceState.getBoolean(WAS_LOADING_KEY, false));
|
||||
filterItemCheckedId = savedInstanceState.getInt(FILTER_CHECKED_ID_KEY, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
if (DEBUG) Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]");
|
||||
return inflater.inflate(R.layout.fragment_search, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View rootView, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
if (DEBUG) Log.d(TAG, "onViewCreated() called with: rootView = [" + rootView + "], savedInstanceState = [" + savedInstanceState + "]");
|
||||
|
||||
if (savedInstanceState != null && savedInstanceState.getBoolean(ERROR_KEY, false)) {
|
||||
search(searchQuery, 0, true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (DEBUG) Log.d(TAG, "onResume() called");
|
||||
if (wasLoading.getAndSet(false) && !TextUtils.isEmpty(searchQuery)) {
|
||||
if (pageNumber > 0) search(searchQuery, pageNumber);
|
||||
else search(searchQuery, 0, true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
if (DEBUG) Log.d(TAG, "onStop() called");
|
||||
|
||||
hideSoftKeyboard(searchEditText);
|
||||
|
||||
wasLoading.set(curSearchWorker != null && curSearchWorker.isRunning());
|
||||
if (curSearchWorker != null && curSearchWorker.isRunning()) curSearchWorker.cancel();
|
||||
if (curSuggestionWorker != null && curSuggestionWorker.isRunning()) curSuggestionWorker.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
if (DEBUG) Log.d(TAG, "onDestroyView() called");
|
||||
unsetSearchListeners();
|
||||
|
||||
resultRecyclerView.removeAllViews();
|
||||
|
||||
searchToolbarContainer = null;
|
||||
searchEditText = null;
|
||||
searchClear = null;
|
||||
|
||||
resultRecyclerView = null;
|
||||
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
if (DEBUG) Log.d(TAG, "onSaveInstanceState() called with: outState = [" + outState + "]");
|
||||
|
||||
String query = searchEditText != null && !TextUtils.isEmpty(searchEditText.getText().toString())
|
||||
? searchEditText.getText().toString() : searchQuery;
|
||||
outState.putString(QUERY_KEY, query);
|
||||
outState.putInt(Constants.KEY_SERVICE_ID, serviceId);
|
||||
outState.putInt(PAGE_NUMBER_KEY, pageNumber);
|
||||
outState.putSerializable(INFO_LIST_KEY, ((ArrayList<InfoItem>) infoListAdapter.getItemsList()));
|
||||
outState.putBoolean(WAS_LOADING_KEY, curSearchWorker != null && curSearchWorker.isRunning());
|
||||
|
||||
if (errorPanel != null && errorPanel.getVisibility() == View.VISIBLE) outState.putBoolean(ERROR_KEY, true);
|
||||
if (filterItemCheckedId != -1) outState.putInt(FILTER_CHECKED_ID_KEY, filterItemCheckedId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
switch (requestCode) {
|
||||
case ReCaptchaActivity.RECAPTCHA_REQUEST:
|
||||
if (resultCode == Activity.RESULT_OK && searchQuery.length() != 0) {
|
||||
search(searchQuery, pageNumber, true);
|
||||
} else Log.e(TAG, "ReCaptcha failed");
|
||||
break;
|
||||
|
||||
default:
|
||||
Log.e(TAG, "Request code from activity not supported [" + requestCode + "]");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init's
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
resultRecyclerView = ((RecyclerView) rootView.findViewById(R.id.result_list_view));
|
||||
resultRecyclerView.setLayoutManager(new LinearLayoutManager(activity));
|
||||
|
||||
if (infoListAdapter == null) {
|
||||
infoListAdapter = new InfoListAdapter(getActivity(), getActivity().findViewById(android.R.id.content));
|
||||
if (savedInstanceState != null) {
|
||||
//noinspection unchecked
|
||||
ArrayList<InfoItem> serializable = (ArrayList<InfoItem>) savedInstanceState.getSerializable(INFO_LIST_KEY);
|
||||
infoListAdapter.addInfoItemList(serializable);
|
||||
}
|
||||
|
||||
infoListAdapter.setFooter(activity.getLayoutInflater().inflate(R.layout.pignate_footer, resultRecyclerView, false));
|
||||
infoListAdapter.showFooter(false);
|
||||
infoListAdapter.setOnStreamInfoItemSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() {
|
||||
@Override
|
||||
public void selected(int serviceId, String url, String title) {
|
||||
NavigationHelper.openVideoDetail(onItemSelectedListener, serviceId, url, title);
|
||||
}
|
||||
});
|
||||
infoListAdapter.setOnChannelInfoItemSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() {
|
||||
@Override
|
||||
public void selected(int serviceId, String url, String title) {
|
||||
NavigationHelper.openChannel(onItemSelectedListener, serviceId, url, title);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
resultRecyclerView.setAdapter(infoListAdapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initListeners() {
|
||||
super.initListeners();
|
||||
resultRecyclerView.clearOnScrollListeners();
|
||||
resultRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
|
||||
int pastVisiblesItems, visibleItemCount, totalItemCount;
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
//check for scroll down
|
||||
if (dy > 0) {
|
||||
LinearLayoutManager layoutManager = (LinearLayoutManager) resultRecyclerView.getLayoutManager();
|
||||
visibleItemCount = resultRecyclerView.getLayoutManager().getChildCount();
|
||||
totalItemCount = resultRecyclerView.getLayoutManager().getItemCount();
|
||||
pastVisiblesItems = layoutManager.findFirstVisibleItemPosition();
|
||||
|
||||
if ((visibleItemCount + pastVisiblesItems) >= totalItemCount && !isLoading.get()) {
|
||||
pageNumber++;
|
||||
recyclerView.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
infoListAdapter.showFooter(true);
|
||||
}
|
||||
});
|
||||
search(searchQuery, pageNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void reloadContent() {
|
||||
if (DEBUG) Log.d(TAG, "reloadContent() called");
|
||||
if (!TextUtils.isEmpty(searchQuery) || (searchEditText != null && !TextUtils.isEmpty(searchEditText.getText()))) {
|
||||
search(!TextUtils.isEmpty(searchQuery) ? searchQuery : searchEditText.getText().toString(), 0, true);
|
||||
} else {
|
||||
if (searchEditText != null) {
|
||||
searchEditText.setText("");
|
||||
showSoftKeyboard(searchEditText);
|
||||
}
|
||||
animateView(errorPanel, false, 200);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||
inflater.inflate(R.menu.search_menu, menu);
|
||||
|
||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if (supportActionBar != null) {
|
||||
supportActionBar.setDisplayShowTitleEnabled(false);
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
searchToolbarContainer = activity.findViewById(R.id.toolbar_search_container);
|
||||
searchEditText = (AutoCompleteTextView) searchToolbarContainer.findViewById(R.id.toolbar_search_edit_text);
|
||||
searchClear = searchToolbarContainer.findViewById(R.id.toolbar_search_clear);
|
||||
setupSearchView();
|
||||
|
||||
restoreFilterChecked(menu, filterItemCheckedId);
|
||||
}
|
||||
|
||||
private void restoreFilterChecked(Menu menu, int itemId) {
|
||||
if (itemId != -1) {
|
||||
MenuItem item = menu.findItem(itemId);
|
||||
if (item == null) return;
|
||||
|
||||
item.setChecked(true);
|
||||
switch (itemId) {
|
||||
case R.id.menu_filter_all:
|
||||
filter = EnumSet.of(SearchEngine.Filter.STREAM, SearchEngine.Filter.CHANNEL);
|
||||
break;
|
||||
case R.id.menu_filter_video:
|
||||
filter = EnumSet.of(SearchEngine.Filter.STREAM);
|
||||
break;
|
||||
case R.id.menu_filter_channel:
|
||||
filter = EnumSet.of(SearchEngine.Filter.CHANNEL);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (DEBUG) Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]");
|
||||
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_filter_all:
|
||||
changeFilter(item, EnumSet.of(SearchEngine.Filter.STREAM, SearchEngine.Filter.CHANNEL));
|
||||
return true;
|
||||
case R.id.menu_filter_video:
|
||||
changeFilter(item, EnumSet.of(SearchEngine.Filter.STREAM));
|
||||
return true;
|
||||
case R.id.menu_filter_channel:
|
||||
changeFilter(item, EnumSet.of(SearchEngine.Filter.CHANNEL));
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Search
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private TextWatcher textWatcher;
|
||||
|
||||
private void setupSearchView() {
|
||||
searchEditText.setText(searchQuery != null ? searchQuery : "");
|
||||
searchEditText.setHint(getString(R.string.search) + "...");
|
||||
////searchEditText.setCursorVisible(true);
|
||||
|
||||
suggestionListAdapter = new SuggestionListAdapter(activity);
|
||||
searchEditText.setAdapter(suggestionListAdapter);
|
||||
|
||||
|
||||
if (TextUtils.isEmpty(searchQuery) || TextUtils.isEmpty(searchEditText.getText())) {
|
||||
searchToolbarContainer.setTranslationX(100);
|
||||
searchToolbarContainer.setAlpha(0f);
|
||||
searchToolbarContainer.setVisibility(View.VISIBLE);
|
||||
searchToolbarContainer.animate().translationX(0).alpha(1f).setDuration(400).setInterpolator(new DecelerateInterpolator()).start();
|
||||
} else {
|
||||
searchToolbarContainer.setTranslationX(0);
|
||||
searchToolbarContainer.setAlpha(1f);
|
||||
searchToolbarContainer.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
//
|
||||
initSearchListeners();
|
||||
|
||||
if (TextUtils.isEmpty(searchQuery)) showSoftKeyboard(searchEditText);
|
||||
else hideSoftKeyboard(searchEditText);
|
||||
|
||||
if (!TextUtils.isEmpty(searchQuery) && searchQuery.length() > 2 && suggestionListAdapter != null && suggestionListAdapter.isEmpty()) {
|
||||
searchSuggestions(searchQuery);
|
||||
}
|
||||
}
|
||||
|
||||
private void initSearchListeners() {
|
||||
searchClear.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]");
|
||||
if (TextUtils.isEmpty(searchEditText.getText())) {
|
||||
NavigationHelper.openMainActivity(activity);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
searchEditText.setText("", false);
|
||||
} else searchEditText.setText("");
|
||||
suggestionListAdapter.updateAdapter(new ArrayList<String>());
|
||||
showSoftKeyboard(searchEditText);
|
||||
}
|
||||
});
|
||||
|
||||
searchClear.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
if (DEBUG) Log.d(TAG, "onLongClick() called with: v = [" + v + "]");
|
||||
showMenuTooltip(v, getString(R.string.clear));
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
searchEditText.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
searchEditText.showDropDown();
|
||||
}
|
||||
});
|
||||
|
||||
searchEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
|
||||
@Override
|
||||
public void onFocusChange(View v, boolean hasFocus) {
|
||||
if (hasFocus) searchEditText.showDropDown();
|
||||
}
|
||||
});
|
||||
|
||||
searchEditText.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
if (DEBUG) Log.d(TAG, "onItemClick() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]");
|
||||
String s = suggestionListAdapter.getSuggestion(position);
|
||||
if (DEBUG) Log.d(TAG, "onItemClick text = " + s);
|
||||
submitQuery(s);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (textWatcher != null) searchEditText.removeTextChangedListener(textWatcher);
|
||||
textWatcher = new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
String newText = searchEditText.getText().toString();
|
||||
if (!TextUtils.isEmpty(newText) && newText.length() > 1) onQueryTextChange(newText);
|
||||
}
|
||||
};
|
||||
searchEditText.addTextChangedListener(textWatcher);
|
||||
|
||||
searchEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
||||
@Override
|
||||
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
|
||||
if (DEBUG) Log.d(TAG, "onEditorAction() called with: v = [" + v + "], actionId = [" + actionId + "], event = [" + event + "]");
|
||||
if (event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER || event.getAction() == EditorInfo.IME_ACTION_SEARCH)) {
|
||||
submitQuery(searchEditText.getText().toString());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void unsetSearchListeners() {
|
||||
searchClear.setOnClickListener(null);
|
||||
searchClear.setOnLongClickListener(null);
|
||||
if (textWatcher != null) searchEditText.removeTextChangedListener(textWatcher);
|
||||
searchEditText.setOnClickListener(null);
|
||||
searchEditText.setOnItemClickListener(null);
|
||||
searchEditText.setOnFocusChangeListener(null);
|
||||
searchEditText.setOnEditorActionListener(null);
|
||||
|
||||
textWatcher = null;
|
||||
}
|
||||
|
||||
public void showSoftKeyboard(View view) {
|
||||
if (DEBUG) Log.d(TAG, "showSoftKeyboard() called with: view = [" + view + "]");
|
||||
if (view == null) return;
|
||||
|
||||
if (view.requestFocus()) {
|
||||
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
|
||||
}
|
||||
}
|
||||
|
||||
public void hideSoftKeyboard(View view) {
|
||||
if (DEBUG) Log.d(TAG, "hideSoftKeyboard() called with: view = [" + view + "]");
|
||||
if (view == null) return;
|
||||
|
||||
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
|
||||
|
||||
view.clearFocus();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void changeFilter(MenuItem item, EnumSet<SearchEngine.Filter> filter) {
|
||||
this.filter = filter;
|
||||
this.filterItemCheckedId = item.getItemId();
|
||||
item.setChecked(true);
|
||||
if (searchQuery != null && !searchQuery.isEmpty()) search(searchQuery, 0, true);
|
||||
}
|
||||
|
||||
public void submitQuery(String query) {
|
||||
if (DEBUG) Log.d(TAG, "submitQuery() called with: query = [" + query + "]");
|
||||
if (query.isEmpty()) return;
|
||||
search(query, 0, true);
|
||||
searchQuery = query;
|
||||
}
|
||||
|
||||
public void onQueryTextChange(String newText) {
|
||||
if (DEBUG) Log.d(TAG, "onQueryTextChange() called with: newText = [" + newText + "]");
|
||||
if (!newText.isEmpty()) searchSuggestions(newText);
|
||||
}
|
||||
|
||||
private void setQuery(int serviceId, String searchQuery) {
|
||||
this.serviceId = serviceId;
|
||||
this.searchQuery = searchQuery;
|
||||
}
|
||||
|
||||
private void searchSuggestions(String query) {
|
||||
if (DEBUG) Log.d(TAG, "searchSuggestions() called with: query = [" + query + "]");
|
||||
if (curSuggestionWorker != null && curSuggestionWorker.isRunning()) curSuggestionWorker.cancel();
|
||||
curSuggestionWorker = SuggestionWorker.startForQuery(activity, serviceId, query, this);
|
||||
}
|
||||
|
||||
private void search(String query, int pageNumber) {
|
||||
if (DEBUG) Log.d(TAG, "search() called with: query = [" + query + "], pageNumber = [" + pageNumber + "]");
|
||||
search(query, pageNumber, false);
|
||||
}
|
||||
|
||||
private void search(String query, int pageNumber, boolean clearList) {
|
||||
if (DEBUG) Log.d(TAG, "search() called with: query = [" + query + "], pageNumber = [" + pageNumber + "], clearList = [" + clearList + "]");
|
||||
isLoading.set(true);
|
||||
hideSoftKeyboard(searchEditText);
|
||||
|
||||
searchQuery = query;
|
||||
this.pageNumber = pageNumber;
|
||||
|
||||
if (clearList) {
|
||||
animateView(resultRecyclerView, false, 50);
|
||||
infoListAdapter.clearStreamItemList();
|
||||
infoListAdapter.showFooter(false);
|
||||
animateView(loadingProgressBar, true, 200);
|
||||
}
|
||||
animateView(errorPanel, false, 200);
|
||||
|
||||
if (curSearchWorker != null && curSearchWorker.isRunning()) curSearchWorker.cancel();
|
||||
curSearchWorker = SearchWorker.startForQuery(activity, serviceId, query, pageNumber, filter, this);
|
||||
}
|
||||
|
||||
protected void setErrorMessage(String message, boolean showRetryButton) {
|
||||
super.setErrorMessage(message, showRetryButton);
|
||||
|
||||
animateView(resultRecyclerView, false, 400);
|
||||
hideSoftKeyboard(searchEditText);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// OnSuggestionResult
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onSuggestionResult(@NonNull List<String> suggestions) {
|
||||
if (DEBUG) Log.d(TAG, "onSuggestionResult() called with: suggestions = [" + suggestions + "]");
|
||||
suggestionListAdapter.updateAdapter(suggestions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuggestionError(int messageId) {
|
||||
if (DEBUG) Log.d(TAG, "onSuggestionError() called with: messageId = [" + messageId + "]");
|
||||
setErrorMessage(getString(messageId), true);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// SearchWorkerResultListener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onSearchResult(SearchResult result) {
|
||||
if (DEBUG) Log.d(TAG, "onSearchResult() called with: result = [" + result + "]");
|
||||
infoListAdapter.addInfoItemList(result.resultList);
|
||||
animateView(resultRecyclerView, true, 400);
|
||||
animateView(loadingProgressBar, false, 200);
|
||||
isLoading.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingFound(String message) {
|
||||
if (DEBUG) Log.d(TAG, "onNothingFound() called with: messageId = [" + message + "]");
|
||||
setErrorMessage(message, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSearchError(int messageId) {
|
||||
if (DEBUG) Log.d(TAG, "onSearchError() called with: messageId = [" + messageId + "]");
|
||||
//Toast.makeText(getActivity(), messageId, Toast.LENGTH_LONG).show();
|
||||
setErrorMessage(getString(messageId), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReCaptchaChallenge() {
|
||||
if (DEBUG) Log.d(TAG, "onReCaptchaChallenge() called");
|
||||
Toast.makeText(getActivity(), R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
|
||||
setErrorMessage(getString(R.string.recaptcha_request_toast), false);
|
||||
|
||||
// Starting ReCaptcha Challenge Activity
|
||||
startActivityForResult(new Intent(getActivity(), ReCaptchaActivity.class), ReCaptchaActivity.RECAPTCHA_REQUEST);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.search_fragment;
|
||||
package org.schabi.newpipe.fragments.search;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
@@ -75,6 +75,11 @@ public class SuggestionListAdapter extends ResourceCursorAdapter {
|
||||
return ((Cursor) getItem(position)).getString(INDEX_TITLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence convertToString(Cursor cursor) {
|
||||
return cursor.getString(INDEX_TITLE);
|
||||
}
|
||||
|
||||
private class ViewHolder {
|
||||
private final TextView suggestionTitle;
|
||||
private ViewHolder(View view) {
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.schabi.newpipe.info_list;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
@@ -35,16 +34,17 @@ public class ChannelInfoItemHolder extends InfoItemHolder {
|
||||
public final TextView itemSubscriberCountView;
|
||||
public final TextView itemVideoCountView;
|
||||
public final TextView itemChannelDescriptionView;
|
||||
public final Button itemButton;
|
||||
|
||||
public final View itemRoot;
|
||||
|
||||
ChannelInfoItemHolder(View v) {
|
||||
super(v);
|
||||
itemRoot = v.findViewById(R.id.itemRoot);
|
||||
itemThumbnailView = (CircleImageView) v.findViewById(R.id.itemThumbnailView);
|
||||
itemChannelTitleView = (TextView) v.findViewById(R.id.itemChannelTitleView);
|
||||
itemSubscriberCountView = (TextView) v.findViewById(R.id.itemSubscriberCountView);
|
||||
itemVideoCountView = (TextView) v.findViewById(R.id.itemVideoCountView);
|
||||
itemChannelDescriptionView = (TextView) v.findViewById(R.id.itemChannelDescriptionView);
|
||||
itemButton = (Button) v.findViewById(R.id.item_button);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package org.schabi.newpipe.info_list;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
@@ -19,40 +19,43 @@ import org.schabi.newpipe.extractor.stream_info.StreamInfoItem;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 26.09.16.
|
||||
*
|
||||
* <p>
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* InfoItemBuilder.java is part of NewPipe.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* <p>
|
||||
* 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 InfoItemBuilder {
|
||||
|
||||
final String viewsS;
|
||||
final String videosS;
|
||||
final String subsS;
|
||||
private final String viewsS;
|
||||
private final String videosS;
|
||||
private final String subsS;
|
||||
private final String subsPluralS;
|
||||
|
||||
final String thousand;
|
||||
final String million;
|
||||
final String billion;
|
||||
private final String thousand;
|
||||
private final String million;
|
||||
private final String billion;
|
||||
|
||||
private static final String TAG = InfoItemBuilder.class.toString();
|
||||
|
||||
public interface OnInfoItemSelectedListener {
|
||||
void selected(String url, int serviceId);
|
||||
void selected(int serviceId, String url, String title);
|
||||
}
|
||||
|
||||
private Activity activity = null;
|
||||
private Context mContext = null;
|
||||
private LayoutInflater inflater;
|
||||
private View rootView = null;
|
||||
private ImageLoader imageLoader = ImageLoader.getInstance();
|
||||
private DisplayImageOptions displayImageOptions =
|
||||
@@ -60,15 +63,17 @@ public class InfoItemBuilder {
|
||||
private OnInfoItemSelectedListener onStreamInfoItemSelectedListener;
|
||||
private OnInfoItemSelectedListener onChannelInfoItemSelectedListener;
|
||||
|
||||
public InfoItemBuilder(Activity a, View rootView) {
|
||||
activity = a;
|
||||
public InfoItemBuilder(Context context, View rootView) {
|
||||
mContext = context;
|
||||
this.rootView = rootView;
|
||||
viewsS = a.getString(R.string.views);
|
||||
videosS = a.getString(R.string.videos);
|
||||
subsS = a.getString(R.string.subscriber);
|
||||
thousand = a.getString(R.string.short_thousand);
|
||||
million = a.getString(R.string.short_million);
|
||||
billion = a.getString(R.string.short_billion);
|
||||
viewsS = context.getString(R.string.views);
|
||||
videosS = context.getString(R.string.videos);
|
||||
subsS = context.getString(R.string.subscriber);
|
||||
subsPluralS = context.getString(R.string.subscriber_plural);
|
||||
thousand = context.getString(R.string.short_thousand);
|
||||
million = context.getString(R.string.short_million);
|
||||
billion = context.getString(R.string.short_billion);
|
||||
inflater = LayoutInflater.from(context);
|
||||
}
|
||||
|
||||
public void setOnStreamInfoItemSelectedListener(
|
||||
@@ -82,9 +87,9 @@ public class InfoItemBuilder {
|
||||
}
|
||||
|
||||
public void buildByHolder(InfoItemHolder holder, final InfoItem i) {
|
||||
if(i.infoType() != holder.infoType())
|
||||
if (i.infoType() != holder.infoType())
|
||||
return;
|
||||
switch(i.infoType()) {
|
||||
switch (i.infoType()) {
|
||||
case STREAM:
|
||||
buildStreamInfoItem((StreamInfoItemHolder) holder, (StreamInfoItem) i);
|
||||
break;
|
||||
@@ -102,15 +107,15 @@ public class InfoItemBuilder {
|
||||
public View buildView(ViewGroup parent, final InfoItem info) {
|
||||
View itemView = null;
|
||||
InfoItemHolder holder = null;
|
||||
switch(info.infoType()) {
|
||||
switch (info.infoType()) {
|
||||
case STREAM:
|
||||
itemView = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.stream_item, parent, false);
|
||||
//long start = System.nanoTime();
|
||||
itemView = inflater.inflate(R.layout.stream_item, parent, false);
|
||||
//Log.d(TAG, "time to inflate: " + ((System.nanoTime() - start) / 1000000L) + "ms");
|
||||
holder = new StreamInfoItemHolder(itemView);
|
||||
break;
|
||||
case CHANNEL:
|
||||
itemView = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.channel_item, parent, false);
|
||||
itemView = inflater.inflate(R.layout.channel_item, parent, false);
|
||||
holder = new ChannelInfoItemHolder(itemView);
|
||||
break;
|
||||
case PLAYLIST:
|
||||
@@ -123,94 +128,92 @@ public class InfoItemBuilder {
|
||||
}
|
||||
|
||||
private void buildStreamInfoItem(StreamInfoItemHolder holder, final StreamInfoItem info) {
|
||||
if(info.infoType() != InfoItem.InfoType.STREAM) {
|
||||
if (info.infoType() != InfoItem.InfoType.STREAM) {
|
||||
Log.e("InfoItemBuilder", "Info type not yet supported");
|
||||
}
|
||||
// fill holder with information
|
||||
holder.itemVideoTitleView.setText(info.title);
|
||||
if(info.uploader != null && !info.uploader.isEmpty()) {
|
||||
holder.itemUploaderView.setText(info.uploader);
|
||||
} else {
|
||||
holder.itemUploaderView.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
if(info.duration > 0) {
|
||||
if (!TextUtils.isEmpty(info.title)) holder.itemVideoTitleView.setText(info.title);
|
||||
|
||||
if (!TextUtils.isEmpty(info.uploader)) holder.itemUploaderView.setText(info.uploader);
|
||||
else holder.itemUploaderView.setVisibility(View.INVISIBLE);
|
||||
|
||||
if (info.duration > 0) {
|
||||
holder.itemDurationView.setText(getDurationString(info.duration));
|
||||
} else {
|
||||
if(info.stream_type == AbstractStreamInfo.StreamType.LIVE_STREAM) {
|
||||
if (info.stream_type == AbstractStreamInfo.StreamType.LIVE_STREAM) {
|
||||
holder.itemDurationView.setText(R.string.duration_live);
|
||||
} else {
|
||||
holder.itemDurationView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
if(info.view_count >= 0) {
|
||||
if (info.view_count >= 0) {
|
||||
holder.itemViewCountView.setText(shortViewCount(info.view_count));
|
||||
} else {
|
||||
holder.itemViewCountView.setVisibility(View.GONE);
|
||||
}
|
||||
if(info.upload_date != null && !info.upload_date.isEmpty()) {
|
||||
holder.itemUploadDateView.setText(info.upload_date + " • ");
|
||||
}
|
||||
if (!TextUtils.isEmpty(info.upload_date)) holder.itemUploadDateView.setText(info.upload_date + " • ");
|
||||
|
||||
holder.itemThumbnailView.setImageResource(R.drawable.dummy_thumbnail);
|
||||
if(info.thumbnail_url != null && !info.thumbnail_url.isEmpty()) {
|
||||
if (!TextUtils.isEmpty(info.thumbnail_url)) {
|
||||
imageLoader.displayImage(info.thumbnail_url,
|
||||
holder.itemThumbnailView,
|
||||
displayImageOptions,
|
||||
new ImageErrorLoadingListener(activity, rootView, info.service_id));
|
||||
holder.itemThumbnailView, displayImageOptions,
|
||||
new ImageErrorLoadingListener(mContext, rootView, info.service_id));
|
||||
}
|
||||
|
||||
holder.itemButton.setOnClickListener(new View.OnClickListener() {
|
||||
holder.itemRoot.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
onStreamInfoItemSelectedListener.selected(info.webpage_url, info.service_id);
|
||||
onStreamInfoItemSelectedListener.selected(info.service_id, info.webpage_url, info.getTitle());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void buildChannelInfoItem(ChannelInfoItemHolder holder, final ChannelInfoItem info) {
|
||||
holder.itemChannelTitleView.setText(info.getTitle());
|
||||
if (!TextUtils.isEmpty(info.getTitle())) holder.itemChannelTitleView.setText(info.getTitle());
|
||||
holder.itemSubscriberCountView.setText(shortSubscriber(info.subscriberCount) + " • ");
|
||||
holder.itemVideoCountView.setText(info.videoAmount + " " + videosS);
|
||||
holder.itemChannelDescriptionView.setText(info.description);
|
||||
if (!TextUtils.isEmpty(info.description)) holder.itemChannelDescriptionView.setText(info.description);
|
||||
|
||||
holder.itemThumbnailView.setImageResource(R.drawable.buddy_channel_item);
|
||||
if(info.thumbnailUrl != null && !info.thumbnailUrl.isEmpty()) {
|
||||
if (!TextUtils.isEmpty(info.thumbnailUrl)) {
|
||||
imageLoader.displayImage(info.thumbnailUrl,
|
||||
holder.itemThumbnailView,
|
||||
displayImageOptions,
|
||||
new ImageErrorLoadingListener(activity, rootView, info.serviceId));
|
||||
new ImageErrorLoadingListener(mContext, rootView, info.serviceId));
|
||||
}
|
||||
|
||||
holder.itemButton.setOnClickListener(new View.OnClickListener() {
|
||||
holder.itemRoot.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
onChannelInfoItemSelectedListener.selected(info.getLink(), info.serviceId);
|
||||
onChannelInfoItemSelectedListener.selected(info.serviceId, info.getLink(), info.channelName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public String shortViewCount(Long viewCount){
|
||||
if(viewCount >= 1000000000){
|
||||
return Long.toString(viewCount/1000000000)+ billion + " " + viewsS;
|
||||
}else if(viewCount>=1000000){
|
||||
return Long.toString(viewCount/1000000)+ million + " " + viewsS;
|
||||
}else if(viewCount>=1000){
|
||||
return Long.toString(viewCount/1000)+ thousand + " " + viewsS;
|
||||
}else {
|
||||
return Long.toString(viewCount)+ " " + viewsS;
|
||||
public String shortViewCount(Long viewCount) {
|
||||
if (viewCount >= 1000000000) {
|
||||
return Long.toString(viewCount / 1000000000) + billion + " " + viewsS;
|
||||
} else if (viewCount >= 1000000) {
|
||||
return Long.toString(viewCount / 1000000) + million + " " + viewsS;
|
||||
} else if (viewCount >= 1000) {
|
||||
return Long.toString(viewCount / 1000) + thousand + " " + viewsS;
|
||||
} else {
|
||||
return Long.toString(viewCount) + " " + viewsS;
|
||||
}
|
||||
}
|
||||
|
||||
public String shortSubscriber(Long count){
|
||||
if(count >= 1000000000){
|
||||
return Long.toString(count/1000000000)+ billion + " " + subsS;
|
||||
}else if(count>=1000000){
|
||||
return Long.toString(count/1000000)+ million + " " + subsS;
|
||||
}else if(count>=1000){
|
||||
return Long.toString(count/1000)+ thousand + " " + subsS;
|
||||
}else {
|
||||
return Long.toString(count)+ " " + subsS;
|
||||
public String shortSubscriber(Long count) {
|
||||
String curSubString = count > 1 ? subsPluralS : subsS;
|
||||
|
||||
if (count >= 1000000000) {
|
||||
return Long.toString(count / 1000000000) + billion + " " + curSubString;
|
||||
} else if (count >= 1000000) {
|
||||
return Long.toString(count / 1000000) + million + " " + curSubString;
|
||||
} else if (count >= 1000) {
|
||||
return Long.toString(count / 1000) + thousand + " " + curSubString;
|
||||
} else {
|
||||
return Long.toString(count) + " " + curSubString;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,13 +227,13 @@ public class InfoItemBuilder {
|
||||
int seconds = duration % 60;
|
||||
|
||||
//handle days
|
||||
if(days > 0) {
|
||||
if (days > 0) {
|
||||
output = Integer.toString(days) + ":";
|
||||
}
|
||||
// handle hours
|
||||
if(hours > 0 || !output.isEmpty()) {
|
||||
if(hours > 0) {
|
||||
if(hours >= 10 || output.isEmpty()) {
|
||||
if (hours > 0 || !output.isEmpty()) {
|
||||
if (hours > 0) {
|
||||
if (hours >= 10 || output.isEmpty()) {
|
||||
output += Integer.toString(hours);
|
||||
} else {
|
||||
output += "0" + Integer.toString(hours);
|
||||
@@ -241,9 +244,9 @@ public class InfoItemBuilder {
|
||||
output += ":";
|
||||
}
|
||||
//handle minutes
|
||||
if(minutes > 0 || !output.isEmpty()) {
|
||||
if(minutes > 0) {
|
||||
if(minutes >= 10 || output.isEmpty()) {
|
||||
if (minutes > 0 || !output.isEmpty()) {
|
||||
if (minutes > 0) {
|
||||
if (minutes >= 10 || output.isEmpty()) {
|
||||
output += Integer.toString(minutes);
|
||||
} else {
|
||||
output += "0" + Integer.toString(minutes);
|
||||
@@ -255,11 +258,11 @@ public class InfoItemBuilder {
|
||||
}
|
||||
|
||||
//handle seconds
|
||||
if(output.isEmpty()) {
|
||||
if (output.isEmpty()) {
|
||||
output += "0:";
|
||||
}
|
||||
|
||||
if(seconds >= 10) {
|
||||
if (seconds >= 10) {
|
||||
output += Integer.toString(seconds);
|
||||
} else {
|
||||
output += "0" + Integer.toString(seconds);
|
||||
|
||||
@@ -10,8 +10,8 @@ import android.view.ViewGroup;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 01.08.16.
|
||||
@@ -57,7 +57,7 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
|
||||
public InfoListAdapter(Activity a, View rootView) {
|
||||
infoItemBuilder = new InfoItemBuilder(a, rootView);
|
||||
infoItemList = new Vector<>();
|
||||
infoItemList = new ArrayList<>();
|
||||
}
|
||||
|
||||
public void setOnStreamInfoItemSelectedListener
|
||||
@@ -77,7 +77,7 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
}
|
||||
}
|
||||
|
||||
public void clearSteamItemList() {
|
||||
public void clearStreamItemList() {
|
||||
infoItemList.clear();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
@@ -92,12 +92,16 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public List<InfoItem> getItemsList() {
|
||||
return infoItemList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
int cound = infoItemList.size();
|
||||
if(header != null) cound++;
|
||||
if(footer != null && showFooter) cound++;
|
||||
return cound;
|
||||
int count = infoItemList.size();
|
||||
if(header != null) count++;
|
||||
if(footer != null && showFooter) count++;
|
||||
return count;
|
||||
}
|
||||
|
||||
// don't ask why we have to do that this way... it's android accept it -.-
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package org.schabi.newpipe.info_list;
|
||||
|
||||
import android.icu.text.IDNA;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
@@ -12,20 +9,20 @@ import org.schabi.newpipe.extractor.InfoItem;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 01.08.16.
|
||||
*
|
||||
* <p>
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* StreamInfoItemHolder.java is part of NewPipe.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* <p>
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
@@ -38,17 +35,17 @@ public class StreamInfoItemHolder extends InfoItemHolder {
|
||||
itemDurationView,
|
||||
itemUploadDateView,
|
||||
itemViewCountView;
|
||||
public final Button itemButton;
|
||||
public final View itemRoot;
|
||||
|
||||
public StreamInfoItemHolder(View v) {
|
||||
super(v);
|
||||
itemRoot = v.findViewById(R.id.itemRoot);
|
||||
itemThumbnailView = (ImageView) v.findViewById(R.id.itemThumbnailView);
|
||||
itemVideoTitleView = (TextView) v.findViewById(R.id.itemVideoTitleView);
|
||||
itemUploaderView = (TextView) v.findViewById(R.id.itemUploaderView);
|
||||
itemDurationView = (TextView) v.findViewById(R.id.itemDurationView);
|
||||
itemUploadDateView = (TextView) v.findViewById(R.id.itemUploadDateView);
|
||||
itemViewCountView = (TextView) v.findViewById(R.id.itemViewCountView);
|
||||
itemButton = (Button) v.findViewById(R.id.item_button);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -4,583 +4,452 @@ 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.Build;
|
||||
import android.os.IBinder;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.os.PowerManager;
|
||||
import android.support.v7.app.NotificationCompat;
|
||||
import android.support.annotation.IntRange;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.util.Log;
|
||||
import android.widget.RemoteViews;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.ActivityCommunicator;
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.detail.VideoItemDetailActivity;
|
||||
import org.schabi.newpipe.detail.VideoItemDetailFragment;
|
||||
import org.schabi.newpipe.util.NavStack;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.stream_info.AudioStream;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Created by Adam Howard on 08/11/15.
|
||||
* Copyright (c) Adam Howard <achdisposable1@gmail.com> 2015
|
||||
* Base players joining the common properties
|
||||
*
|
||||
* 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/>.
|
||||
* @author mauriciocolli
|
||||
*/
|
||||
|
||||
/**Plays the audio stream of videos in the background.*/
|
||||
public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPreparedListener*/ {
|
||||
|
||||
public class BackgroundPlayer extends Service {
|
||||
private static final String TAG = "BackgroundPlayer";
|
||||
private static final String CLASSNAME = "org.schabi.newpipe.player.BackgroundPlayer";
|
||||
private static final String ACTION_STOP = CLASSNAME + ".STOP";
|
||||
private static final String ACTION_PLAYPAUSE = CLASSNAME + ".PLAYPAUSE";
|
||||
private static final String ACTION_REWIND = CLASSNAME + ".REWIND";
|
||||
private static final String ACTION_PLAYBACK_STATE = CLASSNAME + ".PLAYBACK_STATE";
|
||||
private static final String EXTRA_PLAYBACK_STATE = CLASSNAME + ".extras.EXTRA_PLAYBACK_STATE";
|
||||
private static final boolean DEBUG = BasePlayer.DEBUG;
|
||||
|
||||
// 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";
|
||||
public static final String ACTION_CLOSE = "org.schabi.newpipe.player.BackgroundPlayer.CLOSE";
|
||||
public static final String ACTION_PLAY_PAUSE = "org.schabi.newpipe.player.BackgroundPlayer.PLAY_PAUSE";
|
||||
public static final String ACTION_OPEN_DETAIL = "org.schabi.newpipe.player.BackgroundPlayer.OPEN_DETAIL";
|
||||
public static final String ACTION_REPEAT = "org.schabi.newpipe.player.BackgroundPlayer.REPEAT";
|
||||
public static final String ACTION_FAST_REWIND = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_FAST_REWIND";
|
||||
public static final String ACTION_FAST_FORWARD = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_FAST_FORWARD";
|
||||
|
||||
private volatile String webUrl = "";
|
||||
private volatile int serviceId = -1;
|
||||
private volatile String channelName = "";
|
||||
public static final String AUDIO_STREAM = "video_only_audio_stream";
|
||||
private AudioStream audioStream;
|
||||
|
||||
// Determines if the service is already running.
|
||||
// Prevents launching the service twice.
|
||||
public static volatile boolean isRunning;
|
||||
private BasePlayerImpl basePlayerImpl;
|
||||
private PowerManager powerManager;
|
||||
private WifiManager wifiManager;
|
||||
|
||||
public BackgroundPlayer() {
|
||||
super();
|
||||
}
|
||||
private PowerManager.WakeLock wakeLock;
|
||||
private WifiManager.WifiLock wifiLock;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Notification
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
private static final int NOTIFICATION_ID = 123789;
|
||||
|
||||
private NotificationManager notificationManager;
|
||||
private NotificationCompat.Builder notBuilder;
|
||||
private RemoteViews notRemoteView;
|
||||
private RemoteViews bigNotRemoteView;
|
||||
private final String setAlphaMethodName = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) ? "setImageAlpha" : "setAlpha";
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Service's LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
/*PendingIntent pi = PendingIntent.getActivity(this, 0,
|
||||
new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);*/
|
||||
super.onCreate();
|
||||
if (DEBUG) Log.d(TAG, "onCreate() called");
|
||||
notificationManager = ((NotificationManager) getSystemService(NOTIFICATION_SERVICE));
|
||||
powerManager = ((PowerManager) getSystemService(POWER_SERVICE));
|
||||
wifiManager = ((WifiManager) getApplicationContext().getSystemService(WIFI_SERVICE));
|
||||
|
||||
ThemeHelper.setTheme(this);
|
||||
basePlayerImpl = new BasePlayerImpl(this);
|
||||
basePlayerImpl.setup();
|
||||
}
|
||||
|
||||
@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
|
||||
if (DEBUG) Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "], flags = [" + flags + "], startId = [" + startId + "]");
|
||||
basePlayerImpl.handleIntent(intent);
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
// We don't provide binding (yet?), so return null
|
||||
return null;
|
||||
public void onDestroy() {
|
||||
if (DEBUG) Log.d(TAG, "destroy() called");
|
||||
releaseWifiAndCpu();
|
||||
stopForeground(true);
|
||||
if (basePlayerImpl != null) basePlayerImpl.destroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
//Toast.makeText(this, "service done", Toast.LENGTH_SHORT).show();
|
||||
isRunning = false;
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Actions
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
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;
|
||||
private NoteBuilder noteBuilder;
|
||||
private volatile boolean donePlaying = false;
|
||||
public void onOpenDetail(Context context, String videoUrl, String videoTitle) {
|
||||
if (DEBUG) Log.d(TAG, "onOpenDetail() called with: context = [" + context + "], videoUrl = [" + videoUrl + "]");
|
||||
Intent i = new Intent(context, MainActivity.class);
|
||||
i.putExtra(Constants.KEY_SERVICE_ID, 0);
|
||||
i.putExtra(Constants.KEY_URL, videoUrl);
|
||||
i.putExtra(Constants.KEY_TITLE, videoTitle);
|
||||
i.putExtra(Constants.KEY_LINK_TYPE, StreamingService.LinkType.STREAM);
|
||||
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(i);
|
||||
context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
private void onClose() {
|
||||
if (basePlayerImpl != null) basePlayerImpl.destroyPlayer();
|
||||
stopForeground(true);
|
||||
releaseWifiAndCpu();
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
public boolean isDonePlaying() {
|
||||
return donePlaying;
|
||||
}
|
||||
private void onScreenOnOff(boolean on) {
|
||||
if (DEBUG) Log.d(TAG, "onScreenOnOff() called with: on = [" + on + "]");
|
||||
if (on) {
|
||||
if (basePlayerImpl.isPlaying() && !basePlayerImpl.isProgressLoopRunning.get()) basePlayerImpl.startProgressLoop();
|
||||
} else basePlayerImpl.stopProgressLoop();
|
||||
|
||||
private boolean isPlaying() {
|
||||
try {
|
||||
return mediaPlayer.isPlaying();
|
||||
} catch (IllegalStateException e) {
|
||||
Log.w(TAG, "Unable to retrieve playing state", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setDonePlaying() {
|
||||
donePlaying = true;
|
||||
synchronized (PlayerThread.this) {
|
||||
PlayerThread.this.notifyAll();
|
||||
}
|
||||
}
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Notification
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private synchronized PlaybackState getPlaybackState() {
|
||||
try {
|
||||
return new PlaybackState(mediaPlayer.getDuration(), mediaPlayer.getCurrentPosition(), isPlaying());
|
||||
} catch (IllegalStateException e) {
|
||||
// This isn't that nice way to handle this.
|
||||
// maybe there is a better way
|
||||
Log.w(TAG, this + ": Got illegal state exception while creating playback state", e);
|
||||
return PlaybackState.UNPREPARED;
|
||||
}
|
||||
}
|
||||
private NotificationCompat.Builder createNotification() {
|
||||
notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification);
|
||||
bigNotRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification_expanded);
|
||||
|
||||
private void broadcastState() {
|
||||
PlaybackState state = getPlaybackState();
|
||||
if(state == null) return;
|
||||
Intent intent = new Intent(ACTION_PLAYBACK_STATE);
|
||||
intent.putExtra(EXTRA_PLAYBACK_STATE, state);
|
||||
sendBroadcast(intent);
|
||||
}
|
||||
setupNotification(notRemoteView);
|
||||
setupNotification(bigNotRemoteView);
|
||||
|
||||
@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();
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
|
||||
.setOngoing(true)
|
||||
.setSmallIcon(R.drawable.ic_play_circle_filled_white_24dp)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setCustomContentView(notRemoteView)
|
||||
.setCustomBigContentView(bigNotRemoteView);
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) builder.setPriority(Notification.PRIORITY_MAX);
|
||||
return builder;
|
||||
}
|
||||
|
||||
} 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;
|
||||
}
|
||||
private void setupNotification(RemoteViews remoteViews) {
|
||||
//if (videoThumbnail != null) remoteViews.setImageViewBitmap(R.id.notificationCover, videoThumbnail);
|
||||
///else remoteViews.setImageViewResource(R.id.notificationCover, R.drawable.dummy_thumbnail);
|
||||
remoteViews.setTextViewText(R.id.notificationSongName, basePlayerImpl.getVideoTitle());
|
||||
remoteViews.setTextViewText(R.id.notificationArtist, basePlayerImpl.getChannelName());
|
||||
|
||||
try {
|
||||
videoThumbnail = ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail;
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Could not get video thumbnail from ActivityCommunicator");
|
||||
e.printStackTrace();
|
||||
}
|
||||
remoteViews.setOnClickPendingIntent(R.id.notificationPlayPause,
|
||||
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT));
|
||||
remoteViews.setOnClickPendingIntent(R.id.notificationStop,
|
||||
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_CLOSE), PendingIntent.FLAG_UPDATE_CURRENT));
|
||||
remoteViews.setOnClickPendingIntent(R.id.notificationContent,
|
||||
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_OPEN_DETAIL), PendingIntent.FLAG_UPDATE_CURRENT));
|
||||
remoteViews.setOnClickPendingIntent(R.id.notificationRepeat,
|
||||
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_REPEAT), PendingIntent.FLAG_UPDATE_CURRENT));
|
||||
|
||||
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);
|
||||
filter.addAction(ACTION_REWIND);
|
||||
filter.addAction(ACTION_PLAYBACK_STATE);
|
||||
registerReceiver(broadcastReceiver, filter);
|
||||
|
||||
initNotificationBuilder();
|
||||
startForeground(noteID, noteBuilder.build());
|
||||
|
||||
//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 vidLength = mediaPlayer.getDuration();
|
||||
int sleepTime = Math.min(2000, (int)(vidLength / 4));
|
||||
while(!isDonePlaying()) {
|
||||
broadcastState();
|
||||
try {
|
||||
synchronized (this) {
|
||||
wait(sleepTime);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(TAG, "sleep failure", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**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);
|
||||
switch (action) {
|
||||
case ACTION_PLAYPAUSE: {
|
||||
boolean isPlaying = mediaPlayer.isPlaying();
|
||||
if(isPlaying) {
|
||||
mediaPlayer.pause();
|
||||
} else {
|
||||
//reacquire CPU lock after auto-releasing it on pause
|
||||
mediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
|
||||
mediaPlayer.start();
|
||||
}
|
||||
synchronized (PlayerThread.this) {
|
||||
PlayerThread.this.notifyAll();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ACTION_REWIND:
|
||||
mediaPlayer.seekTo(0);
|
||||
synchronized (PlayerThread.this) {
|
||||
PlayerThread.this.notifyAll();
|
||||
}
|
||||
break;
|
||||
case ACTION_STOP:
|
||||
//this auto-releases CPU lock
|
||||
mediaPlayer.stop();
|
||||
afterPlayCleanup();
|
||||
break;
|
||||
case ACTION_PLAYBACK_STATE: {
|
||||
PlaybackState playbackState = intent.getParcelableExtra(EXTRA_PLAYBACK_STATE);
|
||||
if(!playbackState.equals(PlaybackState.UNPREPARED)) {
|
||||
noteBuilder.setProgress(playbackState.getDuration(), playbackState.getPlayedTime(), false);
|
||||
noteBuilder.setIsPlaying(playbackState.isPlaying());
|
||||
} else {
|
||||
noteBuilder.setProgress(0, 0, true);
|
||||
}
|
||||
noteMgr.notify(noteID, noteBuilder.build());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private void afterPlayCleanup() {
|
||||
// Notify thread to stop
|
||||
setDonePlaying();
|
||||
//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);
|
||||
try {
|
||||
// Wait for thread to stop
|
||||
PlayerThread.this.join();
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(TAG, "unable to join player thread", e);
|
||||
}
|
||||
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 void initNotificationBuilder() {
|
||||
Notification note;
|
||||
Resources res = getApplicationContext().getResources();
|
||||
|
||||
/*
|
||||
NotificationCompat.Action pauseButton = new NotificationCompat.Action.Builder
|
||||
(R.drawable.ic_pause_white_24dp, "Pause", playPI).build();
|
||||
*/
|
||||
|
||||
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);
|
||||
PendingIntent rewindPI = PendingIntent.getBroadcast(owner, noteID,
|
||||
new Intent(ACTION_REWIND), PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
//build intent to return to video, on tapping notification
|
||||
Intent openDetailViewIntent = new Intent(getApplicationContext(),
|
||||
VideoItemDetailActivity.class);
|
||||
openDetailViewIntent.putExtra(NavStack.SERVICE_ID, serviceId);
|
||||
openDetailViewIntent.putExtra(NavStack.URL, webUrl);
|
||||
openDetailViewIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
PendingIntent openDetailView = PendingIntent.getActivity(owner, noteID,
|
||||
openDetailViewIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
noteBuilder = new NoteBuilder(owner, playPI, stopPI, rewindPI, openDetailView);
|
||||
noteBuilder
|
||||
.setTitle(title)
|
||||
.setArtist(channelName)
|
||||
.setOngoing(true)
|
||||
.setDeleteIntent(stopPI)
|
||||
//doesn't fit with Notification.MediaStyle
|
||||
//.setProgress(vidLength, 0, false)
|
||||
.setSmallIcon(R.drawable.ic_play_circle_filled_white_24dp)
|
||||
.setContentIntent(PendingIntent.getActivity(getApplicationContext(),
|
||||
noteID, openDetailViewIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.setContentIntent(openDetailView)
|
||||
.setCategory(Notification.CATEGORY_TRANSPORT)
|
||||
//Make notification appear on lockscreen
|
||||
.setVisibility(Notification.VISIBILITY_PUBLIC);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Notification builder which works like the real builder but uses a custom view.
|
||||
*/
|
||||
class NoteBuilder extends NotificationCompat.Builder {
|
||||
|
||||
/**
|
||||
* @param context
|
||||
* @inheritDoc
|
||||
*/
|
||||
public NoteBuilder(Context context, PendingIntent playPI, PendingIntent stopPI,
|
||||
PendingIntent rewindPI, PendingIntent openDetailView) {
|
||||
super(context);
|
||||
setCustomContentView(createCustomContentView(playPI, stopPI, rewindPI, openDetailView));
|
||||
setCustomBigContentView(createCustomBigContentView(playPI, stopPI, rewindPI, openDetailView));
|
||||
}
|
||||
|
||||
private RemoteViews createCustomBigContentView(PendingIntent playPI,
|
||||
PendingIntent stopPI,
|
||||
PendingIntent rewindPI,
|
||||
PendingIntent openDetailView) {
|
||||
//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.notificationCover, videoThumbnail);
|
||||
expandedView.setOnClickPendingIntent(R.id.notificationStop, stopPI);
|
||||
expandedView.setOnClickPendingIntent(R.id.notificationPlayPause, playPI);
|
||||
expandedView.setOnClickPendingIntent(R.id.notificationRewind, rewindPI);
|
||||
expandedView.setOnClickPendingIntent(R.id.notificationContent, openDetailView);
|
||||
return expandedView;
|
||||
}
|
||||
|
||||
private RemoteViews createCustomContentView(PendingIntent playPI, PendingIntent stopPI,
|
||||
PendingIntent rewindPI,
|
||||
PendingIntent openDetailView) {
|
||||
RemoteViews view = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification);
|
||||
view.setImageViewBitmap(R.id.notificationCover, videoThumbnail);
|
||||
view.setOnClickPendingIntent(R.id.notificationStop, stopPI);
|
||||
view.setOnClickPendingIntent(R.id.notificationPlayPause, playPI);
|
||||
view.setOnClickPendingIntent(R.id.notificationRewind, rewindPI);
|
||||
view.setOnClickPendingIntent(R.id.notificationContent, openDetailView);
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the title of the stream
|
||||
* @param title the title of the stream
|
||||
* @return this builder for chaining
|
||||
*/
|
||||
NoteBuilder setTitle(String title) {
|
||||
setContentTitle(title);
|
||||
getContentView().setTextViewText(R.id.notificationSongName, title);
|
||||
getBigContentView().setTextViewText(R.id.notificationSongName, title);
|
||||
setTicker(String.format(getBaseContext().getString(
|
||||
R.string.background_player_time_text), title));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the artist of the stream
|
||||
* @param artist the artist of the stream
|
||||
* @return this builder for chaining
|
||||
*/
|
||||
NoteBuilder setArtist(String artist) {
|
||||
setSubText(artist);
|
||||
getContentView().setTextViewText(R.id.notificationArtist, artist);
|
||||
getBigContentView().setTextViewText(R.id.notificationArtist, artist);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public android.support.v4.app.NotificationCompat.Builder setProgress(int max, int progress, boolean indeterminate) {
|
||||
super.setProgress(max, progress, indeterminate);
|
||||
getBigContentView().setProgressBar(R.id.playbackProgress, max, progress, indeterminate);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the isPlaying state
|
||||
* @param isPlaying the is playing state
|
||||
*/
|
||||
public void setIsPlaying(boolean isPlaying) {
|
||||
RemoteViews views = getContentView(), bigViews = getBigContentView();
|
||||
int imageSrc;
|
||||
if(isPlaying) {
|
||||
imageSrc = R.drawable.ic_pause_white_24dp;
|
||||
} else {
|
||||
imageSrc = R.drawable.ic_play_circle_filled_white_24dp;
|
||||
}
|
||||
views.setImageViewResource(R.id.notificationPlayPause, imageSrc);
|
||||
bigViews.setImageViewResource(R.id.notificationPlayPause, imageSrc);
|
||||
|
||||
}
|
||||
remoteViews.setOnClickPendingIntent(R.id.notificationFRewind,
|
||||
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_FAST_REWIND), PendingIntent.FLAG_UPDATE_CURRENT));
|
||||
remoteViews.setOnClickPendingIntent(R.id.notificationFForward,
|
||||
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_FAST_FORWARD), PendingIntent.FLAG_UPDATE_CURRENT));
|
||||
|
||||
switch (basePlayerImpl.getCurrentRepeatMode()) {
|
||||
case REPEAT_DISABLED:
|
||||
remoteViews.setInt(R.id.notificationRepeat, setAlphaMethodName, 77);
|
||||
break;
|
||||
case REPEAT_ONE:
|
||||
remoteViews.setInt(R.id.notificationRepeat, setAlphaMethodName, 255);
|
||||
break;
|
||||
case REPEAT_ALL:
|
||||
// Waiting :)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the state of the player.
|
||||
* Updates the notification, and the play/pause button in it.
|
||||
* Used for changes on the remoteView
|
||||
*
|
||||
* @param drawableId if != -1, sets the drawable with that id on the play/pause button
|
||||
*/
|
||||
public static class PlaybackState implements Parcelable {
|
||||
|
||||
private static final int INDEX_IS_PLAYING = 0;
|
||||
private static final int INDEX_IS_PREPARED= 1;
|
||||
private static final int INDEX_HAS_ERROR = 2;
|
||||
private final int duration;
|
||||
private final int played;
|
||||
private final boolean[] booleanValues = new boolean[3];
|
||||
|
||||
static final PlaybackState UNPREPARED = new PlaybackState(false, false, false);
|
||||
static final PlaybackState FAILED = new PlaybackState(false, false, true);
|
||||
|
||||
|
||||
PlaybackState(Parcel in) {
|
||||
duration = in.readInt();
|
||||
played = in.readInt();
|
||||
in.readBooleanArray(booleanValues);
|
||||
private void updateNotification(int drawableId) {
|
||||
if (DEBUG) Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]");
|
||||
if (notBuilder == null) return;
|
||||
if (drawableId != -1) {
|
||||
if (notRemoteView != null) notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId);
|
||||
if (bigNotRemoteView != null) bigNotRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId);
|
||||
}
|
||||
notificationManager.notify(NOTIFICATION_ID, notBuilder.build());
|
||||
}
|
||||
|
||||
PlaybackState(int duration, int played, boolean isPlaying) {
|
||||
this.played = played;
|
||||
this.duration = duration;
|
||||
this.booleanValues[INDEX_IS_PLAYING] = isPlaying;
|
||||
this.booleanValues[INDEX_IS_PREPARED] = true;
|
||||
this.booleanValues[INDEX_HAS_ERROR] = false;
|
||||
}
|
||||
private void setControlsOpacity(@IntRange(from = 0, to = 255) int opacity) {
|
||||
if (notRemoteView != null) notRemoteView.setInt(R.id.notificationPlayPause, setAlphaMethodName, opacity);
|
||||
if (bigNotRemoteView != null) bigNotRemoteView.setInt(R.id.notificationPlayPause, setAlphaMethodName, opacity);
|
||||
if (notRemoteView != null) notRemoteView.setInt(R.id.notificationFForward, setAlphaMethodName, opacity);
|
||||
if (bigNotRemoteView != null) bigNotRemoteView.setInt(R.id.notificationFForward, setAlphaMethodName, opacity);
|
||||
if (notRemoteView != null) notRemoteView.setInt(R.id.notificationFRewind, setAlphaMethodName, opacity);
|
||||
if (bigNotRemoteView != null) bigNotRemoteView.setInt(R.id.notificationFRewind, setAlphaMethodName, opacity);
|
||||
}
|
||||
|
||||
private PlaybackState(boolean isPlaying, boolean isPrepared, boolean hasErrors) {
|
||||
this.played = 0;
|
||||
this.duration = 0;
|
||||
this.booleanValues[INDEX_IS_PLAYING] = isPlaying;
|
||||
this.booleanValues[INDEX_IS_PREPARED] = isPrepared;
|
||||
this.booleanValues[INDEX_HAS_ERROR] = hasErrors;
|
||||
}
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
int getDuration() {
|
||||
return duration;
|
||||
}
|
||||
private void lockWifiAndCpu() {
|
||||
if (DEBUG) Log.d(TAG, "lockWifiAndCpu() called");
|
||||
if (wakeLock != null && wakeLock.isHeld() && wifiLock != null && wifiLock.isHeld()) return;
|
||||
|
||||
int getPlayedTime() {
|
||||
return played;
|
||||
}
|
||||
wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
|
||||
wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL, TAG);
|
||||
|
||||
boolean isPlaying() {
|
||||
return booleanValues[INDEX_IS_PLAYING];
|
||||
}
|
||||
if (wakeLock != null) wakeLock.acquire();
|
||||
if (wifiLock != null) wifiLock.acquire();
|
||||
}
|
||||
|
||||
boolean isPrepared() {
|
||||
return booleanValues[INDEX_IS_PREPARED];
|
||||
}
|
||||
private void releaseWifiAndCpu() {
|
||||
if (DEBUG) Log.d(TAG, "releaseWifiAndCpu() called");
|
||||
if (wakeLock != null && wakeLock.isHeld()) wakeLock.release();
|
||||
if (wifiLock != null && wifiLock.isHeld()) wifiLock.release();
|
||||
|
||||
boolean hasErrors() {
|
||||
return booleanValues[INDEX_HAS_ERROR];
|
||||
}
|
||||
wakeLock = null;
|
||||
wifiLock = null;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeInt(duration);
|
||||
dest.writeInt(played);
|
||||
dest.writeBooleanArray(booleanValues);
|
||||
private class BasePlayerImpl extends BasePlayer {
|
||||
|
||||
BasePlayerImpl(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
public void handleIntent(Intent intent) {
|
||||
super.handleIntent(intent);
|
||||
Serializable serializable = intent.getSerializableExtra(BackgroundPlayer.AUDIO_STREAM);
|
||||
if (serializable instanceof AudioStream) audioStream = (AudioStream) serializable;
|
||||
playUrl(audioStream.url, MediaFormat.getSuffixById(audioStream.format), true);
|
||||
|
||||
if (bigNotRemoteView != null) bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 0, false);
|
||||
if (notRemoteView != null) notRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 0, false);
|
||||
}
|
||||
|
||||
public static final Creator<PlaybackState> CREATOR = new Creator<PlaybackState>() {
|
||||
@Override
|
||||
public PlaybackState createFromParcel(Parcel in) {
|
||||
return new PlaybackState(in);
|
||||
@Override
|
||||
public void initThumbnail() {
|
||||
if (notRemoteView != null) notRemoteView.setImageViewResource(R.id.notificationCover, R.drawable.dummy_thumbnail);
|
||||
if (bigNotRemoteView != null) bigNotRemoteView.setImageViewResource(R.id.notificationCover, R.drawable.dummy_thumbnail);
|
||||
updateNotification(-1);
|
||||
super.initThumbnail();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onThumbnailReceived(Bitmap thumbnail) {
|
||||
super.onThumbnailReceived(thumbnail);
|
||||
if (thumbnail != null) {
|
||||
if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, thumbnail);
|
||||
if (bigNotRemoteView != null) bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, thumbnail);
|
||||
updateNotification(-1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlaybackState[] newArray(int size) {
|
||||
return new PlaybackState[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
PlaybackState that = (PlaybackState) o;
|
||||
|
||||
if (duration != that.duration) return false;
|
||||
if (played != that.played) return false;
|
||||
return Arrays.equals(booleanValues, that.booleanValues);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
if(this == UNPREPARED) return 1;
|
||||
if(this == FAILED) return 2;
|
||||
int result = duration;
|
||||
result = 31 * result + played;
|
||||
result = 31 * result + Arrays.hashCode(booleanValues);
|
||||
return result + 2;
|
||||
public void playUrl(String url, String format, boolean autoPlay) {
|
||||
super.playUrl(url, format, autoPlay);
|
||||
|
||||
notBuilder = createNotification();
|
||||
startForeground(NOTIFICATION_ID, notBuilder.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepared(boolean playWhenReady) {
|
||||
super.onPrepared(playWhenReady);
|
||||
if (simpleExoPlayer.getDuration() < 15000) {
|
||||
PROGRESS_LOOP_INTERVAL = 1000;
|
||||
FAST_FORWARD_REWIND_AMOUNT = 2000;
|
||||
} else if (simpleExoPlayer.getDuration() > 60 * 60 * 1000) {
|
||||
PROGRESS_LOOP_INTERVAL = 2000;
|
||||
FAST_FORWARD_REWIND_AMOUNT = 60000;
|
||||
} else {
|
||||
PROGRESS_LOOP_INTERVAL = 2000;
|
||||
FAST_FORWARD_REWIND_AMOUNT = 10000;
|
||||
}
|
||||
basePlayerImpl.getPlayer().setVolume(1f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRepeatClicked() {
|
||||
super.onRepeatClicked();
|
||||
|
||||
int opacity = 255;
|
||||
switch (currentRepeatMode) {
|
||||
case REPEAT_DISABLED:
|
||||
opacity = 77;
|
||||
break;
|
||||
case REPEAT_ONE:
|
||||
opacity = 255;
|
||||
break;
|
||||
case REPEAT_ALL:
|
||||
// Waiting :)
|
||||
break;
|
||||
}
|
||||
if (notRemoteView != null) notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, opacity);
|
||||
if (bigNotRemoteView != null) bigNotRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, opacity);
|
||||
updateNotification(-1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpdateProgress(int currentProgress, int duration, int bufferPercent) {
|
||||
if (bigNotRemoteView != null) bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false);
|
||||
if (notRemoteView != null) notRemoteView.setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false);
|
||||
updateNotification(-1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFastRewind() {
|
||||
super.onFastRewind();
|
||||
triggerProgressUpdate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFastForward() {
|
||||
super.onFastForward();
|
||||
triggerProgressUpdate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingChanged(boolean isLoading) {
|
||||
// Disable default behavior
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
super.destroy();
|
||||
if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, null);
|
||||
if (bigNotRemoteView != null) bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception exception) {
|
||||
exception.printStackTrace();
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Broadcast Receiver
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected void setupBroadcastReceiver(IntentFilter intentFilter) {
|
||||
super.setupBroadcastReceiver(intentFilter);
|
||||
intentFilter.addAction(ACTION_CLOSE);
|
||||
intentFilter.addAction(ACTION_PLAY_PAUSE);
|
||||
intentFilter.addAction(ACTION_OPEN_DETAIL);
|
||||
intentFilter.addAction(ACTION_REPEAT);
|
||||
intentFilter.addAction(ACTION_FAST_FORWARD);
|
||||
intentFilter.addAction(ACTION_FAST_REWIND);
|
||||
|
||||
intentFilter.addAction(Intent.ACTION_SCREEN_ON);
|
||||
intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
|
||||
|
||||
intentFilter.addAction(Intent.ACTION_HEADSET_PLUG);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBroadcastReceived(Intent intent) {
|
||||
super.onBroadcastReceived(intent);
|
||||
if (DEBUG) Log.d(TAG, "onBroadcastReceived() called with: intent = [" + intent + "]");
|
||||
switch (intent.getAction()) {
|
||||
case ACTION_CLOSE:
|
||||
onClose();
|
||||
break;
|
||||
case ACTION_PLAY_PAUSE:
|
||||
onVideoPlayPause();
|
||||
break;
|
||||
case ACTION_OPEN_DETAIL:
|
||||
onOpenDetail(BackgroundPlayer.this, basePlayerImpl.getVideoUrl(), basePlayerImpl.getVideoTitle());
|
||||
break;
|
||||
case ACTION_REPEAT:
|
||||
onRepeatClicked();
|
||||
break;
|
||||
case ACTION_FAST_REWIND:
|
||||
onFastRewind();
|
||||
break;
|
||||
case ACTION_FAST_FORWARD:
|
||||
onFastForward();
|
||||
break;
|
||||
case Intent.ACTION_SCREEN_ON:
|
||||
onScreenOnOff(true);
|
||||
break;
|
||||
case Intent.ACTION_SCREEN_OFF:
|
||||
onScreenOnOff(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// States
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onLoading() {
|
||||
super.onLoading();
|
||||
|
||||
setControlsOpacity(77);
|
||||
updateNotification(-1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaying() {
|
||||
super.onPlaying();
|
||||
|
||||
setControlsOpacity(255);
|
||||
updateNotification(R.drawable.ic_pause_white);
|
||||
|
||||
lockWifiAndCpu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPaused() {
|
||||
super.onPaused();
|
||||
|
||||
updateNotification(R.drawable.ic_play_arrow_white);
|
||||
if (isProgressLoopRunning.get()) stopProgressLoop();
|
||||
|
||||
releaseWifiAndCpu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleted() {
|
||||
super.onCompleted();
|
||||
|
||||
setControlsOpacity(255);
|
||||
if (bigNotRemoteView != null) bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 100, false);
|
||||
if (notRemoteView != null) notRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 100, false);
|
||||
updateNotification(R.drawable.ic_replay_white);
|
||||
|
||||
releaseWifiAndCpu();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
718
app/src/main/java/org/schabi/newpipe/player/BasePlayer.java
Normal file
@@ -0,0 +1,718 @@
|
||||
package org.schabi.newpipe.player;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
import android.media.AudioManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.DefaultLoadControl;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
|
||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor;
|
||||
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Formatter;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Base for the players, joining the common properties
|
||||
*
|
||||
* @author mauriciocolli
|
||||
*/
|
||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||
public abstract class BasePlayer implements ExoPlayer.EventListener, AudioManager.OnAudioFocusChangeListener {
|
||||
public static final boolean DEBUG = false;
|
||||
public static final String TAG = "BasePlayer";
|
||||
|
||||
protected Context context;
|
||||
protected SharedPreferences sharedPreferences;
|
||||
protected AudioManager audioManager;
|
||||
|
||||
protected BroadcastReceiver broadcastReceiver;
|
||||
protected IntentFilter intentFilter;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Intent
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public static final String VIDEO_URL = "video_url";
|
||||
public static final String VIDEO_TITLE = "video_title";
|
||||
public static final String VIDEO_THUMBNAIL_URL = "video_thumbnail_url";
|
||||
public static final String START_POSITION = "start_position";
|
||||
public static final String CHANNEL_NAME = "channel_name";
|
||||
|
||||
protected Bitmap videoThumbnail = null;
|
||||
protected String videoUrl = "";
|
||||
protected String videoTitle = "";
|
||||
protected String videoThumbnailUrl = "";
|
||||
protected int videoStartPos = -1;
|
||||
protected String channelName = "";
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Player
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public int FAST_FORWARD_REWIND_AMOUNT = 10000; // 10 Seconds
|
||||
public static final String CACHE_FOLDER_NAME = "exoplayer";
|
||||
|
||||
protected SimpleExoPlayer simpleExoPlayer;
|
||||
protected boolean isPrepared = false;
|
||||
|
||||
protected MediaSource mediaSource;
|
||||
protected CacheDataSourceFactory cacheDataSourceFactory;
|
||||
protected final DefaultExtractorsFactory extractorsFactory = new DefaultExtractorsFactory();
|
||||
protected final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
|
||||
|
||||
protected int PROGRESS_LOOP_INTERVAL = 100;
|
||||
protected AtomicBoolean isProgressLoopRunning = new AtomicBoolean();
|
||||
protected Handler progressLoop;
|
||||
protected Runnable progressUpdate;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public BasePlayer(Context context) {
|
||||
this.context = context;
|
||||
this.progressLoop = new Handler();
|
||||
this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
this.audioManager = ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE));
|
||||
|
||||
this.broadcastReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
onBroadcastReceived(intent);
|
||||
}
|
||||
};
|
||||
this.intentFilter = new IntentFilter();
|
||||
setupBroadcastReceiver(intentFilter);
|
||||
context.registerReceiver(broadcastReceiver, intentFilter);
|
||||
}
|
||||
|
||||
public void setup() {
|
||||
if (simpleExoPlayer == null) initPlayer();
|
||||
initListeners();
|
||||
}
|
||||
|
||||
private void initExoPlayerCache() {
|
||||
if (cacheDataSourceFactory == null) {
|
||||
DefaultDataSourceFactory dataSourceFactory = new DefaultDataSourceFactory(context, Util.getUserAgent(context, context.getPackageName()), bandwidthMeter);
|
||||
File cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME);
|
||||
if (!cacheDir.exists()) {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
cacheDir.mkdir();
|
||||
}
|
||||
|
||||
if (DEBUG) Log.d(TAG, "initExoPlayerCache: cacheDir = " + cacheDir.getAbsolutePath());
|
||||
SimpleCache simpleCache = new SimpleCache(cacheDir, new LeastRecentlyUsedCacheEvictor(64 * 1024 * 1024L));
|
||||
cacheDataSourceFactory = new CacheDataSourceFactory(simpleCache, dataSourceFactory, CacheDataSource.FLAG_BLOCK_ON_CACHE, 512 * 1024);
|
||||
}
|
||||
}
|
||||
|
||||
public void initPlayer() {
|
||||
if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]");
|
||||
initExoPlayerCache();
|
||||
|
||||
AdaptiveTrackSelection.Factory trackSelectionFactory = new AdaptiveTrackSelection.Factory(bandwidthMeter);
|
||||
DefaultTrackSelector defaultTrackSelector = new DefaultTrackSelector(trackSelectionFactory);
|
||||
DefaultLoadControl loadControl = new DefaultLoadControl();
|
||||
|
||||
simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(context, defaultTrackSelector, loadControl);
|
||||
simpleExoPlayer.addListener(this);
|
||||
}
|
||||
|
||||
public void initListeners() {
|
||||
progressUpdate = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
//if(DEBUG) Log.d(TAG, "progressUpdate run() called");
|
||||
onUpdateProgress((int) simpleExoPlayer.getCurrentPosition(), (int) simpleExoPlayer.getDuration(), simpleExoPlayer.getBufferedPercentage());
|
||||
if (isProgressLoopRunning.get()) progressLoop.postDelayed(this, PROGRESS_LOOP_INTERVAL);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void handleIntent(Intent intent) {
|
||||
if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]");
|
||||
if (intent == null) return;
|
||||
|
||||
videoUrl = intent.getStringExtra(VIDEO_URL);
|
||||
videoTitle = intent.getStringExtra(VIDEO_TITLE);
|
||||
videoThumbnailUrl = intent.getStringExtra(VIDEO_THUMBNAIL_URL);
|
||||
videoStartPos = intent.getIntExtra(START_POSITION, -1);
|
||||
channelName = intent.getStringExtra(CHANNEL_NAME);
|
||||
|
||||
initThumbnail();
|
||||
//play(getSelectedVideoStream(), true);
|
||||
}
|
||||
|
||||
public void initThumbnail() {
|
||||
if (DEBUG) Log.d(TAG, "initThumbnail() called");
|
||||
videoThumbnail = null;
|
||||
if (videoThumbnailUrl == null || videoThumbnailUrl.isEmpty()) return;
|
||||
ImageLoader.getInstance().resume();
|
||||
ImageLoader.getInstance().loadImage(videoThumbnailUrl, new SimpleImageLoadingListener() {
|
||||
@Override
|
||||
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
|
||||
if (simpleExoPlayer == null) return;
|
||||
if (DEBUG) Log.d(TAG, "onLoadingComplete() called with: imageUri = [" + imageUri + "], view = [" + view + "], loadedImage = [" + loadedImage + "]");
|
||||
videoThumbnail = loadedImage;
|
||||
onThumbnailReceived(loadedImage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void playUrl(String url, String format, boolean autoPlay) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "play() called with: url = [" + url + "], autoPlay = [" + autoPlay + "]");
|
||||
}
|
||||
|
||||
if (url == null || simpleExoPlayer == null) {
|
||||
RuntimeException runtimeException = new RuntimeException((url == null ? "Url " : "Player ") + " null");
|
||||
onError(runtimeException);
|
||||
throw runtimeException;
|
||||
}
|
||||
|
||||
changeState(STATE_LOADING);
|
||||
|
||||
isPrepared = false;
|
||||
mediaSource = buildMediaSource(url, format);
|
||||
|
||||
if (simpleExoPlayer.getPlaybackState() != ExoPlayer.STATE_IDLE) simpleExoPlayer.stop();
|
||||
if (videoStartPos > 0) simpleExoPlayer.seekTo(videoStartPos);
|
||||
simpleExoPlayer.prepare(mediaSource);
|
||||
simpleExoPlayer.setPlayWhenReady(autoPlay);
|
||||
}
|
||||
|
||||
public void destroyPlayer() {
|
||||
if (DEBUG) Log.d(TAG, "destroyPlayer() called");
|
||||
if (simpleExoPlayer != null) {
|
||||
simpleExoPlayer.stop();
|
||||
simpleExoPlayer.release();
|
||||
}
|
||||
if (progressLoop != null && isProgressLoopRunning.get()) stopProgressLoop();
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
if (DEBUG) Log.d(TAG, "destroy() called");
|
||||
destroyPlayer();
|
||||
unregisterBroadcastReceiver();
|
||||
videoThumbnail = null;
|
||||
simpleExoPlayer = null;
|
||||
}
|
||||
|
||||
public MediaSource buildMediaSource(String url, String overrideExtension) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "buildMediaSource() called with: url = [" + url + "], overrideExtension = [" + overrideExtension + "]");
|
||||
}
|
||||
Uri uri = Uri.parse(url);
|
||||
int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension);
|
||||
MediaSource mediaSource;
|
||||
switch (type) {
|
||||
case C.TYPE_SS:
|
||||
mediaSource = new SsMediaSource(uri, cacheDataSourceFactory, new DefaultSsChunkSource.Factory(cacheDataSourceFactory), null, null);
|
||||
break;
|
||||
case C.TYPE_DASH:
|
||||
mediaSource = new DashMediaSource(uri, cacheDataSourceFactory, new DefaultDashChunkSource.Factory(cacheDataSourceFactory), null, null);
|
||||
break;
|
||||
case C.TYPE_HLS:
|
||||
mediaSource = new HlsMediaSource(uri, cacheDataSourceFactory, null, null);
|
||||
break;
|
||||
case C.TYPE_OTHER:
|
||||
mediaSource = new ExtractorMediaSource(uri, cacheDataSourceFactory, extractorsFactory, null, null);
|
||||
break;
|
||||
default: {
|
||||
throw new IllegalStateException("Unsupported type: " + type);
|
||||
}
|
||||
}
|
||||
return mediaSource;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Broadcast Receiver
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
/**
|
||||
* Add your action in the intentFilter
|
||||
*
|
||||
* @param intentFilter intent filter that will be used for register the receiver
|
||||
*/
|
||||
protected void setupBroadcastReceiver(IntentFilter intentFilter) {
|
||||
intentFilter.addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
|
||||
}
|
||||
|
||||
public void onBroadcastReceived(Intent intent) {
|
||||
switch (intent.getAction()) {
|
||||
case AudioManager.ACTION_AUDIO_BECOMING_NOISY:
|
||||
if (isPlaying()) simpleExoPlayer.setPlayWhenReady(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void unregisterBroadcastReceiver() {
|
||||
if (broadcastReceiver != null && context != null) {
|
||||
context.unregisterReceiver(broadcastReceiver);
|
||||
broadcastReceiver = null;
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// AudioFocus
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private static final int DUCK_DURATION = 1500;
|
||||
private static final float DUCK_AUDIO_TO = .2f;
|
||||
|
||||
@Override
|
||||
public void onAudioFocusChange(int focusChange) {
|
||||
if (DEBUG) Log.d(TAG, "onAudioFocusChange() called with: focusChange = [" + focusChange + "]");
|
||||
if (simpleExoPlayer == null) return;
|
||||
switch (focusChange) {
|
||||
case AudioManager.AUDIOFOCUS_GAIN:
|
||||
onAudioFocusGain();
|
||||
break;
|
||||
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
|
||||
onAudioFocusLossCanDuck();
|
||||
break;
|
||||
case AudioManager.AUDIOFOCUS_LOSS:
|
||||
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
|
||||
onAudioFocusLoss();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected void onAudioFocusGain() {
|
||||
if (DEBUG) Log.d(TAG, "onAudioFocusGain() called");
|
||||
if (simpleExoPlayer != null) simpleExoPlayer.setVolume(DUCK_AUDIO_TO);
|
||||
animateAudio(DUCK_AUDIO_TO, 1f, DUCK_DURATION);
|
||||
simpleExoPlayer.setPlayWhenReady(true);
|
||||
}
|
||||
|
||||
protected void onAudioFocusLoss() {
|
||||
if (DEBUG) Log.d(TAG, "onAudioFocusLoss() called");
|
||||
simpleExoPlayer.setPlayWhenReady(false);
|
||||
}
|
||||
|
||||
protected void onAudioFocusLossCanDuck() {
|
||||
if (DEBUG) Log.d(TAG, "onAudioFocusLossCanDuck() called");
|
||||
// Set the volume to 1/10 on ducking
|
||||
animateAudio(simpleExoPlayer.getVolume(), DUCK_AUDIO_TO, DUCK_DURATION);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// States Implementation
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public static final int STATE_LOADING = 123;
|
||||
public static final int STATE_PLAYING = 124;
|
||||
public static final int STATE_BUFFERING = 125;
|
||||
public static final int STATE_PAUSED = 126;
|
||||
public static final int STATE_PAUSED_SEEK = 127;
|
||||
public static final int STATE_COMPLETED = 128;
|
||||
|
||||
|
||||
protected int currentState = -1;
|
||||
|
||||
public void changeState(int state) {
|
||||
if (DEBUG) Log.d(TAG, "changeState() called with: state = [" + state + "]");
|
||||
currentState = state;
|
||||
switch (state) {
|
||||
case STATE_LOADING:
|
||||
onLoading();
|
||||
break;
|
||||
case STATE_PLAYING:
|
||||
onPlaying();
|
||||
break;
|
||||
case STATE_BUFFERING:
|
||||
onBuffering();
|
||||
break;
|
||||
case STATE_PAUSED:
|
||||
onPaused();
|
||||
break;
|
||||
case STATE_PAUSED_SEEK:
|
||||
onPausedSeek();
|
||||
break;
|
||||
case STATE_COMPLETED:
|
||||
onCompleted();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void onLoading() {
|
||||
if (DEBUG) Log.d(TAG, "onLoading() called");
|
||||
if (!isProgressLoopRunning.get()) startProgressLoop();
|
||||
}
|
||||
|
||||
public void onPlaying() {
|
||||
if (DEBUG) Log.d(TAG, "onPlaying() called");
|
||||
if (!isProgressLoopRunning.get()) startProgressLoop();
|
||||
}
|
||||
|
||||
public void onBuffering() {
|
||||
}
|
||||
|
||||
public void onPaused() {
|
||||
if (isProgressLoopRunning.get()) stopProgressLoop();
|
||||
}
|
||||
|
||||
public void onPausedSeek() {
|
||||
}
|
||||
|
||||
public void onCompleted() {
|
||||
if (DEBUG) Log.d(TAG, "onCompleted() called");
|
||||
if (isProgressLoopRunning.get()) stopProgressLoop();
|
||||
|
||||
if (currentRepeatMode == RepeatMode.REPEAT_ONE) {
|
||||
changeState(STATE_LOADING);
|
||||
simpleExoPlayer.seekTo(0);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Repeat
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected RepeatMode currentRepeatMode = RepeatMode.REPEAT_DISABLED;
|
||||
|
||||
public enum RepeatMode {
|
||||
REPEAT_DISABLED,
|
||||
REPEAT_ONE,
|
||||
REPEAT_ALL
|
||||
}
|
||||
|
||||
public void onRepeatClicked() {
|
||||
if (DEBUG) Log.d(TAG, "onRepeatClicked() called");
|
||||
// TODO: implement repeat all when playlist is implemented
|
||||
|
||||
// Switch the modes between DISABLED and REPEAT_ONE, till playlist is implemented
|
||||
setCurrentRepeatMode(getCurrentRepeatMode() == RepeatMode.REPEAT_DISABLED ?
|
||||
RepeatMode.REPEAT_ONE :
|
||||
RepeatMode.REPEAT_DISABLED);
|
||||
|
||||
if (DEBUG) Log.d(TAG, "onRepeatClicked() currentRepeatMode = " + getCurrentRepeatMode().name());
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// ExoPlayer Listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, Object manifest) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingChanged(boolean isLoading) {
|
||||
if (DEBUG) Log.d(TAG, "onLoadingChanged() called with: isLoading = [" + isLoading + "]");
|
||||
|
||||
if (!isLoading && getCurrentState() == STATE_PAUSED && isProgressLoopRunning.get()) stopProgressLoop();
|
||||
else if (isLoading && !isProgressLoopRunning.get()) startProgressLoop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
if (DEBUG) Log.d(TAG, "onPlayerStateChanged() called with: playWhenReady = [" + playWhenReady + "], playbackState = [" + playbackState + "]");
|
||||
if (getCurrentState() == STATE_PAUSED_SEEK) {
|
||||
if (DEBUG) Log.d(TAG, "onPlayerStateChanged() currently on PausedSeek");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (playbackState) {
|
||||
case ExoPlayer.STATE_IDLE: // 1
|
||||
isPrepared = false;
|
||||
break;
|
||||
case ExoPlayer.STATE_BUFFERING: // 2
|
||||
if (isPrepared && getCurrentState() != STATE_LOADING) changeState(STATE_BUFFERING);
|
||||
break;
|
||||
case ExoPlayer.STATE_READY: //3
|
||||
if (!isPrepared) {
|
||||
isPrepared = true;
|
||||
onPrepared(playWhenReady);
|
||||
break;
|
||||
}
|
||||
if (currentState == STATE_PAUSED_SEEK) break;
|
||||
changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED);
|
||||
break;
|
||||
case ExoPlayer.STATE_ENDED: // 4
|
||||
changeState(STATE_COMPLETED);
|
||||
isPrepared = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(ExoPlaybackException error) {
|
||||
if (DEBUG) Log.d(TAG, "onPlayerError() called with: error = [" + error + "]");
|
||||
onError(error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity() {
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// General Player
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public abstract void onError(Exception exception);
|
||||
|
||||
public void onPrepared(boolean playWhenReady) {
|
||||
if (DEBUG) Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]");
|
||||
if (playWhenReady) audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
|
||||
changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED);
|
||||
}
|
||||
|
||||
public abstract void onUpdateProgress(int currentProgress, int duration, int bufferPercent);
|
||||
|
||||
public void onVideoPlayPause() {
|
||||
if (DEBUG) Log.d(TAG, "onVideoPlayPause() called");
|
||||
|
||||
if (currentState == STATE_COMPLETED) {
|
||||
onVideoPlayPauseRepeat();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isPlaying()) audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
|
||||
else audioManager.abandonAudioFocus(null);
|
||||
|
||||
simpleExoPlayer.setPlayWhenReady(!isPlaying());
|
||||
}
|
||||
|
||||
public void onVideoPlayPauseRepeat() {
|
||||
if (DEBUG) Log.d(TAG, "onVideoPlayPauseRepeat() called");
|
||||
changeState(STATE_LOADING);
|
||||
setVideoStartPos(0);
|
||||
simpleExoPlayer.seekTo(0);
|
||||
simpleExoPlayer.setPlayWhenReady(true);
|
||||
audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
|
||||
}
|
||||
|
||||
public void onFastRewind() {
|
||||
if (DEBUG) Log.d(TAG, "onFastRewind() called");
|
||||
seekBy(-FAST_FORWARD_REWIND_AMOUNT);
|
||||
}
|
||||
|
||||
public void onFastForward() {
|
||||
if (DEBUG) Log.d(TAG, "onFastForward() called");
|
||||
seekBy(FAST_FORWARD_REWIND_AMOUNT);
|
||||
}
|
||||
|
||||
public void onThumbnailReceived(Bitmap thumbnail) {
|
||||
if (DEBUG) Log.d(TAG, "onThumbnailReceived() called with: thumbnail = [" + thumbnail + "]");
|
||||
}
|
||||
|
||||
public void seekBy(int milliSeconds) {
|
||||
if (DEBUG) Log.d(TAG, "seekBy() called with: milliSeconds = [" + milliSeconds + "]");
|
||||
if (simpleExoPlayer == null || (isCompleted() && milliSeconds > 0) || ((milliSeconds < 0 && simpleExoPlayer.getCurrentPosition() == 0))) return;
|
||||
int progress = (int) (simpleExoPlayer.getCurrentPosition() + milliSeconds);
|
||||
if (progress < 0) progress = 0;
|
||||
simpleExoPlayer.seekTo(progress);
|
||||
}
|
||||
|
||||
public boolean isPlaying() {
|
||||
return simpleExoPlayer.getPlaybackState() == ExoPlayer.STATE_READY && simpleExoPlayer.getPlayWhenReady();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private final StringBuilder stringBuilder = new StringBuilder();
|
||||
private final Formatter formatter = new Formatter(stringBuilder, Locale.getDefault());
|
||||
|
||||
public String getTimeString(int milliSeconds) {
|
||||
long seconds = (milliSeconds % 60000L) / 1000L;
|
||||
long minutes = (milliSeconds % 3600000L) / 60000L;
|
||||
long hours = (milliSeconds % 86400000L) / 3600000L;
|
||||
long days = (milliSeconds % (86400000L * 7L)) / 86400000L;
|
||||
|
||||
stringBuilder.setLength(0);
|
||||
return days > 0 ? formatter.format("%d:%02d:%02d:%02d", days, hours, minutes, seconds).toString()
|
||||
: hours > 0 ? formatter.format("%d:%02d:%02d", hours, minutes, seconds).toString()
|
||||
: formatter.format("%02d:%02d", minutes, seconds).toString();
|
||||
}
|
||||
|
||||
protected void startProgressLoop() {
|
||||
progressLoop.removeCallbacksAndMessages(null);
|
||||
isProgressLoopRunning.set(true);
|
||||
progressLoop.post(progressUpdate);
|
||||
}
|
||||
|
||||
protected void stopProgressLoop() {
|
||||
isProgressLoopRunning.set(false);
|
||||
progressLoop.removeCallbacksAndMessages(null);
|
||||
}
|
||||
|
||||
protected void tryDeleteCacheFiles(Context context) {
|
||||
File cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME);
|
||||
|
||||
if (cacheDir.exists()) {
|
||||
try {
|
||||
if (cacheDir.isDirectory()) {
|
||||
for (File file : cacheDir.listFiles()) {
|
||||
try {
|
||||
if (DEBUG) Log.d(TAG, "tryDeleteCacheFiles: " + file.getAbsolutePath() + " deleted = " + file.delete());
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void triggerProgressUpdate() {
|
||||
onUpdateProgress((int) simpleExoPlayer.getCurrentPosition(), (int) simpleExoPlayer.getDuration(), simpleExoPlayer.getBufferedPercentage());
|
||||
}
|
||||
|
||||
public void animateAudio(final float from, final float to, int duration) {
|
||||
ValueAnimator valueAnimator = new ValueAnimator();
|
||||
valueAnimator.setFloatValues(from, to);
|
||||
valueAnimator.setDuration(duration);
|
||||
valueAnimator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animation) {
|
||||
if (simpleExoPlayer != null) simpleExoPlayer.setVolume(from);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(Animator animation) {
|
||||
if (simpleExoPlayer != null) simpleExoPlayer.setVolume(to);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
if (simpleExoPlayer != null) simpleExoPlayer.setVolume(to);
|
||||
}
|
||||
});
|
||||
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator animation) {
|
||||
if (simpleExoPlayer != null) simpleExoPlayer.setVolume(((float) animation.getAnimatedValue()));
|
||||
}
|
||||
});
|
||||
valueAnimator.start();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Getters and Setters
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public SimpleExoPlayer getPlayer() {
|
||||
return simpleExoPlayer;
|
||||
}
|
||||
|
||||
public SharedPreferences getSharedPreferences() {
|
||||
return sharedPreferences;
|
||||
}
|
||||
|
||||
public RepeatMode getCurrentRepeatMode() {
|
||||
return currentRepeatMode;
|
||||
}
|
||||
|
||||
public void setCurrentRepeatMode(RepeatMode mode) {
|
||||
currentRepeatMode = mode;
|
||||
}
|
||||
|
||||
public int getCurrentState() {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
public String getVideoUrl() {
|
||||
return videoUrl;
|
||||
}
|
||||
|
||||
public void setVideoUrl(String videoUrl) {
|
||||
this.videoUrl = videoUrl;
|
||||
}
|
||||
|
||||
public int getVideoStartPos() {
|
||||
return videoStartPos;
|
||||
}
|
||||
|
||||
public void setVideoStartPos(int videoStartPos) {
|
||||
this.videoStartPos = videoStartPos;
|
||||
}
|
||||
|
||||
public String getVideoTitle() {
|
||||
return videoTitle;
|
||||
}
|
||||
|
||||
public void setVideoTitle(String videoTitle) {
|
||||
this.videoTitle = videoTitle;
|
||||
}
|
||||
|
||||
public String getChannelName() {
|
||||
return channelName;
|
||||
}
|
||||
|
||||
public void setChannelName(String channelName) {
|
||||
this.channelName = channelName;
|
||||
}
|
||||
|
||||
public boolean isCompleted() {
|
||||
return simpleExoPlayer != null && simpleExoPlayer.getPlaybackState() == ExoPlayer.STATE_ENDED;
|
||||
}
|
||||
|
||||
public boolean isPrepared() {
|
||||
return isPrepared;
|
||||
}
|
||||
|
||||
public void setPrepared(boolean prepared) {
|
||||
isPrepared = prepared;
|
||||
}
|
||||
|
||||
public Bitmap getVideoThumbnail() {
|
||||
return videoThumbnail;
|
||||
}
|
||||
|
||||
public void setVideoThumbnail(Bitmap videoThumbnail) {
|
||||
this.videoThumbnail = videoThumbnail;
|
||||
}
|
||||
|
||||
public String getVideoThumbnailUrl() {
|
||||
return videoThumbnailUrl;
|
||||
}
|
||||
|
||||
public void setVideoThumbnailUrl(String videoThumbnailUrl) {
|
||||
this.videoThumbnailUrl = videoThumbnailUrl;
|
||||
}
|
||||
}
|
||||
@@ -1,564 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Extended by Christian Schabesberger on 24.12.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* ExoPlayerActivity.java is part of NewPipe. all changes are under GPL3
|
||||
*
|
||||
* 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.player;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.player.exoplayer.DashRendererBuilder;
|
||||
import org.schabi.newpipe.player.exoplayer.EventLogger;
|
||||
import org.schabi.newpipe.player.exoplayer.ExtractorRendererBuilder;
|
||||
import org.schabi.newpipe.player.exoplayer.HlsRendererBuilder;
|
||||
import org.schabi.newpipe.player.exoplayer.NPExoPlayer;
|
||||
import org.schabi.newpipe.player.exoplayer.NPExoPlayer.RendererBuilder;
|
||||
import org.schabi.newpipe.player.exoplayer.SmoothStreamingRendererBuilder;
|
||||
|
||||
import com.google.android.exoplayer.AspectRatioFrameLayout;
|
||||
import com.google.android.exoplayer.ExoPlaybackException;
|
||||
import com.google.android.exoplayer.ExoPlayer;
|
||||
import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException;
|
||||
import com.google.android.exoplayer.MediaCodecUtil.DecoderQueryException;
|
||||
import com.google.android.exoplayer.MediaFormat;
|
||||
import com.google.android.exoplayer.audio.AudioCapabilities;
|
||||
import com.google.android.exoplayer.audio.AudioCapabilitiesReceiver;
|
||||
import com.google.android.exoplayer.drm.UnsupportedDrmException;
|
||||
import com.google.android.exoplayer.metadata.GeobMetadata;
|
||||
import com.google.android.exoplayer.metadata.PrivMetadata;
|
||||
import com.google.android.exoplayer.metadata.TxxxMetadata;
|
||||
import com.google.android.exoplayer.text.CaptionStyleCompat;
|
||||
import com.google.android.exoplayer.text.Cue;
|
||||
import com.google.android.exoplayer.text.SubtitleLayout;
|
||||
import com.google.android.exoplayer.util.DebugTextViewHelper;
|
||||
import com.google.android.exoplayer.util.MimeTypes;
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
import com.google.android.exoplayer.util.VerboseLogUtil;
|
||||
|
||||
import android.Manifest.permission;
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.View;
|
||||
import android.view.View.OnKeyListener;
|
||||
import android.view.View.OnTouchListener;
|
||||
import android.view.accessibility.CaptioningManager;
|
||||
import android.widget.MediaController;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.PopupMenu.OnMenuItemClickListener;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.net.CookieHandler;
|
||||
import java.net.CookieManager;
|
||||
import java.net.CookiePolicy;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* An activity that plays media using {@link NPExoPlayer}.
|
||||
*/
|
||||
public class ExoPlayerActivity extends Activity {
|
||||
|
||||
// For use within demo app code.
|
||||
public static final String CONTENT_ID_EXTRA = "content_id";
|
||||
public static final String CONTENT_TYPE_EXTRA = "content_type";
|
||||
public static final String PROVIDER_EXTRA = "provider";
|
||||
|
||||
// For use when launching the demo app using adb.
|
||||
private static final String CONTENT_EXT_EXTRA = "type";
|
||||
|
||||
private static final String TAG = "PlayerActivity";
|
||||
private static final int MENU_GROUP_TRACKS = 1;
|
||||
private static final int ID_OFFSET = 2;
|
||||
|
||||
private static final CookieManager defaultCookieManager;
|
||||
static {
|
||||
defaultCookieManager = new CookieManager();
|
||||
defaultCookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
|
||||
}
|
||||
|
||||
private EventLogger eventLogger;
|
||||
private MediaController mediaController;
|
||||
private View shutterView;
|
||||
private AspectRatioFrameLayout videoFrame;
|
||||
private SurfaceView surfaceView;
|
||||
private SubtitleLayout subtitleLayout;
|
||||
|
||||
private NPExoPlayer player;
|
||||
private boolean playerNeedsPrepare;
|
||||
|
||||
private long playerPosition;
|
||||
private boolean enableBackgroundAudio = true;
|
||||
|
||||
private Uri contentUri;
|
||||
private int contentType;
|
||||
private String contentId;
|
||||
private String provider;
|
||||
|
||||
private AudioCapabilitiesReceiver audioCapabilitiesReceiver;
|
||||
|
||||
|
||||
NPExoPlayer.Listener exoPlayerListener = new NPExoPlayer.Listener() {
|
||||
@Override
|
||||
public void onStateChanged(boolean playWhenReady, int playbackState) {
|
||||
if (playbackState == ExoPlayer.STATE_ENDED) {
|
||||
showControls();
|
||||
}
|
||||
String text = "playWhenReady=" + playWhenReady + ", playbackState=";
|
||||
switch(playbackState) {
|
||||
case ExoPlayer.STATE_BUFFERING:
|
||||
text += "buffering";
|
||||
break;
|
||||
case ExoPlayer.STATE_ENDED:
|
||||
text += "ended";
|
||||
break;
|
||||
case ExoPlayer.STATE_IDLE:
|
||||
text += "idle";
|
||||
break;
|
||||
case ExoPlayer.STATE_PREPARING:
|
||||
text += "preparing";
|
||||
break;
|
||||
case ExoPlayer.STATE_READY:
|
||||
text += "ready";
|
||||
break;
|
||||
default:
|
||||
text += "unknown";
|
||||
break;
|
||||
}
|
||||
//todo: put text in some log
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception e) {
|
||||
String errorString = null;
|
||||
if (e instanceof UnsupportedDrmException) {
|
||||
// Special case DRM failures.
|
||||
UnsupportedDrmException unsupportedDrmException = (UnsupportedDrmException) e;
|
||||
errorString = getString(Util.SDK_INT < 18 ? R.string.error_drm_not_supported
|
||||
: unsupportedDrmException.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME
|
||||
? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown);
|
||||
} else if (e instanceof ExoPlaybackException
|
||||
&& e.getCause() instanceof DecoderInitializationException) {
|
||||
// Special case for decoder initialization failures.
|
||||
DecoderInitializationException decoderInitializationException =
|
||||
(DecoderInitializationException) e.getCause();
|
||||
if (decoderInitializationException.decoderName == null) {
|
||||
if (decoderInitializationException.getCause() instanceof DecoderQueryException) {
|
||||
errorString = getString(R.string.error_querying_decoders);
|
||||
} else if (decoderInitializationException.secureDecoderRequired) {
|
||||
errorString = getString(R.string.error_no_secure_decoder,
|
||||
decoderInitializationException.mimeType);
|
||||
} else {
|
||||
errorString = getString(R.string.error_no_decoder,
|
||||
decoderInitializationException.mimeType);
|
||||
}
|
||||
} else {
|
||||
errorString = getString(R.string.error_instantiating_decoder,
|
||||
decoderInitializationException.decoderName);
|
||||
}
|
||||
}
|
||||
if (errorString != null) {
|
||||
Toast.makeText(getApplicationContext(), errorString, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
playerNeedsPrepare = true;
|
||||
showControls();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthAspectRatio) {
|
||||
shutterView.setVisibility(View.GONE);
|
||||
videoFrame.setAspectRatio(
|
||||
height == 0 ? 1 : (width * pixelWidthAspectRatio) / height);
|
||||
}
|
||||
};
|
||||
|
||||
SurfaceHolder.Callback surfaceHolderCallback = new SurfaceHolder.Callback() {
|
||||
@Override
|
||||
public void surfaceCreated(SurfaceHolder holder) {
|
||||
if (player != null) {
|
||||
player.setSurface(holder.getSurface());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||
if (player != null) {
|
||||
player.blockingClearSurface();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
NPExoPlayer.CaptionListener captionListener = new NPExoPlayer.CaptionListener() {
|
||||
@Override
|
||||
public void onCues(List<Cue> cues) {
|
||||
subtitleLayout.setCues(cues);
|
||||
}
|
||||
};
|
||||
|
||||
NPExoPlayer.Id3MetadataListener id3MetadataListener = new NPExoPlayer.Id3MetadataListener() {
|
||||
@Override
|
||||
public void onId3Metadata(Map<String, Object> metadata) {
|
||||
for (Map.Entry<String, Object> entry : metadata.entrySet()) {
|
||||
if (TxxxMetadata.TYPE.equals(entry.getKey())) {
|
||||
TxxxMetadata txxxMetadata = (TxxxMetadata) entry.getValue();
|
||||
Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s, value=%s",
|
||||
TxxxMetadata.TYPE, txxxMetadata.description, txxxMetadata.value));
|
||||
} else if (PrivMetadata.TYPE.equals(entry.getKey())) {
|
||||
PrivMetadata privMetadata = (PrivMetadata) entry.getValue();
|
||||
Log.i(TAG, String.format("ID3 TimedMetadata %s: owner=%s",
|
||||
PrivMetadata.TYPE, privMetadata.owner));
|
||||
} else if (GeobMetadata.TYPE.equals(entry.getKey())) {
|
||||
GeobMetadata geobMetadata = (GeobMetadata) entry.getValue();
|
||||
Log.i(TAG, String.format("ID3 TimedMetadata %s: mimeType=%s, filename=%s, description=%s",
|
||||
GeobMetadata.TYPE, geobMetadata.mimeType, geobMetadata.filename,
|
||||
geobMetadata.description));
|
||||
} else {
|
||||
Log.i(TAG, String.format("ID3 TimedMetadata %s", entry.getKey()));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AudioCapabilitiesReceiver.Listener audioCapabilitiesListener = new AudioCapabilitiesReceiver.Listener() {
|
||||
@Override
|
||||
public void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) {
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
boolean backgrounded = player.getBackgrounded();
|
||||
boolean playWhenReady = player.getPlayWhenReady();
|
||||
releasePlayer();
|
||||
preparePlayer(playWhenReady);
|
||||
player.setBackgrounded(backgrounded);
|
||||
}
|
||||
};
|
||||
|
||||
// Activity lifecycle
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.exo_player_activity);
|
||||
View root = findViewById(R.id.root);
|
||||
root.setOnTouchListener(new OnTouchListener() {
|
||||
@Override
|
||||
public boolean onTouch(View view, MotionEvent motionEvent) {
|
||||
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
toggleControlsVisibility();
|
||||
} else if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
|
||||
view.performClick();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
root.setOnKeyListener(new OnKeyListener() {
|
||||
@Override
|
||||
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE
|
||||
|| keyCode == KeyEvent.KEYCODE_MENU) {
|
||||
return false;
|
||||
}
|
||||
return mediaController.dispatchKeyEvent(event);
|
||||
}
|
||||
});
|
||||
|
||||
shutterView = findViewById(R.id.shutter);
|
||||
|
||||
videoFrame = (AspectRatioFrameLayout) findViewById(R.id.video_frame);
|
||||
surfaceView = (SurfaceView) findViewById(R.id.surface_view);
|
||||
surfaceView.getHolder().addCallback(surfaceHolderCallback);
|
||||
subtitleLayout = (SubtitleLayout) findViewById(R.id.subtitles);
|
||||
|
||||
//todo: replace that creapy mediaController
|
||||
mediaController = new KeyCompatibleMediaController(this);
|
||||
mediaController.setAnchorView(root);
|
||||
|
||||
//todo: check what cookie handler does, and if we even need it
|
||||
CookieHandler currentHandler = CookieHandler.getDefault();
|
||||
if (currentHandler != defaultCookieManager) {
|
||||
CookieHandler.setDefault(defaultCookieManager);
|
||||
}
|
||||
|
||||
audioCapabilitiesReceiver = new AudioCapabilitiesReceiver(this, audioCapabilitiesListener);
|
||||
audioCapabilitiesReceiver.register();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewIntent(Intent intent) {
|
||||
releasePlayer();
|
||||
playerPosition = 0;
|
||||
setIntent(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
Intent intent = getIntent();
|
||||
contentUri = intent.getData();
|
||||
contentType = intent.getIntExtra(CONTENT_TYPE_EXTRA,
|
||||
inferContentType(contentUri, intent.getStringExtra(CONTENT_EXT_EXTRA)));
|
||||
contentId = intent.getStringExtra(CONTENT_ID_EXTRA);
|
||||
provider = intent.getStringExtra(PROVIDER_EXTRA);
|
||||
configureSubtitleView();
|
||||
if (player == null) {
|
||||
if (!maybeRequestPermission()) {
|
||||
preparePlayer(true);
|
||||
}
|
||||
} else {
|
||||
player.setBackgrounded(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
if (!enableBackgroundAudio) {
|
||||
releasePlayer();
|
||||
} else {
|
||||
player.setBackgrounded(true);
|
||||
}
|
||||
shutterView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
audioCapabilitiesReceiver.unregister();
|
||||
releasePlayer();
|
||||
}
|
||||
|
||||
|
||||
// Permission request listener method
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, String[] permissions,
|
||||
int[] grantResults) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
preparePlayer(true);
|
||||
} else {
|
||||
Toast.makeText(getApplicationContext(), R.string.storage_permission_denied,
|
||||
Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
// Permission management methods
|
||||
|
||||
/**
|
||||
* Checks whether it is necessary to ask for permission to read storage. If necessary, it also
|
||||
* requests permission.
|
||||
*
|
||||
* @return true if a permission request is made. False if it is not necessary.
|
||||
*/
|
||||
@TargetApi(23)
|
||||
private boolean maybeRequestPermission() {
|
||||
if (requiresPermission(contentUri)) {
|
||||
requestPermissions(new String[] {permission.READ_EXTERNAL_STORAGE}, 0);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(23)
|
||||
private boolean requiresPermission(Uri uri) {
|
||||
return Util.SDK_INT >= 23
|
||||
&& Util.isLocalFileUri(uri)
|
||||
&& checkSelfPermission(permission.READ_EXTERNAL_STORAGE)
|
||||
!= PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
// Internal methods
|
||||
|
||||
private RendererBuilder getRendererBuilder() {
|
||||
String userAgent = Util.getUserAgent(this, "NewPipeExoPlayer");
|
||||
switch (contentType) {
|
||||
case Util.TYPE_SS:
|
||||
// default
|
||||
//return new SmoothStreamingRendererBuilder(this, userAgent, contentUri.toString());
|
||||
case Util.TYPE_DASH:
|
||||
// if a dash manifest is available
|
||||
//return new DashRendererBuilder(this, userAgent, contentUri.toString());
|
||||
case Util.TYPE_HLS:
|
||||
// for livestreams
|
||||
return new HlsRendererBuilder(this, userAgent, contentUri.toString());
|
||||
case Util.TYPE_OTHER:
|
||||
// video only streaming
|
||||
return new ExtractorRendererBuilder(this, userAgent, contentUri);
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported type: " + contentType);
|
||||
}
|
||||
}
|
||||
|
||||
private void preparePlayer(boolean playWhenReady) {
|
||||
if (player == null) {
|
||||
player = new NPExoPlayer(getRendererBuilder());
|
||||
player.addListener(exoPlayerListener);
|
||||
player.setCaptionListener(captionListener);
|
||||
player.setMetadataListener(id3MetadataListener);
|
||||
player.seekTo(playerPosition);
|
||||
playerNeedsPrepare = true;
|
||||
mediaController.setMediaPlayer(player.getPlayerControl());
|
||||
mediaController.setEnabled(true);
|
||||
eventLogger = new EventLogger();
|
||||
eventLogger.startSession();
|
||||
player.addListener(eventLogger);
|
||||
player.setInfoListener(eventLogger);
|
||||
player.setInternalErrorListener(eventLogger);
|
||||
}
|
||||
if (playerNeedsPrepare) {
|
||||
player.prepare();
|
||||
playerNeedsPrepare = false;
|
||||
}
|
||||
player.setSurface(surfaceView.getHolder().getSurface());
|
||||
player.setPlayWhenReady(playWhenReady);
|
||||
}
|
||||
|
||||
private void releasePlayer() {
|
||||
if (player != null) {
|
||||
playerPosition = player.getCurrentPosition();
|
||||
player.release();
|
||||
player = null;
|
||||
eventLogger.endSession();
|
||||
eventLogger = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void toggleControlsVisibility() {
|
||||
if (mediaController.isShowing()) {
|
||||
mediaController.hide();
|
||||
} else {
|
||||
showControls();
|
||||
}
|
||||
}
|
||||
|
||||
private void showControls() {
|
||||
mediaController.show(0);
|
||||
}
|
||||
|
||||
private void configureSubtitleView() {
|
||||
CaptionStyleCompat style;
|
||||
float fontScale;
|
||||
if (Util.SDK_INT >= 19) {
|
||||
style = getUserCaptionStyleV19();
|
||||
fontScale = getUserCaptionFontScaleV19();
|
||||
} else {
|
||||
style = CaptionStyleCompat.DEFAULT;
|
||||
fontScale = 1.0f;
|
||||
}
|
||||
subtitleLayout.setStyle(style);
|
||||
subtitleLayout.setFractionalTextSize(SubtitleLayout.DEFAULT_TEXT_SIZE_FRACTION * fontScale);
|
||||
}
|
||||
|
||||
@TargetApi(19)
|
||||
private float getUserCaptionFontScaleV19() {
|
||||
CaptioningManager captioningManager =
|
||||
(CaptioningManager) getSystemService(Context.CAPTIONING_SERVICE);
|
||||
return captioningManager.getFontScale();
|
||||
}
|
||||
|
||||
@TargetApi(19)
|
||||
private CaptionStyleCompat getUserCaptionStyleV19() {
|
||||
CaptioningManager captioningManager =
|
||||
(CaptioningManager) getSystemService(Context.CAPTIONING_SERVICE);
|
||||
return CaptionStyleCompat.createFromCaptionStyle(captioningManager.getUserStyle());
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a best guess to infer the type from a media {@link Uri} and an optional overriding file
|
||||
* extension.
|
||||
*
|
||||
* @param uri The {@link Uri} of the media.
|
||||
* @param fileExtension An overriding file extension.
|
||||
* @return The inferred type.
|
||||
*/
|
||||
private static int inferContentType(Uri uri, String fileExtension) {
|
||||
String lastPathSegment = !TextUtils.isEmpty(fileExtension) ? "." + fileExtension
|
||||
: uri.getLastPathSegment();
|
||||
return Util.inferContentType(lastPathSegment);
|
||||
}
|
||||
|
||||
private static final class KeyCompatibleMediaController extends MediaController {
|
||||
|
||||
private MediaController.MediaPlayerControl playerControl;
|
||||
|
||||
public KeyCompatibleMediaController(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMediaPlayer(MediaController.MediaPlayerControl playerControl) {
|
||||
super.setMediaPlayer(playerControl);
|
||||
this.playerControl = playerControl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
int keyCode = event.getKeyCode();
|
||||
if (playerControl.canSeekForward() && keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) {
|
||||
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||
playerControl.seekTo(playerControl.getCurrentPosition() + 15000); // milliseconds
|
||||
show();
|
||||
}
|
||||
return true;
|
||||
} else if (playerControl.canSeekBackward() && keyCode == KeyEvent.KEYCODE_MEDIA_REWIND) {
|
||||
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||
playerControl.seekTo(playerControl.getCurrentPosition() - 5000); // milliseconds
|
||||
show();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
538
app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java
Normal file
@@ -0,0 +1,538 @@
|
||||
package org.schabi.newpipe.player;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.graphics.Color;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
/**
|
||||
* Activity Player implementing VideoPlayer
|
||||
*
|
||||
* @author mauriciocolli
|
||||
*/
|
||||
public class MainVideoPlayer extends Activity {
|
||||
private static final String TAG = ".MainVideoPlayer";
|
||||
private static final boolean DEBUG = BasePlayer.DEBUG;
|
||||
|
||||
private AudioManager audioManager;
|
||||
private GestureDetector gestureDetector;
|
||||
|
||||
private boolean activityPaused;
|
||||
private VideoPlayerImpl playerImpl;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Activity LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
||||
ThemeHelper.setTheme(this);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) getWindow().setStatusBarColor(Color.BLACK);
|
||||
setVolumeControlStream(AudioManager.STREAM_MUSIC);
|
||||
audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
|
||||
|
||||
if (getIntent() == null) {
|
||||
Toast.makeText(this, R.string.general_error, Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
showSystemUi();
|
||||
setContentView(R.layout.activity_main_player);
|
||||
playerImpl = new VideoPlayerImpl();
|
||||
playerImpl.setup(findViewById(android.R.id.content));
|
||||
playerImpl.handleIntent(getIntent());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
if (DEBUG) Log.d(TAG, "onNewIntent() called with: intent = [" + intent + "]");
|
||||
super.onNewIntent(intent);
|
||||
playerImpl.handleIntent(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (DEBUG) Log.d(TAG, "onBackPressed() called");
|
||||
super.onBackPressed();
|
||||
if (playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
if (DEBUG) Log.d(TAG, "onStop() called");
|
||||
activityPaused = true;
|
||||
if (playerImpl.getPlayer() != null) {
|
||||
playerImpl.setVideoStartPos((int) playerImpl.getPlayer().getCurrentPosition());
|
||||
playerImpl.destroyPlayer();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (DEBUG) Log.d(TAG, "onResume() called");
|
||||
if (activityPaused) {
|
||||
playerImpl.initPlayer();
|
||||
playerImpl.getPlayPauseButton().setImageResource(R.drawable.ic_play_arrow_white);
|
||||
playerImpl.play(false);
|
||||
activityPaused = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (DEBUG) Log.d(TAG, "onDestroy() called");
|
||||
if (playerImpl != null) playerImpl.destroy();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void showSystemUi() {
|
||||
if (DEBUG) Log.d(TAG, "showSystemUi() called");
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
getWindow().getDecorView().setSystemUiVisibility(
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
);
|
||||
} else getWindow().getDecorView().setSystemUiVisibility(0);
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
}
|
||||
|
||||
private void hideSystemUi() {
|
||||
if (DEBUG) Log.d(TAG, "hideSystemUi() called");
|
||||
if (android.os.Build.VERSION.SDK_INT >= 16) {
|
||||
int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
| View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) visibility |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
|
||||
getWindow().getDecorView().setSystemUiVisibility(visibility);
|
||||
}
|
||||
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
}
|
||||
|
||||
private void toggleOrientation() {
|
||||
setRequestedOrientation(getResources().getDisplayMetrics().heightPixels > getResources().getDisplayMetrics().widthPixels
|
||||
? ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||
: ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@SuppressWarnings({"unused", "WeakerAccess"})
|
||||
private class VideoPlayerImpl extends VideoPlayer {
|
||||
private TextView titleTextView;
|
||||
private TextView channelTextView;
|
||||
private TextView volumeTextView;
|
||||
private TextView brightnessTextView;
|
||||
private ImageButton repeatButton;
|
||||
|
||||
private ImageButton screenRotationButton;
|
||||
private ImageButton playPauseButton;
|
||||
|
||||
VideoPlayerImpl() {
|
||||
super("VideoPlayerImpl" + MainVideoPlayer.TAG, MainVideoPlayer.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initViews(View rootView) {
|
||||
super.initViews(rootView);
|
||||
this.titleTextView = (TextView) rootView.findViewById(R.id.titleTextView);
|
||||
this.channelTextView = (TextView) rootView.findViewById(R.id.channelTextView);
|
||||
this.volumeTextView = (TextView) rootView.findViewById(R.id.volumeTextView);
|
||||
this.brightnessTextView = (TextView) rootView.findViewById(R.id.brightnessTextView);
|
||||
this.repeatButton = (ImageButton) rootView.findViewById(R.id.repeatButton);
|
||||
|
||||
this.screenRotationButton = (ImageButton) rootView.findViewById(R.id.screenRotationButton);
|
||||
this.playPauseButton = (ImageButton) rootView.findViewById(R.id.playPauseButton);
|
||||
|
||||
// Due to a bug on lower API, lets set the alpha instead of using a drawable
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) repeatButton.setImageAlpha(77);
|
||||
else { //noinspection deprecation
|
||||
repeatButton.setAlpha(77);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initListeners() {
|
||||
super.initListeners();
|
||||
|
||||
MySimpleOnGestureListener listener = new MySimpleOnGestureListener();
|
||||
gestureDetector = new GestureDetector(context, listener);
|
||||
gestureDetector.setIsLongpressEnabled(false);
|
||||
playerImpl.getRootView().setOnTouchListener(listener);
|
||||
|
||||
repeatButton.setOnClickListener(this);
|
||||
playPauseButton.setOnClickListener(this);
|
||||
screenRotationButton.setOnClickListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleIntent(Intent intent) {
|
||||
super.handleIntent(intent);
|
||||
titleTextView.setText(getVideoTitle());
|
||||
channelTextView.setText(getChannelName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playUrl(String url, String format, boolean autoPlay) {
|
||||
super.playUrl(url, format, autoPlay);
|
||||
playPauseButton.setImageResource(autoPlay ? R.drawable.ic_pause_white : R.drawable.ic_play_arrow_white);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFullScreenButtonClicked() {
|
||||
if (DEBUG) Log.d(TAG, "onFullScreenButtonClicked() called");
|
||||
if (playerImpl.getPlayer() == null) return;
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
&& !PermissionHelper.checkSystemAlertWindowPermission(MainVideoPlayer.this)) {
|
||||
Toast.makeText(MainVideoPlayer.this, R.string.msg_popup_permission, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
context.startService(NavigationHelper.getOpenVideoPlayerIntent(context, PopupVideoPlayer.class, playerImpl));
|
||||
if (playerImpl != null) playerImpl.destroyPlayer();
|
||||
|
||||
((View) getControlAnimationView().getParent()).setVisibility(View.GONE);
|
||||
MainVideoPlayer.this.finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("deprecation")
|
||||
public void onRepeatClicked() {
|
||||
super.onRepeatClicked();
|
||||
if (DEBUG) Log.d(TAG, "onRepeatClicked() called");
|
||||
switch (getCurrentRepeatMode()) {
|
||||
case REPEAT_DISABLED:
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) repeatButton.setImageAlpha(77);
|
||||
else repeatButton.setAlpha(77);
|
||||
|
||||
break;
|
||||
case REPEAT_ONE:
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) repeatButton.setImageAlpha(255);
|
||||
else repeatButton.setAlpha(255);
|
||||
|
||||
break;
|
||||
case REPEAT_ALL:
|
||||
// Waiting :)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
super.onClick(v);
|
||||
if (v.getId() == repeatButton.getId()) onRepeatClicked();
|
||||
else if (v.getId() == playPauseButton.getId()) onVideoPlayPause();
|
||||
else if (v.getId() == screenRotationButton.getId()) onScreenRotationClicked();
|
||||
|
||||
if (getCurrentState() != STATE_COMPLETED) {
|
||||
getControlsVisibilityHandler().removeCallbacksAndMessages(null);
|
||||
animateView(playerImpl.getControlsRoot(), true, 300, 0, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (getCurrentState() == STATE_PLAYING && !playerImpl.isQualityMenuVisible()) {
|
||||
hideControls(300, DEFAULT_CONTROLS_HIDE_TIME);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void onScreenRotationClicked() {
|
||||
if (DEBUG) Log.d(TAG, "onScreenRotationClicked() called");
|
||||
toggleOrientation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
super.onStopTrackingTouch(seekBar);
|
||||
if (playerImpl.wasPlaying()) {
|
||||
hideControls(100, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismiss(PopupMenu menu) {
|
||||
super.onDismiss(menu);
|
||||
if (isPlaying()) hideControls(300, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception exception) {
|
||||
exception.printStackTrace();
|
||||
Toast.makeText(context, "Failed to play this video", Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// States
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onLoading() {
|
||||
super.onLoading();
|
||||
playPauseButton.setImageResource(R.drawable.ic_pause_white);
|
||||
animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 100);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBuffering() {
|
||||
super.onBuffering();
|
||||
animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 100);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaying() {
|
||||
super.onPlaying();
|
||||
animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 80, 0, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
playPauseButton.setImageResource(R.drawable.ic_pause_white);
|
||||
animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, true, 200);
|
||||
}
|
||||
});
|
||||
|
||||
showSystemUi();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPaused() {
|
||||
super.onPaused();
|
||||
animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 80, 0, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
playPauseButton.setImageResource(R.drawable.ic_play_arrow_white);
|
||||
animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, true, 200);
|
||||
}
|
||||
});
|
||||
|
||||
showSystemUi();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPausedSeek() {
|
||||
super.onPausedSeek();
|
||||
animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 100);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onCompleted() {
|
||||
if (getCurrentRepeatMode() == RepeatMode.REPEAT_ONE) {
|
||||
playPauseButton.setImageResource(R.drawable.ic_pause_white);
|
||||
} else {
|
||||
showSystemUi();
|
||||
animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 0, 0, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
playPauseButton.setImageResource(R.drawable.ic_replay_white);
|
||||
animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, true, 300);
|
||||
}
|
||||
});
|
||||
}
|
||||
super.onCompleted();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void hideControls(final long duration, long delay) {
|
||||
if (DEBUG) Log.d(TAG, "hideControls() called with: delay = [" + delay + "]");
|
||||
getControlsVisibilityHandler().removeCallbacksAndMessages(null);
|
||||
getControlsVisibilityHandler().postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
animateView(getControlsRoot(), false, duration, 0, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
hideSystemUi();
|
||||
}
|
||||
});
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Getters
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public TextView getTitleTextView() {
|
||||
return titleTextView;
|
||||
}
|
||||
|
||||
public TextView getChannelTextView() {
|
||||
return channelTextView;
|
||||
}
|
||||
|
||||
public TextView getVolumeTextView() {
|
||||
return volumeTextView;
|
||||
}
|
||||
|
||||
public TextView getBrightnessTextView() {
|
||||
return brightnessTextView;
|
||||
}
|
||||
|
||||
public ImageButton getRepeatButton() {
|
||||
return repeatButton;
|
||||
}
|
||||
|
||||
public ImageButton getPlayPauseButton() {
|
||||
return playPauseButton;
|
||||
}
|
||||
}
|
||||
|
||||
private class MySimpleOnGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener {
|
||||
private boolean isMoving;
|
||||
|
||||
@Override
|
||||
public boolean onDoubleTap(MotionEvent e) {
|
||||
if (DEBUG) Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY());
|
||||
if (!playerImpl.isPlaying()) return false;
|
||||
if (e.getX() > playerImpl.getRootView().getWidth() / 2) playerImpl.onFastForward();
|
||||
else playerImpl.onFastRewind();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSingleTapConfirmed(MotionEvent e) {
|
||||
if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]");
|
||||
if (playerImpl.getCurrentState() != BasePlayer.STATE_PLAYING) return true;
|
||||
|
||||
if (playerImpl.isControlsVisible()) playerImpl.hideControls(150, 0);
|
||||
else {
|
||||
playerImpl.showControlsThenHide();
|
||||
showSystemUi();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private final float stepsBrightness = 15, stepBrightness = (1f / stepsBrightness), minBrightness = .01f;
|
||||
private float currentBrightness = .5f;
|
||||
|
||||
private int currentVolume, maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
|
||||
private final float stepsVolume = 15, stepVolume = (float) Math.ceil(maxVolume / stepsVolume), minVolume = 0;
|
||||
|
||||
private final String brightnessUnicode = new String(Character.toChars(0x2600));
|
||||
private final String volumeUnicode = new String(Character.toChars(0x1F508));
|
||||
|
||||
private final int MOVEMENT_THRESHOLD = 40;
|
||||
private final int eventsThreshold = 8;
|
||||
private boolean triggered = false;
|
||||
private int eventsNum;
|
||||
|
||||
// TODO: Improve video gesture controls
|
||||
@Override
|
||||
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
|
||||
//noinspection PointlessBooleanExpression
|
||||
if (DEBUG && false) Log.d(TAG, "MainVideoPlayer.onScroll = " +
|
||||
", e1.getRaw = [" + e1.getRawX() + ", " + e1.getRawY() + "]" +
|
||||
", e2.getRaw = [" + e2.getRawX() + ", " + e2.getRawY() + "]" +
|
||||
", distanceXy = [" + distanceX + ", " + distanceY + "]");
|
||||
float abs = Math.abs(e2.getY() - e1.getY());
|
||||
if (!triggered) {
|
||||
triggered = abs > MOVEMENT_THRESHOLD;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (eventsNum++ % eventsThreshold != 0 || playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) return false;
|
||||
isMoving = true;
|
||||
// boolean up = !((e2.getY() - e1.getY()) > 0) && distanceY > 0; // Android's origin point is on top
|
||||
boolean up = distanceY > 0;
|
||||
|
||||
|
||||
if (e1.getX() > playerImpl.getRootView().getWidth() / 2) {
|
||||
double floor = Math.floor(up ? stepVolume : -stepVolume);
|
||||
currentVolume = (int) (audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) + floor);
|
||||
if (currentVolume >= maxVolume) currentVolume = maxVolume;
|
||||
if (currentVolume <= minVolume) currentVolume = (int) minVolume;
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, currentVolume, 0);
|
||||
|
||||
if (DEBUG) Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume);
|
||||
playerImpl.getVolumeTextView().setText(volumeUnicode + " " + Math.round((((float) currentVolume) / maxVolume) * 100) + "%");
|
||||
|
||||
if (playerImpl.getVolumeTextView().getVisibility() != View.VISIBLE) animateView(playerImpl.getVolumeTextView(), true, 200);
|
||||
if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) playerImpl.getBrightnessTextView().setVisibility(View.GONE);
|
||||
} else {
|
||||
WindowManager.LayoutParams lp = getWindow().getAttributes();
|
||||
currentBrightness += up ? stepBrightness : -stepBrightness;
|
||||
if (currentBrightness >= 1f) currentBrightness = 1f;
|
||||
if (currentBrightness <= minBrightness) currentBrightness = minBrightness;
|
||||
|
||||
lp.screenBrightness = currentBrightness;
|
||||
getWindow().setAttributes(lp);
|
||||
if (DEBUG) Log.d(TAG, "onScroll().brightnessControl, currentBrightness = " + currentBrightness);
|
||||
int brightnessNormalized = Math.round(currentBrightness * 100);
|
||||
|
||||
playerImpl.getBrightnessTextView().setText(brightnessUnicode + " " + (brightnessNormalized == 1 ? 0 : brightnessNormalized) + "%");
|
||||
|
||||
if (playerImpl.getBrightnessTextView().getVisibility() != View.VISIBLE) animateView(playerImpl.getBrightnessTextView(), true, 200);
|
||||
if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) playerImpl.getVolumeTextView().setVisibility(View.GONE);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onScrollEnd() {
|
||||
if (DEBUG) Log.d(TAG, "onScrollEnd() called");
|
||||
triggered = false;
|
||||
eventsNum = 0;
|
||||
/* if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) playerImpl.getVolumeTextView().setVisibility(View.GONE);
|
||||
if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) playerImpl.getBrightnessTextView().setVisibility(View.GONE);*/
|
||||
if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) animateView(playerImpl.getVolumeTextView(), false, 200, 200);
|
||||
if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) animateView(playerImpl.getBrightnessTextView(), false, 200, 200);
|
||||
|
||||
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == BasePlayer.STATE_PLAYING) {
|
||||
playerImpl.hideControls(300, VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
//noinspection PointlessBooleanExpression
|
||||
if (DEBUG && false) Log.d(TAG, "onTouch() called with: v = [" + v + "], event = [" + event + "]");
|
||||
gestureDetector.onTouchEvent(event);
|
||||
if (event.getAction() == MotionEvent.ACTION_UP && isMoving) {
|
||||
isMoving = false;
|
||||
onScrollEnd();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,8 @@ import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.media.MediaPlayer;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaPlayer;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
@@ -28,7 +27,6 @@ import android.widget.MediaController;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.VideoView;
|
||||
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,761 @@
|
||||
package org.schabi.newpipe.player;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.Gravity;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.RemoteViews;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
|
||||
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.util.Utils;
|
||||
import org.schabi.newpipe.workers.StreamExtractorWorker;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
/**
|
||||
* Service Popup Player implementing VideoPlayer
|
||||
*
|
||||
* @author mauriciocolli
|
||||
*/
|
||||
public class PopupVideoPlayer extends Service {
|
||||
private static final String TAG = ".PopupVideoPlayer";
|
||||
private static final boolean DEBUG = BasePlayer.DEBUG;
|
||||
|
||||
private static final int NOTIFICATION_ID = 40028922;
|
||||
public static final String ACTION_CLOSE = "org.schabi.newpipe.player.PopupVideoPlayer.CLOSE";
|
||||
public static final String ACTION_PLAY_PAUSE = "org.schabi.newpipe.player.PopupVideoPlayer.PLAY_PAUSE";
|
||||
public static final String ACTION_OPEN_DETAIL = "org.schabi.newpipe.player.PopupVideoPlayer.OPEN_DETAIL";
|
||||
public static final String ACTION_REPEAT = "org.schabi.newpipe.player.PopupVideoPlayer.REPEAT";
|
||||
|
||||
private static final String POPUP_SAVED_WIDTH = "popup_saved_width";
|
||||
private static final String POPUP_SAVED_X = "popup_saved_x";
|
||||
private static final String POPUP_SAVED_Y = "popup_saved_y";
|
||||
|
||||
private WindowManager windowManager;
|
||||
private WindowManager.LayoutParams windowLayoutParams;
|
||||
private GestureDetector gestureDetector;
|
||||
|
||||
private float screenWidth, screenHeight;
|
||||
private float popupWidth, popupHeight;
|
||||
|
||||
private float minimumWidth, minimumHeight;
|
||||
private float maximumWidth, maximumHeight;
|
||||
|
||||
private final String setAlphaMethodName = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) ? "setImageAlpha" : "setAlpha";
|
||||
private NotificationManager notificationManager;
|
||||
private NotificationCompat.Builder notBuilder;
|
||||
private RemoteViews notRemoteView;
|
||||
|
||||
|
||||
private ImageLoader imageLoader = ImageLoader.getInstance();
|
||||
private DisplayImageOptions displayImageOptions = new DisplayImageOptions.Builder().cacheInMemory(true).build();
|
||||
|
||||
private VideoPlayerImpl playerImpl;
|
||||
private StreamExtractorWorker currentExtractorWorker;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Service LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
|
||||
notificationManager = ((NotificationManager) getSystemService(NOTIFICATION_SERVICE));
|
||||
|
||||
playerImpl = new VideoPlayerImpl();
|
||||
ThemeHelper.setTheme(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public int onStartCommand(final Intent intent, int flags, int startId) {
|
||||
if (DEBUG) Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "], flags = [" + flags + "], startId = [" + startId + "]");
|
||||
if (playerImpl.getPlayer() == null) initPopup();
|
||||
if (!playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(true);
|
||||
|
||||
if (imageLoader != null) imageLoader.clearMemoryCache();
|
||||
if (intent.getStringExtra(Constants.KEY_URL) != null) {
|
||||
playerImpl.setStartedFromNewPipe(false);
|
||||
currentExtractorWorker = new StreamExtractorWorker(this, 0, intent.getStringExtra(Constants.KEY_URL), new FetcherRunnable(this));
|
||||
currentExtractorWorker.start();
|
||||
} else {
|
||||
playerImpl.setStartedFromNewPipe(true);
|
||||
playerImpl.handleIntent(intent);
|
||||
}
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
updateScreenSize();
|
||||
updatePopupSize(windowLayoutParams.width, -1);
|
||||
checkPositionBounds();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
if (DEBUG) Log.d(TAG, "onDestroy() called");
|
||||
stopForeground(true);
|
||||
if (playerImpl != null) {
|
||||
playerImpl.destroy();
|
||||
if (playerImpl.getRootView() != null) windowManager.removeView(playerImpl.getRootView());
|
||||
}
|
||||
if (notificationManager != null) notificationManager.cancel(NOTIFICATION_ID);
|
||||
if (currentExtractorWorker != null) {
|
||||
currentExtractorWorker.cancel();
|
||||
currentExtractorWorker = null;
|
||||
}
|
||||
|
||||
savePositionAndSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@SuppressLint("RtlHardcoded")
|
||||
private void initPopup() {
|
||||
if (DEBUG) Log.d(TAG, "initPopup() called");
|
||||
View rootView = View.inflate(this, R.layout.player_popup, null);
|
||||
playerImpl.setup(rootView);
|
||||
|
||||
updateScreenSize();
|
||||
|
||||
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
boolean popupRememberSizeAndPos = sharedPreferences.getBoolean(getString(R.string.popup_remember_size_pos_key), true);
|
||||
|
||||
float defaultSize = getResources().getDimension(R.dimen.popup_default_width);
|
||||
popupWidth = popupRememberSizeAndPos ? sharedPreferences.getFloat(POPUP_SAVED_WIDTH, defaultSize) : defaultSize;
|
||||
|
||||
windowLayoutParams = new WindowManager.LayoutParams(
|
||||
(int) popupWidth, (int) getMinimumVideoHeight(popupWidth),
|
||||
WindowManager.LayoutParams.TYPE_PHONE,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||
PixelFormat.TRANSLUCENT);
|
||||
windowLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
|
||||
|
||||
int centerX = (int) (screenWidth / 2f - popupWidth / 2f);
|
||||
int centerY = (int) (screenHeight / 2f - popupHeight / 2f);
|
||||
windowLayoutParams.x = popupRememberSizeAndPos ? sharedPreferences.getInt(POPUP_SAVED_X, centerX) : centerX;
|
||||
windowLayoutParams.y = popupRememberSizeAndPos ? sharedPreferences.getInt(POPUP_SAVED_Y, centerY) : centerY;
|
||||
|
||||
checkPositionBounds();
|
||||
|
||||
MySimpleOnGestureListener listener = new MySimpleOnGestureListener();
|
||||
gestureDetector = new GestureDetector(this, listener);
|
||||
//gestureDetector.setIsLongpressEnabled(false);
|
||||
rootView.setOnTouchListener(listener);
|
||||
playerImpl.getLoadingPanel().setMinimumWidth(windowLayoutParams.width);
|
||||
playerImpl.getLoadingPanel().setMinimumHeight(windowLayoutParams.height);
|
||||
windowManager.addView(rootView, windowLayoutParams);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Notification
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private NotificationCompat.Builder createNotification() {
|
||||
notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_popup_notification);
|
||||
|
||||
if (playerImpl.getVideoThumbnail() == null) notRemoteView.setImageViewResource(R.id.notificationCover, R.drawable.dummy_thumbnail);
|
||||
else notRemoteView.setImageViewBitmap(R.id.notificationCover, playerImpl.getVideoThumbnail());
|
||||
|
||||
notRemoteView.setTextViewText(R.id.notificationSongName, playerImpl.getVideoTitle());
|
||||
notRemoteView.setTextViewText(R.id.notificationArtist, playerImpl.getChannelName());
|
||||
|
||||
notRemoteView.setOnClickPendingIntent(R.id.notificationPlayPause,
|
||||
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT));
|
||||
notRemoteView.setOnClickPendingIntent(R.id.notificationStop,
|
||||
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_CLOSE), PendingIntent.FLAG_UPDATE_CURRENT));
|
||||
notRemoteView.setOnClickPendingIntent(R.id.notificationContent,
|
||||
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_OPEN_DETAIL), PendingIntent.FLAG_UPDATE_CURRENT));
|
||||
notRemoteView.setOnClickPendingIntent(R.id.notificationRepeat,
|
||||
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_REPEAT), PendingIntent.FLAG_UPDATE_CURRENT));
|
||||
|
||||
switch (playerImpl.getCurrentRepeatMode()) {
|
||||
case REPEAT_DISABLED:
|
||||
notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, 77);
|
||||
break;
|
||||
case REPEAT_ONE:
|
||||
notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, 255);
|
||||
break;
|
||||
case REPEAT_ALL:
|
||||
// Waiting :)
|
||||
break;
|
||||
}
|
||||
|
||||
return new NotificationCompat.Builder(this)
|
||||
.setOngoing(true)
|
||||
.setSmallIcon(R.drawable.ic_play_arrow_white)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setContent(notRemoteView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the notification, and the play/pause button in it.
|
||||
* Used for changes on the remoteView
|
||||
*
|
||||
* @param drawableId if != -1, sets the drawable with that id on the play/pause button
|
||||
*/
|
||||
private void updateNotification(int drawableId) {
|
||||
if (DEBUG) Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]");
|
||||
if (notBuilder == null || notRemoteView == null) return;
|
||||
if (drawableId != -1) notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId);
|
||||
notificationManager.notify(NOTIFICATION_ID, notBuilder.build());
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Misc
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public void onVideoClose() {
|
||||
if (DEBUG) Log.d(TAG, "onVideoClose() called");
|
||||
savePositionAndSize();
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
public void onOpenDetail(Context context, String videoUrl, String videoTitle) {
|
||||
if (DEBUG) Log.d(TAG, "onOpenDetail() called with: context = [" + context + "], videoUrl = [" + videoUrl + "]");
|
||||
Intent i = new Intent(context, MainActivity.class);
|
||||
i.putExtra(Constants.KEY_SERVICE_ID, 0);
|
||||
i.putExtra(Constants.KEY_URL, videoUrl);
|
||||
i.putExtra(Constants.KEY_TITLE, videoTitle);
|
||||
i.putExtra(Constants.KEY_LINK_TYPE, StreamingService.LinkType.STREAM);
|
||||
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(i);
|
||||
context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void checkPositionBounds() {
|
||||
if (windowLayoutParams.x > screenWidth - windowLayoutParams.width) windowLayoutParams.x = (int) (screenWidth - windowLayoutParams.width);
|
||||
if (windowLayoutParams.x < 0) windowLayoutParams.x = 0;
|
||||
if (windowLayoutParams.y > screenHeight - windowLayoutParams.height) windowLayoutParams.y = (int) (screenHeight - windowLayoutParams.height);
|
||||
if (windowLayoutParams.y < 0) windowLayoutParams.y = 0;
|
||||
}
|
||||
|
||||
private void savePositionAndSize() {
|
||||
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(PopupVideoPlayer.this);
|
||||
sharedPreferences.edit().putInt(POPUP_SAVED_X, windowLayoutParams.x).apply();
|
||||
sharedPreferences.edit().putInt(POPUP_SAVED_Y, windowLayoutParams.y).apply();
|
||||
sharedPreferences.edit().putFloat(POPUP_SAVED_WIDTH, windowLayoutParams.width).apply();
|
||||
}
|
||||
|
||||
private float getMinimumVideoHeight(float width) {
|
||||
//if (DEBUG) Log.d(TAG, "getMinimumVideoHeight() called with: width = [" + width + "], returned: " + height);
|
||||
return width / (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have
|
||||
}
|
||||
|
||||
private void updateScreenSize() {
|
||||
DisplayMetrics metrics = new DisplayMetrics();
|
||||
windowManager.getDefaultDisplay().getMetrics(metrics);
|
||||
|
||||
screenWidth = metrics.widthPixels;
|
||||
screenHeight = metrics.heightPixels;
|
||||
if (DEBUG) Log.d(TAG, "updateScreenSize() called > screenWidth = " + screenWidth + ", screenHeight = " + screenHeight);
|
||||
|
||||
popupWidth = getResources().getDimension(R.dimen.popup_default_width);
|
||||
popupHeight = getMinimumVideoHeight(popupWidth);
|
||||
|
||||
minimumWidth = getResources().getDimension(R.dimen.popup_minimum_width);
|
||||
minimumHeight = getMinimumVideoHeight(minimumWidth);
|
||||
|
||||
maximumWidth = screenWidth;
|
||||
maximumHeight = screenHeight;
|
||||
}
|
||||
|
||||
private void updatePopupSize(int width, int height) {
|
||||
//if (DEBUG) Log.d(TAG, "updatePopupSize() called with: width = [" + width + "], height = [" + height + "]");
|
||||
|
||||
width = (int) (width > maximumWidth ? maximumWidth : width < minimumWidth ? minimumWidth : width);
|
||||
|
||||
if (height == -1) height = (int) getMinimumVideoHeight(width);
|
||||
else height = (int) (height > maximumHeight ? maximumHeight : height < minimumHeight ? minimumHeight : height);
|
||||
|
||||
windowLayoutParams.width = width;
|
||||
windowLayoutParams.height = height;
|
||||
popupWidth = width;
|
||||
popupHeight = height;
|
||||
|
||||
if (DEBUG) Log.d(TAG, "updatePopupSize() updated values: width = [" + width + "], height = [" + height + "]");
|
||||
windowManager.updateViewLayout(playerImpl.getRootView(), windowLayoutParams);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private class VideoPlayerImpl extends VideoPlayer {
|
||||
private TextView resizingIndicator;
|
||||
|
||||
VideoPlayerImpl() {
|
||||
super("VideoPlayerImpl" + PopupVideoPlayer.TAG, PopupVideoPlayer.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playUrl(String url, String format, boolean autoPlay) {
|
||||
super.playUrl(url, format, autoPlay);
|
||||
|
||||
windowLayoutParams.width = (int) popupWidth;
|
||||
windowLayoutParams.height = (int) getMinimumVideoHeight(popupWidth);
|
||||
windowManager.updateViewLayout(getRootView(), windowLayoutParams);
|
||||
|
||||
notBuilder = createNotification();
|
||||
startForeground(NOTIFICATION_ID, notBuilder.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initViews(View rootView) {
|
||||
super.initViews(rootView);
|
||||
resizingIndicator = (TextView) rootView.findViewById(R.id.resizing_indicator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
super.destroy();
|
||||
if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onThumbnailReceived(Bitmap thumbnail) {
|
||||
super.onThumbnailReceived(thumbnail);
|
||||
if (thumbnail != null) {
|
||||
if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, thumbnail);
|
||||
updateNotification(-1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFullScreenButtonClicked() {
|
||||
if (DEBUG) Log.d(TAG, "onFullScreenButtonClicked() called");
|
||||
Intent intent;
|
||||
if (!getSharedPreferences().getBoolean(getResources().getString(R.string.use_old_player_key), false)) {
|
||||
intent = NavigationHelper.getOpenVideoPlayerIntent(context, MainVideoPlayer.class, playerImpl);
|
||||
if (!playerImpl.isStartedFromNewPipe()) intent.putExtra(VideoPlayer.STARTED_FROM_NEWPIPE, false);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
} else {
|
||||
intent = new Intent(PopupVideoPlayer.this, PlayVideoActivity.class)
|
||||
.putExtra(PlayVideoActivity.VIDEO_TITLE, getVideoTitle())
|
||||
.putExtra(PlayVideoActivity.STREAM_URL, getSelectedStreamUri().toString())
|
||||
.putExtra(PlayVideoActivity.VIDEO_URL, getVideoUrl())
|
||||
.putExtra(PlayVideoActivity.START_POSITION, Math.round(getPlayer().getCurrentPosition() / 1000f));
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
}
|
||||
context.startActivity(intent);
|
||||
if (playerImpl != null) playerImpl.destroyPlayer();
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRepeatClicked() {
|
||||
super.onRepeatClicked();
|
||||
switch (getCurrentRepeatMode()) {
|
||||
case REPEAT_DISABLED:
|
||||
// Drawable didn't work on low API :/
|
||||
//notRemoteView.setImageViewResource(R.id.notificationRepeat, R.drawable.ic_repeat_disabled_white);
|
||||
// Set the icon to 30% opacity - 255 (max) * .3
|
||||
notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, 77);
|
||||
break;
|
||||
case REPEAT_ONE:
|
||||
notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, 255);
|
||||
break;
|
||||
case REPEAT_ALL:
|
||||
// Waiting :)
|
||||
break;
|
||||
}
|
||||
updateNotification(-1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismiss(PopupMenu menu) {
|
||||
super.onDismiss(menu);
|
||||
if (isPlaying()) hideControls(500, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception exception) {
|
||||
exception.printStackTrace();
|
||||
Toast.makeText(context, "Failed to play this video", Toast.LENGTH_SHORT).show();
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
super.onStopTrackingTouch(seekBar);
|
||||
if (playerImpl.wasPlaying()) {
|
||||
hideControls(100, 0);
|
||||
}
|
||||
}
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Broadcast Receiver
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected void setupBroadcastReceiver(IntentFilter intentFilter) {
|
||||
super.setupBroadcastReceiver(intentFilter);
|
||||
if (DEBUG) Log.d(TAG, "setupBroadcastReceiver() called with: intentFilter = [" + intentFilter + "]");
|
||||
intentFilter.addAction(ACTION_CLOSE);
|
||||
intentFilter.addAction(ACTION_PLAY_PAUSE);
|
||||
intentFilter.addAction(ACTION_OPEN_DETAIL);
|
||||
intentFilter.addAction(ACTION_REPEAT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBroadcastReceived(Intent intent) {
|
||||
super.onBroadcastReceived(intent);
|
||||
if (DEBUG) Log.d(TAG, "onBroadcastReceived() called with: intent = [" + intent + "]");
|
||||
switch (intent.getAction()) {
|
||||
case ACTION_CLOSE:
|
||||
onVideoClose();
|
||||
break;
|
||||
case ACTION_PLAY_PAUSE:
|
||||
playerImpl.onVideoPlayPause();
|
||||
break;
|
||||
case ACTION_OPEN_DETAIL:
|
||||
onOpenDetail(PopupVideoPlayer.this, playerImpl.getVideoUrl(), playerImpl.getVideoTitle());
|
||||
break;
|
||||
case ACTION_REPEAT:
|
||||
playerImpl.onRepeatClicked();
|
||||
break;
|
||||
}
|
||||
}
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// States
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onLoading() {
|
||||
super.onLoading();
|
||||
updateNotification(R.drawable.ic_play_arrow_white);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaying() {
|
||||
super.onPlaying();
|
||||
updateNotification(R.drawable.ic_pause_white);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBuffering() {
|
||||
super.onBuffering();
|
||||
updateNotification(R.drawable.ic_play_arrow_white);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPaused() {
|
||||
super.onPaused();
|
||||
updateNotification(R.drawable.ic_play_arrow_white);
|
||||
showAndAnimateControl(R.drawable.ic_play_arrow_white, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPausedSeek() {
|
||||
super.onPausedSeek();
|
||||
updateNotification(R.drawable.ic_play_arrow_white);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleted() {
|
||||
super.onCompleted();
|
||||
updateNotification(R.drawable.ic_replay_white);
|
||||
showAndAnimateControl(R.drawable.ic_replay_white, false);
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public TextView getResizingIndicator() {
|
||||
return resizingIndicator;
|
||||
}
|
||||
}
|
||||
|
||||
private class MySimpleOnGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener {
|
||||
private int initialPopupX, initialPopupY;
|
||||
private boolean isMoving;
|
||||
|
||||
private int onDownPopupWidth = 0;
|
||||
private boolean isResizing;
|
||||
private boolean isResizingRightSide;
|
||||
|
||||
@Override
|
||||
public boolean onDoubleTap(MotionEvent e) {
|
||||
if (DEBUG) Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY());
|
||||
if (!playerImpl.isPlaying()) return false;
|
||||
if (e.getX() > popupWidth / 2) playerImpl.onFastForward();
|
||||
else playerImpl.onFastRewind();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSingleTapConfirmed(MotionEvent e) {
|
||||
if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]");
|
||||
if (playerImpl.getPlayer() == null) return false;
|
||||
playerImpl.onVideoPlayPause();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDown(MotionEvent e) {
|
||||
if (DEBUG) Log.d(TAG, "onDown() called with: e = [" + e + "]");
|
||||
initialPopupX = windowLayoutParams.x;
|
||||
initialPopupY = windowLayoutParams.y;
|
||||
popupWidth = windowLayoutParams.width;
|
||||
popupHeight = windowLayoutParams.height;
|
||||
onDownPopupWidth = windowLayoutParams.width;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongPress(MotionEvent e) {
|
||||
if (DEBUG) Log.d(TAG, "onLongPress() called with: e = [" + e + "]");
|
||||
playerImpl.showAndAnimateControl(-1, true);
|
||||
playerImpl.getLoadingPanel().setVisibility(View.GONE);
|
||||
|
||||
playerImpl.hideControls(0, 0);
|
||||
animateView(playerImpl.getCurrentDisplaySeek(), false, 0, 0);
|
||||
animateView(playerImpl.getResizingIndicator(), true, 200, 0);
|
||||
|
||||
isResizing = true;
|
||||
isResizingRightSide = e.getRawX() > windowLayoutParams.x + (windowLayoutParams.width / 2f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
|
||||
if (isResizing) return false;
|
||||
|
||||
if (playerImpl.getCurrentState() != BasePlayer.STATE_BUFFERING
|
||||
&& (!isMoving || playerImpl.getControlsRoot().getAlpha() != 1f)) playerImpl.showControls(0);
|
||||
isMoving = true;
|
||||
|
||||
float diffX = (int) (e2.getRawX() - e1.getRawX()), posX = (int) (initialPopupX + diffX);
|
||||
float diffY = (int) (e2.getRawY() - e1.getRawY()), posY = (int) (initialPopupY + diffY);
|
||||
|
||||
if (posX > (screenWidth - popupWidth)) posX = (int) (screenWidth - popupWidth);
|
||||
else if (posX < 0) posX = 0;
|
||||
|
||||
if (posY > (screenHeight - popupHeight)) posY = (int) (screenHeight - popupHeight);
|
||||
else if (posY < 0) posY = 0;
|
||||
|
||||
windowLayoutParams.x = (int) posX;
|
||||
windowLayoutParams.y = (int) posY;
|
||||
|
||||
//noinspection PointlessBooleanExpression
|
||||
if (DEBUG && false) Log.d(TAG, "PopupVideoPlayer.onScroll = " +
|
||||
", e1.getRaw = [" + e1.getRawX() + ", " + e1.getRawY() + "]" +
|
||||
", e2.getRaw = [" + e2.getRawX() + ", " + e2.getRawY() + "]" +
|
||||
", distanceXy = [" + distanceX + ", " + distanceY + "]" +
|
||||
", posXy = [" + posX + ", " + posY + "]" +
|
||||
", popupWh = [" + popupWidth + " x " + popupHeight + "]");
|
||||
windowManager.updateViewLayout(playerImpl.getRootView(), windowLayoutParams);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onScrollEnd() {
|
||||
if (DEBUG) Log.d(TAG, "onScrollEnd() called");
|
||||
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == BasePlayer.STATE_PLAYING) {
|
||||
playerImpl.hideControls(300, VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
gestureDetector.onTouchEvent(event);
|
||||
if (event.getAction() == MotionEvent.ACTION_MOVE && isResizing && !isMoving) {
|
||||
//if (DEBUG) Log.d(TAG, "onTouch() ACTION_MOVE > v = [" + v + "], e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]");
|
||||
int width;
|
||||
if (isResizingRightSide) width = (int) event.getRawX() - windowLayoutParams.x;
|
||||
else {
|
||||
width = (int) (windowLayoutParams.width + (windowLayoutParams.x - event.getRawX()));
|
||||
if (width > minimumWidth) windowLayoutParams.x = initialPopupX - (width - onDownPopupWidth);
|
||||
}
|
||||
if (width <= maximumWidth && width >= minimumWidth) updatePopupSize(width, -1);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.getAction() == MotionEvent.ACTION_UP) {
|
||||
if (DEBUG) Log.d(TAG, "onTouch() ACTION_UP > v = [" + v + "], e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]");
|
||||
if (isMoving) {
|
||||
isMoving = false;
|
||||
onScrollEnd();
|
||||
}
|
||||
|
||||
if (isResizing) {
|
||||
isResizing = false;
|
||||
animateView(playerImpl.getResizingIndicator(), false, 100, 0);
|
||||
playerImpl.changeState(playerImpl.getCurrentState());
|
||||
}
|
||||
savePositionAndSize();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetcher used if open by a link out of NewPipe
|
||||
*/
|
||||
private class FetcherRunnable implements StreamExtractorWorker.OnStreamInfoReceivedListener {
|
||||
private final Context context;
|
||||
private final Handler mainHandler;
|
||||
|
||||
FetcherRunnable(Context context) {
|
||||
this.mainHandler = new Handler(PopupVideoPlayer.this.getMainLooper());
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(StreamInfo info) {
|
||||
playerImpl.setVideoTitle(info.title);
|
||||
playerImpl.setVideoUrl(info.webpage_url);
|
||||
playerImpl.setVideoThumbnailUrl(info.thumbnail_url);
|
||||
playerImpl.setChannelName(info.uploader);
|
||||
|
||||
playerImpl.setVideoStreamsList(Utils.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false));
|
||||
playerImpl.setAudioStream(Utils.getHighestQualityAudio(info.audio_streams));
|
||||
|
||||
int defaultResolution = Utils.getPopupDefaultResolution(context, playerImpl.getVideoStreamsList());
|
||||
playerImpl.setSelectedIndexStream(defaultResolution);
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "FetcherRunnable.StreamExtractor: chosen = "
|
||||
+ MediaFormat.getNameById(info.video_streams.get(defaultResolution).format) + " "
|
||||
+ info.video_streams.get(defaultResolution).resolution + " > "
|
||||
+ info.video_streams.get(defaultResolution).url);
|
||||
}
|
||||
|
||||
if (info.start_position > 0) playerImpl.setVideoStartPos(info.start_position * 1000);
|
||||
else playerImpl.setVideoStartPos(-1);
|
||||
|
||||
mainHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
playerImpl.play(true);
|
||||
}
|
||||
});
|
||||
|
||||
imageLoader.resume();
|
||||
imageLoader.loadImage(info.thumbnail_url, displayImageOptions, new SimpleImageLoadingListener() {
|
||||
@Override
|
||||
public void onLoadingComplete(final String imageUri, View view, final Bitmap loadedImage) {
|
||||
if (playerImpl == null || playerImpl.getPlayer() == null) return;
|
||||
if (DEBUG) Log.d(TAG, "FetcherRunnable.imageLoader.onLoadingComplete() called with: imageUri = [" + imageUri + "]");
|
||||
mainHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
playerImpl.setVideoThumbnail(loadedImage);
|
||||
if (loadedImage != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage);
|
||||
updateNotification(-1);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(final int messageId) {
|
||||
mainHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(context, messageId, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReCaptchaException() {
|
||||
mainHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(context, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
// Starting ReCaptcha Challenge Activity
|
||||
Intent intent = new Intent(context, ReCaptchaActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBlockedByGemaError() {
|
||||
mainHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(context, R.string.blocked_by_gema, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContentErrorWithMessage(final int messageId) {
|
||||
mainHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(context, messageId, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContentError() {
|
||||
mainHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnrecoverableError(Exception exception) {
|
||||
exception.printStackTrace();
|
||||
stopSelf();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
715
app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java
Normal file
@@ -0,0 +1,715 @@
|
||||
package org.schabi.newpipe.player;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.PropertyValuesHolder;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.View;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.MergingMediaSource;
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.stream_info.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream_info.VideoStream;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Vector;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
/**
|
||||
* Base for <b>video</b> players
|
||||
*
|
||||
* @author mauriciocolli
|
||||
*/
|
||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||
public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.VideoListener, SeekBar.OnSeekBarChangeListener, View.OnClickListener, ExoPlayer.EventListener, PopupMenu.OnMenuItemClickListener, PopupMenu.OnDismissListener {
|
||||
public static final boolean DEBUG = BasePlayer.DEBUG;
|
||||
public final String TAG;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Intent
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public static final String VIDEO_STREAMS_LIST = "video_streams_list";
|
||||
public static final String VIDEO_ONLY_AUDIO_STREAM = "video_only_audio_stream";
|
||||
public static final String INDEX_SEL_VIDEO_STREAM = "index_selected_video_stream";
|
||||
public static final String STARTED_FROM_NEWPIPE = "started_from_newpipe";
|
||||
|
||||
private int selectedIndexStream;
|
||||
private ArrayList<VideoStream> videoStreamsList = new ArrayList<>();
|
||||
private AudioStream videoOnlyAudioStream;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Player
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public static final int DEFAULT_CONTROLS_HIDE_TIME = 3000; // 3 Seconds
|
||||
|
||||
private boolean startedFromNewPipe = true;
|
||||
private boolean wasPlaying = false;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private View rootView;
|
||||
|
||||
private AspectRatioFrameLayout aspectRatioFrameLayout;
|
||||
private SurfaceView surfaceView;
|
||||
private View surfaceForeground;
|
||||
|
||||
private View loadingPanel;
|
||||
private ImageView endScreen;
|
||||
private ImageView controlAnimationView;
|
||||
|
||||
private View controlsRoot;
|
||||
private TextView currentDisplaySeek;
|
||||
|
||||
private View bottomControlsRoot;
|
||||
private SeekBar playbackSeekBar;
|
||||
private TextView playbackCurrentTime;
|
||||
private TextView playbackEndTime;
|
||||
|
||||
private View topControlsRoot;
|
||||
private TextView qualityTextView;
|
||||
private ImageButton fullScreenButton;
|
||||
|
||||
private ValueAnimator controlViewAnimator;
|
||||
private Handler controlsVisibilityHandler = new Handler();
|
||||
|
||||
private boolean isQualityPopupMenuVisible = false;
|
||||
private boolean qualityChanged = false;
|
||||
private int qualityPopupMenuGroupId = 69;
|
||||
private PopupMenu qualityPopupMenu;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public VideoPlayer(String debugTag, Context context) {
|
||||
super(context);
|
||||
this.TAG = debugTag;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public void setup(View rootView) {
|
||||
initViews(rootView);
|
||||
setup();
|
||||
}
|
||||
|
||||
public void initViews(View rootView) {
|
||||
this.rootView = rootView;
|
||||
this.aspectRatioFrameLayout = (AspectRatioFrameLayout) rootView.findViewById(R.id.aspectRatioLayout);
|
||||
this.surfaceView = (SurfaceView) rootView.findViewById(R.id.surfaceView);
|
||||
this.surfaceForeground = rootView.findViewById(R.id.surfaceForeground);
|
||||
this.loadingPanel = rootView.findViewById(R.id.loading_panel);
|
||||
this.endScreen = (ImageView) rootView.findViewById(R.id.endScreen);
|
||||
this.controlAnimationView = (ImageView) rootView.findViewById(R.id.controlAnimationView);
|
||||
this.controlsRoot = rootView.findViewById(R.id.playbackControlRoot);
|
||||
this.currentDisplaySeek = (TextView) rootView.findViewById(R.id.currentDisplaySeek);
|
||||
this.playbackSeekBar = (SeekBar) rootView.findViewById(R.id.playbackSeekBar);
|
||||
this.playbackCurrentTime = (TextView) rootView.findViewById(R.id.playbackCurrentTime);
|
||||
this.playbackEndTime = (TextView) rootView.findViewById(R.id.playbackEndTime);
|
||||
this.bottomControlsRoot = rootView.findViewById(R.id.bottomControls);
|
||||
this.topControlsRoot = rootView.findViewById(R.id.topControls);
|
||||
this.qualityTextView = (TextView) rootView.findViewById(R.id.qualityTextView);
|
||||
this.fullScreenButton = (ImageButton) rootView.findViewById(R.id.fullScreenButton);
|
||||
|
||||
//this.aspectRatioFrameLayout.setAspectRatio(16.0f / 9.0f);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
|
||||
this.playbackSeekBar.getProgressDrawable().setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY);
|
||||
|
||||
this.qualityPopupMenu = new PopupMenu(context, qualityTextView);
|
||||
|
||||
((ProgressBar) this.loadingPanel.findViewById(R.id.progressBarLoadingPanel)).getIndeterminateDrawable().setColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initListeners() {
|
||||
super.initListeners();
|
||||
playbackSeekBar.setOnSeekBarChangeListener(this);
|
||||
fullScreenButton.setOnClickListener(this);
|
||||
qualityTextView.setOnClickListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initPlayer() {
|
||||
super.initPlayer();
|
||||
simpleExoPlayer.setVideoSurfaceView(surfaceView);
|
||||
simpleExoPlayer.setVideoListener(this);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void handleIntent(Intent intent) {
|
||||
super.handleIntent(intent);
|
||||
if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]");
|
||||
if (intent == null) return;
|
||||
|
||||
selectedIndexStream = intent.getIntExtra(INDEX_SEL_VIDEO_STREAM, -1);
|
||||
|
||||
Serializable serializable = intent.getSerializableExtra(VIDEO_STREAMS_LIST);
|
||||
|
||||
if (serializable instanceof ArrayList) videoStreamsList = (ArrayList<VideoStream>) serializable;
|
||||
if (serializable instanceof Vector) videoStreamsList = new ArrayList<>((List<VideoStream>) serializable);
|
||||
|
||||
Serializable audioStream = intent.getSerializableExtra(VIDEO_ONLY_AUDIO_STREAM);
|
||||
if (audioStream != null) videoOnlyAudioStream = (AudioStream) audioStream;
|
||||
|
||||
startedFromNewPipe = intent.getBooleanExtra(STARTED_FROM_NEWPIPE, true);
|
||||
play(true);
|
||||
}
|
||||
|
||||
|
||||
public void play(boolean autoPlay) {
|
||||
playUrl(getSelectedVideoStream().url, MediaFormat.getSuffixById(getSelectedVideoStream().format), autoPlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playUrl(String url, String format, boolean autoPlay) {
|
||||
if (DEBUG) Log.d(TAG, "play() called with: url = [" + url + "], autoPlay = [" + autoPlay + "]");
|
||||
qualityChanged = false;
|
||||
|
||||
if (url == null || simpleExoPlayer == null) {
|
||||
RuntimeException runtimeException = new RuntimeException((url == null ? "Url " : "Player ") + " null");
|
||||
onError(runtimeException);
|
||||
throw runtimeException;
|
||||
}
|
||||
|
||||
qualityPopupMenu.getMenu().removeGroup(qualityPopupMenuGroupId);
|
||||
buildQualityMenu(qualityPopupMenu);
|
||||
|
||||
super.playUrl(url, format, autoPlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaSource buildMediaSource(String url, String overrideExtension) {
|
||||
MediaSource mediaSource = super.buildMediaSource(url, overrideExtension);
|
||||
if (!getSelectedVideoStream().isVideoOnly) return mediaSource;
|
||||
|
||||
Uri audioUri = Uri.parse(videoOnlyAudioStream.url);
|
||||
return new MergingMediaSource(mediaSource, new ExtractorMediaSource(audioUri, cacheDataSourceFactory, extractorsFactory, null, null));
|
||||
}
|
||||
|
||||
public void buildQualityMenu(PopupMenu popupMenu) {
|
||||
for (int i = 0; i < videoStreamsList.size(); i++) {
|
||||
VideoStream videoStream = videoStreamsList.get(i);
|
||||
popupMenu.getMenu().add(qualityPopupMenuGroupId, i, Menu.NONE, MediaFormat.getNameById(videoStream.format) + " " + videoStream.resolution);
|
||||
}
|
||||
qualityTextView.setText(getSelectedVideoStream().resolution);
|
||||
popupMenu.setOnMenuItemClickListener(this);
|
||||
popupMenu.setOnDismissListener(this);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// States Implementation
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onLoading() {
|
||||
if (DEBUG) Log.d(TAG, "onLoading() called");
|
||||
|
||||
if (!isProgressLoopRunning.get()) startProgressLoop();
|
||||
|
||||
controlsVisibilityHandler.removeCallbacksAndMessages(null);
|
||||
animateView(controlsRoot, false, 300);
|
||||
|
||||
showAndAnimateControl(-1, true);
|
||||
playbackSeekBar.setEnabled(true);
|
||||
playbackSeekBar.setProgress(0);
|
||||
|
||||
// Bug on lower api, disabling and enabling the seekBar resets the thumb color -.-, so sets the color again
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
|
||||
|
||||
animateView(endScreen, false, 0);
|
||||
loadingPanel.setBackgroundColor(Color.BLACK);
|
||||
animateView(loadingPanel, true, 0);
|
||||
animateView(surfaceForeground, true, 100);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaying() {
|
||||
if (DEBUG) Log.d(TAG, "onPlaying() called");
|
||||
if (!isProgressLoopRunning.get()) startProgressLoop();
|
||||
showAndAnimateControl(-1, true);
|
||||
loadingPanel.setVisibility(View.GONE);
|
||||
showControlsThenHide();
|
||||
animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBuffering() {
|
||||
if (DEBUG) Log.d(TAG, "onBuffering() called");
|
||||
loadingPanel.setBackgroundColor(Color.TRANSPARENT);
|
||||
animateView(loadingPanel, true, 500);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPaused() {
|
||||
if (DEBUG) Log.d(TAG, "onPaused() called");
|
||||
showControls(400);
|
||||
loadingPanel.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPausedSeek() {
|
||||
if (DEBUG) Log.d(TAG, "onPausedSeek() called");
|
||||
showAndAnimateControl(-1, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleted() {
|
||||
if (DEBUG) Log.d(TAG, "onCompleted() called");
|
||||
|
||||
if (isProgressLoopRunning.get()) stopProgressLoop();
|
||||
|
||||
showControls(500);
|
||||
animateView(endScreen, true, 800);
|
||||
animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200);
|
||||
loadingPanel.setVisibility(View.GONE);
|
||||
|
||||
playbackSeekBar.setMax((int) simpleExoPlayer.getDuration());
|
||||
playbackSeekBar.setProgress(playbackSeekBar.getMax());
|
||||
playbackSeekBar.setEnabled(false);
|
||||
playbackEndTime.setText(getTimeString(playbackSeekBar.getMax()));
|
||||
playbackCurrentTime.setText(playbackEndTime.getText());
|
||||
// Bug on lower api, disabling and enabling the seekBar resets the thumb color -.-, so sets the color again
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
|
||||
|
||||
animateView(surfaceForeground, true, 100);
|
||||
|
||||
if (currentRepeatMode == RepeatMode.REPEAT_ONE) {
|
||||
changeState(STATE_LOADING);
|
||||
simpleExoPlayer.seekTo(0);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// ExoPlayer Video Listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onVideoSizeChanged() called with: width / height = [" + width + " / " + height + " = " + (((float) width) / height) + "], unappliedRotationDegrees = [" + unappliedRotationDegrees + "], pixelWidthHeightRatio = [" + pixelWidthHeightRatio + "]");
|
||||
}
|
||||
aspectRatioFrameLayout.setAspectRatio(((float) width) / height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRenderedFirstFrame() {
|
||||
animateView(surfaceForeground, false, 100);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// General Player
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onPrepared(boolean playWhenReady) {
|
||||
if (DEBUG) Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]");
|
||||
|
||||
if (videoStartPos > 0) {
|
||||
playbackSeekBar.setProgress(videoStartPos);
|
||||
playbackCurrentTime.setText(getTimeString(videoStartPos));
|
||||
videoStartPos = -1;
|
||||
}
|
||||
|
||||
playbackSeekBar.setMax((int) simpleExoPlayer.getDuration());
|
||||
playbackEndTime.setText(getTimeString((int) simpleExoPlayer.getDuration()));
|
||||
|
||||
super.onPrepared(playWhenReady);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
super.destroy();
|
||||
if (endScreen != null) endScreen.setImageBitmap(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpdateProgress(int currentProgress, int duration, int bufferPercent) {
|
||||
if (!isPrepared) return;
|
||||
|
||||
if (currentState != STATE_PAUSED) {
|
||||
if (currentState != STATE_PAUSED_SEEK) playbackSeekBar.setProgress(currentProgress);
|
||||
playbackCurrentTime.setText(getTimeString(currentProgress));
|
||||
}
|
||||
if (simpleExoPlayer.isLoading() || bufferPercent > 90) {
|
||||
playbackSeekBar.setSecondaryProgress((int) (playbackSeekBar.getMax() * ((float) bufferPercent / 100)));
|
||||
}
|
||||
if (DEBUG && bufferPercent % 20 == 0) { //Limit log
|
||||
Log.d(TAG, "updateProgress() called with: isVisible = " + isControlsVisible() + ", currentProgress = [" + currentProgress + "], duration = [" + duration + "], bufferPercent = [" + bufferPercent + "]");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVideoPlayPauseRepeat() {
|
||||
if (DEBUG) Log.d(TAG, "onVideoPlayPauseRepeat() called");
|
||||
if (qualityChanged) {
|
||||
setVideoStartPos(0);
|
||||
play(true);
|
||||
} else super.onVideoPlayPauseRepeat();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onThumbnailReceived(Bitmap thumbnail) {
|
||||
super.onThumbnailReceived(thumbnail);
|
||||
if (thumbnail != null) endScreen.setImageBitmap(thumbnail);
|
||||
}
|
||||
|
||||
protected abstract void onFullScreenButtonClicked();
|
||||
|
||||
@Override
|
||||
public void onFastRewind() {
|
||||
super.onFastRewind();
|
||||
showAndAnimateControl(R.drawable.ic_action_av_fast_rewind, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFastForward() {
|
||||
super.onFastForward();
|
||||
showAndAnimateControl(R.drawable.ic_action_av_fast_forward, true);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// OnClick related
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]");
|
||||
if (v.getId() == fullScreenButton.getId()) {
|
||||
onFullScreenButtonClicked();
|
||||
} else if (v.getId() == qualityTextView.getId()) {
|
||||
onQualitySelectorClicked();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an item of the quality selector is selected
|
||||
*/
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem menuItem) {
|
||||
if (DEBUG) Log.d(TAG, "onMenuItemClick() called with: menuItem = [" + menuItem + "], menuItem.getItemId = [" + menuItem.getItemId() + "]");
|
||||
if (selectedIndexStream == menuItem.getItemId()) return true;
|
||||
setVideoStartPos((int) simpleExoPlayer.getCurrentPosition());
|
||||
|
||||
selectedIndexStream = menuItem.getItemId();
|
||||
if (!(getCurrentState() == STATE_COMPLETED)) play(wasPlaying);
|
||||
else qualityChanged = true;
|
||||
|
||||
qualityTextView.setText(menuItem.getTitle());
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the quality selector is dismissed
|
||||
*/
|
||||
@Override
|
||||
public void onDismiss(PopupMenu menu) {
|
||||
if (DEBUG) Log.d(TAG, "onDismiss() called with: menu = [" + menu + "]");
|
||||
isQualityPopupMenuVisible = false;
|
||||
qualityTextView.setText(getSelectedVideoStream().resolution);
|
||||
}
|
||||
|
||||
public void onQualitySelectorClicked() {
|
||||
if (DEBUG) Log.d(TAG, "onQualitySelectorClicked() called");
|
||||
qualityPopupMenu.show();
|
||||
isQualityPopupMenuVisible = true;
|
||||
showControls(300);
|
||||
|
||||
VideoStream videoStream = getSelectedVideoStream();
|
||||
qualityTextView.setText(MediaFormat.getNameById(videoStream.format) + " " + videoStream.resolution);
|
||||
wasPlaying = isPlaying();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// SeekBar Listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
if (DEBUG && fromUser) Log.d(TAG, "onProgressChanged() called with: seekBar = [" + seekBar + "], progress = [" + progress + "]");
|
||||
//if (fromUser) playbackCurrentTime.setText(getTimeString(progress));
|
||||
if (fromUser) currentDisplaySeek.setText(getTimeString(progress));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
if (DEBUG) Log.d(TAG, "onStartTrackingTouch() called with: seekBar = [" + seekBar + "]");
|
||||
if (getCurrentState() != STATE_PAUSED_SEEK) changeState(STATE_PAUSED_SEEK);
|
||||
|
||||
wasPlaying = isPlaying();
|
||||
if (isPlaying()) simpleExoPlayer.setPlayWhenReady(false);
|
||||
|
||||
showControls(0);
|
||||
animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, true, 300);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
if (DEBUG) Log.d(TAG, "onStopTrackingTouch() called with: seekBar = [" + seekBar + "]");
|
||||
|
||||
simpleExoPlayer.seekTo(seekBar.getProgress());
|
||||
if (wasPlaying || simpleExoPlayer.getDuration() == seekBar.getProgress()) simpleExoPlayer.setPlayWhenReady(true);
|
||||
|
||||
playbackCurrentTime.setText(getTimeString(seekBar.getProgress()));
|
||||
animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200);
|
||||
|
||||
if (getCurrentState() == STATE_PAUSED_SEEK) changeState(STATE_BUFFERING);
|
||||
if (!isProgressLoopRunning.get()) startProgressLoop();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public boolean isControlsVisible() {
|
||||
return controlsRoot != null && controlsRoot.getVisibility() == View.VISIBLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a animation, and depending on goneOnEnd, will stay on the screen or be gone
|
||||
*
|
||||
* @param drawableId the drawable that will be used to animate, pass -1 to clear any animation that is visible
|
||||
* @param goneOnEnd will set the animation view to GONE on the end of the animation
|
||||
*/
|
||||
public void showAndAnimateControl(final int drawableId, final boolean goneOnEnd) {
|
||||
if (DEBUG) Log.d(TAG, "showAndAnimateControl() called with: drawableId = [" + drawableId + "], goneOnEnd = [" + goneOnEnd + "]");
|
||||
if (controlViewAnimator != null && controlViewAnimator.isRunning()) {
|
||||
if (DEBUG) Log.d(TAG, "showAndAnimateControl: controlViewAnimator.isRunning");
|
||||
controlViewAnimator.end();
|
||||
}
|
||||
|
||||
if (drawableId == -1) {
|
||||
if (controlAnimationView.getVisibility() == View.VISIBLE) {
|
||||
controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder(controlAnimationView,
|
||||
PropertyValuesHolder.ofFloat(View.ALPHA, 1f, 0f),
|
||||
PropertyValuesHolder.ofFloat(View.SCALE_X, 1.4f, 1f),
|
||||
PropertyValuesHolder.ofFloat(View.SCALE_Y, 1.4f, 1f)
|
||||
).setDuration(300);
|
||||
controlViewAnimator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
controlAnimationView.setVisibility(View.GONE);
|
||||
}
|
||||
});
|
||||
controlViewAnimator.start();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
float scaleFrom = goneOnEnd ? 1f : 1f, scaleTo = goneOnEnd ? 1.8f : 1.4f;
|
||||
float alphaFrom = goneOnEnd ? 1f : 0f, alphaTo = goneOnEnd ? 0f : 1f;
|
||||
|
||||
|
||||
controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder(controlAnimationView,
|
||||
PropertyValuesHolder.ofFloat(View.ALPHA, alphaFrom, alphaTo),
|
||||
PropertyValuesHolder.ofFloat(View.SCALE_X, scaleFrom, scaleTo),
|
||||
PropertyValuesHolder.ofFloat(View.SCALE_Y, scaleFrom, scaleTo)
|
||||
);
|
||||
controlViewAnimator.setDuration(goneOnEnd ? 1000 : 500);
|
||||
controlViewAnimator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
if (goneOnEnd) controlAnimationView.setVisibility(View.GONE);
|
||||
else controlAnimationView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
controlAnimationView.setVisibility(View.VISIBLE);
|
||||
controlAnimationView.setImageDrawable(ContextCompat.getDrawable(context, drawableId));
|
||||
controlViewAnimator.start();
|
||||
}
|
||||
|
||||
public boolean isQualityMenuVisible() {
|
||||
return isQualityPopupMenuVisible;
|
||||
}
|
||||
|
||||
public void showControlsThenHide() {
|
||||
if (DEBUG) Log.d(TAG, "showControlsThenHide() called");
|
||||
animateView(controlsRoot, true, 300, 0, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
hideControls(300, DEFAULT_CONTROLS_HIDE_TIME);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void showControls(long duration) {
|
||||
if (DEBUG) Log.d(TAG, "showControls() called");
|
||||
controlsVisibilityHandler.removeCallbacksAndMessages(null);
|
||||
animateView(controlsRoot, true, duration);
|
||||
}
|
||||
|
||||
public void hideControls(final long duration, long delay) {
|
||||
if (DEBUG) Log.d(TAG, "hideControls() called with: delay = [" + delay + "]");
|
||||
controlsVisibilityHandler.removeCallbacksAndMessages(null);
|
||||
controlsVisibilityHandler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
animateView(controlsRoot, false, duration);
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Getters and Setters
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public AspectRatioFrameLayout getAspectRatioFrameLayout() {
|
||||
return aspectRatioFrameLayout;
|
||||
}
|
||||
|
||||
public SurfaceView getSurfaceView() {
|
||||
return surfaceView;
|
||||
}
|
||||
|
||||
public boolean wasPlaying() {
|
||||
return wasPlaying;
|
||||
}
|
||||
|
||||
public VideoStream getSelectedVideoStream() {
|
||||
return videoStreamsList.get(selectedIndexStream);
|
||||
}
|
||||
|
||||
public Uri getSelectedStreamUri() {
|
||||
return Uri.parse(getSelectedVideoStream().url);
|
||||
}
|
||||
|
||||
public int getQualityPopupMenuGroupId() {
|
||||
return qualityPopupMenuGroupId;
|
||||
}
|
||||
|
||||
public int getSelectedStreamIndex() {
|
||||
return selectedIndexStream;
|
||||
}
|
||||
|
||||
public void setSelectedIndexStream(int selectedIndexStream) {
|
||||
this.selectedIndexStream = selectedIndexStream;
|
||||
}
|
||||
|
||||
public void setAudioStream(AudioStream audioStream) {
|
||||
this.videoOnlyAudioStream = audioStream;
|
||||
}
|
||||
|
||||
public AudioStream getAudioStream() {
|
||||
return videoOnlyAudioStream;
|
||||
}
|
||||
|
||||
public ArrayList<VideoStream> getVideoStreamsList() {
|
||||
return videoStreamsList;
|
||||
}
|
||||
|
||||
public void setVideoStreamsList(ArrayList<VideoStream> videoStreamsList) {
|
||||
this.videoStreamsList = videoStreamsList;
|
||||
}
|
||||
|
||||
public boolean isStartedFromNewPipe() {
|
||||
return startedFromNewPipe;
|
||||
}
|
||||
|
||||
public void setStartedFromNewPipe(boolean startedFromNewPipe) {
|
||||
this.startedFromNewPipe = startedFromNewPipe;
|
||||
}
|
||||
|
||||
public Handler getControlsVisibilityHandler() {
|
||||
return controlsVisibilityHandler;
|
||||
}
|
||||
|
||||
public View getRootView() {
|
||||
return rootView;
|
||||
}
|
||||
|
||||
public void setRootView(View rootView) {
|
||||
this.rootView = rootView;
|
||||
}
|
||||
|
||||
public View getLoadingPanel() {
|
||||
return loadingPanel;
|
||||
}
|
||||
|
||||
public ImageView getEndScreen() {
|
||||
return endScreen;
|
||||
}
|
||||
|
||||
public ImageView getControlAnimationView() {
|
||||
return controlAnimationView;
|
||||
}
|
||||
|
||||
public View getControlsRoot() {
|
||||
return controlsRoot;
|
||||
}
|
||||
|
||||
public View getBottomControlsRoot() {
|
||||
return bottomControlsRoot;
|
||||
}
|
||||
|
||||
public SeekBar getPlaybackSeekBar() {
|
||||
return playbackSeekBar;
|
||||
}
|
||||
|
||||
public TextView getPlaybackCurrentTime() {
|
||||
return playbackCurrentTime;
|
||||
}
|
||||
|
||||
public TextView getPlaybackEndTime() {
|
||||
return playbackEndTime;
|
||||
}
|
||||
|
||||
public View getTopControlsRoot() {
|
||||
return topControlsRoot;
|
||||
}
|
||||
|
||||
public TextView getQualityTextView() {
|
||||
return qualityTextView;
|
||||
}
|
||||
|
||||
public ImageButton getFullScreenButton() {
|
||||
return fullScreenButton;
|
||||
}
|
||||
|
||||
public PopupMenu getQualityPopupMenu() {
|
||||
return qualityPopupMenu;
|
||||
}
|
||||
|
||||
public View getSurfaceForeground() {
|
||||
return surfaceForeground;
|
||||
}
|
||||
|
||||
public TextView getCurrentDisplaySeek() {
|
||||
return currentDisplaySeek;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,268 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.schabi.newpipe.player.exoplayer;
|
||||
|
||||
import org.schabi.newpipe.player.exoplayer.NPExoPlayer.RendererBuilder;
|
||||
|
||||
import com.google.android.exoplayer.DefaultLoadControl;
|
||||
import com.google.android.exoplayer.LoadControl;
|
||||
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
|
||||
import com.google.android.exoplayer.MediaCodecSelector;
|
||||
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
|
||||
import com.google.android.exoplayer.TrackRenderer;
|
||||
import com.google.android.exoplayer.audio.AudioCapabilities;
|
||||
import com.google.android.exoplayer.chunk.ChunkSampleSource;
|
||||
import com.google.android.exoplayer.chunk.ChunkSource;
|
||||
import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator;
|
||||
import com.google.android.exoplayer.dash.DashChunkSource;
|
||||
import com.google.android.exoplayer.dash.DefaultDashTrackSelector;
|
||||
import com.google.android.exoplayer.dash.mpd.AdaptationSet;
|
||||
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
|
||||
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionParser;
|
||||
import com.google.android.exoplayer.dash.mpd.Period;
|
||||
import com.google.android.exoplayer.dash.mpd.UtcTimingElement;
|
||||
import com.google.android.exoplayer.dash.mpd.UtcTimingElementResolver;
|
||||
import com.google.android.exoplayer.dash.mpd.UtcTimingElementResolver.UtcTimingCallback;
|
||||
import com.google.android.exoplayer.drm.MediaDrmCallback;
|
||||
import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
|
||||
import com.google.android.exoplayer.drm.UnsupportedDrmException;
|
||||
import com.google.android.exoplayer.text.TextTrackRenderer;
|
||||
import com.google.android.exoplayer.upstream.DataSource;
|
||||
import com.google.android.exoplayer.upstream.DefaultAllocator;
|
||||
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer.upstream.DefaultUriDataSource;
|
||||
import com.google.android.exoplayer.upstream.UriDataSource;
|
||||
import com.google.android.exoplayer.util.ManifestFetcher;
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaCodec;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* A {@link RendererBuilder} for DASH.
|
||||
*/
|
||||
public class DashRendererBuilder implements RendererBuilder {
|
||||
|
||||
private static final String TAG = "DashRendererBuilder";
|
||||
|
||||
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
|
||||
private static final int VIDEO_BUFFER_SEGMENTS = 200;
|
||||
private static final int AUDIO_BUFFER_SEGMENTS = 54;
|
||||
private static final int TEXT_BUFFER_SEGMENTS = 2;
|
||||
private static final int LIVE_EDGE_LATENCY_MS = 30000;
|
||||
|
||||
private static final int SECURITY_LEVEL_UNKNOWN = -1;
|
||||
private static final int SECURITY_LEVEL_1 = 1;
|
||||
private static final int SECURITY_LEVEL_3 = 3;
|
||||
|
||||
private final Context context;
|
||||
private final String userAgent;
|
||||
private final String url;
|
||||
private final MediaDrmCallback drmCallback;
|
||||
|
||||
private AsyncRendererBuilder currentAsyncBuilder;
|
||||
|
||||
public DashRendererBuilder(Context context, String userAgent, String url,
|
||||
MediaDrmCallback drmCallback) {
|
||||
this.context = context;
|
||||
this.userAgent = userAgent;
|
||||
this.url = url;
|
||||
this.drmCallback = drmCallback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buildRenderers(NPExoPlayer player) {
|
||||
currentAsyncBuilder = new AsyncRendererBuilder(context, userAgent, url, drmCallback, player);
|
||||
currentAsyncBuilder.init();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
if (currentAsyncBuilder != null) {
|
||||
currentAsyncBuilder.cancel();
|
||||
currentAsyncBuilder = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class AsyncRendererBuilder
|
||||
implements ManifestFetcher.ManifestCallback<MediaPresentationDescription>, UtcTimingCallback {
|
||||
|
||||
private final Context context;
|
||||
private final String userAgent;
|
||||
private final MediaDrmCallback drmCallback;
|
||||
private final NPExoPlayer player;
|
||||
private final ManifestFetcher<MediaPresentationDescription> manifestFetcher;
|
||||
private final UriDataSource manifestDataSource;
|
||||
|
||||
private boolean canceled;
|
||||
private MediaPresentationDescription manifest;
|
||||
private long elapsedRealtimeOffset;
|
||||
|
||||
public AsyncRendererBuilder(Context context, String userAgent, String url,
|
||||
MediaDrmCallback drmCallback, NPExoPlayer player) {
|
||||
this.context = context;
|
||||
this.userAgent = userAgent;
|
||||
this.drmCallback = drmCallback;
|
||||
this.player = player;
|
||||
MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser();
|
||||
manifestDataSource = new DefaultUriDataSource(context, userAgent);
|
||||
manifestFetcher = new ManifestFetcher<>(url, manifestDataSource, parser);
|
||||
}
|
||||
|
||||
public void init() {
|
||||
manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this);
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
canceled = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSingleManifest(MediaPresentationDescription manifest) {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.manifest = manifest;
|
||||
if (manifest.dynamic && manifest.utcTiming != null) {
|
||||
UtcTimingElementResolver.resolveTimingElement(manifestDataSource, manifest.utcTiming,
|
||||
manifestFetcher.getManifestLoadCompleteTimestamp(), this);
|
||||
} else {
|
||||
buildRenderers();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSingleManifestError(IOException e) {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
player.onRenderersError(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimestampResolved(UtcTimingElement utcTiming, long elapsedRealtimeOffset) {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.elapsedRealtimeOffset = elapsedRealtimeOffset;
|
||||
buildRenderers();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimestampError(UtcTimingElement utcTiming, IOException e) {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.e(TAG, "Failed to resolve UtcTiming element [" + utcTiming + "]", e);
|
||||
// Be optimistic and continue in the hope that the device clock is correct.
|
||||
buildRenderers();
|
||||
}
|
||||
|
||||
private void buildRenderers() {
|
||||
Period period = manifest.getPeriod(0);
|
||||
Handler mainHandler = player.getMainHandler();
|
||||
LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE));
|
||||
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, player);
|
||||
|
||||
boolean hasContentProtection = false;
|
||||
for (int i = 0; i < period.adaptationSets.size(); i++) {
|
||||
AdaptationSet adaptationSet = period.adaptationSets.get(i);
|
||||
if (adaptationSet.type != AdaptationSet.TYPE_UNKNOWN) {
|
||||
hasContentProtection |= adaptationSet.hasContentProtection();
|
||||
}
|
||||
}
|
||||
|
||||
// Check drm support if necessary.
|
||||
boolean filterHdContent = false;
|
||||
StreamingDrmSessionManager drmSessionManager = null;
|
||||
if (hasContentProtection) {
|
||||
if (Util.SDK_INT < 18) {
|
||||
player.onRenderersError(
|
||||
new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
drmSessionManager = StreamingDrmSessionManager.newWidevineInstance(
|
||||
player.getPlaybackLooper(), drmCallback, null, player.getMainHandler(), player);
|
||||
filterHdContent = getWidevineSecurityLevel(drmSessionManager) != SECURITY_LEVEL_1;
|
||||
} catch (UnsupportedDrmException e) {
|
||||
player.onRenderersError(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Build the video renderer.
|
||||
DataSource videoDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||
ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher,
|
||||
DefaultDashTrackSelector.newVideoInstance(context, true, filterHdContent),
|
||||
videoDataSource, new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS,
|
||||
elapsedRealtimeOffset, mainHandler, player, NPExoPlayer.TYPE_VIDEO);
|
||||
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
|
||||
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
|
||||
NPExoPlayer.TYPE_VIDEO);
|
||||
TrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context, videoSampleSource,
|
||||
MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000,
|
||||
drmSessionManager, true, mainHandler, player, 50);
|
||||
|
||||
// Build the audio renderer.
|
||||
DataSource audioDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||
ChunkSource audioChunkSource = new DashChunkSource(manifestFetcher,
|
||||
DefaultDashTrackSelector.newAudioInstance(), audioDataSource, null, LIVE_EDGE_LATENCY_MS,
|
||||
elapsedRealtimeOffset, mainHandler, player, NPExoPlayer.TYPE_AUDIO);
|
||||
ChunkSampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
|
||||
AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
|
||||
NPExoPlayer.TYPE_AUDIO);
|
||||
TrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource,
|
||||
MediaCodecSelector.DEFAULT, drmSessionManager, true, mainHandler, player,
|
||||
AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC);
|
||||
|
||||
// Build the text renderer.
|
||||
DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||
ChunkSource textChunkSource = new DashChunkSource(manifestFetcher,
|
||||
DefaultDashTrackSelector.newTextInstance(), textDataSource, null, LIVE_EDGE_LATENCY_MS,
|
||||
elapsedRealtimeOffset, mainHandler, player, NPExoPlayer.TYPE_TEXT);
|
||||
ChunkSampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl,
|
||||
TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
|
||||
NPExoPlayer.TYPE_TEXT);
|
||||
TrackRenderer textRenderer = new TextTrackRenderer(textSampleSource, player,
|
||||
mainHandler.getLooper());
|
||||
|
||||
// Invoke the callback.
|
||||
TrackRenderer[] renderers = new TrackRenderer[NPExoPlayer.RENDERER_COUNT];
|
||||
renderers[NPExoPlayer.TYPE_VIDEO] = videoRenderer;
|
||||
renderers[NPExoPlayer.TYPE_AUDIO] = audioRenderer;
|
||||
renderers[NPExoPlayer.TYPE_TEXT] = textRenderer;
|
||||
player.onRenderers(renderers, bandwidthMeter);
|
||||
}
|
||||
|
||||
private static int getWidevineSecurityLevel(StreamingDrmSessionManager sessionManager) {
|
||||
String securityLevelProperty = sessionManager.getPropertyString("securityLevel");
|
||||
return securityLevelProperty.equals("L1") ? SECURITY_LEVEL_1 : securityLevelProperty
|
||||
.equals("L3") ? SECURITY_LEVEL_3 : SECURITY_LEVEL_UNKNOWN;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,214 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.schabi.newpipe.player.exoplayer;
|
||||
|
||||
import com.google.android.exoplayer.ExoPlayer;
|
||||
import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException;
|
||||
import com.google.android.exoplayer.TimeRange;
|
||||
import com.google.android.exoplayer.audio.AudioTrack;
|
||||
import com.google.android.exoplayer.chunk.Format;
|
||||
import com.google.android.exoplayer.util.VerboseLogUtil;
|
||||
|
||||
import android.media.MediaCodec.CryptoException;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Logs player events using {@link Log}.
|
||||
*/
|
||||
public class EventLogger implements NPExoPlayer.Listener, NPExoPlayer.InfoListener,
|
||||
NPExoPlayer.InternalErrorListener {
|
||||
|
||||
private static final String TAG = "EventLogger";
|
||||
private static final NumberFormat TIME_FORMAT;
|
||||
static {
|
||||
TIME_FORMAT = NumberFormat.getInstance(Locale.US);
|
||||
TIME_FORMAT.setMinimumFractionDigits(2);
|
||||
TIME_FORMAT.setMaximumFractionDigits(2);
|
||||
}
|
||||
|
||||
private long sessionStartTimeMs;
|
||||
private long[] loadStartTimeMs;
|
||||
private long[] availableRangeValuesUs;
|
||||
|
||||
public EventLogger() {
|
||||
loadStartTimeMs = new long[NPExoPlayer.RENDERER_COUNT];
|
||||
}
|
||||
|
||||
public void startSession() {
|
||||
sessionStartTimeMs = SystemClock.elapsedRealtime();
|
||||
Log.d(TAG, "start [0]");
|
||||
}
|
||||
|
||||
public void endSession() {
|
||||
Log.d(TAG, "end [" + getSessionTimeString() + "]");
|
||||
}
|
||||
|
||||
// NPExoPlayer.Listener
|
||||
|
||||
@Override
|
||||
public void onStateChanged(boolean playWhenReady, int state) {
|
||||
Log.d(TAG, "state [" + getSessionTimeString() + ", " + playWhenReady + ", "
|
||||
+ getStateString(state) + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception e) {
|
||||
Log.e(TAG, "playerFailed [" + getSessionTimeString() + "]", e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
|
||||
float pixelWidthHeightRatio) {
|
||||
Log.d(TAG, "videoSizeChanged [" + width + ", " + height + ", " + unappliedRotationDegrees
|
||||
+ ", " + pixelWidthHeightRatio + "]");
|
||||
}
|
||||
|
||||
// NPExoPlayer.InfoListener
|
||||
|
||||
@Override
|
||||
public void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate) {
|
||||
Log.d(TAG, "bandwidth [" + getSessionTimeString() + ", " + bytes + ", "
|
||||
+ getTimeString(elapsedMs) + ", " + bitrateEstimate + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDroppedFrames(int count, long elapsed) {
|
||||
Log.d(TAG, "droppedFrames [" + getSessionTimeString() + ", " + count + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadStarted(int sourceId, long length, int type, int trigger, Format format,
|
||||
long mediaStartTimeMs, long mediaEndTimeMs) {
|
||||
loadStartTimeMs[sourceId] = SystemClock.elapsedRealtime();
|
||||
if (VerboseLogUtil.isTagEnabled(TAG)) {
|
||||
Log.v(TAG, "loadStart [" + getSessionTimeString() + ", " + sourceId + ", " + type
|
||||
+ ", " + mediaStartTimeMs + ", " + mediaEndTimeMs + "]");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger, Format format,
|
||||
long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs) {
|
||||
if (VerboseLogUtil.isTagEnabled(TAG)) {
|
||||
long downloadTime = SystemClock.elapsedRealtime() - loadStartTimeMs[sourceId];
|
||||
Log.v(TAG, "loadEnd [" + getSessionTimeString() + ", " + sourceId + ", " + downloadTime
|
||||
+ "]");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVideoFormatEnabled(Format format, int trigger, long mediaTimeMs) {
|
||||
Log.d(TAG, "videoFormat [" + getSessionTimeString() + ", " + format.id + ", "
|
||||
+ Integer.toString(trigger) + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioFormatEnabled(Format format, int trigger, long mediaTimeMs) {
|
||||
Log.d(TAG, "audioFormat [" + getSessionTimeString() + ", " + format.id + ", "
|
||||
+ Integer.toString(trigger) + "]");
|
||||
}
|
||||
|
||||
// NPExoPlayer.InternalErrorListener
|
||||
|
||||
@Override
|
||||
public void onLoadError(int sourceId, IOException e) {
|
||||
printInternalError("loadError", e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRendererInitializationError(Exception e) {
|
||||
printInternalError("rendererInitError", e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrmSessionManagerError(Exception e) {
|
||||
printInternalError("drmSessionManagerError", e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDecoderInitializationError(DecoderInitializationException e) {
|
||||
printInternalError("decoderInitializationError", e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioTrackInitializationError(AudioTrack.InitializationException e) {
|
||||
printInternalError("audioTrackInitializationError", e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioTrackWriteError(AudioTrack.WriteException e) {
|
||||
printInternalError("audioTrackWriteError", e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
||||
printInternalError("audioTrackUnderrun [" + bufferSize + ", " + bufferSizeMs + ", "
|
||||
+ elapsedSinceLastFeedMs + "]", null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCryptoError(CryptoException e) {
|
||||
printInternalError("cryptoError", e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDecoderInitialized(String decoderName, long elapsedRealtimeMs,
|
||||
long initializationDurationMs) {
|
||||
Log.d(TAG, "decoderInitialized [" + getSessionTimeString() + ", " + decoderName + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAvailableRangeChanged(int sourceId, TimeRange availableRange) {
|
||||
availableRangeValuesUs = availableRange.getCurrentBoundsUs(availableRangeValuesUs);
|
||||
Log.d(TAG, "availableRange [" + availableRange.isStatic() + ", " + availableRangeValuesUs[0]
|
||||
+ ", " + availableRangeValuesUs[1] + "]");
|
||||
}
|
||||
|
||||
private void printInternalError(String type, Exception e) {
|
||||
Log.e(TAG, "internalError [" + getSessionTimeString() + ", " + type + "]", e);
|
||||
}
|
||||
|
||||
private String getStateString(int state) {
|
||||
switch (state) {
|
||||
case ExoPlayer.STATE_BUFFERING:
|
||||
return "B";
|
||||
case ExoPlayer.STATE_ENDED:
|
||||
return "E";
|
||||
case ExoPlayer.STATE_IDLE:
|
||||
return "I";
|
||||
case ExoPlayer.STATE_PREPARING:
|
||||
return "P";
|
||||
case ExoPlayer.STATE_READY:
|
||||
return "R";
|
||||
default:
|
||||
return "?";
|
||||
}
|
||||
}
|
||||
|
||||
private String getSessionTimeString() {
|
||||
return getTimeString(SystemClock.elapsedRealtime() - sessionStartTimeMs);
|
||||
}
|
||||
|
||||
private String getTimeString(long timeMs) {
|
||||
return TIME_FORMAT.format((timeMs) / 1000f);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.schabi.newpipe.player.exoplayer;
|
||||
|
||||
import org.schabi.newpipe.player.exoplayer.NPExoPlayer.RendererBuilder;
|
||||
|
||||
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
|
||||
import com.google.android.exoplayer.MediaCodecSelector;
|
||||
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
|
||||
import com.google.android.exoplayer.TrackRenderer;
|
||||
import com.google.android.exoplayer.audio.AudioCapabilities;
|
||||
import com.google.android.exoplayer.extractor.Extractor;
|
||||
import com.google.android.exoplayer.extractor.ExtractorSampleSource;
|
||||
import com.google.android.exoplayer.text.TextTrackRenderer;
|
||||
import com.google.android.exoplayer.upstream.Allocator;
|
||||
import com.google.android.exoplayer.upstream.DataSource;
|
||||
import com.google.android.exoplayer.upstream.DefaultAllocator;
|
||||
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer.upstream.DefaultUriDataSource;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaCodec;
|
||||
import android.net.Uri;
|
||||
|
||||
/**
|
||||
* A {@link RendererBuilder} for streams that can be read using an {@link Extractor}.
|
||||
*/
|
||||
public class ExtractorRendererBuilder implements RendererBuilder {
|
||||
|
||||
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
|
||||
private static final int BUFFER_SEGMENT_COUNT = 256;
|
||||
|
||||
private final Context context;
|
||||
private final String userAgent;
|
||||
private final Uri uri;
|
||||
|
||||
public ExtractorRendererBuilder(Context context, String userAgent, Uri uri) {
|
||||
this.context = context;
|
||||
this.userAgent = userAgent;
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buildRenderers(NPExoPlayer player) {
|
||||
Allocator allocator = new DefaultAllocator(BUFFER_SEGMENT_SIZE);
|
||||
|
||||
// Build the video and audio renderers.
|
||||
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(player.getMainHandler(),
|
||||
null);
|
||||
DataSource dataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||
ExtractorSampleSource sampleSource = new ExtractorSampleSource(uri, dataSource, allocator,
|
||||
BUFFER_SEGMENT_COUNT * BUFFER_SEGMENT_SIZE);
|
||||
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context,
|
||||
sampleSource, MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000,
|
||||
player.getMainHandler(), player, 50);
|
||||
MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource,
|
||||
MediaCodecSelector.DEFAULT, null, true, player.getMainHandler(), player,
|
||||
AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC);
|
||||
TrackRenderer textRenderer = new TextTrackRenderer(sampleSource, player,
|
||||
player.getMainHandler().getLooper());
|
||||
|
||||
// Invoke the callback.
|
||||
TrackRenderer[] renderers = new TrackRenderer[NPExoPlayer.RENDERER_COUNT];
|
||||
renderers[NPExoPlayer.TYPE_VIDEO] = videoRenderer;
|
||||
renderers[NPExoPlayer.TYPE_AUDIO] = audioRenderer;
|
||||
renderers[NPExoPlayer.TYPE_TEXT] = textRenderer;
|
||||
player.onRenderers(renderers, bandwidthMeter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.schabi.newpipe.player.exoplayer;
|
||||
|
||||
import org.schabi.newpipe.player.exoplayer.NPExoPlayer.RendererBuilder;
|
||||
|
||||
import com.google.android.exoplayer.DefaultLoadControl;
|
||||
import com.google.android.exoplayer.LoadControl;
|
||||
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
|
||||
import com.google.android.exoplayer.MediaCodecSelector;
|
||||
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
|
||||
import com.google.android.exoplayer.TrackRenderer;
|
||||
import com.google.android.exoplayer.audio.AudioCapabilities;
|
||||
import com.google.android.exoplayer.hls.DefaultHlsTrackSelector;
|
||||
import com.google.android.exoplayer.hls.HlsChunkSource;
|
||||
import com.google.android.exoplayer.hls.HlsMasterPlaylist;
|
||||
import com.google.android.exoplayer.hls.HlsPlaylist;
|
||||
import com.google.android.exoplayer.hls.HlsPlaylistParser;
|
||||
import com.google.android.exoplayer.hls.HlsSampleSource;
|
||||
import com.google.android.exoplayer.hls.PtsTimestampAdjusterProvider;
|
||||
import com.google.android.exoplayer.metadata.Id3Parser;
|
||||
import com.google.android.exoplayer.metadata.MetadataTrackRenderer;
|
||||
import com.google.android.exoplayer.text.TextTrackRenderer;
|
||||
import com.google.android.exoplayer.text.eia608.Eia608TrackRenderer;
|
||||
import com.google.android.exoplayer.upstream.DataSource;
|
||||
import com.google.android.exoplayer.upstream.DefaultAllocator;
|
||||
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer.upstream.DefaultUriDataSource;
|
||||
import com.google.android.exoplayer.util.ManifestFetcher;
|
||||
import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaCodec;
|
||||
import android.os.Handler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A {@link RendererBuilder} for HLS.
|
||||
*/
|
||||
public class HlsRendererBuilder implements RendererBuilder {
|
||||
|
||||
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
|
||||
private static final int MAIN_BUFFER_SEGMENTS = 256;
|
||||
private static final int TEXT_BUFFER_SEGMENTS = 2;
|
||||
|
||||
private final Context context;
|
||||
private final String userAgent;
|
||||
private final String url;
|
||||
|
||||
private AsyncRendererBuilder currentAsyncBuilder;
|
||||
|
||||
public HlsRendererBuilder(Context context, String userAgent, String url) {
|
||||
this.context = context;
|
||||
this.userAgent = userAgent;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buildRenderers(NPExoPlayer player) {
|
||||
currentAsyncBuilder = new AsyncRendererBuilder(context, userAgent, url, player);
|
||||
currentAsyncBuilder.init();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
if (currentAsyncBuilder != null) {
|
||||
currentAsyncBuilder.cancel();
|
||||
currentAsyncBuilder = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class AsyncRendererBuilder implements ManifestCallback<HlsPlaylist> {
|
||||
|
||||
private final Context context;
|
||||
private final String userAgent;
|
||||
private final String url;
|
||||
private final NPExoPlayer player;
|
||||
private final ManifestFetcher<HlsPlaylist> playlistFetcher;
|
||||
|
||||
private boolean canceled;
|
||||
|
||||
public AsyncRendererBuilder(Context context, String userAgent, String url, NPExoPlayer player) {
|
||||
this.context = context;
|
||||
this.userAgent = userAgent;
|
||||
this.url = url;
|
||||
this.player = player;
|
||||
HlsPlaylistParser parser = new HlsPlaylistParser();
|
||||
playlistFetcher = new ManifestFetcher<>(url, new DefaultUriDataSource(context, userAgent),
|
||||
parser);
|
||||
}
|
||||
|
||||
public void init() {
|
||||
playlistFetcher.singleLoad(player.getMainHandler().getLooper(), this);
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
canceled = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSingleManifestError(IOException e) {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
player.onRenderersError(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSingleManifest(HlsPlaylist manifest) {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
Handler mainHandler = player.getMainHandler();
|
||||
LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE));
|
||||
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
|
||||
PtsTimestampAdjusterProvider timestampAdjusterProvider = new PtsTimestampAdjusterProvider();
|
||||
|
||||
// Build the video/audio/metadata renderers.
|
||||
DataSource dataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||
HlsChunkSource chunkSource = new HlsChunkSource(true /* isMaster */, dataSource, url,
|
||||
manifest, DefaultHlsTrackSelector.newDefaultInstance(context), bandwidthMeter,
|
||||
timestampAdjusterProvider, HlsChunkSource.ADAPTIVE_MODE_SPLICE);
|
||||
HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, loadControl,
|
||||
MAIN_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, NPExoPlayer.TYPE_VIDEO);
|
||||
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context,
|
||||
sampleSource, MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT,
|
||||
5000, mainHandler, player, 50);
|
||||
MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource,
|
||||
MediaCodecSelector.DEFAULT, null, true, player.getMainHandler(), player,
|
||||
AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC);
|
||||
MetadataTrackRenderer<Map<String, Object>> id3Renderer = new MetadataTrackRenderer<>(
|
||||
sampleSource, new Id3Parser(), player, mainHandler.getLooper());
|
||||
|
||||
// Build the text renderer, preferring Webvtt where available.
|
||||
boolean preferWebvtt = false;
|
||||
if (manifest instanceof HlsMasterPlaylist) {
|
||||
preferWebvtt = !((HlsMasterPlaylist) manifest).subtitles.isEmpty();
|
||||
}
|
||||
TrackRenderer textRenderer;
|
||||
if (preferWebvtt) {
|
||||
DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||
HlsChunkSource textChunkSource = new HlsChunkSource(false /* isMaster */, textDataSource,
|
||||
url, manifest, DefaultHlsTrackSelector.newVttInstance(), bandwidthMeter,
|
||||
timestampAdjusterProvider, HlsChunkSource.ADAPTIVE_MODE_SPLICE);
|
||||
HlsSampleSource textSampleSource = new HlsSampleSource(textChunkSource, loadControl,
|
||||
TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, NPExoPlayer.TYPE_TEXT);
|
||||
textRenderer = new TextTrackRenderer(textSampleSource, player, mainHandler.getLooper());
|
||||
} else {
|
||||
textRenderer = new Eia608TrackRenderer(sampleSource, player, mainHandler.getLooper());
|
||||
}
|
||||
|
||||
TrackRenderer[] renderers = new TrackRenderer[NPExoPlayer.RENDERER_COUNT];
|
||||
renderers[NPExoPlayer.TYPE_VIDEO] = videoRenderer;
|
||||
renderers[NPExoPlayer.TYPE_AUDIO] = audioRenderer;
|
||||
renderers[NPExoPlayer.TYPE_METADATA] = id3Renderer;
|
||||
renderers[NPExoPlayer.TYPE_TEXT] = textRenderer;
|
||||
player.onRenderers(renderers, bandwidthMeter);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,599 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.schabi.newpipe.player.exoplayer;
|
||||
|
||||
import com.google.android.exoplayer.CodecCounters;
|
||||
import com.google.android.exoplayer.DummyTrackRenderer;
|
||||
import com.google.android.exoplayer.ExoPlaybackException;
|
||||
import com.google.android.exoplayer.ExoPlayer;
|
||||
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
|
||||
import com.google.android.exoplayer.MediaCodecTrackRenderer;
|
||||
import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException;
|
||||
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
|
||||
import com.google.android.exoplayer.MediaFormat;
|
||||
import com.google.android.exoplayer.TimeRange;
|
||||
import com.google.android.exoplayer.TrackRenderer;
|
||||
import com.google.android.exoplayer.audio.AudioTrack;
|
||||
import com.google.android.exoplayer.chunk.ChunkSampleSource;
|
||||
import com.google.android.exoplayer.chunk.Format;
|
||||
import com.google.android.exoplayer.dash.DashChunkSource;
|
||||
import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
|
||||
import com.google.android.exoplayer.hls.HlsSampleSource;
|
||||
import com.google.android.exoplayer.metadata.MetadataTrackRenderer.MetadataRenderer;
|
||||
import com.google.android.exoplayer.text.Cue;
|
||||
import com.google.android.exoplayer.text.TextRenderer;
|
||||
import com.google.android.exoplayer.upstream.BandwidthMeter;
|
||||
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer.util.DebugTextViewHelper;
|
||||
import com.google.android.exoplayer.util.PlayerControl;
|
||||
|
||||
import android.media.MediaCodec.CryptoException;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.view.Surface;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* A wrapper around {@link ExoPlayer} that provides a higher level interface. It can be prepared
|
||||
* with one of a number of {@link RendererBuilder} classes to suit different use cases (e.g. DASH,
|
||||
* SmoothStreaming and so on).
|
||||
*/
|
||||
public class NPExoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener,
|
||||
HlsSampleSource.EventListener, DefaultBandwidthMeter.EventListener,
|
||||
MediaCodecVideoTrackRenderer.EventListener, MediaCodecAudioTrackRenderer.EventListener,
|
||||
StreamingDrmSessionManager.EventListener, DashChunkSource.EventListener, TextRenderer,
|
||||
MetadataRenderer<Map<String, Object>>, DebugTextViewHelper.Provider {
|
||||
|
||||
/**
|
||||
* Builds renderers for the player.
|
||||
*/
|
||||
public interface RendererBuilder {
|
||||
/**
|
||||
* Builds renderers for playback.
|
||||
*
|
||||
* @param player The player for which renderers are being built. {@link NPExoPlayer#onRenderers}
|
||||
* should be invoked once the renderers have been built. If building fails,
|
||||
* {@link NPExoPlayer#onRenderersError} should be invoked.
|
||||
*/
|
||||
void buildRenderers(NPExoPlayer player);
|
||||
/**
|
||||
* Cancels the current build operation, if there is one. Else does nothing.
|
||||
* <p>
|
||||
* A canceled build operation must not invoke {@link NPExoPlayer#onRenderers} or
|
||||
* {@link NPExoPlayer#onRenderersError} on the player, which may have been released.
|
||||
*/
|
||||
void cancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* A listener for core events.
|
||||
*/
|
||||
public interface Listener {
|
||||
void onStateChanged(boolean playWhenReady, int playbackState);
|
||||
void onError(Exception e);
|
||||
void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
|
||||
float pixelWidthHeightRatio);
|
||||
}
|
||||
|
||||
/**
|
||||
* A listener for internal errors.
|
||||
* <p>
|
||||
* These errors are not visible to the user, and hence this listener is provided for
|
||||
* informational purposes only. Note however that an internal error may cause a fatal
|
||||
* error if the player fails to recover. If this happens, {@link Listener#onError(Exception)}
|
||||
* will be invoked.
|
||||
*/
|
||||
public interface InternalErrorListener {
|
||||
void onRendererInitializationError(Exception e);
|
||||
void onAudioTrackInitializationError(AudioTrack.InitializationException e);
|
||||
void onAudioTrackWriteError(AudioTrack.WriteException e);
|
||||
void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs);
|
||||
void onDecoderInitializationError(DecoderInitializationException e);
|
||||
void onCryptoError(CryptoException e);
|
||||
void onLoadError(int sourceId, IOException e);
|
||||
void onDrmSessionManagerError(Exception e);
|
||||
}
|
||||
|
||||
/**
|
||||
* A listener for debugging information.
|
||||
*/
|
||||
public interface InfoListener {
|
||||
void onVideoFormatEnabled(Format format, int trigger, long mediaTimeMs);
|
||||
void onAudioFormatEnabled(Format format, int trigger, long mediaTimeMs);
|
||||
void onDroppedFrames(int count, long elapsed);
|
||||
void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate);
|
||||
void onLoadStarted(int sourceId, long length, int type, int trigger, Format format,
|
||||
long mediaStartTimeMs, long mediaEndTimeMs);
|
||||
void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger, Format format,
|
||||
long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs);
|
||||
void onDecoderInitialized(String decoderName, long elapsedRealtimeMs,
|
||||
long initializationDurationMs);
|
||||
void onAvailableRangeChanged(int sourceId, TimeRange availableRange);
|
||||
}
|
||||
|
||||
/**
|
||||
* A listener for receiving notifications of timed text.
|
||||
*/
|
||||
public interface CaptionListener {
|
||||
void onCues(List<Cue> cues);
|
||||
}
|
||||
|
||||
/**
|
||||
* A listener for receiving ID3 metadata parsed from the media stream.
|
||||
*/
|
||||
public interface Id3MetadataListener {
|
||||
void onId3Metadata(Map<String, Object> metadata);
|
||||
}
|
||||
|
||||
// Constants pulled into this class for convenience.
|
||||
public static final int STATE_IDLE = ExoPlayer.STATE_IDLE;
|
||||
public static final int STATE_PREPARING = ExoPlayer.STATE_PREPARING;
|
||||
public static final int STATE_BUFFERING = ExoPlayer.STATE_BUFFERING;
|
||||
public static final int STATE_READY = ExoPlayer.STATE_READY;
|
||||
public static final int STATE_ENDED = ExoPlayer.STATE_ENDED;
|
||||
public static final int TRACK_DISABLED = ExoPlayer.TRACK_DISABLED;
|
||||
public static final int TRACK_DEFAULT = ExoPlayer.TRACK_DEFAULT;
|
||||
|
||||
public static final int RENDERER_COUNT = 4;
|
||||
public static final int TYPE_VIDEO = 0;
|
||||
public static final int TYPE_AUDIO = 1;
|
||||
public static final int TYPE_TEXT = 2;
|
||||
public static final int TYPE_METADATA = 3;
|
||||
|
||||
private static final int RENDERER_BUILDING_STATE_IDLE = 1;
|
||||
private static final int RENDERER_BUILDING_STATE_BUILDING = 2;
|
||||
private static final int RENDERER_BUILDING_STATE_BUILT = 3;
|
||||
|
||||
private final RendererBuilder rendererBuilder;
|
||||
private final ExoPlayer player;
|
||||
private final PlayerControl playerControl;
|
||||
private final Handler mainHandler;
|
||||
private final CopyOnWriteArrayList<Listener> listeners;
|
||||
|
||||
private int rendererBuildingState;
|
||||
private int lastReportedPlaybackState;
|
||||
private boolean lastReportedPlayWhenReady;
|
||||
|
||||
private Surface surface;
|
||||
private TrackRenderer videoRenderer;
|
||||
private CodecCounters codecCounters;
|
||||
private Format videoFormat;
|
||||
private int videoTrackToRestore;
|
||||
|
||||
private BandwidthMeter bandwidthMeter;
|
||||
private boolean backgrounded;
|
||||
|
||||
private CaptionListener captionListener;
|
||||
private Id3MetadataListener id3MetadataListener;
|
||||
private InternalErrorListener internalErrorListener;
|
||||
private InfoListener infoListener;
|
||||
|
||||
public NPExoPlayer(RendererBuilder rendererBuilder) {
|
||||
this.rendererBuilder = rendererBuilder;
|
||||
player = ExoPlayer.Factory.newInstance(RENDERER_COUNT, 1000, 5000);
|
||||
player.addListener(this);
|
||||
playerControl = new PlayerControl(player);
|
||||
mainHandler = new Handler();
|
||||
listeners = new CopyOnWriteArrayList<>();
|
||||
lastReportedPlaybackState = STATE_IDLE;
|
||||
rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
|
||||
// Disable text initially.
|
||||
player.setSelectedTrack(TYPE_TEXT, TRACK_DISABLED);
|
||||
}
|
||||
|
||||
public PlayerControl getPlayerControl() {
|
||||
return playerControl;
|
||||
}
|
||||
|
||||
public void addListener(Listener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
public void removeListener(Listener listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
public void setInternalErrorListener(InternalErrorListener listener) {
|
||||
internalErrorListener = listener;
|
||||
}
|
||||
|
||||
public void setInfoListener(InfoListener listener) {
|
||||
infoListener = listener;
|
||||
}
|
||||
|
||||
public void setCaptionListener(CaptionListener listener) {
|
||||
captionListener = listener;
|
||||
}
|
||||
|
||||
public void setMetadataListener(Id3MetadataListener listener) {
|
||||
id3MetadataListener = listener;
|
||||
}
|
||||
|
||||
public void setSurface(Surface surface) {
|
||||
this.surface = surface;
|
||||
pushSurface(false);
|
||||
}
|
||||
|
||||
public Surface getSurface() {
|
||||
return surface;
|
||||
}
|
||||
|
||||
public void blockingClearSurface() {
|
||||
surface = null;
|
||||
pushSurface(true);
|
||||
}
|
||||
|
||||
public int getTrackCount(int type) {
|
||||
return player.getTrackCount(type);
|
||||
}
|
||||
|
||||
public MediaFormat getTrackFormat(int type, int index) {
|
||||
return player.getTrackFormat(type, index);
|
||||
}
|
||||
|
||||
public int getSelectedTrack(int type) {
|
||||
return player.getSelectedTrack(type);
|
||||
}
|
||||
|
||||
public void setSelectedTrack(int type, int index) {
|
||||
player.setSelectedTrack(type, index);
|
||||
if (type == TYPE_TEXT && index < 0 && captionListener != null) {
|
||||
captionListener.onCues(Collections.<Cue>emptyList());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean getBackgrounded() {
|
||||
return backgrounded;
|
||||
}
|
||||
|
||||
public void setBackgrounded(boolean backgrounded) {
|
||||
if (this.backgrounded == backgrounded) {
|
||||
return;
|
||||
}
|
||||
this.backgrounded = backgrounded;
|
||||
if (backgrounded) {
|
||||
videoTrackToRestore = getSelectedTrack(TYPE_VIDEO);
|
||||
setSelectedTrack(TYPE_VIDEO, TRACK_DISABLED);
|
||||
blockingClearSurface();
|
||||
} else {
|
||||
setSelectedTrack(TYPE_VIDEO, videoTrackToRestore);
|
||||
}
|
||||
}
|
||||
|
||||
public void prepare() {
|
||||
if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILT) {
|
||||
player.stop();
|
||||
}
|
||||
rendererBuilder.cancel();
|
||||
videoFormat = null;
|
||||
videoRenderer = null;
|
||||
rendererBuildingState = RENDERER_BUILDING_STATE_BUILDING;
|
||||
maybeReportPlayerState();
|
||||
rendererBuilder.buildRenderers(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked with the results from a {@link RendererBuilder}.
|
||||
*
|
||||
* @param renderers Renderers indexed by {@link NPExoPlayer} TYPE_* constants. An individual
|
||||
* element may be null if there do not exist tracks of the corresponding type.
|
||||
* @param bandwidthMeter Provides an estimate of the currently available bandwidth. May be null.
|
||||
*/
|
||||
/* package */ void onRenderers(TrackRenderer[] renderers, BandwidthMeter bandwidthMeter) {
|
||||
for (int i = 0; i < RENDERER_COUNT; i++) {
|
||||
if (renderers[i] == null) {
|
||||
// Convert a null renderer to a dummy renderer.
|
||||
renderers[i] = new DummyTrackRenderer();
|
||||
}
|
||||
}
|
||||
// Complete preparation.
|
||||
this.videoRenderer = renderers[TYPE_VIDEO];
|
||||
this.codecCounters = videoRenderer instanceof MediaCodecTrackRenderer
|
||||
? ((MediaCodecTrackRenderer) videoRenderer).codecCounters
|
||||
: renderers[TYPE_AUDIO] instanceof MediaCodecTrackRenderer
|
||||
? ((MediaCodecTrackRenderer) renderers[TYPE_AUDIO]).codecCounters : null;
|
||||
this.bandwidthMeter = bandwidthMeter;
|
||||
pushSurface(false);
|
||||
player.prepare(renderers);
|
||||
rendererBuildingState = RENDERER_BUILDING_STATE_BUILT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked if a {@link RendererBuilder} encounters an error.
|
||||
*
|
||||
* @param e Describes the error.
|
||||
*/
|
||||
/* package */ void onRenderersError(Exception e) {
|
||||
if (internalErrorListener != null) {
|
||||
internalErrorListener.onRendererInitializationError(e);
|
||||
}
|
||||
for (Listener listener : listeners) {
|
||||
listener.onError(e);
|
||||
}
|
||||
rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
|
||||
maybeReportPlayerState();
|
||||
}
|
||||
|
||||
public void setPlayWhenReady(boolean playWhenReady) {
|
||||
player.setPlayWhenReady(playWhenReady);
|
||||
}
|
||||
|
||||
public void seekTo(long positionMs) {
|
||||
player.seekTo(positionMs);
|
||||
}
|
||||
|
||||
public void release() {
|
||||
rendererBuilder.cancel();
|
||||
rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
|
||||
surface = null;
|
||||
player.release();
|
||||
}
|
||||
|
||||
public int getPlaybackState() {
|
||||
if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILDING) {
|
||||
return STATE_PREPARING;
|
||||
}
|
||||
int playerState = player.getPlaybackState();
|
||||
if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILT && playerState == STATE_IDLE) {
|
||||
// This is an edge case where the renderers are built, but are still being passed to the
|
||||
// player's playback thread.
|
||||
return STATE_PREPARING;
|
||||
}
|
||||
return playerState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Format getFormat() {
|
||||
return videoFormat;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BandwidthMeter getBandwidthMeter() {
|
||||
return bandwidthMeter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CodecCounters getCodecCounters() {
|
||||
return codecCounters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getCurrentPosition() {
|
||||
return player.getCurrentPosition();
|
||||
}
|
||||
|
||||
public long getDuration() {
|
||||
return player.getDuration();
|
||||
}
|
||||
|
||||
public int getBufferedPercentage() {
|
||||
return player.getBufferedPercentage();
|
||||
}
|
||||
|
||||
public boolean getPlayWhenReady() {
|
||||
return player.getPlayWhenReady();
|
||||
}
|
||||
|
||||
/* package */ Looper getPlaybackLooper() {
|
||||
return player.getPlaybackLooper();
|
||||
}
|
||||
|
||||
/* package */ Handler getMainHandler() {
|
||||
return mainHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int state) {
|
||||
maybeReportPlayerState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(ExoPlaybackException exception) {
|
||||
rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
|
||||
for (Listener listener : listeners) {
|
||||
listener.onError(exception);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
|
||||
float pixelWidthHeightRatio) {
|
||||
for (Listener listener : listeners) {
|
||||
listener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDroppedFrames(int count, long elapsed) {
|
||||
if (infoListener != null) {
|
||||
infoListener.onDroppedFrames(count, elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate) {
|
||||
if (infoListener != null) {
|
||||
infoListener.onBandwidthSample(elapsedMs, bytes, bitrateEstimate);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownstreamFormatChanged(int sourceId, Format format, int trigger,
|
||||
long mediaTimeMs) {
|
||||
if (infoListener == null) {
|
||||
return;
|
||||
}
|
||||
if (sourceId == TYPE_VIDEO) {
|
||||
videoFormat = format;
|
||||
infoListener.onVideoFormatEnabled(format, trigger, mediaTimeMs);
|
||||
} else if (sourceId == TYPE_AUDIO) {
|
||||
infoListener.onAudioFormatEnabled(format, trigger, mediaTimeMs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrmKeysLoaded() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrmSessionManagerError(Exception e) {
|
||||
if (internalErrorListener != null) {
|
||||
internalErrorListener.onDrmSessionManagerError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDecoderInitializationError(DecoderInitializationException e) {
|
||||
if (internalErrorListener != null) {
|
||||
internalErrorListener.onDecoderInitializationError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioTrackInitializationError(AudioTrack.InitializationException e) {
|
||||
if (internalErrorListener != null) {
|
||||
internalErrorListener.onAudioTrackInitializationError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioTrackWriteError(AudioTrack.WriteException e) {
|
||||
if (internalErrorListener != null) {
|
||||
internalErrorListener.onAudioTrackWriteError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
||||
if (internalErrorListener != null) {
|
||||
internalErrorListener.onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCryptoError(CryptoException e) {
|
||||
if (internalErrorListener != null) {
|
||||
internalErrorListener.onCryptoError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDecoderInitialized(String decoderName, long elapsedRealtimeMs,
|
||||
long initializationDurationMs) {
|
||||
if (infoListener != null) {
|
||||
infoListener.onDecoderInitialized(decoderName, elapsedRealtimeMs, initializationDurationMs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadError(int sourceId, IOException e) {
|
||||
if (internalErrorListener != null) {
|
||||
internalErrorListener.onLoadError(sourceId, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCues(List<Cue> cues) {
|
||||
if (captionListener != null && getSelectedTrack(TYPE_TEXT) != TRACK_DISABLED) {
|
||||
captionListener.onCues(cues);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMetadata(Map<String, Object> metadata) {
|
||||
if (id3MetadataListener != null && getSelectedTrack(TYPE_METADATA) != TRACK_DISABLED) {
|
||||
id3MetadataListener.onId3Metadata(metadata);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAvailableRangeChanged(int sourceId, TimeRange availableRange) {
|
||||
if (infoListener != null) {
|
||||
infoListener.onAvailableRangeChanged(sourceId, availableRange);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayWhenReadyCommitted() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrawnToSurface(Surface surface) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadStarted(int sourceId, long length, int type, int trigger, Format format,
|
||||
long mediaStartTimeMs, long mediaEndTimeMs) {
|
||||
if (infoListener != null) {
|
||||
infoListener.onLoadStarted(sourceId, length, type, trigger, format, mediaStartTimeMs,
|
||||
mediaEndTimeMs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger, Format format,
|
||||
long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs) {
|
||||
if (infoListener != null) {
|
||||
infoListener.onLoadCompleted(sourceId, bytesLoaded, type, trigger, format, mediaStartTimeMs,
|
||||
mediaEndTimeMs, elapsedRealtimeMs, loadDurationMs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCanceled(int sourceId, long bytesLoaded) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpstreamDiscarded(int sourceId, long mediaStartTimeMs, long mediaEndTimeMs) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
private void maybeReportPlayerState() {
|
||||
boolean playWhenReady = player.getPlayWhenReady();
|
||||
int playbackState = getPlaybackState();
|
||||
if (lastReportedPlayWhenReady != playWhenReady || lastReportedPlaybackState != playbackState) {
|
||||
for (Listener listener : listeners) {
|
||||
listener.onStateChanged(playWhenReady, playbackState);
|
||||
}
|
||||
lastReportedPlayWhenReady = playWhenReady;
|
||||
lastReportedPlaybackState = playbackState;
|
||||
}
|
||||
}
|
||||
|
||||
private void pushSurface(boolean blockForSurfacePush) {
|
||||
if (videoRenderer == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (blockForSurfacePush) {
|
||||
player.blockingSendMessage(
|
||||
videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface);
|
||||
} else {
|
||||
player.sendMessage(
|
||||
videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.schabi.newpipe.player.exoplayer;
|
||||
|
||||
|
||||
import org.schabi.newpipe.player.exoplayer.NPExoPlayer.RendererBuilder;
|
||||
|
||||
import com.google.android.exoplayer.DefaultLoadControl;
|
||||
import com.google.android.exoplayer.LoadControl;
|
||||
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
|
||||
import com.google.android.exoplayer.MediaCodecSelector;
|
||||
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
|
||||
import com.google.android.exoplayer.TrackRenderer;
|
||||
import com.google.android.exoplayer.audio.AudioCapabilities;
|
||||
import com.google.android.exoplayer.chunk.ChunkSampleSource;
|
||||
import com.google.android.exoplayer.chunk.ChunkSource;
|
||||
import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator;
|
||||
import com.google.android.exoplayer.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer.drm.MediaDrmCallback;
|
||||
import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
|
||||
import com.google.android.exoplayer.drm.UnsupportedDrmException;
|
||||
import com.google.android.exoplayer.smoothstreaming.DefaultSmoothStreamingTrackSelector;
|
||||
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingChunkSource;
|
||||
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest;
|
||||
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifestParser;
|
||||
import com.google.android.exoplayer.text.TextTrackRenderer;
|
||||
import com.google.android.exoplayer.upstream.DataSource;
|
||||
import com.google.android.exoplayer.upstream.DefaultAllocator;
|
||||
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer.upstream.DefaultHttpDataSource;
|
||||
import com.google.android.exoplayer.upstream.DefaultUriDataSource;
|
||||
import com.google.android.exoplayer.util.ManifestFetcher;
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaCodec;
|
||||
import android.os.Handler;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* A {@link RendererBuilder} for SmoothStreaming.
|
||||
*/
|
||||
public class SmoothStreamingRendererBuilder implements RendererBuilder {
|
||||
|
||||
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
|
||||
private static final int VIDEO_BUFFER_SEGMENTS = 200;
|
||||
private static final int AUDIO_BUFFER_SEGMENTS = 54;
|
||||
private static final int TEXT_BUFFER_SEGMENTS = 2;
|
||||
private static final int LIVE_EDGE_LATENCY_MS = 30000;
|
||||
|
||||
private final Context context;
|
||||
private final String userAgent;
|
||||
private final String url;
|
||||
private final MediaDrmCallback drmCallback;
|
||||
|
||||
private AsyncRendererBuilder currentAsyncBuilder;
|
||||
|
||||
public SmoothStreamingRendererBuilder(Context context, String userAgent, String url,
|
||||
MediaDrmCallback drmCallback) {
|
||||
this.context = context;
|
||||
this.userAgent = userAgent;
|
||||
this.url = Util.toLowerInvariant(url).endsWith("/manifest") ? url : url + "/Manifest";
|
||||
this.drmCallback = drmCallback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buildRenderers(NPExoPlayer player) {
|
||||
currentAsyncBuilder = new AsyncRendererBuilder(context, userAgent, url, drmCallback, player);
|
||||
currentAsyncBuilder.init();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
if (currentAsyncBuilder != null) {
|
||||
currentAsyncBuilder.cancel();
|
||||
currentAsyncBuilder = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class AsyncRendererBuilder
|
||||
implements ManifestFetcher.ManifestCallback<SmoothStreamingManifest> {
|
||||
|
||||
private final Context context;
|
||||
private final String userAgent;
|
||||
private final MediaDrmCallback drmCallback;
|
||||
private final NPExoPlayer player;
|
||||
private final ManifestFetcher<SmoothStreamingManifest> manifestFetcher;
|
||||
|
||||
private boolean canceled;
|
||||
|
||||
public AsyncRendererBuilder(Context context, String userAgent, String url,
|
||||
MediaDrmCallback drmCallback, NPExoPlayer player) {
|
||||
this.context = context;
|
||||
this.userAgent = userAgent;
|
||||
this.drmCallback = drmCallback;
|
||||
this.player = player;
|
||||
SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser();
|
||||
manifestFetcher = new ManifestFetcher<>(url, new DefaultHttpDataSource(userAgent, null),
|
||||
parser);
|
||||
}
|
||||
|
||||
public void init() {
|
||||
manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this);
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
canceled = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSingleManifestError(IOException exception) {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
player.onRenderersError(exception);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSingleManifest(SmoothStreamingManifest manifest) {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
Handler mainHandler = player.getMainHandler();
|
||||
LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE));
|
||||
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, player);
|
||||
|
||||
// Check drm support if necessary.
|
||||
DrmSessionManager drmSessionManager = null;
|
||||
if (manifest.protectionElement != null) {
|
||||
if (Util.SDK_INT < 18) {
|
||||
player.onRenderersError(
|
||||
new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
drmSessionManager = new StreamingDrmSessionManager(manifest.protectionElement.uuid,
|
||||
player.getPlaybackLooper(), drmCallback, null, player.getMainHandler(), player);
|
||||
} catch (UnsupportedDrmException e) {
|
||||
player.onRenderersError(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Build the video renderer.
|
||||
DataSource videoDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||
ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifestFetcher,
|
||||
DefaultSmoothStreamingTrackSelector.newVideoInstance(context, true, false),
|
||||
videoDataSource, new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS);
|
||||
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
|
||||
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
|
||||
NPExoPlayer.TYPE_VIDEO);
|
||||
TrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context, videoSampleSource,
|
||||
MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000,
|
||||
drmSessionManager, true, mainHandler, player, 50);
|
||||
|
||||
// Build the audio renderer.
|
||||
DataSource audioDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||
ChunkSource audioChunkSource = new SmoothStreamingChunkSource(manifestFetcher,
|
||||
DefaultSmoothStreamingTrackSelector.newAudioInstance(),
|
||||
audioDataSource, null, LIVE_EDGE_LATENCY_MS);
|
||||
ChunkSampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
|
||||
AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
|
||||
NPExoPlayer.TYPE_AUDIO);
|
||||
TrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource,
|
||||
MediaCodecSelector.DEFAULT, drmSessionManager, true, mainHandler, player,
|
||||
AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC);
|
||||
|
||||
// Build the text renderer.
|
||||
DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||
ChunkSource textChunkSource = new SmoothStreamingChunkSource(manifestFetcher,
|
||||
DefaultSmoothStreamingTrackSelector.newTextInstance(),
|
||||
textDataSource, null, LIVE_EDGE_LATENCY_MS);
|
||||
ChunkSampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl,
|
||||
TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
|
||||
NPExoPlayer.TYPE_TEXT);
|
||||
TrackRenderer textRenderer = new TextTrackRenderer(textSampleSource, player,
|
||||
mainHandler.getLooper());
|
||||
|
||||
// Invoke the callback.
|
||||
TrackRenderer[] renderers = new TrackRenderer[NPExoPlayer.RENDERER_COUNT];
|
||||
renderers[NPExoPlayer.TYPE_VIDEO] = videoRenderer;
|
||||
renderers[NPExoPlayer.TYPE_AUDIO] = audioRenderer;
|
||||
renderers[NPExoPlayer.TYPE_TEXT] = textRenderer;
|
||||
player.onRenderers(renderers, bandwidthMeter);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import android.support.design.widget.Snackbar;
|
||||
import android.support.v4.app.NavUtils;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
@@ -121,6 +122,7 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
Intent intent = new Intent(context, ErrorActivity.class);
|
||||
intent.putExtra(ERROR_INFO, errorInfo);
|
||||
intent.putExtra(ERROR_LIST, elToSl(el));
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}).show();
|
||||
@@ -130,6 +132,7 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
Intent intent = new Intent(context, ErrorActivity.class);
|
||||
intent.putExtra(ERROR_INFO, errorInfo);
|
||||
intent.putExtra(ERROR_LIST, elToSl(el));
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
@@ -180,7 +183,7 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
Intent intent = new Intent(context, ErrorActivity.class);
|
||||
intent.putExtra(ERROR_INFO, errorInfo);
|
||||
intent.putExtra(ERROR_LIST, el);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
@@ -203,19 +206,19 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
ThemeHelper.setTheme(this, true);
|
||||
ThemeHelper.setTheme(this);
|
||||
setContentView(R.layout.activity_error);
|
||||
|
||||
Intent intent = getIntent();
|
||||
|
||||
try {
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
actionBar.setTitle(R.string.error_report_title);
|
||||
actionBar.setDisplayShowTitleEnabled(true);
|
||||
} catch (Throwable e) {
|
||||
Log.e(TAG, "Error turing exception handling");
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
reportButton = (Button) findViewById(R.id.errorReportButton);
|
||||
|
||||
@@ -1,386 +0,0 @@
|
||||
package org.schabi.newpipe.search_fragment;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.SearchView;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.ChannelActivity;
|
||||
import org.schabi.newpipe.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.search.SearchEngine;
|
||||
import org.schabi.newpipe.extractor.search.SearchResult;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.detail.VideoItemDetailActivity;
|
||||
import org.schabi.newpipe.detail.VideoItemDetailFragment;
|
||||
import org.schabi.newpipe.info_list.InfoListAdapter;
|
||||
import org.schabi.newpipe.util.NavStack;
|
||||
|
||||
import java.util.EnumSet;
|
||||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
import static org.schabi.newpipe.ReCaptchaActivity.RECAPTCHA_REQUEST;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 02.08.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* SearchInfoItemFragment.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 SearchInfoItemFragment extends Fragment {
|
||||
|
||||
private static final String TAG = SearchInfoItemFragment.class.toString();
|
||||
|
||||
private EnumSet<SearchEngine.Filter> filter =
|
||||
EnumSet.of(SearchEngine.Filter.CHANNEL, SearchEngine.Filter.STREAM);
|
||||
|
||||
/**
|
||||
* Listener for search queries
|
||||
*/
|
||||
public class SearchQueryListener implements SearchView.OnQueryTextListener {
|
||||
|
||||
@Override
|
||||
public boolean onQueryTextSubmit(String query) {
|
||||
Activity a = getActivity();
|
||||
try {
|
||||
search(query);
|
||||
|
||||
// hide virtual keyboard
|
||||
InputMethodManager inputManager =
|
||||
(InputMethodManager) a.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
try {
|
||||
//noinspection ConstantConditions
|
||||
inputManager.hideSoftInputFromWindow(
|
||||
a.getCurrentFocus().getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
|
||||
} catch (NullPointerException e) {
|
||||
e.printStackTrace();
|
||||
ErrorActivity.reportError(a, e, null,
|
||||
a.findViewById(android.R.id.content),
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED,
|
||||
NewPipe.getNameOfService(streamingServiceId),
|
||||
"Could not get widget with focus", R.string.general_error));
|
||||
}
|
||||
// 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
|
||||
a.getCurrentFocus().clearFocus();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onQueryTextChange(String newText) {
|
||||
if (!newText.isEmpty()) {
|
||||
searchSuggestions(newText);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private int streamingServiceId = -1;
|
||||
private String searchQuery = "";
|
||||
private boolean isLoading = false;
|
||||
|
||||
private ProgressBar loadingIndicator = null;
|
||||
private int pageNumber = 0;
|
||||
private SuggestionListAdapter suggestionListAdapter = null;
|
||||
private InfoListAdapter infoListAdapter = null;
|
||||
private LinearLayoutManager streamInfoListLayoutManager = null;
|
||||
|
||||
// savedInstanceBundle arguments
|
||||
private static final String QUERY = "query";
|
||||
private static final String STREAMING_SERVICE = "streaming_service";
|
||||
|
||||
/**
|
||||
* Mandatory empty constructor for the fragment manager to instantiate the
|
||||
* fragment (e.g. upon screen orientation changes).
|
||||
*/
|
||||
public SearchInfoItemFragment() {
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static SearchInfoItemFragment newInstance(int streamingServiceId, String searchQuery) {
|
||||
Bundle args = new Bundle();
|
||||
args.putInt(STREAMING_SERVICE, streamingServiceId);
|
||||
args.putString(QUERY, searchQuery);
|
||||
SearchInfoItemFragment fragment = new SearchInfoItemFragment();
|
||||
fragment.setArguments(args);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
searchQuery = "";
|
||||
if (savedInstanceState != null) {
|
||||
searchQuery = savedInstanceState.getString(QUERY);
|
||||
streamingServiceId = savedInstanceState.getInt(STREAMING_SERVICE);
|
||||
} else {
|
||||
try {
|
||||
Bundle args = getArguments();
|
||||
if(args != null) {
|
||||
searchQuery = args.getString(QUERY);
|
||||
streamingServiceId = args.getInt(STREAMING_SERVICE);
|
||||
} else {
|
||||
streamingServiceId = NewPipe.getIdOfService("Youtube");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
ErrorActivity.reportError(getActivity(), e, null,
|
||||
getActivity().findViewById(android.R.id.content),
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED,
|
||||
NewPipe.getNameOfService(streamingServiceId),
|
||||
"", R.string.general_error));
|
||||
}
|
||||
}
|
||||
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
SearchWorker sw = SearchWorker.getInstance();
|
||||
sw.setSearchWorkerResultListener(new SearchWorker.SearchWorkerResultListener() {
|
||||
@Override
|
||||
public void onResult(SearchResult result) {
|
||||
infoListAdapter.addInfoItemList(result.resultList);
|
||||
setDoneLoading();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingFound(int stringResource) {
|
||||
//setListShown(true);
|
||||
Toast.makeText(getActivity(), getString(stringResource),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
setDoneLoading();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(String message) {
|
||||
//setListShown(true);
|
||||
Toast.makeText(getActivity(), message,
|
||||
Toast.LENGTH_LONG).show();
|
||||
setDoneLoading();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReCaptchaChallenge() {
|
||||
Toast.makeText(getActivity(), "ReCaptcha Challenge requested",
|
||||
Toast.LENGTH_LONG).show();
|
||||
|
||||
// Starting ReCaptcha Challenge Activity
|
||||
startActivityForResult(
|
||||
new Intent(getActivity(), ReCaptchaActivity.class),
|
||||
RECAPTCHA_REQUEST);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_searchinfoitem, container, false);
|
||||
|
||||
Context context = view.getContext();
|
||||
loadingIndicator = (ProgressBar) view.findViewById(R.id.progressBar);
|
||||
RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.list);
|
||||
streamInfoListLayoutManager = new LinearLayoutManager(context);
|
||||
recyclerView.setLayoutManager(streamInfoListLayoutManager);
|
||||
|
||||
infoListAdapter = new InfoListAdapter(getActivity(),
|
||||
getActivity().findViewById(android.R.id.content));
|
||||
infoListAdapter.setFooter(inflater.inflate(R.layout.pignate_footer, recyclerView, false));
|
||||
infoListAdapter.showFooter(false);
|
||||
infoListAdapter.setOnStreamInfoItemSelectedListener(
|
||||
new InfoItemBuilder.OnInfoItemSelectedListener() {
|
||||
@Override
|
||||
public void selected(String url, int serviceId) {
|
||||
NavStack.getInstance()
|
||||
.openDetailActivity(getContext(), url, serviceId);
|
||||
}
|
||||
});
|
||||
infoListAdapter.setOnChannelInfoItemSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() {
|
||||
@Override
|
||||
public void selected(String url, int serviceId) {
|
||||
NavStack.getInstance()
|
||||
.openChannelActivity(getContext(), url, serviceId);
|
||||
}
|
||||
});
|
||||
recyclerView.setAdapter(infoListAdapter);
|
||||
recyclerView.clearOnScrollListeners();
|
||||
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
|
||||
int pastVisiblesItems, visibleItemCount, totalItemCount;
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
if (dy > 0) //check for scroll down
|
||||
{
|
||||
visibleItemCount = streamInfoListLayoutManager.getChildCount();
|
||||
totalItemCount = streamInfoListLayoutManager.getItemCount();
|
||||
pastVisiblesItems = streamInfoListLayoutManager.findFirstVisibleItemPosition();
|
||||
|
||||
if ((visibleItemCount + pastVisiblesItems) >= totalItemCount && !isLoading) {
|
||||
pageNumber++;
|
||||
search(searchQuery, pageNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
if(!searchQuery.isEmpty()) {
|
||||
search(searchQuery);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putString(QUERY, searchQuery);
|
||||
outState.putInt(STREAMING_SERVICE, streamingServiceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
inflater.inflate(R.menu.search_menu, menu);
|
||||
|
||||
MenuItem searchItem = menu.findItem(R.id.action_search);
|
||||
SearchView searchView = (SearchView) searchItem.getActionView();
|
||||
setupSearchView(searchView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch(item.getItemId()) {
|
||||
case R.id.menu_filter_all:
|
||||
changeFilter(item, EnumSet.of(SearchEngine.Filter.STREAM, SearchEngine.Filter.CHANNEL));
|
||||
return true;
|
||||
case R.id.menu_filter_video:
|
||||
changeFilter(item, EnumSet.of(SearchEngine.Filter.STREAM));
|
||||
return true;
|
||||
case R.id.menu_filter_channel:
|
||||
changeFilter(item, EnumSet.of(SearchEngine.Filter.CHANNEL));
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void changeFilter(MenuItem item, EnumSet<SearchEngine.Filter> filter) {
|
||||
this.filter = filter;
|
||||
item.setChecked(true);
|
||||
if(searchQuery != null && !searchQuery.isEmpty()) {
|
||||
Log.d(TAG, "Fuck+ " + searchQuery);
|
||||
search(searchQuery);
|
||||
}
|
||||
}
|
||||
|
||||
private void setupSearchView(SearchView searchView) {
|
||||
suggestionListAdapter = new SuggestionListAdapter(getActivity());
|
||||
searchView.setSuggestionsAdapter(suggestionListAdapter);
|
||||
searchView.setOnSuggestionListener(new SearchSuggestionListener(searchView, suggestionListAdapter));
|
||||
searchView.setOnQueryTextListener(new SearchQueryListener());
|
||||
if (searchQuery != null && !searchQuery.isEmpty()) {
|
||||
searchView.setQuery(searchQuery, false);
|
||||
searchView.setIconifiedByDefault(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void search(String query) {
|
||||
infoListAdapter.clearSteamItemList();
|
||||
infoListAdapter.showFooter(false);
|
||||
pageNumber = 0;
|
||||
searchQuery = query;
|
||||
search(query, pageNumber);
|
||||
hideBackground();
|
||||
loadingIndicator.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
private void search(String query, int page) {
|
||||
isLoading = true;
|
||||
SearchWorker sw = SearchWorker.getInstance();
|
||||
sw.search(streamingServiceId,
|
||||
query,
|
||||
page,
|
||||
getActivity(),
|
||||
filter);
|
||||
}
|
||||
|
||||
private void setDoneLoading() {
|
||||
this.isLoading = false;
|
||||
loadingIndicator.setVisibility(View.GONE);
|
||||
infoListAdapter.showFooter(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the "dummy" background when no results are shown
|
||||
*/
|
||||
private void hideBackground() {
|
||||
View view = getView();
|
||||
if(view == null) return;
|
||||
view.findViewById(R.id.mainBG).setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private void searchSuggestions(String query) {
|
||||
SuggestionSearchRunnable suggestionSearchRunnable =
|
||||
new SuggestionSearchRunnable(streamingServiceId, query, getActivity(), suggestionListAdapter);
|
||||
Thread suggestionThread = new Thread(suggestionSearchRunnable);
|
||||
suggestionThread.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
switch (requestCode) {
|
||||
case RECAPTCHA_REQUEST:
|
||||
if (resultCode == RESULT_OK) {
|
||||
if (searchQuery.length() != 0) {
|
||||
search(searchQuery);
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "ReCaptcha failed");
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
Log.e(TAG, "Request code from activity not supported [" + requestCode + "]");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package org.schabi.newpipe.search_fragment;
|
||||
|
||||
import android.support.v7.widget.SearchView;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 02.08.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* SearchSuggestionListener.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 SearchSuggestionListener implements SearchView.OnSuggestionListener{
|
||||
|
||||
private final SearchView searchView;
|
||||
private final SuggestionListAdapter adapter;
|
||||
|
||||
public SearchSuggestionListener(SearchView searchView, SuggestionListAdapter adapter) {
|
||||
this.searchView = searchView;
|
||||
this.adapter = adapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSuggestionSelect(int position) {
|
||||
String suggestion = adapter.getSuggestion(position);
|
||||
searchView.setQuery(suggestion,true);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSuggestionClick(int position) {
|
||||
String suggestion = adapter.getSuggestion(position);
|
||||
searchView.setQuery(suggestion,true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
package org.schabi.newpipe.search_fragment;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Handler;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
import org.schabi.newpipe.extractor.search.SearchEngine;
|
||||
import org.schabi.newpipe.extractor.search.SearchResult;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.EnumSet;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 02.08.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* SearchWorker.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 SearchWorker {
|
||||
private static final String TAG = SearchWorker.class.toString();
|
||||
|
||||
public interface SearchWorkerResultListener {
|
||||
void onResult(SearchResult result);
|
||||
void onNothingFound(final int stringResource);
|
||||
void onError(String message);
|
||||
void onReCaptchaChallenge();
|
||||
}
|
||||
|
||||
private class ResultRunnable implements Runnable {
|
||||
private final SearchResult result;
|
||||
private int requestId = 0;
|
||||
public ResultRunnable(SearchResult result, int requestId) {
|
||||
this.result = result;
|
||||
this.requestId = requestId;
|
||||
}
|
||||
@Override
|
||||
public void run() {
|
||||
if(this.requestId == SearchWorker.this.requestId) {
|
||||
searchWorkerResultListener.onResult(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SearchRunnable implements Runnable {
|
||||
public static final String YOUTUBE = "Youtube";
|
||||
private final String query;
|
||||
private final int page;
|
||||
private final EnumSet<SearchEngine.Filter> filter;
|
||||
final Handler h = new Handler();
|
||||
private volatile boolean runs = true;
|
||||
private Activity a = null;
|
||||
private int serviceId = -1;
|
||||
public SearchRunnable(int serviceId,
|
||||
String query,
|
||||
int page,
|
||||
EnumSet<SearchEngine.Filter> filter,
|
||||
Activity activity,
|
||||
int requestId) {
|
||||
this.serviceId = serviceId;
|
||||
this.query = query;
|
||||
this.page = page;
|
||||
this.filter = filter;
|
||||
this.a = activity;
|
||||
}
|
||||
void terminate() {
|
||||
runs = false;
|
||||
}
|
||||
@Override
|
||||
public void run() {
|
||||
final String serviceName = NewPipe.getNameOfService(serviceId);
|
||||
SearchResult result = null;
|
||||
SearchEngine engine = null;
|
||||
|
||||
try {
|
||||
engine = NewPipe.getService(serviceId)
|
||||
.getSearchEngineInstance();
|
||||
} catch(ExtractionException e) {
|
||||
ErrorActivity.reportError(h, a, e, null, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED,
|
||||
Integer.toString(serviceId), query, R.string.general_error));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(a);
|
||||
String searchLanguageKey = a.getString(R.string.search_language_key);
|
||||
String searchLanguage = sp.getString(searchLanguageKey,
|
||||
a.getString(R.string.default_language_value));
|
||||
result = SearchResult
|
||||
.getSearchResult(engine, query, page, searchLanguage, filter);
|
||||
if(runs) {
|
||||
h.post(new ResultRunnable(result, requestId));
|
||||
}
|
||||
|
||||
// look for errors during extraction
|
||||
// soft errors:
|
||||
View rootView = a.findViewById(android.R.id.content);
|
||||
if(result != null &&
|
||||
!result.errors.isEmpty()) {
|
||||
Log.e(TAG, "OCCURRED ERRORS DURING SEARCH EXTRACTION:");
|
||||
for(Throwable e : result.errors) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "------");
|
||||
}
|
||||
|
||||
if(result.resultList.isEmpty()&& !result.errors.isEmpty()) {
|
||||
// if it compleatly failes dont show snackbar, instead show error directlry
|
||||
ErrorActivity.reportError(h, a, result.errors, null, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED,
|
||||
serviceName, query, R.string.parsing_error));
|
||||
} else {
|
||||
// if it partly show snackbar
|
||||
ErrorActivity.reportError(h, a, result.errors, null, rootView,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED,
|
||||
serviceName, query, R.string.light_parsing_error));
|
||||
}
|
||||
}
|
||||
// hard errors:
|
||||
} catch (ReCaptchaException e) {
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
searchWorkerResultListener.onReCaptchaChallenge();
|
||||
}
|
||||
});
|
||||
} catch(IOException e) {
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
searchWorkerResultListener.onNothingFound(R.string.network_error);
|
||||
}
|
||||
});
|
||||
e.printStackTrace();
|
||||
} catch(final SearchEngine.NothingFoundException e) {
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
searchWorkerResultListener.onError(e.getMessage());
|
||||
}
|
||||
});
|
||||
} catch(ExtractionException e) {
|
||||
ErrorActivity.reportError(h, a, e, null, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED,
|
||||
serviceName, query, R.string.parsing_error));
|
||||
//postNewErrorToast(h, R.string.parsing_error);
|
||||
e.printStackTrace();
|
||||
|
||||
} catch(Exception e) {
|
||||
ErrorActivity.reportError(h, a, e, null, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED,
|
||||
/* todo: this shoudl not be assigned static */ YOUTUBE, query, R.string.general_error));
|
||||
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static SearchWorker searchWorker = null;
|
||||
private SearchWorkerResultListener searchWorkerResultListener = null;
|
||||
private SearchRunnable runnable = null;
|
||||
private int requestId = 0; //prevents running requests that have already ben expired
|
||||
|
||||
public static SearchWorker getInstance() {
|
||||
return searchWorker == null ? (searchWorker = new SearchWorker()) : searchWorker;
|
||||
}
|
||||
|
||||
public void setSearchWorkerResultListener(SearchWorkerResultListener listener) {
|
||||
searchWorkerResultListener = listener;
|
||||
}
|
||||
|
||||
private SearchWorker() {
|
||||
|
||||
}
|
||||
|
||||
public void search(int serviceId,
|
||||
String query,
|
||||
int page,
|
||||
Activity a,
|
||||
EnumSet<SearchEngine.Filter> filter) {
|
||||
if(runnable != null) {
|
||||
terminate();
|
||||
}
|
||||
runnable = new SearchRunnable(serviceId, query, page, filter, a, requestId);
|
||||
Thread thread = new Thread(runnable);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
public void terminate() {
|
||||
requestId++;
|
||||
runnable.terminate();
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
package org.schabi.newpipe.search_fragment;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Handler;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.SuggestionExtractor;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 02.08.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* SuggestionSearchRunnable.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 SuggestionSearchRunnable implements Runnable{
|
||||
|
||||
/**
|
||||
* Runnable to update a {@link SuggestionListAdapter}
|
||||
*/
|
||||
private class SuggestionResultRunnable implements Runnable{
|
||||
|
||||
private final List<String> suggestions;
|
||||
|
||||
private SuggestionResultRunnable(List<String> suggestions) {
|
||||
this.suggestions = suggestions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
adapter.updateAdapter(suggestions);
|
||||
}
|
||||
}
|
||||
|
||||
private final int serviceId;
|
||||
private final String query;
|
||||
private final Handler h = new Handler();
|
||||
private final Activity a;
|
||||
private final SuggestionListAdapter adapter;
|
||||
public SuggestionSearchRunnable(int serviceId, String query,
|
||||
Activity activity, SuggestionListAdapter adapter) {
|
||||
this.serviceId = serviceId;
|
||||
this.query = query;
|
||||
this.a = activity;
|
||||
this.adapter = adapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
SuggestionExtractor se =
|
||||
NewPipe.getService(serviceId).getSuggestionExtractorInstance();
|
||||
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(a);
|
||||
String searchLanguageKey = a.getString(R.string.search_language_key);
|
||||
String searchLanguage = sp.getString(searchLanguageKey,
|
||||
a.getString(R.string.default_language_value));
|
||||
List<String> suggestions = se.suggestionList(query, searchLanguage);
|
||||
h.post(new SuggestionResultRunnable(suggestions));
|
||||
} catch (ExtractionException e) {
|
||||
ErrorActivity.reportError(h, a, e, null, a.findViewById(android.R.id.content),
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED,
|
||||
NewPipe.getNameOfService(serviceId), query, R.string.parsing_error));
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
postNewErrorToast(h, R.string.network_error);
|
||||
e.printStackTrace();
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportError(h, a, e, null, a.findViewById(android.R.id.content),
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED,
|
||||
NewPipe.getNameOfService(serviceId), query, R.string.general_error));
|
||||
}
|
||||
}
|
||||
|
||||
private void postNewErrorToast(Handler h, final int stringResource) {
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(a, a.getString(stringResource),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,11 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceActivity;
|
||||
import android.support.annotation.LayoutRes;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AppCompatDelegate;
|
||||
import android.view.MenuInflater;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
@@ -38,9 +31,7 @@ import org.schabi.newpipe.util.ThemeHelper;
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class SettingsActivity extends PreferenceActivity {
|
||||
SettingsFragment f = new SettingsFragment();
|
||||
private AppCompatDelegate mDelegate = null;
|
||||
public class SettingsActivity extends AppCompatActivity {
|
||||
|
||||
public static void initSettings(Context context) {
|
||||
NewPipeSettings.initSettings(context);
|
||||
@@ -48,104 +39,25 @@ public class SettingsActivity extends PreferenceActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceBundle) {
|
||||
ThemeHelper.setTheme(this, true);
|
||||
getDelegate().installViewFactory();
|
||||
getDelegate().onCreate(savedInstanceBundle);
|
||||
ThemeHelper.setTheme(this);
|
||||
super.onCreate(savedInstanceBundle);
|
||||
setContentView(R.layout.settings_layout);
|
||||
|
||||
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
actionBar.setTitle(R.string.settings_title);
|
||||
actionBar.setDisplayShowTitleEnabled(true);
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
actionBar.setTitle(R.string.settings_title);
|
||||
actionBar.setDisplayShowTitleEnabled(true);
|
||||
}
|
||||
|
||||
getFragmentManager().beginTransaction()
|
||||
.replace(android.R.id.content, f)
|
||||
.replace(R.id.fragment_holder, new SettingsFragment())
|
||||
.commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
f.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
getDelegate().onPostCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
private ActionBar getSupportActionBar() {
|
||||
return getDelegate().getSupportActionBar();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public MenuInflater getMenuInflater() {
|
||||
return getDelegate().getMenuInflater();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContentView(@LayoutRes int layoutResID) {
|
||||
getDelegate().setContentView(layoutResID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContentView(View view) {
|
||||
getDelegate().setContentView(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContentView(View view, ViewGroup.LayoutParams params) {
|
||||
getDelegate().setContentView(view, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addContentView(View view, ViewGroup.LayoutParams params) {
|
||||
getDelegate().addContentView(view, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostResume() {
|
||||
super.onPostResume();
|
||||
getDelegate().onPostResume();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTitleChanged(CharSequence title, int color) {
|
||||
super.onTitleChanged(title, color);
|
||||
getDelegate().setTitle(title);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
getDelegate().onConfigurationChanged(newConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
getDelegate().onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
getDelegate().onDestroy();
|
||||
}
|
||||
|
||||
public void invalidateOptionsMenu() {
|
||||
getDelegate().invalidateOptionsMenu();
|
||||
}
|
||||
|
||||
private AppCompatDelegate getDelegate() {
|
||||
if (mDelegate == null) {
|
||||
mDelegate = AppCompatDelegate.create(this, null);
|
||||
}
|
||||
return mDelegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
int id = item.getItemId();
|
||||
|
||||
@@ -1,192 +1,84 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ClipData;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.preference.ListPreference;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceFragment;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.preference.PreferenceScreen;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.nononsenseapps.filepicker.FilePickerActivity;
|
||||
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
|
||||
import info.guardianproject.netcipher.proxy.OrbotHelper;
|
||||
|
||||
/**
|
||||
* Created by david on 15/06/16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* SettingsFragment.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 SettingsFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private static final int REQUEST_INSTALL_ORBOT = 0x1234;
|
||||
private static final int REQUEST_DOWNLOAD_PATH = 0x1235;
|
||||
private static final int REQUEST_DOWNLOAD_AUDIO_PATH = 0x1236;
|
||||
|
||||
public class SettingsFragment extends PreferenceFragment
|
||||
implements SharedPreferences.OnSharedPreferenceChangeListener
|
||||
{
|
||||
public static final int REQUEST_INSTALL_ORBOT = 0x1234;
|
||||
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;
|
||||
String THEME;
|
||||
private ListPreference defaultResolutionPreference;
|
||||
private ListPreference defaultAudioFormatPreference;
|
||||
private ListPreference searchLanguagePreference;
|
||||
private Preference downloadPathPreference;
|
||||
private Preference downloadPathAudioPreference;
|
||||
private Preference themePreference;
|
||||
private String DOWNLOAD_PATH_PREFERENCE;
|
||||
private String DOWNLOAD_PATH_AUDIO_PREFERENCE;
|
||||
private String USE_TOR_KEY;
|
||||
private String THEME;
|
||||
|
||||
private String currentTheme;
|
||||
private SharedPreferences defaultPreferences;
|
||||
|
||||
private Activity activity;
|
||||
|
||||
@Override
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
activity = getActivity();
|
||||
addPreferencesFromResource(R.xml.settings);
|
||||
|
||||
final Activity activity = getActivity();
|
||||
|
||||
defaultPreferences = PreferenceManager.getDefaultSharedPreferences(activity);
|
||||
initKeys();
|
||||
updatePreferencesSummary();
|
||||
|
||||
// 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);
|
||||
currentTheme = defaultPreferences.getString(THEME, getString(R.string.default_theme_value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
defaultPreferences.registerOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
defaultPreferences.unregisterOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
private void initKeys() {
|
||||
DOWNLOAD_PATH_PREFERENCE = getString(R.string.download_path_key);
|
||||
DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key);
|
||||
THEME = getString(R.string.theme_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 = findPreference(DOWNLOAD_PATH_PREFERENCE);
|
||||
downloadPathAudioPreference = findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE);
|
||||
themePreference = findPreference(THEME);
|
||||
|
||||
final String currentTheme = defaultPreferences.getString(THEME, "Light");
|
||||
|
||||
prefListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
|
||||
String key) {
|
||||
Activity a = getActivity();
|
||||
if(a == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (key == USE_TOR_KEY)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
else if (key == DOWNLOAD_PATH_PREFERENCE)
|
||||
{
|
||||
String downloadPath = sharedPreferences
|
||||
.getString(DOWNLOAD_PATH_PREFERENCE,
|
||||
getString(R.string.download_path_summary));
|
||||
downloadPathPreference
|
||||
.setSummary(downloadPath);
|
||||
}
|
||||
else if (key == DOWNLOAD_PATH_AUDIO_PREFERENCE)
|
||||
{
|
||||
String downloadPath = sharedPreferences
|
||||
.getString(DOWNLOAD_PATH_AUDIO_PREFERENCE,
|
||||
getString(R.string.download_path_audio_summary));
|
||||
downloadPathAudioPreference
|
||||
.setSummary(downloadPath);
|
||||
}
|
||||
else if (key == THEME)
|
||||
{
|
||||
String selectedTheme = sharedPreferences.getString(THEME, "Light");
|
||||
themePreference.setSummary(selectedTheme);
|
||||
|
||||
if(!selectedTheme.equals(currentTheme)) { // If it's not the current theme
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.restart_title)
|
||||
.setMessage(R.string.msg_restart)
|
||||
.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
Intent intentToMain = new Intent(activity, MainActivity.class);
|
||||
intentToMain.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
activity.startActivity(intentToMain);
|
||||
|
||||
activity.finish();
|
||||
Runtime.getRuntime().exit(0);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.later, null)
|
||||
.create().show();
|
||||
}
|
||||
}
|
||||
updateSummary();
|
||||
}
|
||||
};
|
||||
defaultPreferences.registerOnSharedPreferenceChangeListener(prefListener);
|
||||
|
||||
updateSummary();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) {
|
||||
|
||||
if(preference.getKey().equals(downloadPathPreference.getKey()) ||
|
||||
preference.getKey().equals(downloadPathAudioPreference.getKey()))
|
||||
{
|
||||
Activity activity = getActivity();
|
||||
Intent i = new Intent(activity, FilePickerActivity.class);
|
||||
|
||||
i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false);
|
||||
i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true);
|
||||
i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR);
|
||||
if(preference.getKey().equals(downloadPathPreference.getKey()))
|
||||
{
|
||||
activity.startActivityForResult(i, R.string.download_path_key);
|
||||
}
|
||||
else if (preference.getKey().equals(downloadPathAudioPreference.getKey()))
|
||||
{
|
||||
activity.startActivityForResult(i, R.string.download_path_audio_key);
|
||||
Log.d("TAG", "onPreferenceTreeClick() called with: preferenceScreen = [" + preferenceScreen + "], preference = [" + preference + "]");
|
||||
if (preference.getKey().equals(DOWNLOAD_PATH_PREFERENCE) || preference.getKey().equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
|
||||
Intent i = new Intent(activity, FilePickerActivity.class)
|
||||
.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)
|
||||
.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true)
|
||||
.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR);
|
||||
if (preference.getKey().equals(DOWNLOAD_PATH_PREFERENCE)) {
|
||||
startActivityForResult(i, REQUEST_DOWNLOAD_PATH);
|
||||
} else if (preference.getKey().equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
|
||||
startActivityForResult(i, REQUEST_DOWNLOAD_AUDIO_PATH);
|
||||
}
|
||||
}
|
||||
return super.onPreferenceTreeClick(preferenceScreen, preference);
|
||||
@@ -195,84 +87,56 @@ public class SettingsFragment extends PreferenceFragment
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
Activity a = getActivity();
|
||||
Log.d("TAG", "onActivityResult() called with: requestCode = [" + requestCode + "], resultCode = [" + resultCode + "], data = [" + data + "]");
|
||||
|
||||
if ((requestCode == R.string.download_path_audio_key
|
||||
|| requestCode == R.string.download_path_key)
|
||||
&& resultCode == Activity.RESULT_OK) {
|
||||
|
||||
Uri uri = null;
|
||||
if (data.getBooleanExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)) {
|
||||
// For JellyBean and above
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
ClipData clip = data.getClipData();
|
||||
|
||||
if (clip != null) {
|
||||
for (int i = 0; i < clip.getItemCount(); i++) {
|
||||
uri = clip.getItemAt(i).getUri();
|
||||
}
|
||||
}
|
||||
// For Ice Cream Sandwich
|
||||
} else {
|
||||
ArrayList<String> paths = data.getStringArrayListExtra
|
||||
(FilePickerActivity.EXTRA_PATHS);
|
||||
|
||||
if (paths != null) {
|
||||
for (String path: paths) {
|
||||
uri = Uri.parse(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
uri = data.getData();
|
||||
}
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(a);
|
||||
|
||||
//requestCode is equal to R.string.download_path_key or
|
||||
//R.string.download_path_audio_key
|
||||
String key = getString(requestCode);
|
||||
if ((requestCode == REQUEST_DOWNLOAD_PATH || requestCode == REQUEST_DOWNLOAD_AUDIO_PATH) && resultCode == Activity.RESULT_OK) {
|
||||
String key = getString(requestCode == REQUEST_DOWNLOAD_PATH ? R.string.download_path_key : R.string.download_path_audio_key);
|
||||
String path = data.getData().toString().substring(7);
|
||||
prefs.edit()
|
||||
.putString(key, path)
|
||||
.apply();
|
||||
|
||||
}
|
||||
else if(requestCode == REQUEST_INSTALL_ORBOT)
|
||||
{
|
||||
defaultPreferences.edit().putString(key, path).apply();
|
||||
updatePreferencesSummary();
|
||||
} else if (requestCode == REQUEST_INSTALL_ORBOT) {
|
||||
// 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(a));
|
||||
App.configureTor(OrbotHelper.requestStartTor(activity));
|
||||
}
|
||||
|
||||
updateSummary();
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
// 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)));
|
||||
themePreference.setSummary(
|
||||
defaultPreferences.getString(THEME,
|
||||
getString(R.string.light_theme_title)));
|
||||
/**
|
||||
* Update ONLY the summary of some preferences that don't fire in the onSharedPreferenceChanged or CAN'T be update via xml (%s)
|
||||
*
|
||||
* For example, the download_path use the startActivityForResult, firing the onStop of this fragment,
|
||||
* unregistering the listener (unregisterOnSharedPreferenceChangeListener)
|
||||
*/
|
||||
private void updatePreferencesSummary() {
|
||||
findPreference(DOWNLOAD_PATH_PREFERENCE).setSummary(defaultPreferences.getString(DOWNLOAD_PATH_PREFERENCE, getString(R.string.download_path_summary)));
|
||||
findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE).setSummary(defaultPreferences.getString(DOWNLOAD_PATH_AUDIO_PREFERENCE, getString(R.string.download_path_audio_summary)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
Log.d("TAG", "onSharedPreferenceChanged() called with: sharedPreferences = [" + sharedPreferences + "], key = [" + key + "]");
|
||||
String summary = null;
|
||||
|
||||
if (key.equals(USE_TOR_KEY)) {
|
||||
if (defaultPreferences.getBoolean(USE_TOR_KEY, false)) {
|
||||
if (OrbotHelper.isOrbotInstalled(activity)) {
|
||||
App.configureTor(true);
|
||||
OrbotHelper.requestStartTor(activity);
|
||||
} else {
|
||||
Intent intent = OrbotHelper.getOrbotInstallIntent(activity);
|
||||
startActivityForResult(intent, REQUEST_INSTALL_ORBOT);
|
||||
}
|
||||
} else App.configureTor(false);
|
||||
return;
|
||||
} else if (key.equals(THEME)) {
|
||||
summary = sharedPreferences.getString(THEME, getString(R.string.default_theme_value));
|
||||
if (!summary.equals(currentTheme)) { // If it's not the current theme
|
||||
Intent intentToMain = new Intent(activity, MainActivity.class);
|
||||
intentToMain.putExtra(Constants.KEY_THEME_CHANGE, true);
|
||||
startActivity(intentToMain);
|
||||
}
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(summary)) findPreference(key).setSummary(summary);
|
||||
}
|
||||
}
|
||||
|
||||
126
app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java
Normal file
@@ -0,0 +1,126 @@
|
||||
package org.schabi.newpipe.util;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import org.schabi.newpipe.player.BasePlayer;
|
||||
|
||||
public class AnimationUtils {
|
||||
private static final String TAG = "AnimationUtils";
|
||||
private static final boolean DEBUG = BasePlayer.DEBUG;
|
||||
|
||||
public enum Type {
|
||||
ALPHA, SCALE_AND_ALPHA
|
||||
}
|
||||
|
||||
public static void animateView(View view, boolean enterOrExit, long duration) {
|
||||
animateView(view, Type.ALPHA, enterOrExit, duration, 0, null);
|
||||
}
|
||||
|
||||
public static void animateView(View view, boolean enterOrExit, long duration, long delay) {
|
||||
animateView(view, Type.ALPHA, enterOrExit, duration, delay, null);
|
||||
}
|
||||
|
||||
public static void animateView(View view, boolean enterOrExit, long duration, long delay, Runnable execOnEnd) {
|
||||
animateView(view, Type.ALPHA, enterOrExit, duration, delay, execOnEnd);
|
||||
}
|
||||
|
||||
public static void animateView(View view, Type animationType, boolean enterOrExit, long duration) {
|
||||
animateView(view, animationType, enterOrExit, duration, 0, null);
|
||||
}
|
||||
|
||||
public static void animateView(View view, Type animationType, boolean enterOrExit, long duration, long delay) {
|
||||
animateView(view, animationType, enterOrExit, duration, delay, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate the view
|
||||
*
|
||||
* @param view view that will be animated
|
||||
* @param animationType {@link Type} of the animation
|
||||
* @param enterOrExit true to enter, false to exit
|
||||
* @param duration how long the animation will take, in milliseconds
|
||||
* @param delay how long the animation will wait to start, in milliseconds
|
||||
* @param execOnEnd runnable that will be executed when the animation ends
|
||||
*/
|
||||
public static void animateView(final View view, Type animationType, boolean enterOrExit, long duration, long delay, Runnable execOnEnd) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "animateView() called with: view = [" + view + "], animationType = [" + animationType + "], enterOrExit = [" + enterOrExit + "], duration = [" + duration + "], delay = [" + delay + "], execOnEnd = [" + execOnEnd + "]");
|
||||
}
|
||||
|
||||
if (view.getVisibility() == View.VISIBLE && enterOrExit) {
|
||||
if (DEBUG) Log.d(TAG, "animateView() view was already visible > view = [" + view + "]");
|
||||
view.animate().setListener(null).cancel();
|
||||
view.setVisibility(View.VISIBLE);
|
||||
view.setAlpha(1f);
|
||||
if (execOnEnd != null) execOnEnd.run();
|
||||
return;
|
||||
} else if ((view.getVisibility() == View.GONE || view.getVisibility() == View.INVISIBLE) && !enterOrExit) {
|
||||
if (DEBUG) Log.d(TAG, "animateView() view was already gone > view = [" + view + "]");
|
||||
view.animate().setListener(null).cancel();
|
||||
view.setVisibility(View.GONE);
|
||||
view.setAlpha(0f);
|
||||
if (execOnEnd != null) execOnEnd.run();
|
||||
return;
|
||||
}
|
||||
|
||||
view.animate().setListener(null).cancel();
|
||||
view.setVisibility(View.VISIBLE);
|
||||
|
||||
switch (animationType) {
|
||||
case ALPHA:
|
||||
animateAlpha(view, enterOrExit, duration, delay, execOnEnd);
|
||||
break;
|
||||
case SCALE_AND_ALPHA:
|
||||
animateScaleAndAlpha(view, enterOrExit, duration, delay, execOnEnd);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void animateScaleAndAlpha(final View view, boolean enterOrExit, long duration, long delay, final Runnable execOnEnd) {
|
||||
if (enterOrExit) {
|
||||
view.setAlpha(0f);
|
||||
view.setScaleX(.8f);
|
||||
view.setScaleY(.8f);
|
||||
view.animate().alpha(1f).scaleX(1f).scaleY(1f).setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
if (execOnEnd != null) execOnEnd.run();
|
||||
}
|
||||
}).start();
|
||||
} else {
|
||||
view.setAlpha(1f);
|
||||
view.setScaleX(1f);
|
||||
view.setScaleY(1f);
|
||||
view.animate().alpha(0f).scaleX(.8f).scaleY(.8f).setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
view.setVisibility(View.GONE);
|
||||
if (execOnEnd != null) execOnEnd.run();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static void animateAlpha(final View view, boolean enterOrExit, long duration, long delay, final Runnable execOnEnd) {
|
||||
if (enterOrExit) {
|
||||
view.animate().alpha(1f).setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
if (execOnEnd != null) execOnEnd.run();
|
||||
}
|
||||
}).start();
|
||||
} else {
|
||||
view.animate().alpha(0f).setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
view.setVisibility(View.GONE);
|
||||
if (execOnEnd != null) execOnEnd.run();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
}
|
||||
12
app/src/main/java/org/schabi/newpipe/util/Constants.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package org.schabi.newpipe.util;
|
||||
|
||||
public class Constants {
|
||||
public static final String KEY_SERVICE_ID = "key_service_id";
|
||||
public static final String KEY_URL = "key_url";
|
||||
public static final String KEY_TITLE = "key_title";
|
||||
public static final String KEY_LINK_TYPE = "key_link_type";
|
||||
public static final String KEY_OPEN_SEARCH = "key_open_search";
|
||||
public static final String KEY_QUERY = "key_query";
|
||||
|
||||
public static final String KEY_THEME_CHANGE = "key_theme_change";
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
package org.schabi.newpipe;
|
||||
package org.schabi.newpipe.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.NumberFormat;
|
||||
import java.text.ParseException;
|
||||
@@ -1,151 +0,0 @@
|
||||
package org.schabi.newpipe.util;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.NavUtils;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.util.Log;
|
||||
|
||||
import org.schabi.newpipe.ChannelActivity;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.detail.VideoItemDetailActivity;
|
||||
import org.schabi.newpipe.detail.VideoItemDetailFragment;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Stack;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 16.02.17.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* NavStack.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 helps to navigate within the app
|
||||
* IMPORTAND: the top of the stack is the current activity !!!
|
||||
*/
|
||||
public class NavStack {
|
||||
private static final String TAG = NavStack.class.toString();
|
||||
public static final String SERVICE_ID = "service_id";
|
||||
public static final String URL = "url";
|
||||
|
||||
private static final String NAV_STACK="nav_stack";
|
||||
|
||||
private enum ActivityId {
|
||||
CHANNEL,
|
||||
DETAIL
|
||||
}
|
||||
|
||||
private class NavEntry {
|
||||
public NavEntry(String url, int serviceId) {
|
||||
this.url = url;
|
||||
this.serviceId = serviceId;
|
||||
}
|
||||
public String url;
|
||||
public int serviceId;
|
||||
}
|
||||
|
||||
private static NavStack instance = new NavStack();
|
||||
private Stack<NavEntry> stack = new Stack<NavEntry>();
|
||||
|
||||
private NavStack() {
|
||||
}
|
||||
|
||||
public static NavStack getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
private void addEntry(String url, Class ac, int serviceId) {
|
||||
stack.push(new NavEntry(url, serviceId));
|
||||
}
|
||||
|
||||
public void navBack(Activity activity) throws Exception {
|
||||
if(stack.size() == 0) { // if stack is already empty here, activity was probably called
|
||||
// from another app
|
||||
activity.finish();
|
||||
return;
|
||||
}
|
||||
stack.pop(); // remove curent activty, since we dont want to return to itself
|
||||
if (stack.size() == 0) {
|
||||
openMainActivity(activity); // if no more page is on the stack this means we are home
|
||||
return;
|
||||
}
|
||||
NavEntry entry = stack.pop(); // this element will reapear, since by calling the old page
|
||||
// this element will be pushed on top again
|
||||
try {
|
||||
StreamingService service = NewPipe.getService(entry.serviceId);
|
||||
switch (service.getLinkTypeByUrl(entry.url)) {
|
||||
case STREAM:
|
||||
openDetailActivity(activity, entry.url, entry.serviceId);
|
||||
break;
|
||||
case CHANNEL:
|
||||
openChannelActivity(activity, entry.url, entry.serviceId);
|
||||
break;
|
||||
case NONE:
|
||||
throw new Exception("Url not known to service. service="
|
||||
+ Integer.toString(entry.serviceId) + " url=" + entry.url);
|
||||
default:
|
||||
openMainActivity(activity);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void openChannelActivity(Context context, String url, int serviceId) {
|
||||
openActivity(context, url, serviceId, ChannelActivity.class);
|
||||
}
|
||||
|
||||
public void openDetailActivity(Context context, String url, int serviceId) {
|
||||
openActivity(context, url, serviceId, VideoItemDetailActivity.class);
|
||||
}
|
||||
|
||||
private void openActivity(Context context, String url, int serviceId, Class acitivtyClass) {
|
||||
stack.push(new NavEntry(url, serviceId));
|
||||
Intent i = new Intent(context, acitivtyClass);
|
||||
i.putExtra(SERVICE_ID, serviceId);
|
||||
i.putExtra(URL, url);
|
||||
context.startActivity(i);
|
||||
}
|
||||
|
||||
public void openMainActivity(Activity a) {
|
||||
stack.clear();
|
||||
Intent i = new Intent(a, MainActivity.class);
|
||||
i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
NavUtils.navigateUpTo(a, i);
|
||||
}
|
||||
|
||||
public void onSaveInstanceState(Bundle state) {
|
||||
ArrayList<String> sa = new ArrayList<>();
|
||||
for(NavEntry entry : stack) {
|
||||
sa.add(entry.url);
|
||||
}
|
||||
state.putStringArrayList(NAV_STACK, sa);
|
||||
}
|
||||
|
||||
public void restoreSavedInstanceState(Bundle state) {
|
||||
ArrayList<String> sa = state.getStringArrayList(NAV_STACK);
|
||||
for(String url : sa) {
|
||||
stack.push(new NavEntry(url, NewPipe.getServiceByUrl(url).getServiceId()));
|
||||
}
|
||||
}
|
||||
}
|
||||
155
app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
Normal file
@@ -0,0 +1,155 @@
|
||||
package org.schabi.newpipe.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.stream_info.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
|
||||
import org.schabi.newpipe.fragments.OnItemSelectedListener;
|
||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
||||
import org.schabi.newpipe.player.BackgroundPlayer;
|
||||
import org.schabi.newpipe.player.BasePlayer;
|
||||
import org.schabi.newpipe.player.VideoPlayer;
|
||||
|
||||
@SuppressWarnings({"unused", "WeakerAccess"})
|
||||
public class NavigationHelper {
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Players
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public static Intent getOpenVideoPlayerIntent(Context context, Class targetClazz, StreamInfo info, int selectedStreamIndex) {
|
||||
Intent mIntent = new Intent(context, targetClazz)
|
||||
.putExtra(BasePlayer.VIDEO_TITLE, info.title)
|
||||
.putExtra(BasePlayer.VIDEO_URL, info.webpage_url)
|
||||
.putExtra(BasePlayer.VIDEO_THUMBNAIL_URL, info.thumbnail_url)
|
||||
.putExtra(BasePlayer.CHANNEL_NAME, info.uploader)
|
||||
.putExtra(VideoPlayer.INDEX_SEL_VIDEO_STREAM, selectedStreamIndex)
|
||||
.putExtra(VideoPlayer.VIDEO_STREAMS_LIST, Utils.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false))
|
||||
.putExtra(VideoPlayer.VIDEO_ONLY_AUDIO_STREAM, Utils.getHighestQualityAudio(info.audio_streams));
|
||||
if (info.start_position > 0) mIntent.putExtra(BasePlayer.START_POSITION, info.start_position * 1000);
|
||||
return mIntent;
|
||||
}
|
||||
|
||||
|
||||
public static Intent getOpenVideoPlayerIntent(Context context, Class targetClazz, VideoPlayer instance) {
|
||||
return new Intent(context, targetClazz)
|
||||
.putExtra(BasePlayer.VIDEO_TITLE, instance.getVideoTitle())
|
||||
.putExtra(BasePlayer.VIDEO_URL, instance.getVideoUrl())
|
||||
.putExtra(BasePlayer.VIDEO_THUMBNAIL_URL, instance.getVideoThumbnailUrl())
|
||||
.putExtra(BasePlayer.CHANNEL_NAME, instance.getChannelName())
|
||||
.putExtra(VideoPlayer.INDEX_SEL_VIDEO_STREAM, instance.getSelectedStreamIndex())
|
||||
.putExtra(VideoPlayer.VIDEO_STREAMS_LIST, instance.getVideoStreamsList())
|
||||
.putExtra(VideoPlayer.VIDEO_ONLY_AUDIO_STREAM, instance.getAudioStream())
|
||||
.putExtra(BasePlayer.START_POSITION, ((int) instance.getPlayer().getCurrentPosition()));
|
||||
}
|
||||
|
||||
public static Intent getOpenBackgroundPlayerIntent(Context context, StreamInfo info) {
|
||||
return getOpenBackgroundPlayerIntent(context, info, info.audio_streams.get(Utils.getPreferredAudioFormat(context, info.audio_streams)));
|
||||
}
|
||||
|
||||
public static Intent getOpenBackgroundPlayerIntent(Context context, StreamInfo info, AudioStream audioStream) {
|
||||
Intent mIntent = new Intent(context, BackgroundPlayer.class)
|
||||
.putExtra(BasePlayer.VIDEO_TITLE, info.title)
|
||||
.putExtra(BasePlayer.VIDEO_URL, info.webpage_url)
|
||||
.putExtra(BasePlayer.VIDEO_THUMBNAIL_URL, info.thumbnail_url)
|
||||
.putExtra(BasePlayer.CHANNEL_NAME, info.uploader)
|
||||
.putExtra(BasePlayer.CHANNEL_NAME, info.uploader)
|
||||
.putExtra(BackgroundPlayer.AUDIO_STREAM, audioStream);
|
||||
if (info.start_position > 0) mIntent.putExtra(BasePlayer.START_POSITION, info.start_position * 1000);
|
||||
return mIntent;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Through Interface (faster)
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public static void openChannel(OnItemSelectedListener listener, int serviceId, String url) {
|
||||
openChannel(listener, serviceId, url, null);
|
||||
}
|
||||
|
||||
public static void openChannel(OnItemSelectedListener listener, int serviceId, String url, String name) {
|
||||
listener.onItemSelected(StreamingService.LinkType.CHANNEL, serviceId, url, name);
|
||||
}
|
||||
|
||||
public static void openVideoDetail(OnItemSelectedListener listener, int serviceId, String url) {
|
||||
openVideoDetail(listener, serviceId, url, null);
|
||||
}
|
||||
|
||||
public static void openVideoDetail(OnItemSelectedListener listener, int serviceId, String url, String title) {
|
||||
listener.onItemSelected(StreamingService.LinkType.STREAM, serviceId, url, title);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Through Intents
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public static void openByLink(Context context, String url) throws Exception {
|
||||
context.startActivity(getIntentByLink(context, url));
|
||||
}
|
||||
|
||||
public static void openChannel(Context context, int serviceId, String url) {
|
||||
openChannel(context, serviceId, url, null);
|
||||
}
|
||||
|
||||
public static void openChannel(Context context, int serviceId, String url, String name) {
|
||||
Intent openIntent = getOpenIntent(context, url, serviceId, StreamingService.LinkType.CHANNEL);
|
||||
if (name != null && !name.isEmpty()) openIntent.putExtra(Constants.KEY_TITLE, name);
|
||||
context.startActivity(openIntent);
|
||||
}
|
||||
|
||||
public static void openVideoDetail(Context context, int serviceId, String url) {
|
||||
openVideoDetail(context, serviceId, url, null);
|
||||
}
|
||||
|
||||
public static void openVideoDetail(Context context, int serviceId, String url, String title) {
|
||||
Intent openIntent = getOpenIntent(context, url, serviceId, StreamingService.LinkType.STREAM);
|
||||
if (title != null && !title.isEmpty()) openIntent.putExtra(Constants.KEY_TITLE, title);
|
||||
context.startActivity(openIntent);
|
||||
}
|
||||
|
||||
public static void openMainActivity(Context context) {
|
||||
Intent mIntent = new Intent(context, MainActivity.class);
|
||||
context.startActivity(mIntent);
|
||||
}
|
||||
|
||||
public static void openSearch(Context context, int serviceId, String query) {
|
||||
Intent mIntent = new Intent(context, MainActivity.class);
|
||||
mIntent.putExtra(Constants.KEY_SERVICE_ID, serviceId);
|
||||
mIntent.putExtra(Constants.KEY_QUERY, query);
|
||||
mIntent.putExtra(Constants.KEY_OPEN_SEARCH, true);
|
||||
context.startActivity(mIntent);
|
||||
}
|
||||
|
||||
private static Intent getOpenIntent(Context context, String url, int serviceId, StreamingService.LinkType type) {
|
||||
Intent mIntent = new Intent(context, MainActivity.class);
|
||||
mIntent.putExtra(Constants.KEY_SERVICE_ID, serviceId);
|
||||
mIntent.putExtra(Constants.KEY_URL, url);
|
||||
mIntent.putExtra(Constants.KEY_LINK_TYPE, type);
|
||||
return mIntent;
|
||||
}
|
||||
|
||||
private static Intent getIntentByLink(Context context, String url) throws Exception {
|
||||
StreamingService service = NewPipe.getServiceByUrl(url);
|
||||
if (service == null) throw new Exception("NewPipe.getServiceByUrl returned null for url > \"" + url + "\"");
|
||||
int serviceId = service.getServiceId();
|
||||
switch (service.getLinkTypeByUrl(url)) {
|
||||
case STREAM:
|
||||
Intent sIntent = getOpenIntent(context, url, serviceId, StreamingService.LinkType.STREAM);
|
||||
sIntent.putExtra(VideoDetailFragment.AUTO_PLAY, PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean(context.getString(R.string.autoplay_through_intent_key), false));
|
||||
return sIntent;
|
||||
case CHANNEL:
|
||||
return getOpenIntent(context, url, serviceId, StreamingService.LinkType.CHANNEL);
|
||||
case NONE:
|
||||
throw new Exception("Url not known to service. service="
|
||||
+ Integer.toString(serviceId) + " url=" + url);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,12 @@ package org.schabi.newpipe.util;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
import android.support.annotation.RequiresApi;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
@@ -11,7 +15,7 @@ import android.support.v4.content.ContextCompat;
|
||||
public class PermissionHelper {
|
||||
public static final int PERMISSION_WRITE_STORAGE = 778;
|
||||
public static final int PERMISSION_READ_STORAGE = 777;
|
||||
|
||||
public static final int PERMISSION_SYSTEM_ALERT_WINDOW = 779;
|
||||
|
||||
|
||||
public static boolean checkStoragePermissions(Activity activity) {
|
||||
@@ -65,4 +69,27 @@ public class PermissionHelper {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* In order to be able to draw over other apps, the permission android.permission.SYSTEM_ALERT_WINDOW have to be granted.
|
||||
* <p>
|
||||
* On < API 23 (MarshMallow) the permission was granted when the user installed the application (via AndroidManifest),
|
||||
* on > 23, however, it have to start a activity asking the user if he agree.
|
||||
* <p>
|
||||
* This method just return if canDraw over other apps, if it doesn't, try to get the permission,
|
||||
* it does not get the result of the startActivityForResult, if the user accept, the next time that he tries to open
|
||||
* it will return true.
|
||||
*
|
||||
* @param activity context to startActivityForResult
|
||||
* @return returns {@link Settings#canDrawOverlays(Context)}
|
||||
**/
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
public static boolean checkSystemAlertWindowPermission(Activity activity) {
|
||||
if (!Settings.canDrawOverlays(activity)) {
|
||||
Intent i = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + activity.getPackageName()));
|
||||
activity.startActivityForResult(i, PERMISSION_SYSTEM_ALERT_WINDOW);
|
||||
return false;
|
||||
}else return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,25 +7,36 @@ import org.schabi.newpipe.R;
|
||||
|
||||
public class ThemeHelper {
|
||||
|
||||
public static void setTheme(Context context, boolean mode) {
|
||||
// mode is true for normal theme, false for no action bar theme.
|
||||
/**
|
||||
* Apply the selected theme (on NewPipe settings) in the context
|
||||
*
|
||||
* @param context context that the theme will be applied
|
||||
*/
|
||||
public static void setTheme(Context context) {
|
||||
|
||||
String themeKey = context.getString(R.string.theme_key);
|
||||
//String lightTheme = context.getResources().getString(R.string.light_theme_title);
|
||||
String darkTheme = context.getResources().getString(R.string.dark_theme_title);
|
||||
String blackTheme = context.getResources().getString(R.string.black_theme_title);
|
||||
|
||||
String sp = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getString(themeKey, context.getResources().getString(R.string.light_theme_title));
|
||||
String sp = PreferenceManager.getDefaultSharedPreferences(context).getString(themeKey, context.getResources().getString(R.string.light_theme_title));
|
||||
|
||||
if (mode) {
|
||||
if (sp.equals(darkTheme)) context.setTheme(R.style.DarkTheme);
|
||||
else if (sp.equals(blackTheme)) context.setTheme(R.style.BlackTheme);
|
||||
else context.setTheme(R.style.AppTheme);
|
||||
} else {
|
||||
if (sp.equals(darkTheme)) context.setTheme(R.style.DarkTheme_NoActionBar);
|
||||
else if (sp.equals(blackTheme)) context.setTheme(R.style.BlackTheme_NoActionBar);
|
||||
else context.setTheme(R.style.AppTheme_NoActionBar);
|
||||
}
|
||||
if (sp.equals(darkTheme)) context.setTheme(R.style.DarkTheme);
|
||||
else if (sp.equals(blackTheme)) context.setTheme(R.style.BlackTheme);
|
||||
else context.setTheme(R.style.AppTheme);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the selected theme (on NewPipe settings) is the Light theme
|
||||
*
|
||||
* @param context context to get the preference
|
||||
*/
|
||||
public static boolean isLightThemeSelected(Context context) {
|
||||
String themeKey = context.getString(R.string.theme_key);
|
||||
String darkTheme = context.getResources().getString(R.string.dark_theme_title);
|
||||
String blackTheme = context.getResources().getString(R.string.black_theme_title);
|
||||
|
||||
String sp = PreferenceManager.getDefaultSharedPreferences(context).getString(themeKey, context.getResources().getString(R.string.light_theme_title));
|
||||
|
||||
return !(sp.equals(darkTheme) || sp.equals(blackTheme));
|
||||
}
|
||||
}
|
||||
|
||||
273
app/src/main/java/org/schabi/newpipe/util/Utils.java
Normal file
@@ -0,0 +1,273 @@
|
||||
package org.schabi.newpipe.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.stream_info.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream_info.VideoStream;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class Utils {
|
||||
|
||||
private static final List<String> HIGH_RESOLUTION_LIST = Arrays.asList("1440p", "2160p", "1440p60", "2160p60");
|
||||
|
||||
/**
|
||||
* Return the index of the default stream in the list, based on the parameters
|
||||
* defaultResolution and preferredFormat
|
||||
*
|
||||
* @param videoStreams the list that will be extracted the index
|
||||
*
|
||||
* @return index of the preferred resolution&format
|
||||
*/
|
||||
public static int getDefaultResolution(String defaultResolution, String preferredFormat, List<VideoStream> videoStreams) {
|
||||
|
||||
if (defaultResolution.equals("Best resolution")) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// first try to find the one with the right resolution
|
||||
int selectedFormat = 0;
|
||||
for (int i = 0; i < videoStreams.size(); i++) {
|
||||
VideoStream item = videoStreams.get(i);
|
||||
if (defaultResolution.equals(item.resolution)) {
|
||||
selectedFormat = i;
|
||||
}
|
||||
}
|
||||
|
||||
// than try to find the one with the right resolution and format
|
||||
for (int i = 0; i < videoStreams.size(); i++) {
|
||||
VideoStream item = videoStreams.get(i);
|
||||
if (defaultResolution.equals(item.resolution)
|
||||
&& preferredFormat.equals(MediaFormat.getNameById(item.format))) {
|
||||
selectedFormat = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedFormat == 0 && !videoStreams.get(selectedFormat).resolution.contains(defaultResolution.replace("p60", "p"))) {
|
||||
// Maybe there's no 60 fps variant available, so fallback to the normal version
|
||||
String replace = defaultResolution.replace("p60", "p");
|
||||
for (int i = 0; i < videoStreams.size(); i++) {
|
||||
VideoStream item = videoStreams.get(i);
|
||||
if (replace.equals(item.resolution)) selectedFormat = i;
|
||||
}
|
||||
|
||||
// than try to find the one with the right resolution and format
|
||||
for (int i = 0; i < videoStreams.size(); i++) {
|
||||
VideoStream item = videoStreams.get(i);
|
||||
if (replace.equals(item.resolution)
|
||||
&& preferredFormat.equals(MediaFormat.getNameById(item.format))) {
|
||||
selectedFormat = i;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// this is actually an error,
|
||||
// but maybe there is really no stream fitting to the default value.
|
||||
return selectedFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #getDefaultResolution(String, String, List)
|
||||
*/
|
||||
public static int getDefaultResolution(Context context, List<VideoStream> videoStreams) {
|
||||
SharedPreferences defaultPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
if (defaultPreferences == null) return 0;
|
||||
|
||||
String defaultResolution = defaultPreferences
|
||||
.getString(context.getString(R.string.default_resolution_key), context.getString(R.string.default_resolution_value));
|
||||
|
||||
String preferredFormat = defaultPreferences
|
||||
.getString(context.getString(R.string.preferred_video_format_key), context.getString(R.string.preferred_video_format_default));
|
||||
|
||||
return getDefaultResolution(defaultResolution, preferredFormat, videoStreams);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #getDefaultResolution(String, String, List)
|
||||
*/
|
||||
public static int getPopupDefaultResolution(Context context, List<VideoStream> videoStreams) {
|
||||
SharedPreferences defaultPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
if (defaultPreferences == null) return 0;
|
||||
|
||||
String defaultResolution = defaultPreferences
|
||||
.getString(context.getString(R.string.default_popup_resolution_key), context.getString(R.string.default_popup_resolution_value));
|
||||
|
||||
String preferredFormat = defaultPreferences
|
||||
.getString(context.getString(R.string.preferred_video_format_key), context.getString(R.string.preferred_video_format_default));
|
||||
|
||||
return getDefaultResolution(defaultResolution, preferredFormat, videoStreams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the index of the default stream in the list, based on the
|
||||
* preferred audio format chosen in the settings
|
||||
*
|
||||
* @param context context to get the preferred audio format
|
||||
* @param audioStreams the list that will be extracted the index
|
||||
*
|
||||
* @return index of the preferred format
|
||||
*/
|
||||
public static int getPreferredAudioFormat(Context context, List<AudioStream> audioStreams) {
|
||||
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
if (sharedPreferences == null) return 0;
|
||||
|
||||
String preferredFormatString = sharedPreferences.getString(context.getString(R.string.default_audio_format_key), "webm");
|
||||
|
||||
int preferredFormat = MediaFormat.WEBMA.id;
|
||||
switch (preferredFormatString) {
|
||||
case "webm":
|
||||
preferredFormat = MediaFormat.WEBMA.id;
|
||||
break;
|
||||
case "m4a":
|
||||
preferredFormat = MediaFormat.M4A.id;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
int highestQualityIndex = 0;
|
||||
|
||||
// Try to find a audio stream with the preferred format
|
||||
for (int i = 0; i < audioStreams.size(); i++) if (audioStreams.get(i).format == preferredFormat) highestQualityIndex = i;
|
||||
|
||||
// Try to find a audio stream with the highest bitrate and preferred format
|
||||
for (int i = 0; i < audioStreams.size(); i++) {
|
||||
AudioStream audioStream = audioStreams.get(i);
|
||||
if (audioStream.avgBitrate > audioStreams.get(highestQualityIndex).avgBitrate
|
||||
&& audioStream.format == preferredFormat) highestQualityIndex = i;
|
||||
}
|
||||
|
||||
return highestQualityIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the audio from the list with the highest bitrate
|
||||
*
|
||||
* @param audioStreams list the audio streams
|
||||
* @return audio with highest average bitrate
|
||||
*/
|
||||
public static AudioStream getHighestQualityAudio(List<AudioStream> audioStreams) {
|
||||
int highestQualityIndex = 0;
|
||||
|
||||
for (int i = 1; i < audioStreams.size(); i++) {
|
||||
AudioStream audioStream = audioStreams.get(i);
|
||||
if (audioStream.avgBitrate > audioStreams.get(highestQualityIndex).avgBitrate) highestQualityIndex = i;
|
||||
}
|
||||
|
||||
return audioStreams.get(highestQualityIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Join the two lists of video streams (video_only and normal videos), and sort them according with preferred format
|
||||
* chosen by the user
|
||||
*
|
||||
* @param context context to search for the format to give preference
|
||||
* @param videoStreams normal videos list
|
||||
* @param videoOnlyStreams video only stream list
|
||||
* @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest
|
||||
* @return the sorted list
|
||||
*/
|
||||
public static ArrayList<VideoStream> getSortedStreamVideosList(Context context, List<VideoStream> videoStreams, List<VideoStream> videoOnlyStreams, boolean ascendingOrder) {
|
||||
boolean showHigherResolutions = PreferenceManager.getDefaultSharedPreferences(context).getBoolean(context.getString(R.string.show_higher_resolutions_key), false);
|
||||
String preferredFormatString = PreferenceManager.getDefaultSharedPreferences(context).getString(context.getString(R.string.preferred_video_format_key), context.getString(R.string.preferred_video_format_default));
|
||||
MediaFormat preferredFormat = MediaFormat.WEBM;
|
||||
switch (preferredFormatString) {
|
||||
case "WebM":
|
||||
preferredFormat = MediaFormat.WEBM;
|
||||
break;
|
||||
case "MPEG-4":
|
||||
preferredFormat = MediaFormat.MPEG_4;
|
||||
break;
|
||||
case "3GPP":
|
||||
preferredFormat = MediaFormat.v3GPP;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return getSortedStreamVideosList(preferredFormat, showHigherResolutions, videoStreams, videoOnlyStreams, ascendingOrder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Join the two lists of video streams (video_only and normal videos), and sort them according with preferred format
|
||||
* chosen by the user
|
||||
*
|
||||
* @param preferredFormat format to give preference
|
||||
* @param showHigherResolutions show >1080p resolutions
|
||||
* @param videoStreams normal videos list
|
||||
* @param videoOnlyStreams video only stream list
|
||||
* @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest @return the sorted list
|
||||
* @return the sorted list
|
||||
*/
|
||||
public static ArrayList<VideoStream> getSortedStreamVideosList(MediaFormat preferredFormat, boolean showHigherResolutions, List<VideoStream> videoStreams, List<VideoStream> videoOnlyStreams, boolean ascendingOrder) {
|
||||
ArrayList<VideoStream> retList = new ArrayList<>();
|
||||
HashMap<String, VideoStream> hashMap = new HashMap<>();
|
||||
|
||||
if (videoOnlyStreams != null) {
|
||||
for (VideoStream stream : videoOnlyStreams) {
|
||||
if (!showHigherResolutions && HIGH_RESOLUTION_LIST.contains(stream.resolution)) continue;
|
||||
retList.add(stream);
|
||||
}
|
||||
}
|
||||
if (videoStreams != null) {
|
||||
for (VideoStream stream : videoStreams) {
|
||||
if (!showHigherResolutions && HIGH_RESOLUTION_LIST.contains(stream.resolution)) continue;
|
||||
retList.add(stream);
|
||||
}
|
||||
}
|
||||
|
||||
// Add all to the hashmap
|
||||
for (VideoStream videoStream : retList) hashMap.put(videoStream.resolution, videoStream);
|
||||
|
||||
// Override the values when the key == resolution, with the preferredFormat
|
||||
for (VideoStream videoStream : retList) {
|
||||
if (videoStream.format == preferredFormat.id) hashMap.put(videoStream.resolution, videoStream);
|
||||
}
|
||||
|
||||
retList.clear();
|
||||
retList.addAll(hashMap.values());
|
||||
sortStreamList(retList, ascendingOrder);
|
||||
return retList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort the streams list depending on the parameter ascendingOrder;
|
||||
* <p>
|
||||
* It works like that:<br>
|
||||
* - Take a string resolution, remove the letters, replace "0p60" (for 60fps videos) with "1"
|
||||
* and sort by the greatest:<br>
|
||||
* <blockquote><pre>
|
||||
* 720p -> 720
|
||||
* 720p60 -> 721
|
||||
* 360p -> 360
|
||||
* 1080p -> 1080
|
||||
* 1080p60 -> 1081
|
||||
* <p>
|
||||
* ascendingOrder ? 360 < 720 < 721 < 1080 < 1081
|
||||
* !ascendingOrder ? 1081 < 1080 < 721 < 720 < 360/pre></blockquote>
|
||||
* <p>
|
||||
* @param videoStreams list that the sorting will be applied
|
||||
* @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest
|
||||
*/
|
||||
public static void sortStreamList(List<VideoStream> videoStreams, final boolean ascendingOrder) {
|
||||
Collections.sort(videoStreams, new Comparator<VideoStream>() {
|
||||
@Override
|
||||
public int compare(VideoStream o1, VideoStream o2) {
|
||||
int res1 = Integer.parseInt(o1.resolution.replace("0p60", "1").replaceAll("[^\\d.]", ""));
|
||||
int res2 = Integer.parseInt(o2.resolution.replace("0p60", "1").replaceAll("[^\\d.]", ""));
|
||||
|
||||
return ascendingOrder ? res1 - res2 : res2 - res1;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
118
app/src/main/java/org/schabi/newpipe/workers/AbstractWorker.java
Normal file
@@ -0,0 +1,118 @@
|
||||
package org.schabi.newpipe.workers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
|
||||
|
||||
import java.io.InterruptedIOException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Common properties of Workers
|
||||
*
|
||||
* @author mauriciocolli
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public abstract class AbstractWorker extends Thread {
|
||||
|
||||
private final AtomicBoolean isRunning = new AtomicBoolean(false);
|
||||
|
||||
private final int serviceId;
|
||||
private Context context;
|
||||
private Handler handler;
|
||||
private StreamingService service;
|
||||
|
||||
public AbstractWorker(Context context, int serviceId) {
|
||||
this.context = context;
|
||||
this.serviceId = serviceId;
|
||||
this.handler = new Handler(context.getMainLooper());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
isRunning.set(true);
|
||||
service = NewPipe.getService(serviceId);
|
||||
doWork(serviceId);
|
||||
} catch (Exception e) {
|
||||
// Handle the exception only if thread is not interrupted
|
||||
e.printStackTrace();
|
||||
if (!isInterrupted() && !(e instanceof InterruptedIOException) && !(e.getCause() instanceof InterruptedIOException)) {
|
||||
handleException(e, serviceId);
|
||||
}
|
||||
} finally {
|
||||
isRunning.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Here is the place that the heavy work is realized
|
||||
*
|
||||
* @param serviceId serviceId that was passed when created this object
|
||||
*
|
||||
* @throws Exception these exceptions are handled by the {@link #handleException(Exception, int)}
|
||||
*/
|
||||
protected abstract void doWork(int serviceId) throws Exception;
|
||||
|
||||
|
||||
/**
|
||||
* Method that handle the exception thrown by the {@link #doWork(int)}.
|
||||
*
|
||||
* @param exception {@link Exception} that was thrown by {@link #doWork(int)}
|
||||
*/
|
||||
protected abstract void handleException(Exception exception, int serviceId);
|
||||
|
||||
/**
|
||||
* Return true if the extraction is not completed yet
|
||||
*
|
||||
* @return the value of the AtomicBoolean {@link #isRunning}
|
||||
*/
|
||||
public boolean isRunning() {
|
||||
return isRunning.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel this ExtractorWorker, calling {@link #onDestroy()} and interrupting this thread.
|
||||
* <p>
|
||||
* <b>Note:</b> Any I/O that is active in the moment that this method is called will be canceled and a Exception will be thrown, because of the {@link #interrupt()}.<br>
|
||||
* This is useful when you don't want the resulting {@link StreamInfo} anymore, but don't want to waste bandwidth, otherwise it'd run till it receives the StreamInfo.
|
||||
*/
|
||||
public void cancel() {
|
||||
onDestroy();
|
||||
this.interrupt();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that discards everything that doesn't need anymore.<br>
|
||||
* Subclasses can override this method to destroy their garbage.
|
||||
*/
|
||||
protected void onDestroy() {
|
||||
this.isRunning.set(false);
|
||||
this.context = null;
|
||||
this.handler = null;
|
||||
this.service = null;
|
||||
}
|
||||
|
||||
public Handler getHandler() {
|
||||
return handler;
|
||||
}
|
||||
|
||||
public StreamingService getService() {
|
||||
return service;
|
||||
}
|
||||
|
||||
public int getServiceId() {
|
||||
return serviceId;
|
||||
}
|
||||
|
||||
public String getServiceName() {
|
||||
return service == null ? "none" : service.getServiceInfo().name;
|
||||
}
|
||||
|
||||
public Context getContext() {
|
||||
return context;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package org.schabi.newpipe.workers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelExtractor;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Extract {@link ChannelInfo} with {@link ChannelExtractor} from the given url of the given service
|
||||
*
|
||||
* @author mauriciocolli
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class ChannelExtractorWorker extends ExtractorWorker {
|
||||
//private static final String TAG = "ChannelExtractorWorker";
|
||||
|
||||
private int pageNumber;
|
||||
private boolean onlyVideos;
|
||||
|
||||
private ChannelInfo channelInfo = null;
|
||||
private OnChannelInfoReceive callback;
|
||||
|
||||
/**
|
||||
* Interface which will be called for result and errors
|
||||
*/
|
||||
public interface OnChannelInfoReceive {
|
||||
void onReceive(ChannelInfo info, boolean onlyVideos);
|
||||
void onError(int messageId);
|
||||
/**
|
||||
* Called when an unrecoverable error has occurred.
|
||||
* <p> This is a good place to finish the caller. </p>
|
||||
*/
|
||||
void onUnrecoverableError(Exception exception);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param context context for error reporting purposes
|
||||
* @param serviceId id of the request service
|
||||
* @param channelUrl channelUrl of the service (e.g. https://www.youtube.com/channel/UC_aEa8K-EOJ3D6gOs7HcyNg)
|
||||
* @param pageNumber which page to extract
|
||||
* @param onlyVideos flag that will be send by {@link OnChannelInfoReceive#onReceive(ChannelInfo, boolean)}
|
||||
* @param callback listener that will be called-back when events occur (check {@link ChannelExtractorWorker.OnChannelInfoReceive})
|
||||
*/
|
||||
public ChannelExtractorWorker(Context context, int serviceId, String channelUrl, int pageNumber, boolean onlyVideos, OnChannelInfoReceive callback) {
|
||||
super(context, channelUrl, serviceId);
|
||||
this.pageNumber = pageNumber;
|
||||
this.callback = callback;
|
||||
this.onlyVideos = onlyVideos;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
this.callback = null;
|
||||
this.channelInfo = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doWork(int serviceId, String url) throws Exception {
|
||||
ChannelExtractor extractor = getService().getChannelExtractorInstance(url, pageNumber);
|
||||
channelInfo = ChannelInfo.getInfo(extractor);
|
||||
|
||||
if (!channelInfo.errors.isEmpty()) handleErrorsDuringExtraction(channelInfo.errors, ErrorActivity.REQUESTED_CHANNEL);
|
||||
|
||||
if (callback != null && channelInfo != null && !isInterrupted()) getHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isInterrupted() || callback == null) return;
|
||||
|
||||
callback.onReceive(channelInfo, onlyVideos);
|
||||
onDestroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void handleException(final Exception exception, int serviceId, String url) {
|
||||
if (callback == null || getHandler() == null || isInterrupted()) return;
|
||||
|
||||
if (exception instanceof IOException) {
|
||||
getHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onError(R.string.network_error);
|
||||
}
|
||||
});
|
||||
} else if (exception instanceof ParsingException || exception instanceof ExtractionException) {
|
||||
ErrorActivity.reportError(getHandler(), getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_CHANNEL, getServiceName(), url, R.string.parsing_error));
|
||||
getHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onUnrecoverableError(exception);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ErrorActivity.reportError(getHandler(), getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_CHANNEL, getServiceName(), url, R.string.general_error));
|
||||
getHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onUnrecoverableError(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package org.schabi.newpipe.workers;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Common properties of ExtractorWorkers
|
||||
*
|
||||
* @author mauriciocolli
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public abstract class ExtractorWorker extends AbstractWorker {
|
||||
private final String url;
|
||||
|
||||
public ExtractorWorker(Context context, String url, int serviceId) {
|
||||
super(context, serviceId);
|
||||
this.url = url;
|
||||
if (url.length() >= 40) setName("Thread-" + url.substring(url.length() - 11, url.length()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doWork(int serviceId) throws Exception {
|
||||
doWork(serviceId, url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Here is the place that the heavy work is realized
|
||||
*
|
||||
* @param serviceId serviceId that was passed when created this object
|
||||
* @param url url that was passed when created this object
|
||||
*
|
||||
* @throws Exception these exceptions are handled by the {@link #handleException(Exception, int, String)}
|
||||
*/
|
||||
protected abstract void doWork(int serviceId, String url) throws Exception;
|
||||
|
||||
@Override
|
||||
protected void handleException(Exception exception, int serviceId) {
|
||||
handleException(exception, serviceId, url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that handle the exception thrown by the {@link #doWork(int, String)}.
|
||||
*
|
||||
* @param exception {@link Exception} that was thrown by {@link #doWork(int, String)}
|
||||
*/
|
||||
protected abstract void handleException(Exception exception, int serviceId, String url);
|
||||
|
||||
/**
|
||||
* Handle the errors <b>during</b> extraction and shows a Report button to the user.<br/>
|
||||
* Subclasses <b>maybe</b> call this method.
|
||||
*
|
||||
* @param errorsList list of exceptions that happened during extraction
|
||||
* @param errorUserAction what action was the user performing during the error.
|
||||
* (One of the {@link ErrorActivity}.REQUEST_* error (message) ids)
|
||||
*/
|
||||
protected void handleErrorsDuringExtraction(List<Throwable> errorsList, int errorUserAction){
|
||||
String errorString = "<error id>";
|
||||
switch (errorUserAction) {
|
||||
case ErrorActivity.REQUESTED_STREAM:
|
||||
errorString= ErrorActivity.REQUESTED_STREAM_STRING;
|
||||
break;
|
||||
case ErrorActivity.REQUESTED_CHANNEL:
|
||||
errorString= ErrorActivity.REQUESTED_CHANNEL_STRING;
|
||||
break;
|
||||
}
|
||||
|
||||
Log.e(errorString, "OCCURRED ERRORS DURING EXTRACTION:");
|
||||
for (Throwable e : errorsList) {
|
||||
e.printStackTrace();
|
||||
Log.e(errorString, "------");
|
||||
}
|
||||
|
||||
if (getContext() instanceof Activity) {
|
||||
View rootView = getContext() instanceof Activity ? ((Activity) getContext()).findViewById(android.R.id.content) : null;
|
||||
ErrorActivity.reportError(getHandler(), getContext(), errorsList, null, rootView, ErrorActivity.ErrorInfo.make(errorUserAction, getServiceName(), url, 0 /* no message for the user */));
|
||||
}
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
127
app/src/main/java/org/schabi/newpipe/workers/SearchWorker.java
Normal file
@@ -0,0 +1,127 @@
|
||||
package org.schabi.newpipe.workers;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.view.View;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
import org.schabi.newpipe.extractor.search.SearchEngine;
|
||||
import org.schabi.newpipe.extractor.search.SearchResult;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.EnumSet;
|
||||
|
||||
/**
|
||||
* Return list of results based on a query
|
||||
*
|
||||
* @author mauriciocolli
|
||||
*/
|
||||
public class SearchWorker extends AbstractWorker {
|
||||
|
||||
private EnumSet<SearchEngine.Filter> filter;
|
||||
private String query;
|
||||
private int page;
|
||||
private OnSearchResult callback;
|
||||
|
||||
/**
|
||||
* Interface which will be called for result and errors
|
||||
*/
|
||||
public interface OnSearchResult {
|
||||
void onSearchResult(SearchResult result);
|
||||
void onNothingFound(String message);
|
||||
void onSearchError(int messageId);
|
||||
void onReCaptchaChallenge();
|
||||
}
|
||||
|
||||
public SearchWorker(Context context, int serviceId, String query, int page, EnumSet<SearchEngine.Filter> filter, OnSearchResult callback) {
|
||||
super(context, serviceId);
|
||||
this.callback = callback;
|
||||
this.query = query;
|
||||
this.page = page;
|
||||
this.filter = filter;
|
||||
}
|
||||
|
||||
public static SearchWorker startForQuery(Context context, int serviceId, @NonNull String query, int page, EnumSet<SearchEngine.Filter> filter, OnSearchResult callback) {
|
||||
SearchWorker worker = new SearchWorker(context, serviceId, query, page, filter, callback);
|
||||
worker.start();
|
||||
return worker;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
this.callback = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doWork(int serviceId) throws Exception {
|
||||
SearchEngine searchEngine = getService().getSearchEngineInstance();
|
||||
|
||||
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
String searchLanguageKey = getContext().getString(R.string.search_language_key);
|
||||
String searchLanguage = sharedPreferences.getString(searchLanguageKey, getContext().getString(R.string.default_language_value));
|
||||
|
||||
final SearchResult searchResult = SearchResult.getSearchResult(searchEngine, query, page, searchLanguage, filter);
|
||||
if (callback != null && searchResult != null && !isInterrupted()) getHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isInterrupted() || callback == null) return;
|
||||
|
||||
callback.onSearchResult(searchResult);
|
||||
onDestroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleException(final Exception exception, int serviceId) {
|
||||
if (callback == null || getHandler() == null || isInterrupted()) return;
|
||||
|
||||
if (exception instanceof ReCaptchaException) {
|
||||
getHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onReCaptchaChallenge();
|
||||
}
|
||||
});
|
||||
} else if (exception instanceof IOException) {
|
||||
getHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onSearchError(R.string.network_error);
|
||||
}
|
||||
});
|
||||
} else if (exception instanceof SearchEngine.NothingFoundException) {
|
||||
getHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onNothingFound(exception.getMessage());
|
||||
}
|
||||
});
|
||||
} else if (exception instanceof ExtractionException) {
|
||||
View rootView = getContext() instanceof Activity ? ((Activity) getContext()).findViewById(android.R.id.content) : null;
|
||||
ErrorActivity.reportError(getHandler(), getContext(), exception, null, rootView, ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED, getServiceName(), query, R.string.parsing_error));
|
||||
getHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onSearchError(R.string.parsing_error);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
View rootView = getContext() instanceof Activity ? ((Activity) getContext()).findViewById(android.R.id.content) : null;
|
||||
ErrorActivity.reportError(getHandler(), getContext(), exception, null, rootView, ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED, getServiceName(), query, R.string.general_error));
|
||||
getHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onSearchError(R.string.general_error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package org.schabi.newpipe.workers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor;
|
||||
import org.schabi.newpipe.extractor.stream_info.StreamExtractor;
|
||||
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Extract {@link StreamInfo} with {@link StreamExtractor} from the given url of the given service
|
||||
*
|
||||
* @author mauriciocolli
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class StreamExtractorWorker extends ExtractorWorker {
|
||||
//private static final String TAG = "StreamExtractorWorker";
|
||||
|
||||
private StreamInfo streamInfo = null;
|
||||
private OnStreamInfoReceivedListener callback;
|
||||
|
||||
/**
|
||||
* Interface which will be called for result and errors
|
||||
*/
|
||||
public interface OnStreamInfoReceivedListener {
|
||||
void onReceive(StreamInfo info);
|
||||
void onError(int messageId);
|
||||
void onReCaptchaException();
|
||||
void onBlockedByGemaError();
|
||||
void onContentErrorWithMessage(int messageId);
|
||||
void onContentError();
|
||||
|
||||
/**
|
||||
* Called when an unrecoverable error has occurred.
|
||||
* <p> This is a good place to finish the caller. </p>
|
||||
*/
|
||||
void onUnrecoverableError(Exception exception);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param context context for error reporting purposes
|
||||
* @param serviceId id of the request service
|
||||
* @param videoUrl videoUrl of the service (e.g. https://www.youtube.com/watch?v=HyHNuVaZJ-k)
|
||||
* @param callback listener that will be called-back when events occur (check {@link StreamExtractorWorker.OnStreamInfoReceivedListener})
|
||||
*/
|
||||
public StreamExtractorWorker(Context context, int serviceId, String videoUrl, OnStreamInfoReceivedListener callback) {
|
||||
super(context, videoUrl, serviceId);
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
this.callback = null;
|
||||
this.streamInfo = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doWork(int serviceId, String url) throws Exception {
|
||||
StreamExtractor streamExtractor = getService().getExtractorInstance(url);
|
||||
streamInfo = StreamInfo.getVideoInfo(streamExtractor);
|
||||
|
||||
if (streamInfo != null && !streamInfo.errors.isEmpty()) handleErrorsDuringExtraction(streamInfo.errors, ErrorActivity.REQUESTED_STREAM);
|
||||
|
||||
if (callback != null && getHandler() != null && streamInfo != null && !isInterrupted()) getHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isInterrupted() || callback == null) return;
|
||||
|
||||
callback.onReceive(streamInfo);
|
||||
onDestroy();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleException(final Exception exception, int serviceId, final String url) {
|
||||
if (callback == null || getHandler() == null || isInterrupted()) return;
|
||||
|
||||
if (exception instanceof ReCaptchaException) {
|
||||
getHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onReCaptchaException();
|
||||
}
|
||||
});
|
||||
} else if (exception instanceof IOException) {
|
||||
getHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onError(R.string.network_error);
|
||||
}
|
||||
});
|
||||
} else if (exception instanceof YoutubeStreamExtractor.GemaException) {
|
||||
getHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onBlockedByGemaError();
|
||||
}
|
||||
});
|
||||
} else if (exception instanceof YoutubeStreamExtractor.LiveStreamException) {
|
||||
getHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onContentErrorWithMessage(R.string.live_streams_not_supported);
|
||||
}
|
||||
});
|
||||
} else if (exception instanceof StreamExtractor.ContentNotAvailableException) {
|
||||
getHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onContentError();
|
||||
}
|
||||
});
|
||||
} else if (exception instanceof YoutubeStreamExtractor.DecryptException) {
|
||||
// custom service related exceptions
|
||||
ErrorActivity.reportError(getHandler(), getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, getServiceName(), url, R.string.youtube_signature_decryption_error));
|
||||
getHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onUnrecoverableError(exception);
|
||||
}
|
||||
});
|
||||
} else if (exception instanceof StreamInfo.StreamExctractException) {
|
||||
if (!streamInfo.errors.isEmpty()) {
|
||||
// !!! if this case ever kicks in someone gets kicked out !!!
|
||||
ErrorActivity.reportError(getHandler(), getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, getServiceName(), url, R.string.could_not_get_stream));
|
||||
} else {
|
||||
ErrorActivity.reportError(getHandler(), getContext(), streamInfo.errors, MainActivity.class, null, ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, getServiceName(), url, R.string.could_not_get_stream));
|
||||
}
|
||||
|
||||
getHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onUnrecoverableError(exception);
|
||||
}
|
||||
});
|
||||
} else if (exception instanceof ParsingException) {
|
||||
ErrorActivity.reportError(getHandler(), getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, getServiceName(), url, R.string.parsing_error));
|
||||
getHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onUnrecoverableError(exception);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ErrorActivity.reportError(getHandler(), getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, getServiceName(), url, R.string.general_error));
|
||||
getHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onUnrecoverableError(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package org.schabi.newpipe.workers;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.view.View;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.SuggestionExtractor;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Worker that get suggestions based on the query
|
||||
*
|
||||
* @author mauriciocolli
|
||||
*/
|
||||
public class SuggestionWorker extends AbstractWorker {
|
||||
|
||||
private String query;
|
||||
private OnSuggestionResult callback;
|
||||
|
||||
/**
|
||||
* Interface which will be called for result and errors
|
||||
*/
|
||||
public interface OnSuggestionResult {
|
||||
void onSuggestionResult(@NonNull List<String> suggestions);
|
||||
void onSuggestionError(int messageId);
|
||||
}
|
||||
|
||||
public SuggestionWorker(Context context, int serviceId, String query, OnSuggestionResult callback) {
|
||||
super(context, serviceId);
|
||||
this.callback = callback;
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
public static SuggestionWorker startForQuery(Context context, int serviceId, @NonNull String query, OnSuggestionResult callback) {
|
||||
SuggestionWorker worker = new SuggestionWorker(context, serviceId, query, callback);
|
||||
worker.start();
|
||||
return worker;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
this.callback = null;
|
||||
this.query = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doWork(int serviceId) throws Exception {
|
||||
SuggestionExtractor suggestionExtractor = getService().getSuggestionExtractorInstance();
|
||||
|
||||
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
String searchLanguageKey = getContext().getString(R.string.search_language_key);
|
||||
String searchLanguage = sharedPreferences.getString(searchLanguageKey, getContext().getString(R.string.default_language_value));
|
||||
|
||||
final List<String> suggestions = suggestionExtractor.suggestionList(query, searchLanguage);
|
||||
|
||||
if (callback != null && suggestions != null && !isInterrupted()) getHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isInterrupted() || callback == null) return;
|
||||
|
||||
callback.onSuggestionResult(suggestions);
|
||||
onDestroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleException(final Exception exception, int serviceId) {
|
||||
if (callback == null || getHandler() == null || isInterrupted()) return;
|
||||
|
||||
if (exception instanceof ExtractionException) {
|
||||
View rootView = getContext() instanceof Activity ? ((Activity) getContext()).findViewById(android.R.id.content) : null;
|
||||
ErrorActivity.reportError(getHandler(), getContext(), exception, null, rootView, ErrorActivity.ErrorInfo.make(ErrorActivity.GET_SUGGESTIONS, getServiceName(), query, R.string.parsing_error));
|
||||
getHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onSuggestionError(R.string.parsing_error);
|
||||
}
|
||||
});
|
||||
} else if (exception instanceof IOException) {
|
||||
getHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onSuggestionError(R.string.network_error);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
View rootView = getContext() instanceof Activity ? ((Activity) getContext()).findViewById(android.R.id.content) : null;
|
||||
ErrorActivity.reportError(getHandler(), getContext(), exception, null, rootView, ErrorActivity.ErrorInfo.make(ErrorActivity.GET_SUGGESTIONS, getServiceName(), query, R.string.general_error));
|
||||
getHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onSuggestionError(R.string.general_error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
BIN
app/src/main/res/drawable-hdpi/ic_action_av_fast_forward.png
Normal file
|
After Width: | Height: | Size: 575 B |
BIN
app/src/main/res/drawable-hdpi/ic_close_black_24dp.png
Normal file
|
After Width: | Height: | Size: 207 B |
BIN
app/src/main/res/drawable-hdpi/ic_expand_less_black_24dp.png
Normal file
|
After Width: | Height: | Size: 149 B |
BIN
app/src/main/res/drawable-hdpi/ic_expand_less_white_24dp.png
Normal file
|
After Width: | Height: | Size: 156 B |
BIN
app/src/main/res/drawable-hdpi/ic_expand_more_black_24dp.png
Normal file
|
After Width: | Height: | Size: 151 B |
BIN
app/src/main/res/drawable-hdpi/ic_expand_more_white_24dp.png
Normal file
|
After Width: | Height: | Size: 159 B |
BIN
app/src/main/res/drawable-hdpi/ic_filter_list_black_24dp.png
Normal file
|
After Width: | Height: | Size: 107 B |
BIN
app/src/main/res/drawable-hdpi/ic_filter_list_white_24dp.png
Normal file
|
After Width: | Height: | Size: 111 B |
BIN
app/src/main/res/drawable-hdpi/ic_fullscreen_exit_white.png
Normal file
|
After Width: | Height: | Size: 105 B |
BIN
app/src/main/res/drawable-hdpi/ic_fullscreen_white.png
Normal file
|
After Width: | Height: | Size: 107 B |
|
Before Width: | Height: | Size: 92 B After Width: | Height: | Size: 92 B |
|
Before Width: | Height: | Size: 105 B |
|
After Width: | Height: | Size: 189 B |
|
After Width: | Height: | Size: 193 B |
|
Before Width: | Height: | Size: 283 B After Width: | Height: | Size: 283 B |
BIN
app/src/main/res/drawable-hdpi/ic_repeat_white.png
Normal file
|
After Width: | Height: | Size: 234 B |
BIN
app/src/main/res/drawable-hdpi/ic_replay_white.png
Normal file
|
After Width: | Height: | Size: 675 B |
BIN
app/src/main/res/drawable-hdpi/ic_screen_rotation_white.png
Normal file
|
After Width: | Height: | Size: 976 B |
|
Before Width: | Height: | Size: 858 B |
BIN
app/src/main/res/drawable-hdpi/ic_search_black_24dp.png
Normal file
|
After Width: | Height: | Size: 390 B |
BIN
app/src/main/res/drawable-hdpi/ic_search_white_24dp.png
Normal file
|
After Width: | Height: | Size: 396 B |
BIN
app/src/main/res/drawable-mdpi/ic_action_av_fast_forward.png
Normal file
|
After Width: | Height: | Size: 277 B |
BIN
app/src/main/res/drawable-mdpi/ic_close_black_24dp.png
Normal file
|
After Width: | Height: | Size: 164 B |
BIN
app/src/main/res/drawable-mdpi/ic_expand_less_black_24dp.png
Normal file
|
After Width: | Height: | Size: 126 B |
BIN
app/src/main/res/drawable-mdpi/ic_expand_less_white_24dp.png
Normal file
|
After Width: | Height: | Size: 129 B |
BIN
app/src/main/res/drawable-mdpi/ic_expand_more_black_24dp.png
Normal file
|
After Width: | Height: | Size: 125 B |
BIN
app/src/main/res/drawable-mdpi/ic_expand_more_white_24dp.png
Normal file
|
After Width: | Height: | Size: 129 B |
BIN
app/src/main/res/drawable-mdpi/ic_filter_list_black_24dp.png
Normal file
|
After Width: | Height: | Size: 89 B |
BIN
app/src/main/res/drawable-mdpi/ic_filter_list_white_24dp.png
Normal file
|
After Width: | Height: | Size: 90 B |
BIN
app/src/main/res/drawable-mdpi/ic_fullscreen_exit_white.png
Normal file
|
After Width: | Height: | Size: 101 B |
BIN
app/src/main/res/drawable-mdpi/ic_fullscreen_white.png
Normal file
|
After Width: | Height: | Size: 101 B |