From 5e48e724a75639660bdfea90a7cdf3639c0403d4 Mon Sep 17 00:00:00 2001 From: Jannik Date: Sun, 6 Jun 2021 17:54:19 +0200 Subject: [PATCH] update some libraries & coroutines 1.5.0 * androidx.core 1.3.2 -> 1.5.0 * androidx.appcompat 1.2.0 -> 1.3.0 * gson 2.8.6 -> 2.8.7 * coroutines-android 1.4.3 -> 1.5.0 * don't use GlobalScope, use lifecycleScope and vieModelScope instead. This fixes a few issues when fragments where destroied befor the coroutine finished. * gradle wrapper 7.0 -> 7.9.2 --- app/build.gradle | 10 +- .../java/org/mosad/teapod/parser/AoDParser.kt | 431 +++++++++--------- .../teapod/ui/activity/main/MainActivity.kt | 12 +- .../main/fragments/AccountFragment.kt | 4 +- .../activity/main/fragments/HomeFragment.kt | 9 +- .../main/fragments/LibraryFragment.kt | 22 +- .../activity/main/fragments/MediaFragment.kt | 16 +- .../activity/main/fragments/SearchFragment.kt | 7 +- .../ui/activity/onboarding/OnLoginFragment.kt | 3 +- .../ui/activity/player/PlayerActivity.kt | 4 +- .../ui/activity/player/PlayerViewModel.kt | 6 +- .../mosad/teapod/util/StorageController.kt | 6 +- .../mosad/teapod/util/TMDBApiController.kt | 72 ++- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 15 files changed, 305 insertions(+), 301 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 11ce402..9f30c77 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -41,18 +41,20 @@ android { dependencies { implementation fileTree(dir: "libs", include: ["*.jar"]) implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0' - implementation 'androidx.core:core-ktx:1.3.2' - implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.core:core-ktx:1.5.0' + implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5' implementation 'androidx.navigation:navigation-ui-ktx:2.3.5' implementation 'androidx.security:security-crypto:1.1.0-alpha03' implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1' implementation 'com.google.android.material:material:1.3.0' - implementation 'com.google.code.gson:gson:2.8.6' + implementation 'com.google.code.gson:gson:2.8.7' implementation 'com.google.android.exoplayer:exoplayer-core:2.13.3' implementation 'com.google.android.exoplayer:exoplayer-hls:2.13.3' implementation 'com.google.android.exoplayer:exoplayer-dash:2.13.3' 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 9bce88a..d68a8fb 100644 --- a/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt +++ b/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt @@ -99,10 +99,12 @@ object AoDParser { /** * initially load all media and home screen data */ - fun initialLoading() = listOf( - loadHome(), - listAnimes() - ) + suspend fun initialLoading() { + coroutineScope { + launch { loadHome() } + launch { listAnimes() } + } + } /** * get a media by it's ID (int) @@ -121,15 +123,16 @@ object AoDParser { /** * get subscription info from aod website, remove "Anime-Abo" Prefix and trim */ - fun getSubscriptionInfoAsync(): Deferred { - return GlobalScope.async(Dispatchers.IO) { - // get the subscription page - val res = Jsoup.connect(baseUrl + subscriptionPath) - .cookies(sessionCookies) - .get() + suspend fun getSubscriptionInfoAsync(): Deferred { + return coroutineScope { + async(Dispatchers.IO) { + val res = Jsoup.connect(baseUrl + subscriptionPath) + .cookies(sessionCookies) + .get() - return@async res.select("a:contains(Anime-Abo)").text() - .removePrefix("Anime-Abo").trim() + return@async res.select("a:contains(Anime-Abo)").text() + .removePrefix("Anime-Abo").trim() + } } } @@ -137,7 +140,7 @@ object AoDParser { return baseUrl + subscriptionPath } - fun markAsWatched(mediaId: Int, episodeId: Int) = GlobalScope.launch { + suspend fun markAsWatched(mediaId: Int, episodeId: Int) { val episode = getMediaById(mediaId).getEpisodeById(episodeId) episode.watched = true sendCallback(episode.watchedCallback) @@ -146,137 +149,145 @@ object AoDParser { } // TODO don't use jsoup here - private fun sendCallback(callbackPath: String) = GlobalScope.launch(Dispatchers.IO) { - val headers = mutableMapOf( - Pair("Accept", "application/json, text/javascript, */*; q=0.01"), - Pair("Accept-Language", "de,en-US;q=0.7,en;q=0.3"), - Pair("Accept-Encoding", "gzip, deflate, br"), - Pair("X-CSRF-Token", csrfToken), - Pair("X-Requested-With", "XMLHttpRequest"), - ) + private suspend fun sendCallback(callbackPath: String) = coroutineScope { + launch(Dispatchers.IO) { + val headers = mutableMapOf( + Pair("Accept", "application/json, text/javascript, */*; q=0.01"), + Pair("Accept-Language", "de,en-US;q=0.7,en;q=0.3"), + Pair("Accept-Encoding", "gzip, deflate, br"), + Pair("X-CSRF-Token", csrfToken), + Pair("X-Requested-With", "XMLHttpRequest"), + ) - try { - Jsoup.connect(baseUrl + callbackPath) - .ignoreContentType(true) - .cookies(sessionCookies) - .headers(headers) - .execute() - } catch (ex: IOException) { - Log.e(javaClass.name, "Callback for $callbackPath failed.", ex) + try { + Jsoup.connect(baseUrl + callbackPath) + .ignoreContentType(true) + .cookies(sessionCookies) + .headers(headers) + .execute() + } catch (ex: IOException) { + Log.e(javaClass.name, "Callback for $callbackPath failed.", ex) + } } - } /** * load all media from aod into itemMediaList and mediaList + * TODO private suspend fun listAnimes() = withContext(Dispatchers.IO) should also work, maybe a bug in android studio? */ - private fun listAnimes() = GlobalScope.launch(Dispatchers.IO) { - val resAnimes = Jsoup.connect(baseUrl + libraryPath).get() - //println(resAnimes) + private suspend fun listAnimes() = withContext(Dispatchers.IO) { + launch(Dispatchers.IO) { + val resAnimes = Jsoup.connect(baseUrl + libraryPath).get() + //println(resAnimes) - itemMediaList.clear() - mediaList.clear() - resAnimes.select("div.animebox").forEach { - val type = if (it.select("p.animebox-link").select("a").text().lowercase(Locale.ROOT) == "zur serie") { - MediaType.TVSHOW - } else { - MediaType.MOVIE + itemMediaList.clear() + mediaList.clear() + resAnimes.select("div.animebox").forEach { + val type = if (it.select("p.animebox-link").select("a").text().lowercase(Locale.ROOT) == "zur serie") { + MediaType.TVSHOW + } else { + MediaType.MOVIE + } + val mediaTitle = it.select("h3.animebox-title").text() + val mediaLink = it.select("p.animebox-link").select("a").attr("href") + val mediaImage = it.select("p.animebox-image").select("img").attr("src") + val mediaShortText = it.select("p.animebox-shorttext").text() + val mediaId = mediaLink.substringAfterLast("/").toInt() + + itemMediaList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) + mediaList.add(Media(mediaId, mediaLink, type).apply { + info.title = mediaTitle + info.posterUrl = mediaImage + info.shortDesc = mediaShortText + }) } - val mediaTitle = it.select("h3.animebox-title").text() - val mediaLink = it.select("p.animebox-link").select("a").attr("href") - val mediaImage = it.select("p.animebox-image").select("img").attr("src") - val mediaShortText = it.select("p.animebox-shorttext").text() - val mediaId = mediaLink.substringAfterLast("/").toInt() - itemMediaList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) - mediaList.add(Media(mediaId, mediaLink, type).apply { - info.title = mediaTitle - info.posterUrl = mediaImage - info.shortDesc = mediaShortText - }) + Log.i(javaClass.name, "Total library size is: ${mediaList.size}") } - - Log.i(javaClass.name, "Total library size is: ${mediaList.size}") } /** * load new episodes, titles and highlights */ - private fun loadHome() = GlobalScope.launch(Dispatchers.IO) { - val resHome = Jsoup.connect(baseUrl).get() + private suspend fun loadHome() = withContext(Dispatchers.IO) { + launch(Dispatchers.IO) { + val resHome = Jsoup.connect(baseUrl).get() - // get highlights from AoD - highlightsList.clear() - resHome.select("#aod-highlights").select("div.news-item").forEach { - val mediaId = it.select("div.news-item-text").select("a.serienlink") - .attr("href").substringAfterLast("/").toIntOrNull() - val mediaTitle = it.select("div.news-title").select("h2").text() - val mediaImage = it.select("img").attr("src") + // get highlights from AoD + highlightsList.clear() + resHome.select("#aod-highlights").select("div.news-item").forEach { + val mediaId = it.select("div.news-item-text").select("a.serienlink") + .attr("href").substringAfterLast("/").toIntOrNull() + val mediaTitle = it.select("div.news-title").select("h2").text() + val mediaImage = it.select("img").attr("src") - if (mediaId != null) { - highlightsList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) + if (mediaId != null) { + highlightsList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) + } } - } - // get all new episodes from AoD - newEpisodesList.clear() - resHome.select("h2:contains(Neue Episoden)").next().select("li").forEach { - val mediaId = it.select("a.thumbs").attr("href") - .substringAfterLast("/").toIntOrNull() - val mediaImage = it.select("a.thumbs > img").attr("src") - val mediaTitle = "${it.select("a").text()} - ${it.select("span.neweps").text()}" + // get all new episodes from AoD + newEpisodesList.clear() + resHome.select("h2:contains(Neue Episoden)").next().select("li").forEach { + val mediaId = it.select("a.thumbs").attr("href") + .substringAfterLast("/").toIntOrNull() + val mediaImage = it.select("a.thumbs > img").attr("src") + val mediaTitle = "${it.select("a").text()} - ${it.select("span.neweps").text()}" - if (mediaId != null) { - newEpisodesList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) + if (mediaId != null) { + newEpisodesList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) + } } - } - // get new simulcasts from AoD - newSimulcastsList.clear() - resHome.select("h2:contains(Neue Simulcasts)").next().select("li").forEach { - val mediaId = it.select("a.thumbs").attr("href") - .substringAfterLast("/").toIntOrNull() - val mediaImage = it.select("a.thumbs > img").attr("src") - val mediaTitle = it.select("a").text() + // get new simulcasts from AoD + newSimulcastsList.clear() + resHome.select("h2:contains(Neue Simulcasts)").next().select("li").forEach { + val mediaId = it.select("a.thumbs").attr("href") + .substringAfterLast("/").toIntOrNull() + val mediaImage = it.select("a.thumbs > img").attr("src") + val mediaTitle = it.select("a").text() - if (mediaId != null) { - newSimulcastsList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) + if (mediaId != null) { + newSimulcastsList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) + } } - } - // get new titles from AoD - newTitlesList.clear() - resHome.select("h2:contains(Neue Anime-Titel)").next().select("li").forEach { - val mediaId = it.select("a.thumbs").attr("href") - .substringAfterLast("/").toIntOrNull() - val mediaImage = it.select("a.thumbs > img").attr("src") - val mediaTitle = it.select("a").text() + // get new titles from AoD + newTitlesList.clear() + resHome.select("h2:contains(Neue Anime-Titel)").next().select("li").forEach { + val mediaId = it.select("a.thumbs").attr("href") + .substringAfterLast("/").toIntOrNull() + val mediaImage = it.select("a.thumbs > img").attr("src") + val mediaTitle = it.select("a").text() - if (mediaId != null) { - newTitlesList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) + if (mediaId != null) { + newTitlesList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) + } } - } - // get top ten from AoD - topTenList.clear() - resHome.select("h2:contains(Anime Top 10)").next().select("li").forEach { - val mediaId = it.select("a.thumbs").attr("href") - .substringAfterLast("/").toIntOrNull() - val mediaImage = it.select("a.thumbs > img").attr("src") - val mediaTitle = it.select("a").text() + // get top ten from AoD + topTenList.clear() + resHome.select("h2:contains(Anime Top 10)").next().select("li").forEach { + val mediaId = it.select("a.thumbs").attr("href") + .substringAfterLast("/").toIntOrNull() + val mediaImage = it.select("a.thumbs > img").attr("src") + val mediaTitle = it.select("a").text() - if (mediaId != null) { - topTenList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) + if (mediaId != null) { + topTenList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) + } } - } - // if highlights is empty, add a random new title - if (highlightsList.isEmpty()) { - if (newTitlesList.isNotEmpty()) { - highlightsList.add(newTitlesList[Random.nextInt(0, newTitlesList.size)]) - } else { - highlightsList.add(ItemMedia(0,"", "")) + // if highlights is empty, add a random new title + if (highlightsList.isEmpty()) { + if (newTitlesList.isNotEmpty()) { + highlightsList.add(newTitlesList[Random.nextInt(0, newTitlesList.size)]) + } else { + highlightsList.add(ItemMedia(0,"", "")) + } } + + Log.i(javaClass.name, "loaded home") } } @@ -286,112 +297,114 @@ object AoDParser { * load streams for the media path, movies have one episode * @param media is used as call ba reference */ - private fun loadStreams(media: Media) = GlobalScope.launch(Dispatchers.IO) { - if (sessionCookies.isEmpty()) login() + private suspend fun loadStreams(media: Media) = coroutineScope { + launch(Dispatchers.IO) { + if (sessionCookies.isEmpty()) login() - if (!loginSuccess) { - Log.w(javaClass.name, "Login, was not successful.") - return@launch - } - - // get the media page - val res = Jsoup.connect(baseUrl + media.link) - .cookies(sessionCookies) - .get() - - //println(res) - - if (csrfToken.isEmpty()) { - csrfToken = res.select("meta[name=csrf-token]").attr("content") - //Log.i(javaClass.name, "New csrf token is $csrfToken") - } - - val besides = res.select("div.besides").first() - val playlists = besides.select("input.streamstarter_html5").map { streamstarter -> - parsePlaylistAsync( - streamstarter.attr("data-playlist"), - streamstarter.attr("data-lang") - ) - }.awaitAll() - - playlists.forEach { aod -> - // TODO improve language handling - val locale = when (aod.extLanguage) { - "ger" -> Locale.GERMAN - "jap" -> Locale.JAPANESE - else -> Locale.ROOT + if (!loginSuccess) { + Log.w(javaClass.name, "Login, was not successful.") + return@launch } - aod.playlist.forEach { ep -> - try { - if (media.hasEpisode(ep.mediaid)) { - media.getEpisodeById(ep.mediaid).streams.add( - Stream(ep.sources.first().file, locale) - ) - } else { - media.episodes.add(Episode( - id = ep.mediaid, - streams = mutableListOf(Stream(ep.sources.first().file, locale)), - posterUrl = ep.image, - title = ep.title, - description = ep.description, - number = getNumberFromTitle(ep.title, media.type) - )) - } - } catch (ex: Exception) { - Log.w(javaClass.name, "Could not parse episode information.", ex) + // get the media page + val res = Jsoup.connect(baseUrl + media.link) + .cookies(sessionCookies) + .get() + + //println(res) + + if (csrfToken.isEmpty()) { + csrfToken = res.select("meta[name=csrf-token]").attr("content") + //Log.i(javaClass.name, "New csrf token is $csrfToken") + } + + val besides = res.select("div.besides").first() + val playlists = besides.select("input.streamstarter_html5").map { streamstarter -> + parsePlaylistAsync( + streamstarter.attr("data-playlist"), + streamstarter.attr("data-lang") + ) + }.awaitAll() + + playlists.forEach { aod -> + // TODO improve language handling + val locale = when (aod.extLanguage) { + "ger" -> Locale.GERMAN + "jap" -> Locale.JAPANESE + else -> Locale.ROOT } - } - } - Log.i(javaClass.name, "Loaded playlists successfully") - // additional info from the media page - res.select("table.vertical-table").select("tr").forEach { row -> - when (row.select("th").text().lowercase(Locale.ROOT)) { - "produktionsjahr" -> media.info.year = row.select("td").text().toInt() - "fsk" -> media.info.age = row.select("td").text().toInt() - "episodenanzahl" -> { - media.info.episodesCount = row.select("td").text() - .substringBefore("/") - .filter { it.isDigit() } - .toInt() - } - } - } - - // similar titles from media page - media.info.similar = res.select("h2:contains(Ähnliche Animes)").next().select("li").mapNotNull { - val mediaId = it.select("a.thumbs").attr("href") - .substringAfterLast("/").toIntOrNull() - val mediaImage = it.select("a.thumbs > img").attr("src") - val mediaTitle = it.select("a").text() - - if (mediaId != null) { - ItemMedia(mediaId, mediaTitle, mediaImage) - } else { - null - } - } - - // additional information for tv shows the episode title (description) is loaded from the "api" - if (media.type == MediaType.TVSHOW) { - res.select("div.three-box-container > div.episodebox").forEach { episodebox -> - // make sure the episode has a streaming link - if (episodebox.select("input.streamstarter_html5").isNotEmpty()) { - val episodeId = episodebox.select("div.flip-front").attr("id").substringAfter("-").toInt() - val episodeShortDesc = episodebox.select("p.episodebox-shorttext").text() - val episodeWatched = episodebox.select("div.episodebox-icons > div").hasClass("status-icon-orange") - val episodeWatchedCallback = episodebox.select("input.streamstarter_html5").eachAttr("data-playlist").first() - - media.episodes.firstOrNull { it.id == episodeId }?.apply { - shortDesc = episodeShortDesc - watched = episodeWatched - watchedCallback = episodeWatchedCallback + aod.playlist.forEach { ep -> + try { + if (media.hasEpisode(ep.mediaid)) { + media.getEpisodeById(ep.mediaid).streams.add( + Stream(ep.sources.first().file, locale) + ) + } else { + media.episodes.add(Episode( + id = ep.mediaid, + streams = mutableListOf(Stream(ep.sources.first().file, locale)), + posterUrl = ep.image, + title = ep.title, + description = ep.description, + number = getNumberFromTitle(ep.title, media.type) + )) + } + } catch (ex: Exception) { + Log.w(javaClass.name, "Could not parse episode information.", ex) } } } + Log.i(javaClass.name, "Loaded playlists successfully") + + // additional info from the media page + res.select("table.vertical-table").select("tr").forEach { row -> + when (row.select("th").text().lowercase(Locale.ROOT)) { + "produktionsjahr" -> media.info.year = row.select("td").text().toInt() + "fsk" -> media.info.age = row.select("td").text().toInt() + "episodenanzahl" -> { + media.info.episodesCount = row.select("td").text() + .substringBefore("/") + .filter { it.isDigit() } + .toInt() + } + } + } + + // similar titles from media page + media.info.similar = res.select("h2:contains(Ähnliche Animes)").next().select("li").mapNotNull { + val mediaId = it.select("a.thumbs").attr("href") + .substringAfterLast("/").toIntOrNull() + val mediaImage = it.select("a.thumbs > img").attr("src") + val mediaTitle = it.select("a").text() + + if (mediaId != null) { + ItemMedia(mediaId, mediaTitle, mediaImage) + } else { + null + } + } + + // additional information for tv shows the episode title (description) is loaded from the "api" + if (media.type == MediaType.TVSHOW) { + res.select("div.three-box-container > div.episodebox").forEach { episodebox -> + // make sure the episode has a streaming link + if (episodebox.select("input.streamstarter_html5").isNotEmpty()) { + val episodeId = episodebox.select("div.flip-front").attr("id").substringAfter("-").toInt() + val episodeShortDesc = episodebox.select("p.episodebox-shorttext").text() + val episodeWatched = episodebox.select("div.episodebox-icons > div").hasClass("status-icon-orange") + val episodeWatchedCallback = episodebox.select("input.streamstarter_html5").eachAttr("data-playlist").first() + + media.episodes.firstOrNull { it.id == episodeId }?.apply { + shortDesc = episodeShortDesc + watched = episodeWatched + watchedCallback = episodeWatchedCallback + } + } + } + } + Log.i(javaClass.name, "media loaded successfully") } - Log.i(javaClass.name, "media loaded successfully") } /** @@ -402,7 +415,7 @@ object AoDParser { return CompletableDeferred(AoDObject(listOf(), language)) } - return GlobalScope.async(Dispatchers.IO) { + return CoroutineScope(Dispatchers.IO).async(Dispatchers.IO) { val headers = mutableMapOf( Pair("Accept", "application/json, text/javascript, */*; q=0.01"), Pair("Accept-Language", "de,en-US;q=0.7,en;q=0.3"), diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/MainActivity.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/MainActivity.kt index 19dd297..a301b55 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/MainActivity.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/MainActivity.kt @@ -32,20 +32,19 @@ import androidx.fragment.app.commit import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.callbacks.onDismiss import com.google.android.material.bottomnavigation.BottomNavigationView -import kotlinx.coroutines.joinAll -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.* import org.mosad.teapod.R import org.mosad.teapod.databinding.ActivityMainBinding import org.mosad.teapod.parser.AoDParser -import org.mosad.teapod.ui.activity.player.PlayerActivity import org.mosad.teapod.preferences.EncryptedPreferences import org.mosad.teapod.preferences.Preferences -import org.mosad.teapod.ui.components.LoginDialog import org.mosad.teapod.ui.activity.main.fragments.AccountFragment import org.mosad.teapod.ui.activity.main.fragments.HomeFragment import org.mosad.teapod.ui.activity.main.fragments.LibraryFragment import org.mosad.teapod.ui.activity.main.fragments.SearchFragment import org.mosad.teapod.ui.activity.onboarding.OnboardingActivity +import org.mosad.teapod.ui.activity.player.PlayerActivity +import org.mosad.teapod.ui.components.LoginDialog import org.mosad.teapod.util.DataTypes import org.mosad.teapod.util.StorageController import org.mosad.teapod.util.exitAndRemoveTask @@ -138,7 +137,8 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS */ private fun load() { val time = measureTimeMillis { - val loadingJob = AoDParser.initialLoading() // start the initial loading + val loadingJob = CoroutineScope(Dispatchers.IO + CoroutineName("InitialLoadingScope")) + .async { AoDParser.initialLoading() } // start the initial loading // load all saved stuff here Preferences.load(this) @@ -165,7 +165,7 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS } } - runBlocking { loadingJob.joinAll() } // wait for initial loading to finish + runBlocking { loadingJob.await() } // wait for initial loading to finish } Log.i(javaClass.name, "loading and login in $time ms") diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt index eb8000f..64c7b89 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt @@ -12,9 +12,9 @@ import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.core.view.isVisible import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.list.listItemsSingleChoice -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.mosad.teapod.BuildConfig import org.mosad.teapod.R @@ -64,7 +64,7 @@ class AccountFragment : Fragment() { // load subscription (async) info before anything else binding.textAccountSubscription.text = getString(R.string.account_subscription, getString(R.string.loading)) - GlobalScope.launch { + lifecycleScope.launch { binding.textAccountSubscription.text = getString( R.string.account_subscription, AoDParser.getSubscriptionInfoAsync().await() diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt index 6e19d11..c81ebb7 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt @@ -6,14 +6,13 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import com.bumptech.glide.Glide -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.mosad.teapod.R -import org.mosad.teapod.ui.activity.main.MainActivity import org.mosad.teapod.databinding.FragmentHomeBinding import org.mosad.teapod.parser.AoDParser +import org.mosad.teapod.ui.activity.main.MainActivity import org.mosad.teapod.util.ItemMedia import org.mosad.teapod.util.StorageController import org.mosad.teapod.util.adapter.MediaItemAdapter @@ -40,7 +39,7 @@ class HomeFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - GlobalScope.launch(Dispatchers.Main) { + lifecycleScope.launch { context?.let { initHighlight() initRecyclerViews() @@ -101,7 +100,7 @@ class HomeFragment : Fragment() { private fun initActions() { binding.buttonPlayHighlight.setOnClickListener { // TODO get next episode - GlobalScope.launch { + lifecycleScope.launch { val media = AoDParser.getMediaById(highlightMedia.id) Log.d(javaClass.name, "Starting Player with mediaId: ${media.id}") diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/LibraryFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/LibraryFragment.kt index 01ab191..f757b7a 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/LibraryFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/LibraryFragment.kt @@ -5,10 +5,8 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope +import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.mosad.teapod.databinding.FragmentLibraryBinding import org.mosad.teapod.parser.AoDParser import org.mosad.teapod.util.adapter.MediaItemAdapter @@ -29,18 +27,16 @@ class LibraryFragment : Fragment() { super.onViewCreated(view, savedInstanceState) // init async - GlobalScope.launch { + lifecycleScope.launch { // create and set the adapter, needs context - withContext(Dispatchers.Main) { - context?.let { - adapter = MediaItemAdapter(AoDParser.itemMediaList) - adapter.onItemClick = { mediaId, _ -> - activity?.showFragment(MediaFragment(mediaId)) - } - - binding.recyclerMediaLibrary.adapter = adapter - binding.recyclerMediaLibrary.addItemDecoration(MediaItemDecoration(9)) + context?.let { + adapter = MediaItemAdapter(AoDParser.itemMediaList) + adapter.onItemClick = { mediaId, _ -> + activity?.showFragment(MediaFragment(mediaId)) } + + binding.recyclerMediaLibrary.adapter = adapter + binding.recyclerMediaLibrary.addItemDecoration(MediaItemDecoration(9)) } } diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragment.kt index 6e5f750..a762032 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragment.kt @@ -10,20 +10,21 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope import androidx.viewpager2.adapter.FragmentStateAdapter import com.bumptech.glide.Glide import com.bumptech.glide.request.RequestOptions import com.google.android.material.appbar.AppBarLayout import com.google.android.material.tabs.TabLayoutMediator import jp.wasabeef.glide.transformations.BlurTransformation -import kotlinx.coroutines.* +import kotlinx.coroutines.launch import org.mosad.teapod.R import org.mosad.teapod.databinding.FragmentMediaBinding import org.mosad.teapod.ui.activity.main.MainActivity import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel -import org.mosad.teapod.util.* import org.mosad.teapod.util.DataTypes.MediaType - +import org.mosad.teapod.util.Episode +import org.mosad.teapod.util.StorageController /** * The media detail fragment. @@ -61,13 +62,12 @@ class MediaFragment(private val mediaId: Int) : Fragment() { } }.attach() - GlobalScope.launch(Dispatchers.Main) { + + lifecycleScope.launch { model.load(mediaId) // load the streams and tmdb for the selected media - if (this@MediaFragment.isAdded) { - updateGUI() - initActions() - } + updateGUI() + initActions() } } diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/SearchFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/SearchFragment.kt index 57c43b1..b430092 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/SearchFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/SearchFragment.kt @@ -6,7 +6,8 @@ import android.view.View import android.view.ViewGroup import android.widget.SearchView import androidx.fragment.app.Fragment -import kotlinx.coroutines.* +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.launch import org.mosad.teapod.databinding.FragmentSearchBinding import org.mosad.teapod.parser.AoDParser import org.mosad.teapod.util.decoration.MediaItemDecoration @@ -26,9 +27,8 @@ class SearchFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - GlobalScope.launch { + lifecycleScope.launch { // create and set the adapter, needs context - withContext(Dispatchers.Main) { context?.let { adapter = MediaItemAdapter(AoDParser.itemMediaList) adapter!!.onItemClick = { mediaId, _ -> @@ -39,7 +39,6 @@ class SearchFragment : Fragment() { binding.recyclerMediaSearch.adapter = adapter binding.recyclerMediaSearch.addItemDecoration(MediaItemDecoration(9)) } - } } initActions() diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/onboarding/OnLoginFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/onboarding/OnLoginFragment.kt index f65ab30..6a329be 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/onboarding/OnLoginFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/onboarding/OnLoginFragment.kt @@ -5,6 +5,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.* import org.mosad.teapod.R import org.mosad.teapod.databinding.FragmentOnLoginBinding @@ -35,7 +36,7 @@ class OnLoginFragment: Fragment() { EncryptedPreferences.saveCredentials(email, password, requireContext()) // save the credentials binding.buttonLogin.isClickable = false - loginJob = GlobalScope.launch { + loginJob = lifecycleScope.launch { if (AoDParser.login()) { // if login was successful, switch to main if (activity is OnboardingActivity) { diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt index 6c29035..44e3b55 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt @@ -19,6 +19,7 @@ import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import androidx.core.view.GestureDetectorCompat import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.ui.StyledPlayerControlView @@ -26,7 +27,6 @@ import com.google.android.exoplayer2.util.Util import kotlinx.android.synthetic.main.activity_player.* import kotlinx.android.synthetic.main.player_controls.* import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.mosad.teapod.R @@ -255,7 +255,7 @@ class PlayerActivity : AppCompatActivity() { } timerUpdates = Timer().scheduleAtFixedRate(0, 500) { - GlobalScope.launch { + lifecycleScope.launch { var btnNextEpIsVisible: Boolean var controlsVisible: Boolean diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt index 4efb7c4..19a4caf 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt @@ -4,6 +4,7 @@ import android.app.Application import android.net.Uri import android.util.Log import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope import com.google.android.exoplayer2.C import com.google.android.exoplayer2.MediaItem import com.google.android.exoplayer2.SimpleExoPlayer @@ -11,6 +12,7 @@ import com.google.android.exoplayer2.source.MediaSource import com.google.android.exoplayer2.source.hls.HlsMediaSource import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory import com.google.android.exoplayer2.util.Util +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.mosad.teapod.R import org.mosad.teapod.parser.AoDParser @@ -107,7 +109,9 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) // if episodes has not been watched, mark as watched if (!episode.watched) { - AoDParser.markAsWatched(media.id, episode.id) + viewModelScope.launch { + AoDParser.markAsWatched(media.id, episode.id) + } } } diff --git a/app/src/main/java/org/mosad/teapod/util/StorageController.kt b/app/src/main/java/org/mosad/teapod/util/StorageController.kt index 291ac81..14a0f26 100644 --- a/app/src/main/java/org/mosad/teapod/util/StorageController.kt +++ b/app/src/main/java/org/mosad/teapod/util/StorageController.kt @@ -3,16 +3,12 @@ package org.mosad.teapod.util import android.content.Context import android.net.Uri import android.util.Log -import android.widget.Toast import com.google.gson.Gson import com.google.gson.JsonParser import kotlinx.coroutines.* -import org.mosad.teapod.R import java.io.File import java.io.FileReader import java.io.FileWriter -import java.lang.Exception -import java.net.URI /** * This controller contains the logic for permanently saved data. @@ -45,7 +41,7 @@ object StorageController { fun saveMyList(context: Context): Job { val file = File(context.filesDir, fileNameMyList) - return GlobalScope.launch(Dispatchers.IO) { + return CoroutineScope(Dispatchers.IO).launch { file.writeText(Gson().toJson(myList.distinct())) } } diff --git a/app/src/main/java/org/mosad/teapod/util/TMDBApiController.kt b/app/src/main/java/org/mosad/teapod/util/TMDBApiController.kt index bb76090..9d51653 100644 --- a/app/src/main/java/org/mosad/teapod/util/TMDBApiController.kt +++ b/app/src/main/java/org/mosad/teapod/util/TMDBApiController.kt @@ -24,8 +24,8 @@ class TMDBApiController { val searchTerm = title.replace("(Sub)", "").trim() return when (type) { - MediaType.MOVIE -> searchMovie(searchTerm).await() - MediaType.TVSHOW -> searchTVShow(searchTerm).await() + MediaType.MOVIE -> searchMovie(searchTerm) + MediaType.TVSHOW -> searchTVShow(searchTerm) else -> { Log.e(javaClass.name, "Wrong Type: $type") TMDBResponse() @@ -34,62 +34,56 @@ class TMDBApiController { } - fun searchTVShow(title: String): Deferred { + @Suppress("BlockingMethodInNonBlockingContext") + private suspend fun searchTVShow(title: String): TMDBResponse = withContext(Dispatchers.IO) { val url = URL("$searchTVUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}") + val response = JsonParser.parseString(url.readText()).asJsonObject + //println(response) - return GlobalScope.async { - val response = JsonParser.parseString(url.readText()).asJsonObject - //println(response) + return@withContext if (response.get("total_results").asInt > 0) { + response.get("results").asJsonArray.first().asJsonObject.let { + val id = getStringNotNull(it, "id").toInt() + val overview = getStringNotNull(it, "overview") + val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl) + val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl) - if (response.get("total_results").asInt > 0) { - response.get("results").asJsonArray.first().asJsonObject.let { - val id = getStringNotNull(it, "id").toInt() - val overview = getStringNotNull(it, "overview") - val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl) - val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl) - - TMDBResponse(id, "", overview, posterPath, backdropPath) - } - } else { - TMDBResponse() + TMDBResponse(id, "", overview, posterPath, backdropPath) } + } else { + TMDBResponse() } } - fun searchMovie(title: String): Deferred { + @Suppress("BlockingMethodInNonBlockingContext") + private suspend fun searchMovie(title: String): TMDBResponse = withContext(Dispatchers.IO) { val url = URL("$searchMovieUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}") + val response = JsonParser.parseString(url.readText()).asJsonObject + //println(response) - return GlobalScope.async { - val response = JsonParser.parseString(url.readText()).asJsonObject - //println(response) + return@withContext if (response.get("total_results").asInt > 0) { + response.get("results").asJsonArray.first().asJsonObject.let { + val id = getStringNotNull(it,"id").toInt() + val overview = getStringNotNull(it,"overview") + val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl) + val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl) + val runtime = getMovieRuntime(id) - if (response.get("total_results").asInt > 0) { - response.get("results").asJsonArray.first().asJsonObject.let { - val id = getStringNotNull(it,"id").toInt() - val overview = getStringNotNull(it,"overview") - val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl) - val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl) - val runtime = getMovieRuntime(id) - - TMDBResponse(id, "", overview, posterPath, backdropPath, runtime) - } - } else { - TMDBResponse() + TMDBResponse(id, "", overview, posterPath, backdropPath, runtime) } + } else { + TMDBResponse() } } /** * currently only used for runtime, need a rework */ - fun getMovieRuntime(id: Int): Int = runBlocking { + @Suppress("BlockingMethodInNonBlockingContext") + suspend fun getMovieRuntime(id: Int): Int = withContext(Dispatchers.IO) { val url = URL("$getMovieUrl/$id?api_key=$apiKey&language=$language") - GlobalScope.async { - val response = JsonParser.parseString(url.readText()).asJsonObject - - return@async getStringNotNull(response,"runtime").toInt() - }.await() + val response = JsonParser.parseString(url.readText()).asJsonObject + return@withContext getStringNotNull(response,"runtime").toInt() } /** diff --git a/build.gradle b/build.gradle index e016c38..94a91d3 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = "1.5.0" + ext.kotlin_version = "1.5.10" repositories { google() mavenCentral() diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f371643..0f80bbf 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists