diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..aa724b77 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..26d33521 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 00000000..d0fb6256 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +My TV \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 00000000..b589d56e --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml new file mode 100644 index 00000000..57272bc1 --- /dev/null +++ b/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 00000000..ae388c2a --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 00000000..183eebe5 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..0ad17cbd --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 00000000..b10e3e8b --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,43 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'com.lizongying.mytv' + compileSdk 33 + + defaultConfig { + applicationId "com.lizongying.mytv" + minSdk 23 + targetSdk 33 + versionCode 1 + versionName "1.0" + + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget=17 + } +} + +dependencies { + + implementation 'androidx.core:core-ktx:1.11.0-beta02' + implementation 'androidx.leanback:leanback:1.0.0' + implementation 'com.github.bumptech.glide:glide:4.11.0' + implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2" + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0-RC") +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..92c788ad --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/lizongying/mytv/CardPresenter.kt b/app/src/main/java/com/lizongying/mytv/CardPresenter.kt new file mode 100644 index 00000000..7e5067b9 --- /dev/null +++ b/app/src/main/java/com/lizongying/mytv/CardPresenter.kt @@ -0,0 +1,105 @@ +package com.lizongying.mytv + +import android.graphics.Bitmap +import android.media.MediaMetadataRetriever +import android.util.Log +import android.view.ContextThemeWrapper +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.leanback.widget.ImageCardView +import androidx.leanback.widget.Presenter +import androidx.lifecycle.LifecycleCoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlin.properties.Delegates + + +/** + * A CardPresenter is used to generate Views and bind Objects to them on demand. + * It contains an ImageCardView. + */ +class CardPresenter(private val lifecycleScope: LifecycleCoroutineScope) : Presenter() { + + override fun onCreateViewHolder(parent: ViewGroup): ViewHolder { + Log.d(TAG, "onCreateViewHolder") + + val cardView = object : + ImageCardView(ContextThemeWrapper(parent.context, R.style.CustomImageCardTheme)) { + override fun setSelected(selected: Boolean) { +// updateCardBackgroundColor(this) + super.setSelected(selected) + } + } + + cardView.isFocusable = true + cardView.isFocusableInTouchMode = true + return ViewHolder(cardView) + } + + override fun onBindViewHolder(viewHolder: ViewHolder, item: Any) { + val tv = item as TV + val cardView = viewHolder.view as ImageCardView + + Log.d(TAG, "onBindViewHolder") + if (tv.videoUrl != null) { + cardView.titleText = tv.title + cardView.setMainImageDimensions(CARD_WIDTH, CARD_HEIGHT) + cardView.tag = tv.videoUrl + +// lifecycleScope.launch(Dispatchers.IO) { +// val videoThumbnail = tv.videoUrl?.let { getVideoThumbnail(it) } +// +// withContext(Dispatchers.Main) { +// cardView.mainImageView.setImageBitmap(videoThumbnail) +// } +// } + } + } + + override fun onUnbindViewHolder(viewHolder: ViewHolder) { + Log.d(TAG, "onUnbindViewHolder") + val cardView = viewHolder.view as ImageCardView + // Remove references to images so that the garbage collector can free up memory + cardView.badgeImage = null + cardView.mainImage = null + } + + private fun updateCardBackgroundColor(view: ImageCardView) { + val currentTag = view.tag + lifecycleScope.launch(Dispatchers.IO) { + delay(1000) + if (view.isSelected && view.tag != null && currentTag == view.tag) { + val videoThumbnail = view.tag.toString().let { getVideoThumbnail(it) } + withContext(Dispatchers.Main) { + if (view.isSelected && currentTag == view.tag) { + view.mainImageView.setImageBitmap(videoThumbnail) + } + } + } + } + } + + private fun getVideoThumbnail(url: String): Bitmap? { + val mediaMetadataRetriever = MediaMetadataRetriever() + try { + val map = HashMap() + mediaMetadataRetriever.setDataSource(url, map) + return mediaMetadataRetriever.frameAtTime + } catch (e: Exception) { + e.printStackTrace() + } finally { + mediaMetadataRetriever.release() + } + return null + } + + companion object { + private const val TAG = "CardPresenter" + + private const val CARD_WIDTH = 313 + + private const val CARD_HEIGHT = 176 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lizongying/mytv/Info.kt b/app/src/main/java/com/lizongying/mytv/Info.kt new file mode 100644 index 00000000..07f7d6c7 --- /dev/null +++ b/app/src/main/java/com/lizongying/mytv/Info.kt @@ -0,0 +1,10 @@ +package com.lizongying.mytv + +import java.io.Serializable + + +data class Info( + var rowPosition: Int = 0, + var itemPosition: Int = 0, + var item: TV? = null, +) : Serializable \ No newline at end of file diff --git a/app/src/main/java/com/lizongying/mytv/MainActivity.kt b/app/src/main/java/com/lizongying/mytv/MainActivity.kt new file mode 100644 index 00000000..3783c6f2 --- /dev/null +++ b/app/src/main/java/com/lizongying/mytv/MainActivity.kt @@ -0,0 +1,168 @@ +package com.lizongying.mytv + +import android.app.AlertDialog +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.view.KeyEvent +import android.widget.ImageView +import android.widget.Toast +import androidx.core.content.ContextCompat +import androidx.fragment.app.FragmentActivity + +/** + * Loads [MainFragment]. + */ +class MainActivity : FragmentActivity() { + + private val playbackFragment = PlaybackFragment() + private val mainFragment = MainFragment() + + private val handler = Handler(Looper.getMainLooper()) + private var hideTask: Runnable? = null + + private var doubleBackToExitPressedOnce = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + if (savedInstanceState == null) { + supportFragmentManager.beginTransaction() + .add(R.id.main_browse_fragment, playbackFragment) + .add(R.id.main_browse_fragment, mainFragment) + .commit() + + hideTask = Runnable { + Log.i(TAG, "hideTask") + hideMainFragment() + } + } + } + + fun play(tv: TV) { + playbackFragment.play(tv) + } + + fun prev() { + Log.i(TAG, "prev") + mainFragment.prev() + } + + fun next() { + Log.i(TAG, "next") + mainFragment.next() + } + + fun switchMainFragment() { + val transaction = supportFragmentManager.beginTransaction() + + if (mainFragment.isHidden) { + focusMainFragment() + transaction.show(mainFragment) + } else { + transaction.hide(mainFragment) + } + + transaction.commit() + } + + fun focusMainFragment() { + mainFragment.focus() + } + + fun fragmentIsHidden(): Boolean { + return mainFragment.isHidden + } + + fun hideMainFragment() { + if (!mainFragment.isHidden) { + supportFragmentManager.beginTransaction() + .hide(mainFragment) + .commit() + } + } + + fun startHideTask(delayMillis: Long) { + hideTask?.let { handler.postDelayed(it, delayMillis) } + } + + fun cancelHideTask() { + hideTask?.let { handler.removeCallbacks(it) } + } + + override fun onDestroy() { + cancelHideTask() + super.onDestroy() + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { + when (keyCode) { + KeyEvent.KEYCODE_BACK -> { + if (doubleBackToExitPressedOnce) { + super.onBackPressed() + return true + } + + hideMainFragment() + this.doubleBackToExitPressedOnce = true + Toast.makeText(this, "再按一次退出", Toast.LENGTH_SHORT).show() + + Handler(Looper.getMainLooper()).postDelayed({ + doubleBackToExitPressedOnce = false + }, 2000) + return true + } + + KeyEvent.KEYCODE_MENU -> { + val imageView = ImageView(this) + val drawable = ContextCompat.getDrawable(this, R.drawable.appreciate) + imageView.setImageDrawable(drawable) + + val builder: AlertDialog.Builder = AlertDialog.Builder(this) + builder + .setMessage("地址: https://github.com/lizongying/my-tv/") + .setView(imageView) + + val dialog: AlertDialog = builder.create() + dialog.show() + return true + } + + KeyEvent.KEYCODE_DPAD_CENTER -> { + switchMainFragment() + } + + KeyEvent.KEYCODE_DPAD_UP -> { + if (mainFragment.isHidden) { + prev() + } + } + + KeyEvent.KEYCODE_DPAD_DOWN -> { + if (mainFragment.isHidden) { + next() + } + } + + KeyEvent.KEYCODE_DPAD_LEFT -> { + if (mainFragment.isHidden) { + prev() + } + } + + KeyEvent.KEYCODE_DPAD_RIGHT -> { + if (mainFragment.isHidden) { + next() + } + } + } + + return super.onKeyDown(keyCode, event) + } + + companion object { + private const val TAG = "MainActivity" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lizongying/mytv/MainFragment.kt b/app/src/main/java/com/lizongying/mytv/MainFragment.kt new file mode 100644 index 00000000..33a2011d --- /dev/null +++ b/app/src/main/java/com/lizongying/mytv/MainFragment.kt @@ -0,0 +1,171 @@ +package com.lizongying.mytv + +import android.os.Bundle +import android.util.Log +import android.view.View +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import androidx.leanback.app.BrowseSupportFragment +import androidx.leanback.widget.ArrayObjectAdapter +import androidx.leanback.widget.HeaderItem +import androidx.leanback.widget.ListRow +import androidx.leanback.widget.ListRowPresenter +import androidx.leanback.widget.ListRowPresenter.SelectItemViewHolderTask +import androidx.leanback.widget.OnItemViewClickedListener +import androidx.leanback.widget.OnItemViewSelectedListener +import androidx.leanback.widget.Presenter +import androidx.leanback.widget.Row +import androidx.leanback.widget.RowPresenter +import androidx.lifecycle.lifecycleScope + +/** + * Loads a grid of cards with movies to browse. + */ +class MainFragment : BrowseSupportFragment() { + + var itemPosition: Int = 0 + + private val list2: MutableList = mutableListOf() + + override fun onActivityCreated(savedInstanceState: Bundle?) { + Log.i(TAG, "onCreate") + super.onActivityCreated(savedInstanceState) + + setupUIElements() + + loadRows() + + setupEventListeners() + } + + fun show() { + if (!view?.isVisible!!) { + view?.visibility = View.VISIBLE + } + } + + private fun setupUIElements() { + // set fastLane (or headers) background color + brandColor = ContextCompat.getColor(context!!, R.color.fastlane_background) +// headersState = HEADERS_DISABLED + } + + private fun loadRows() { + val list = TVList.list + val rowsAdapter = ArrayObjectAdapter(ListRowPresenter()) + val cardPresenter = CardPresenter(lifecycleScope) + + var idx: Long = 0 + for ((k, v) in list) { + val listRowAdapter = ArrayObjectAdapter(cardPresenter) + for ((idx2, v1) in v.withIndex()) { + listRowAdapter.add(v1) + list2.add(Info(idx.toInt(), idx2, v1)) + } + val header = HeaderItem(idx, k) + rowsAdapter.add(ListRow(header, listRowAdapter)) + idx++ + } + + adapter = rowsAdapter + + (activity as? MainActivity)?.play(list.values.first()[0]) + (activity as? MainActivity)?.switchMainFragment() + } + + fun focus() { + if (!view?.isFocused!!) { + view?.requestFocus() + } + } + + fun prev() { + view?.post { + itemPosition-- + if (itemPosition == -1) { + itemPosition = list2.size - 1 + } + + val l = list2[itemPosition] + l.item?.let { (activity as? MainActivity)?.play(it) } + setSelectedPosition( + l.rowPosition, false, + SelectItemViewHolderTask(l.itemPosition) + ) +// Toast.makeText( +// activity, +// "${l.title} $selectedPosition $itemPosition", +// Toast.LENGTH_SHORT +// ).show() + } + } + + fun next() { + view?.post { + itemPosition++ + if (itemPosition == list2.size) { + itemPosition = 0 + } + + val l = list2[itemPosition] + l.item?.let { (activity as? MainActivity)?.play(it) } + setSelectedPosition( + l.rowPosition, false, + SelectItemViewHolderTask(l.itemPosition) + ) +// Toast.makeText( +// activity, +// "${l.title} $selectedPosition $itemPosition", +// Toast.LENGTH_SHORT +// ).show() + } + } + + private fun setupEventListeners() { + onItemViewClickedListener = ItemViewClickedListener() + onItemViewSelectedListener = ItemViewSelectedListener() + } + + private inner class ItemViewClickedListener : OnItemViewClickedListener { + override fun onItemClicked( + itemViewHolder: Presenter.ViewHolder, + item: Any, + rowViewHolder: RowPresenter.ViewHolder, + row: Row + ) { + Log.d(TAG, "onItemClicked") + if (item is TV) { + Log.d(TAG, "Item: $item") + (activity as? MainActivity)?.play(item) + (activity as? MainActivity)?.switchMainFragment() + itemPosition = item.id + } + } + } + + private inner class ItemViewSelectedListener : OnItemViewSelectedListener { + override fun onItemSelected( + itemViewHolder: Presenter.ViewHolder?, item: Any?, + rowViewHolder: RowPresenter.ViewHolder, row: Row + ) { + if (item is TV) { + Log.i(TAG, "Item: ${item.id}") + + } + if (itemViewHolder == null) { + view?.post { + val l = list2[itemPosition] + Log.i(TAG, "$l") + setSelectedPosition( + l.rowPosition, false, + SelectItemViewHolderTask(l.itemPosition) + ) + } + } + } + } + + companion object { + private const val TAG = "MainFragment" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lizongying/mytv/PlaybackControlGlue.kt b/app/src/main/java/com/lizongying/mytv/PlaybackControlGlue.kt new file mode 100644 index 00000000..32b24287 --- /dev/null +++ b/app/src/main/java/com/lizongying/mytv/PlaybackControlGlue.kt @@ -0,0 +1,53 @@ +package com.lizongying.mytv + +import android.content.Context +import android.view.KeyEvent +import android.view.View +import androidx.leanback.media.MediaPlayerAdapter +import androidx.leanback.media.PlaybackTransportControlGlue + +class PlaybackControlGlue( + context: Context?, + playerAdapter: MediaPlayerAdapter?, +) : + PlaybackTransportControlGlue(context, playerAdapter) { + + override fun onKey(v: View?, keyCode: Int, event: KeyEvent?): Boolean { + if (event!!.action == KeyEvent.ACTION_DOWN) { + when (keyCode) { + KeyEvent.KEYCODE_DPAD_CENTER -> { + (context as? MainActivity)?.switchMainFragment() + } + + KeyEvent.KEYCODE_DPAD_UP -> { + if ((context as? MainActivity)?.fragmentIsHidden() == true) { + (context as? MainActivity)?.prev() + } + } + + KeyEvent.KEYCODE_DPAD_DOWN -> { + if ((context as? MainActivity)?.fragmentIsHidden() == true) { + (context as? MainActivity)?.next() + } + } + + KeyEvent.KEYCODE_DPAD_LEFT -> { + if ((context as? MainActivity)?.fragmentIsHidden() == true) { + (context as? MainActivity)?.prev() + } + } + + KeyEvent.KEYCODE_DPAD_RIGHT -> { + if ((context as? MainActivity)?.fragmentIsHidden() == true) { + (context as? MainActivity)?.next() + } + } + } + } + return super.onKey(v, keyCode, event) + } + + companion object { + private const val TAG = "CustomPlaybackControlGlue" + } +} diff --git a/app/src/main/java/com/lizongying/mytv/PlaybackFragment.kt b/app/src/main/java/com/lizongying/mytv/PlaybackFragment.kt new file mode 100644 index 00000000..03254cd6 --- /dev/null +++ b/app/src/main/java/com/lizongying/mytv/PlaybackFragment.kt @@ -0,0 +1,75 @@ +package com.lizongying.mytv + +import android.net.Uri +import android.os.Bundle +import android.util.Log +import androidx.leanback.app.VideoSupportFragment +import androidx.leanback.app.VideoSupportFragmentGlueHost +import androidx.leanback.media.MediaPlayerAdapter +import androidx.leanback.media.PlaybackTransportControlGlue +import androidx.leanback.widget.PlaybackControlsRow +import java.io.IOException + +/** Handles video playback with media controls. */ +class PlaybackFragment : VideoSupportFragment() { + + private lateinit var mTransportControlGlue: PlaybackTransportControlGlue + private var playerAdapter: MediaPlayerAdapter? = null + private var lastVideoUrl: String = "" + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + playerAdapter = MediaPlayerAdapter(context) + playerAdapter?.setRepeatAction(PlaybackControlsRow.RepeatAction.INDEX_NONE) + + view?.isFocusable = false + view?.isFocusableInTouchMode = false + } + + override fun showControlsOverlay(runAnimation: Boolean) { + // We will do nothing here, and thus controls will never be shown + } + + fun play(tv: TV) { + if (tv.videoUrl.isNullOrBlank()) { + Log.e(TAG, "videoUrl is empty") + return + } + + if (tv.videoUrl.equals(lastVideoUrl)) { + Log.e(TAG, "videoUrl is duplication") + return + } + + lastVideoUrl = tv.videoUrl!! + + playerAdapter?.reset() + + val glueHost = VideoSupportFragmentGlueHost(this@PlaybackFragment) + mTransportControlGlue = PlaybackControlGlue(activity, playerAdapter) + mTransportControlGlue.host = glueHost + mTransportControlGlue.playWhenPrepared() + + try { + playerAdapter?.setDataSource(Uri.parse(tv.videoUrl)) + } catch (e: IOException) { + // Handle the exception + return + } + hideControlsOverlay(false) + } + + override fun onDestroy() { + if (playerAdapter?.mediaPlayer != null) { + playerAdapter?.release() + Log.d(TAG, "playerAdapter released") + } + + super.onDestroy() + } + + companion object { + private const val TAG = "PlaybackVideoFragment" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lizongying/mytv/TV.kt b/app/src/main/java/com/lizongying/mytv/TV.kt new file mode 100644 index 00000000..1fe2b6e0 --- /dev/null +++ b/app/src/main/java/com/lizongying/mytv/TV.kt @@ -0,0 +1,25 @@ +package com.lizongying.mytv + +import java.io.Serializable + +/** + * Movie class represents video entity with title, description, image thumbs and video url. + */ +data class TV( + var id: Int = 0, + var title: String? = null, + var videoUrl: String? = null, +) : Serializable { + + override fun toString(): String { + return "TV{" + + "id=" + id + + ", title='" + title + '\'' + + ", videoUrl='" + videoUrl + '\'' + + '}' + } + + companion object { + internal const val serialVersionUID = 727566175075960653L + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lizongying/mytv/TVList.kt b/app/src/main/java/com/lizongying/mytv/TVList.kt new file mode 100644 index 00000000..91fcf095 --- /dev/null +++ b/app/src/main/java/com/lizongying/mytv/TVList.kt @@ -0,0 +1,115 @@ +package com.lizongying.mytv + +object TVList { + val list: Map> by lazy { + setupTV() + } + private var count: Int = 0 + + private fun setupTV(): Map> { + val tv = arrayOf( + arrayOf( + "央视频道", + "CCTV1", + "http://hlsbkmgsplive.miguvideo.com/migu/kailu/cctv1hd265/57/20191230/index.m3u8?&encrypt=", + ), + arrayOf( + "央视频道", + "CCTV2", + "http://hlsbkmgsplive.miguvideo.com/migu/kailu/cctv2hd265/55/20200407/index.m3u8?&encrypt=", + ), + arrayOf( + "央视频道", + "CCTV3", + "http://hlsbkmgsplive.miguvideo.com/wd_r2/ocn/cctv3hd/3000/index.m3u8?&encrypt=", + ), + arrayOf( + "央视频道", + "CCTV4", + "http://hlsbkmgsplive.miguvideo.com/wd_r2/cctv/cctv4hd/1500/index.m3u8?&encrypt=", + ), + arrayOf( + "央视频道", + "CCTV4美洲", + "http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv4meihd/57/index.m3u8?&encrypt=", + ), + arrayOf( + "央视频道", + "CCTV4欧洲", + "http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv4ouhd/51/index.m3u8?&encrypt=", + ), + arrayOf( + "央视频道", + "CCTV5", + "http://hlsbkmgsplive.miguvideo.com/migu/kailu/cctv5hd265/57/20191230/index.m3u8?&encrypt=", + ), + arrayOf( + "央视频道", + "CCTV5+", + "http://hlsbkmgsplive.miguvideo.com/wd_r2/cctv/cctv5plusnew/2500/index.m3u8?&encrypt=", + ), + arrayOf( + "央视频", + "CCTV1", + "http://hlsbkmgsplive.miguvideo.com/migu/kailu/cctv1hd265/57/20191230/index.m3u8?&encrypt=", + ), + arrayOf( + "央视频", + "CCTV2", + "http://hlsbkmgsplive.miguvideo.com/migu/kailu/cctv2hd265/55/20200407/index.m3u8?&encrypt=", + ), + arrayOf( + "央视频", + "CCTV3", + "http://hlsbkmgsplive.miguvideo.com/wd_r2/ocn/cctv3hd/3000/index.m3u8?&encrypt=", + ), + arrayOf( + "央视频", + "CCTV4", + "http://hlsbkmgsplive.miguvideo.com/wd_r2/cctv/cctv4hd/1500/index.m3u8?&encrypt=", + ), + arrayOf( + "央视频", + "CCTV4美洲", + "http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv4meihd/57/index.m3u8?&encrypt=", + ), + arrayOf( + "央视频", + "CCTV4欧洲", + "http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv4ouhd/51/index.m3u8?&encrypt=", + ), + arrayOf( + "央视频", + "CCTV5", + "http://hlsbkmgsplive.miguvideo.com/migu/kailu/cctv5hd265/57/20191230/index.m3u8?&encrypt=", + ), + arrayOf( + "央视频", + "CCTV5+", + "http://hlsbkmgsplive.miguvideo.com/wd_r2/cctv/cctv5plusnew/2500/index.m3u8?&encrypt=", + ), + ) + + val map: MutableMap> = mutableMapOf() + + for (i in tv) { + val channelName = i[0] + val movieList = map[channelName] ?: mutableListOf() + movieList.add(buildTV(i[1], i[2])) + map[channelName] = movieList + } + + return map + } + + private fun buildTV( + title: String, + videoUrl: String, + ): TV { + val tv = TV() + tv.id = count++ + tv.title = title + tv.videoUrl = videoUrl + return tv + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/appreciate.jpg b/app/src/main/res/drawable/appreciate.jpg new file mode 100644 index 00000000..fe31a80e Binary files /dev/null and b/app/src/main/res/drawable/appreciate.jpg differ diff --git a/app/src/main/res/drawable/tv.png b/app/src/main/res/drawable/tv.png new file mode 100644 index 00000000..f762dc19 Binary files /dev/null and b/app/src/main/res/drawable/tv.png differ diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..86bbdaec --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,9 @@ + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 00000000..c209e78e Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 00000000..4f0f1d64 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 00000000..948a3070 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 00000000..28d4b77f Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 00000000..aa7d6427 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..005847ca --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,3 @@ + + #30000000 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..8e397fad --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + My TV + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..51bdee83 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 00000000..eece25f9 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,4 @@ + + +