mirror of
https://github.com/revanced/revanced-integrations.git
synced 2024-11-07 20:57:02 +01:00
feat: swipe-controls
rewrite (#64)
* rewrite swipe controls without deep hooks + merge changes + refactor class names * fix gesture detection behaviour * add option to disable press-to-swipe * add config options related to swipe * restore default device brightness when exiting fullscreen player fixes https://github.com/revanced/revanced-patches/issues/128 * set to default brightness after reaching 0% * block swipe-to-dismiss when not using press-to-swipe * fix: TouchThiefLayout potentially attaches multiple times * remove last references to 'fenster' name * move updatePlayerType hook into its own patch * refactor 'swipe-controls' patch * make feedback text backgrond semi-transparent * update swipe-controls overlay * fix swipe-controls leaking host activity context * fix saved screen brightness resetting between videos * fix crash on re-enter activity * make overlay more configurable * add settings to revanced_prefs.xml
This commit is contained in:
parent
376ffc0844
commit
fcabebf3a7
@ -1,33 +0,0 @@
|
|||||||
package app.revanced.integrations.fenster
|
|
||||||
|
|
||||||
import app.revanced.integrations.settings.SettingsEnum
|
|
||||||
|
|
||||||
/**
|
|
||||||
* controls fenster feature enablement
|
|
||||||
*/
|
|
||||||
object FensterEnablement {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* should fenster be enabled? (global setting)
|
|
||||||
*/
|
|
||||||
val shouldEnableFenster: Boolean
|
|
||||||
get() {
|
|
||||||
return shouldEnableFensterVolumeControl || shouldEnableFensterBrightnessControl
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* should swipe controls for volume be enabled?
|
|
||||||
*/
|
|
||||||
val shouldEnableFensterVolumeControl: Boolean
|
|
||||||
get() {
|
|
||||||
return SettingsEnum.ENABLE_SWIPE_VOLUME_BOOLEAN.boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* should swipe controls for volume be enabled?
|
|
||||||
*/
|
|
||||||
val shouldEnableFensterBrightnessControl: Boolean
|
|
||||||
get() {
|
|
||||||
return SettingsEnum.ENABLE_SWIPE_BRIGHTNESS_BOOLEAN.boolean
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
package app.revanced.integrations.fenster
|
|
||||||
|
|
||||||
/**
|
|
||||||
* WatchWhile player types
|
|
||||||
*/
|
|
||||||
@Suppress("unused")
|
|
||||||
enum class WatchWhilePlayerType {
|
|
||||||
NONE,
|
|
||||||
HIDDEN,
|
|
||||||
WATCH_WHILE_MINIMIZED,
|
|
||||||
WATCH_WHILE_MAXIMIZED,
|
|
||||||
WATCH_WHILE_FULLSCREEN,
|
|
||||||
WATCH_WHILE_SLIDING_MAXIMIZED_FULLSCREEN,
|
|
||||||
WATCH_WHILE_SLIDING_MINIMIZED_MAXIMIZED,
|
|
||||||
WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED,
|
|
||||||
WATCH_WHILE_SLIDING_FULLSCREEN_DISMISSED,
|
|
||||||
INLINE_MINIMAL,
|
|
||||||
VIRTUAL_REALITY_FULLSCREEN,
|
|
||||||
WATCH_WHILE_PICTURE_IN_PICTURE;
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
@JvmStatic
|
|
||||||
fun safeParseFromString(name: String): WatchWhilePlayerType? {
|
|
||||||
return values().firstOrNull { it.name == name }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,243 +0,0 @@
|
|||||||
package app.revanced.integrations.fenster.controllers
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.TypedValue
|
|
||||||
import android.view.GestureDetector
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import app.revanced.integrations.fenster.FensterEnablement
|
|
||||||
import app.revanced.integrations.fenster.util.ScrollDistanceHelper
|
|
||||||
import app.revanced.integrations.fenster.util.SwipeControlZone
|
|
||||||
import app.revanced.integrations.fenster.util.applyDimension
|
|
||||||
import app.revanced.integrations.fenster.util.getSwipeControlZone
|
|
||||||
import app.revanced.integrations.utils.LogHelper
|
|
||||||
import kotlin.math.abs
|
|
||||||
|
|
||||||
/**
|
|
||||||
* main controller class for 'FensterV2' swipe controls
|
|
||||||
*/
|
|
||||||
class FensterController {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* are the swipe controls currently enabled?
|
|
||||||
*/
|
|
||||||
var isEnabled: Boolean
|
|
||||||
get() = _isEnabled
|
|
||||||
set(value) {
|
|
||||||
_isEnabled = value && FensterEnablement.shouldEnableFenster
|
|
||||||
overlayController?.setOverlayVisible(_isEnabled)
|
|
||||||
LogHelper.debug(this.javaClass, "FensterController.isEnabled set to $_isEnabled")
|
|
||||||
}
|
|
||||||
private var _isEnabled = false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the activity that hosts the controller
|
|
||||||
*/
|
|
||||||
private var hostActivity: Activity? = null
|
|
||||||
private var audioController: AudioVolumeController? = null
|
|
||||||
private var screenController: ScreenBrightnessController? = null
|
|
||||||
private var overlayController: FensterOverlayController? = null
|
|
||||||
|
|
||||||
private var gestureListener: FensterGestureListener? = null
|
|
||||||
private var gestureDetector: GestureDetector? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the controller.
|
|
||||||
* this function *may* be called after [initializeOverlay], but must be called before [onTouchEvent]
|
|
||||||
*
|
|
||||||
* @param host the activity that hosts the controller. this must be the same activity that the view hook for [onTouchEvent] is on
|
|
||||||
*/
|
|
||||||
fun initializeController(host: Activity) {
|
|
||||||
if (hostActivity != null) {
|
|
||||||
if (host == hostActivity) {
|
|
||||||
// function was called twice, ignore the call
|
|
||||||
LogHelper.debug(
|
|
||||||
this.javaClass,
|
|
||||||
"initializeController was called twice, ignoring secondary call"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LogHelper.debug(this.javaClass, "initializing FensterV2 controllers")
|
|
||||||
hostActivity = host
|
|
||||||
audioController = if (FensterEnablement.shouldEnableFensterVolumeControl)
|
|
||||||
AudioVolumeController(host) else null
|
|
||||||
screenController = if (FensterEnablement.shouldEnableFensterBrightnessControl)
|
|
||||||
ScreenBrightnessController(host) else null
|
|
||||||
|
|
||||||
gestureListener = FensterGestureListener(host)
|
|
||||||
gestureDetector = GestureDetector(host, gestureListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the user feedback overlay, adding it as a child to the provided parent.
|
|
||||||
* this function *may* not be called, but in that case you'll have no user feedback
|
|
||||||
*
|
|
||||||
* @param parent parent view group that the overlay is added to
|
|
||||||
*/
|
|
||||||
fun initializeOverlay(parent: ViewGroup) {
|
|
||||||
LogHelper.debug(this.javaClass, "initializing FensterV2 overlay")
|
|
||||||
|
|
||||||
// create and add overlay
|
|
||||||
overlayController = FensterOverlayController(parent.context)
|
|
||||||
parent.addView(overlayController!!.overlayRootView, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process touch events from the view hook.
|
|
||||||
* the hooked view *must* be a child of the activity used for [initializeController]
|
|
||||||
*
|
|
||||||
* @param event the motion event to process
|
|
||||||
* @return was the event consumed by the controller?
|
|
||||||
*/
|
|
||||||
fun onTouchEvent(event: MotionEvent): Boolean {
|
|
||||||
// if disabled, we shall not consume any events
|
|
||||||
if (!isEnabled) return false
|
|
||||||
|
|
||||||
// if important components are not present, there is no point in processing the event here
|
|
||||||
if (hostActivity == null || gestureDetector == null || gestureListener == null) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// send event to gesture detector
|
|
||||||
if (event.action == MotionEvent.ACTION_UP) {
|
|
||||||
gestureListener?.onUp(event)
|
|
||||||
}
|
|
||||||
val consumed = gestureDetector?.onTouchEvent(event) ?: false
|
|
||||||
|
|
||||||
// if the event was inside a control zone, we always consume the event
|
|
||||||
val swipeZone = event.getSwipeControlZone(hostActivity!!)
|
|
||||||
var inControlZone = false
|
|
||||||
if (audioController != null) {
|
|
||||||
inControlZone = inControlZone || swipeZone == SwipeControlZone.VOLUME_CONTROL
|
|
||||||
}
|
|
||||||
if (screenController != null) {
|
|
||||||
inControlZone = inControlZone || swipeZone == SwipeControlZone.BRIGHTNESS_CONTROL
|
|
||||||
}
|
|
||||||
|
|
||||||
return consumed || inControlZone
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* primary gesture listener that handles the following behaviour:
|
|
||||||
*
|
|
||||||
* - Volume & Brightness swipe controls:
|
|
||||||
* when swiping on the right or left side of the screen, the volume or brightness is adjusted accordingly.
|
|
||||||
* swipe controls are only unlocked after a long- press in the corresponding screen half
|
|
||||||
*
|
|
||||||
* - Fling- to- mute:
|
|
||||||
* when quickly flinging down, the volume is instantly muted
|
|
||||||
*/
|
|
||||||
inner class FensterGestureListener(
|
|
||||||
private val context: Context
|
|
||||||
) : GestureDetector.SimpleOnGestureListener() {
|
|
||||||
|
|
||||||
|
|
||||||
private var inSwipeSession = true
|
|
||||||
|
|
||||||
/**
|
|
||||||
* scroller for volume adjustment
|
|
||||||
*/
|
|
||||||
private val volumeScroller = ScrollDistanceHelper(
|
|
||||||
10.applyDimension(
|
|
||||||
context,
|
|
||||||
TypedValue.COMPLEX_UNIT_DIP
|
|
||||||
)
|
|
||||||
) { _, _, direction ->
|
|
||||||
audioController?.apply {
|
|
||||||
volume += direction
|
|
||||||
overlayController?.showNewVolume((volume * 100.0) / maxVolume)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* scroller for screen brightness adjustment
|
|
||||||
*/
|
|
||||||
private val brightnessScroller = ScrollDistanceHelper(
|
|
||||||
1.applyDimension(
|
|
||||||
context,
|
|
||||||
TypedValue.COMPLEX_UNIT_DIP
|
|
||||||
)
|
|
||||||
) { _, _, direction ->
|
|
||||||
screenController?.apply {
|
|
||||||
screenBrightness += direction
|
|
||||||
overlayController?.showNewBrightness(screenBrightness)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* custom handler for ACTION_UP event, because GestureDetector doesn't offer that :|
|
|
||||||
*
|
|
||||||
* @param e the motion event
|
|
||||||
*/
|
|
||||||
fun onUp(e: MotionEvent) {
|
|
||||||
LogHelper.debug(this.javaClass, "onUp(${e.x}, ${e.y}, ${e.action})")
|
|
||||||
inSwipeSession = true
|
|
||||||
volumeScroller.reset()
|
|
||||||
brightnessScroller.reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun onScroll(
|
|
||||||
eFrom: MotionEvent?,
|
|
||||||
eTo: MotionEvent?,
|
|
||||||
disX: Float,
|
|
||||||
disY: Float
|
|
||||||
): Boolean {
|
|
||||||
if (eFrom == null || eTo == null) return false
|
|
||||||
LogHelper.debug(
|
|
||||||
this.javaClass,
|
|
||||||
"onScroll(from: [${eFrom.x}, ${eFrom.y}, ${eFrom.action}], to: [${eTo.x}, ${eTo.y}, ${eTo.action}], d: [$disX, $disY])"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ignore if scroll not in scroll session
|
|
||||||
if (!inSwipeSession) return false
|
|
||||||
|
|
||||||
// do the adjustment
|
|
||||||
when (eFrom.getSwipeControlZone(context)) {
|
|
||||||
SwipeControlZone.VOLUME_CONTROL -> {
|
|
||||||
volumeScroller.add(disY.toDouble())
|
|
||||||
}
|
|
||||||
SwipeControlZone.BRIGHTNESS_CONTROL -> {
|
|
||||||
brightnessScroller.add(disY.toDouble())
|
|
||||||
}
|
|
||||||
SwipeControlZone.NONE -> {}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFling(
|
|
||||||
eFrom: MotionEvent?,
|
|
||||||
eTo: MotionEvent?,
|
|
||||||
velX: Float,
|
|
||||||
velY: Float
|
|
||||||
): Boolean {
|
|
||||||
if (eFrom == null || eTo == null) return false
|
|
||||||
LogHelper.debug(
|
|
||||||
this.javaClass,
|
|
||||||
"onFling(from: [${eFrom.x}, ${eFrom.y}, ${eFrom.action}], to: [${eTo.x}, ${eTo.y}, ${eTo.action}], v: [$velX, $velY])"
|
|
||||||
)
|
|
||||||
|
|
||||||
// filter out flings that are not very vertical
|
|
||||||
if (abs(velY) < abs(velX * 2)) return false
|
|
||||||
|
|
||||||
// check if either of the events was in the volume zone
|
|
||||||
if ((eFrom.getSwipeControlZone(context) == SwipeControlZone.VOLUME_CONTROL)
|
|
||||||
|| (eTo.getSwipeControlZone(context) == SwipeControlZone.VOLUME_CONTROL)
|
|
||||||
) {
|
|
||||||
// if the fling was very aggressive, trigger instant- mute
|
|
||||||
if (velY > 5000) {
|
|
||||||
audioController?.apply {
|
|
||||||
volume = 0
|
|
||||||
overlayController?.notifyFlingToMutePerformed()
|
|
||||||
overlayController?.showNewVolume((volume * 100.0) / maxVolume)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,130 +0,0 @@
|
|||||||
package app.revanced.integrations.fenster.controllers
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.util.TypedValue
|
|
||||||
import android.view.HapticFeedbackConstants
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.RelativeLayout
|
|
||||||
import android.widget.TextView
|
|
||||||
import app.revanced.integrations.fenster.util.applyDimension
|
|
||||||
import kotlin.math.round
|
|
||||||
|
|
||||||
/**
|
|
||||||
* controller for the fenster overlay
|
|
||||||
*
|
|
||||||
* @param context the context to create the overlay in
|
|
||||||
*/
|
|
||||||
class FensterOverlayController(
|
|
||||||
context: Context
|
|
||||||
) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the main overlay view
|
|
||||||
*/
|
|
||||||
val overlayRootView: RelativeLayout
|
|
||||||
private val feedbackTextView: TextView
|
|
||||||
|
|
||||||
init {
|
|
||||||
// create root container
|
|
||||||
overlayRootView = RelativeLayout(context).apply {
|
|
||||||
layoutParams = ViewGroup.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT
|
|
||||||
)
|
|
||||||
isClickable = false
|
|
||||||
isFocusable = false
|
|
||||||
z = 1000f
|
|
||||||
//elevation = 1000f
|
|
||||||
}
|
|
||||||
|
|
||||||
// add other views
|
|
||||||
val feedbackTextViewPadding = 2.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
|
|
||||||
feedbackTextView = TextView(context).apply {
|
|
||||||
layoutParams = RelativeLayout.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
||||||
).apply {
|
|
||||||
addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE)
|
|
||||||
setPadding(
|
|
||||||
feedbackTextViewPadding,
|
|
||||||
feedbackTextViewPadding,
|
|
||||||
feedbackTextViewPadding,
|
|
||||||
feedbackTextViewPadding
|
|
||||||
)
|
|
||||||
}
|
|
||||||
setBackgroundColor(Color.BLACK)
|
|
||||||
setTextColor(Color.WHITE)
|
|
||||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 20f)
|
|
||||||
visibility = View.GONE
|
|
||||||
}
|
|
||||||
overlayRootView.addView(feedbackTextView)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val feedbackHideHandler = Handler(Looper.getMainLooper())
|
|
||||||
private val feedbackHideCallback = Runnable {
|
|
||||||
feedbackTextView.visibility = View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* set the overlay visibility
|
|
||||||
*
|
|
||||||
* @param visible should the overlay be visible?
|
|
||||||
*/
|
|
||||||
fun setOverlayVisible(visible: Boolean) {
|
|
||||||
overlayRootView.visibility = if (visible) View.VISIBLE else View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* show the new volume level on the overlay
|
|
||||||
*
|
|
||||||
* @param volume the new volume level, in percent (range 0.0 - 100.0)
|
|
||||||
*/
|
|
||||||
fun showNewVolume(volume: Double) {
|
|
||||||
feedbackTextView.text = "Volume ${round(volume).toInt()}%"
|
|
||||||
showFeedbackView()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* show the new screen brightness on the overlay
|
|
||||||
*
|
|
||||||
* @param brightness the new screen brightness, in percent (range 0.0 - 100.0)
|
|
||||||
*/
|
|
||||||
fun showNewBrightness(brightness: Double) {
|
|
||||||
feedbackTextView.text = "Brightness ${round(brightness).toInt()}%"
|
|
||||||
showFeedbackView()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* notify the user that a new swipe- session has started
|
|
||||||
*/
|
|
||||||
fun notifyEnterSwipeSession() {
|
|
||||||
overlayRootView.performHapticFeedback(
|
|
||||||
HapticFeedbackConstants.LONG_PRESS,
|
|
||||||
HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* notify the user that fling-to-mute was triggered
|
|
||||||
*/
|
|
||||||
fun notifyFlingToMutePerformed() {
|
|
||||||
overlayRootView.performHapticFeedback(
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) HapticFeedbackConstants.REJECT else HapticFeedbackConstants.LONG_PRESS,
|
|
||||||
HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* show the feedback view for a given time
|
|
||||||
*/
|
|
||||||
private fun showFeedbackView() {
|
|
||||||
feedbackTextView.visibility = View.VISIBLE
|
|
||||||
feedbackHideHandler.removeCallbacks(feedbackHideCallback)
|
|
||||||
feedbackHideHandler.postDelayed(feedbackHideCallback, 500)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
package app.revanced.integrations.fenster.controllers
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import app.revanced.integrations.fenster.util.clamp
|
|
||||||
|
|
||||||
/**
|
|
||||||
* controller to adjust the screen brightness level
|
|
||||||
*
|
|
||||||
* @param host the host activity of which the brightness is adjusted
|
|
||||||
*/
|
|
||||||
class ScreenBrightnessController(
|
|
||||||
private val host: Activity
|
|
||||||
) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the current screen brightness in percent, ranging from 0.0 to 100.0
|
|
||||||
*/
|
|
||||||
var screenBrightness: Double
|
|
||||||
get() {
|
|
||||||
return host.window.attributes.screenBrightness * 100.0
|
|
||||||
}
|
|
||||||
set(value) {
|
|
||||||
val attr = host.window.attributes
|
|
||||||
attr.screenBrightness = (value.toFloat() / 100f).clamp(0f, 1f)
|
|
||||||
host.window.attributes = attr
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,82 +0,0 @@
|
|||||||
package app.revanced.integrations.fenster.util
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.TypedValue
|
|
||||||
import android.view.MotionEvent
|
|
||||||
|
|
||||||
/**
|
|
||||||
* zones for swipe controls
|
|
||||||
*/
|
|
||||||
enum class SwipeControlZone {
|
|
||||||
/**
|
|
||||||
* not in any zone, should do nothing
|
|
||||||
*/
|
|
||||||
NONE,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* in volume zone, adjust volume
|
|
||||||
*/
|
|
||||||
VOLUME_CONTROL,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* in brightness zone, adjust brightness
|
|
||||||
*/
|
|
||||||
BRIGHTNESS_CONTROL;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get the control zone in which this motion event is
|
|
||||||
*
|
|
||||||
* @return the swipe control zone
|
|
||||||
*/
|
|
||||||
@Suppress("UnnecessaryVariable", "LocalVariableName")
|
|
||||||
fun MotionEvent.getSwipeControlZone(context: Context): SwipeControlZone {
|
|
||||||
// get screen size
|
|
||||||
val screenWidth = device.getMotionRange(MotionEvent.AXIS_X).range
|
|
||||||
val screenHeight = device.getMotionRange(MotionEvent.AXIS_Y).range
|
|
||||||
|
|
||||||
// check in what detection zone the event is in
|
|
||||||
val _40dp = 40.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP).toFloat()
|
|
||||||
val _80dp = 80.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP).toFloat()
|
|
||||||
val _200dp = 200.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP).toFloat()
|
|
||||||
|
|
||||||
// Y- Axis:
|
|
||||||
// -------- 0
|
|
||||||
// ^
|
|
||||||
// dead | 40dp
|
|
||||||
// v
|
|
||||||
// -------- yDeadTop
|
|
||||||
// ^
|
|
||||||
// swipe |
|
|
||||||
// v
|
|
||||||
// -------- yDeadBtm
|
|
||||||
// ^
|
|
||||||
// dead | 80dp
|
|
||||||
// v
|
|
||||||
// -------- screenHeight
|
|
||||||
val yDeadTop = _40dp
|
|
||||||
val yDeadBtm = screenHeight - _80dp
|
|
||||||
|
|
||||||
// X- Axis:
|
|
||||||
// 0 xBrigStart xBrigEnd xVolStart xVolEnd screenWidth
|
|
||||||
// | | | | | |
|
|
||||||
// | 40dp | 200dp | | 200dp | 40dp |
|
|
||||||
// | <------> | <------> | <------> | <------> | <------> |
|
|
||||||
// | dead | brightness | dead | volume | dead |
|
|
||||||
val xBrightStart = _40dp
|
|
||||||
val xBrightEnd = xBrightStart + _200dp
|
|
||||||
val xVolEnd = screenWidth - _40dp
|
|
||||||
val xVolStart = xVolEnd - _200dp
|
|
||||||
|
|
||||||
// test detection zone
|
|
||||||
if (y in yDeadTop..yDeadBtm) {
|
|
||||||
return when (x) {
|
|
||||||
in xBrightStart..xBrightEnd -> SwipeControlZone.BRIGHTNESS_CONTROL
|
|
||||||
in xVolStart..xVolEnd -> SwipeControlZone.VOLUME_CONTROL
|
|
||||||
else -> SwipeControlZone.NONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// not in bounds
|
|
||||||
return SwipeControlZone.NONE
|
|
||||||
}
|
|
@ -1,101 +0,0 @@
|
|||||||
package app.revanced.integrations.patches;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import app.revanced.integrations.fenster.WatchWhilePlayerType;
|
|
||||||
import app.revanced.integrations.fenster.controllers.FensterController;
|
|
||||||
import app.revanced.integrations.utils.LogHelper;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook receiver class for 'FensterV2' video swipe controls.
|
|
||||||
*
|
|
||||||
* @usedBy app.revanced.patches.youtube.interaction.fenster.patch.FensterPatch
|
|
||||||
* @smali Lapp/revanced/integrations/patches/FensterSwipePatch;
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public final class FensterSwipePatch {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* main fenster controller instance
|
|
||||||
*/
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
|
||||||
private static final FensterController FENSTER = new FensterController();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook into the main activity lifecycle
|
|
||||||
*
|
|
||||||
* @param thisRef reference to the WatchWhileActivity instance
|
|
||||||
* @smali Lapp/revanced/integrations/patches/FensterSwipePatch;->WatchWhileActivity_onStartHookEX(Ljava/lang/Object;)V
|
|
||||||
*/
|
|
||||||
public static void WatchWhileActivity_onStartHookEX(Object thisRef) {
|
|
||||||
if (thisRef == null) return;
|
|
||||||
if (thisRef instanceof Activity) {
|
|
||||||
FENSTER.initializeController((Activity) thisRef);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* hook into the player overlays lifecycle
|
|
||||||
*
|
|
||||||
* @param thisRef reference to the PlayerOverlays instance
|
|
||||||
* @smali Lapp/revanced/integrations/patches/FensterSwipePatch;->YouTubePlayerOverlaysLayout_onFinishInflateHookEX(Ljava/lang/Object;)V
|
|
||||||
*/
|
|
||||||
public static void YouTubePlayerOverlaysLayout_onFinishInflateHookEX(Object thisRef) {
|
|
||||||
if (thisRef == null) return;
|
|
||||||
if (thisRef instanceof ViewGroup) {
|
|
||||||
FENSTER.initializeOverlay((ViewGroup) thisRef);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook into updatePlayerLayout() method
|
|
||||||
*
|
|
||||||
* @param type the new player type
|
|
||||||
* @smali Lapp/revanced/integrations/patches/FensterSwipePatch;->YouTubePlayerOverlaysLayout_updatePlayerTypeHookEX(Ljava/lang/Object;)V
|
|
||||||
*/
|
|
||||||
public static void YouTubePlayerOverlaysLayout_updatePlayerTypeHookEX(Object type) {
|
|
||||||
if (type == null) return;
|
|
||||||
|
|
||||||
// disable processing events if not watching fullscreen video
|
|
||||||
WatchWhilePlayerType playerType = WatchWhilePlayerType.safeParseFromString(type.toString());
|
|
||||||
FENSTER.setEnabled(playerType == WatchWhilePlayerType.WATCH_WHILE_FULLSCREEN);
|
|
||||||
LogHelper.debug(FensterSwipePatch.class, "WatchWhile player type was updated to " + playerType);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook into NextGenWatchLayout.onTouchEvent
|
|
||||||
*
|
|
||||||
* @param thisRef reference to NextGenWatchLayout instance
|
|
||||||
* @param motionEvent event parameter
|
|
||||||
* @return was the event consumed by the hook?
|
|
||||||
* @smali Lapp/revanced/integrations/patches/FensterSwipePatch;->NextGenWatchLayout_onTouchEventHookEX(Ljava/lang/Object;Ljava/lang/Object;)Z
|
|
||||||
*/
|
|
||||||
public static boolean NextGenWatchLayout_onTouchEventHookEX(Object thisRef, Object motionEvent) {
|
|
||||||
if (motionEvent == null) return false;
|
|
||||||
if (motionEvent instanceof MotionEvent) {
|
|
||||||
return FENSTER.onTouchEvent((MotionEvent) motionEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook into NextGenWatchLayout.onInterceptTouchEvent
|
|
||||||
*
|
|
||||||
* @param thisRef reference to NextGenWatchLayout instance
|
|
||||||
* @param motionEvent event parameter
|
|
||||||
* @return was the event consumed by the hook?
|
|
||||||
* @smali Lapp/revanced/integrations/patches/FensterSwipePatch;->NextGenWatchLayout_onInterceptTouchEventHookEX(Ljava/lang/Object;Ljava/lang/Object;)Z
|
|
||||||
*/
|
|
||||||
public static boolean NextGenWatchLayout_onInterceptTouchEventHookEX(Object thisRef, Object motionEvent) {
|
|
||||||
if (motionEvent == null) return false;
|
|
||||||
if (motionEvent instanceof MotionEvent) {
|
|
||||||
return FENSTER.onTouchEvent((MotionEvent) motionEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,32 @@
|
|||||||
|
package app.revanced.integrations.patches;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import app.revanced.integrations.utils.LogHelper;
|
||||||
|
import app.revanced.integrations.utils.PlayerType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook receiver class for 'player-type-hook' patch
|
||||||
|
*
|
||||||
|
* @usedBy app.revanced.patches.youtube.misc.playertype.patch.PlayerTypeHookPatch
|
||||||
|
* @smali Lapp/revanced/integrations/patches/PlayerTypeHookPatch;
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class PlayerTypeHookPatch {
|
||||||
|
/**
|
||||||
|
* Hook into YouTubePlayerOverlaysLayout.updatePlayerLayout() method
|
||||||
|
*
|
||||||
|
* @param type the new player type
|
||||||
|
* @smali YouTubePlayerOverlaysLayout_updatePlayerTypeHookEX(Ljava/lang/Object;)V
|
||||||
|
*/
|
||||||
|
public static void YouTubePlayerOverlaysLayout_updatePlayerTypeHookEX(@Nullable Object type) {
|
||||||
|
if (type == null) return;
|
||||||
|
|
||||||
|
// update current player type
|
||||||
|
final PlayerType newType = PlayerType.safeParseFromString(type.toString());
|
||||||
|
if (newType != null) {
|
||||||
|
PlayerType.setCurrent(newType);
|
||||||
|
LogHelper.debug(PlayerTypeHookPatch.class, "YouTubePlayerOverlaysLayout player type was updated to " + newType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
package app.revanced.integrations.patches;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
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 {
|
||||||
|
/**
|
||||||
|
* 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.attachTo((Activity) thisRef, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -66,6 +66,12 @@ public enum SettingsEnum {
|
|||||||
//Swipe controls
|
//Swipe controls
|
||||||
ENABLE_SWIPE_BRIGHTNESS_BOOLEAN("revanced_enable_swipe_brightness", true),
|
ENABLE_SWIPE_BRIGHTNESS_BOOLEAN("revanced_enable_swipe_brightness", true),
|
||||||
ENABLE_SWIPE_VOLUME_BOOLEAN("revanced_enable_swipe_volume", true),
|
ENABLE_SWIPE_VOLUME_BOOLEAN("revanced_enable_swipe_volume", true),
|
||||||
|
ENABLE_PRESS_TO_SWIPE_BOOLEAN("revanced_enable_press_to_swipe", false),
|
||||||
|
ENABLE_SWIPE_HAPTIC_FEEDBACK_BOOLEAN("revanced_enable_swipe_haptic_feedback", true),
|
||||||
|
SWIPE_OVERLAY_TIMEOUT_LONG("revanced_swipe_overlay_timeout", 500L),
|
||||||
|
SWIPE_OVERLAY_TEXT_SIZE_FLOAT("revanced_swipe_overlay_text_size", 22f),
|
||||||
|
SWIPE_OVERLAY_BACKGROUND_ALPHA_INTEGER("revanced_swipe_overlay_background_alpha", 127),
|
||||||
|
SWIPE_MAGNITUDE_THRESHOLD_FLOAT("revanced_swipe_magnitude_threshold", 30f),
|
||||||
|
|
||||||
//Buffer Settings
|
//Buffer Settings
|
||||||
MAX_BUFFER_INTEGER("revanced_pref_max_buffer_ms", 120000),
|
MAX_BUFFER_INTEGER("revanced_pref_max_buffer_ms", 120000),
|
||||||
|
@ -0,0 +1,89 @@
|
|||||||
|
package app.revanced.integrations.swipecontrols
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import app.revanced.integrations.settings.SettingsEnum
|
||||||
|
import app.revanced.integrations.utils.PlayerType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* provider for configuration for volume and brightness swipe controls
|
||||||
|
*
|
||||||
|
* @param context the context to create in
|
||||||
|
*/
|
||||||
|
class SwipeControlsConfigurationProvider(
|
||||||
|
private val context: Context
|
||||||
|
) {
|
||||||
|
//region swipe enable
|
||||||
|
/**
|
||||||
|
* should swipe controls be enabled? (global setting
|
||||||
|
*/
|
||||||
|
val enableSwipeControls: Boolean
|
||||||
|
get() = isFullscreenVideo && (enableVolumeControls || enableBrightnessControl)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* should swipe controls for volume be enabled?
|
||||||
|
*/
|
||||||
|
val enableVolumeControls: Boolean
|
||||||
|
get() = SettingsEnum.ENABLE_SWIPE_VOLUME_BOOLEAN.boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* should swipe controls for volume be enabled?
|
||||||
|
*/
|
||||||
|
val enableBrightnessControl: Boolean
|
||||||
|
get() = SettingsEnum.ENABLE_SWIPE_BRIGHTNESS_BOOLEAN.boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* is the video player currently in fullscreen mode?
|
||||||
|
*/
|
||||||
|
private val isFullscreenVideo: Boolean
|
||||||
|
get() = PlayerType.current == PlayerType.WATCH_WHILE_FULLSCREEN
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region gesture adjustments
|
||||||
|
/**
|
||||||
|
* should press-to-swipe be enabled?
|
||||||
|
*/
|
||||||
|
val shouldEnablePressToSwipe: Boolean
|
||||||
|
get() = SettingsEnum.ENABLE_PRESS_TO_SWIPE_BOOLEAN.boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* threshold for swipe detection
|
||||||
|
* this may be called rapidly in onScroll, so we have to load it once and then leave it constant
|
||||||
|
*/
|
||||||
|
val swipeMagnitudeThreshold: Float = SettingsEnum.SWIPE_MAGNITUDE_THRESHOLD_FLOAT.float
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region overlay adjustments
|
||||||
|
|
||||||
|
/**
|
||||||
|
* should the overlay enable haptic feedback?
|
||||||
|
*/
|
||||||
|
val shouldEnableHapticFeedback: Boolean
|
||||||
|
get() = SettingsEnum.ENABLE_SWIPE_HAPTIC_FEEDBACK_BOOLEAN.boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* how long the overlay should be shown on changes
|
||||||
|
*/
|
||||||
|
val overlayShowTimeoutMillis: Long
|
||||||
|
get() = SettingsEnum.SWIPE_OVERLAY_TIMEOUT_LONG.long
|
||||||
|
|
||||||
|
/**
|
||||||
|
* text size for the overlay, in sp
|
||||||
|
*/
|
||||||
|
val overlayTextSize: Float
|
||||||
|
get() = SettingsEnum.SWIPE_OVERLAY_TEXT_SIZE_FLOAT.float
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get the background color for text on the overlay, as a color int
|
||||||
|
*/
|
||||||
|
val overlayTextBackgroundColor: Int
|
||||||
|
get() = Color.argb(SettingsEnum.SWIPE_OVERLAY_BACKGROUND_ALPHA_INTEGER.int, 0, 0, 0)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get the foreground color for text on the overlay, as a color int
|
||||||
|
*/
|
||||||
|
val overlayForegroundColor: Int
|
||||||
|
get() = Color.WHITE
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
}
|
@ -1,9 +1,9 @@
|
|||||||
package app.revanced.integrations.fenster.controllers
|
package app.revanced.integrations.swipecontrols.controller
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import app.revanced.integrations.fenster.util.clamp
|
import app.revanced.integrations.swipecontrols.misc.clamp
|
||||||
import app.revanced.integrations.utils.LogHelper
|
import app.revanced.integrations.utils.LogHelper
|
||||||
import kotlin.properties.Delegates
|
import kotlin.properties.Delegates
|
||||||
|
|
@ -0,0 +1,65 @@
|
|||||||
|
package app.revanced.integrations.swipecontrols.controller
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.view.WindowManager
|
||||||
|
import app.revanced.integrations.swipecontrols.misc.clamp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* controller to adjust the screen brightness level
|
||||||
|
*
|
||||||
|
* @param host the host activity of which the brightness is adjusted
|
||||||
|
*/
|
||||||
|
class ScreenBrightnessController(
|
||||||
|
private val host: Activity
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* screen brightness saved by [save]
|
||||||
|
*/
|
||||||
|
private var savedScreenBrightness: Float? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the current screen brightness in percent, ranging from 0.0 to 100.0
|
||||||
|
*/
|
||||||
|
var screenBrightness: Double
|
||||||
|
get() = rawScreenBrightness * 100.0
|
||||||
|
set(value) {
|
||||||
|
rawScreenBrightness = (value.toFloat() / 100f).clamp(0f, 1f)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* restore the screen brightness to the default device brightness
|
||||||
|
*/
|
||||||
|
fun restoreDefaultBrightness() {
|
||||||
|
rawScreenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* save the current screen brightness, to be brought back using [restore]
|
||||||
|
*/
|
||||||
|
fun save() {
|
||||||
|
if(savedScreenBrightness == null) {
|
||||||
|
savedScreenBrightness = rawScreenBrightness
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* restore the screen brightness saved using [save]
|
||||||
|
*/
|
||||||
|
fun restore() {
|
||||||
|
savedScreenBrightness?.let {
|
||||||
|
rawScreenBrightness = it
|
||||||
|
}
|
||||||
|
savedScreenBrightness = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* wrapper for the raw screen brightness in [WindowManager.LayoutParams.screenBrightness]
|
||||||
|
*/
|
||||||
|
private var rawScreenBrightness: Float
|
||||||
|
get() = host.window.attributes.screenBrightness
|
||||||
|
set(value) {
|
||||||
|
val attr = host.window.attributes
|
||||||
|
attr.screenBrightness = value
|
||||||
|
host.window.attributes = attr
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package app.revanced.integrations.swipecontrols.controller.gesture
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import app.revanced.integrations.swipecontrols.views.SwipeControlsHostLayout
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [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) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* to disable press-to-swipe, we have to become press-to-swipe
|
||||||
|
*/
|
||||||
|
override var inSwipeSession
|
||||||
|
get() = true
|
||||||
|
set(_) {}
|
||||||
|
|
||||||
|
override fun onLongPress(e: MotionEvent?) {
|
||||||
|
if (e == null) return
|
||||||
|
|
||||||
|
// send GestureDetector a ACTION_CANCEL event so it will handle further events
|
||||||
|
// if this is left out, swipe-to-dismiss is triggered when scrolling down
|
||||||
|
e.action = MotionEvent.ACTION_CANCEL
|
||||||
|
detector.onTouchEvent(e)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,213 @@
|
|||||||
|
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.views.SwipeControlsHostLayout
|
||||||
|
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.utils.LogHelper
|
||||||
|
import kotlin.math.abs
|
||||||
|
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
|
||||||
|
) :
|
||||||
|
GestureDetector.SimpleOnGestureListener(),
|
||||||
|
SwipeControlsHostLayout.TouchEventListener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the main gesture detector that powers everything
|
||||||
|
*/
|
||||||
|
protected open val detector = GestureDetector(context, this)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* to enable swipe controls, users must first long- press. this flags monitors that long- press
|
||||||
|
* NOTE: if you dislike press-to-swipe, and want it disabled, have a look at [NoPtSSwipeGestureController]. it does exactly that
|
||||||
|
*/
|
||||||
|
protected open var inSwipeSession = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* currently in- progress swipe
|
||||||
|
*/
|
||||||
|
protected open var currentSwipe: SwipeDirection = SwipeDirection.NONE
|
||||||
|
|
||||||
|
/**
|
||||||
|
* were downstream event cancelled already? used by [onScroll]
|
||||||
|
*/
|
||||||
|
protected open var didCancelDownstream = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* should [onTouchEvent] force- intercept all touch events?
|
||||||
|
*/
|
||||||
|
protected open val shouldForceInterceptEvents: Boolean
|
||||||
|
get() = currentSwipe == SwipeDirection.VERTICAL && inSwipeSession
|
||||||
|
|
||||||
|
/**
|
||||||
|
* scroller for volume adjustment
|
||||||
|
*/
|
||||||
|
protected open val volumeScroller = ScrollDistanceHelper(
|
||||||
|
10.applyDimension(
|
||||||
|
context,
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP
|
||||||
|
)
|
||||||
|
) { _, _, direction ->
|
||||||
|
controller.audio?.apply {
|
||||||
|
volume += direction
|
||||||
|
controller.overlay.onVolumeChanged(volume, maxVolume)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* scroller for screen brightness adjustment
|
||||||
|
*/
|
||||||
|
protected open val brightnessScroller = ScrollDistanceHelper(
|
||||||
|
1.applyDimension(
|
||||||
|
context,
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP
|
||||||
|
)
|
||||||
|
) { _, _, direction ->
|
||||||
|
controller.screen?.apply {
|
||||||
|
if (screenBrightness > 0 || direction > 0) {
|
||||||
|
screenBrightness += direction
|
||||||
|
} else {
|
||||||
|
restoreDefaultBrightness()
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.overlay.onBrightnessChanged(screenBrightness)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTouchEvent(motionEvent: MotionEvent): Boolean {
|
||||||
|
if (!controller.config.enableSwipeControls) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (motionEvent.action == MotionEvent.ACTION_UP) {
|
||||||
|
onUp(motionEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
return detector.onTouchEvent(motionEvent) or shouldForceInterceptEvents
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* custom handler for ACTION_UP event, because GestureDetector doesn't offer that :|
|
||||||
|
* basically just resets all flags to non- swiping values
|
||||||
|
*
|
||||||
|
* @param e the motion event
|
||||||
|
*/
|
||||||
|
open fun onUp(e: MotionEvent) {
|
||||||
|
LogHelper.debug(this.javaClass, "onUp(${e.x}, ${e.y}, ${e.action})")
|
||||||
|
inSwipeSession = false
|
||||||
|
currentSwipe = SwipeDirection.NONE
|
||||||
|
didCancelDownstream = false
|
||||||
|
volumeScroller.reset()
|
||||||
|
brightnessScroller.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLongPress(e: MotionEvent?) {
|
||||||
|
if (e == null) return
|
||||||
|
LogHelper.debug(this.javaClass, "onLongPress(${e.x}, ${e.y}, ${e.action})")
|
||||||
|
|
||||||
|
// enter swipe session with feedback
|
||||||
|
inSwipeSession = true
|
||||||
|
controller.overlay.onEnterSwipeSession()
|
||||||
|
|
||||||
|
// send GestureDetector a ACTION_CANCEL event so it will handle further events
|
||||||
|
e.action = MotionEvent.ACTION_CANCEL
|
||||||
|
detector.onTouchEvent(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onScroll(
|
||||||
|
eFrom: MotionEvent?,
|
||||||
|
eTo: MotionEvent?,
|
||||||
|
disX: Float,
|
||||||
|
disY: Float
|
||||||
|
): Boolean {
|
||||||
|
if (eFrom == null || eTo == null) return false
|
||||||
|
LogHelper.debug(
|
||||||
|
this.javaClass,
|
||||||
|
"onScroll(from: [${eFrom.x}, ${eFrom.y}, ${eFrom.action}], to: [${eTo.x}, ${eTo.y}, ${eTo.action}], d: [$disX, $disY])"
|
||||||
|
)
|
||||||
|
|
||||||
|
return when (currentSwipe) {
|
||||||
|
// no swipe direction was detected yet, try to detect one
|
||||||
|
// if the user did not swipe far enough, we cannot detect what direction they swiped
|
||||||
|
// so we wait until a greater distance was swiped
|
||||||
|
// NOTE: sqrt() can be high- cost, so using squared magnitudes here
|
||||||
|
SwipeDirection.NONE -> {
|
||||||
|
val deltaX = abs(eTo.x - eFrom.x)
|
||||||
|
val deltaY = abs(eTo.y - eFrom.y)
|
||||||
|
val swipeMagnitudeSquared = deltaX.pow(2) + deltaY.pow(2)
|
||||||
|
if (swipeMagnitudeSquared > controller.config.swipeMagnitudeThreshold.pow(2)) {
|
||||||
|
currentSwipe = if (deltaY > deltaX) {
|
||||||
|
SwipeDirection.VERTICAL
|
||||||
|
} else {
|
||||||
|
SwipeDirection.HORIZONTAL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// horizontal swipe, we should leave this one for downstream to handle
|
||||||
|
SwipeDirection.HORIZONTAL -> false
|
||||||
|
|
||||||
|
// vertical swipe, could be for us
|
||||||
|
SwipeDirection.VERTICAL -> {
|
||||||
|
if (!inSwipeSession) {
|
||||||
|
// not in swipe session, let downstream handle this one
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// vertical & in swipe session, handle this one:
|
||||||
|
// first, send ACTION_CANCEL to downstream to let them known they should stop tracking events
|
||||||
|
if (!didCancelDownstream) {
|
||||||
|
val eCancel = MotionEvent.obtain(eFrom)
|
||||||
|
eCancel.action = MotionEvent.ACTION_CANCEL
|
||||||
|
controller.dispatchDownstreamTouchEvent(eCancel)
|
||||||
|
eCancel.recycle()
|
||||||
|
didCancelDownstream = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// then, process the event
|
||||||
|
when (eFrom.toPoint()) {
|
||||||
|
in controller.volumeZone -> volumeScroller.add(disY.toDouble())
|
||||||
|
in controller.brightnessZone -> brightnessScroller.add(disY.toDouble())
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* direction of a swipe
|
||||||
|
*/
|
||||||
|
enum class SwipeDirection {
|
||||||
|
/**
|
||||||
|
* swipe has no direction or no swipe
|
||||||
|
*/
|
||||||
|
NONE,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* swipe along the X- Axes
|
||||||
|
*/
|
||||||
|
HORIZONTAL,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* swipe along the Y- Axes
|
||||||
|
*/
|
||||||
|
VERTICAL
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
package app.revanced.integrations.swipecontrols.misc
|
||||||
|
|
||||||
|
import android.view.MotionEvent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* a simple 2D point class
|
||||||
|
*/
|
||||||
|
data class Point(
|
||||||
|
val x: Int,
|
||||||
|
val y: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* convert the motion event coordinates to a point
|
||||||
|
*/
|
||||||
|
fun MotionEvent.toPoint(): Point =
|
||||||
|
Point(x.toInt(), y.toInt())
|
@ -0,0 +1,23 @@
|
|||||||
|
package app.revanced.integrations.swipecontrols.misc
|
||||||
|
|
||||||
|
/**
|
||||||
|
* a simple rectangle class
|
||||||
|
*/
|
||||||
|
data class Rectangle(
|
||||||
|
val x: Int,
|
||||||
|
val y: Int,
|
||||||
|
val width: Int,
|
||||||
|
val height: Int
|
||||||
|
) {
|
||||||
|
val left = x
|
||||||
|
val right = x + width
|
||||||
|
val top = y
|
||||||
|
val bottom = y + height
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* is the point within this rectangle?
|
||||||
|
*/
|
||||||
|
operator fun Rectangle.contains(p: Point): Boolean =
|
||||||
|
p.x in left..right && p.y in top..bottom
|
@ -1,4 +1,4 @@
|
|||||||
package app.revanced.integrations.fenster.util
|
package app.revanced.integrations.swipecontrols.misc
|
||||||
|
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.sign
|
import kotlin.math.sign
|
@ -0,0 +1,26 @@
|
|||||||
|
package app.revanced.integrations.swipecontrols.misc
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for all overlays for swipe controls
|
||||||
|
*/
|
||||||
|
interface SwipeControlsOverlay {
|
||||||
|
/**
|
||||||
|
* called when the currently set volume level was changed
|
||||||
|
*
|
||||||
|
* @param newVolume the new volume level
|
||||||
|
* @param maximumVolume the maximum volume index
|
||||||
|
*/
|
||||||
|
fun onVolumeChanged(newVolume: Int, maximumVolume: Int)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* called when the currently set screen brightness was changed
|
||||||
|
*
|
||||||
|
* @param brightness the new screen brightness, in percent (range 0.0 - 100.0)
|
||||||
|
*/
|
||||||
|
fun onBrightnessChanged(brightness: Double)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* called when a new swipe- session has started
|
||||||
|
*/
|
||||||
|
fun onEnterSwipeSession()
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package app.revanced.integrations.fenster.util
|
package app.revanced.integrations.swipecontrols.misc
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
@ -0,0 +1,73 @@
|
|||||||
|
package app.revanced.integrations.swipecontrols.misc
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.TypedValue
|
||||||
|
|
||||||
|
//TODO reimplement this, again with 1/3rd for the zone size
|
||||||
|
// because in shorts, the screen is way less wide than this code expects!
|
||||||
|
/**
|
||||||
|
* Y- Axis:
|
||||||
|
* -------- 0
|
||||||
|
* ^
|
||||||
|
* dead | 40dp
|
||||||
|
* v
|
||||||
|
* -------- yDeadTop
|
||||||
|
* ^
|
||||||
|
* swipe |
|
||||||
|
* v
|
||||||
|
* -------- yDeadBtm
|
||||||
|
* ^
|
||||||
|
* dead | 80dp
|
||||||
|
* v
|
||||||
|
* -------- screenHeight
|
||||||
|
*
|
||||||
|
* X- Axis:
|
||||||
|
* 0 xBrigStart xBrigEnd xVolStart xVolEnd screenWidth
|
||||||
|
* | | | | | |
|
||||||
|
* | 40dp | 200dp | | 200dp | 40dp |
|
||||||
|
* | <------> | <------> | <------> | <------> | <------> |
|
||||||
|
* | dead | brightness | dead | volume | dead |
|
||||||
|
*/
|
||||||
|
@Suppress("LocalVariableName")
|
||||||
|
object SwipeZonesHelper {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get the zone for volume control
|
||||||
|
*
|
||||||
|
* @param context the current context
|
||||||
|
* @param screenRect the screen rectangle in the current orientation
|
||||||
|
* @return the rectangle for the control zone
|
||||||
|
*/
|
||||||
|
fun getVolumeControlZone(context: Context, screenRect: Rectangle): Rectangle {
|
||||||
|
val _40dp = 40.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
|
||||||
|
val _80dp = 80.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
|
||||||
|
val _200dp = 200.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
|
||||||
|
|
||||||
|
return Rectangle(
|
||||||
|
screenRect.right - _40dp - _200dp,
|
||||||
|
screenRect.top + _40dp,
|
||||||
|
_200dp,
|
||||||
|
screenRect.height - _40dp - _80dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get the zone for brightness control
|
||||||
|
*
|
||||||
|
* @param context the current context
|
||||||
|
* @param screenRect the screen rectangle in the current orientation
|
||||||
|
* @return the rectangle for the control zone
|
||||||
|
*/
|
||||||
|
fun getBrightnessControlZone(context: Context, screenRect: Rectangle): Rectangle {
|
||||||
|
val _40dp = 40.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
|
||||||
|
val _80dp = 80.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
|
||||||
|
val _200dp = 200.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
|
||||||
|
|
||||||
|
return Rectangle(
|
||||||
|
screenRect.left + _40dp,
|
||||||
|
screenRect.top + _40dp,
|
||||||
|
_200dp,
|
||||||
|
screenRect.height - _40dp - _80dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,215 @@
|
|||||||
|
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.swipecontrols.SwipeControlsConfigurationProvider
|
||||||
|
import app.revanced.integrations.swipecontrols.controller.AudioVolumeController
|
||||||
|
import app.revanced.integrations.swipecontrols.controller.ScreenBrightnessController
|
||||||
|
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.swipecontrols.misc.SwipeZonesHelper
|
||||||
|
import app.revanced.integrations.utils.LogHelper
|
||||||
|
import app.revanced.integrations.utils.PlayerType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the current screen rectangle
|
||||||
|
*/
|
||||||
|
private val screenRect: Rectangle
|
||||||
|
get() = Rectangle(x.toInt(), y.toInt(), width, height)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the rectangle of the volume control zone
|
||||||
|
*/
|
||||||
|
val volumeZone: Rectangle
|
||||||
|
get() = SwipeZonesHelper.getVolumeControlZone(hostActivity, screenRect)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the rectangle of the screen brightness control zone
|
||||||
|
*/
|
||||||
|
val brightnessZone: Rectangle
|
||||||
|
get() = SwipeZonesHelper.getBrightnessControlZone(hostActivity, screenRect)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,140 @@
|
|||||||
|
package app.revanced.integrations.swipecontrols.views
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.HapticFeedbackConstants
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.RelativeLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import app.revanced.integrations.swipecontrols.SwipeControlsConfigurationProvider
|
||||||
|
import app.revanced.integrations.swipecontrols.misc.SwipeControlsOverlay
|
||||||
|
import app.revanced.integrations.swipecontrols.misc.applyDimension
|
||||||
|
import app.revanced.integrations.utils.ReVancedUtils
|
||||||
|
import kotlin.math.round
|
||||||
|
|
||||||
|
/**
|
||||||
|
* main overlay layout for volume and brightness swipe controls
|
||||||
|
*
|
||||||
|
* @param context context to create in
|
||||||
|
*/
|
||||||
|
class SwipeControlsOverlayLayout(
|
||||||
|
context: Context,
|
||||||
|
private val config: SwipeControlsConfigurationProvider
|
||||||
|
) : RelativeLayout(context), SwipeControlsOverlay {
|
||||||
|
/**
|
||||||
|
* DO NOT use this, for tools only
|
||||||
|
*/
|
||||||
|
constructor(context: Context) : this(context, SwipeControlsConfigurationProvider(context))
|
||||||
|
|
||||||
|
private val feedbackTextView: TextView
|
||||||
|
private val autoBrightnessIcon: Drawable
|
||||||
|
private val manualBrightnessIcon: Drawable
|
||||||
|
private val mutedVolumeIcon: Drawable
|
||||||
|
private val normalVolumeIcon: Drawable
|
||||||
|
|
||||||
|
private fun getDrawable(name: String, width: Int, height: Int): Drawable {
|
||||||
|
return resources.getDrawable(
|
||||||
|
ReVancedUtils.getResourceIdByName(context, "drawable", name),
|
||||||
|
context.theme
|
||||||
|
).apply {
|
||||||
|
setTint(config.overlayForegroundColor)
|
||||||
|
setBounds(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
width,
|
||||||
|
height
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
// init views
|
||||||
|
val feedbackTextViewPadding = 2.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
|
||||||
|
val compoundIconPadding = 4.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
|
||||||
|
feedbackTextView = TextView(context).apply {
|
||||||
|
layoutParams = LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
).apply {
|
||||||
|
addRule(CENTER_IN_PARENT, TRUE)
|
||||||
|
setPadding(
|
||||||
|
feedbackTextViewPadding,
|
||||||
|
feedbackTextViewPadding,
|
||||||
|
feedbackTextViewPadding,
|
||||||
|
feedbackTextViewPadding
|
||||||
|
)
|
||||||
|
}
|
||||||
|
background = GradientDrawable().apply {
|
||||||
|
cornerRadius = 8f
|
||||||
|
setColor(config.overlayTextBackgroundColor)
|
||||||
|
}
|
||||||
|
setTextColor(config.overlayForegroundColor)
|
||||||
|
setTextSize(TypedValue.COMPLEX_UNIT_SP, config.overlayTextSize)
|
||||||
|
compoundDrawablePadding = compoundIconPadding
|
||||||
|
visibility = GONE
|
||||||
|
}
|
||||||
|
addView(feedbackTextView)
|
||||||
|
|
||||||
|
// get icons scaled, assuming square icons
|
||||||
|
val iconHeight = round(feedbackTextView.lineHeight * .8).toInt()
|
||||||
|
autoBrightnessIcon = getDrawable("ic_sc_brightness_auto", iconHeight, iconHeight)
|
||||||
|
manualBrightnessIcon = getDrawable("ic_sc_brightness_manual", iconHeight, iconHeight)
|
||||||
|
mutedVolumeIcon = getDrawable("ic_sc_volume_mute", iconHeight, iconHeight)
|
||||||
|
normalVolumeIcon = getDrawable("ic_sc_volume_normal", iconHeight, iconHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val feedbackHideHandler = Handler(Looper.getMainLooper())
|
||||||
|
private val feedbackHideCallback = Runnable {
|
||||||
|
feedbackTextView.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* show the feedback view for a given time
|
||||||
|
*
|
||||||
|
* @param message the message to show
|
||||||
|
* @param icon the icon to use
|
||||||
|
*/
|
||||||
|
private fun showFeedbackView(message: String, icon: Drawable) {
|
||||||
|
feedbackHideHandler.removeCallbacks(feedbackHideCallback)
|
||||||
|
feedbackHideHandler.postDelayed(feedbackHideCallback, config.overlayShowTimeoutMillis)
|
||||||
|
feedbackTextView.apply {
|
||||||
|
text = message
|
||||||
|
setCompoundDrawablesRelative(
|
||||||
|
icon,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
visibility = VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onVolumeChanged(newVolume: Int, maximumVolume: Int) {
|
||||||
|
showFeedbackView(
|
||||||
|
"$newVolume",
|
||||||
|
if (newVolume > 0) normalVolumeIcon else mutedVolumeIcon
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBrightnessChanged(brightness: Double) {
|
||||||
|
if (brightness > 0) {
|
||||||
|
showFeedbackView("${round(brightness).toInt()}%", manualBrightnessIcon)
|
||||||
|
} else {
|
||||||
|
showFeedbackView("AUTO", autoBrightnessIcon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEnterSwipeSession() {
|
||||||
|
if (config.shouldEnableHapticFeedback) {
|
||||||
|
performHapticFeedback(
|
||||||
|
HapticFeedbackConstants.LONG_PRESS,
|
||||||
|
HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
app/src/main/java/app/revanced/integrations/utils/Event.kt
Normal file
22
app/src/main/java/app/revanced/integrations/utils/Event.kt
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package app.revanced.integrations.utils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* generic event provider class
|
||||||
|
*/
|
||||||
|
class Event<T> {
|
||||||
|
private val eventListeners = mutableSetOf<(T) -> Unit>()
|
||||||
|
|
||||||
|
operator fun plusAssign(observer: (T) -> Unit) {
|
||||||
|
eventListeners.add(observer)
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun minusAssign(observer: (T) -> Unit) {
|
||||||
|
eventListeners.remove(observer)
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun invoke(value: T) {
|
||||||
|
for (observer in eventListeners)
|
||||||
|
observer.invoke(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
|||||||
|
package app.revanced.integrations.utils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WatchWhile player type
|
||||||
|
*/
|
||||||
|
@Suppress("unused")
|
||||||
|
enum class PlayerType {
|
||||||
|
NONE,
|
||||||
|
HIDDEN,
|
||||||
|
WATCH_WHILE_MINIMIZED,
|
||||||
|
WATCH_WHILE_MAXIMIZED,
|
||||||
|
WATCH_WHILE_FULLSCREEN,
|
||||||
|
WATCH_WHILE_SLIDING_MAXIMIZED_FULLSCREEN,
|
||||||
|
WATCH_WHILE_SLIDING_MINIMIZED_MAXIMIZED,
|
||||||
|
WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED,
|
||||||
|
WATCH_WHILE_SLIDING_FULLSCREEN_DISMISSED,
|
||||||
|
INLINE_MINIMAL,
|
||||||
|
VIRTUAL_REALITY_FULLSCREEN,
|
||||||
|
WATCH_WHILE_PICTURE_IN_PICTURE;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* safely parse from a string
|
||||||
|
*
|
||||||
|
* @param name the name to find
|
||||||
|
* @return the enum constant, or null if not found
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun safeParseFromString(name: String): PlayerType? {
|
||||||
|
return values().firstOrNull { it.name == name }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the current player type, as reported by [app.revanced.integrations.patches.PlayerTypeHookPatch.YouTubePlayerOverlaysLayout_updatePlayerTypeHookEX]
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
var current
|
||||||
|
get() = currentPlayerType
|
||||||
|
set(value) {
|
||||||
|
currentPlayerType = value
|
||||||
|
onChange(currentPlayerType)
|
||||||
|
}
|
||||||
|
private var currentPlayerType = NONE
|
||||||
|
|
||||||
|
/**
|
||||||
|
* player type change listener
|
||||||
|
*/
|
||||||
|
val onChange = Event<PlayerType>()
|
||||||
|
}
|
||||||
|
}
|
@ -51,6 +51,16 @@ public class ReVancedUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Integer getResourceIdByName(Context context, String type, String name) {
|
||||||
|
try {
|
||||||
|
Resources res = context.getResources();
|
||||||
|
return res.getIdentifier(name, type, context.getPackageName());
|
||||||
|
} catch (Throwable exception) {
|
||||||
|
LogHelper.printException(ReVancedUtils.class, "Resource not found.", exception);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void setPlayerType(PlayerType type) {
|
public static void setPlayerType(PlayerType type) {
|
||||||
env = type;
|
env = type;
|
||||||
}
|
}
|
||||||
|
5
app/src/main/res/drawable/ic_sc_brightness_auto.xml
Normal file
5
app/src/main/res/drawable/ic_sc_brightness_auto.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp" android:tint="#000000"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M10.85,12.65h2.3L12,9l-1.15,3.65zM20,8.69V4h-4.69L12,0.69 8.69,4H4v4.69L0.69,12 4,15.31V20h4.69L12,23.31 15.31,20H20v-4.69L23.31,12 20,8.69zM14.3,16l-0.7,-2h-3.2l-0.7,2H7.8L11,7h2l3.2,9h-1.9z"/>
|
||||||
|
</vector>
|
5
app/src/main/res/drawable/ic_sc_brightness_manual.xml
Normal file
5
app/src/main/res/drawable/ic_sc_brightness_manual.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp" android:tint="#000000"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M20,15.31L23.31,12 20,8.69V4h-4.69L12,0.69 8.69,4H4v4.69L0.69,12 4,15.31V20h4.69L12,23.31 15.31,20H20v-4.69zM12,18V6c3.31,0 6,2.69 6,6s-2.69,6 -6,6z"/>
|
||||||
|
</vector>
|
5
app/src/main/res/drawable/ic_sc_volume_mute.xml
Normal file
5
app/src/main/res/drawable/ic_sc_volume_mute.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp" android:tint="#000000"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M16.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v2.21l2.45,2.45c0.03,-0.2 0.05,-0.41 0.05,-0.63zM19,12c0,0.94 -0.2,1.82 -0.54,2.64l1.51,1.51C20.63,14.91 21,13.5 21,12c0,-4.28 -2.99,-7.86 -7,-8.77v2.06c2.89,0.86 5,3.54 5,6.71zM4.27,3L3,4.27 7.73,9L3,9v6h4l5,5v-6.73l4.25,4.25c-0.67,0.52 -1.42,0.93 -2.25,1.18v2.06c1.38,-0.31 2.63,-0.95 3.69,-1.81L19.73,21 21,19.73l-9,-9L4.27,3zM12,4L9.91,6.09 12,8.18L12,4z"/>
|
||||||
|
</vector>
|
5
app/src/main/res/drawable/ic_sc_volume_normal.xml
Normal file
5
app/src/main/res/drawable/ic_sc_volume_normal.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp" android:tint="#000000"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M3,9v6h4l5,5L12,4L7,9L3,9zM16.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v8.05c1.48,-0.73 2.5,-2.25 2.5,-4.02zM14,3.23v2.06c2.89,0.86 5,3.54 5,6.71s-2.11,5.85 -5,6.71v2.06c4.01,-0.91 7,-4.49 7,-8.77s-2.99,-7.86 -7,-8.77z"/>
|
||||||
|
</vector>
|
@ -93,6 +93,20 @@
|
|||||||
<string name="revanced_xfenster_volume_summary_off">Swipe controls for volume are disabled</string>
|
<string name="revanced_xfenster_volume_summary_off">Swipe controls for volume are disabled</string>
|
||||||
<string name="revanced_xfenster_volume_summary_on">Swipe controls for volume are enabled</string>
|
<string name="revanced_xfenster_volume_summary_on">Swipe controls for volume are enabled</string>
|
||||||
<string name="revanced_xfenster_volume_title">Swipe controls for Volume</string>
|
<string name="revanced_xfenster_volume_title">Swipe controls for Volume</string>
|
||||||
|
<string name="revanced_swipe_pts_title">Press-to-Swipe</string>
|
||||||
|
<string name="revanced_swipe_pts_summary_off">Swipe controls are always active</string>
|
||||||
|
<string name="revanced_swipe_pts_summary_on">Swipe controls require a long-press before activating</string>
|
||||||
|
<string name="revanced_swipe_pts_haptic_title">Vibrate on Press-to-Swipe</string>
|
||||||
|
<string name="revanced_swipe_pts_haptic_summary_on">You\'ll get haptic feedback when activating Press-to-Swipe</string>
|
||||||
|
<string name="revanced_swipe_pts_haptic_summary_off">You won\'t get haptic feedback when activating Press-to-Swipe</string>
|
||||||
|
<string name="revanced_swipe_overlay_timeout_title">Overlay Timeout</string>
|
||||||
|
<string name="revanced_swipe_overlay_timeout_summary">How long the overlay is shown after changes (ms)</string>
|
||||||
|
<string name="revanced_swipe_overlay_text_size_title">Overlay Text Size</string>
|
||||||
|
<string name="revanced_swipe_overlay_text_size_summary">Text size on the overlay</string>
|
||||||
|
<string name="revanced_swipe_overlay_bg_alpha_title">Overlay Background Transparency</string>
|
||||||
|
<string name="revanced_swipe_overlay_bg_alpha_summary">Transparency value of the overlay background (0–255)</string>
|
||||||
|
<string name="revanced_swipe_magnitude_threshold_title">Swipe Magnitude Threshold</string>
|
||||||
|
<string name="revanced_swipe_magnitude_threshold_summary">Minimum magnitude before a swipe is detected</string>
|
||||||
<string name="revanced_website_summary">Tap to open our website</string>
|
<string name="revanced_website_summary">Tap to open our website</string>
|
||||||
<string name="revanced_website_title">ReVanced website</string>
|
<string name="revanced_website_title">ReVanced website</string>
|
||||||
<string name="revanced_home_ads_summary_off">Home ADS are hidden</string>
|
<string name="revanced_home_ads_summary_off">Home ADS are hidden</string>
|
||||||
|
@ -57,11 +57,12 @@
|
|||||||
<PreferenceScreen android:title="@string/revanced_xfenster_title" android:key="xfenster_screen" android:summary="@string/revanced_xfenster_screen_summary">
|
<PreferenceScreen android:title="@string/revanced_xfenster_title" android:key="xfenster_screen" android:summary="@string/revanced_xfenster_screen_summary">
|
||||||
<SwitchPreference android:title="@string/revanced_xfenster_brightness_title" android:key="revanced_enable_swipe_brightness" android:defaultValue="false" android:summaryOn="@string/revanced_xfenster_brightness_summary_on" android:summaryOff="@string/revanced_xfenster_brightness_summary_off" />
|
<SwitchPreference android:title="@string/revanced_xfenster_brightness_title" android:key="revanced_enable_swipe_brightness" android:defaultValue="false" android:summaryOn="@string/revanced_xfenster_brightness_summary_on" android:summaryOff="@string/revanced_xfenster_brightness_summary_off" />
|
||||||
<SwitchPreference android:title="@string/revanced_xfenster_volume_title" android:key="revanced_enable_swipe_volume" android:defaultValue="false" android:summaryOn="@string/revanced_xfenster_volume_summary_on" android:summaryOff="@string/revanced_xfenster_volume_summary_off" />
|
<SwitchPreference android:title="@string/revanced_xfenster_volume_title" android:key="revanced_enable_swipe_volume" android:defaultValue="false" android:summaryOn="@string/revanced_xfenster_volume_summary_on" android:summaryOff="@string/revanced_xfenster_volume_summary_off" />
|
||||||
<SwitchPreference android:title="@string/revanced_xfenster_tablet_title" android:key="revanced_swipe_tablet_mode" android:defaultValue="false" android:summaryOn="@string/revanced_xfenster_tablet_summary_on" android:summaryOff="@string/revanced_xfenster_tablet_summary_off" />
|
<SwitchPreference android:title="@string/revanced_swipe_pts_title" android:key="revanced_enable_press_to_swipe" android:defaultValue="false" android:summaryOn="@string/revanced_swipe_pts_summary_on" android:summaryOff="@string/revanced_swipe_pts_summary_off" />
|
||||||
<EditTextPreference android:numeric="integer" android:title="@string/revanced_swipe_threshold_title" android:key="revanced_swipe_threshold" android:summary="@string/revanced_swipe_threshold_summary" android:defaultValue="0" />
|
<SwitchPreference android:title="@string/revanced_swipe_pts_haptic_title" android:key="revanced_enable_swipe_haptic_feedback" android:defaultValue="false" android:summaryOn="@string/revanced_swipe_pts_haptic_summary_on" android:summaryOff="@string/revanced_swipe_pts_summary_off" />
|
||||||
<EditTextPreference android:numeric="integer" android:title="@string/revanced_swipe_padding_top_title" android:key="revanced_swipe_padding_top" android:summary="@string/revanced_swipe_padding_top_summary" android:defaultValue="20" />
|
<EditTextPreference android:numeric="integer" android:title="@string/revanced_swipe_overlay_timeout_title" android:key="revanced_swipe_overlay_timeout" android:summary="@string/revanced_swipe_overlay_timeout_summary" android:defaultValue="0" />
|
||||||
<SwitchPreference android:title="@string/revanced_xfenster_brightness_title" android:key="pref_xfenster_brightness" android:defaultValue="false" android:summaryOn="@string/revanced_xfenster_brightness_summary_on" android:summaryOff="@string/revanced_xfenster_brightness_summary_off" />
|
<EditTextPreference android:numeric="decimal" android:title="@string/revanced_swipe_overlay_text_size_title" android:key="revanced_swipe_overlay_text_size" android:summary="@string/revanced_swipe_overlay_text_size_summary" android:defaultValue="0" />
|
||||||
<SwitchPreference android:title="@string/revanced_xfenster_volume_title" android:key="pref_xfenster_volume" android:defaultValue="false" android:summaryOn="@string/revanced_xfenster_volume_summary_on" android:summaryOff="@string/revanced_xfenster_volume_summary_off" />
|
<EditTextPreference android:numeric="integer" android:title="@string/revanced_swipe_overlay_bg_alpha_title" android:key="revanced_swipe_overlay_background_alpha" android:summary="@string/revanced_swipe_overlay_bg_alpha_summary" android:defaultValue="0" />
|
||||||
|
<EditTextPreference android:numeric="decimal" android:title="@string/revanced_swipe_magnitude_threshold_title" android:key="revanced_swipe_magnitude_threshold" android:summary="@string/revanced_swipe_magnitude_threshold_summary" android:defaultValue="0" />
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
||||||
<PreferenceScreen android:title="@string/revanced_buffer_title" android:key="buffer_screen">
|
<PreferenceScreen android:title="@string/revanced_buffer_title" android:key="buffer_screen">
|
||||||
<EditTextPreference android:numeric="integer" android:title="@string/revanced_maximum_buffer_title" android:key="revanced_pref_max_buffer_ms" android:summary="@string/revanced_maximum_buffer_summary" android:defaultValue="120000" />
|
<EditTextPreference android:numeric="integer" android:title="@string/revanced_maximum_buffer_title" android:key="revanced_pref_max_buffer_ms" android:summary="@string/revanced_maximum_buffer_summary" android:defaultValue="120000" />
|
||||||
|
Loading…
Reference in New Issue
Block a user