feat: swipe controls override volume button behaviour (#114)

This commit is contained in:
Chris 2022-08-14 22:19:26 +02:00 committed by GitHub
parent 166bfd35ed
commit 8056e2e9eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 263 additions and 275 deletions

View File

@ -3,7 +3,7 @@ package app.revanced.integrations.patches;
import android.view.WindowManager;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.swipecontrols.views.SwipeControlsHostLayout;
import app.revanced.integrations.swipecontrols.SwipeControlsHostActivity;
/**
* Patch class for 'hdr-auto-brightness' patch
@ -27,7 +27,7 @@ public class HDRAutoBrightnessPatch {
// override with brightness set by swipe-controls
// only when swipe-controls is active and has overridden the brightness
final SwipeControlsHostLayout swipeControlsHost = SwipeControlsPatch.CURRENT_HOST.get();
final SwipeControlsHostActivity swipeControlsHost = SwipeControlsHostActivity.getCurrentHost().get();
if (swipeControlsHost != null
&& swipeControlsHost.getScreen() != null
&& swipeControlsHost.getConfig().getEnableBrightnessControl()

View File

@ -1,42 +0,0 @@
package app.revanced.integrations.patches;
import android.app.Activity;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.lang.ref.WeakReference;
import app.revanced.integrations.swipecontrols.views.SwipeControlsHostLayout;
/**
* Hook receiver class for 'swipe-controls' patch
*
* @usedBy app.revanced.patches.youtube.interaction.swipecontrols.patch.SwipeControlsPatch
* @smali Lapp/revanced/integrations/patches/SwipeControlsPatch;
*/
@SuppressWarnings("unused")
public class SwipeControlsPatch {
/**
* the currently active swipe controls host.
* the reference may be null!
*/
@NonNull
public static WeakReference<SwipeControlsHostLayout> CURRENT_HOST = new WeakReference<>(null);
/**
* Hook into the main activity lifecycle
* (using onStart here, but really anything up until onResume should be fine)
*
* @param thisRef reference to the WatchWhileActivity instance
* @smali WatchWhileActivity_onStartHookEX(Ljava / lang / Object ;)V
*/
public static void WatchWhileActivity_onStartHookEX(@Nullable Object thisRef) {
if (thisRef == null) return;
if (thisRef instanceof Activity) {
SwipeControlsHostLayout swipeControlsHost = SwipeControlsHostLayout.attachTo((Activity) thisRef, false);
CURRENT_HOST = new WeakReference<>(swipeControlsHost);
}
}
}

View File

@ -15,7 +15,7 @@ class SwipeControlsConfigurationProvider(
) {
//region swipe enable
/**
* should swipe controls be enabled? (global setting
* should swipe controls be enabled? (global setting)
*/
val enableSwipeControls: Boolean
get() = isFullscreenVideo && (enableVolumeControls || enableBrightnessControl)
@ -39,6 +39,14 @@ class SwipeControlsConfigurationProvider(
get() = PlayerType.current == PlayerType.WATCH_WHILE_FULLSCREEN
//endregion
//region keys enable
/**
* should volume key controls be overwritten? (global setting)
*/
val overwriteVolumeKeyControls: Boolean
get() = isFullscreenVideo && enableVolumeControls
//endregioin
//region gesture adjustments
/**
* should press-to-swipe be enabled?

View File

@ -0,0 +1,178 @@
package app.revanced.integrations.swipecontrols
import android.app.Activity
import android.os.Bundle
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.ViewGroup
import app.revanced.integrations.shared.PlayerType
import app.revanced.integrations.swipecontrols.controller.AudioVolumeController
import app.revanced.integrations.swipecontrols.controller.ScreenBrightnessController
import app.revanced.integrations.swipecontrols.controller.SwipeZonesController
import app.revanced.integrations.swipecontrols.controller.VolumeKeysController
import app.revanced.integrations.swipecontrols.controller.gesture.NoPtSSwipeGestureController
import app.revanced.integrations.swipecontrols.controller.gesture.SwipeGestureController
import app.revanced.integrations.swipecontrols.misc.Rectangle
import app.revanced.integrations.swipecontrols.views.SwipeControlsOverlayLayout
import app.revanced.integrations.utils.LogHelper
import java.lang.ref.WeakReference
/**
* The main controller for volume and brightness swipe controls.
* note that the superclass is overwritten to the superclass of the WatchWhileActivity at patch time
*
* @smali Lapp/revanced/integrations/swipecontrols/SwipeControlsHostActivity;
*/
class SwipeControlsHostActivity : Activity() {
/**
* current instance of [AudioVolumeController]
*/
var audio: AudioVolumeController? = null
/**
* current instance of [ScreenBrightnessController]
*/
var screen: ScreenBrightnessController? = null
/**
* current instance of [SwipeControlsConfigurationProvider]
*/
lateinit var config: SwipeControlsConfigurationProvider
/**
* current instance of [SwipeControlsOverlayLayout]
*/
lateinit var overlay: SwipeControlsOverlayLayout
/**
* current instance of [SwipeZonesController]
*/
lateinit var zones: SwipeZonesController
/**
* main gesture controller
*/
private lateinit var gesture: SwipeGestureController
/**
* main volume keys controller
*/
private lateinit var keys: VolumeKeysController
/**
* current content view with id [android.R.id.content]
*/
private val contentRoot
get() = window.decorView.findViewById<ViewGroup>(android.R.id.content)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// create controllers
LogHelper.info(this.javaClass, "initializing swipe controls controllers")
config = SwipeControlsConfigurationProvider(this)
gesture = createGestureController()
keys = VolumeKeysController(this)
audio = createAudioController()
screen = createScreenController()
// create overlay
SwipeControlsOverlayLayout(this).let {
overlay = it
contentRoot.addView(it)
}
// create swipe zone controller
zones = SwipeZonesController(this) {
Rectangle(
contentRoot.x.toInt(),
contentRoot.y.toInt(),
contentRoot.width,
contentRoot.height
)
}
// listen for changes in the player type
PlayerType.onChange += this::onPlayerTypeChanged
// set current instance reference
currentHost = WeakReference(this)
}
override fun onStart() {
super.onStart()
// (re) attach overlay
LogHelper.info(this.javaClass, "attaching swipe controls overlay")
contentRoot.removeView(overlay)
contentRoot.addView(overlay)
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
return if ((ev != null) && gesture.onTouchEvent(ev)) true else {
super.dispatchTouchEvent(ev)
}
}
override fun dispatchKeyEvent(ev: KeyEvent?): Boolean {
return if((ev != null) && keys.onKeyEvent(ev)) true else {
super.dispatchKeyEvent(ev)
}
}
/**
* dispatch a touch event to downstream views
*
* @param event the event to dispatch
* @return was the event consumed?
*/
fun dispatchDownstreamTouchEvent(event: MotionEvent) =
super.dispatchTouchEvent(event)
/**
* called when the player type changes
*
* @param type the new player type
*/
private fun onPlayerTypeChanged(type: PlayerType) {
when (type) {
PlayerType.WATCH_WHILE_FULLSCREEN -> screen?.restore()
else -> {
screen?.save()
screen?.restoreDefaultBrightness()
}
}
}
/**
* create the audio volume controller
*/
private fun createAudioController() =
if (config.enableVolumeControls)
AudioVolumeController(this) else null
/**
* create the screen brightness controller instance
*/
private fun createScreenController() =
if (config.enableBrightnessControl)
ScreenBrightnessController(this) else null
/**
* create the gesture controller based on settings
*/
private fun createGestureController() =
if (config.shouldEnablePressToSwipe)
SwipeGestureController(this)
else NoPtSSwipeGestureController(this)
companion object {
/**
* the currently active swipe controls host.
* the reference may be null!
*/
@JvmStatic
var currentHost: WeakReference<SwipeControlsHostActivity> = WeakReference(null)
private set
}
}

View File

@ -1,6 +1,6 @@
package app.revanced.integrations.swipecontrols.controller
import android.content.Context
import android.app.Activity
import android.util.TypedValue
import android.view.ViewGroup
import app.revanced.integrations.swipecontrols.misc.Rectangle
@ -35,29 +35,28 @@ import kotlin.math.min
*/
@Suppress("PrivatePropertyName")
class SwipeZonesController(
context: Context,
private val parentView: ViewGroup,
private val host: Activity,
private val fallbackScreenRect: () -> Rectangle
) {
/**
* 20dp, in pixels
*/
private val _20dp = 20.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
private val _20dp = 20.applyDimension(host, TypedValue.COMPLEX_UNIT_DIP)
/**
* 40dp, in pixels
*/
private val _40dp = 40.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
private val _40dp = 40.applyDimension(host, TypedValue.COMPLEX_UNIT_DIP)
/**
* 80dp, in pixels
*/
private val _80dp = 80.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
private val _80dp = 80.applyDimension(host, TypedValue.COMPLEX_UNIT_DIP)
/**
* id for R.id.player_view
*/
private val playerViewId = ReVancedUtils.getResourceIdByName(context, "id", "player_view")
private val playerViewId = ReVancedUtils.getResourceIdByName(host, "id", "player_view")
/**
* current bounding rectangle of the player
@ -114,7 +113,7 @@ class SwipeZonesController(
*/
private fun maybeAttachPlayerBoundsListener() {
if (playerRect != null) return
parentView.findViewById<ViewGroup>(playerViewId)?.let {
host.findViewById<ViewGroup>(playerViewId)?.let {
onPlayerViewLayout(it)
it.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
onPlayerViewLayout(it)

View File

@ -0,0 +1,51 @@
package app.revanced.integrations.swipecontrols.controller
import android.view.KeyEvent
import app.revanced.integrations.swipecontrols.SwipeControlsHostActivity
/**
* controller for custom volume button behaviour
*
* @param controller main controller instance
*/
class VolumeKeysController(
private val controller: SwipeControlsHostActivity
) {
/**
* key event handler
*
* @param event the key event
* @return consume the event?
*/
fun onKeyEvent(event: KeyEvent): Boolean {
if(!controller.config.overwriteVolumeKeyControls) {
return false
}
return when (event.keyCode) {
KeyEvent.KEYCODE_VOLUME_DOWN ->
handleVolumeKeyEvent(event, false)
KeyEvent.KEYCODE_VOLUME_UP ->
handleVolumeKeyEvent(event, true)
else -> false
}
}
/**
* handle a volume up / down key event
*
* @param event the key event
* @param volumeUp was the key pressed the volume up key?
* @return consume the event?
*/
private fun handleVolumeKeyEvent(event: KeyEvent, volumeUp: Boolean): Boolean {
if (event.action == KeyEvent.ACTION_DOWN) {
controller.audio?.apply {
volume += if (volumeUp) 1 else -1
controller.overlay.onVolumeChanged(volume, maxVolume)
}
}
return true
}
}

View File

@ -1,15 +1,14 @@
package app.revanced.integrations.swipecontrols.controller.gesture
import android.content.Context
import android.view.MotionEvent
import app.revanced.integrations.swipecontrols.views.SwipeControlsHostLayout
import app.revanced.integrations.swipecontrols.SwipeControlsHostActivity
/**
* [SwipeGestureController], but with press-to-swipe disabled because a lot of people dislike the feature.
* If you want to change something, try to do it in [SwipeGestureController] so that both configurations can benefit from it
*/
class NoPtSSwipeGestureController(context: Context, controller: SwipeControlsHostLayout) :
SwipeGestureController(context, controller) {
class NoPtSSwipeGestureController(controller: SwipeControlsHostActivity) :
SwipeGestureController(controller) {
/**
* to disable press-to-swipe, we have to become press-to-swipe

View File

@ -1,14 +1,13 @@
package app.revanced.integrations.swipecontrols.controller.gesture
import android.content.Context
import android.util.TypedValue
import android.view.GestureDetector
import android.view.MotionEvent
import app.revanced.integrations.swipecontrols.SwipeControlsHostActivity
import app.revanced.integrations.swipecontrols.misc.ScrollDistanceHelper
import app.revanced.integrations.swipecontrols.misc.applyDimension
import app.revanced.integrations.swipecontrols.misc.contains
import app.revanced.integrations.swipecontrols.misc.toPoint
import app.revanced.integrations.swipecontrols.views.SwipeControlsHostLayout
import app.revanced.integrations.utils.LogHelper
import kotlin.math.abs
import kotlin.math.pow
@ -17,21 +16,18 @@ import kotlin.math.pow
* base gesture controller for volume and brightness swipe controls controls, with press-to-swipe enabled
* for the controller without press-to-swipe, see [NoPtSSwipeGestureController]
*
* @param context the context to create in
* @param controller reference to main controller instance
*/
@Suppress("LeakingThis")
open class SwipeGestureController(
context: Context,
private val controller: SwipeControlsHostLayout
private val controller: SwipeControlsHostActivity
) :
GestureDetector.SimpleOnGestureListener(),
SwipeControlsHostLayout.TouchEventListener {
GestureDetector.SimpleOnGestureListener() {
/**
* the main gesture detector that powers everything
*/
protected open val detector = GestureDetector(context, this)
protected open val detector = GestureDetector(controller, this)
/**
* to enable swipe controls, users must first long- press. this flags monitors that long- press
@ -60,7 +56,7 @@ open class SwipeGestureController(
*/
protected open val volumeScroller = ScrollDistanceHelper(
10.applyDimension(
context,
controller,
TypedValue.COMPLEX_UNIT_DIP
)
) { _, _, direction ->
@ -75,7 +71,7 @@ open class SwipeGestureController(
*/
protected open val brightnessScroller = ScrollDistanceHelper(
1.applyDimension(
context,
controller,
TypedValue.COMPLEX_UNIT_DIP
)
) { _, _, direction ->
@ -90,7 +86,13 @@ open class SwipeGestureController(
}
}
override fun onTouchEvent(motionEvent: MotionEvent): Boolean {
/**
* touch event callback
*
* @param motionEvent the motion event that was received
* @return intercept the event? if true, child views will not receive the event
*/
fun onTouchEvent(motionEvent: MotionEvent): Boolean {
if (!controller.config.enableSwipeControls) {
return false
}

View File

@ -1,207 +0,0 @@
package app.revanced.integrations.swipecontrols.views
import android.annotation.SuppressLint
import android.app.Activity
import android.graphics.Color
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import app.revanced.integrations.shared.PlayerType
import app.revanced.integrations.swipecontrols.SwipeControlsConfigurationProvider
import app.revanced.integrations.swipecontrols.controller.AudioVolumeController
import app.revanced.integrations.swipecontrols.controller.ScreenBrightnessController
import app.revanced.integrations.swipecontrols.controller.SwipeZonesController
import app.revanced.integrations.swipecontrols.controller.gesture.NoPtSSwipeGestureController
import app.revanced.integrations.swipecontrols.controller.gesture.SwipeGestureController
import app.revanced.integrations.swipecontrols.misc.Rectangle
import app.revanced.integrations.swipecontrols.misc.SwipeControlsOverlay
import app.revanced.integrations.utils.LogHelper
/**
* The main controller for volume and brightness swipe controls
*
* @param hostActivity the activity that should host the controller
* @param debugTouchableZone show a overlay on all zones covered by this layout
*/
@SuppressLint("ViewConstructor")
class SwipeControlsHostLayout(
private val hostActivity: Activity,
private val mainContentChild: View,
debugTouchableZone: Boolean = false
) : FrameLayout(hostActivity) {
init {
isFocusable = false
isClickable = false
if (debugTouchableZone) {
val zoneOverlay = View(context).apply {
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
setBackgroundColor(Color.argb(50, 0, 255, 0))
z = 9999f
}
addView(zoneOverlay)
}
}
/**
* current instance of [AudioVolumeController]
*/
val audio: AudioVolumeController?
/**
* current instance of [ScreenBrightnessController]
*/
val screen: ScreenBrightnessController?
/**
* current instance of [SwipeControlsConfigurationProvider]
*/
val config: SwipeControlsConfigurationProvider
/**
* current instance of [SwipeControlsOverlayLayout]
*/
val overlay: SwipeControlsOverlay
/**
* current instance of [SwipeZonesController]
*/
val zones: SwipeZonesController
/**
* main gesture controller
*/
private val gesture: SwipeGestureController
init {
// create controllers
LogHelper.info(this.javaClass, "initializing swipe controls controllers")
config = SwipeControlsConfigurationProvider(hostActivity)
gesture = createGestureController()
audio = createAudioController()
screen = createScreenController()
// create overlay
SwipeControlsOverlayLayout(hostActivity).let {
overlay = it
addView(it)
}
// create swipe zone controller
zones = SwipeZonesController(context, this) {
Rectangle(x.toInt(), y.toInt(), width, height)
}
// listen for changes in the player type
PlayerType.onChange += this::onPlayerTypeChanged
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
return if (ev != null && gesture.onTouchEvent(ev)) true else {
super.dispatchTouchEvent(ev)
}
}
override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
// main content is always at index 0, all other are inserted after
if (child == mainContentChild) {
super.addView(child, 0, params)
} else {
super.addView(child, childCount, params)
}
}
/**
* called when the player type changes
*
* @param type the new player type
*/
private fun onPlayerTypeChanged(type: PlayerType) {
when (type) {
PlayerType.WATCH_WHILE_FULLSCREEN -> screen?.restore()
else -> {
screen?.save()
screen?.restoreDefaultBrightness()
}
}
}
/**
* dispatch a touch event to downstream views
*
* @param event the event to dispatch
* @return was the event consumed?
*/
fun dispatchDownstreamTouchEvent(event: MotionEvent) =
super.dispatchTouchEvent(event)
/**
* create the audio volume controller
*/
private fun createAudioController() =
if (config.enableVolumeControls)
AudioVolumeController(context) else null
/**
* create the screen brightness controller instance
*/
private fun createScreenController() =
if (config.enableBrightnessControl)
ScreenBrightnessController(hostActivity) else null
/**
* create the gesture controller based on settings
*/
private fun createGestureController() =
if (config.shouldEnablePressToSwipe)
SwipeGestureController(hostActivity, this)
else NoPtSSwipeGestureController(hostActivity, this)
interface TouchEventListener {
/**
* touch event callback
*
* @param motionEvent the motion event that was received
* @return intercept the event? if true, child views will not receive the event
*/
fun onTouchEvent(motionEvent: MotionEvent): Boolean
}
companion object {
/**
* attach a [SwipeControlsHostLayout] to the activity
*
* @param debugTouchableZone show a overlay on all zones covered by this layout
* @return the attached instance
*/
@JvmStatic
fun Activity.attachTo(debugTouchableZone: Boolean = false): SwipeControlsHostLayout {
// get targets
val contentView: ViewGroup = window.decorView.findViewById(android.R.id.content)!!
var content = contentView.getChildAt(0)
// detach previously attached swipe host first
if (content is SwipeControlsHostLayout) {
contentView.removeView(content)
content.removeAllViews()
content = content.mainContentChild
}
// create swipe host
val swipeHost = SwipeControlsHostLayout(this, content, debugTouchableZone).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
// insert the swipe host as parent to the actual content
contentView.removeView(content)
contentView.addView(swipeHost)
swipeHost.addView(content)
return swipeHost
}
}
}