parent
818981190d
commit
2866d01c22
|
@ -37,14 +37,17 @@ dependencies {
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
|
||||||
implementation 'androidx.core:core-ktx:1.3.2'
|
implementation 'androidx.core:core-ktx:1.3.2'
|
||||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||||
implementation 'com.google.android.material:material:1.2.1'
|
|
||||||
implementation 'com.google.code.gson:gson:2.8.6'
|
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.2'
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.2'
|
||||||
implementation 'androidx.navigation:navigation-fragment:2.3.0'
|
implementation 'androidx.navigation:navigation-fragment:2.3.0'
|
||||||
implementation 'androidx.navigation:navigation-ui:2.3.0'
|
implementation 'androidx.navigation:navigation-ui:2.3.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.0'
|
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.0'
|
||||||
implementation 'androidx.navigation:navigation-ui-ktx:2.3.0'
|
implementation 'androidx.navigation:navigation-ui-ktx:2.3.0'
|
||||||
|
implementation 'androidx.security:security-crypto:1.1.0-alpha02'
|
||||||
|
|
||||||
|
implementation 'com.google.android.material:material:1.2.1'
|
||||||
|
implementation 'com.google.code.gson:gson:2.8.6'
|
||||||
|
|
||||||
implementation 'org.jsoup:jsoup:1.13.1'
|
implementation 'org.jsoup:jsoup:1.13.1'
|
||||||
implementation 'com.github.bumptech.glide:glide:4.11.0'
|
implementation 'com.github.bumptech.glide:glide:4.11.0'
|
||||||
|
@ -52,6 +55,11 @@ dependencies {
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
|
||||||
|
|
||||||
|
implementation 'com.google.android.exoplayer:exoplayer-core:2.12.0'
|
||||||
|
implementation 'com.google.android.exoplayer:exoplayer-hls:2.12.0'
|
||||||
|
implementation 'com.google.android.exoplayer:exoplayer-dash:2.12.0'
|
||||||
|
implementation 'com.google.android.exoplayer:exoplayer-ui:2.12.0'
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.12'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme">
|
||||||
|
<activity android:name=".PlayerActivity"></activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:label="@string/app_name">
|
android:label="@string/app_name">
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package org.mosad.teapod
|
package org.mosad.teapod
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
@ -8,6 +10,7 @@ import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
import kotlinx.android.synthetic.main.activity_main.*
|
import kotlinx.android.synthetic.main.activity_main.*
|
||||||
import org.mosad.teapod.parser.AoDParser
|
import org.mosad.teapod.parser.AoDParser
|
||||||
|
import org.mosad.teapod.preferences.EncryptedPreferences
|
||||||
import org.mosad.teapod.ui.MediaFragment
|
import org.mosad.teapod.ui.MediaFragment
|
||||||
import org.mosad.teapod.ui.account.AccountFragment
|
import org.mosad.teapod.ui.account.AccountFragment
|
||||||
import org.mosad.teapod.ui.home.HomeFragment
|
import org.mosad.teapod.ui.home.HomeFragment
|
||||||
|
@ -69,7 +72,14 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun load() {
|
private fun load() {
|
||||||
// TODO
|
EncryptedPreferences.readCredentials(this)
|
||||||
|
|
||||||
|
if (EncryptedPreferences.password.isEmpty()) {
|
||||||
|
Log.i(javaClass.name, "please login!")
|
||||||
|
|
||||||
|
// TODO show login dialog
|
||||||
|
//EncryptedPreferences.saveCredentials("", "", this)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showDetailFragment(media: GUIMedia) {
|
fun showDetailFragment(media: GUIMedia) {
|
||||||
|
@ -85,4 +95,11 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
|
||||||
println("visible !!!: " + mediaFragment.isVisible)
|
println("visible !!!: " + mediaFragment.isVisible)
|
||||||
println(supportFragmentManager.backStackEntryCount)
|
println(supportFragmentManager.backStackEntryCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun startPlayer(streamUrl: String) {
|
||||||
|
val intent = Intent(this, PlayerActivity::class.java).apply {
|
||||||
|
putExtra("streamUrl", streamUrl)
|
||||||
|
}
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
package org.mosad.teapod
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.google.android.exoplayer2.ExoPlayer
|
||||||
|
import com.google.android.exoplayer2.MediaItem
|
||||||
|
import com.google.android.exoplayer2.Player
|
||||||
|
import com.google.android.exoplayer2.SimpleExoPlayer
|
||||||
|
import com.google.android.exoplayer2.source.hls.HlsMediaSource
|
||||||
|
import com.google.android.exoplayer2.upstream.DataSource
|
||||||
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
|
||||||
|
import com.google.android.exoplayer2.util.Util
|
||||||
|
import kotlinx.android.synthetic.main.activity_player.*
|
||||||
|
|
||||||
|
|
||||||
|
class PlayerActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var player: SimpleExoPlayer
|
||||||
|
private lateinit var dataSourceFactory: DataSource.Factory
|
||||||
|
|
||||||
|
private var streamUrl = ""
|
||||||
|
|
||||||
|
private var playWhenReady = true
|
||||||
|
private var currentWindow = 0
|
||||||
|
private var playbackPosition: Long = 0
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_player)
|
||||||
|
|
||||||
|
streamUrl = intent.getStringExtra("streamUrl").toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
if (Util.SDK_INT > 23) {
|
||||||
|
initPlayer()
|
||||||
|
if (video_view != null) video_view.onResume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
if (Util.SDK_INT <= 23) {
|
||||||
|
initPlayer()
|
||||||
|
if (video_view != null) video_view.onResume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
if (Util.SDK_INT <= 23) {
|
||||||
|
if (video_view != null) video_view.onPause()
|
||||||
|
releasePlayer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
if (Util.SDK_INT > 23) {
|
||||||
|
if (video_view != null) video_view.onPause()
|
||||||
|
releasePlayer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initPlayer() {
|
||||||
|
if (streamUrl.isEmpty()) {
|
||||||
|
println("no streamUrl set")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
player = SimpleExoPlayer.Builder(this).build()
|
||||||
|
dataSourceFactory = DefaultDataSourceFactory(this, Util.getUserAgent(this, "Teapod"))
|
||||||
|
|
||||||
|
val mediaSource = HlsMediaSource.Factory(dataSourceFactory)
|
||||||
|
.createMediaSource(MediaItem.fromUri(Uri.parse(streamUrl)))
|
||||||
|
|
||||||
|
player.setMediaSource(mediaSource)
|
||||||
|
player.prepare()
|
||||||
|
player.play()
|
||||||
|
|
||||||
|
player.addListener(object : Player.EventListener {
|
||||||
|
override fun onPlaybackStateChanged(state: Int) {
|
||||||
|
super.onPlaybackStateChanged(state)
|
||||||
|
|
||||||
|
loading.visibility = when (state) {
|
||||||
|
ExoPlayer.STATE_READY -> View.GONE
|
||||||
|
ExoPlayer.STATE_BUFFERING -> View.VISIBLE
|
||||||
|
else -> View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
video_view.player = player
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun releasePlayer(){
|
||||||
|
playbackPosition = player.currentPosition
|
||||||
|
currentWindow = player.currentWindowIndex
|
||||||
|
playWhenReady = player.playWhenReady
|
||||||
|
player.release()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,57 +0,0 @@
|
||||||
package org.mosad.teapod.dummy
|
|
||||||
|
|
||||||
import java.util.ArrayList
|
|
||||||
import java.util.HashMap
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper class for providing sample content for user interfaces created by
|
|
||||||
* Android template wizards.
|
|
||||||
*
|
|
||||||
* TODO: Replace all uses of this class before publishing your app.
|
|
||||||
*/
|
|
||||||
object DummyContent {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An array of sample (dummy) items.
|
|
||||||
*/
|
|
||||||
val ITEMS: MutableList<DummyItem> = ArrayList()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A map of sample (dummy) items, by ID.
|
|
||||||
*/
|
|
||||||
val ITEM_MAP: MutableMap<String, DummyItem> = HashMap()
|
|
||||||
|
|
||||||
private val COUNT = 25
|
|
||||||
|
|
||||||
init {
|
|
||||||
// Add some sample items.
|
|
||||||
for (i in 1..COUNT) {
|
|
||||||
addItem(createDummyItem(i))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addItem(item: DummyItem) {
|
|
||||||
ITEMS.add(item)
|
|
||||||
ITEM_MAP.put(item.id, item)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createDummyItem(position: Int): DummyItem {
|
|
||||||
return DummyItem(position.toString(), "Item " + position, makeDetails(position))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun makeDetails(position: Int): String {
|
|
||||||
val builder = StringBuilder()
|
|
||||||
builder.append("Details about Item: ").append(position)
|
|
||||||
for (i in 0..position - 1) {
|
|
||||||
builder.append("\nMore details information here.")
|
|
||||||
}
|
|
||||||
return builder.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A dummy item representing a piece of content.
|
|
||||||
*/
|
|
||||||
data class DummyItem(val id: String, val content: String, val details: String) {
|
|
||||||
override fun toString(): String = content
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,16 +4,14 @@ import com.google.gson.JsonParser
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import org.jsoup.Connection
|
import org.jsoup.Connection
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
|
import org.mosad.teapod.preferences.EncryptedPreferences
|
||||||
import org.mosad.teapod.util.GUIMedia
|
import org.mosad.teapod.util.GUIMedia
|
||||||
|
|
||||||
class AoDParser {
|
class AoDParser {
|
||||||
|
|
||||||
private val baseURL = "https://www.anime-on-demand.de"
|
private val baseURL = "https://www.anime-on-demand.de"
|
||||||
private val loginPath = "/users/sign_in"
|
private val loginPath = "/users/sign_in"
|
||||||
|
private val libraryPath = "/animes"
|
||||||
// TODO
|
|
||||||
private val login = ""
|
|
||||||
private val pwd = ""
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private var sessionCookies = mutableMapOf<String, String>()
|
private var sessionCookies = mutableMapOf<String, String>()
|
||||||
|
@ -39,8 +37,8 @@ class AoDParser {
|
||||||
println("cookies: $cookies")
|
println("cookies: $cookies")
|
||||||
|
|
||||||
val data = mapOf(
|
val data = mapOf(
|
||||||
Pair("user[login]", login),
|
Pair("user[login]", EncryptedPreferences.login),
|
||||||
Pair("user[password]", pwd),
|
Pair("user[password]", EncryptedPreferences.password),
|
||||||
Pair("user[remember_me]", "1"),
|
Pair("user[remember_me]", "1"),
|
||||||
Pair("commit", "Einloggen"),
|
Pair("commit", "Einloggen"),
|
||||||
Pair("authenticity_token", authenticityToken)
|
Pair("authenticity_token", authenticityToken)
|
||||||
|
@ -67,7 +65,7 @@ class AoDParser {
|
||||||
if (sessionCookies.isEmpty()) login()
|
if (sessionCookies.isEmpty()) login()
|
||||||
|
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
val resAnimes = Jsoup.connect("$baseURL/animes")
|
val resAnimes = Jsoup.connect(baseURL + libraryPath)
|
||||||
.cookies(sessionCookies)
|
.cookies(sessionCookies)
|
||||||
.get()
|
.get()
|
||||||
|
|
||||||
|
@ -147,7 +145,7 @@ class AoDParser {
|
||||||
.get("sources").asJsonArray
|
.get("sources").asJsonArray
|
||||||
|
|
||||||
return@withContext sources.toList().map {
|
return@withContext sources.toList().map {
|
||||||
it.asJsonObject.get("file").toString()
|
it.asJsonObject.get("file").asString
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
package org.mosad.teapod.preferences
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.security.keystore.KeyGenParameterSpec
|
||||||
|
import android.security.keystore.KeyProperties
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.security.crypto.EncryptedSharedPreferences
|
||||||
|
import androidx.security.crypto.MasterKey
|
||||||
|
import org.mosad.teapod.R
|
||||||
|
|
||||||
|
object EncryptedPreferences {
|
||||||
|
|
||||||
|
var login = ""
|
||||||
|
internal set
|
||||||
|
var password = ""
|
||||||
|
internal set
|
||||||
|
|
||||||
|
fun saveCredentials(login: String, password: String, context: Context) {
|
||||||
|
this.login = login
|
||||||
|
this.password = password
|
||||||
|
|
||||||
|
with(getEncryptedPreferences(context)?.edit()) {
|
||||||
|
this?.putString(context.getString(R.string.save_key_user_login), login)
|
||||||
|
this?.putString(context.getString(R.string.save_key_user_password), password)
|
||||||
|
this?.apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readCredentials(context: Context) {
|
||||||
|
with(getEncryptedPreferences(context)) {
|
||||||
|
login = this?.getString(context.getString(R.string.save_key_user_login), "").toString()
|
||||||
|
password = this?.getString(context.getString(R.string.save_key_user_password), "").toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create a encrypted shared preference
|
||||||
|
*/
|
||||||
|
private fun getEncryptedPreferences(context: Context): SharedPreferences? {
|
||||||
|
return try {
|
||||||
|
val spec = KeyGenParameterSpec.Builder(
|
||||||
|
MasterKey.DEFAULT_MASTER_KEY_ALIAS,
|
||||||
|
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
||||||
|
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
||||||
|
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||||
|
.setKeySize(MasterKey.DEFAULT_AES_GCM_MASTER_KEY_SIZE)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val masterKey = MasterKey.Builder(context)
|
||||||
|
.setKeyGenParameterSpec(spec)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
EncryptedSharedPreferences.create(
|
||||||
|
context,
|
||||||
|
context.getString(R.string.encrypted_preference_file_key),
|
||||||
|
masterKey,
|
||||||
|
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||||
|
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||||
|
)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Log.e(javaClass.name, "Could not create encrypted shared preference.", ex)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,8 +7,11 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import kotlinx.android.synthetic.main.fragment_media.*
|
import kotlinx.android.synthetic.main.fragment_media.*
|
||||||
|
import org.mosad.teapod.MainActivity
|
||||||
import org.mosad.teapod.R
|
import org.mosad.teapod.R
|
||||||
import org.mosad.teapod.util.GUIMedia
|
import org.mosad.teapod.util.GUIMedia
|
||||||
|
import java.net.URL
|
||||||
|
import java.net.URLEncoder
|
||||||
|
|
||||||
class MediaFragment(val media: GUIMedia, val streams: List<String>) : Fragment() {
|
class MediaFragment(val media: GUIMedia, val streams: List<String>) : Fragment() {
|
||||||
|
|
||||||
|
@ -36,7 +39,8 @@ class MediaFragment(val media: GUIMedia, val streams: List<String>) : Fragment()
|
||||||
private fun onClickButtonPlay() {
|
private fun onClickButtonPlay() {
|
||||||
println("play ${streams.first()}")
|
println("play ${streams.first()}")
|
||||||
|
|
||||||
|
val mainActivity = activity as MainActivity
|
||||||
|
mainActivity.startPlayer(streams.first())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="#000000"
|
||||||
|
tools:context=".PlayerActivity">
|
||||||
|
|
||||||
|
<com.google.android.exoplayer2.ui.PlayerView
|
||||||
|
android:id="@+id/video_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="center" />
|
||||||
|
<!-- app:controller_layout_id="@layout/player_custom_control"/>-->
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/loading"
|
||||||
|
android:layout_width="70dp"
|
||||||
|
android:layout_height="70dp"
|
||||||
|
android:layout_gravity="center" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
|
@ -4,4 +4,9 @@
|
||||||
<string name="title_library">Library</string>
|
<string name="title_library">Library</string>
|
||||||
<string name="title_search">Search</string>
|
<string name="title_search">Search</string>
|
||||||
<string name="title_account">Account</string>
|
<string name="title_account">Account</string>
|
||||||
|
|
||||||
|
<!-- save keys -->
|
||||||
|
<string name="encrypted_preference_file_key" translatable="false">org.mosad.teapod.encrypted_preferences</string>
|
||||||
|
<string name="save_key_user_login" translatable="false">org.mosad.teapod.user_login</string>
|
||||||
|
<string name="save_key_user_password" translatable="false">org.mosad.teapod.user_password</string>
|
||||||
</resources>
|
</resources>
|
Loading…
Reference in New Issue