diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 306d2c77..e389f7b0 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -48,7 +48,7 @@ android {
targetSdk = 33
versionCode = 55
- versionName = "3.2.5"
+ versionName = "3.2.6"
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d827a3e8..11b82afb 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -11,7 +11,7 @@
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
index 1f208502..a1bd0177 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
@@ -54,13 +54,17 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2A
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringRepo
+import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringResumeWatching
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringSearch
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.inAppAuths
import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
+import com.lagradost.cloudstream3.ui.home.HomeViewModel
+import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST
import com.lagradost.cloudstream3.ui.search.SearchFragment
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
+import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsGeneral
import com.lagradost.cloudstream3.ui.setup.HAS_DONE_SETUP_KEY
@@ -69,6 +73,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
import com.lagradost.cloudstream3.utils.AppUtils.loadCache
import com.lagradost.cloudstream3.utils.AppUtils.loadRepository
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
+import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.DataStore.getKey
@@ -83,6 +88,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
import com.lagradost.cloudstream3.utils.UIHelper.getResourceColor
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.UIHelper.navigate
+import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
import com.lagradost.cloudstream3.utils.UIHelper.requestRW
import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
@@ -289,6 +295,19 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
nextSearchQuery =
URLDecoder.decode(str.substringAfter("$appStringSearch://"), "UTF-8")
nav_view.selectedItemId = R.id.navigation_search
+ } else if (safeURI(str)?.scheme == appStringResumeWatching) {
+ val id =
+ str.substringAfter("$appStringResumeWatching://").toIntOrNull()
+ ?: return false
+ ioSafe {
+ val resumeWatchingCard =
+ HomeViewModel.getResumeWatching()?.firstOrNull { it.id == id }
+ ?: return@ioSafe
+ activity.loadSearchResult(
+ resumeWatchingCard,
+ START_ACTION_RESUME_LATEST
+ )
+ }
} else if (!isWebview) {
if (str.startsWith(DOWNLOAD_NAVIGATE_TO)) {
this.navigate(R.id.navigation_downloads)
@@ -449,12 +468,33 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
onUserLeaveHint(this)
}
+ private fun showConfirmExitDialog() {
+ val builder: AlertDialog.Builder = AlertDialog.Builder(this, R.style.AlertDialogCustom)
+ builder.setTitle(R.string.confirm_exit_dialog)
+ builder.apply {
+ setPositiveButton(R.string.yes) { _, _ -> super.onBackPressed() }
+ setNegativeButton(R.string.no) { _, _ -> }
+ }
+ builder.show()
+ }
+
private fun backPressed() {
this.window?.navigationBarColor =
this.colorFromAttribute(R.attr.primaryGrayBackground)
this.updateLocale()
- super.onBackPressed()
this.updateLocale()
+
+ val navHostFragment =
+ supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as? NavHostFragment
+ val navController = navHostFragment?.navController
+ val isAtHome =
+ navController?.currentDestination?.matchDestination(R.id.navigation_home) == true
+
+ if (isAtHome && isTrueTvSettings()) {
+ showConfirmExitDialog()
+ } else {
+ super.onBackPressed()
+ }
}
override fun onBackPressed() {
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt
index be915baf..f09bf8fe 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt
@@ -48,6 +48,9 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
// Instantly start the search given a query
const val appStringSearch = "cloudstreamsearch"
+ // Instantly resume watching a show
+ const val appStringResumeWatching = "cloudstreamcontinuewatching"
+
val unixTime: Long
get() = System.currentTimeMillis() / 1000L
val unixTimeMs: Long
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt
index 00b53fa7..b7803e6c 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt
@@ -7,6 +7,7 @@ import android.content.DialogInterface
import android.content.Intent
import android.content.res.Configuration
import android.net.Uri
+import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@@ -56,6 +57,7 @@ import com.lagradost.cloudstream3.ui.search.*
import com.lagradost.cloudstream3.ui.search.SearchHelper.handleSearchClickCallback
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
+import com.lagradost.cloudstream3.utils.AppUtils.addProgramsToContinueWatching
import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
@@ -552,7 +554,7 @@ class HomeFragment : Fragment() {
observe(homeViewModel.preview) { preview ->
// Always reset the padding, otherwise the will move lower and lower
- // home_fix_padding?.setPadding(0, 0, 0, 0)
+ // home_fix_padding?.setPadding(0, 0, 0, 0)
home_fix_padding?.let { v ->
val params = v.layoutParams
params.height = 0
@@ -596,7 +598,7 @@ class HomeFragment : Fragment() {
val callback: OnPageChangeCallback = object : OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
- // home_search?.isIconified = true
+ // home_search?.isIconified = true
//home_search?.isVisible = true
//home_search?.clearFocus()
@@ -927,11 +929,13 @@ class HomeFragment : Fragment() {
resumeWatching
)
- //if (context?.isTvSettings() == true) {
- // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- // context?.addProgramsToContinueWatching(resumeWatching.mapNotNull { it as? DataStoreHelper.ResumeWatchingResult })
- // }
- //}
+ if (isTrueTvSettings()) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ ioSafe {
+ activity?.addProgramsToContinueWatching(resumeWatching.mapNotNull { it as? DataStoreHelper.ResumeWatchingResult })
+ }
+ }
+ }
home_watch_child_more_info?.setOnClickListener {
activity?.loadHomepageList(
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt
index 9d75b0f0..3bb196e2 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt
@@ -36,6 +36,42 @@ import java.util.*
import kotlin.collections.set
class HomeViewModel : ViewModel() {
+ companion object {
+ suspend fun getResumeWatching(): List? {
+ val resumeWatching = withContext(Dispatchers.IO) {
+ getAllResumeStateIds()?.mapNotNull { id ->
+ getLastWatched(id)
+ }?.sortedBy { -it.updateTime }
+ }
+ val resumeWatchingResult = withContext(Dispatchers.IO) {
+ resumeWatching?.mapNotNull { resume ->
+
+ val data = getKey(
+ DOWNLOAD_HEADER_CACHE,
+ resume.parentId.toString()
+ ) ?: return@mapNotNull null
+
+ val watchPos = getViewPos(resume.episodeId)
+
+ DataStoreHelper.ResumeWatchingResult(
+ data.name,
+ data.url,
+ data.apiName,
+ data.type,
+ data.poster,
+ watchPos,
+ resume.episodeId,
+ resume.parentId,
+ resume.episode,
+ resume.season,
+ resume.isFromDownload
+ )
+ }
+ }
+ return resumeWatchingResult
+ }
+ }
+
private var repo: APIRepository? = null
private val _apiName = MutableLiveData()
@@ -66,36 +102,7 @@ class HomeViewModel : ViewModel() {
val preview: LiveData>>> = _preview
fun loadResumeWatching() = viewModelScope.launchSafe {
- val resumeWatching = withContext(Dispatchers.IO) {
- getAllResumeStateIds()?.mapNotNull { id ->
- getLastWatched(id)
- }?.sortedBy { -it.updateTime }
- }
-
- // val resumeWatchingResult = ArrayList()
-
- val resumeWatchingResult = withContext(Dispatchers.IO) {
- resumeWatching?.map { resume ->
- val data = getKey(
- DOWNLOAD_HEADER_CACHE,
- resume.parentId.toString()
- ) ?: return@map null
- val watchPos = getViewPos(resume.episodeId)
- DataStoreHelper.ResumeWatchingResult(
- data.name,
- data.url,
- data.apiName,
- data.type,
- data.poster,
- watchPos,
- resume.episodeId,
- resume.parentId,
- resume.episode,
- resume.season,
- resume.isFromDownload
- )
- }?.filterNotNull()
- }
+ val resumeWatchingResult = getResumeWatching()
resumeWatchingResult?.let {
_resumeWatching.postValue(it)
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt
index cf3fbfde..597316c3 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt
@@ -3,9 +3,7 @@ package com.lagradost.cloudstream3.utils
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Activity.RESULT_CANCELED
-import android.content.ContentValues
-import android.content.Context
-import android.content.Intent
+import android.content.*
import android.content.pm.PackageManager
import android.database.Cursor
import android.media.AudioAttributes
@@ -26,7 +24,6 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.annotation.WorkerThread
import androidx.appcompat.app.AlertDialog
-import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat
import androidx.core.text.toSpanned
@@ -35,9 +32,7 @@ import androidx.fragment.app.FragmentActivity
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
-import androidx.tvprovider.media.tv.PreviewChannelHelper
-import androidx.tvprovider.media.tv.TvContractCompat
-import androidx.tvprovider.media.tv.WatchNextProgram
+import androidx.tvprovider.media.tv.*
import androidx.tvprovider.media.tv.WatchNextProgram.fromCursor
import com.fasterxml.jackson.module.kotlin.readValue
import com.google.android.gms.cast.framework.CastContext
@@ -51,6 +46,7 @@ import com.lagradost.cloudstream3.MainActivity.Companion.afterRepositoryLoadedEv
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.plugins.RepositoryManager
+import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringResumeWatching
import com.lagradost.cloudstream3.ui.WebviewFragment
import com.lagradost.cloudstream3.ui.result.ResultFragment
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
@@ -58,9 +54,13 @@ import com.lagradost.cloudstream3.ui.settings.extensions.PluginsViewModel.Compan
import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main
+import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllResumeStateIds
+import com.lagradost.cloudstream3.utils.DataStoreHelper.getLastWatched
import com.lagradost.cloudstream3.utils.FillerEpisodeCheck.toClassDir
import com.lagradost.cloudstream3.utils.JsUnpacker.Companion.load
import com.lagradost.cloudstream3.utils.UIHelper.navigate
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
import okhttp3.Cache
import java.io.*
import java.net.URL
@@ -110,7 +110,8 @@ object AppUtils {
@SuppressLint("RestrictedApi")
private fun buildWatchNextProgramUri(
context: Context,
- card: DataStoreHelper.ResumeWatchingResult
+ card: DataStoreHelper.ResumeWatchingResult,
+ resumeWatching: VideoDownloadHelper.ResumeWatching?
): WatchNextProgram {
val isSeries = card.type?.isMovieType() == false
val title = if (isSeries) {
@@ -129,15 +130,18 @@ object AppUtils {
.setWatchNextType(TvContractCompat.WatchNextPrograms.WATCH_NEXT_TYPE_CONTINUE)
.setTitle(title)
.setPosterArtUri(Uri.parse(card.posterUrl))
- .setIntentUri(Uri.parse(card.url)) //TODO FIX intent
+ .setIntentUri(Uri.parse(card.id?.let {
+ "$appStringResumeWatching://$it"
+ } ?: card.url))
.setInternalProviderId(card.url)
- //.setLastEngagementTimeUtcMillis(System.currentTimeMillis())
+ .setLastEngagementTimeUtcMillis(
+ resumeWatching?.updateTime ?: System.currentTimeMillis()
+ )
card.watchPos?.let {
builder.setDurationMillis(it.duration.toInt())
builder.setLastPlaybackPositionMillis(it.position.toInt())
}
- // .setLastEngagementTimeUtcMillis() //TODO
if (isSeries)
card.episode?.let {
@@ -147,6 +151,27 @@ object AppUtils {
return builder.build()
}
+ @SuppressLint("RestrictedApi")
+ fun getAllWatchNextPrograms(context: Context): Set {
+ val COLUMN_WATCH_NEXT_ID_INDEX = 0
+ val cursor = context.contentResolver.query(
+ TvContractCompat.WatchNextPrograms.CONTENT_URI,
+ WatchNextProgram.PROJECTION,
+ /* selection = */ null,
+ /* selectionArgs = */ null,
+ /* sortOrder = */ null
+ )
+ val set = mutableSetOf()
+ cursor?.use {
+ if (it.moveToFirst()) {
+ do {
+ set.add(cursor.getLong(COLUMN_WATCH_NEXT_ID_INDEX))
+ } while (it.moveToNext())
+ }
+ }
+ return set
+ }
+
/**
* Find the Watch Next program for given id.
* Returns the first instance available.
@@ -164,7 +189,7 @@ object AppUtils {
WatchNextProgram.PROJECTION,
/* selection = */ null,
/* selectionArgs = */ null,
- /* sortOrder= */ null
+ /* sortOrder = */ null
)
cursor?.use {
if (it.moveToFirst()) {
@@ -195,17 +220,32 @@ object AppUtils {
}
}
+ /** Prevents losing data when removing and adding simultaneously */
+ private val continueWatchingLock = Mutex()
+
// https://github.com/googlearchive/leanback-homescreen-channels/blob/master/app/src/main/java/com/google/android/tvhomescreenchannels/SampleTvProvider.java
@SuppressLint("RestrictedApi")
@WorkerThread
- fun Context.addProgramsToContinueWatching(data: List) {
+ suspend fun Context.addProgramsToContinueWatching(data: List) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val context = this
- ioSafe {
- data.forEach { episodeInfo ->
+ continueWatchingLock.withLock {
+ // A way to get all last watched timestamps
+ val timeStampHashMap = HashMap()
+ getAllResumeStateIds()?.forEach { id ->
+ val lastWatched = getLastWatched(id) ?: return@forEach
+ timeStampHashMap[lastWatched.parentId] = lastWatched
+ }
+
+ val currentProgramIds = data.mapNotNull { episodeInfo ->
try {
- val (program, id) = getWatchNextProgramByVideoId(episodeInfo.url, context)
- val nextProgram = buildWatchNextProgramUri(context, episodeInfo)
+ val customId = "${episodeInfo.id}|${episodeInfo.apiName}|${episodeInfo.url}"
+ val (program, id) = getWatchNextProgramByVideoId(customId, context)
+ val nextProgram = buildWatchNextProgramUri(
+ context,
+ episodeInfo,
+ timeStampHashMap[episodeInfo.id]
+ )
// If the program is already in the Watch Next row, update it
if (program != null && id != null) {
@@ -213,13 +253,25 @@ object AppUtils {
nextProgram,
id,
)
+ id
} else {
PreviewChannelHelper(context)
.publishWatchNextProgram(nextProgram)
}
} catch (e: Exception) {
logError(e)
+ null
}
+ }.toSet()
+
+ val allOldPrograms = getAllWatchNextPrograms(context) - currentProgramIds
+
+ // Ensures synced watch next progress by deleting all old programs.
+ allOldPrograms.forEach {
+ context.contentResolver.delete(
+ TvContractCompat.buildWatchNextProgramUri(it),
+ null, null
+ )
}
}
}
@@ -267,7 +319,7 @@ object AppUtils {
fun Activity.downloadAllPluginsDialog(repositoryUrl: String, repositoryName: String) {
runOnUiThread {
val context = this
- val builder: AlertDialog.Builder = AlertDialog.Builder(this)
+ val builder: AlertDialog.Builder = AlertDialog.Builder(this, R.style.AlertDialogCustom)
builder.setTitle(
repositoryName
)
@@ -279,7 +331,7 @@ object AppUtils {
downloadAll(context, repositoryUrl, null)
}
- setNegativeButton(R.string.cancel) { _, _ -> }
+ setNegativeButton(R.string.no) { _, _ -> }
}
builder.show()
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index db042b95..4a4e6505 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -655,4 +655,8 @@
History
Show skip popups for opening/ending
Too much text. Unable to save to clipboard.
+ Mark as watched
+ Are you sure you want to exit?
+ Yes
+ No