diff --git a/app/build.gradle b/app/build.gradle index 91a85df..dfa7db0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -37,14 +37,17 @@ dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' implementation 'androidx.core:core-ktx:1.3.2' 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.navigation:navigation-fragment:2.3.0' implementation 'androidx.navigation:navigation-ui:2.3.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.navigation:navigation-fragment-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 'com.github.bumptech.glide:glide:4.11.0' @@ -52,6 +55,11 @@ dependencies { implementation 'androidx.recyclerview:recyclerview:1.1.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' androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3a11a13..86d309e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,6 +11,7 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> + diff --git a/app/src/main/java/org/mosad/teapod/MainActivity.kt b/app/src/main/java/org/mosad/teapod/MainActivity.kt index 502230e..0be7892 100644 --- a/app/src/main/java/org/mosad/teapod/MainActivity.kt +++ b/app/src/main/java/org/mosad/teapod/MainActivity.kt @@ -1,6 +1,8 @@ package org.mosad.teapod +import android.content.Intent import android.os.Bundle +import android.util.Log import android.view.MenuItem import com.google.android.material.bottomnavigation.BottomNavigationView import androidx.appcompat.app.AppCompatActivity @@ -8,6 +10,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.commit import kotlinx.android.synthetic.main.activity_main.* import org.mosad.teapod.parser.AoDParser +import org.mosad.teapod.preferences.EncryptedPreferences import org.mosad.teapod.ui.MediaFragment import org.mosad.teapod.ui.account.AccountFragment import org.mosad.teapod.ui.home.HomeFragment @@ -69,7 +72,14 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS } 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) { @@ -85,4 +95,11 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS println("visible !!!: " + mediaFragment.isVisible) println(supportFragmentManager.backStackEntryCount) } + + fun startPlayer(streamUrl: String) { + val intent = Intent(this, PlayerActivity::class.java).apply { + putExtra("streamUrl", streamUrl) + } + startActivity(intent) + } } \ No newline at end of file diff --git a/app/src/main/java/org/mosad/teapod/PlayerActivity.kt b/app/src/main/java/org/mosad/teapod/PlayerActivity.kt new file mode 100644 index 0000000..19e317f --- /dev/null +++ b/app/src/main/java/org/mosad/teapod/PlayerActivity.kt @@ -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() + } + +} diff --git a/app/src/main/java/org/mosad/teapod/dummy/DummyContent.kt b/app/src/main/java/org/mosad/teapod/dummy/DummyContent.kt deleted file mode 100644 index 0707517..0000000 --- a/app/src/main/java/org/mosad/teapod/dummy/DummyContent.kt +++ /dev/null @@ -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 = ArrayList() - - /** - * A map of sample (dummy) items, by ID. - */ - val ITEM_MAP: MutableMap = 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 - } -} \ No newline at end of file diff --git a/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt b/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt index 680751b..705ceeb 100644 --- a/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt +++ b/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt @@ -4,16 +4,14 @@ import com.google.gson.JsonParser import kotlinx.coroutines.* import org.jsoup.Connection import org.jsoup.Jsoup +import org.mosad.teapod.preferences.EncryptedPreferences import org.mosad.teapod.util.GUIMedia class AoDParser { private val baseURL = "https://www.anime-on-demand.de" private val loginPath = "/users/sign_in" - - // TODO - private val login = "" - private val pwd = "" + private val libraryPath = "/animes" companion object { private var sessionCookies = mutableMapOf() @@ -39,8 +37,8 @@ class AoDParser { println("cookies: $cookies") val data = mapOf( - Pair("user[login]", login), - Pair("user[password]", pwd), + Pair("user[login]", EncryptedPreferences.login), + Pair("user[password]", EncryptedPreferences.password), Pair("user[remember_me]", "1"), Pair("commit", "Einloggen"), Pair("authenticity_token", authenticityToken) @@ -67,7 +65,7 @@ class AoDParser { if (sessionCookies.isEmpty()) login() withContext(Dispatchers.Default) { - val resAnimes = Jsoup.connect("$baseURL/animes") + val resAnimes = Jsoup.connect(baseURL + libraryPath) .cookies(sessionCookies) .get() @@ -147,7 +145,7 @@ class AoDParser { .get("sources").asJsonArray return@withContext sources.toList().map { - it.asJsonObject.get("file").toString() + it.asJsonObject.get("file").asString } } } diff --git a/app/src/main/java/org/mosad/teapod/preferences/EncryptedPreferences.kt b/app/src/main/java/org/mosad/teapod/preferences/EncryptedPreferences.kt new file mode 100644 index 0000000..de4c5ed --- /dev/null +++ b/app/src/main/java/org/mosad/teapod/preferences/EncryptedPreferences.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mosad/teapod/ui/MediaFragment.kt b/app/src/main/java/org/mosad/teapod/ui/MediaFragment.kt index 8f7bdbc..8fba103 100644 --- a/app/src/main/java/org/mosad/teapod/ui/MediaFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/MediaFragment.kt @@ -7,8 +7,11 @@ import android.view.View import android.view.ViewGroup import com.bumptech.glide.Glide import kotlinx.android.synthetic.main.fragment_media.* +import org.mosad.teapod.MainActivity import org.mosad.teapod.R import org.mosad.teapod.util.GUIMedia +import java.net.URL +import java.net.URLEncoder class MediaFragment(val media: GUIMedia, val streams: List) : Fragment() { @@ -36,7 +39,8 @@ class MediaFragment(val media: GUIMedia, val streams: List) : Fragment() private fun onClickButtonPlay() { println("play ${streams.first()}") - + val mainActivity = activity as MainActivity + mainActivity.startPlayer(streams.first()) } } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_player.xml b/app/src/main/res/layout/activity_player.xml new file mode 100644 index 0000000..f214940 --- /dev/null +++ b/app/src/main/res/layout/activity_player.xml @@ -0,0 +1,23 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 585dd9b..e5789b3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4,4 +4,9 @@ Library Search Account + + + org.mosad.teapod.encrypted_preferences + org.mosad.teapod.user_login + org.mosad.teapod.user_password \ No newline at end of file