diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e267b893..e98441e9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,26 +1,29 @@ + xmlns:tools="http://schemas.android.com/tools" package="com.lagradost.cloudstream3"> + + + android:theme="@style/AppTheme" android:fullBackupContent="@xml/backup_descriptor"> + android:label="@string/app_name" + android:resizeableActivity="true" + android:supportsPictureInPicture="true"> @@ -41,5 +44,4 @@ android:resource="@xml/provider_paths"/> - \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 7dd77387..c1d282e4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -1,24 +1,58 @@ package com.lagradost.cloudstream3 +import android.app.PictureInPictureParams +import android.content.pm.PackageManager +import android.os.Build import android.os.Bundle import com.google.android.material.bottomnavigation.BottomNavigationView import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContentProviderCompat.requireContext import androidx.lifecycle.ViewModelStore import androidx.lifecycle.ViewModelStoreOwner import androidx.navigation.findNavController import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.setupWithNavController import com.lagradost.cloudstream3.UIHelper.checkWrite +import com.lagradost.cloudstream3.UIHelper.hasPIPPermission import com.lagradost.cloudstream3.UIHelper.requestRW +import com.lagradost.cloudstream3.UIHelper.shouldShowPIPMode -class MainActivity : AppCompatActivity() {/*, ViewModelStoreOwner { - private val appViewModelStore: ViewModelStore by lazy { - ViewModelStore() +class MainActivity : AppCompatActivity() { + /*, ViewModelStoreOwner { + private val appViewModelStore: ViewModelStore by lazy { + ViewModelStore() + } + + override fun getViewModelStore(): ViewModelStore { + return appViewModelStore + }*/ + companion object { + var isInPlayer: Boolean = false + var canShowPipMode: Boolean = false + var isInPIPMode: Boolean = false } - override fun getViewModelStore(): ViewModelStore { - return appViewModelStore - }*/ + private fun enterPIPMode() { + if (!shouldShowPIPMode(isInPlayer) || !canShowPipMode) return + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + try { + enterPictureInPictureMode(PictureInPictureParams.Builder().build()) + } catch (e: Exception) { + enterPictureInPictureMode() + } + } else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + enterPictureInPictureMode() + } + } + } + + override fun onUserLeaveHint() { + super.onUserLeaveHint() + if (isInPlayer && canShowPipMode) { + enterPIPMode() + } + } private fun AppCompatActivity.backPressed(): Boolean { val currentFragment = supportFragmentManager.fragments.last { @@ -46,6 +80,13 @@ class MainActivity : AppCompatActivity() {/*, ViewModelStoreOwner { setContentView(R.layout.activity_main) val navView: BottomNavigationView = findViewById(R.id.nav_view) + //https://stackoverflow.com/questions/52594181/how-to-know-if-user-has-disabled-picture-in-picture-feature-permission + //https://developer.android.com/guide/topics/ui/picture-in-picture + canShowPipMode = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && // OS SUPPORT + packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && // HAS FEATURE, MIGHT BE BLOCKED DUE TO POWER DRAIN + hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS + val navController = findNavController(R.id.nav_host_fragment) // Passing each menu ID as a set of Ids because each // menu should be considered as top level destinations. diff --git a/app/src/main/java/com/lagradost/cloudstream3/UIHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/UIHelper.kt index 79e2ff06..96520582 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/UIHelper.kt @@ -2,6 +2,7 @@ package com.lagradost.cloudstream3 import android.Manifest import android.app.Activity +import android.app.AppOpsManager import android.content.Context import android.content.pm.PackageManager import android.content.res.Resources @@ -11,6 +12,7 @@ import android.media.AudioManager import android.os.Build import android.view.View import android.view.WindowManager +import android.view.inputmethod.InputMethodManager import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat @@ -19,6 +21,7 @@ import androidx.preference.PreferenceManager import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability +import com.lagradost.cloudstream3.UIHelper.getGridFormat import com.lagradost.cloudstream3.ui.result.ResultFragment import com.lagradost.cloudstream3.utils.Event @@ -261,4 +264,24 @@ object UIHelper { ) // or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION // window.clearFlags(View.KEEP_SCREEN_ON) } + + fun Context.shouldShowPIPMode(isInPlayer: Boolean): Boolean { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + return settingsManager?.getBoolean("pip_enabled", true) ?: true && isInPlayer + } + + fun Context.hasPIPPermission(): Boolean { + val appOps = + getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager + return appOps.checkOpNoThrow( + AppOpsManager.OPSTR_PICTURE_IN_PICTURE, + android.os.Process.myUid(), + packageName + ) == AppOpsManager.MODE_ALLOWED + } + + fun Context.hideKeyboard(view: View) { + val inputMethodManager = getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager + inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0) + } } \ No newline at end of file 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 2d0f2b47..6b08a1e0 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 @@ -3,22 +3,28 @@ package com.lagradost.cloudstream3.ui.player import android.animation.ObjectAnimator import android.annotation.SuppressLint import android.app.Activity +import android.app.PendingIntent +import android.app.PictureInPictureParams +import android.app.RemoteAction +import android.content.* import android.content.Context.AUDIO_SERVICE -import android.content.SharedPreferences import android.content.pm.ActivityInfo import android.content.res.Resources import android.database.ContentObserver import android.graphics.Color +import android.graphics.drawable.Icon import android.media.AudioManager import android.net.Uri import android.os.* -import android.util.DisplayMetrics import android.view.LayoutInflater import android.view.MotionEvent import android.view.View import android.view.View.* import android.view.ViewGroup -import android.view.animation.* +import android.view.animation.AccelerateInterpolator +import android.view.animation.AlphaAnimation +import android.view.animation.Animation +import android.view.animation.AnimationUtils import android.widget.ProgressBar import android.widget.Toast import android.widget.Toast.LENGTH_SHORT @@ -26,7 +32,6 @@ import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider -import androidx.mediarouter.app.MediaRouteButton import androidx.preference.PreferenceManager import androidx.transition.Fade import androidx.transition.Transition @@ -44,6 +49,7 @@ 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.google.android.exoplayer2.util.Util import com.google.android.gms.cast.MediaInfo import com.google.android.gms.cast.MediaMetadata import com.google.android.gms.cast.MediaQueueItem @@ -53,10 +59,13 @@ import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.cast.framework.CastState import com.google.android.gms.common.images.WebImage import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.MainActivity.Companion.isInPIPMode +import com.lagradost.cloudstream3.MainActivity.Companion.isInPlayer import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.UIHelper.getFocusRequest import com.lagradost.cloudstream3.UIHelper.getNavigationBarHeight import com.lagradost.cloudstream3.UIHelper.getStatusBarHeight +import com.lagradost.cloudstream3.UIHelper.hideKeyboard import com.lagradost.cloudstream3.UIHelper.hideSystemUI import com.lagradost.cloudstream3.UIHelper.isCastApiAvailable import com.lagradost.cloudstream3.UIHelper.popCurrentPage @@ -72,12 +81,10 @@ import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.getId import kotlinx.android.synthetic.main.fragment_player.* -import kotlinx.android.synthetic.main.fragment_result.* import kotlinx.android.synthetic.main.player_custom_layout.* import kotlinx.coroutines.* import org.json.JSONObject import java.io.File -import java.util.* import javax.net.ssl.HttpsURLConnection import javax.net.ssl.SSLContext import javax.net.ssl.SSLSession @@ -129,12 +136,12 @@ data class PlayerData( ) class PlayerFragment : Fragment() { + private var isCurrentlyPlaying: Boolean = false private val mapper = JsonMapper.builder().addModule(KotlinModule()) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build() private var isFullscreen = false private var isPlayerPlaying = true - private var doubleTapEnabled = false private lateinit var viewModel: ResultViewModel private lateinit var playerData: PlayerData private var isLoading = true @@ -281,20 +288,21 @@ class PlayerFragment : Fragment() { ) } - fun skipOP() { + private fun skipOP() { seekTime(85000L) } + private var swipeEnabled = true // = ArrayList() var currentPoster: String? = null + //region PIP MODE + private fun getPen(code: PlayerEventType): PendingIntent { + return getPen(code.value) + } + + private fun getPen(code: Int): PendingIntent { + return PendingIntent.getBroadcast( + activity, + code, + Intent("media_control").putExtra("control_type", code), + 0 + ) + } + + @SuppressLint("NewApi") + private fun getRemoteAction(id: Int, title: String, event: PlayerEventType): RemoteAction { + return RemoteAction( + Icon.createWithResource(activity, id), + title, + title, + getPen(event) + ) + } + + @SuppressLint("NewApi") + private fun updatePIPModeActions() { + if (!isInPIPMode || !this::exoPlayer.isInitialized) return + + val actions: ArrayList = ArrayList() + + actions.add(getRemoteAction(R.drawable.go_back_30, "Go Back", PlayerEventType.SeekBack)) + + if (exoPlayer.isPlaying) { + actions.add(getRemoteAction(R.drawable.netflix_pause, "Pause", PlayerEventType.Pause)) + } else { + actions.add(getRemoteAction(R.drawable.ic_baseline_play_arrow_24, "Play", PlayerEventType.Play)) + } + + actions.add(getRemoteAction(R.drawable.go_forward_30, "Go Forward", PlayerEventType.SeekForward)) + activity?.setPictureInPictureParams(PictureInPictureParams.Builder().setActions(actions).build()) + } + + private var receiver: BroadcastReceiver? = null + override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) { + isInPIPMode = isInPictureInPictureMode + if (isInPictureInPictureMode) { + // Hide the full-screen UI (controls, etc.) while in picture-in-picture mode. + player_holder.alpha = 0f + receiver = object : BroadcastReceiver() { + override fun onReceive( + context: Context, + intent: Intent, + ) { + if (ACTION_MEDIA_CONTROL != intent.action) { + return + } + handlePlayerEvent(intent.getIntExtra(EXTRA_CONTROL_TYPE, 0)) + } + } + val filter = IntentFilter() + filter.addAction( + ACTION_MEDIA_CONTROL + ) + activity?.registerReceiver(receiver, filter) + updatePIPModeActions() + } else { + // Restore the full-screen UI. + player_holder.alpha = 1f + receiver?.let { + activity?.unregisterReceiver(it) + } + activity?.hideSystemUI() + this.view?.let { activity?.hideKeyboard(it) } + } + } + + private fun handlePlayerEvent(event: PlayerEventType) { + handlePlayerEvent(event.value) + } + + private fun handlePlayerEvent(event: Int) { + when (event) { + PlayerEventType.Play.value -> exoPlayer.play() + PlayerEventType.Pause.value -> exoPlayer.pause() + PlayerEventType.SeekBack.value -> seekTime(-30000L) + PlayerEventType.SeekForward.value -> seekTime(30000L) + } + } +//endregion + @SuppressLint("SetTextI18n") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + settingsManager = PreferenceManager.getDefaultSharedPreferences(activity) + swipeEnabled = settingsManager.getBoolean("swipe_enabled", true) + swipeVerticalEnabled = settingsManager.getBoolean("swipe_vertical_enabled", true) + playBackSpeedEnabled = settingsManager.getBoolean("playback_speed_enabled", false) + playerResizeEnabled = settingsManager.getBoolean("player_resize_enabled", true) + doubleTapEnabled = settingsManager.getBoolean("double_tap_enabled", false) + + isInPlayer = true // NEED REFERENCE TO MAIN ACTIVITY FOR PIP + navigationBarHeight = requireContext().getNavigationBarHeight() statusBarHeight = requireContext().getStatusBarHeight() @@ -723,9 +831,6 @@ class PlayerFragment : Fragment() { } } - println(episodes) - settingsManager = PreferenceManager.getDefaultSharedPreferences(activity) - val fastForwardTime = settingsManager.getInt("fast_forward_button_time", 10) exo_rew_text.text = fastForwardTime.toString() exo_ffwd_text.text = fastForwardTime.toString() @@ -928,6 +1033,8 @@ class PlayerFragment : Fragment() { } changeSkip() + + initPlayer() } private fun getCurrentUrl(): ExtractorLink? { @@ -1003,24 +1110,28 @@ class PlayerFragment : Fragment() { override fun onStart() { super.onStart() - thread { - // initPlayer() - if (player_view != null) player_view.onResume() + if (!isCurrentlyPlaying) { + initPlayer() } + if (player_view != null) player_view.onResume() } override fun onResume() { super.onResume() activity?.hideSystemUI() activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE - thread { - initPlayer() + if (Util.SDK_INT <= 23) { + if (!isCurrentlyPlaying) { + initPlayer() + } if (player_view != null) player_view.onResume() } } + //TODO FIX NON PIP MODE BUG override fun onDestroy() { super.onDestroy() + isInPlayer = false // releasePlayer() activity?.showSystemUI() @@ -1029,14 +1140,18 @@ class PlayerFragment : Fragment() { override fun onPause() { super.onPause() - if (player_view != null) player_view.onPause() - releasePlayer() + if (Util.SDK_INT <= 23) { + if (player_view != null) player_view.onPause() + releasePlayer() + } } override fun onStop() { super.onStop() - if (player_view != null) player_view.onPause() - releasePlayer() + if (Util.SDK_INT > 23) { + if (player_view != null) player_view.onPause() + releasePlayer() + } } override fun onSaveInstanceState(outState: Bundle) { @@ -1229,7 +1344,7 @@ class PlayerFragment : Fragment() { } override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { - // updatePIPModeActions() + updatePIPModeActions() if (activity == null) return if (playWhenReady) { when (playbackState) { @@ -1296,6 +1411,7 @@ class PlayerFragment : Fragment() { //http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4 @SuppressLint("SetTextI18n") private fun initPlayer() { + isCurrentlyPlaying = true 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 val tempUrl = getCurrentUrl() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt new file mode 100644 index 00000000..7024fe34 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt @@ -0,0 +1,11 @@ +package com.lagradost.cloudstream3.ui.settings + +import android.os.Bundle +import androidx.preference.PreferenceFragmentCompat +import com.lagradost.cloudstream3.R + +class SettingsFragment : PreferenceFragmentCompat() { + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.settings, rootKey) + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_discord_24.xml b/app/src/main/res/drawable/ic_baseline_discord_24.xml new file mode 100644 index 00000000..de884ab6 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_discord_24.xml @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_ondemand_video_24.xml b/app/src/main/res/drawable/ic_baseline_ondemand_video_24.xml new file mode 100644 index 00000000..bbd6d57b --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_ondemand_video_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_picture_in_picture_alt_24.xml b/app/src/main/res/drawable/ic_baseline_picture_in_picture_alt_24.xml new file mode 100644 index 00000000..941e803b --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_picture_in_picture_alt_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_touch_app_24.xml b/app/src/main/res/drawable/ic_baseline_touch_app_24.xml new file mode 100644 index 00000000..3a060f63 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_touch_app_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_github_logo.xml b/app/src/main/res/drawable/ic_github_logo.xml new file mode 100644 index 00000000..f7be1e9e --- /dev/null +++ b/app/src/main/res/drawable/ic_github_logo.xml @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/menu/bottom_nav_menu.xml b/app/src/main/res/menu/bottom_nav_menu.xml index a3aa6fcc..acd53108 100644 --- a/app/src/main/res/menu/bottom_nav_menu.xml +++ b/app/src/main/res/menu/bottom_nav_menu.xml @@ -15,5 +15,9 @@ android:id="@+id/navigation_notifications" android:icon="@drawable/netflix_download" android:title="@string/title_downloads"/> + \ No newline at end of file diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index c1a96728..343a8392 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -22,4 +22,10 @@ android:name="com.lagradost.cloudstream3.ui.notifications.NotificationsFragment" android:label="@string/title_downloads" tools:layout="@layout/fragment_notifications"/> + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1956de1e..9e1bcf7d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3,6 +3,7 @@ Home Search Downloads + Settings Search... Change Providers search_providers_list diff --git a/app/src/main/res/xml/backup_descriptor.xml b/app/src/main/res/xml/backup_descriptor.xml new file mode 100644 index 00000000..761b8012 --- /dev/null +++ b/app/src/main/res/xml/backup_descriptor.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/xml/settings.xml b/app/src/main/res/xml/settings.xml new file mode 100644 index 00000000..8456618a --- /dev/null +++ b/app/src/main/res/xml/settings.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file