forked from recloudstream/cloudstream
+ Added Confirm exit on Android TV
+ Added support for Play Next on Android TV
This commit is contained in:
parent
4f54bf3ae4
commit
95f4a15864
8 changed files with 182 additions and 59 deletions
|
@ -48,7 +48,7 @@ android {
|
||||||
targetSdk = 33
|
targetSdk = 33
|
||||||
|
|
||||||
versionCode = 55
|
versionCode = 55
|
||||||
versionName = "3.2.5"
|
versionName = "3.2.6"
|
||||||
|
|
||||||
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
|
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <!-- Used for player vertical slide -->
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <!-- Used for player vertical slide -->
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <!-- Used for app update -->
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <!-- Used for app update -->
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <!-- Used for app notifications on Android 13+ -->
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <!-- Used for app notifications on Android 13+ -->
|
||||||
<!-- <uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" /> not used atm, but code exist that requires it that are not run -->
|
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" /> <!-- not used atm, but code exist that requires it that are not run -->
|
||||||
<!-- <permission android:name="android.permission.QUERY_ALL_PACKAGES" /> <!– Used for getting if vlc is installed –> -->
|
<!-- <permission android:name="android.permission.QUERY_ALL_PACKAGES" /> <!– Used for getting if vlc is installed –> -->
|
||||||
<!-- Fixes android tv fuckery -->
|
<!-- Fixes android tv fuckery -->
|
||||||
<uses-feature
|
<uses-feature
|
||||||
|
@ -122,6 +122,19 @@
|
||||||
<data android:scheme="cloudstreamsearch" />
|
<data android:scheme="cloudstreamsearch" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Allow opening from continue watching with intents: cloudstreamsearch://1234
|
||||||
|
Used on Android TV Watch Next
|
||||||
|
-->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="cloudstreamcontinuewatching" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
|
|
@ -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.accountManagers
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringRepo
|
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.appStringSearch
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.inAppAuths
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.inAppAuths
|
||||||
import com.lagradost.cloudstream3.ui.APIRepository
|
import com.lagradost.cloudstream3.ui.APIRepository
|
||||||
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
|
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.SearchFragment
|
||||||
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
|
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
|
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.SettingsFragment.Companion.isTvSettings
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsGeneral
|
import com.lagradost.cloudstream3.ui.settings.SettingsGeneral
|
||||||
import com.lagradost.cloudstream3.ui.setup.HAS_DONE_SETUP_KEY
|
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.loadCache
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.loadRepository
|
import com.lagradost.cloudstream3.utils.AppUtils.loadRepository
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
|
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.BackupUtils.setUpBackup
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
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.getResourceColor
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
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.UIHelper.requestRW
|
||||||
import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
|
import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
|
||||||
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
|
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
|
||||||
|
@ -289,6 +295,19 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
||||||
nextSearchQuery =
|
nextSearchQuery =
|
||||||
URLDecoder.decode(str.substringAfter("$appStringSearch://"), "UTF-8")
|
URLDecoder.decode(str.substringAfter("$appStringSearch://"), "UTF-8")
|
||||||
nav_view.selectedItemId = R.id.navigation_search
|
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) {
|
} else if (!isWebview) {
|
||||||
if (str.startsWith(DOWNLOAD_NAVIGATE_TO)) {
|
if (str.startsWith(DOWNLOAD_NAVIGATE_TO)) {
|
||||||
this.navigate(R.id.navigation_downloads)
|
this.navigate(R.id.navigation_downloads)
|
||||||
|
@ -449,12 +468,33 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
||||||
onUserLeaveHint(this)
|
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() {
|
private fun backPressed() {
|
||||||
this.window?.navigationBarColor =
|
this.window?.navigationBarColor =
|
||||||
this.colorFromAttribute(R.attr.primaryGrayBackground)
|
this.colorFromAttribute(R.attr.primaryGrayBackground)
|
||||||
this.updateLocale()
|
this.updateLocale()
|
||||||
super.onBackPressed()
|
|
||||||
this.updateLocale()
|
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() {
|
override fun onBackPressed() {
|
||||||
|
|
|
@ -48,6 +48,9 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
|
||||||
// Instantly start the search given a query
|
// Instantly start the search given a query
|
||||||
const val appStringSearch = "cloudstreamsearch"
|
const val appStringSearch = "cloudstreamsearch"
|
||||||
|
|
||||||
|
// Instantly resume watching a show
|
||||||
|
const val appStringResumeWatching = "cloudstreamcontinuewatching"
|
||||||
|
|
||||||
val unixTime: Long
|
val unixTime: Long
|
||||||
get() = System.currentTimeMillis() / 1000L
|
get() = System.currentTimeMillis() / 1000L
|
||||||
val unixTimeMs: Long
|
val unixTimeMs: Long
|
||||||
|
|
|
@ -7,6 +7,7 @@ import android.content.DialogInterface
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
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.search.SearchHelper.handleSearchClickCallback
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
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.isRecyclerScrollable
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
|
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
|
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
|
||||||
|
@ -927,11 +929,13 @@ class HomeFragment : Fragment() {
|
||||||
resumeWatching
|
resumeWatching
|
||||||
)
|
)
|
||||||
|
|
||||||
//if (context?.isTvSettings() == true) {
|
if (isTrueTvSettings()) {
|
||||||
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
// context?.addProgramsToContinueWatching(resumeWatching.mapNotNull { it as? DataStoreHelper.ResumeWatchingResult })
|
ioSafe {
|
||||||
// }
|
activity?.addProgramsToContinueWatching(resumeWatching.mapNotNull { it as? DataStoreHelper.ResumeWatchingResult })
|
||||||
//}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
home_watch_child_more_info?.setOnClickListener {
|
home_watch_child_more_info?.setOnClickListener {
|
||||||
activity?.loadHomepageList(
|
activity?.loadHomepageList(
|
||||||
|
|
|
@ -36,6 +36,42 @@ import java.util.*
|
||||||
import kotlin.collections.set
|
import kotlin.collections.set
|
||||||
|
|
||||||
class HomeViewModel : ViewModel() {
|
class HomeViewModel : ViewModel() {
|
||||||
|
companion object {
|
||||||
|
suspend fun getResumeWatching(): List<DataStoreHelper.ResumeWatchingResult>? {
|
||||||
|
val resumeWatching = withContext(Dispatchers.IO) {
|
||||||
|
getAllResumeStateIds()?.mapNotNull { id ->
|
||||||
|
getLastWatched(id)
|
||||||
|
}?.sortedBy { -it.updateTime }
|
||||||
|
}
|
||||||
|
val resumeWatchingResult = withContext(Dispatchers.IO) {
|
||||||
|
resumeWatching?.mapNotNull { resume ->
|
||||||
|
|
||||||
|
val data = getKey<VideoDownloadHelper.DownloadHeaderCached>(
|
||||||
|
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 var repo: APIRepository? = null
|
||||||
|
|
||||||
private val _apiName = MutableLiveData<String>()
|
private val _apiName = MutableLiveData<String>()
|
||||||
|
@ -66,36 +102,7 @@ class HomeViewModel : ViewModel() {
|
||||||
val preview: LiveData<Resource<Pair<Boolean, List<LoadResponse>>>> = _preview
|
val preview: LiveData<Resource<Pair<Boolean, List<LoadResponse>>>> = _preview
|
||||||
|
|
||||||
fun loadResumeWatching() = viewModelScope.launchSafe {
|
fun loadResumeWatching() = viewModelScope.launchSafe {
|
||||||
val resumeWatching = withContext(Dispatchers.IO) {
|
val resumeWatchingResult = getResumeWatching()
|
||||||
getAllResumeStateIds()?.mapNotNull { id ->
|
|
||||||
getLastWatched(id)
|
|
||||||
}?.sortedBy { -it.updateTime }
|
|
||||||
}
|
|
||||||
|
|
||||||
// val resumeWatchingResult = ArrayList<DataStoreHelper.ResumeWatchingResult>()
|
|
||||||
|
|
||||||
val resumeWatchingResult = withContext(Dispatchers.IO) {
|
|
||||||
resumeWatching?.map { resume ->
|
|
||||||
val data = getKey<VideoDownloadHelper.DownloadHeaderCached>(
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
resumeWatchingResult?.let {
|
resumeWatchingResult?.let {
|
||||||
_resumeWatching.postValue(it)
|
_resumeWatching.postValue(it)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,7 @@ package com.lagradost.cloudstream3.utils
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.Activity.RESULT_CANCELED
|
import android.app.Activity.RESULT_CANCELED
|
||||||
import android.content.ContentValues
|
import android.content.*
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.media.AudioAttributes
|
import android.media.AudioAttributes
|
||||||
|
@ -26,7 +24,6 @@ import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.text.HtmlCompat
|
import androidx.core.text.HtmlCompat
|
||||||
import androidx.core.text.toSpanned
|
import androidx.core.text.toSpanned
|
||||||
|
@ -35,9 +32,7 @@ import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.tvprovider.media.tv.PreviewChannelHelper
|
import androidx.tvprovider.media.tv.*
|
||||||
import androidx.tvprovider.media.tv.TvContractCompat
|
|
||||||
import androidx.tvprovider.media.tv.WatchNextProgram
|
|
||||||
import androidx.tvprovider.media.tv.WatchNextProgram.fromCursor
|
import androidx.tvprovider.media.tv.WatchNextProgram.fromCursor
|
||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
import com.google.android.gms.cast.framework.CastContext
|
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.logError
|
||||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||||
import com.lagradost.cloudstream3.plugins.RepositoryManager
|
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.WebviewFragment
|
||||||
import com.lagradost.cloudstream3.ui.result.ResultFragment
|
import com.lagradost.cloudstream3.ui.result.ResultFragment
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
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.ui.settings.extensions.RepositoryData
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
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.FillerEpisodeCheck.toClassDir
|
||||||
import com.lagradost.cloudstream3.utils.JsUnpacker.Companion.load
|
import com.lagradost.cloudstream3.utils.JsUnpacker.Companion.load
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import okhttp3.Cache
|
import okhttp3.Cache
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
@ -110,7 +110,8 @@ object AppUtils {
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
private fun buildWatchNextProgramUri(
|
private fun buildWatchNextProgramUri(
|
||||||
context: Context,
|
context: Context,
|
||||||
card: DataStoreHelper.ResumeWatchingResult
|
card: DataStoreHelper.ResumeWatchingResult,
|
||||||
|
resumeWatching: VideoDownloadHelper.ResumeWatching?
|
||||||
): WatchNextProgram {
|
): WatchNextProgram {
|
||||||
val isSeries = card.type?.isMovieType() == false
|
val isSeries = card.type?.isMovieType() == false
|
||||||
val title = if (isSeries) {
|
val title = if (isSeries) {
|
||||||
|
@ -129,15 +130,18 @@ object AppUtils {
|
||||||
.setWatchNextType(TvContractCompat.WatchNextPrograms.WATCH_NEXT_TYPE_CONTINUE)
|
.setWatchNextType(TvContractCompat.WatchNextPrograms.WATCH_NEXT_TYPE_CONTINUE)
|
||||||
.setTitle(title)
|
.setTitle(title)
|
||||||
.setPosterArtUri(Uri.parse(card.posterUrl))
|
.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)
|
.setInternalProviderId(card.url)
|
||||||
//.setLastEngagementTimeUtcMillis(System.currentTimeMillis())
|
.setLastEngagementTimeUtcMillis(
|
||||||
|
resumeWatching?.updateTime ?: System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
|
||||||
card.watchPos?.let {
|
card.watchPos?.let {
|
||||||
builder.setDurationMillis(it.duration.toInt())
|
builder.setDurationMillis(it.duration.toInt())
|
||||||
builder.setLastPlaybackPositionMillis(it.position.toInt())
|
builder.setLastPlaybackPositionMillis(it.position.toInt())
|
||||||
}
|
}
|
||||||
// .setLastEngagementTimeUtcMillis() //TODO
|
|
||||||
|
|
||||||
if (isSeries)
|
if (isSeries)
|
||||||
card.episode?.let {
|
card.episode?.let {
|
||||||
|
@ -147,6 +151,27 @@ object AppUtils {
|
||||||
return builder.build()
|
return builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
|
fun getAllWatchNextPrograms(context: Context): Set<Long> {
|
||||||
|
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<Long>()
|
||||||
|
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.
|
* Find the Watch Next program for given id.
|
||||||
* Returns the first instance available.
|
* Returns the first instance available.
|
||||||
|
@ -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
|
// https://github.com/googlearchive/leanback-homescreen-channels/blob/master/app/src/main/java/com/google/android/tvhomescreenchannels/SampleTvProvider.java
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
fun Context.addProgramsToContinueWatching(data: List<DataStoreHelper.ResumeWatchingResult>) {
|
suspend fun Context.addProgramsToContinueWatching(data: List<DataStoreHelper.ResumeWatchingResult>) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||||
val context = this
|
val context = this
|
||||||
ioSafe {
|
continueWatchingLock.withLock {
|
||||||
data.forEach { episodeInfo ->
|
// A way to get all last watched timestamps
|
||||||
|
val timeStampHashMap = HashMap<Int, VideoDownloadHelper.ResumeWatching>()
|
||||||
|
getAllResumeStateIds()?.forEach { id ->
|
||||||
|
val lastWatched = getLastWatched(id) ?: return@forEach
|
||||||
|
timeStampHashMap[lastWatched.parentId] = lastWatched
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentProgramIds = data.mapNotNull { episodeInfo ->
|
||||||
try {
|
try {
|
||||||
val (program, id) = getWatchNextProgramByVideoId(episodeInfo.url, context)
|
val customId = "${episodeInfo.id}|${episodeInfo.apiName}|${episodeInfo.url}"
|
||||||
val nextProgram = buildWatchNextProgramUri(context, episodeInfo)
|
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 the program is already in the Watch Next row, update it
|
||||||
if (program != null && id != null) {
|
if (program != null && id != null) {
|
||||||
|
@ -213,13 +253,25 @@ object AppUtils {
|
||||||
nextProgram,
|
nextProgram,
|
||||||
id,
|
id,
|
||||||
)
|
)
|
||||||
|
id
|
||||||
} else {
|
} else {
|
||||||
PreviewChannelHelper(context)
|
PreviewChannelHelper(context)
|
||||||
.publishWatchNextProgram(nextProgram)
|
.publishWatchNextProgram(nextProgram)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logError(e)
|
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) {
|
fun Activity.downloadAllPluginsDialog(repositoryUrl: String, repositoryName: String) {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
val context = this
|
val context = this
|
||||||
val builder: AlertDialog.Builder = AlertDialog.Builder(this)
|
val builder: AlertDialog.Builder = AlertDialog.Builder(this, R.style.AlertDialogCustom)
|
||||||
builder.setTitle(
|
builder.setTitle(
|
||||||
repositoryName
|
repositoryName
|
||||||
)
|
)
|
||||||
|
@ -279,7 +331,7 @@ object AppUtils {
|
||||||
downloadAll(context, repositoryUrl, null)
|
downloadAll(context, repositoryUrl, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
setNegativeButton(R.string.cancel) { _, _ -> }
|
setNegativeButton(R.string.no) { _, _ -> }
|
||||||
}
|
}
|
||||||
builder.show()
|
builder.show()
|
||||||
}
|
}
|
||||||
|
|
|
@ -655,4 +655,8 @@
|
||||||
<string name="history">History</string>
|
<string name="history">History</string>
|
||||||
<string name="enable_skip_op_from_database_des">Show skip popups for opening/ending</string>
|
<string name="enable_skip_op_from_database_des">Show skip popups for opening/ending</string>
|
||||||
<string name="clipboard_too_large">Too much text. Unable to save to clipboard.</string>
|
<string name="clipboard_too_large">Too much text. Unable to save to clipboard.</string>
|
||||||
|
<string name="action_mark_as_watched">Mark as watched</string>
|
||||||
|
<string name="confirm_exit_dialog">Are you sure you want to exit?</string>
|
||||||
|
<string name="yes">Yes</string>
|
||||||
|
<string name="no">No</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Reference in a new issue