From 1bbbbce326f7dc18001fa9f00b954aa068c40aac Mon Sep 17 00:00:00 2001
From: LagradOst <11805592+LagradOst@users.noreply.github.com>
Date: Sun, 13 Feb 2022 01:53:40 +0100
Subject: [PATCH] android tv recc not working :(
---
app/build.gradle | 11 +-
app/src/main/AndroidManifest.xml | 2 +
.../ui/home/HomeChildItemAdapter.kt | 15 +-
.../cloudstream3/ui/home/HomeFragment.kt | 28 +--
.../ui/home/HomeParentItemAdapter.kt | 4 +-
.../cloudstream3/ui/result/ResultFragment.kt | 6 +-
.../lagradost/cloudstream3/utils/AppUtils.kt | 165 ++++++++++++++++--
7 files changed, 197 insertions(+), 34 deletions(-)
diff --git a/app/build.gradle b/app/build.gradle
index 542a11ad..96c54f37 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -88,6 +88,7 @@ repositories {
}
dependencies {
+ implementation 'com.google.android.mediahome:video:1.0.0'
testImplementation 'org.json:json:20180813'
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
@@ -95,10 +96,10 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
- implementation 'androidx.navigation:navigation-fragment-ktx:2.5.0-alpha01'
- implementation 'androidx.navigation:navigation-ui-ktx:2.5.0-alpha01'
- implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.0'
- implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'
+ implementation 'androidx.navigation:navigation-fragment-ktx:2.5.0-alpha02'
+ implementation 'androidx.navigation:navigation-ui-ktx:2.5.0-alpha02'
+ implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
@@ -167,4 +168,6 @@ dependencies {
// for shimmer when loading
implementation 'com.facebook.shimmer:shimmer:0.5.0'
+
+ implementation "androidx.tvprovider:tvprovider:1.0.0"
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 22dab297..ac05b212 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -9,6 +9,7 @@
+
@@ -137,6 +138,7 @@
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
+ android:enabled="true"
android:grantUriPermissions="true">
,
+ val cardList: MutableList,
val layout: Int = R.layout.home_result_grid,
private val nextFocusUp: Int? = null,
private val nextFocusDown: Int? = null,
@@ -44,6 +46,17 @@ class HomeChildItemAdapter(
return (cardList[position].id ?: position).toLong()
}
+ fun updateList(newList: List) {
+ val diffResult = DiffUtil.calculateDiff(
+ SearchResponseDiffCallback(this.cardList, newList)
+ )
+
+ cardList.clear()
+ cardList.addAll(newList)
+
+ diffResult.dispatchUpdatesTo(this)
+ }
+
class CardViewHolder
constructor(
itemView: View, private val clickCallback: (SearchClickCallback) -> Unit, private val itemCount: Int,
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 3d797c05..488a9015 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
@@ -124,7 +124,7 @@ class HomeFragment : Fragment() {
val spanListener = { span: Int ->
recycle.spanCount = span
- (recycle.adapter as SearchAdapter).notifyDataSetChanged()
+ //(recycle.adapter as SearchAdapter).notifyDataSetChanged()
}
configEvent += spanListener
@@ -133,7 +133,7 @@ class HomeFragment : Fragment() {
configEvent -= spanListener
}
- (recycle.adapter as SearchAdapter).notifyDataSetChanged()
+ //(recycle.adapter as SearchAdapter).notifyDataSetChanged()
bottomSheetDialogBuilder.show()
}
@@ -399,7 +399,7 @@ class HomeFragment : Fragment() {
val randomSize = items.size
home_main_poster_recyclerview?.adapter =
HomeChildItemAdapter(
- items,
+ items.toMutableList(),
R.layout.home_result_big_grid,
nextFocusUp = home_main_poster_recyclerview.nextFocusUpId,
nextFocusDown = home_main_poster_recyclerview.nextFocusDownId
@@ -438,7 +438,7 @@ class HomeFragment : Fragment() {
val d = data.value
currentHomePage = d
- (home_master_recycler?.adapter as ParentItemAdapter?)?.updateList(
+ (home_master_recycler?.adapter as? ParentItemAdapter?)?.updateList(
d?.items?.mapNotNull {
try {
HomePageList(it.name, it.list.filterSearchResponse())
@@ -483,6 +483,7 @@ class HomeFragment : Fragment() {
home_loaded?.isVisible = false
}
is Resource.Loading -> {
+ (home_master_recycler?.adapter as? ParentItemAdapter?)?.updateList(listOf())
home_loading_shimmer?.startShimmer()
home_loading?.isVisible = true
home_loading_error?.isVisible = false
@@ -556,13 +557,12 @@ class HomeFragment : Fragment() {
home_bookmarked_parent_item_title?.text = getString(availableWatchStatusTypes.first.stringRes)*/
}
- observe(homeViewModel.bookmarks) { pair ->
- home_bookmarked_holder.isVisible = pair.first
+ observe(homeViewModel.bookmarks) { (isVis, bookmarks) ->
+ home_bookmarked_holder.isVisible = isVis
- val bookmarks = pair.second
- (home_bookmarked_child_recyclerview?.adapter as HomeChildItemAdapter?)?.cardList =
+ (home_bookmarked_child_recyclerview?.adapter as? HomeChildItemAdapter?)?.updateList(
bookmarks
- home_bookmarked_child_recyclerview?.adapter?.notifyDataSetChanged()
+ )
home_bookmarked_child_more_info?.setOnClickListener {
activity?.loadHomepageList(
@@ -576,9 +576,15 @@ class HomeFragment : Fragment() {
observe(homeViewModel.resumeWatching) { resumeWatching ->
home_watch_holder?.isVisible = resumeWatching.isNotEmpty()
- (home_watch_child_recyclerview?.adapter as HomeChildItemAdapter?)?.cardList =
+ (home_watch_child_recyclerview?.adapter as? HomeChildItemAdapter?)?.updateList(
resumeWatching
- home_watch_child_recyclerview?.adapter?.notifyDataSetChanged()
+ )
+
+ //if (context?.isTvSettings() == true) {
+ // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ // context?.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/HomeParentItemAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt
index 8425dcca..58df0b49 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt
@@ -65,12 +65,12 @@ class ParentItemAdapter(
fun bind(info: HomePageList) {
title.text = info.name
recyclerView.adapter = HomeChildItemAdapter(
- info.list,
+ info.list.toMutableList(),
clickCallback = clickCallback,
nextFocusUp = recyclerView.nextFocusUpId,
nextFocusDown = recyclerView.nextFocusDownId
)
- (recyclerView.adapter as HomeChildItemAdapter).notifyDataSetChanged()
+ //(recyclerView.adapter as HomeChildItemAdapter).notifyDataSetChanged()
moreInfo.setOnClickListener {
moreInfoClickCallback.invoke(info)
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt
index e61d3f81..05a29440 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt
@@ -1183,9 +1183,9 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
result_episode_loading?.isVisible = false
if (result_episodes == null || result_episodes.adapter == null) return@observe
currentEpisodes = episodes.value
- (result_episodes?.adapter as EpisodeAdapter?)?.cardList = episodes.value
- (result_episodes?.adapter as EpisodeAdapter?)?.updateLayout()
- (result_episodes?.adapter as EpisodeAdapter?)?.notifyDataSetChanged()
+ (result_episodes?.adapter as? EpisodeAdapter?)?.cardList = episodes.value
+ (result_episodes?.adapter as? EpisodeAdapter?)?.updateLayout()
+ (result_episodes?.adapter as? EpisodeAdapter?)?.notifyDataSetChanged()
}
}
}
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 0dce75c2..3b2f6592 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt
@@ -1,5 +1,6 @@
package com.lagradost.cloudstream3.utils
+import android.annotation.SuppressLint
import android.app.Activity
import android.content.ComponentName
import android.content.ContentValues
@@ -10,6 +11,7 @@ import android.database.Cursor
import android.media.AudioAttributes
import android.media.AudioFocusRequest
import android.media.AudioManager
+import android.media.tv.TvContract.Channels.COLUMN_INTERNAL_PROVIDER_ID
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.Uri
@@ -18,20 +20,23 @@ import android.os.Environment
import android.os.ParcelFileDescriptor
import android.provider.MediaStore
import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.annotation.WorkerThread
import androidx.appcompat.app.AppCompatActivity
+import androidx.tvprovider.media.tv.PreviewChannelHelper
+import androidx.tvprovider.media.tv.TvContractCompat
+import androidx.tvprovider.media.tv.WatchNextProgram
+import androidx.tvprovider.media.tv.WatchNextProgram.fromCursor
import com.fasterxml.jackson.module.kotlin.readValue
import com.google.android.gms.cast.framework.CastContext
import com.google.android.gms.cast.framework.CastState
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.gms.common.wrappers.Wrappers
-import com.lagradost.cloudstream3.MainActivity
-import com.lagradost.cloudstream3.R
-import com.lagradost.cloudstream3.SearchResponse
-import com.lagradost.cloudstream3.mapper
-import com.lagradost.cloudstream3.movieproviders.MeloMovieProvider
+import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.result.ResultFragment
+import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.FillerEpisodeCheck.toClassDir
import com.lagradost.cloudstream3.utils.JsUnpacker.Companion.load
import com.lagradost.cloudstream3.utils.UIHelper.navigate
@@ -41,6 +46,136 @@ import java.net.URL
import java.net.URLDecoder
object AppUtils {
+ //fun Context.deleteFavorite(data: SearchResponse) {
+ // if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
+ // normalSafeApiCall {
+ // val existingId =
+ // getWatchNextProgramByVideoId(data.url, this).second ?: return@normalSafeApiCall
+ // contentResolver.delete(
+//
+ // TvContractCompat.buildWatchNextProgramUri(existingId),
+ // null, null
+ // )
+ // }
+ //}
+ @SuppressLint("RestrictedApi")
+ private fun buildWatchNextProgramUri(
+ context: Context,
+ card: DataStoreHelper.ResumeWatchingResult
+ ): WatchNextProgram {
+ val isSeries = !card.type.isMovieType()
+ val title = if (isSeries) {
+ context.getNameFull(card.name, card.episode, card.season)
+ } else {
+ card.name
+ }
+
+ val builder = WatchNextProgram.Builder()
+ .setEpisodeTitle(title)
+ .setType(
+ if (isSeries) {
+ TvContractCompat.WatchNextPrograms.TYPE_TV_EPISODE
+ } else TvContractCompat.WatchNextPrograms.TYPE_MOVIE
+ )
+ .setWatchNextType(TvContractCompat.WatchNextPrograms.WATCH_NEXT_TYPE_CONTINUE)
+ .setTitle(title)
+ .setPosterArtUri(Uri.parse(card.posterUrl))
+ .setIntentUri(Uri.parse(card.url)) //TODO FIX intent
+ .setInternalProviderId(card.url)
+ //.setLastEngagementTimeUtcMillis(System.currentTimeMillis())
+
+ card.watchPos?.let {
+ builder.setDurationMillis(it.duration.toInt())
+ builder.setLastPlaybackPositionMillis(it.position.toInt())
+ }
+ // .setLastEngagementTimeUtcMillis() //TODO
+
+ if (isSeries)
+ card.episode?.let {
+ builder.setEpisodeNumber(it)
+ }
+
+ return builder.build()
+ }
+
+ /**
+ * Find the Watch Next program for given id.
+ * Returns the first instance available.
+ */
+ @SuppressLint("RestrictedApi")
+ // Suppress RestrictedApi due to https://issuetracker.google.com/138150076
+ fun findFirstWatchNextProgram(context: Context, predicate: (Cursor) -> Boolean):
+ Pair {
+ val COLUMN_WATCH_NEXT_ID_INDEX = 0
+// val COLUMN_WATCH_NEXT_INTERNAL_PROVIDER_ID_INDEX = 1
+// val COLUMN_WATCH_NEXT_COLUMN_BROWSABLE_INDEX = 2
+
+ val cursor = context.contentResolver.query(
+ TvContractCompat.WatchNextPrograms.CONTENT_URI,
+ WatchNextProgram.PROJECTION,
+ /* selection = */ null,
+ /* selectionArgs = */ null,
+ /* sortOrder= */ null
+ )
+ cursor?.use {
+ if (it.moveToFirst()) {
+ do {
+ if (predicate(cursor)) {
+ return fromCursor(cursor) to cursor.getLong(COLUMN_WATCH_NEXT_ID_INDEX)
+ }
+ } while (it.moveToNext())
+ }
+ }
+ return null to null
+ }
+
+ /**
+ * Query the Watch Next list and find the program with given videoId.
+ * Return null if not found.
+ */
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ @SuppressLint("Range")
+ @Synchronized
+ private fun getWatchNextProgramByVideoId(
+ id: String,
+ context: Context
+ ): Pair {
+ return findFirstWatchNextProgram(context) { cursor ->
+ (cursor.getString(cursor.getColumnIndex(COLUMN_INTERNAL_PROVIDER_ID)) == id)
+ }
+ }
+
+ // 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) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
+
+ ioSafe {
+ data.forEach { episodeInfo ->
+ try {
+ val (program, id) = getWatchNextProgramByVideoId(episodeInfo.url, this)
+ val nextProgram = buildWatchNextProgramUri(this, episodeInfo)
+
+ // If the program is already in the Watch Next row, update it
+ if (program != null && id != null) {
+ PreviewChannelHelper(this).updateWatchNextProgram(
+ nextProgram,
+ id,
+ )
+ } else {
+ PreviewChannelHelper(this)
+ .publishWatchNextProgram(nextProgram)
+ }
+ } catch (e: Exception) {
+ logError(e)
+ }
+ }
+ }
+ }
+
+ @SuppressLint("Range")
fun getVideoContentUri(context: Context, videoFilePath: String): Uri? {
val cursor = context.contentResolver.query(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI, arrayOf(MediaStore.Video.Media._ID),
@@ -94,14 +229,14 @@ object AppUtils {
return mapper.writeValueAsString(this)
}
- inline fun parseJson(value : String): T {
+ inline fun parseJson(value: String): T {
return mapper.readValue(value)
}
- inline fun tryParseJson(value : String): T? {
+ inline fun tryParseJson(value: String): T? {
return try {
parseJson(value)
- } catch (_ : Exception) {
+ } catch (_: Exception) {
null
}
}
@@ -168,7 +303,7 @@ object AppUtils {
startAction: Int = 0,
startValue: Int = 0
) {
- (this as AppCompatActivity?)?.loadResult(card.url, card.apiName, startAction, startValue)
+ (this as? AppCompatActivity?)?.loadResult(card.url, card.apiName, startAction, startValue)
}
fun Activity.requestLocalAudioFocus(focusRequest: AudioFocusRequest?) {
@@ -230,6 +365,7 @@ object AppUtils {
}
// Copied from https://github.com/videolan/vlc-android/blob/master/application/vlc-android/src/org/videolan/vlc/util/FileUtils.kt
+ @SuppressLint("Range")
fun Context.getUri(data: Uri?): Uri? {
var uri = data
val ctx = this
@@ -245,11 +381,13 @@ object AppUtils {
arrayOf(MediaStore.MediaColumns.DISPLAY_NAME), null, null, null
)
if (cursor != null && cursor.moveToFirst()) {
- val filename = cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME))
- .replace("/", "")
+ val filename =
+ cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME))
+ .replace("/", "")
inputStream = ctx.contentResolver.openInputStream(data)
if (inputStream == null) return data
- os = FileOutputStream(Environment.getExternalStorageDirectory().path + "/Download/" + filename)
+ os =
+ FileOutputStream(Environment.getExternalStorageDirectory().path + "/Download/" + filename)
val buffer = ByteArray(1024)
var bytesRead = inputStream.read(buffer)
while (bytesRead >= 0) {
@@ -272,7 +410,8 @@ object AppUtils {
arrayOf(MediaStore.Video.Media.DATA), null, null, null
)?.use {
val columnIndex = it.getColumnIndexOrThrow(MediaStore.Video.Media.DATA)
- if (it.moveToFirst()) Uri.fromFile(File(it.getString(columnIndex))) ?: data else data
+ if (it.moveToFirst()) Uri.fromFile(File(it.getString(columnIndex)))
+ ?: data else data
}
//uri = MediaUtils.getContentMediaUri(data)
/*} else if (data.authority == ctx.getString(R.string.tv_provider_authority)) {