android tv recc not working :(

This commit is contained in:
LagradOst 2022-02-13 01:53:40 +01:00
parent c5d53d7621
commit 1bbbbce326
7 changed files with 197 additions and 34 deletions

View file

@ -88,6 +88,7 @@ repositories {
} }
dependencies { dependencies {
implementation 'com.google.android.mediahome:video:1.0.0'
testImplementation 'org.json:json:20180813' testImplementation 'org.json:json:20180813'
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
@ -95,10 +96,10 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.4.1' implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0' implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3' implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.0-alpha01' implementation 'androidx.navigation:navigation-fragment-ktx:2.5.0-alpha02'
implementation 'androidx.navigation:navigation-ui-ktx:2.5.0-alpha01' implementation 'androidx.navigation:navigation-ui-ktx:2.5.0-alpha02'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.0' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
@ -167,4 +168,6 @@ dependencies {
// for shimmer when loading // for shimmer when loading
implementation 'com.facebook.shimmer:shimmer:0.5.0' implementation 'com.facebook.shimmer:shimmer:0.5.0'
implementation "androidx.tvprovider:tvprovider:1.0.0"
} }

View file

@ -9,6 +9,7 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />
<uses-feature android:name="android.hardware.touchscreen" <uses-feature android:name="android.hardware.touchscreen"
android:required="false"/> android:required="false"/>
@ -137,6 +138,7 @@
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider" android:authorities="${applicationId}.provider"
android:exported="false" android:exported="false"
android:enabled="true"
android:grantUriPermissions="true"> android:grantUriPermissions="true">
<meta-data <meta-data
android:name="android.support.FILE_PROVIDER_PATHS" android:name="android.support.FILE_PROVIDER_PATHS"

View file

@ -3,14 +3,16 @@ package com.lagradost.cloudstream3.ui.home
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import com.lagradost.cloudstream3.ui.search.SearchResponseDiffCallback
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
class HomeChildItemAdapter( class HomeChildItemAdapter(
var cardList: List<SearchResponse>, val cardList: MutableList<SearchResponse>,
val layout: Int = R.layout.home_result_grid, val layout: Int = R.layout.home_result_grid,
private val nextFocusUp: Int? = null, private val nextFocusUp: Int? = null,
private val nextFocusDown: Int? = null, private val nextFocusDown: Int? = null,
@ -44,6 +46,17 @@ class HomeChildItemAdapter(
return (cardList[position].id ?: position).toLong() return (cardList[position].id ?: position).toLong()
} }
fun updateList(newList: List<SearchResponse>) {
val diffResult = DiffUtil.calculateDiff(
SearchResponseDiffCallback(this.cardList, newList)
)
cardList.clear()
cardList.addAll(newList)
diffResult.dispatchUpdatesTo(this)
}
class CardViewHolder class CardViewHolder
constructor( constructor(
itemView: View, private val clickCallback: (SearchClickCallback) -> Unit, private val itemCount: Int, itemView: View, private val clickCallback: (SearchClickCallback) -> Unit, private val itemCount: Int,

View file

@ -124,7 +124,7 @@ class HomeFragment : Fragment() {
val spanListener = { span: Int -> val spanListener = { span: Int ->
recycle.spanCount = span recycle.spanCount = span
(recycle.adapter as SearchAdapter).notifyDataSetChanged() //(recycle.adapter as SearchAdapter).notifyDataSetChanged()
} }
configEvent += spanListener configEvent += spanListener
@ -133,7 +133,7 @@ class HomeFragment : Fragment() {
configEvent -= spanListener configEvent -= spanListener
} }
(recycle.adapter as SearchAdapter).notifyDataSetChanged() //(recycle.adapter as SearchAdapter).notifyDataSetChanged()
bottomSheetDialogBuilder.show() bottomSheetDialogBuilder.show()
} }
@ -399,7 +399,7 @@ class HomeFragment : Fragment() {
val randomSize = items.size val randomSize = items.size
home_main_poster_recyclerview?.adapter = home_main_poster_recyclerview?.adapter =
HomeChildItemAdapter( HomeChildItemAdapter(
items, items.toMutableList(),
R.layout.home_result_big_grid, R.layout.home_result_big_grid,
nextFocusUp = home_main_poster_recyclerview.nextFocusUpId, nextFocusUp = home_main_poster_recyclerview.nextFocusUpId,
nextFocusDown = home_main_poster_recyclerview.nextFocusDownId nextFocusDown = home_main_poster_recyclerview.nextFocusDownId
@ -438,7 +438,7 @@ class HomeFragment : Fragment() {
val d = data.value val d = data.value
currentHomePage = d currentHomePage = d
(home_master_recycler?.adapter as ParentItemAdapter?)?.updateList( (home_master_recycler?.adapter as? ParentItemAdapter?)?.updateList(
d?.items?.mapNotNull { d?.items?.mapNotNull {
try { try {
HomePageList(it.name, it.list.filterSearchResponse()) HomePageList(it.name, it.list.filterSearchResponse())
@ -483,6 +483,7 @@ class HomeFragment : Fragment() {
home_loaded?.isVisible = false home_loaded?.isVisible = false
} }
is Resource.Loading -> { is Resource.Loading -> {
(home_master_recycler?.adapter as? ParentItemAdapter?)?.updateList(listOf())
home_loading_shimmer?.startShimmer() home_loading_shimmer?.startShimmer()
home_loading?.isVisible = true home_loading?.isVisible = true
home_loading_error?.isVisible = false home_loading_error?.isVisible = false
@ -556,13 +557,12 @@ class HomeFragment : Fragment() {
home_bookmarked_parent_item_title?.text = getString(availableWatchStatusTypes.first.stringRes)*/ home_bookmarked_parent_item_title?.text = getString(availableWatchStatusTypes.first.stringRes)*/
} }
observe(homeViewModel.bookmarks) { pair -> observe(homeViewModel.bookmarks) { (isVis, bookmarks) ->
home_bookmarked_holder.isVisible = pair.first home_bookmarked_holder.isVisible = isVis
val bookmarks = pair.second (home_bookmarked_child_recyclerview?.adapter as? HomeChildItemAdapter?)?.updateList(
(home_bookmarked_child_recyclerview?.adapter as HomeChildItemAdapter?)?.cardList =
bookmarks bookmarks
home_bookmarked_child_recyclerview?.adapter?.notifyDataSetChanged() )
home_bookmarked_child_more_info?.setOnClickListener { home_bookmarked_child_more_info?.setOnClickListener {
activity?.loadHomepageList( activity?.loadHomepageList(
@ -576,9 +576,15 @@ class HomeFragment : Fragment() {
observe(homeViewModel.resumeWatching) { resumeWatching -> observe(homeViewModel.resumeWatching) { resumeWatching ->
home_watch_holder?.isVisible = resumeWatching.isNotEmpty() home_watch_holder?.isVisible = resumeWatching.isNotEmpty()
(home_watch_child_recyclerview?.adapter as HomeChildItemAdapter?)?.cardList = (home_watch_child_recyclerview?.adapter as? HomeChildItemAdapter?)?.updateList(
resumeWatching 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 { home_watch_child_more_info?.setOnClickListener {
activity?.loadHomepageList( activity?.loadHomepageList(

View file

@ -65,12 +65,12 @@ class ParentItemAdapter(
fun bind(info: HomePageList) { fun bind(info: HomePageList) {
title.text = info.name title.text = info.name
recyclerView.adapter = HomeChildItemAdapter( recyclerView.adapter = HomeChildItemAdapter(
info.list, info.list.toMutableList(),
clickCallback = clickCallback, clickCallback = clickCallback,
nextFocusUp = recyclerView.nextFocusUpId, nextFocusUp = recyclerView.nextFocusUpId,
nextFocusDown = recyclerView.nextFocusDownId nextFocusDown = recyclerView.nextFocusDownId
) )
(recyclerView.adapter as HomeChildItemAdapter).notifyDataSetChanged() //(recyclerView.adapter as HomeChildItemAdapter).notifyDataSetChanged()
moreInfo.setOnClickListener { moreInfo.setOnClickListener {
moreInfoClickCallback.invoke(info) moreInfoClickCallback.invoke(info)

View file

@ -1183,9 +1183,9 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
result_episode_loading?.isVisible = false result_episode_loading?.isVisible = false
if (result_episodes == null || result_episodes.adapter == null) return@observe if (result_episodes == null || result_episodes.adapter == null) return@observe
currentEpisodes = episodes.value currentEpisodes = episodes.value
(result_episodes?.adapter as EpisodeAdapter?)?.cardList = episodes.value (result_episodes?.adapter as? EpisodeAdapter?)?.cardList = episodes.value
(result_episodes?.adapter as EpisodeAdapter?)?.updateLayout() (result_episodes?.adapter as? EpisodeAdapter?)?.updateLayout()
(result_episodes?.adapter as EpisodeAdapter?)?.notifyDataSetChanged() (result_episodes?.adapter as? EpisodeAdapter?)?.notifyDataSetChanged()
} }
} }
} }

View file

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3.utils package com.lagradost.cloudstream3.utils
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.ComponentName import android.content.ComponentName
import android.content.ContentValues import android.content.ContentValues
@ -10,6 +11,7 @@ import android.database.Cursor
import android.media.AudioAttributes import android.media.AudioAttributes
import android.media.AudioFocusRequest import android.media.AudioFocusRequest
import android.media.AudioManager import android.media.AudioManager
import android.media.tv.TvContract.Channels.COLUMN_INTERNAL_PROVIDER_ID
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.net.Uri import android.net.Uri
@ -18,20 +20,23 @@ import android.os.Environment
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi
import androidx.annotation.WorkerThread
import androidx.appcompat.app.AppCompatActivity 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.fasterxml.jackson.module.kotlin.readValue
import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.cast.framework.CastContext
import com.google.android.gms.cast.framework.CastState import com.google.android.gms.cast.framework.CastState
import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.gms.common.wrappers.Wrappers import com.google.android.gms.common.wrappers.Wrappers
import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.*
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.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.result.ResultFragment 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.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
@ -41,6 +46,136 @@ import java.net.URL
import java.net.URLDecoder import java.net.URLDecoder
object AppUtils { 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<WatchNextProgram?, Long?> {
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<WatchNextProgram?, Long?> {
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<DataStoreHelper.ResumeWatchingResult>) {
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? { fun getVideoContentUri(context: Context, videoFilePath: String): Uri? {
val cursor = context.contentResolver.query( val cursor = context.contentResolver.query(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI, arrayOf(MediaStore.Video.Media._ID), MediaStore.Video.Media.EXTERNAL_CONTENT_URI, arrayOf(MediaStore.Video.Media._ID),
@ -94,14 +229,14 @@ object AppUtils {
return mapper.writeValueAsString(this) return mapper.writeValueAsString(this)
} }
inline fun <reified T> parseJson(value : String): T { inline fun <reified T> parseJson(value: String): T {
return mapper.readValue(value) return mapper.readValue(value)
} }
inline fun <reified T> tryParseJson(value : String): T? { inline fun <reified T> tryParseJson(value: String): T? {
return try { return try {
parseJson(value) parseJson(value)
} catch (_ : Exception) { } catch (_: Exception) {
null null
} }
} }
@ -168,7 +303,7 @@ object AppUtils {
startAction: Int = 0, startAction: Int = 0,
startValue: 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?) { 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 // 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? { fun Context.getUri(data: Uri?): Uri? {
var uri = data var uri = data
val ctx = this val ctx = this
@ -245,11 +381,13 @@ object AppUtils {
arrayOf(MediaStore.MediaColumns.DISPLAY_NAME), null, null, null arrayOf(MediaStore.MediaColumns.DISPLAY_NAME), null, null, null
) )
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {
val filename = cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME)) val filename =
.replace("/", "") cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME))
.replace("/", "")
inputStream = ctx.contentResolver.openInputStream(data) inputStream = ctx.contentResolver.openInputStream(data)
if (inputStream == null) return data if (inputStream == null) return data
os = FileOutputStream(Environment.getExternalStorageDirectory().path + "/Download/" + filename) os =
FileOutputStream(Environment.getExternalStorageDirectory().path + "/Download/" + filename)
val buffer = ByteArray(1024) val buffer = ByteArray(1024)
var bytesRead = inputStream.read(buffer) var bytesRead = inputStream.read(buffer)
while (bytesRead >= 0) { while (bytesRead >= 0) {
@ -272,7 +410,8 @@ object AppUtils {
arrayOf(MediaStore.Video.Media.DATA), null, null, null arrayOf(MediaStore.Video.Media.DATA), null, null, null
)?.use { )?.use {
val columnIndex = it.getColumnIndexOrThrow(MediaStore.Video.Media.DATA) 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) //uri = MediaUtils.getContentMediaUri(data)
/*} else if (data.authority == ctx.getString(R.string.tv_provider_authority)) { /*} else if (data.authority == ctx.getString(R.string.tv_provider_authority)) {