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:
Chris 2022-07-11 14:29:39 +02:00 committed by GitHub
parent 376ffc0844
commit fcabebf3a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1084 additions and 652 deletions

View File

@ -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
}
}

View File

@ -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 }
}
}
}

View File

@ -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
}
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -66,6 +66,12 @@ public enum SettingsEnum {
//Swipe controls
ENABLE_SWIPE_BRIGHTNESS_BOOLEAN("revanced_enable_swipe_brightness", 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
MAX_BUFFER_INTEGER("revanced_pref_max_buffer_ms", 120000),

View File

@ -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
}

View File

@ -1,9 +1,9 @@
package app.revanced.integrations.fenster.controllers
package app.revanced.integrations.swipecontrols.controller
import android.content.Context
import android.media.AudioManager
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 kotlin.properties.Delegates

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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())

View File

@ -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

View File

@ -1,4 +1,4 @@
package app.revanced.integrations.fenster.util
package app.revanced.integrations.swipecontrols.misc
import kotlin.math.abs
import kotlin.math.sign

View File

@ -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()
}

View File

@ -1,4 +1,4 @@
package app.revanced.integrations.fenster.util
package app.revanced.integrations.swipecontrols.misc
import android.content.Context
import android.util.TypedValue

View File

@ -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
)
}
}

View File

@ -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
}
}
}

View File

@ -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
)
}
}
}

View 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)
}
}

View File

@ -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>()
}
}

View File

@ -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) {
env = type;
}

View 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>

View 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>

View 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>

View 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>

View File

@ -93,6 +93,20 @@
<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_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 (0255)</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_title">ReVanced website</string>
<string name="revanced_home_ads_summary_off">Home ADS are hidden</string>

View File

@ -57,11 +57,12 @@
<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_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" />
<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" />
<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" />
<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" />
<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" />
<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" />
<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_overlay_timeout_title" android:key="revanced_swipe_overlay_timeout" android:summary="@string/revanced_swipe_overlay_timeout_summary" android:defaultValue="0" />
<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" />
<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 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" />