forked from recloudstream/cloudstream
		
	android tv recc not working :(
This commit is contained in:
		
							parent
							
								
									c5d53d7621
								
							
						
					
					
						commit
						1bbbbce326
					
				
					 7 changed files with 197 additions and 34 deletions
				
			
		|  | @ -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" | ||||||
| } | } | ||||||
|  | @ -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" | ||||||
|  |  | ||||||
|  | @ -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, | ||||||
|  |  | ||||||
|  | @ -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( | ||||||
|  |  | ||||||
|  | @ -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) | ||||||
|  |  | ||||||
|  | @ -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() | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -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 = | ||||||
|  |                             cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME)) | ||||||
|                                 .replace("/", "") |                                 .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)) { | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue