diff --git a/app/build.gradle b/app/build.gradle index a871388b..9ffbc147 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,6 +21,7 @@ android { buildTypes { release { minifyEnabled false + debuggable true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 311e64d1..0318a76c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,16 +4,19 @@ + diff --git a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt b/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt index 9d884110..453ea8ea 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt @@ -11,6 +11,13 @@ fun LifecycleOwner.observe(liveData: LiveData, action: (t: T) -> Unit) { liveData.observe(this, Observer { it?.let { t -> action(t) } }) } +fun LifecycleOwner.observeDirectly(liveData: LiveData, action: (t: T) -> Unit) { + liveData.observe(this, Observer { it?.let { t -> action(t) } }) + val currentValue = liveData.value + if (currentValue != null) + action(currentValue) +} + sealed class Resource { data class Success(val value: T) : Resource() data class Failure( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerFragment.kt index 4b60db60..655c0947 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerFragment.kt @@ -1,29 +1,55 @@ package com.lagradost.cloudstream3.ui.player +import android.app.Activity +import android.content.Context.AUDIO_SERVICE +import android.content.pm.ActivityInfo +import android.database.ContentObserver +import android.media.AudioManager +import android.net.Uri import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.view.LayoutInflater import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE import android.view.ViewGroup import android.view.animation.AlphaAnimation +import android.widget.ProgressBar +import android.widget.Toast +import android.widget.Toast.LENGTH_LONG import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider +import androidx.preference.PreferenceManager import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.module.kotlin.KotlinModule -import com.google.android.exoplayer2.SimpleExoPlayer +import com.google.android.exoplayer2.* +import com.google.android.exoplayer2.source.DefaultMediaSourceFactory +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector import com.google.android.exoplayer2.ui.AspectRatioFrameLayout +import com.google.android.exoplayer2.upstream.DataSource +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory +import com.google.android.exoplayer2.util.MimeTypes import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.ui.result.ResultViewModel -import com.lagradost.cloudstream3.utils.DataStore import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.ExtractorLink -import java.io.FileDescriptor -import java.io.PrintWriter +import kotlinx.android.synthetic.main.fragment_player.* +import kotlinx.android.synthetic.main.player_custom_layout.* +import java.io.File +import javax.net.ssl.HttpsURLConnection +import javax.net.ssl.SSLContext +import javax.net.ssl.SSLSession +import kotlin.concurrent.thread +//http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4 const val STATE_RESUME_WINDOW = "resumeWindow" const val STATE_RESUME_POSITION = "resumePosition" const val STATE_PLAYER_FULLSCREEN = "playerFullscreen" @@ -75,24 +101,33 @@ class PlayerFragment : Fragment() { } private fun releasePlayer() { + val alphaAnimation = AlphaAnimation(0f, 1f) + alphaAnimation.duration = 100 + alphaAnimation.fillAfter = true + loading_overlay.startAnimation(alphaAnimation) + video_go_back_holder.visibility = VISIBLE if (this::exoPlayer.isInitialized) { + isPlayerPlaying = exoPlayer.playWhenReady + playbackPosition = exoPlayer.currentPosition + currentWindow = exoPlayer.currentWindowIndex exoPlayer.release() } } - override fun onSaveInstanceState(outState: Bundle) { - if (this::exoPlayer.isInitialized) { - outState.putInt(STATE_RESUME_WINDOW, exoPlayer.currentWindowIndex) - outState.putLong(STATE_RESUME_POSITION, exoPlayer.currentPosition) + private class SettingsContentObserver(handler: Handler?, val activity: Activity) : ContentObserver(handler) { + private val audioManager = activity.getSystemService(AUDIO_SERVICE) as? AudioManager + override fun onChange(selfChange: Boolean) { + val currentVolume = audioManager?.getStreamVolume(AudioManager.STREAM_MUSIC) + val maxVolume = audioManager?.getStreamMaxVolume(AudioManager.STREAM_MUSIC) + val progressBarRight = activity.findViewById(R.id.progressBarRight) + if (currentVolume != null && maxVolume != null) { + progressBarRight?.progress = currentVolume * 100 / maxVolume + } } - outState.putBoolean(STATE_PLAYER_FULLSCREEN, isFullscreen) - outState.putBoolean(STATE_PLAYER_PLAYING, isPlayerPlaying) - outState.putInt(RESIZE_MODE_KEY, resizeMode) - outState.putFloat(PLAYBACK_SPEED, playbackSpeed) - savePos() - super.onSaveInstanceState(outState) } + private lateinit var volumeObserver: SettingsContentObserver + companion object { fun newInstance(data: PlayerData) = PlayerFragment().apply { @@ -126,16 +161,41 @@ class PlayerFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + + if (savedInstanceState != null) { + currentWindow = savedInstanceState.getInt(STATE_RESUME_WINDOW) + playbackPosition = savedInstanceState.getLong(STATE_RESUME_POSITION) + isFullscreen = savedInstanceState.getBoolean(STATE_PLAYER_FULLSCREEN) + isPlayerPlaying = savedInstanceState.getBoolean(STATE_PLAYER_PLAYING) + resizeMode = savedInstanceState.getInt(RESIZE_MODE_KEY) + playbackSpeed = savedInstanceState.getFloat(PLAYBACK_SPEED) + } + resizeMode = requireContext().getKey(RESIZE_MODE_KEY, 0)!! playbackSpeed = requireContext().getKey(PLAYBACK_SPEED_KEY, 1f)!! + volumeObserver = SettingsContentObserver( + Handler( + Looper.getMainLooper() + ), requireActivity() + ) + + activity?.contentResolver + ?.registerContentObserver( + android.provider.Settings.System.CONTENT_URI, true, volumeObserver + ) + + viewModel = ViewModelProvider(requireActivity()).get(ResultViewModel::class.java) + arguments?.getString("data")?.let { + playerData = mapper.readValue(it, PlayerData::class.java) + } + observe(viewModel.episodes) { _episodes -> episodes = _episodes if (isLoading) { if (playerData.episodeIndex > 0 && playerData.episodeIndex < episodes.size) { - } - else { + } else { // WHAT THE FUCK DID YOU DO } } @@ -159,14 +219,211 @@ class PlayerFragment : Fragment() { } } - println(allEpisodes) + println(episodes) + } + + fun getCurrentUrl(): ExtractorLink { + return ExtractorLink("", + "TEST", + "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", + "", + 0) + } + + override fun onStart() { + super.onStart() + thread { + // initPlayer() + if (player_view != null) player_view.onResume() + } + } + + override fun onResume() { + super.onResume() + activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE + thread { + initPlayer() + if (player_view != null) player_view.onResume() + } + } + + override fun onDestroy() { + super.onDestroy() + // releasePlayer() + activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER + } + + override fun onPause() { + super.onPause() + if (player_view != null) player_view.onPause() + releasePlayer() + } + + override fun onStop() { + super.onStop() + if (player_view != null) player_view.onPause() + releasePlayer() + } + + override fun onSaveInstanceState(outState: Bundle) { + if (this::exoPlayer.isInitialized) { + outState.putInt(STATE_RESUME_WINDOW, exoPlayer.currentWindowIndex) + outState.putLong(STATE_RESUME_POSITION, exoPlayer.currentPosition) + } + outState.putBoolean(STATE_PLAYER_FULLSCREEN, isFullscreen) + outState.putBoolean(STATE_PLAYER_PLAYING, isPlayerPlaying) + outState.putInt(RESIZE_MODE_KEY, resizeMode) + outState.putFloat(PLAYBACK_SPEED, playbackSpeed) + savePos() + super.onSaveInstanceState(outState) + } + + private var currentWindow = 0 + private var playbackPosition: Long = 0 + //http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4 + private fun initPlayer() { + println("INIT PLAYER") + view?.setOnTouchListener { _, _ -> return@setOnTouchListener true } // VERY IMPORTANT https://stackoverflow.com/questions/28818926/prevent-clicking-on-a-button-in-an-activity-while-showing-a-fragment + thread { + val currentUrl = getCurrentUrl() + if (currentUrl == null) { + activity?.runOnUiThread { + Toast.makeText(activity, "Error getting link", LENGTH_LONG).show() + //MainActivity.popCurrentPage() + } + } else { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(activity) + try { + activity?.runOnUiThread { + val isOnline = + currentUrl.url.startsWith("https://") || currentUrl.url.startsWith("http://") + + if (settingsManager?.getBoolean("ignore_ssl", true) == true) { + // Disables ssl check + val sslContext: SSLContext = SSLContext.getInstance("TLS") + sslContext.init(null, arrayOf(SSLTrustManager()), java.security.SecureRandom()) + sslContext.createSSLEngine() + HttpsURLConnection.setDefaultHostnameVerifier { _: String, _: SSLSession -> + true + } + HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory) + } + + class CustomFactory : DataSource.Factory { + override fun createDataSource(): DataSource { + return if (isOnline) { + val dataSource = DefaultHttpDataSourceFactory(USER_AGENT).createDataSource() + /*FastAniApi.currentHeaders?.forEach { + dataSource.setRequestProperty(it.key, it.value) + }*/ + dataSource.setRequestProperty("Referer", currentUrl.referer) + dataSource + } else { + DefaultDataSourceFactory(requireContext(), USER_AGENT).createDataSource() + } + } + } + + val mimeType = if (currentUrl.isM3u8) MimeTypes.APPLICATION_M3U8 else MimeTypes.APPLICATION_MP4 + val _mediaItem = MediaItem.Builder() + //Replace needed for android 6.0.0 https://github.com/google/ExoPlayer/issues/5983 + .setMimeType(mimeType) + + if (isOnline) { + _mediaItem.setUri(currentUrl.url) + } else { + _mediaItem.setUri(Uri.fromFile(File(currentUrl.url))) + } + + val mediaItem = _mediaItem.build() + val trackSelector = DefaultTrackSelector(requireContext()) + // Disable subtitles + trackSelector.parameters = DefaultTrackSelector.ParametersBuilder(requireContext()) + .setRendererDisabled(C.TRACK_TYPE_VIDEO, true) + .setRendererDisabled(C.TRACK_TYPE_TEXT, true) + .setDisabledTextTrackSelectionFlags(C.TRACK_TYPE_TEXT) + .clearSelectionOverrides() + .build() + + val _exoPlayer = + SimpleExoPlayer.Builder(this.requireContext()) + .setTrackSelector(trackSelector) + + _exoPlayer.setMediaSourceFactory(DefaultMediaSourceFactory(CustomFactory())) + exoPlayer = _exoPlayer.build().apply { + playWhenReady = isPlayerPlaying + seekTo(currentWindow, playbackPosition) + setMediaItem(mediaItem, false) + prepare() + } + + val alphaAnimation = AlphaAnimation(1f, 0f) + alphaAnimation.duration = 300 + alphaAnimation.fillAfter = true + loading_overlay.startAnimation(alphaAnimation) + video_go_back_holder.visibility = GONE + + exoPlayer.setHandleAudioBecomingNoisy(true) // WHEN HEADPHONES ARE PLUGGED OUT https://github.com/google/ExoPlayer/issues/7288 + player_view.player = exoPlayer + // Sets the speed + exoPlayer.setPlaybackParameters(PlaybackParameters(playbackSpeed!!)) + player_speed_text?.text = "Speed (${playbackSpeed}x)".replace(".0x", "x") + + //https://stackoverflow.com/questions/47731779/detect-pause-resume-in-exoplayer + exoPlayer.addListener(object : Player.Listener { + // @SuppressLint("NewApi") + override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { + // updatePIPModeActions() + if (playWhenReady && playbackState == Player.STATE_READY) { + // focusRequest?.let { activity?.requestAudioFocus(it) } + } + } + + override fun onPlayerError(error: ExoPlaybackException) { + // Lets pray this doesn't spam Toasts :) + when (error.type) { + ExoPlaybackException.TYPE_SOURCE -> { + if (currentUrl.url != "") { + Toast.makeText( + activity, + "Source error\n" + error.sourceException.message, + LENGTH_LONG + ) + .show() + } + } + ExoPlaybackException.TYPE_REMOTE -> { + Toast.makeText(activity, "Remote error", LENGTH_LONG) + .show() + } + ExoPlaybackException.TYPE_RENDERER -> { + Toast.makeText( + activity, + "Renderer error\n" + error.rendererException.message, + LENGTH_LONG + ) + .show() + } + ExoPlaybackException.TYPE_UNEXPECTED -> { + Toast.makeText( + activity, + "Unexpected player error\n" + error.unexpectedException.message, + LENGTH_LONG + ).show() + } + } + } + }) + } + } catch (e: java.lang.IllegalStateException) { + println("Warning: Illegal state exception in PlayerFragment") + } + } + } + //isLoadingNextEpisode = false } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - viewModel = ViewModelProvider(requireActivity()).get(ResultViewModel::class.java) - arguments?.getString("data")?.let { - playerData = mapper.readValue(it, PlayerData::class.java) - } return inflater.inflate(R.layout.fragment_player, container, false) } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/SSLTrustManager.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/SSLTrustManager.kt new file mode 100644 index 00000000..0c7226d2 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/SSLTrustManager.kt @@ -0,0 +1,16 @@ +package com.lagradost.cloudstream3.ui.player + +import java.security.cert.X509Certificate +import javax.net.ssl.X509TrustManager + +class SSLTrustManager : X509TrustManager { + override fun checkClientTrusted(p0: Array?, p1: String?) { + } + + override fun checkServerTrusted(p0: Array?, p1: String?) { + } + + override fun getAcceptedIssuers(): Array { + return arrayOf() + } +} \ No newline at end of file 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 26528883..4ec71a82 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 @@ -106,12 +106,58 @@ class ResultFragment : Fragment() { activity?.onBackPressed() } + + val adapter: RecyclerView.Adapter? = activity?.let { it -> + EpisodeAdapter( + it, + ArrayList(), + result_episodes, + ) { episodeClick -> + val id = episodeClick.data.id + val index = episodeClick.data.index + val buildInPlayer = true + if (buildInPlayer) { + (requireActivity() as AppCompatActivity).supportFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.enter_anim, + R.anim.exit_anim, + R.anim.pop_enter, + R.anim.pop_exit) + .add(R.id.homeRoot, PlayerFragment.newInstance(PlayerData(index))) + .commit() + } else { + when (episodeClick.action) { + + /* + ACTION_PLAY_EPISODE -> { + if (allEpisodes.containsKey(id)) { + playEpisode(allEpisodes[id], index) + } else { + viewModel.loadEpisode(episodeClick.data) { res -> + if (res is Resource.Success) { + playEpisode(allEpisodes[id], index) + } + } + } + } + ACTION_RELOAD_EPISODE -> viewModel.loadEpisode(episodeClick.data) { res -> + if (res is Resource.Success) { + playEpisode(allEpisodes[id], index) + } + }*/ + } + } + } + } + + result_episodes.adapter = adapter + result_episodes.layoutManager = GridLayoutManager(context, 1) + observe(viewModel.allEpisodes) { allEpisodes = it } observe(viewModel.episodes) { episodes -> - if(result_episodes == null) return@observe + if(result_episodes == null || result_episodes.adapter == null) return@observe (result_episodes.adapter as EpisodeAdapter).cardList = episodes (result_episodes.adapter as EpisodeAdapter).notifyDataSetChanged() } @@ -203,51 +249,6 @@ activity?.startActivityForResult(vlcIntent, REQUEST_CODE) } } - val adapter: RecyclerView.Adapter? = activity?.let { it -> - EpisodeAdapter( - it, - ArrayList(), - result_episodes, - ) { episodeClick -> - val id = episodeClick.data.id - val index = episodeClick.data.index - val buildInPlayer = true - if (buildInPlayer) { - (requireActivity() as AppCompatActivity).supportFragmentManager.beginTransaction() - .setCustomAnimations(R.anim.enter_anim, - R.anim.exit_anim, - R.anim.pop_enter, - R.anim.pop_exit) - .add(R.id.homeRoot, PlayerFragment.newInstance(PlayerData(index))) - .commit() - } else { - when (episodeClick.action) { - - /* - ACTION_PLAY_EPISODE -> { - if (allEpisodes.containsKey(id)) { - playEpisode(allEpisodes[id], index) - } else { - viewModel.loadEpisode(episodeClick.data) { res -> - if (res is Resource.Success) { - playEpisode(allEpisodes[id], index) - } - } - } - } - ACTION_RELOAD_EPISODE -> viewModel.loadEpisode(episodeClick.data) { res -> - if (res is Resource.Success) { - playEpisode(allEpisodes[id], index) - } - }*/ - } - } - } - } - - result_episodes.adapter = adapter - result_episodes.layoutManager = GridLayoutManager(context, 1) - if (d.plot != null) { var syno = d.plot!! if (syno.length > MAX_SYNO_LENGH) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt index 9753bc6d..62546bf9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt @@ -8,6 +8,7 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.Toast import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.SearchView import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider @@ -21,6 +22,8 @@ import com.lagradost.cloudstream3.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.UIHelper.getGridIsCompact import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.observe +import com.lagradost.cloudstream3.ui.player.PlayerData +import com.lagradost.cloudstream3.ui.player.PlayerFragment import kotlinx.android.synthetic.main.fragment_search.* class SearchFragment : Fragment() { @@ -126,5 +129,14 @@ class SearchFragment : Fragment() { search_exit_icon.alpha = 1f search_loading_bar.alpha = 0f } + + + (requireActivity() as AppCompatActivity).supportFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.enter_anim, + R.anim.exit_anim, + R.anim.pop_enter, + R.anim.pop_exit) + .add(R.id.homeRoot, PlayerFragment.newInstance(PlayerData(0))) + .commit() } } \ No newline at end of file