Merge remote-tracking branch 'touch/develop'
7
.gitignore
vendored
|
@ -1,7 +1,8 @@
|
||||||
*.iml
|
*.iml
|
||||||
.gradle
|
.gradle
|
||||||
/local.properties
|
/local.properties
|
||||||
/.idea
|
/.idea/workspace.xml
|
||||||
|
/.idea/libraries
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/build
|
/build
|
||||||
/captures
|
/captures
|
||||||
|
@ -62,7 +63,3 @@ google-services.json
|
||||||
freeline.py
|
freeline.py
|
||||||
freeline/
|
freeline/
|
||||||
freeline_project_description.json
|
freeline_project_description.json
|
||||||
projectFilesBackup/
|
|
||||||
|
|
||||||
|
|
||||||
.gradletasknamecache
|
|
||||||
|
|
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2017 Felix Bürkle
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
15
README.md
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# Chaosflix
|
||||||
|
|
||||||
|
A Android TV / FireTV app to watch content from media.ccc.de
|
||||||
|
|
||||||
|
[![Install from Amazon](https://images-na.ssl-images-amazon.com/images/G/01/mobile-apps/devportal2/res/images/amazon-underground-app-us-black.png)](http://www.amazon.com/gp/product/B06Y3GYYGB/ref=mas_pm_chaosflix)
|
||||||
|
[![Installieren auf Amazon](https://images-na.ssl-images-amazon.com/images/G/01/mobile-apps/devportal2/res/images/amazon-underground-app-de-black.png)](http://www.amazon.de/gp/product/B06Y3GYYGB/ref=mas_pm_chaosflix)
|
||||||
|
|
||||||
|
You can get an APK you can install under [Releases](https://github.com/NiciDieNase/chaosflix/releases).
|
||||||
|
If you don't know how install that on your device, have a look [here](http://www.aftvnews.com/sideload/)
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
![screenshot](screenshots/homescreen.png)
|
||||||
|
![screenshot](screenshots/device-2017-04-06-191834.png)
|
||||||
|
![screenshot](screenshots/device-2017-04-06-191926.png)
|
||||||
|
![screenshot](screenshots/device-2017-04-06-192059.png)
|
BIN
amazon_icons/app_icon_1280x720.png
Normal file
After Width: | Height: | Size: 65 KiB |
BIN
amazon_icons/background_1920x1080.png
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
amazon_icons/icon_large_512x512.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
amazon_icons/icon_notext_144x144.png
Normal file
After Width: | Height: | Size: 7.8 KiB |
BIN
amazon_icons/icon_notext_48x48.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
amazon_icons/icon_notext_512x512.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
amazon_icons/icon_small_114x144.png
Normal file
After Width: | Height: | Size: 7.5 KiB |
BIN
amazon_icons/toolbar_icon.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
48
build.gradle
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
ext.kotlin_version = '1.2.30'
|
||||||
|
repositories {
|
||||||
|
jcenter()
|
||||||
|
mavenCentral()
|
||||||
|
google()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath 'com.android.tools.build:gradle:3.1.4'
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
|
||||||
|
|
||||||
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
// in the individual module build.gradle files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
mavenLocal()
|
||||||
|
jcenter()
|
||||||
|
maven { url "https://oss.sonatype.org/content/repositories/snapshots/" }
|
||||||
|
google()
|
||||||
|
maven { url 'https://jitpack.io' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ext{
|
||||||
|
compileSdkVersion = 27
|
||||||
|
buildToolsVersion = "27.0.3"
|
||||||
|
supportLibraryVersion = "27.1.1"
|
||||||
|
constraintLayoutVersion = "1.0.2"
|
||||||
|
archCompVersion = "1.1.1"
|
||||||
|
minSDK = 22
|
||||||
|
targetSDK = 27
|
||||||
|
}
|
||||||
|
|
||||||
|
task clean(type: Delete) {
|
||||||
|
delete rootProject.buildDir
|
||||||
|
}
|
||||||
|
|
||||||
|
configurations.all {
|
||||||
|
resolutionStrategy {
|
||||||
|
cacheChangingModulesFor 0, 'seconds'
|
||||||
|
}
|
||||||
|
}
|
419
logo.svg
Normal file
After Width: | Height: | Size: 149 KiB |
1
settings.gradle
Normal file
|
@ -0,0 +1 @@
|
||||||
|
include ':touch'
|
1
touch/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/build
|
120
touch/build.gradle
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
apply plugin: 'com.android.application'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
apply plugin: 'kotlin-kapt'
|
||||||
|
apply plugin: 'kotlin-android-extensions'
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||||
|
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "de.nicidienase.chaosflix"
|
||||||
|
minSdkVersion rootProject.ext.minSDK
|
||||||
|
targetSdkVersion rootProject.ext.targetSDK
|
||||||
|
// odd for touch, even for leanback
|
||||||
|
versionCode 25
|
||||||
|
versionName "0.2.9"
|
||||||
|
multiDexEnabled true
|
||||||
|
}
|
||||||
|
buildTypes {
|
||||||
|
debug {
|
||||||
|
minifyEnabled false
|
||||||
|
useProguard false
|
||||||
|
}
|
||||||
|
release {
|
||||||
|
shrinkResources true
|
||||||
|
minifyEnabled true
|
||||||
|
useProguard false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'),
|
||||||
|
'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
packagingOptions {
|
||||||
|
exclude 'META-INF/ASL2.0'
|
||||||
|
exclude 'META-INF/LICENSE'
|
||||||
|
exclude 'META-INF/license.txt'
|
||||||
|
exclude 'META-INF/NOTICE'
|
||||||
|
exclude 'META-INF/notice.txt'
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
defaultConfig {
|
||||||
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
|
}
|
||||||
|
lintOptions {
|
||||||
|
abortOnError false
|
||||||
|
}
|
||||||
|
dataBinding {
|
||||||
|
enabled = true
|
||||||
|
}
|
||||||
|
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// implementation 'com.github.NiciDieNase:chaosflix-common:2.0.0-SNAPSHOT'
|
||||||
|
implementation 'de.nicidienase.chaosflix:common:2.0.0-SNAPSHOT'
|
||||||
|
|
||||||
|
implementation('com.mikepenz:aboutlibraries:6.1.1@aar') {
|
||||||
|
transitive = true
|
||||||
|
}
|
||||||
|
implementation 'com.github.medyo:android-about-page:1.2.2'
|
||||||
|
implementation 'com.android.support:multidex:1.0.3'
|
||||||
|
|
||||||
|
implementation "com.android.support:support-v4:${rootProject.ext.supportLibraryVersion}"
|
||||||
|
implementation "com.android.support:recyclerview-v7:${rootProject.ext.supportLibraryVersion}"
|
||||||
|
implementation "com.android.support:cardview-v7:${rootProject.ext.supportLibraryVersion}"
|
||||||
|
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
|
||||||
|
implementation "com.android.support:design:${rootProject.ext.supportLibraryVersion}"
|
||||||
|
implementation "com.android.support:preference-v14:${rootProject.ext.supportLibraryVersion}"
|
||||||
|
|
||||||
|
// implementation "android.arch.lifecycle:runtime:1.0.3"
|
||||||
|
implementation "android.arch.lifecycle:extensions:${rootProject.ext.archCompVersion}"
|
||||||
|
implementation "android.arch.lifecycle:common-java8:1.1.1"
|
||||||
|
implementation "android.arch.persistence.room:runtime:${rootProject.ext.archCompVersion}"
|
||||||
|
// kapt 'com.android.databinding:compiler:3.2.0-alpha10'
|
||||||
|
kapt "android.arch.lifecycle:compiler:${rootProject.ext.archCompVersion}"
|
||||||
|
// kapt "android.arch.persistence.room:compiler:${rootProject.ext.archCompVersion}"
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-reflect:1.2.61"
|
||||||
|
|
||||||
|
implementation 'com.squareup.retrofit2:retrofit:2.3.0'
|
||||||
|
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
|
||||||
|
implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.9.0'
|
||||||
|
|
||||||
|
implementation 'com.google.android.exoplayer:exoplayer:r2.5.2'
|
||||||
|
implementation 'com.squareup.picasso:picasso:2.5.2'
|
||||||
|
|
||||||
|
debugImplementation 'com.facebook.stetho:stetho:1.4.2'
|
||||||
|
debugImplementation 'com.facebook.stetho:stetho-okhttp3:1.4.2'
|
||||||
|
|
||||||
|
implementation 'net.opacapp:multiline-collapsingtoolbar:1.6.0'
|
||||||
|
implementation 'net.rdrei.android.dirchooser:library:3.2@aar'
|
||||||
|
implementation 'com.gu:option:1.3'
|
||||||
|
|
||||||
|
androidTestImplementation('com.android.support.test:rules:0.5') {
|
||||||
|
exclude module: 'support-annotations'
|
||||||
|
}
|
||||||
|
androidTestImplementation('com.android.support.test:runner:0.5') {
|
||||||
|
exclude module: 'support-annotations'
|
||||||
|
}
|
||||||
|
androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.3'
|
||||||
|
androidTestImplementation 'org.hamcrest:hamcrest-library:1.3'
|
||||||
|
testImplementation 'org.mockito:mockito-core:2.11.0'
|
||||||
|
androidTestImplementation 'com.android.support.test.espresso:espresso-core:2.2.2', {
|
||||||
|
exclude group: 'com.android.support', module: 'support-annotations'
|
||||||
|
}
|
||||||
|
implementation 'commons-io:commons-io:2.4'
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
|
||||||
|
}
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
maven { url 'http://guardian.github.com/maven/repo-releases' }
|
||||||
|
mavenLocal()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
69
touch/proguard-rules.pro
vendored
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
||||||
|
|
||||||
|
-dontwarn com.squareup.okhttp.**
|
||||||
|
|
||||||
|
# Proguard configuration for Jackson 2.x (fasterxml package instead of codehaus package)
|
||||||
|
-keep class com.fasterxml.jackson.databind.ObjectMapper {
|
||||||
|
public <methods>;
|
||||||
|
protected <methods>;
|
||||||
|
}
|
||||||
|
-keep class com.fasterxml.jackson.databind.ObjectWriter {
|
||||||
|
public ** writeValueAsString(**);
|
||||||
|
}
|
||||||
|
-keepnames class com.fasterxml.jackson.** { *; }
|
||||||
|
-dontwarn com.fasterxml.jackson.databind.**
|
||||||
|
|
||||||
|
|
||||||
|
-keep class org.jetbrains.kotlin.** { *; }
|
||||||
|
-keep class org.jetbrains.annotations.** { *; }
|
||||||
|
-keepclassmembers class ** {
|
||||||
|
@org.jetbrains.annotations.ReadOnly public *;
|
||||||
|
}
|
||||||
|
-keepattributes *Annotation*
|
||||||
|
|
||||||
|
-keep class kotlin.** { *; }
|
||||||
|
-keep class org.jetbrains.** { *; }
|
||||||
|
|
||||||
|
# Platform used when running on Java 8 VMs. Will not be used at runtime.
|
||||||
|
-dontwarn retrofit2.Platform$Java8
|
||||||
|
# Retain generic type information for use by reflection by converters and adapters.
|
||||||
|
-keepattributes Signature
|
||||||
|
# Retain declared checked exceptions for use by a Proxy instance.
|
||||||
|
-keepattributes Exceptions
|
||||||
|
-dontwarn javax.annotation.**
|
||||||
|
-dontwarn sun.misc.Unsafe
|
||||||
|
-dontwarn okio.**
|
||||||
|
-keep class de.nicidienase.chaosflix.common.entities.** { *; }
|
||||||
|
#retrofit
|
||||||
|
-dontwarn retrofit2.**
|
||||||
|
-keep class retrofit2.** { *; }
|
||||||
|
-keepattributes *Annotation*,Signature, Exceptions
|
||||||
|
|
||||||
|
-keepclasseswithmembers class * {
|
||||||
|
@retrofit2.http.* <methods>;
|
||||||
|
}
|
||||||
|
#endRetrofit
|
||||||
|
|
||||||
|
-keepclassmembers public class com.cypressworks.kotlinreflectionproguard.** {
|
||||||
|
public *;
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
package de.nicidienase.chaosflix
|
||||||
|
|
||||||
|
import android.arch.lifecycle.ViewModelProviders
|
||||||
|
import android.support.test.InstrumentationRegistry
|
||||||
|
import android.support.test.runner.AndroidJUnit4
|
||||||
|
import android.test.ActivityInstrumentationTestCase2
|
||||||
|
import android.view.View
|
||||||
|
import de.nicidienase.chaosflix.common.entities.userdata.PlaybackProgress
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
import de.nicidienase.chaosflix.touch.ViewModelFactory
|
||||||
|
import io.reactivex.functions.Consumer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by felix on 31.10.17.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class PersistenceTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test1() {
|
||||||
|
val playbackProgressDao = ViewModelFactory.database.playbackProgressDao()
|
||||||
|
playbackProgressDao.saveProgress(PlaybackProgress(23,1337))
|
||||||
|
playbackProgressDao.getProgressForEvent(23)
|
||||||
|
.observeForever { it -> assert(it?.eventId == 23L && it?.progress == 1337L) }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package de.nicidienase.chaosflix.touch
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import com.facebook.stetho.Stetho
|
||||||
|
|
||||||
|
class ChaosflixApplication : Application() {
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
APPLICATION_CONTEXT = this
|
||||||
|
Stetho.initializeWithDefaults(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
lateinit var APPLICATION_CONTEXT: Context
|
||||||
|
}
|
||||||
|
}
|
66
touch/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
package="de.nicidienase.chaosflix"
|
||||||
|
tools:ignore="MissingLeanbackLauncher">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:name=".touch.ChaosflixApplication"
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@drawable/icon_notext"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/AppTheme"
|
||||||
|
tools:replace="android:theme">
|
||||||
|
<activity
|
||||||
|
android:name=".touch.SplashActivity"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:theme="@style/SplashTheme">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<activity android:name=".touch.browse.BrowseActivity">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.app.searchable"
|
||||||
|
android:resource="@xml/searchable"/>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".touch.browse.eventslist.EventsListActivity"
|
||||||
|
android:parentActivityName=".touch.browse.BrowseActivity"/>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".touch.eventdetails.EventDetailsActivity"
|
||||||
|
android:parentActivityName="de.nicidienase.chaosflix.touch.browse.eventslist.EventsListActivity"/>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".touch.about.AboutActivity"
|
||||||
|
android:parentActivityName=".touch.browse.BrowseActivity"/>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".touch.playback.PlayerActivity"
|
||||||
|
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
|
||||||
|
android:launchMode="singleTop"
|
||||||
|
android:parentActivityName=".touch.eventdetails.EventDetailsActivity"/>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".touch.settings.SettingsActivity"
|
||||||
|
android:parentActivityName=".touch.browse.BrowseActivity"/>
|
||||||
|
|
||||||
|
<activity android:name="net.rdrei.android.dirchooser.DirectoryChooserActivity" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".touch.sync.DownloadJobService"
|
||||||
|
android:permission="android.permission.BIND_JOB_SERVICE"/>
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
56
touch/src/main/assets/about.html
Normal file
1
touch/src/main/assets/icon.png
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../../../amazon_icons/icon_small_114x144.png
|
1
touch/src/main/assets/license.txt
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../../../LICENSE
|
|
@ -0,0 +1,29 @@
|
||||||
|
package de.nicidienase.chaosflix.touch
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.animation.Animation
|
||||||
|
import android.view.animation.LinearInterpolator
|
||||||
|
import android.view.animation.RotateAnimation
|
||||||
|
import android.widget.ImageView
|
||||||
|
import de.nicidienase.chaosflix.R
|
||||||
|
|
||||||
|
class ChaosflixLoadingSpinner(context: Context, attributeSet: AttributeSet) : ImageView(context, attributeSet) {
|
||||||
|
init {
|
||||||
|
val typedArray = context.theme.obtainStyledAttributes(attributeSet, R.styleable.ChaosflixLoadingSpinner, 0, 0)
|
||||||
|
val duration = typedArray.getInt(R.styleable.ChaosflixLoadingSpinner_duration, 2000)
|
||||||
|
val clockwise = typedArray.getBoolean(R.styleable.ChaosflixLoadingSpinner_clockwise, true)
|
||||||
|
|
||||||
|
val anim = if (clockwise) {
|
||||||
|
RotateAnimation(0.0f, 360.0f,
|
||||||
|
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f)
|
||||||
|
} else {
|
||||||
|
RotateAnimation(360.0f, 0.0f,
|
||||||
|
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f)
|
||||||
|
}
|
||||||
|
anim.interpolator = LinearInterpolator()
|
||||||
|
anim.duration = duration.toLong()
|
||||||
|
anim.repeatCount = Animation.INFINITE
|
||||||
|
animation = anim
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package de.nicidienase.chaosflix.touch
|
||||||
|
|
||||||
|
import android.arch.persistence.room.Room
|
||||||
|
import android.content.Context
|
||||||
|
import de.nicidienase.chaosflix.common.ChaosflixDatabase
|
||||||
|
|
||||||
|
class DatabaseFactory (context: Context) {
|
||||||
|
val database = Room.databaseBuilder(
|
||||||
|
context.applicationContext,
|
||||||
|
ChaosflixDatabase::class.java, "mediaccc.de")
|
||||||
|
.addMigrations(
|
||||||
|
ChaosflixDatabase.migration_2_3,
|
||||||
|
ChaosflixDatabase.migration_3_4,
|
||||||
|
ChaosflixDatabase.migration_4_5)
|
||||||
|
.fallbackToDestructiveMigration()
|
||||||
|
.build()
|
||||||
|
}
|
|
@ -0,0 +1,205 @@
|
||||||
|
package de.nicidienase.chaosflix.touch
|
||||||
|
|
||||||
|
import android.app.DownloadManager
|
||||||
|
import android.arch.lifecycle.LiveData
|
||||||
|
import android.arch.lifecycle.MutableLiveData
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.database.sqlite.SQLiteConstraintException
|
||||||
|
import android.databinding.ObservableField
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Environment
|
||||||
|
import android.preference.PreferenceManager
|
||||||
|
import android.util.Log
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentEvent
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentRecording
|
||||||
|
import de.nicidienase.chaosflix.common.userdata.entities.download.OfflineEvent
|
||||||
|
import de.nicidienase.chaosflix.common.userdata.entities.download.OfflineEventDao
|
||||||
|
import de.nicidienase.chaosflix.common.util.ThreadHandler
|
||||||
|
import de.nicidienase.chaosflix.touch.eventdetails.DetailsViewModel
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class OfflineItemManager(downloadRefs: List<Long>? = emptyList(),val offlineEventDao: OfflineEventDao) {
|
||||||
|
|
||||||
|
val downloadStatus: MutableMap<Long, DownloadStatus>
|
||||||
|
|
||||||
|
val downloadManager: DownloadManager
|
||||||
|
= ChaosflixApplication.APPLICATION_CONTEXT
|
||||||
|
.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||||
|
|
||||||
|
private val handler = ThreadHandler()
|
||||||
|
|
||||||
|
init {
|
||||||
|
downloadStatus = HashMap()
|
||||||
|
downloadRefs?.map { downloadStatus.put(it, DownloadStatus()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateDownloadStatus() {
|
||||||
|
updateDownloads(downloadStatus.keys.toLongArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateDownloadStatus(offlineEvents: List<OfflineEvent>) {
|
||||||
|
if (offlineEvents.size > 0) {
|
||||||
|
val downloadRef = offlineEvents.map { it.downloadReference }.toTypedArray().toLongArray() ?: longArrayOf()
|
||||||
|
updateDownloads(downloadRef)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateDownloads(downloadRefs: LongArray) {
|
||||||
|
val cursor = downloadManager.query(DownloadManager.Query().setFilterById(*downloadRefs))
|
||||||
|
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
do {
|
||||||
|
val columnId = cursor.getColumnIndex(DownloadManager.COLUMN_ID)
|
||||||
|
val id = cursor.getLong(columnId)
|
||||||
|
val columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)
|
||||||
|
val status = cursor.getInt(columnIndex)
|
||||||
|
val bytesSoFarIndex = cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)
|
||||||
|
val bytesSoFar = cursor.getInt(bytesSoFarIndex)
|
||||||
|
val bytesTotalIndex = cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)
|
||||||
|
val bytesTotal = cursor.getInt(bytesTotalIndex)
|
||||||
|
|
||||||
|
val statusText: String =
|
||||||
|
when (status) {
|
||||||
|
DownloadManager.STATUS_RUNNING -> "Running"
|
||||||
|
DownloadManager.STATUS_FAILED -> "Failed"
|
||||||
|
DownloadManager.STATUS_PAUSED -> "Paused"
|
||||||
|
DownloadManager.STATUS_SUCCESSFUL -> "Successful"
|
||||||
|
DownloadManager.STATUS_PENDING -> "Pending"
|
||||||
|
else -> "UNKNOWN"
|
||||||
|
}
|
||||||
|
if (downloadStatus.containsKey(id)) {
|
||||||
|
val item = downloadStatus[id]
|
||||||
|
item?.statusText?.set(statusText)
|
||||||
|
item?.currentBytes?.set(bytesSoFar)
|
||||||
|
item?.totalBytes?.set(bytesTotal)
|
||||||
|
item?.status = status
|
||||||
|
} else {
|
||||||
|
downloadStatus.put(
|
||||||
|
id,
|
||||||
|
DownloadStatus(statusText, bytesSoFar, bytesTotal, status))
|
||||||
|
}
|
||||||
|
} while (cursor.moveToNext())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun download(event: PersistentEvent, recording: PersistentRecording): LiveData<Boolean> {
|
||||||
|
val result = MutableLiveData<Boolean>()
|
||||||
|
handler.runOnBackgroundThread {
|
||||||
|
|
||||||
|
|
||||||
|
val offlineEvent = offlineEventDao.getByEventGuidSync(event.guid)
|
||||||
|
if (offlineEvent == null) {
|
||||||
|
val downloadManager: DownloadManager
|
||||||
|
= ChaosflixApplication.APPLICATION_CONTEXT
|
||||||
|
.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||||
|
|
||||||
|
val request = DownloadManager.Request(Uri.parse(recording.recordingUrl))
|
||||||
|
request.setTitle(event.title)
|
||||||
|
|
||||||
|
request.setDestinationUri(
|
||||||
|
Uri.withAppendedPath(Uri.fromFile(
|
||||||
|
File(getDownloadDir())), recording.filename))
|
||||||
|
|
||||||
|
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)
|
||||||
|
request.setVisibleInDownloadsUi(true)
|
||||||
|
|
||||||
|
if(!PreferencesManager.getMetered()){
|
||||||
|
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI)
|
||||||
|
request.setAllowedOverMetered(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
val downloadReference = downloadManager.enqueue(request)
|
||||||
|
Log.d(DetailsViewModel.TAG, "download started $downloadReference")
|
||||||
|
|
||||||
|
val cancelHandler = DownloadCancelHandler(downloadReference, offlineEventDao)
|
||||||
|
val intentFilter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
|
||||||
|
ChaosflixApplication.APPLICATION_CONTEXT.registerReceiver(cancelHandler, intentFilter)
|
||||||
|
|
||||||
|
try {
|
||||||
|
offlineEventDao.insert(
|
||||||
|
OfflineEvent(eventGuid = event.guid,
|
||||||
|
recordingId = recording.id,
|
||||||
|
localPath = getDownloadDir() + recording.filename,
|
||||||
|
downloadReference = downloadReference))
|
||||||
|
} catch (ex: SQLiteConstraintException) {
|
||||||
|
Log.d(DetailsViewModel.TAG, ex.message)
|
||||||
|
}
|
||||||
|
result.postValue(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteOfflineItem(downloadId: Long) {
|
||||||
|
val offlineEvent = offlineEventDao.getByDownloadReferenceSync(downloadId)
|
||||||
|
if (offlineEvent != null) {
|
||||||
|
deleteOfflineItem(offlineEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteOfflineItem(item: OfflineEvent) {
|
||||||
|
downloadManager.remove(item.downloadReference)
|
||||||
|
val file = File(item.localPath)
|
||||||
|
if (file.exists()) file.delete()
|
||||||
|
offlineEventDao.deleteById(item.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class DownloadStatus(statusText: String = "",
|
||||||
|
currentBytes: Int = 0,
|
||||||
|
totalBytes: Int = 0,
|
||||||
|
var status: Int = DownloadManager.STATUS_FAILED) {
|
||||||
|
val statusText: ObservableField<String> = ObservableField()
|
||||||
|
val currentBytes: ObservableField<Int> = ObservableField()
|
||||||
|
val totalBytes: ObservableField<Int> = ObservableField()
|
||||||
|
|
||||||
|
init {
|
||||||
|
this.statusText.set(statusText)
|
||||||
|
this.currentBytes.set(currentBytes)
|
||||||
|
this.totalBytes.set(totalBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DownloadCancelHandler(val id: Long, val offlineEventDao: OfflineEventDao) : BroadcastReceiver() {
|
||||||
|
private val TAG = DownloadCancelHandler::class.simpleName
|
||||||
|
|
||||||
|
val handler = ThreadHandler()
|
||||||
|
|
||||||
|
override fun onReceive(p0: Context?, p1: Intent?) {
|
||||||
|
val downloadId = p1?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0);
|
||||||
|
if (downloadId != null && downloadId == id) {
|
||||||
|
val offlineItemManager = OfflineItemManager(listOf(downloadId),offlineEventDao)
|
||||||
|
offlineItemManager.updateDownloadStatus()
|
||||||
|
val downloadStatus = offlineItemManager.downloadStatus[downloadId]
|
||||||
|
if (downloadStatus?.status == DownloadManager.STATUS_FAILED) {
|
||||||
|
Log.d(TAG, "Deleting item")
|
||||||
|
handler.runOnBackgroundThread {
|
||||||
|
offlineItemManager.deleteOfflineItem(downloadId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p0?.unregisterReceiver(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMovieDir(): String {
|
||||||
|
val sharedPref: SharedPreferences = PreferenceManager
|
||||||
|
.getDefaultSharedPreferences(ChaosflixApplication.APPLICATION_CONTEXT);
|
||||||
|
var dir = sharedPref.getString("download_folder", null)
|
||||||
|
if (dir == null) {
|
||||||
|
dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES).path
|
||||||
|
}
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDownloadDir(): String {
|
||||||
|
return getMovieDir() + DOWNLOAD_DIR;
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val DOWNLOAD_DIR = "/chaosflix/"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package de.nicidienase.chaosflix.touch
|
||||||
|
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentEvent
|
||||||
|
|
||||||
|
interface OnEventSelectedListener {
|
||||||
|
fun onEventSelected(event: PersistentEvent);
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package de.nicidienase.chaosflix.touch
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.preference.PreferenceManager
|
||||||
|
|
||||||
|
object PreferencesManager {
|
||||||
|
private val keyMetered = "allow_metered_networks"
|
||||||
|
private val keyAutoselectStream = "auto_select_stream"
|
||||||
|
private val keyAutoselectRecording = "auto_select_recording"
|
||||||
|
|
||||||
|
val sharedPref: SharedPreferences = PreferenceManager
|
||||||
|
.getDefaultSharedPreferences(ChaosflixApplication.APPLICATION_CONTEXT)
|
||||||
|
|
||||||
|
fun getMetered() = sharedPref.getBoolean(keyMetered, false)
|
||||||
|
|
||||||
|
fun getAutoselectStream() = sharedPref.getBoolean(keyAutoselectStream, false)
|
||||||
|
|
||||||
|
fun getAutoselectRecording() = sharedPref.getBoolean(keyAutoselectRecording, false)
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package de.nicidienase.chaosflix.touch
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.v7.app.AppCompatActivity
|
||||||
|
import de.nicidienase.chaosflix.touch.browse.BrowseActivity
|
||||||
|
|
||||||
|
class SplashActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
startActivity(Intent(this, BrowseActivity::class.java))
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package de.nicidienase.chaosflix.touch
|
||||||
|
|
||||||
|
import android.arch.lifecycle.ViewModel
|
||||||
|
import android.arch.lifecycle.ViewModelProvider
|
||||||
|
import android.content.Context
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.network.ApiFactory
|
||||||
|
import de.nicidienase.chaosflix.touch.browse.BrowseViewModel
|
||||||
|
import de.nicidienase.chaosflix.touch.eventdetails.DetailsViewModel
|
||||||
|
import de.nicidienase.chaosflix.touch.playback.PlayerViewModel
|
||||||
|
|
||||||
|
class ViewModelFactory(context: Context) : ViewModelProvider.Factory {
|
||||||
|
|
||||||
|
val apiFactory = ApiFactory(context.resources)
|
||||||
|
|
||||||
|
val database = DatabaseFactory(context).database
|
||||||
|
val recordingApi = apiFactory.recordingApi
|
||||||
|
val streamingApi = apiFactory.streamingApi
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||||
|
if (modelClass.isAssignableFrom(BrowseViewModel::class.java)) {
|
||||||
|
return BrowseViewModel(database, recordingApi, streamingApi) as T
|
||||||
|
|
||||||
|
} else if (modelClass.isAssignableFrom(PlayerViewModel::class.java)) {
|
||||||
|
return PlayerViewModel(database) as T
|
||||||
|
|
||||||
|
} else if (modelClass.isAssignableFrom(DetailsViewModel::class.java)) {
|
||||||
|
return DetailsViewModel(database, recordingApi) as T
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw UnsupportedOperationException("The requested ViewModel is currently unsupported. " +
|
||||||
|
"Please make sure to implement are correct creation of it. " +
|
||||||
|
" Request: ${modelClass.getCanonicalName()}");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
package de.nicidienase.chaosflix.touch.about
|
||||||
|
|
||||||
|
|
||||||
|
import android.databinding.DataBindingUtil
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.v7.app.AppCompatActivity
|
||||||
|
import android.view.View
|
||||||
|
import de.nicidienase.chaosflix.R
|
||||||
|
import de.nicidienase.chaosflix.databinding.ActivityAboutBinding
|
||||||
|
import mehdi.sakout.aboutpage.AboutPage
|
||||||
|
import mehdi.sakout.aboutpage.Element
|
||||||
|
|
||||||
|
class AboutActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
val binding = DataBindingUtil.setContentView<ActivityAboutBinding>(
|
||||||
|
this, R.layout.activity_about)
|
||||||
|
|
||||||
|
binding.toolbarInc?.toolbar?.title = getString(R.string.about_chaosflix)
|
||||||
|
setSupportActionBar(binding.toolbarInc?.toolbar)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
|
||||||
|
val showLibs = Element()
|
||||||
|
showLibs.title = resources.getString(R.string.showLibs)
|
||||||
|
showLibs.onClickListener = object : View.OnClickListener {
|
||||||
|
|
||||||
|
override fun onClick(p0: View?) {
|
||||||
|
LibsFragment().show(supportFragmentManager, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val pInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
|
||||||
|
val version = pInfo.versionName;
|
||||||
|
val aboutView = AboutPage(this)
|
||||||
|
.setImage(R.drawable.icon_notext_144x144)
|
||||||
|
.setDescription(resources.getString(R.string.description))
|
||||||
|
.addItem(Element().setTitle("Version ${version}"))
|
||||||
|
.addWebsite("https://github.com/NiciDieNase/chaosflix/blob/master/LICENSE",
|
||||||
|
getString(R.string.chaosflix_licence))
|
||||||
|
.addWebsite("https://morr.cc/voctocat/",
|
||||||
|
resources.getString(R.string.about_voctocat))
|
||||||
|
.addItem(showLibs)
|
||||||
|
.addGroup("Connect with us")
|
||||||
|
.addGitHub("nicidienase/chaosflix", "Find the source on Github")
|
||||||
|
.addTwitter("nicidienase", "Follow the developer on Twitter")
|
||||||
|
.addPlayStore("de.nicidienase.chaosflix", "Rate us on Google Play")
|
||||||
|
.create()
|
||||||
|
|
||||||
|
binding.container.addView(aboutView)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
package de.nicidienase.chaosflix.touch.about
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.v4.app.DialogFragment
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import com.mikepenz.aboutlibraries.LibsBuilder
|
||||||
|
import com.mikepenz.aboutlibraries.ui.LibsSupportFragment
|
||||||
|
import de.nicidienase.chaosflix.R
|
||||||
|
|
||||||
|
class LibsFragment: DialogFragment(){
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
val layout = inflater.inflate(R.layout.fragment_libs,container, false)
|
||||||
|
childFragmentManager.beginTransaction()
|
||||||
|
.replace(R.id.layout_container,getLibsFragment())
|
||||||
|
.commit()
|
||||||
|
return layout
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLibsFragment(): LibsSupportFragment {
|
||||||
|
val aboutLibs: LibsSupportFragment = LibsBuilder()
|
||||||
|
// .withAboutIconShown(true)
|
||||||
|
// .withAboutVersionShown(true)
|
||||||
|
// .withAboutDescription(resources.getString(R.string.description))
|
||||||
|
.supportFragment()
|
||||||
|
return aboutLibs
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,245 @@
|
||||||
|
package de.nicidienase.chaosflix.touch.browse
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.databinding.DataBindingUtil
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.PersistableBundle
|
||||||
|
import android.support.design.widget.NavigationView
|
||||||
|
import android.support.design.widget.Snackbar
|
||||||
|
import android.support.v4.app.Fragment
|
||||||
|
import android.support.v4.app.FragmentTransaction
|
||||||
|
import android.support.v7.app.ActionBarDrawerToggle
|
||||||
|
import android.support.v7.app.AlertDialog
|
||||||
|
import android.support.v7.app.AppCompatActivity
|
||||||
|
import android.support.v7.widget.Toolbar
|
||||||
|
import android.transition.TransitionInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.Toast
|
||||||
|
import de.nicidienase.chaosflix.R
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentConference
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentEvent
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.entities.streaming.StreamUrl
|
||||||
|
import de.nicidienase.chaosflix.databinding.ActivityBrowseBinding
|
||||||
|
import de.nicidienase.chaosflix.touch.OnEventSelectedListener
|
||||||
|
import de.nicidienase.chaosflix.touch.PreferencesManager
|
||||||
|
import de.nicidienase.chaosflix.touch.about.AboutActivity
|
||||||
|
import de.nicidienase.chaosflix.touch.browse.download.DownloadsListFragment
|
||||||
|
import de.nicidienase.chaosflix.touch.browse.eventslist.EventsListActivity
|
||||||
|
import de.nicidienase.chaosflix.touch.browse.eventslist.EventsListFragment
|
||||||
|
import de.nicidienase.chaosflix.touch.browse.streaming.LivestreamListFragment
|
||||||
|
import de.nicidienase.chaosflix.touch.browse.streaming.StreamingItem
|
||||||
|
import de.nicidienase.chaosflix.touch.eventdetails.EventDetailsActivity
|
||||||
|
import de.nicidienase.chaosflix.touch.playback.PlayerActivity
|
||||||
|
import de.nicidienase.chaosflix.touch.settings.SettingsActivity
|
||||||
|
|
||||||
|
class BrowseActivity : AppCompatActivity(),
|
||||||
|
ConferencesTabBrowseFragment.OnInteractionListener,
|
||||||
|
LivestreamListFragment.InteractionListener,
|
||||||
|
DownloadsListFragment.InteractionListener,
|
||||||
|
OnEventSelectedListener {
|
||||||
|
|
||||||
|
private var drawerOpen: Boolean = false
|
||||||
|
|
||||||
|
private val TAG = BrowseActivity::class.simpleName
|
||||||
|
|
||||||
|
private lateinit var drawerToggle: ActionBarDrawerToggle
|
||||||
|
private lateinit var binding: ActivityBrowseBinding
|
||||||
|
|
||||||
|
protected val numColumns: Int
|
||||||
|
get() = resources.getInteger(R.integer.num_columns)
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = DataBindingUtil.setContentView(this, R.layout.activity_browse)
|
||||||
|
|
||||||
|
val navigationView = findViewById<NavigationView>(R.id.navigation_view)
|
||||||
|
navigationView.setNavigationItemSelectedListener { item ->
|
||||||
|
when (item.itemId) {
|
||||||
|
R.id.nav_recordings -> showConferencesFragment()
|
||||||
|
R.id.nav_bookmarks -> showBookmarksFragment()
|
||||||
|
R.id.nav_inprogress -> showInProgressFragment()
|
||||||
|
R.id.nav_about -> showAboutPage()
|
||||||
|
R.id.nav_streams -> showStreamsFragment()
|
||||||
|
R.id.nav_downloads -> showDownloadsFragment()
|
||||||
|
R.id.nav_preferences -> showSettingsPage()
|
||||||
|
else -> Snackbar.make(binding.drawerLayout, "Not implemented yet", Snackbar.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
binding.drawerLayout.closeDrawers()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
showConferencesFragment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setupDrawerToggle(toolbar: Toolbar?) {
|
||||||
|
if (toolbar != null) {
|
||||||
|
drawerToggle = object : ActionBarDrawerToggle(this, binding.drawerLayout,
|
||||||
|
toolbar, R.string.drawer_open, R.string.drawer_close) {
|
||||||
|
override fun onDrawerOpened(drawerView: View) {
|
||||||
|
super.onDrawerOpened(drawerView)
|
||||||
|
drawerOpen = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDrawerClosed(drawerView: View) {
|
||||||
|
super.onDrawerClosed(drawerView)
|
||||||
|
drawerOpen = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
drawerToggle = object : ActionBarDrawerToggle(this, binding.drawerLayout,
|
||||||
|
R.string.drawer_open, R.string.drawer_close) {
|
||||||
|
override fun onDrawerOpened(drawerView: View) {
|
||||||
|
super.onDrawerOpened(drawerView)
|
||||||
|
drawerOpen = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDrawerClosed(drawerView: View) {
|
||||||
|
super.onDrawerClosed(drawerView)
|
||||||
|
drawerOpen = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.drawerLayout.addDrawerListener(drawerToggle)
|
||||||
|
drawerToggle.syncState()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPostCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
|
||||||
|
super.onPostCreate(savedInstanceState, persistentState)
|
||||||
|
drawerToggle.syncState()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
drawerToggle.onConfigurationChanged(newConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
if (drawerToggle.onOptionsItemSelected(item)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConferenceSelected(conference: PersistentConference) {
|
||||||
|
EventsListActivity.start(this, conference)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStreamSelected(streamingItem: StreamingItem) {
|
||||||
|
val entries = HashMap<String, StreamUrl>()
|
||||||
|
|
||||||
|
val dashStreams = streamingItem.room.streams.filter { it.slug == "dash-native" }
|
||||||
|
if (dashStreams.size > 0
|
||||||
|
&& PreferencesManager.getAutoselectStream()) {
|
||||||
|
playStream(streamingItem.conference.conference,
|
||||||
|
streamingItem.room.display,
|
||||||
|
dashStreams.first().urls["dash"]
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
streamingItem.room.streams.flatMap { stream ->
|
||||||
|
stream.urls.map { entry ->
|
||||||
|
entries.put(stream.slug + " " + entry.key, entry.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val builder = AlertDialog.Builder(this)
|
||||||
|
val strings = entries.keys.sorted().toTypedArray()
|
||||||
|
builder.setTitle("Select Stream")
|
||||||
|
.setItems(strings, { _, i ->
|
||||||
|
Toast.makeText(this, strings[i], Toast.LENGTH_LONG).show()
|
||||||
|
|
||||||
|
playStream(
|
||||||
|
streamingItem.conference.conference,
|
||||||
|
streamingItem.room.display,
|
||||||
|
entries[strings[i]])
|
||||||
|
})
|
||||||
|
builder.create().show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun playStream(conference: String, room: String, streamUrl: StreamUrl?) {
|
||||||
|
if (streamUrl != null) {
|
||||||
|
PlayerActivity.launch(this, conference, room, streamUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showConferencesFragment() {
|
||||||
|
showFragment(ConferencesTabBrowseFragment.newInstance(numColumns), "conferences")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showBookmarksFragment() {
|
||||||
|
val bookmarksFragment = EventsListFragment.newInstance(EventsListFragment.TYPE_BOOKMARKS, null, numColumns)
|
||||||
|
showFragment(bookmarksFragment, "bookmarks")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showInProgressFragment() {
|
||||||
|
val progressEventsFragment = EventsListFragment.newInstance(EventsListFragment.TYPE_IN_PROGRESS, null, numColumns)
|
||||||
|
showFragment(progressEventsFragment, "in_progress")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showStreamsFragment() {
|
||||||
|
val fragment = LivestreamListFragment.newInstance(numColumns)
|
||||||
|
showFragment(fragment, "streams")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showDownloadsFragment() {
|
||||||
|
val fragment = DownloadsListFragment.getInstance(numColumns)
|
||||||
|
showFragment(fragment, "downloads")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showSettingsPage() {
|
||||||
|
val intent = Intent(this, SettingsActivity::class.java)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showAboutPage() {
|
||||||
|
val intent = Intent(this, AboutActivity::class.java)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
if (drawerOpen) {
|
||||||
|
binding.drawerLayout.closeDrawers()
|
||||||
|
} else {
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun showFragment(fragment: Fragment, tag: String) {
|
||||||
|
val fm = supportFragmentManager
|
||||||
|
val oldFragment = fm.findFragmentById(R.id.fragment_container)
|
||||||
|
|
||||||
|
val transitionInflater = TransitionInflater.from(this)
|
||||||
|
if (oldFragment != null) {
|
||||||
|
if (oldFragment.tag.equals(tag)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
oldFragment.exitTransition = transitionInflater.inflateTransition(android.R.transition.fade)
|
||||||
|
}
|
||||||
|
fragment.enterTransition = transitionInflater.inflateTransition(android.R.transition.fade)
|
||||||
|
|
||||||
|
// val slideTransition = Slide(Gravity.RIGHT)
|
||||||
|
// fragment.enterTransition = slideTransition
|
||||||
|
|
||||||
|
val ft = fm.beginTransaction()
|
||||||
|
ft.replace(R.id.fragment_container, fragment, tag)
|
||||||
|
ft.setReorderingAllowed(true)
|
||||||
|
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
|
||||||
|
ft.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEventSelected(event: PersistentEvent) {
|
||||||
|
EventDetailsActivity.launch(this, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun launch(context: Context){
|
||||||
|
context.startActivity(Intent(context,BrowseActivity::class.java))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package de.nicidienase.chaosflix.touch.browse
|
||||||
|
|
||||||
|
import android.arch.lifecycle.ViewModelProviders
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.v4.app.Fragment
|
||||||
|
import android.support.v7.app.AppCompatActivity
|
||||||
|
import android.support.v7.widget.Toolbar
|
||||||
|
import android.view.View
|
||||||
|
import de.nicidienase.chaosflix.touch.ViewModelFactory
|
||||||
|
|
||||||
|
open class BrowseFragment : Fragment() {
|
||||||
|
|
||||||
|
lateinit var viewModel: BrowseViewModel
|
||||||
|
var overlay: View? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
viewModel = ViewModelProviders.of(activity!!, ViewModelFactory(requireContext())).get(BrowseViewModel::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmOverloads
|
||||||
|
protected fun setupToolbar(toolbar: Toolbar, title: Int, isRoot: Boolean = true) {
|
||||||
|
setupToolbar(toolbar, resources.getString(title), isRoot)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmOverloads
|
||||||
|
protected fun setupToolbar(toolbar: Toolbar, title: String, isRoot: Boolean = true) {
|
||||||
|
val activity = activity as AppCompatActivity
|
||||||
|
if (activity is BrowseActivity) {
|
||||||
|
activity.setupDrawerToggle(toolbar)
|
||||||
|
}
|
||||||
|
activity.setSupportActionBar(toolbar)
|
||||||
|
activity.supportActionBar?.setTitle(title)
|
||||||
|
if (isRoot) {
|
||||||
|
activity.supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
} else {
|
||||||
|
activity.supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun setLoadingOverlayVisibility(visible: Boolean) {
|
||||||
|
if (visible) {
|
||||||
|
overlay?.setVisibility(View.VISIBLE)
|
||||||
|
} else {
|
||||||
|
overlay?.setVisibility(View.INVISIBLE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,122 @@
|
||||||
|
package de.nicidienase.chaosflix.touch.browse
|
||||||
|
|
||||||
|
class BrowseViewModel(
|
||||||
|
val database: ChaosflixDatabase,
|
||||||
|
recordingApi: RecordingService,
|
||||||
|
val streamingApi: StreamingService
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
val downloader = Downloader(recordingApi, database)
|
||||||
|
lateinit var offlineItemManager: OfflineItemManager
|
||||||
|
private val handler = ThreadHandler()
|
||||||
|
|
||||||
|
init {
|
||||||
|
handler.runOnBackgroundThread {
|
||||||
|
val downloadRefs =
|
||||||
|
database
|
||||||
|
.offlineEventDao()
|
||||||
|
.getAllSync()
|
||||||
|
.map { it.downloadReference }
|
||||||
|
offlineItemManager = OfflineItemManager(downloadRefs, database.offlineEventDao())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getConferenceGroups(): LiveData<List<ConferenceGroup>> {
|
||||||
|
downloader.updateConferencesAndGroups()
|
||||||
|
return database.conferenceGroupDao().getAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getConference(conferenceId: Long)
|
||||||
|
= database.conferenceDao().findConferenceById(conferenceId)
|
||||||
|
|
||||||
|
fun getConferencesByGroup(groupId: Long)
|
||||||
|
= database.conferenceDao().findConferenceByGroup(groupId)
|
||||||
|
|
||||||
|
fun getEventsforConference(conference: PersistentConference)
|
||||||
|
= database.eventDao().findEventsByConference(conference.id)
|
||||||
|
|
||||||
|
fun updateConferences()
|
||||||
|
= downloader.updateConferencesAndGroups()
|
||||||
|
|
||||||
|
fun updateEventsForConference(conference: PersistentConference)
|
||||||
|
= downloader.updateEventsForConference(conference)
|
||||||
|
|
||||||
|
fun getBookmarkedEvents(){
|
||||||
|
// database.
|
||||||
|
// = database.eventDao().findBookmarkedEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getInProgressEvents()
|
||||||
|
= database.eventDao().findInProgressEvents()
|
||||||
|
|
||||||
|
private val TAG = BrowseViewModel::class.simpleName
|
||||||
|
|
||||||
|
fun getLivestreams(): LiveData<List<LiveConference>> {
|
||||||
|
val result = MutableLiveData<List<LiveConference>>()
|
||||||
|
handler.runOnBackgroundThread {
|
||||||
|
val conferences = streamingApi.getStreamingConferences().execute()
|
||||||
|
if(!conferences.isSuccessful){
|
||||||
|
result.postValue(emptyList())
|
||||||
|
return@runOnBackgroundThread
|
||||||
|
}
|
||||||
|
result.postValue(conferences.body())
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getOfflineEvents(): LiveData<List<Pair<OfflineEvent,PersistentEvent>>> {
|
||||||
|
val result = MutableLiveData<List<Pair<OfflineEvent, PersistentEvent>>>()
|
||||||
|
handler.runOnBackgroundThread {
|
||||||
|
val offlineEventMap = database.offlineEventDao().getAllSync()
|
||||||
|
.map { it.eventGuid to it }.toMap()
|
||||||
|
val persistentEventMap = database.eventDao().findEventsByGUIDsSync(offlineEventMap.keys.toList())
|
||||||
|
.map { it.guid to it }.toMap()
|
||||||
|
|
||||||
|
val resultList = ArrayList<Pair<OfflineEvent, PersistentEvent>>()
|
||||||
|
for (key in offlineEventMap.keys){
|
||||||
|
val offlineEvent = offlineEventMap[key]
|
||||||
|
var persistentEvent: PersistentEvent? = persistentEventMap[key]
|
||||||
|
if(persistentEvent == null){
|
||||||
|
persistentEvent = downloader.updateSingleEvent(key)
|
||||||
|
}
|
||||||
|
if(persistentEvent != null && offlineEvent != null){
|
||||||
|
resultList.add(Pair(offlineEvent, persistentEvent))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.postValue(resultList)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEventById(eventId: Long) = database.eventDao().findEventById(eventId)
|
||||||
|
|
||||||
|
fun getRecordingByid(recordingId: Long) = database.recordingDao().findRecordingById(recordingId)
|
||||||
|
|
||||||
|
fun updateDownloadStatus() {
|
||||||
|
handler.runOnBackgroundThread {
|
||||||
|
offlineItemManager.updateDownloadStatus(database.offlineEventDao().getAllSync())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteOfflineItem(item: OfflineEvent) {
|
||||||
|
handler.runOnBackgroundThread {
|
||||||
|
offlineItemManager.deleteOfflineItem(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
import android.arch.lifecycle.LiveData
|
||||||
|
import android.arch.lifecycle.MutableLiveData
|
||||||
|
import android.arch.lifecycle.ViewModel
|
||||||
|
import de.nicidienase.chaosflix.common.ChaosflixDatabase
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.ConferenceGroup
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentConference
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentEvent
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.entities.streaming.LiveConference
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.network.RecordingService
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.network.StreamingService
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.sync.Downloader
|
||||||
|
import de.nicidienase.chaosflix.common.userdata.entities.download.OfflineEvent
|
||||||
|
import de.nicidienase.chaosflix.common.util.ThreadHandler
|
||||||
|
import de.nicidienase.chaosflix.touch.OfflineItemManager
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,118 @@
|
||||||
|
package de.nicidienase.chaosflix.touch.browse;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
import android.support.v7.widget.GridLayoutManager;
|
||||||
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import de.nicidienase.chaosflix.R;
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.ConferenceGroup;
|
||||||
|
import de.nicidienase.chaosflix.touch.browse.adapters.ConferenceRecyclerViewAdapter;
|
||||||
|
|
||||||
|
public class ConferenceGroupFragment extends BrowseFragment {
|
||||||
|
|
||||||
|
private static final String TAG = ConferenceGroupFragment.class.getSimpleName();
|
||||||
|
|
||||||
|
private static final String ARG_COLUMN_COUNT = "column-count";
|
||||||
|
private static final String ARG_GROUP = "group-name";
|
||||||
|
private static final String LAYOUTMANAGER_STATE = "layoutmanager-state";
|
||||||
|
private ConferencesTabBrowseFragment.OnInteractionListener listener;
|
||||||
|
|
||||||
|
private int columnCount = 1;
|
||||||
|
private ConferenceGroup conferenceGroup;
|
||||||
|
|
||||||
|
private ConferenceRecyclerViewAdapter conferencesAdapter;
|
||||||
|
|
||||||
|
private RecyclerView.LayoutManager layoutManager;
|
||||||
|
|
||||||
|
public ConferenceGroupFragment() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ConferenceGroupFragment newInstance(ConferenceGroup group, int columnCount) {
|
||||||
|
ConferenceGroupFragment fragment = new ConferenceGroupFragment();
|
||||||
|
Bundle args = new Bundle();
|
||||||
|
args.putInt(ARG_COLUMN_COUNT, columnCount);
|
||||||
|
args.putParcelable(ARG_GROUP, group);
|
||||||
|
fragment.setArguments(args);
|
||||||
|
return fragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
if (getArguments() != null) {
|
||||||
|
columnCount = getArguments().getInt(ARG_COLUMN_COUNT);
|
||||||
|
conferenceGroup = getArguments().getParcelable(ARG_GROUP);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
|
View view = inflater.inflate(R.layout.fragment_conferences_page, container, false);
|
||||||
|
|
||||||
|
if (view instanceof RecyclerView) {
|
||||||
|
Context context = view.getContext();
|
||||||
|
RecyclerView recyclerView = (RecyclerView) view;
|
||||||
|
if (columnCount <= 1) {
|
||||||
|
layoutManager = new LinearLayoutManager(context);
|
||||||
|
} else {
|
||||||
|
layoutManager = new GridLayoutManager(context, columnCount);
|
||||||
|
}
|
||||||
|
recyclerView.setLayoutManager(layoutManager);
|
||||||
|
|
||||||
|
conferencesAdapter = new ConferenceRecyclerViewAdapter(listener);
|
||||||
|
recyclerView.setAdapter(conferencesAdapter);
|
||||||
|
getViewModel().getConferencesByGroup(conferenceGroup.getId()).observe(this, conferenceList -> {
|
||||||
|
if(conferenceList != null){
|
||||||
|
if(conferenceList.size() > 0){
|
||||||
|
setLoadingOverlayVisibility(false);
|
||||||
|
}
|
||||||
|
conferencesAdapter.setItems(conferenceList);
|
||||||
|
Parcelable layoutState = getArguments().getParcelable(LAYOUTMANAGER_STATE);
|
||||||
|
if (layoutState != null) {
|
||||||
|
layoutManager.onRestoreInstanceState(layoutState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAttach(Context context) {
|
||||||
|
super.onAttach(context);
|
||||||
|
if (context instanceof ConferencesTabBrowseFragment.OnInteractionListener) {
|
||||||
|
listener = (ConferencesTabBrowseFragment.OnInteractionListener) context;
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException(context.toString() + " must implement OnListFragmentInteractionListener");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSaveInstanceState(Bundle outState) {
|
||||||
|
super.onSaveInstanceState(outState);
|
||||||
|
if(layoutManager != null){
|
||||||
|
outState.putParcelable(LAYOUTMANAGER_STATE, layoutManager.onSaveInstanceState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
if(layoutManager != null){
|
||||||
|
getArguments().putParcelable(LAYOUTMANAGER_STATE, layoutManager.onSaveInstanceState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDetach() {
|
||||||
|
super.onDetach();
|
||||||
|
listener = null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,124 @@
|
||||||
|
package de.nicidienase.chaosflix.touch.browse;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.design.widget.Snackbar;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import de.nicidienase.chaosflix.R;
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentConference;
|
||||||
|
import de.nicidienase.chaosflix.databinding.FragmentTabPagerLayoutBinding;
|
||||||
|
import de.nicidienase.chaosflix.touch.browse.adapters.ConferenceGroupsFragmentPager;
|
||||||
|
|
||||||
|
|
||||||
|
public class ConferencesTabBrowseFragment extends BrowseFragment {
|
||||||
|
|
||||||
|
private static final String TAG = ConferencesTabBrowseFragment.class.getSimpleName();
|
||||||
|
|
||||||
|
private static final String ARG_COLUMN_COUNT = "column-count";
|
||||||
|
private static final String CURRENTTAB_KEY = "current_tab";
|
||||||
|
private static final String VIEWPAGER_STATE = "viewpager_state";
|
||||||
|
private int mColumnCount = 1;
|
||||||
|
private OnInteractionListener listener;
|
||||||
|
private FragmentTabPagerLayoutBinding binding;
|
||||||
|
private Snackbar snackbar;
|
||||||
|
|
||||||
|
public static ConferencesTabBrowseFragment newInstance(int columnCount) {
|
||||||
|
ConferencesTabBrowseFragment fragment = new ConferencesTabBrowseFragment();
|
||||||
|
Bundle args = new Bundle();
|
||||||
|
args.putInt(ARG_COLUMN_COUNT, columnCount);
|
||||||
|
fragment.setArguments(args);
|
||||||
|
return fragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAttach(Context context) {
|
||||||
|
super.onAttach(context);
|
||||||
|
if (context instanceof OnInteractionListener) {
|
||||||
|
listener = (OnInteractionListener) context;
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException(context.toString() + " must implement OnInteractionListener");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
Log.d(TAG, "onCreate");
|
||||||
|
if (getArguments() != null) {
|
||||||
|
mColumnCount = getArguments().getInt(ARG_COLUMN_COUNT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
|
binding = FragmentTabPagerLayoutBinding.inflate(inflater, container, false);
|
||||||
|
|
||||||
|
setupToolbar(binding.incToolbar.toolbar, R.string.app_name);
|
||||||
|
setOverlay(binding.incOverlay.loadingOverlay);
|
||||||
|
|
||||||
|
getViewModel().getConferenceGroups().observe(this, conferenceGroups -> {
|
||||||
|
ConferenceGroupsFragmentPager fragmentPager = new ConferenceGroupsFragmentPager(this.getContext(), getChildFragmentManager());
|
||||||
|
fragmentPager.setContent(conferenceGroups);
|
||||||
|
binding.viewpager.setAdapter(fragmentPager);
|
||||||
|
binding.viewpager.onRestoreInstanceState(getArguments().getParcelable(VIEWPAGER_STATE));
|
||||||
|
|
||||||
|
binding.slidingTabs.setupWithViewPager(binding.viewpager);
|
||||||
|
if (conferenceGroups.size() > 0) {
|
||||||
|
setLoadingOverlayVisibility(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
getViewModel().updateConferences().observe(this, state -> {
|
||||||
|
if(state == null){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (state.getState()){
|
||||||
|
case RUNNING:
|
||||||
|
setLoadingOverlayVisibility(true);
|
||||||
|
break;
|
||||||
|
case DONE:
|
||||||
|
setLoadingOverlayVisibility(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(state.getError() != null){
|
||||||
|
showSnackbar(state.getError());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return binding.getRoot();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void showSnackbar(String message) {
|
||||||
|
View view1 = getView();
|
||||||
|
if(snackbar!= null){
|
||||||
|
snackbar.dismiss();
|
||||||
|
}
|
||||||
|
if(view1 != null){
|
||||||
|
snackbar = Snackbar.make(view1, message, Snackbar.LENGTH_LONG);
|
||||||
|
snackbar.setAction("Okay", view -> snackbar.dismiss());
|
||||||
|
snackbar.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
getArguments().putParcelable(VIEWPAGER_STATE, binding.viewpager.onSaveInstanceState());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDetach() {
|
||||||
|
super.onDetach();
|
||||||
|
listener = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface OnInteractionListener {
|
||||||
|
void onConferenceSelected(PersistentConference conference);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
package de.nicidienase.chaosflix.touch.browse.adapters;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.support.v4.app.Fragment;
|
||||||
|
import android.support.v4.app.FragmentManager;
|
||||||
|
import android.support.v4.app.FragmentPagerAdapter;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import de.nicidienase.chaosflix.R;
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.ConferenceGroup;
|
||||||
|
import de.nicidienase.chaosflix.touch.browse.ConferenceGroupFragment;
|
||||||
|
|
||||||
|
public class ConferenceGroupsFragmentPager extends FragmentPagerAdapter {
|
||||||
|
|
||||||
|
private static final String TAG = ConferenceGroupsFragmentPager.class.getSimpleName();
|
||||||
|
private final Context mContext;
|
||||||
|
private List<ConferenceGroup> conferenceGroupList = new ArrayList<>();
|
||||||
|
|
||||||
|
public ConferenceGroupsFragmentPager(Context context, FragmentManager fm) {
|
||||||
|
super(fm);
|
||||||
|
this.mContext = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Fragment getItem(int position) {
|
||||||
|
ConferenceGroup conferenceGroup = conferenceGroupList.get(position);
|
||||||
|
ConferenceGroupFragment conferenceFragment = ConferenceGroupFragment.newInstance(conferenceGroup, getNumColumns());
|
||||||
|
Log.d(TAG, "Created Fragment for: " + conferenceGroup.getName());
|
||||||
|
return conferenceFragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getItemId(int position) {
|
||||||
|
return conferenceGroupList.get(position).getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCount() {
|
||||||
|
return conferenceGroupList.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CharSequence getPageTitle(int position) {
|
||||||
|
return conferenceGroupList.get(position).getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getNumColumns() {
|
||||||
|
return mContext.getResources().getInteger(R.integer.num_columns);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addGroup(ConferenceGroup conferenceGroup) {
|
||||||
|
conferenceGroupList.add(conferenceGroup);
|
||||||
|
// Collections.sort(conferenceGroupList,(g1, g2) -> g1.getIndex()-g2.getIndex());
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContent(List<ConferenceGroup> conferenceGroups) {
|
||||||
|
conferenceGroupList = conferenceGroups;
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package de.nicidienase.chaosflix.touch.browse.adapters
|
||||||
|
|
||||||
|
import com.squareup.picasso.Picasso
|
||||||
|
import de.nicidienase.chaosflix.R
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentConference
|
||||||
|
import de.nicidienase.chaosflix.touch.browse.ConferencesTabBrowseFragment
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class ConferenceRecyclerViewAdapter(private val mListener: ConferencesTabBrowseFragment.OnInteractionListener?) : ItemRecyclerViewAdapter<PersistentConference>() {
|
||||||
|
override fun getFilteredProperties(item: PersistentConference): List<String> {
|
||||||
|
return listOf(item.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val layout = R.layout.item_conference_cardview
|
||||||
|
|
||||||
|
override fun getComparator(): Comparator<in PersistentConference>? {
|
||||||
|
// return Comparator { o1, o2 -> o1.acronym.compareTo(o2.acronym) * -1 }
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ItemRecyclerViewAdapter<PersistentConference>.ViewHolder, position: Int) {
|
||||||
|
holder.titleText.setText(items[position].title)
|
||||||
|
holder.subtitle.setText(items[position].acronym)
|
||||||
|
Picasso.with(holder.icon.context)
|
||||||
|
.load(items[position].logoUrl)
|
||||||
|
.fit()
|
||||||
|
.centerInside()
|
||||||
|
.into(holder.icon)
|
||||||
|
|
||||||
|
holder.mView.setOnClickListener { _ ->
|
||||||
|
mListener?.onConferenceSelected((items[position]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
package de.nicidienase.chaosflix.touch.browse.adapters
|
||||||
|
|
||||||
|
import android.support.v4.view.ViewCompat
|
||||||
|
import android.view.View
|
||||||
|
import com.squareup.picasso.Picasso
|
||||||
|
import de.nicidienase.chaosflix.R
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentEvent
|
||||||
|
import de.nicidienase.chaosflix.touch.OnEventSelectedListener
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
open class EventRecyclerViewAdapter(val listener: OnEventSelectedListener) :
|
||||||
|
ItemRecyclerViewAdapter<PersistentEvent>() {
|
||||||
|
|
||||||
|
override fun getComparator(): Comparator<in PersistentEvent>? {
|
||||||
|
return Comparator { o1, o2 -> o1.title.compareTo(o2.title) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFilteredProperties(item: PersistentEvent): List<String> {
|
||||||
|
return listOf(item.title,
|
||||||
|
item.subtitle,
|
||||||
|
item.description,
|
||||||
|
item.getSpeakerString()
|
||||||
|
).filterNotNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override val layout = R.layout.item_event_cardview
|
||||||
|
var showTags: Boolean = false
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ItemRecyclerViewAdapter<PersistentEvent>.ViewHolder, position: Int) {
|
||||||
|
val event = items[position]
|
||||||
|
|
||||||
|
holder.titleText.text = event.title
|
||||||
|
holder.subtitle.text = event.subtitle
|
||||||
|
if (showTags) {
|
||||||
|
val tagString = StringBuilder()
|
||||||
|
for (tag in event.tags!!) {
|
||||||
|
if (tagString.length > 0) {
|
||||||
|
tagString.append(", ")
|
||||||
|
}
|
||||||
|
tagString.append(tag)
|
||||||
|
}
|
||||||
|
holder.tag.text = tagString
|
||||||
|
}
|
||||||
|
Picasso.with(holder.icon.context)
|
||||||
|
.load(event.thumbUrl)
|
||||||
|
.noFade()
|
||||||
|
.fit()
|
||||||
|
.centerInside()
|
||||||
|
.into(holder.icon)
|
||||||
|
|
||||||
|
val resources = holder.titleText.context.getResources()
|
||||||
|
ViewCompat.setTransitionName(holder.titleText,
|
||||||
|
resources.getString(R.string.title) + event.id)
|
||||||
|
ViewCompat.setTransitionName(holder.subtitle,
|
||||||
|
resources.getString(R.string.subtitle) + event.id)
|
||||||
|
ViewCompat.setTransitionName(holder.icon,
|
||||||
|
resources.getString(R.string.thumbnail) + event.id)
|
||||||
|
|
||||||
|
holder.mView.setOnClickListener({ _: View -> listener.onEventSelected(items[position]) })
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
package de.nicidienase.chaosflix.touch.browse.adapters
|
||||||
|
|
||||||
|
import android.support.v7.widget.RecyclerView
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Filter
|
||||||
|
import android.widget.Filterable
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import de.nicidienase.chaosflix.R
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
abstract class ItemRecyclerViewAdapter<T>()
|
||||||
|
: RecyclerView.Adapter<ItemRecyclerViewAdapter<T>.ViewHolder>(), Filterable {
|
||||||
|
|
||||||
|
internal abstract val layout: Int
|
||||||
|
|
||||||
|
abstract fun getComparator(): Comparator<in T>?
|
||||||
|
|
||||||
|
internal abstract fun getFilteredProperties(item: T): List<String>
|
||||||
|
|
||||||
|
private val _filter by lazy { ItemFilter() }
|
||||||
|
|
||||||
|
override fun getFilter(): Filter {
|
||||||
|
return _filter
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _items: MutableList<T> = ArrayList<T>()
|
||||||
|
|
||||||
|
private var filteredItems: MutableList<T> = _items
|
||||||
|
|
||||||
|
var items: MutableList<T>
|
||||||
|
get() = filteredItems
|
||||||
|
set(value) {
|
||||||
|
_items = value
|
||||||
|
if (getComparator() != null) {
|
||||||
|
Collections.sort(_items, getComparator())
|
||||||
|
}
|
||||||
|
filteredItems = _items
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addItem(item: T) {
|
||||||
|
if (items.contains(item)) {
|
||||||
|
val index = items.indexOf(item)
|
||||||
|
items[index] = item
|
||||||
|
notifyItemChanged(index)
|
||||||
|
} else {
|
||||||
|
items.add(item)
|
||||||
|
notifyItemInserted(items.size - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val view = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(layout, parent, false)
|
||||||
|
return ViewHolder(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return filteredItems.size
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class ViewHolder(val mView: View) : RecyclerView.ViewHolder(mView) {
|
||||||
|
val icon: ImageView = mView.findViewById<View>(R.id.imageView) as ImageView
|
||||||
|
val titleText: TextView = mView.findViewById<View>(R.id.title_text) as TextView
|
||||||
|
val subtitle: TextView = mView.findViewById<View>(R.id.subtitle_text) as TextView
|
||||||
|
val tag: TextView = mView.findViewById<View>(R.id.tag_text) as TextView
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class ItemFilter : Filter() {
|
||||||
|
override fun performFiltering(filterText: CharSequence?): FilterResults {
|
||||||
|
val filterResults = FilterResults()
|
||||||
|
filterText?.let { text: CharSequence ->
|
||||||
|
if (text.length > 0) {
|
||||||
|
val list = _items.filter { getFilteredProperties(it).any { it.contains(text, true) } }
|
||||||
|
filterResults.values = list
|
||||||
|
filterResults.count = list.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filterResults
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun publishResults(filterText: CharSequence?, filterResults: FilterResults?) {
|
||||||
|
if (filterResults?.values != null) {
|
||||||
|
filteredItems = filterResults.values as MutableList<T>
|
||||||
|
} else {
|
||||||
|
filteredItems = _items
|
||||||
|
}
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
package de.nicidienase.chaosflix.touch.browse.download
|
||||||
|
|
||||||
|
import android.arch.lifecycle.Observer
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.support.v7.widget.GridLayoutManager
|
||||||
|
import android.support.v7.widget.LinearLayoutManager
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import de.nicidienase.chaosflix.R
|
||||||
|
import de.nicidienase.chaosflix.common.userdata.entities.download.OfflineEvent
|
||||||
|
import de.nicidienase.chaosflix.databinding.FragmentDownloadsBinding
|
||||||
|
import de.nicidienase.chaosflix.touch.OnEventSelectedListener
|
||||||
|
import de.nicidienase.chaosflix.touch.browse.BrowseFragment
|
||||||
|
|
||||||
|
class DownloadsListFragment : BrowseFragment() {
|
||||||
|
|
||||||
|
private lateinit var listener: InteractionListener
|
||||||
|
private lateinit var binding: FragmentDownloadsBinding
|
||||||
|
|
||||||
|
private val handler = Handler()
|
||||||
|
|
||||||
|
private val UPDATE_DELAY = 700L
|
||||||
|
private var columnCount = 1;
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
columnCount = arguments?.getInt(ARG_COLUMN_COUNT) ?: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttach(context: Context?) {
|
||||||
|
super.onAttach(context)
|
||||||
|
if (context is InteractionListener) {
|
||||||
|
listener = context
|
||||||
|
} else {
|
||||||
|
throw RuntimeException(context.toString() + " must implement LivestreamListFragment.InteractionListener")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
binding = FragmentDownloadsBinding.inflate(inflater, container, false)
|
||||||
|
setupToolbar(binding.incToolbar?.toolbar!!, R.string.downloads)
|
||||||
|
overlay = binding.incOverlay?.loadingOverlay
|
||||||
|
val offlineEventAdapter = OfflineEventAdapter(emptyList(), viewModel, listener)
|
||||||
|
binding.list.adapter = offlineEventAdapter
|
||||||
|
if (columnCount <= 1) {
|
||||||
|
binding.list.layoutManager = LinearLayoutManager(context)
|
||||||
|
} else {
|
||||||
|
binding.list.layoutManager = GridLayoutManager(context, columnCount - 1)
|
||||||
|
}
|
||||||
|
viewModel.getOfflineEvents().observe(this, Observer { events ->
|
||||||
|
if(events != null){
|
||||||
|
offlineEventAdapter.items = events
|
||||||
|
offlineEventAdapter.notifyDataSetChanged()
|
||||||
|
setLoadingOverlayVisibility(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
private var updateRunnable: Runnable? = null
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
updateRunnable = object: Runnable {
|
||||||
|
override fun run() {
|
||||||
|
viewModel.updateDownloadStatus()
|
||||||
|
handler.postDelayed(this, UPDATE_DELAY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handler.post(updateRunnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
handler.removeCallbacks(updateRunnable)
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
setLoadingOverlayVisibility(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val ARG_COLUMN_COUNT = "column_count"
|
||||||
|
|
||||||
|
fun getInstance(columnCount: Int = 1): DownloadsListFragment{
|
||||||
|
val fragment = DownloadsListFragment()
|
||||||
|
val args = Bundle()
|
||||||
|
args.putInt(ARG_COLUMN_COUNT, columnCount)
|
||||||
|
fragment.arguments = args
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InteractionListener: OnEventSelectedListener {
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
package de.nicidienase.chaosflix.touch.browse.download
|
||||||
|
|
||||||
|
import android.databinding.DataBindingUtil
|
||||||
|
import android.support.v7.widget.RecyclerView
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import com.squareup.picasso.Picasso
|
||||||
|
import de.nicidienase.chaosflix.R
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentEvent
|
||||||
|
import de.nicidienase.chaosflix.common.userdata.entities.download.OfflineEvent
|
||||||
|
import de.nicidienase.chaosflix.databinding.ItemOfflineEventBinding
|
||||||
|
import de.nicidienase.chaosflix.touch.OnEventSelectedListener
|
||||||
|
import de.nicidienase.chaosflix.touch.browse.BrowseViewModel
|
||||||
|
|
||||||
|
class OfflineEventAdapter(var items: List<Pair<OfflineEvent, PersistentEvent>>, val viewModel: BrowseViewModel, val listener: OnEventSelectedListener) :
|
||||||
|
RecyclerView.Adapter<OfflineEventAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: OfflineEventAdapter.ViewHolder, position: Int) {
|
||||||
|
val item = items[position]
|
||||||
|
|
||||||
|
holder.binding.event = item.second
|
||||||
|
Picasso.with(holder.thumbnail.context)
|
||||||
|
.load(item.second.thumbUrl)
|
||||||
|
.noFade()
|
||||||
|
.fit()
|
||||||
|
.centerInside()
|
||||||
|
.into(holder.thumbnail)
|
||||||
|
|
||||||
|
|
||||||
|
with(holder.binding){
|
||||||
|
downloadStatus = viewModel.offlineItemManager.downloadStatus[item.first.downloadReference]
|
||||||
|
buttonDelete.setOnClickListener {
|
||||||
|
viewModel.deleteOfflineItem(item.first)
|
||||||
|
}
|
||||||
|
content?.setOnClickListener { view ->
|
||||||
|
listener.onEventSelected(item.second)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return items.size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val binding = DataBindingUtil.inflate<ItemOfflineEventBinding>(
|
||||||
|
LayoutInflater.from(parent.context), R.layout.item_offline_event, parent, false)
|
||||||
|
return ViewHolder(binding, binding.root)
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class ViewHolder(val binding: ItemOfflineEventBinding, val view: View) : RecyclerView.ViewHolder(view) {
|
||||||
|
val thumbnail = binding.imageView
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
package de.nicidienase.chaosflix.touch.browse.eventslist
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.v7.app.AppCompatActivity
|
||||||
|
import de.nicidienase.chaosflix.R
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentConference
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentEvent
|
||||||
|
import de.nicidienase.chaosflix.touch.OnEventSelectedListener
|
||||||
|
import de.nicidienase.chaosflix.touch.eventdetails.EventDetailsActivity
|
||||||
|
|
||||||
|
class EventsListActivity : AppCompatActivity(), OnEventSelectedListener {
|
||||||
|
|
||||||
|
protected val numColumns: Int
|
||||||
|
get() = resources.getInteger(R.integer.num_columns)
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_events_list)
|
||||||
|
|
||||||
|
val conference = intent.getParcelableExtra<PersistentConference>(CONFERENCE_KEY)
|
||||||
|
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
val eventsListFragment = EventsListFragment.newInstance(EventsListFragment.TYPE_EVENTS, conference, numColumns)
|
||||||
|
|
||||||
|
supportFragmentManager.beginTransaction()
|
||||||
|
.replace(R.id.fragment_container, eventsListFragment)
|
||||||
|
.commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEventSelected(event: PersistentEvent) {
|
||||||
|
EventDetailsActivity.launch(this, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val CONFERENCE_KEY = "conference_id"
|
||||||
|
|
||||||
|
fun start(context: Context, conference: PersistentConference) {
|
||||||
|
val i = Intent(context, EventsListActivity::class.java)
|
||||||
|
i.putExtra(CONFERENCE_KEY, conference)
|
||||||
|
context.startActivity(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,198 @@
|
||||||
|
package de.nicidienase.chaosflix.touch.browse.eventslist;
|
||||||
|
|
||||||
|
import android.app.SearchManager;
|
||||||
|
import android.arch.lifecycle.Observer;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.design.widget.Snackbar;
|
||||||
|
import android.support.v7.app.AppCompatActivity;
|
||||||
|
import android.support.v7.widget.GridLayoutManager;
|
||||||
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
|
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.SearchView;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import de.nicidienase.chaosflix.R;
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentConference;
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentEvent;
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.sync.Downloader;
|
||||||
|
import de.nicidienase.chaosflix.databinding.FragmentEventsListBinding;
|
||||||
|
import de.nicidienase.chaosflix.touch.OnEventSelectedListener;
|
||||||
|
import de.nicidienase.chaosflix.touch.browse.BrowseFragment;
|
||||||
|
import de.nicidienase.chaosflix.touch.browse.adapters.EventRecyclerViewAdapter;
|
||||||
|
|
||||||
|
public class EventsListFragment extends BrowseFragment implements SearchView.OnQueryTextListener {
|
||||||
|
|
||||||
|
private static final String ARG_COLUMN_COUNT = "column-count";
|
||||||
|
private static final String ARG_TYPE = "type";
|
||||||
|
private static final String ARG_CONFERENCE = "conference";
|
||||||
|
private static final String LAYOUTMANAGER_STATE = "layoutmanager-state";
|
||||||
|
private static final String TAG = EventsListFragment.class.getSimpleName();
|
||||||
|
|
||||||
|
public static final int TYPE_EVENTS = 0;
|
||||||
|
public static final int TYPE_BOOKMARKS = 1;
|
||||||
|
public static final int TYPE_IN_PROGRESS = 2;
|
||||||
|
|
||||||
|
private int columnCount = 1;
|
||||||
|
private OnEventSelectedListener listener;
|
||||||
|
|
||||||
|
private EventRecyclerViewAdapter eventAdapter;
|
||||||
|
private PersistentConference conference;
|
||||||
|
|
||||||
|
private LinearLayoutManager layoutManager;
|
||||||
|
private Snackbar snackbar;
|
||||||
|
private int type;
|
||||||
|
|
||||||
|
public static EventsListFragment newInstance(int type, PersistentConference conference, int columnCount) {
|
||||||
|
EventsListFragment fragment = new EventsListFragment();
|
||||||
|
Bundle args = new Bundle();
|
||||||
|
args.putInt(ARG_TYPE, type);
|
||||||
|
args.putInt(ARG_COLUMN_COUNT, columnCount);
|
||||||
|
args.putParcelable(ARG_CONFERENCE, conference);
|
||||||
|
fragment.setArguments(args);
|
||||||
|
return fragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAttach(Context context) {
|
||||||
|
super.onAttach(context);
|
||||||
|
setHasOptionsMenu(true);
|
||||||
|
if (context instanceof OnEventSelectedListener) {
|
||||||
|
listener = (OnEventSelectedListener) context;
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException(context.toString() + " must implement OnListFragmentInteractionListener");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
if (getArguments() != null) {
|
||||||
|
columnCount = getArguments().getInt(ARG_COLUMN_COUNT);
|
||||||
|
type = getArguments().getInt(ARG_TYPE);
|
||||||
|
conference = getArguments().getParcelable(ARG_CONFERENCE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
|
FragmentEventsListBinding binding = FragmentEventsListBinding.inflate(inflater, container, false);
|
||||||
|
|
||||||
|
AppCompatActivity activity = (AppCompatActivity) requireActivity();
|
||||||
|
activity.setSupportActionBar(binding.incToolbar.toolbar);
|
||||||
|
setOverlay(binding.incOverlay.loadingOverlay);
|
||||||
|
|
||||||
|
if (columnCount <= 1) {
|
||||||
|
layoutManager = new LinearLayoutManager(getContext());
|
||||||
|
} else {
|
||||||
|
layoutManager = new GridLayoutManager(getContext(), columnCount);
|
||||||
|
}
|
||||||
|
binding.list.setLayoutManager(layoutManager);
|
||||||
|
|
||||||
|
eventAdapter = new EventRecyclerViewAdapter(listener);
|
||||||
|
binding.list.setAdapter(eventAdapter);
|
||||||
|
|
||||||
|
Observer<List<PersistentEvent>> listObserver = persistentEvents -> {
|
||||||
|
setEvents(persistentEvents);
|
||||||
|
if (persistentEvents.size() > 0) {
|
||||||
|
setLoadingOverlayVisibility(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (type == TYPE_BOOKMARKS) {
|
||||||
|
setupToolbar(binding.incToolbar.toolbar, R.string.bookmarks);
|
||||||
|
getViewModel().getBookmarkedEvents().observe(this, listObserver);
|
||||||
|
setLoadingOverlayVisibility(false);
|
||||||
|
} else if (type == TYPE_IN_PROGRESS) {
|
||||||
|
setupToolbar(binding.incToolbar.toolbar, R.string.continue_watching);
|
||||||
|
getViewModel().getInProgressEvents().observe(this, listObserver);
|
||||||
|
setLoadingOverlayVisibility(false);
|
||||||
|
} else if (type == TYPE_EVENTS) {
|
||||||
|
{
|
||||||
|
setupToolbar(binding.incToolbar.toolbar, conference.getTitle(), false);
|
||||||
|
eventAdapter.setShowTags(conference.getTagsUsefull());
|
||||||
|
getViewModel().getEventsforConference(conference).observe(this, listObserver);
|
||||||
|
// getViewModel().updateEventsForConference(conference).observe(this, loadingFinished -> setLoadingOverlayVisibility(!loadingFinished));
|
||||||
|
getViewModel().updateEventsForConference(conference).observe(this, state -> {
|
||||||
|
Downloader.DownloaderState downloaderState = state.getState();
|
||||||
|
switch (downloaderState){
|
||||||
|
case RUNNING:
|
||||||
|
setLoadingOverlayVisibility(true);
|
||||||
|
break;
|
||||||
|
case DONE:
|
||||||
|
setLoadingOverlayVisibility(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(state.getError() != null){
|
||||||
|
showSnackbar(state.getError());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return binding.getRoot();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showSnackbar(String message) {
|
||||||
|
if(snackbar!= null){
|
||||||
|
snackbar.dismiss();
|
||||||
|
}
|
||||||
|
snackbar = Snackbar.make(getView(), message, Snackbar.LENGTH_LONG);
|
||||||
|
snackbar.setAction("Okay", view -> snackbar.dismiss());
|
||||||
|
snackbar.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setEvents(List<PersistentEvent> persistentEvents) {
|
||||||
|
eventAdapter.setItems(persistentEvents);
|
||||||
|
|
||||||
|
Parcelable layoutState = getArguments().getParcelable(LAYOUTMANAGER_STATE);
|
||||||
|
if (layoutState != null) { layoutManager.onRestoreInstanceState(layoutState); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
getArguments().putParcelable(LAYOUTMANAGER_STATE, layoutManager.onSaveInstanceState());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||||
|
super.onCreateOptionsMenu(menu, inflater);
|
||||||
|
inflater.inflate(R.menu.events_menu, menu);
|
||||||
|
|
||||||
|
MenuItem searchMenuItem = menu.findItem(R.id.search);
|
||||||
|
SearchView searchView = (SearchView) searchMenuItem.getActionView();
|
||||||
|
SearchManager searchManager = (SearchManager) getActivity().getSystemService(Context.SEARCH_SERVICE);
|
||||||
|
|
||||||
|
searchView.setSearchableInfo(searchManager.
|
||||||
|
getSearchableInfo(getActivity().getComponentName()));
|
||||||
|
searchView.setSubmitButtonEnabled(true);
|
||||||
|
searchView.setIconified(false);
|
||||||
|
searchView.setOnQueryTextListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDetach() {
|
||||||
|
super.onDetach();
|
||||||
|
listener = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onQueryTextSubmit(String query) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onQueryTextChange(String newText) {
|
||||||
|
eventAdapter.getFilter().filter(newText);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
package de.nicidienase.chaosflix.touch.browse.streaming
|
||||||
|
|
||||||
|
|
||||||
|
import android.support.v7.widget.RecyclerView
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import com.squareup.picasso.Picasso
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.entities.streaming.LiveConference
|
||||||
|
import de.nicidienase.chaosflix.databinding.ItemLiveeventCardviewBinding
|
||||||
|
|
||||||
|
class LivestreamAdapter(val listener: LivestreamListFragment.InteractionListener , liveConferences: List<LiveConference> = emptyList()) : RecyclerView.Adapter<LivestreamAdapter.ViewHolder>(){
|
||||||
|
|
||||||
|
lateinit var items: MutableList<StreamingItem>
|
||||||
|
|
||||||
|
init {
|
||||||
|
setContent(liveConferences)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun convertToStreamingItemList(liveConferences: List<LiveConference>) {
|
||||||
|
liveConferences.map { liveConference ->
|
||||||
|
liveConference.groups.map { group ->
|
||||||
|
group.rooms.map { room ->
|
||||||
|
items.add(StreamingItem(liveConference, group, room))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val TAG = LivestreamAdapter::class.simpleName
|
||||||
|
|
||||||
|
fun setContent(liveConferences: List<LiveConference>){
|
||||||
|
items = ArrayList()
|
||||||
|
convertToStreamingItemList(liveConferences)
|
||||||
|
Log.d(TAG,"Size:" + items.size)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return items.size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val binding =
|
||||||
|
ItemLiveeventCardviewBinding.inflate(LayoutInflater.from(parent.context),parent,false)
|
||||||
|
return ViewHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
val item = items[position]
|
||||||
|
|
||||||
|
holder.binding.item = item
|
||||||
|
Picasso.with(holder.binding.root.context)
|
||||||
|
.load(item.room.thumb)
|
||||||
|
.noFade()
|
||||||
|
.fit()
|
||||||
|
.centerInside()
|
||||||
|
.into(holder.binding.imageView)
|
||||||
|
|
||||||
|
holder.binding.root.setOnClickListener(View.OnClickListener {
|
||||||
|
listener.onStreamSelected(item)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
inner class ViewHolder(val binding: ItemLiveeventCardviewBinding) : RecyclerView.ViewHolder(binding.root)
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
package de.nicidienase.chaosflix.touch.browse.streaming
|
||||||
|
|
||||||
|
import android.arch.lifecycle.Observer
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.design.widget.Snackbar
|
||||||
|
import android.support.v7.widget.GridLayoutManager
|
||||||
|
import android.support.v7.widget.LinearLayoutManager
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import de.nicidienase.chaosflix.R
|
||||||
|
import de.nicidienase.chaosflix.databinding.FragmentLivestreamsBinding
|
||||||
|
import de.nicidienase.chaosflix.touch.browse.BrowseFragment
|
||||||
|
|
||||||
|
class LivestreamListFragment : BrowseFragment() {
|
||||||
|
|
||||||
|
private lateinit var listener: InteractionListener
|
||||||
|
private lateinit var binding: FragmentLivestreamsBinding
|
||||||
|
lateinit var adapter: LivestreamAdapter
|
||||||
|
lateinit var snackbar: Snackbar
|
||||||
|
|
||||||
|
private var columnCount = 1
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
if (arguments != null) {
|
||||||
|
columnCount = arguments!!.getInt(ARG_COLUMN_COUNT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttach(context: Context?) {
|
||||||
|
super.onAttach(context)
|
||||||
|
if (context is InteractionListener) {
|
||||||
|
listener = context
|
||||||
|
adapter = LivestreamAdapter(listener)
|
||||||
|
} else {
|
||||||
|
throw RuntimeException(context.toString() + " must implement LivestreamListFragment.InteractionListener")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
binding = FragmentLivestreamsBinding.inflate(inflater, container, false)
|
||||||
|
setupToolbar(binding.incToolbar?.toolbar!!, R.string.livestreams)
|
||||||
|
if (columnCount <= 1) {
|
||||||
|
binding.list.layoutManager = LinearLayoutManager(context)
|
||||||
|
} else {
|
||||||
|
binding.list.layoutManager = GridLayoutManager(context, columnCount)
|
||||||
|
}
|
||||||
|
binding.list.adapter = adapter
|
||||||
|
binding.swipeRefreshLayout.setOnRefreshListener {
|
||||||
|
updateList()
|
||||||
|
}
|
||||||
|
snackbar = Snackbar.make(binding.root,R.string.no_livestreams,Snackbar.LENGTH_INDEFINITE)
|
||||||
|
.setAction(R.string.reload, View.OnClickListener { this.updateList() })
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
updateList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val TAG = LivestreamListFragment::class.simpleName
|
||||||
|
|
||||||
|
private fun updateList() {
|
||||||
|
// binding.swipeRefreshLayout.postDelayed( Runnable {
|
||||||
|
// binding.swipeRefreshLayout.isRefreshing = true
|
||||||
|
// }, 500)
|
||||||
|
binding.swipeRefreshLayout.isRefreshing = true
|
||||||
|
Log.d(TAG,"Refresh starting")
|
||||||
|
viewModel.getLivestreams().observe(this, Observer {
|
||||||
|
it?.let { adapter.setContent(it) }
|
||||||
|
binding.swipeRefreshLayout.isRefreshing = false
|
||||||
|
if(it?.size == 0 && !snackbar.isShown){
|
||||||
|
snackbar.show()
|
||||||
|
} else {
|
||||||
|
snackbar.dismiss()
|
||||||
|
}
|
||||||
|
Log.d(TAG, "Refresh done")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InteractionListener {
|
||||||
|
fun onStreamSelected(streamingItem: StreamingItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val ARG_COLUMN_COUNT = "column-count"
|
||||||
|
|
||||||
|
fun newInstance(columnCount: Int): LivestreamListFragment {
|
||||||
|
val fragment = LivestreamListFragment()
|
||||||
|
val args = Bundle()
|
||||||
|
args.putInt(ARG_COLUMN_COUNT, columnCount)
|
||||||
|
fragment.arguments = args
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package de.nicidienase.chaosflix.touch.browse.streaming
|
||||||
|
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.entities.streaming.Group
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.entities.streaming.LiveConference
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.entities.streaming.Room
|
||||||
|
|
||||||
|
data class StreamingItem(val conference: LiveConference,
|
||||||
|
val group: Group,
|
||||||
|
val room: Room)
|
|
@ -0,0 +1,134 @@
|
||||||
|
package de.nicidienase.chaosflix.touch.eventdetails
|
||||||
|
|
||||||
|
import android.arch.lifecycle.LiveData
|
||||||
|
import android.arch.lifecycle.MutableLiveData
|
||||||
|
import android.arch.lifecycle.ViewModel
|
||||||
|
import android.os.Bundle
|
||||||
|
import de.nicidienase.chaosflix.common.ChaosflixDatabase
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentEvent
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentRecording
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.network.RecordingService
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.sync.Downloader
|
||||||
|
import de.nicidienase.chaosflix.common.userdata.entities.watchlist.WatchlistItem
|
||||||
|
import de.nicidienase.chaosflix.common.util.LiveEvent
|
||||||
|
import de.nicidienase.chaosflix.common.util.SingleLiveEvent
|
||||||
|
import de.nicidienase.chaosflix.common.util.ThreadHandler
|
||||||
|
import de.nicidienase.chaosflix.touch.OfflineItemManager
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class DetailsViewModel(
|
||||||
|
val database: ChaosflixDatabase,
|
||||||
|
recordingApi: RecordingService
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
val state: SingleLiveEvent<LiveEvent<DetailsViewModelState,Bundle,String>>
|
||||||
|
= SingleLiveEvent()
|
||||||
|
|
||||||
|
val downloader = Downloader(recordingApi, database)
|
||||||
|
var writeExternalStorageAllowed: Boolean = false
|
||||||
|
val offlineItemManager: OfflineItemManager = OfflineItemManager(offlineEventDao = database.offlineEventDao())
|
||||||
|
|
||||||
|
private val handler = ThreadHandler()
|
||||||
|
|
||||||
|
fun setEvent(persistentEvent: PersistentEvent): LiveData<PersistentEvent?> {
|
||||||
|
downloader.updateRecordingsForEvent(persistentEvent)
|
||||||
|
return database.eventDao().findEventByGuid(persistentEvent.guid)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getRecordingForEvent(persistentEvent: PersistentEvent): LiveData<List<PersistentRecording>> {
|
||||||
|
downloader.updateRecordingsForEvent(persistentEvent)
|
||||||
|
return database.recordingDao().findRecordingByEvent(persistentEvent.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBookmarkForEvent(guid: String): LiveData<WatchlistItem> =
|
||||||
|
database.watchlistItemDao().getItemForEvent(guid)
|
||||||
|
|
||||||
|
fun createBookmark(guid: String) {
|
||||||
|
handler.runOnBackgroundThread {
|
||||||
|
database.watchlistItemDao().saveItem(WatchlistItem(eventGuid = guid))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeBookmark(guid: String) {
|
||||||
|
handler.runOnBackgroundThread {
|
||||||
|
database.watchlistItemDao().deleteItem(guid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun download(event: PersistentEvent, recording: PersistentRecording)
|
||||||
|
= offlineItemManager.download(event, recording)
|
||||||
|
|
||||||
|
private fun fileExists(guid: String): Boolean {
|
||||||
|
val offlineItem = database.offlineEventDao().getByEventGuidSync(guid)
|
||||||
|
return offlineItem != null && File(offlineItem.localPath).exists()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteOfflineItem(event: PersistentEvent): LiveData<Boolean> {
|
||||||
|
val result = MutableLiveData<Boolean>()
|
||||||
|
handler.runOnBackgroundThread {
|
||||||
|
database.offlineEventDao().getByEventGuidSync(event.guid)?.let {
|
||||||
|
offlineItemManager.deleteOfflineItem(it)
|
||||||
|
}
|
||||||
|
result.postValue(true)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getRelatedEvents(event: PersistentEvent): LiveData<List<PersistentEvent>>{
|
||||||
|
val data = MutableLiveData<List<PersistentEvent>>()
|
||||||
|
handler.runOnBackgroundThread {
|
||||||
|
val guids = database.relatedEventDao().getRelatedEventsForEventSync(event.id).map { it.relatedEventGuid }
|
||||||
|
data.postValue(database.eventDao().findEventsByGUIDsSync(guids))
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
fun playEvent(event: PersistentEvent) {
|
||||||
|
handler.runOnBackgroundThread {
|
||||||
|
|
||||||
|
val offlineEvent = database.offlineEventDao().getByEventGuidSync(event.guid)
|
||||||
|
if(offlineEvent != null){
|
||||||
|
// Play offlineEvent
|
||||||
|
if(!fileExists(event.guid)){
|
||||||
|
state.postValue(LiveEvent(DetailsViewModelState.Error, error = "File is gone"))
|
||||||
|
return@runOnBackgroundThread
|
||||||
|
}
|
||||||
|
val bundle = Bundle()
|
||||||
|
bundle.putString(KEY_LOCAL_PATH, offlineEvent.localPath)
|
||||||
|
state.postValue(LiveEvent(DetailsViewModelState.PlayOfflineItem, data = bundle))
|
||||||
|
} else {
|
||||||
|
// select quality then playEvent
|
||||||
|
val items = database.recordingDao().findRecordingByEventSync(event.id).toTypedArray()
|
||||||
|
val bundle = Bundle()
|
||||||
|
bundle.putParcelableArray(KEY_SELECT_RECORDINGS, items)
|
||||||
|
state.postValue(LiveEvent(DetailsViewModelState.SelectRecording, data = bundle ))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun playRecording(recording: PersistentRecording){
|
||||||
|
val bundle = Bundle()
|
||||||
|
bundle.putParcelable(KEY_PLAY_RECORDING, recording)
|
||||||
|
state.postValue(LiveEvent(DetailsViewModelState.PlayOnlineItem, data = bundle))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun offlineItemExists(event: PersistentEvent): LiveData<Boolean> {
|
||||||
|
val liveData = MutableLiveData<Boolean>()
|
||||||
|
handler.runOnBackgroundThread {
|
||||||
|
database.offlineEventDao().getByEventGuidSync(event.guid)
|
||||||
|
}
|
||||||
|
return liveData
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class DetailsViewModelState{
|
||||||
|
PlayOfflineItem, PlayOnlineItem, SelectRecording, Error
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val TAG = DetailsViewModel::class.simpleName
|
||||||
|
val KEY_LOCAL_PATH = "local_path"
|
||||||
|
val KEY_SELECT_RECORDINGS = "select_recordings"
|
||||||
|
val KEY_PLAY_RECORDING = "play_recording"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
package de.nicidienase.chaosflix.touch.eventdetails
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.arch.lifecycle.ViewModelProviders
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.v4.app.ActivityCompat
|
||||||
|
import android.support.v7.app.AppCompatActivity
|
||||||
|
import de.nicidienase.chaosflix.R
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentEvent
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentRecording
|
||||||
|
import de.nicidienase.chaosflix.touch.OnEventSelectedListener
|
||||||
|
import de.nicidienase.chaosflix.touch.ViewModelFactory
|
||||||
|
import de.nicidienase.chaosflix.touch.playback.PlayerActivity
|
||||||
|
|
||||||
|
class EventDetailsActivity : AppCompatActivity(),
|
||||||
|
EventDetailsFragment.OnEventDetailsFragmentInteractionListener,
|
||||||
|
OnEventSelectedListener {
|
||||||
|
private lateinit var viewModel: DetailsViewModel
|
||||||
|
|
||||||
|
private val PERMISSION_REQUEST_CODE: Int = 1;
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_eventdetails)
|
||||||
|
viewModel = ViewModelProviders.of(this, ViewModelFactory(this)).get(DetailsViewModel::class.java)
|
||||||
|
viewModel.writeExternalStorageAllowed = hasWriteStoragePermission()
|
||||||
|
|
||||||
|
val event = intent.getParcelableExtra<PersistentEvent>(EXTRA_EVENT)
|
||||||
|
|
||||||
|
showFragmentForEvent(event)
|
||||||
|
if (!ActivityCompat.shouldShowRequestPermissionRationale(this,
|
||||||
|
Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||||
|
requestWriteStoragePermission()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showFragmentForEvent(event: PersistentEvent, addToBackStack: Boolean = false) {
|
||||||
|
val detailsFragment = EventDetailsFragment.newInstance(event)
|
||||||
|
|
||||||
|
detailsFragment.allowEnterTransitionOverlap = true
|
||||||
|
detailsFragment.allowReturnTransitionOverlap = true
|
||||||
|
|
||||||
|
val ft = supportFragmentManager.beginTransaction()
|
||||||
|
ft.replace(R.id.fragment_container, detailsFragment)
|
||||||
|
if (addToBackStack) {
|
||||||
|
ft.addToBackStack(null)
|
||||||
|
}
|
||||||
|
ft.setReorderingAllowed(true)
|
||||||
|
|
||||||
|
ft.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEventSelected(event: PersistentEvent) {
|
||||||
|
showFragmentForEvent(event, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onToolbarStateChange() {
|
||||||
|
invalidateOptionsMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun playItem(event: PersistentEvent, recording: PersistentRecording) {
|
||||||
|
PlayerActivity.launch(this, event, recording)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun playItem(event: PersistentEvent, uri: String) {
|
||||||
|
PlayerActivity.launch(this, event, uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
|
if (requestCode == PERMISSION_REQUEST_CODE && grantResults.size > 0) {
|
||||||
|
if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
// requestWriteStoragePermission()
|
||||||
|
} else {
|
||||||
|
viewModel.writeExternalStorageAllowed = true
|
||||||
|
invalidateOptionsMenu()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requestWriteStoragePermission() {
|
||||||
|
ActivityCompat.requestPermissions(this,
|
||||||
|
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), PERMISSION_REQUEST_CODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasWriteStoragePermission(): Boolean {
|
||||||
|
return ActivityCompat.checkSelfPermission(
|
||||||
|
this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private val EXTRA_EVENT = "extra_event"
|
||||||
|
private val EXTRA_URI = "extra_uri"
|
||||||
|
|
||||||
|
fun launch(context: Context, event: PersistentEvent) {
|
||||||
|
val intent = Intent(context, EventDetailsActivity::class.java)
|
||||||
|
intent.putExtra(EXTRA_EVENT, event)
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launch(context: Context, eventId: Uri) {
|
||||||
|
val intent = Intent(context, EventDetailsActivity::class.java)
|
||||||
|
intent.putExtra(EXTRA_URI, eventId)
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,338 @@
|
||||||
|
package de.nicidienase.chaosflix.touch.eventdetails
|
||||||
|
|
||||||
|
import android.arch.lifecycle.Observer
|
||||||
|
import android.arch.lifecycle.ViewModelProviders
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.design.widget.Snackbar
|
||||||
|
import android.support.v4.app.Fragment
|
||||||
|
import android.support.v7.app.AlertDialog
|
||||||
|
import android.support.v7.app.AppCompatActivity
|
||||||
|
import android.support.v7.widget.LinearLayoutManager
|
||||||
|
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 com.squareup.picasso.Callback
|
||||||
|
import com.squareup.picasso.Picasso
|
||||||
|
import de.nicidienase.chaosflix.R
|
||||||
|
import de.nicidienase.chaosflix.common.Util
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentEvent
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentRecording
|
||||||
|
import de.nicidienase.chaosflix.common.userdata.entities.watchlist.WatchlistItem
|
||||||
|
import de.nicidienase.chaosflix.databinding.FragmentEventDetailsBinding
|
||||||
|
import de.nicidienase.chaosflix.touch.OnEventSelectedListener
|
||||||
|
import de.nicidienase.chaosflix.touch.PreferencesManager
|
||||||
|
import de.nicidienase.chaosflix.touch.ViewModelFactory
|
||||||
|
import de.nicidienase.chaosflix.touch.browse.adapters.EventRecyclerViewAdapter
|
||||||
|
|
||||||
|
class EventDetailsFragment : Fragment() {
|
||||||
|
|
||||||
|
private var listener: OnEventDetailsFragmentInteractionListener? = null
|
||||||
|
|
||||||
|
private var appBarExpanded: Boolean = false
|
||||||
|
private lateinit var event: PersistentEvent
|
||||||
|
private var watchlistItem: WatchlistItem? = null
|
||||||
|
private var eventSelectedListener: OnEventSelectedListener? = null
|
||||||
|
private var selectDialog: AlertDialog? = null
|
||||||
|
|
||||||
|
private lateinit var viewModel: DetailsViewModel
|
||||||
|
private lateinit var relatedEventsAdapter: EventRecyclerViewAdapter
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setHasOptionsMenu(true)
|
||||||
|
// postponeEnterTransition()
|
||||||
|
// val transition = TransitionInflater.from(context)
|
||||||
|
// .inflateTransition(android.R.transition.move)
|
||||||
|
// // transition.setDuration(getResources().getInteger(R.integer.anim_duration));
|
||||||
|
// sharedElementEnterTransition = transition
|
||||||
|
|
||||||
|
if (arguments != null) {
|
||||||
|
val parcelable = arguments?.getParcelable<PersistentEvent>(EVENT_PARAM)
|
||||||
|
if(parcelable != null){
|
||||||
|
event = parcelable
|
||||||
|
} else {
|
||||||
|
throw IllegalStateException("Event Missing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?): View? {
|
||||||
|
return inflater.inflate(R.layout.fragment_event_details, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
val binding = FragmentEventDetailsBinding.bind(view)
|
||||||
|
binding.event = event
|
||||||
|
binding.playFab.setOnClickListener { _ -> play() }
|
||||||
|
if (listener != null) {
|
||||||
|
(activity as AppCompatActivity).setSupportActionBar(binding.animToolbar)
|
||||||
|
(activity as AppCompatActivity).supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
eventSelectedListener?.let {
|
||||||
|
relatedEventsAdapter = RelatedEventsRecyclerViewAdapter(eventSelectedListener!!)
|
||||||
|
binding.relatedItemsList.adapter = relatedEventsAdapter
|
||||||
|
binding.relatedItemsList.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.appbar.addOnOffsetChangedListener { appBarLayout, verticalOffset ->
|
||||||
|
val v = Math.abs(verticalOffset).toDouble() / appBarLayout.totalScrollRange
|
||||||
|
if (appBarExpanded xor (v > 0.8)) {
|
||||||
|
if (listener != null) {
|
||||||
|
listener!!.onToolbarStateChange()
|
||||||
|
}
|
||||||
|
appBarExpanded = v > 0.8
|
||||||
|
// binding.collapsingToolbar.isTitleEnabled = appBarExpanded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel = ViewModelProviders.of(
|
||||||
|
requireActivity(),
|
||||||
|
ViewModelFactory(requireContext()))
|
||||||
|
.get(DetailsViewModel::class.java)
|
||||||
|
|
||||||
|
viewModel.setEvent(event)
|
||||||
|
.observe(this, Observer {
|
||||||
|
Log.d(TAG,"Loading Event ${event.title}, ${event.guid}")
|
||||||
|
updateBookmark(event.guid)
|
||||||
|
binding.thumbImage.transitionName = getString(R.string.thumbnail) + event.guid
|
||||||
|
Picasso.with(context)
|
||||||
|
.load(event.thumbUrl)
|
||||||
|
.noFade()
|
||||||
|
.into(binding.thumbImage, object : Callback {
|
||||||
|
override fun onSuccess() {
|
||||||
|
// startPostponedEnterTransition()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError() {
|
||||||
|
// startPostponedEnterTransition()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
viewModel.getRelatedEvents(event).observe(this, Observer {
|
||||||
|
relatedEventsAdapter.items = ArrayList(it)
|
||||||
|
})
|
||||||
|
|
||||||
|
viewModel.state.observe(this, Observer { liveEvent ->
|
||||||
|
if(liveEvent == null){
|
||||||
|
return@Observer
|
||||||
|
}
|
||||||
|
when(liveEvent.state){
|
||||||
|
DetailsViewModel.DetailsViewModelState.PlayOfflineItem -> {
|
||||||
|
liveEvent.data?.getParcelable<PersistentRecording>(DetailsViewModel.KEY_PLAY_RECORDING)?.let {
|
||||||
|
listener?.playItem(event, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DetailsViewModel.DetailsViewModelState.PlayOnlineItem -> {
|
||||||
|
liveEvent.data?.getParcelable<PersistentRecording>(DetailsViewModel.KEY_PLAY_RECORDING)?.let {
|
||||||
|
listener?.playItem(event,it)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
DetailsViewModel.DetailsViewModelState.SelectRecording -> {
|
||||||
|
val selectItems: Array<PersistentRecording> =
|
||||||
|
liveEvent.data?.getParcelableArray(DetailsViewModel.KEY_SELECT_RECORDINGS) as Array<PersistentRecording>
|
||||||
|
selectRecording(selectItems.asList()) {
|
||||||
|
viewModel.playRecording(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DetailsViewModel.DetailsViewModelState.Error -> liveEvent.error?.let { showSnackbar(it) }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateBookmark(guid: String) {
|
||||||
|
viewModel.getBookmarkForEvent(guid)
|
||||||
|
.observe(this, Observer { watchlistItem: WatchlistItem? ->
|
||||||
|
this.watchlistItem = watchlistItem
|
||||||
|
listener?.invalidateOptionsMenu()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showSnackbar(message: String, duration: Int = Snackbar.LENGTH_LONG ){
|
||||||
|
view?.let {
|
||||||
|
Snackbar.make(it, message, duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun play() {
|
||||||
|
if(listener == null){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
viewModel.playEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectRecording(persistentRecordings: List<PersistentRecording>, action: (recording: PersistentRecording) -> Unit) {
|
||||||
|
val stream = Util.getOptimalStream(persistentRecordings)
|
||||||
|
if (stream != null && PreferencesManager.getAutoselectStream()) {
|
||||||
|
action.invoke(stream)
|
||||||
|
} else {
|
||||||
|
val items: List<String> = persistentRecordings.map { getStringForRecording(it) }
|
||||||
|
selectRecordingFromList(items, DialogInterface.OnClickListener { dialogInterface, i ->
|
||||||
|
action.invoke(persistentRecordings[i])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getStringForRecording(recording: PersistentRecording): String {
|
||||||
|
return "${if (recording.isHighQuality) "HD" else "SD"} ${recording.folder} [${recording.language}]"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectRecordingFromList(items: List<String>, resultHandler: DialogInterface.OnClickListener) {
|
||||||
|
this.context?.let { context ->
|
||||||
|
if (selectDialog != null) {
|
||||||
|
selectDialog?.dismiss()
|
||||||
|
}
|
||||||
|
val builder = AlertDialog.Builder(context)
|
||||||
|
builder.setItems(items.toTypedArray(), resultHandler)
|
||||||
|
selectDialog = builder.create()
|
||||||
|
selectDialog?.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttach(context: Context?) {
|
||||||
|
super.onAttach(context)
|
||||||
|
if (context is OnEventSelectedListener) {
|
||||||
|
eventSelectedListener = context
|
||||||
|
}
|
||||||
|
if (context is OnEventDetailsFragmentInteractionListener) {
|
||||||
|
listener = context
|
||||||
|
} else {
|
||||||
|
throw RuntimeException(context!!.toString() + " must implement OnFragmentInteractionListener")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
super.onDetach()
|
||||||
|
listener = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||||
|
Log.d(TAG, "OnPrepareOptionsMenu")
|
||||||
|
super.onPrepareOptionsMenu(menu)
|
||||||
|
if (watchlistItem != null) {
|
||||||
|
menu.findItem(R.id.action_bookmark).isVisible = false
|
||||||
|
menu.findItem(R.id.action_unbookmark).isVisible = true
|
||||||
|
} else {
|
||||||
|
menu.findItem(R.id.action_bookmark).isVisible = true
|
||||||
|
menu.findItem(R.id.action_unbookmark).isVisible = false
|
||||||
|
}
|
||||||
|
menu.findItem(R.id.action_download).isVisible = viewModel.writeExternalStorageAllowed
|
||||||
|
viewModel.offlineItemExists(event).observe(this, Observer { itemExists->
|
||||||
|
itemExists?.let {exists ->
|
||||||
|
menu.findItem(R.id.action_download).isVisible =
|
||||||
|
viewModel.writeExternalStorageAllowed && !exists
|
||||||
|
menu.findItem(R.id.action_delete_offline_item).isVisible =
|
||||||
|
viewModel.writeExternalStorageAllowed && exists
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
menu.findItem(R.id.action_play).isVisible = appBarExpanded
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
|
||||||
|
super.onCreateOptionsMenu(menu, inflater)
|
||||||
|
// if (appBarExpanded)
|
||||||
|
inflater!!.inflate(R.menu.details_menu, menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
|
||||||
|
when (item!!.itemId) {
|
||||||
|
android.R.id.home -> {
|
||||||
|
activity?.finish()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
R.id.action_play -> {
|
||||||
|
play()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
R.id.action_bookmark -> {
|
||||||
|
viewModel.createBookmark(event.guid)
|
||||||
|
updateBookmark(event.guid)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
R.id.action_unbookmark -> {
|
||||||
|
viewModel.removeBookmark(event.guid)
|
||||||
|
watchlistItem = null
|
||||||
|
listener!!.invalidateOptionsMenu()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
R.id.action_download -> {
|
||||||
|
viewModel.getRecordingForEvent(event).observe(this, Observer { recordings ->
|
||||||
|
if (recordings != null) {
|
||||||
|
selectRecording(recordings, { recording -> downloadRecording(recording) })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
R.id.action_delete_offline_item -> {
|
||||||
|
viewModel.deleteOfflineItem(event).observe(this, Observer { success ->
|
||||||
|
if (success != null) {
|
||||||
|
view?.let { Snackbar.make(it, "Deleted Download", Snackbar.LENGTH_SHORT).show() }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
R.id.action_share -> {
|
||||||
|
val shareIntent = Intent(Intent.ACTION_SEND, Uri.parse(event.frontendLink))
|
||||||
|
shareIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.watch_this))
|
||||||
|
shareIntent.putExtra(Intent.EXTRA_TEXT, event.frontendLink)
|
||||||
|
shareIntent.setType("text/plain")
|
||||||
|
startActivity(shareIntent)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
R.id.action_external_player -> {
|
||||||
|
viewModel.getRecordingForEvent(event).observe(this, Observer { recordings ->
|
||||||
|
if (recordings != null) {
|
||||||
|
selectRecording(recordings) { recording ->
|
||||||
|
val shareIntent = Intent(Intent.ACTION_VIEW, Uri.parse(recording.recordingUrl))
|
||||||
|
startActivity(shareIntent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
else -> return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun downloadRecording(recording: PersistentRecording) {
|
||||||
|
viewModel.download(event, recording).observe(this, Observer {
|
||||||
|
if (it != null) {
|
||||||
|
val message = if (it) "Download started" else "Error starting download"
|
||||||
|
Snackbar.make(view!!, message, Snackbar.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OnEventDetailsFragmentInteractionListener {
|
||||||
|
fun onToolbarStateChange()
|
||||||
|
fun invalidateOptionsMenu()
|
||||||
|
fun playItem(event: PersistentEvent, recording: PersistentRecording)
|
||||||
|
fun playItem(event: PersistentEvent, uri: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = EventDetailsFragment::class.java.simpleName
|
||||||
|
private val EVENT_PARAM = "event_param"
|
||||||
|
|
||||||
|
fun newInstance(event: PersistentEvent): EventDetailsFragment {
|
||||||
|
val fragment = EventDetailsFragment()
|
||||||
|
val args = Bundle()
|
||||||
|
args.putParcelable(EVENT_PARAM, event)
|
||||||
|
fragment.arguments = args
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package de.nicidienase.chaosflix.touch.eventdetails
|
||||||
|
|
||||||
|
import de.nicidienase.chaosflix.R
|
||||||
|
import de.nicidienase.chaosflix.touch.OnEventSelectedListener
|
||||||
|
import de.nicidienase.chaosflix.touch.browse.adapters.EventRecyclerViewAdapter
|
||||||
|
|
||||||
|
class RelatedEventsRecyclerViewAdapter(listener: OnEventSelectedListener) : EventRecyclerViewAdapter(listener) {
|
||||||
|
override val layout = R.layout.related_event_cardview_layout
|
||||||
|
}
|
|
@ -0,0 +1,256 @@
|
||||||
|
package de.nicidienase.chaosflix.touch.playback;
|
||||||
|
|
||||||
|
import android.arch.lifecycle.ViewModelProviders;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.databinding.DataBindingUtil;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.design.widget.Snackbar;
|
||||||
|
import android.support.v4.app.Fragment;
|
||||||
|
import android.support.v4.app.FragmentActivity;
|
||||||
|
import android.support.v7.app.AppCompatActivity;
|
||||||
|
import android.support.v7.widget.Toolbar;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.Window;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.DefaultLoadControl;
|
||||||
|
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||||
|
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||||
|
import com.google.android.exoplayer2.LoadControl;
|
||||||
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
|
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.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.upstream.DataSource;
|
||||||
|
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||||
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||||
|
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
|
||||||
|
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||||
|
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
|
||||||
|
import de.nicidienase.chaosflix.R;
|
||||||
|
import de.nicidienase.chaosflix.databinding.FragmentExoPlayerBinding;
|
||||||
|
import de.nicidienase.chaosflix.touch.ViewModelFactory;
|
||||||
|
|
||||||
|
public class ExoPlayerFragment extends Fragment implements PlayerEventListener.PlayerStateChangeListener {
|
||||||
|
private static final String TAG = ExoPlayerFragment.class.getSimpleName();
|
||||||
|
private static final String PLAYBACK_STATE = "playback_state";
|
||||||
|
private static final String ARG_item = "item";
|
||||||
|
|
||||||
|
private OnMediaPlayerInteractionListener listener;
|
||||||
|
private final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter();
|
||||||
|
|
||||||
|
private String userAgent;
|
||||||
|
private Handler mainHandler = new Handler();
|
||||||
|
private boolean playbackState = true;
|
||||||
|
private SimpleExoPlayer exoPlayer;
|
||||||
|
private PlayerViewModel viewModel;
|
||||||
|
private PlaybackItem item;
|
||||||
|
|
||||||
|
FragmentExoPlayerBinding binding;
|
||||||
|
|
||||||
|
public ExoPlayerFragment() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ExoPlayerFragment newInstance(PlaybackItem item) {
|
||||||
|
ExoPlayerFragment fragment = new ExoPlayerFragment();
|
||||||
|
Bundle args = new Bundle();
|
||||||
|
args.putParcelable(ARG_item, item);
|
||||||
|
fragment.setArguments(args);
|
||||||
|
return fragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
if (getArguments() != null) {
|
||||||
|
item = getArguments().getParcelable(ARG_item);
|
||||||
|
}
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
playbackState = savedInstanceState.getBoolean(PLAYBACK_STATE, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel = ViewModelProviders.of(this, new ViewModelFactory(requireContext())).get(PlayerViewModel.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
|
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_exo_player, container, false);
|
||||||
|
|
||||||
|
Toolbar toolbar = binding.getRoot().findViewById(R.id.toolbar);
|
||||||
|
toolbar.setTitle(item.getTitle());
|
||||||
|
toolbar.setSubtitle(item.getSubtitle());
|
||||||
|
((AppCompatActivity) getActivity()).setSupportActionBar(toolbar);
|
||||||
|
((AppCompatActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
|
|
||||||
|
return binding.getRoot();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
if (exoPlayer == null) {
|
||||||
|
exoPlayer = setupPlayer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
if (exoPlayer != null) {
|
||||||
|
viewModel.setPlaybackProgress(item.getEventId(), exoPlayer.getCurrentPosition());
|
||||||
|
exoPlayer.setPlayWhenReady(false);
|
||||||
|
}
|
||||||
|
FragmentActivity activity = getActivity();
|
||||||
|
if(activity != null){
|
||||||
|
Window window = activity.getWindow();
|
||||||
|
window.addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
|
||||||
|
window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
FragmentActivity activity = getActivity();
|
||||||
|
if(activity != null){
|
||||||
|
Window window = activity.getWindow();
|
||||||
|
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||||
|
window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
|
||||||
|
}
|
||||||
|
if (exoPlayer != null) {
|
||||||
|
exoPlayer.setPlayWhenReady(playbackState);
|
||||||
|
viewModel.getPlaybackProgress(item.getEventId()).observe(this, playbackProgress -> {
|
||||||
|
if (playbackProgress != null) {
|
||||||
|
exoPlayer.seekTo(playbackProgress.getProgress());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
binding.videoView.setPlayer(exoPlayer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSaveInstanceState(Bundle outState) {
|
||||||
|
super.onSaveInstanceState(outState);
|
||||||
|
if (exoPlayer != null) {
|
||||||
|
outState.putBoolean(PLAYBACK_STATE, exoPlayer.getPlayWhenReady());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SimpleExoPlayer setupPlayer() {
|
||||||
|
Log.d(TAG, "Setting up Player.");
|
||||||
|
binding.videoView.setKeepScreenOn(true);
|
||||||
|
|
||||||
|
userAgent = Util.getUserAgent(getContext(), getResources().getString(R.string.app_name));
|
||||||
|
|
||||||
|
AdaptiveTrackSelection.Factory trackSelectorFactory = new AdaptiveTrackSelection.Factory(BANDWIDTH_METER);
|
||||||
|
DefaultTrackSelector trackSelector = new DefaultTrackSelector(trackSelectorFactory);
|
||||||
|
LoadControl loadControl = new DefaultLoadControl();
|
||||||
|
DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(getContext(), null, DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF);
|
||||||
|
|
||||||
|
|
||||||
|
exoPlayer = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector, loadControl);
|
||||||
|
PlayerEventListener listener = new PlayerEventListener(exoPlayer, this);
|
||||||
|
exoPlayer.addVideoListener(listener);
|
||||||
|
exoPlayer.addListener(listener);
|
||||||
|
|
||||||
|
exoPlayer.setPlayWhenReady(playbackState);
|
||||||
|
|
||||||
|
exoPlayer.prepare(buildMediaSource(Uri.parse(item.getUri()), ""));
|
||||||
|
return exoPlayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAttach(Context context) {
|
||||||
|
super.onAttach(context);
|
||||||
|
if (context instanceof OnMediaPlayerInteractionListener) {
|
||||||
|
listener = (OnMediaPlayerInteractionListener) context;
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException(context.toString() + " must implement OnFragmentInteractionListener");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDetach() {
|
||||||
|
super.onDetach();
|
||||||
|
listener = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void notifyLoadingStart() {
|
||||||
|
if (binding.progressBar != null) {
|
||||||
|
binding.progressBar.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void notifyLoadingFinished() {
|
||||||
|
if (binding.progressBar != null) {
|
||||||
|
binding.progressBar.setVisibility(View.INVISIBLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void notifyError(String errorMessage) {
|
||||||
|
Snackbar.make(binding.videoView, errorMessage, Snackbar.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void notifyEnd() {
|
||||||
|
viewModel.deletePlaybackProgress(item.getEventId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface OnMediaPlayerInteractionListener {
|
||||||
|
}
|
||||||
|
|
||||||
|
private MediaSource buildMediaSource(Uri uri, String overrideExtension) {
|
||||||
|
DataSource.Factory mediaDataSourceFactory = buildDataSourceFactory(true);
|
||||||
|
int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension);
|
||||||
|
switch (type) {
|
||||||
|
case C.TYPE_SS:
|
||||||
|
return new SsMediaSource(uri, buildDataSourceFactory(false), new DefaultSsChunkSource.Factory(mediaDataSourceFactory), mainHandler, null);
|
||||||
|
case C.TYPE_DASH:
|
||||||
|
return new DashMediaSource(uri, buildDataSourceFactory(false), new DefaultDashChunkSource.Factory(mediaDataSourceFactory), mainHandler, null);
|
||||||
|
case C.TYPE_HLS:
|
||||||
|
return new HlsMediaSource(uri, mediaDataSourceFactory, mainHandler, null);
|
||||||
|
case C.TYPE_OTHER:
|
||||||
|
return new ExtractorMediaSource(uri, mediaDataSourceFactory, new DefaultExtractorsFactory(), mainHandler, null);
|
||||||
|
default: {
|
||||||
|
throw new IllegalStateException("Unsupported type: " + type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DataSource.Factory buildDataSourceFactory(boolean useBandwidthMeter) {
|
||||||
|
return buildDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DataSource.Factory buildDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) {
|
||||||
|
return new DefaultDataSourceFactory(getContext(), bandwidthMeter, buildHttpDataSourceFactory(bandwidthMeter));
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpDataSource.Factory buildHttpDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) {
|
||||||
|
return new DefaultHttpDataSourceFactory(userAgent,
|
||||||
|
bandwidthMeter,
|
||||||
|
DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
||||||
|
DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS,
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package de.nicidienase.chaosflix.touch.playback
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
|
||||||
|
data class PlaybackItem (val title: String, val subtitle: String, val eventId: Long, val uri: String) : Parcelable {
|
||||||
|
|
||||||
|
constructor(parcel: Parcel) : this(
|
||||||
|
parcel.readString(),
|
||||||
|
parcel.readString(),
|
||||||
|
parcel.readLong(),
|
||||||
|
parcel.readString()) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
|
parcel.writeString(title)
|
||||||
|
parcel.writeString(subtitle)
|
||||||
|
parcel.writeLong(eventId)
|
||||||
|
parcel.writeString(uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun describeContents(): Int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object CREATOR : Parcelable.Creator<PlaybackItem> {
|
||||||
|
override fun createFromParcel(parcel: Parcel): PlaybackItem {
|
||||||
|
return PlaybackItem(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<PlaybackItem?> {
|
||||||
|
return arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
package de.nicidienase.chaosflix.touch.playback
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.v7.app.AppCompatActivity
|
||||||
|
import android.view.MenuItem
|
||||||
|
import de.nicidienase.chaosflix.R
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentEvent
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentRecording
|
||||||
|
import de.nicidienase.chaosflix.common.mediadata.entities.streaming.StreamUrl
|
||||||
|
|
||||||
|
class PlayerActivity : AppCompatActivity(), ExoPlayerFragment.OnMediaPlayerInteractionListener {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_player)
|
||||||
|
|
||||||
|
if (savedInstanceState == null && intent.extras != null) {
|
||||||
|
val contentType = intent.getStringExtra(CONTENT_TYPE)
|
||||||
|
var playbackItem = PlaybackItem("Empty", "Empty", 0, "")
|
||||||
|
if (contentType.equals(CONTENT_RECORDING)) {
|
||||||
|
val event = intent.extras.getParcelable<PersistentEvent>(EVENT_KEY)
|
||||||
|
val recording = intent.extras.getParcelable<PersistentRecording>(RECORDING_KEY)
|
||||||
|
val recordingUri = intent.extras.getString(OFFLINE_URI)
|
||||||
|
playbackItem = PlaybackItem(
|
||||||
|
event?.title ?: "",
|
||||||
|
event?.subtitle ?: "",
|
||||||
|
event?.id ?: 0,
|
||||||
|
recordingUri ?: recording?.recordingUrl ?: "")
|
||||||
|
} else if (contentType.equals(CONTENT_STREAM)) {
|
||||||
|
// TODO implement Player for Stream
|
||||||
|
val conference = intent.extras.getString(CONFERENCE,"")
|
||||||
|
val room = intent.extras.getString(ROOM,"")
|
||||||
|
val stream = intent.extras.getString(STREAM, "")
|
||||||
|
playbackItem = PlaybackItem(conference,room,0, stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
val ft = supportFragmentManager.beginTransaction()
|
||||||
|
val playerFragment = ExoPlayerFragment.newInstance(playbackItem)
|
||||||
|
ft.replace(R.id.fragment_container, playerFragment)
|
||||||
|
ft.commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
|
||||||
|
if (item?.itemId == android.R.id.home) {
|
||||||
|
finish()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val CONTENT_TYPE = "content"
|
||||||
|
val CONTENT_RECORDING = "content_recording"
|
||||||
|
val CONTENT_STREAM = "content_stream"
|
||||||
|
|
||||||
|
val EVENT_KEY = "event"
|
||||||
|
val RECORDING_KEY = "recording"
|
||||||
|
val CONFERENCE = "live_conferences"
|
||||||
|
val ROOM = "room"
|
||||||
|
val STREAM = "stream"
|
||||||
|
|
||||||
|
val OFFLINE_URI = "recording_uri"
|
||||||
|
|
||||||
|
fun launch(context: Context, event: PersistentEvent, uri: String) {
|
||||||
|
val i = Intent(context, PlayerActivity::class.java)
|
||||||
|
i.putExtra(CONTENT_TYPE, CONTENT_RECORDING)
|
||||||
|
i.putExtra(PlayerActivity.EVENT_KEY, event)
|
||||||
|
i.putExtra(PlayerActivity.OFFLINE_URI, uri)
|
||||||
|
context.startActivity(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launch(context: Context, event: PersistentEvent, recording: PersistentRecording) {
|
||||||
|
val i = Intent(context, PlayerActivity::class.java)
|
||||||
|
i.putExtra(CONTENT_TYPE, CONTENT_RECORDING)
|
||||||
|
i.putExtra(PlayerActivity.EVENT_KEY, event)
|
||||||
|
i.putExtra(PlayerActivity.RECORDING_KEY, recording)
|
||||||
|
context.startActivity(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launch(context: Context, conference: String, room: String, stream: StreamUrl) {
|
||||||
|
val i = Intent(context, PlayerActivity::class.java)
|
||||||
|
i.putExtra(CONTENT_TYPE, CONTENT_STREAM)
|
||||||
|
i.putExtra(CONFERENCE, conference)
|
||||||
|
i.putExtra(ROOM, room)
|
||||||
|
i.putExtra(STREAM, stream.url)
|
||||||
|
context.startActivity(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
package de.nicidienase.chaosflix.touch.playback;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||||
|
import com.google.android.exoplayer2.PlaybackParameters;
|
||||||
|
import com.google.android.exoplayer2.Player;
|
||||||
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
|
import com.google.android.exoplayer2.Timeline;
|
||||||
|
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||||
|
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by felix on 27.09.17.
|
||||||
|
*/
|
||||||
|
class PlayerEventListener implements Player.EventListener, SimpleExoPlayer.VideoListener {
|
||||||
|
private static final String TAG = PlayerEventListener.class.getSimpleName();
|
||||||
|
private SimpleExoPlayer player;
|
||||||
|
private PlayerStateChangeListener listener;
|
||||||
|
|
||||||
|
public PlayerEventListener(SimpleExoPlayer player, PlayerStateChangeListener listener) {
|
||||||
|
this.player = player;
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTimelineChanged(Timeline timeline, Object manifest) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoadingChanged(boolean isLoading) {
|
||||||
|
if (isLoading && player.getPlaybackState() != Player.STATE_READY) {
|
||||||
|
listener.notifyLoadingStart();
|
||||||
|
} else {
|
||||||
|
listener.notifyLoadingFinished();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||||
|
switch (playbackState) {
|
||||||
|
case Player.STATE_BUFFERING:
|
||||||
|
if (player.isLoading()) {
|
||||||
|
listener.notifyLoadingStart();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Player.STATE_ENDED:
|
||||||
|
Log.d(TAG, "Finished Playback");
|
||||||
|
listener.notifyEnd();
|
||||||
|
break;
|
||||||
|
case Player.STATE_IDLE:
|
||||||
|
case Player.STATE_READY:
|
||||||
|
default:
|
||||||
|
listener.notifyLoadingFinished();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRepeatModeChanged(int repeatMode) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlayerError(ExoPlaybackException error) {
|
||||||
|
String errorMessage = error.getCause().getMessage();
|
||||||
|
listener.notifyError(errorMessage);
|
||||||
|
Log.d(TAG, errorMessage, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPositionDiscontinuity() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRenderedFirstFrame() {
|
||||||
|
listener.notifyLoadingFinished();
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface PlayerStateChangeListener {
|
||||||
|
void notifyLoadingStart();
|
||||||
|
|
||||||
|
void notifyLoadingFinished();
|
||||||
|
|
||||||
|
void notifyError(String errorMessage);
|
||||||
|
|
||||||
|
void notifyEnd();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package de.nicidienase.chaosflix.touch.playback
|
||||||
|
|
||||||
|
import android.arch.lifecycle.LiveData
|
||||||
|
import android.arch.lifecycle.ViewModel
|
||||||
|
import de.nicidienase.chaosflix.common.ChaosflixDatabase
|
||||||
|
import de.nicidienase.chaosflix.common.userdata.entities.progress.PlaybackProgress
|
||||||
|
import de.nicidienase.chaosflix.common.util.ThreadHandler
|
||||||
|
|
||||||
|
internal class PlayerViewModel(val database: ChaosflixDatabase) : ViewModel() {
|
||||||
|
|
||||||
|
val handler = ThreadHandler()
|
||||||
|
|
||||||
|
fun getPlaybackProgress(apiID: Long): LiveData<PlaybackProgress>
|
||||||
|
= database.playbackProgressDao().getProgressForEvent(apiID)
|
||||||
|
|
||||||
|
fun setPlaybackProgress(eventId: Long, progress: Long) {
|
||||||
|
handler.runOnBackgroundThread {
|
||||||
|
database.playbackProgressDao().saveProgress(PlaybackProgress(eventId, progress))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deletePlaybackProgress(eventId: Long) {
|
||||||
|
handler.runOnBackgroundThread {
|
||||||
|
database.playbackProgressDao().deleteItem(eventId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package de.nicidienase.chaosflix.touch.settings
|
||||||
|
|
||||||
|
import android.databinding.DataBindingUtil
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.v7.app.AppCompatActivity
|
||||||
|
import de.nicidienase.chaosflix.R
|
||||||
|
import de.nicidienase.chaosflix.databinding.ActivitySettingsBinding
|
||||||
|
|
||||||
|
class SettingsActivity: AppCompatActivity(){
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
val binding = DataBindingUtil
|
||||||
|
.setContentView<ActivitySettingsBinding>(this, R.layout.activity_settings)
|
||||||
|
setSupportActionBar(binding.toolbarInc?.toolbar)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
supportActionBar?.setTitle(R.string.settings)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
package de.nicidienase.chaosflix.touch.settings
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.preference.PreferenceManager
|
||||||
|
import android.support.v7.preference.PreferenceFragmentCompat
|
||||||
|
import de.nicidienase.chaosflix.R
|
||||||
|
import de.nicidienase.chaosflix.touch.ChaosflixApplication
|
||||||
|
import net.rdrei.android.dirchooser.DirectoryChooserActivity
|
||||||
|
import net.rdrei.android.dirchooser.DirectoryChooserConfig
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
|
private val REQUEST_DIRECTORY: Int = 0
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
|
if (requestCode == REQUEST_DIRECTORY) {
|
||||||
|
if (resultCode == DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED) {
|
||||||
|
val dir = data!!.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR)
|
||||||
|
val sharedPref = PreferenceManager.getDefaultSharedPreferences(ChaosflixApplication.APPLICATION_CONTEXT)
|
||||||
|
val edit = sharedPref.edit()
|
||||||
|
edit.putString("download_folder", dir)
|
||||||
|
edit.apply()
|
||||||
|
this.updateSummary()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateSummary() {
|
||||||
|
val sharedPref = PreferenceManager.getDefaultSharedPreferences(ChaosflixApplication.APPLICATION_CONTEXT)
|
||||||
|
val folder = sharedPref.getString("download_folder", "")
|
||||||
|
val pref = this.findPreference("download_folder")
|
||||||
|
pref.setSummary(folder)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
|
setPreferencesFromResource(R.xml.preferences,rootKey)
|
||||||
|
updateSummary()
|
||||||
|
val pref = this.findPreference("download_folder")
|
||||||
|
|
||||||
|
pref.setOnPreferenceClickListener({
|
||||||
|
val chooserIntent = Intent(context, DirectoryChooserActivity::class.java)
|
||||||
|
|
||||||
|
val config = DirectoryChooserConfig.builder()
|
||||||
|
.newDirectoryName("Download folder")
|
||||||
|
.allowReadOnlyDirectory(false)
|
||||||
|
.allowNewDirectoryNameModification(true)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
chooserIntent.putExtra(DirectoryChooserActivity.EXTRA_CONFIG, config)
|
||||||
|
startActivityForResult(chooserIntent, REQUEST_DIRECTORY)
|
||||||
|
|
||||||
|
return@setOnPreferenceClickListener true
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getInstance(): SettingsFragment {
|
||||||
|
val fragment = SettingsFragment()
|
||||||
|
val args = Bundle()
|
||||||
|
fragment.arguments = args
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
touch/src/main/res/drawable-hdpi/ic_action_play.png
Normal file
After Width: | Height: | Size: 262 B |
BIN
touch/src/main/res/drawable-hdpi/ic_action_search.png
Normal file
After Width: | Height: | Size: 582 B |
BIN
touch/src/main/res/drawable-hdpi/ic_action_share.png
Normal file
After Width: | Height: | Size: 554 B |
BIN
touch/src/main/res/drawable-hdpi/ic_bookmark.png
Normal file
After Width: | Height: | Size: 272 B |
BIN
touch/src/main/res/drawable-hdpi/ic_bookmark_border.png
Normal file
After Width: | Height: | Size: 356 B |
BIN
touch/src/main/res/drawable-hdpi/ic_camera.png
Normal file
After Width: | Height: | Size: 238 B |
BIN
touch/src/main/res/drawable-hdpi/ic_delete.png
Normal file
After Width: | Height: | Size: 227 B |
BIN
touch/src/main/res/drawable-hdpi/ic_delete_dark.png
Normal file
After Width: | Height: | Size: 257 B |
BIN
touch/src/main/res/drawable-hdpi/ic_download.png
Normal file
After Width: | Height: | Size: 244 B |
BIN
touch/src/main/res/drawable-hdpi/ic_download_dark.png
Normal file
After Width: | Height: | Size: 250 B |
BIN
touch/src/main/res/drawable-hdpi/ic_video_archive.png
Normal file
After Width: | Height: | Size: 417 B |
BIN
touch/src/main/res/drawable-mdpi/ic_action_play.png
Normal file
After Width: | Height: | Size: 196 B |
BIN
touch/src/main/res/drawable-mdpi/ic_action_search.png
Normal file
After Width: | Height: | Size: 410 B |
BIN
touch/src/main/res/drawable-mdpi/ic_action_share.png
Normal file
After Width: | Height: | Size: 397 B |
BIN
touch/src/main/res/drawable-mdpi/ic_bookmark.png
Normal file
After Width: | Height: | Size: 205 B |
BIN
touch/src/main/res/drawable-mdpi/ic_bookmark_border.png
Normal file
After Width: | Height: | Size: 260 B |
BIN
touch/src/main/res/drawable-mdpi/ic_camera.png
Normal file
After Width: | Height: | Size: 185 B |
BIN
touch/src/main/res/drawable-mdpi/ic_delete.png
Normal file
After Width: | Height: | Size: 178 B |
BIN
touch/src/main/res/drawable-mdpi/ic_delete_dark.png
Normal file
After Width: | Height: | Size: 197 B |
BIN
touch/src/main/res/drawable-mdpi/ic_download.png
Normal file
After Width: | Height: | Size: 174 B |
BIN
touch/src/main/res/drawable-mdpi/ic_download_dark.png
Normal file
After Width: | Height: | Size: 184 B |
BIN
touch/src/main/res/drawable-mdpi/ic_video_archive.png
Normal file
After Width: | Height: | Size: 329 B |
BIN
touch/src/main/res/drawable-xhdpi/ic_action_play.png
Normal file
After Width: | Height: | Size: 280 B |
BIN
touch/src/main/res/drawable-xhdpi/ic_action_search.png
Normal file
After Width: | Height: | Size: 783 B |
BIN
touch/src/main/res/drawable-xhdpi/ic_action_share.png
Normal file
After Width: | Height: | Size: 758 B |
BIN
touch/src/main/res/drawable-xhdpi/ic_bookmark.png
Normal file
After Width: | Height: | Size: 327 B |
BIN
touch/src/main/res/drawable-xhdpi/ic_bookmark_border.png
Normal file
After Width: | Height: | Size: 434 B |
BIN
touch/src/main/res/drawable-xhdpi/ic_camera.png
Normal file
After Width: | Height: | Size: 263 B |
BIN
touch/src/main/res/drawable-xhdpi/ic_delete.png
Normal file
After Width: | Height: | Size: 255 B |
BIN
touch/src/main/res/drawable-xhdpi/ic_delete_dark.png
Normal file
After Width: | Height: | Size: 279 B |
BIN
touch/src/main/res/drawable-xhdpi/ic_download.png
Normal file
After Width: | Height: | Size: 246 B |
BIN
touch/src/main/res/drawable-xhdpi/ic_download_dark.png
Normal file
After Width: | Height: | Size: 253 B |
BIN
touch/src/main/res/drawable-xhdpi/ic_video_archive.png
Normal file
After Width: | Height: | Size: 523 B |
BIN
touch/src/main/res/drawable-xxhdpi/ic_action_play.png
Normal file
After Width: | Height: | Size: 501 B |
BIN
touch/src/main/res/drawable-xxhdpi/ic_action_search.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
touch/src/main/res/drawable-xxhdpi/ic_action_share.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
touch/src/main/res/drawable-xxhdpi/ic_bookmark.png
Normal file
After Width: | Height: | Size: 565 B |
BIN
touch/src/main/res/drawable-xxhdpi/ic_bookmark_border.png
Normal file
After Width: | Height: | Size: 765 B |
BIN
touch/src/main/res/drawable-xxhdpi/ic_camera.png
Normal file
After Width: | Height: | Size: 433 B |
BIN
touch/src/main/res/drawable-xxhdpi/ic_delete.png
Normal file
After Width: | Height: | Size: 418 B |
BIN
touch/src/main/res/drawable-xxhdpi/ic_delete_dark.png
Normal file
After Width: | Height: | Size: 474 B |