diff --git a/app/src/main/java/app/revanced/integrations/fenster/FensterEnablement.kt b/app/src/main/java/app/revanced/integrations/fenster/FensterEnablement.kt deleted file mode 100644 index c745df0b..00000000 --- a/app/src/main/java/app/revanced/integrations/fenster/FensterEnablement.kt +++ /dev/null @@ -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 - } -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/fenster/WatchWhilePlayerType.kt b/app/src/main/java/app/revanced/integrations/fenster/WatchWhilePlayerType.kt deleted file mode 100644 index 8fa2ae99..00000000 --- a/app/src/main/java/app/revanced/integrations/fenster/WatchWhilePlayerType.kt +++ /dev/null @@ -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 } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/fenster/controllers/FensterController.kt b/app/src/main/java/app/revanced/integrations/fenster/controllers/FensterController.kt deleted file mode 100644 index c20e8613..00000000 --- a/app/src/main/java/app/revanced/integrations/fenster/controllers/FensterController.kt +++ /dev/null @@ -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 - } - } -} diff --git a/app/src/main/java/app/revanced/integrations/fenster/controllers/FensterOverlayController.kt b/app/src/main/java/app/revanced/integrations/fenster/controllers/FensterOverlayController.kt deleted file mode 100644 index 2a2d5afc..00000000 --- a/app/src/main/java/app/revanced/integrations/fenster/controllers/FensterOverlayController.kt +++ /dev/null @@ -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) - } -} diff --git a/app/src/main/java/app/revanced/integrations/fenster/controllers/ScreenBrightnessController.kt b/app/src/main/java/app/revanced/integrations/fenster/controllers/ScreenBrightnessController.kt deleted file mode 100644 index dcfcefcb..00000000 --- a/app/src/main/java/app/revanced/integrations/fenster/controllers/ScreenBrightnessController.kt +++ /dev/null @@ -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 - } -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/fenster/util/SwipeControlZoneHelper.kt b/app/src/main/java/app/revanced/integrations/fenster/util/SwipeControlZoneHelper.kt deleted file mode 100644 index 82564913..00000000 --- a/app/src/main/java/app/revanced/integrations/fenster/util/SwipeControlZoneHelper.kt +++ /dev/null @@ -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 -} diff --git a/app/src/main/java/app/revanced/integrations/patches/FensterSwipePatch.java b/app/src/main/java/app/revanced/integrations/patches/FensterSwipePatch.java deleted file mode 100644 index 431496f0..00000000 --- a/app/src/main/java/app/revanced/integrations/patches/FensterSwipePatch.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/app/revanced/integrations/patches/PlayerTypeHookPatch.java b/app/src/main/java/app/revanced/integrations/patches/PlayerTypeHookPatch.java new file mode 100644 index 00000000..c91390e2 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/patches/PlayerTypeHookPatch.java @@ -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); + } + } +} diff --git a/app/src/main/java/app/revanced/integrations/patches/SwipeControlsPatch.java b/app/src/main/java/app/revanced/integrations/patches/SwipeControlsPatch.java new file mode 100644 index 00000000..8af3d818 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/patches/SwipeControlsPatch.java @@ -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); + } + } +} diff --git a/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java b/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java index f0a05f85..6895af02 100644 --- a/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java +++ b/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java @@ -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), diff --git a/app/src/main/java/app/revanced/integrations/swipecontrols/SwipeControlsConfigurationProvider.kt b/app/src/main/java/app/revanced/integrations/swipecontrols/SwipeControlsConfigurationProvider.kt new file mode 100644 index 00000000..0f666262 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/swipecontrols/SwipeControlsConfigurationProvider.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/fenster/controllers/AudioVolumeController.kt b/app/src/main/java/app/revanced/integrations/swipecontrols/controller/AudioVolumeController.kt similarity index 95% rename from app/src/main/java/app/revanced/integrations/fenster/controllers/AudioVolumeController.kt rename to app/src/main/java/app/revanced/integrations/swipecontrols/controller/AudioVolumeController.kt index b2362aa7..a06e5489 100644 --- a/app/src/main/java/app/revanced/integrations/fenster/controllers/AudioVolumeController.kt +++ b/app/src/main/java/app/revanced/integrations/swipecontrols/controller/AudioVolumeController.kt @@ -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 diff --git a/app/src/main/java/app/revanced/integrations/swipecontrols/controller/ScreenBrightnessController.kt b/app/src/main/java/app/revanced/integrations/swipecontrols/controller/ScreenBrightnessController.kt new file mode 100644 index 00000000..44769984 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/swipecontrols/controller/ScreenBrightnessController.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/swipecontrols/controller/gesture/NoPtSSwipeGestureController.kt b/app/src/main/java/app/revanced/integrations/swipecontrols/controller/gesture/NoPtSSwipeGestureController.kt new file mode 100644 index 00000000..3791defc --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/swipecontrols/controller/gesture/NoPtSSwipeGestureController.kt @@ -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) + } +} diff --git a/app/src/main/java/app/revanced/integrations/swipecontrols/controller/gesture/SwipeGestureController.kt b/app/src/main/java/app/revanced/integrations/swipecontrols/controller/gesture/SwipeGestureController.kt new file mode 100644 index 00000000..e3fac02d --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/swipecontrols/controller/gesture/SwipeGestureController.kt @@ -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 + } +} diff --git a/app/src/main/java/app/revanced/integrations/swipecontrols/misc/Point.kt b/app/src/main/java/app/revanced/integrations/swipecontrols/misc/Point.kt new file mode 100644 index 00000000..f0ed3378 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/swipecontrols/misc/Point.kt @@ -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()) diff --git a/app/src/main/java/app/revanced/integrations/swipecontrols/misc/Rectangle.kt b/app/src/main/java/app/revanced/integrations/swipecontrols/misc/Rectangle.kt new file mode 100644 index 00000000..b35cd63b --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/swipecontrols/misc/Rectangle.kt @@ -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 diff --git a/app/src/main/java/app/revanced/integrations/fenster/util/ScrollDistanceHelper.kt b/app/src/main/java/app/revanced/integrations/swipecontrols/misc/ScrollDistanceHelper.kt similarity index 96% rename from app/src/main/java/app/revanced/integrations/fenster/util/ScrollDistanceHelper.kt rename to app/src/main/java/app/revanced/integrations/swipecontrols/misc/ScrollDistanceHelper.kt index 579dacae..9948b427 100644 --- a/app/src/main/java/app/revanced/integrations/fenster/util/ScrollDistanceHelper.kt +++ b/app/src/main/java/app/revanced/integrations/swipecontrols/misc/ScrollDistanceHelper.kt @@ -1,4 +1,4 @@ -package app.revanced.integrations.fenster.util +package app.revanced.integrations.swipecontrols.misc import kotlin.math.abs import kotlin.math.sign diff --git a/app/src/main/java/app/revanced/integrations/swipecontrols/misc/SwipeControlsOverlay.kt b/app/src/main/java/app/revanced/integrations/swipecontrols/misc/SwipeControlsOverlay.kt new file mode 100644 index 00000000..46a3bc54 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/swipecontrols/misc/SwipeControlsOverlay.kt @@ -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() +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/fenster/util/FensterUtils.kt b/app/src/main/java/app/revanced/integrations/swipecontrols/misc/SwipeControlsUtils.kt similarity index 91% rename from app/src/main/java/app/revanced/integrations/fenster/util/FensterUtils.kt rename to app/src/main/java/app/revanced/integrations/swipecontrols/misc/SwipeControlsUtils.kt index 2181001f..92d6ed80 100644 --- a/app/src/main/java/app/revanced/integrations/fenster/util/FensterUtils.kt +++ b/app/src/main/java/app/revanced/integrations/swipecontrols/misc/SwipeControlsUtils.kt @@ -1,4 +1,4 @@ -package app.revanced.integrations.fenster.util +package app.revanced.integrations.swipecontrols.misc import android.content.Context import android.util.TypedValue diff --git a/app/src/main/java/app/revanced/integrations/swipecontrols/misc/SwipeZonesHelper.kt b/app/src/main/java/app/revanced/integrations/swipecontrols/misc/SwipeZonesHelper.kt new file mode 100644 index 00000000..e4ca1565 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/swipecontrols/misc/SwipeZonesHelper.kt @@ -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 + ) + } +} diff --git a/app/src/main/java/app/revanced/integrations/swipecontrols/views/SwipeControlsHostLayout.kt b/app/src/main/java/app/revanced/integrations/swipecontrols/views/SwipeControlsHostLayout.kt new file mode 100644 index 00000000..6f50aeee --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/swipecontrols/views/SwipeControlsHostLayout.kt @@ -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 + } + } +} diff --git a/app/src/main/java/app/revanced/integrations/swipecontrols/views/SwipeControlsOverlayLayout.kt b/app/src/main/java/app/revanced/integrations/swipecontrols/views/SwipeControlsOverlayLayout.kt new file mode 100644 index 00000000..f27d0366 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/swipecontrols/views/SwipeControlsOverlayLayout.kt @@ -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 + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/utils/Event.kt b/app/src/main/java/app/revanced/integrations/utils/Event.kt new file mode 100644 index 00000000..f2fa21c3 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/utils/Event.kt @@ -0,0 +1,22 @@ +package app.revanced.integrations.utils + +/** + * generic event provider class + */ +class Event { + 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) + } +} + diff --git a/app/src/main/java/app/revanced/integrations/utils/PlayerType.kt b/app/src/main/java/app/revanced/integrations/utils/PlayerType.kt new file mode 100644 index 00000000..c82e62fc --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/utils/PlayerType.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/utils/ReVancedUtils.java b/app/src/main/java/app/revanced/integrations/utils/ReVancedUtils.java index ae1ce798..ea238117 100644 --- a/app/src/main/java/app/revanced/integrations/utils/ReVancedUtils.java +++ b/app/src/main/java/app/revanced/integrations/utils/ReVancedUtils.java @@ -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; } diff --git a/app/src/main/res/drawable/ic_sc_brightness_auto.xml b/app/src/main/res/drawable/ic_sc_brightness_auto.xml new file mode 100644 index 00000000..469b3359 --- /dev/null +++ b/app/src/main/res/drawable/ic_sc_brightness_auto.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_sc_brightness_manual.xml b/app/src/main/res/drawable/ic_sc_brightness_manual.xml new file mode 100644 index 00000000..2f6c7072 --- /dev/null +++ b/app/src/main/res/drawable/ic_sc_brightness_manual.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_sc_volume_mute.xml b/app/src/main/res/drawable/ic_sc_volume_mute.xml new file mode 100644 index 00000000..73dc595f --- /dev/null +++ b/app/src/main/res/drawable/ic_sc_volume_mute.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_sc_volume_normal.xml b/app/src/main/res/drawable/ic_sc_volume_normal.xml new file mode 100644 index 00000000..30dff4be --- /dev/null +++ b/app/src/main/res/drawable/ic_sc_volume_normal.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 700dbc0f..e1c16377 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -93,6 +93,20 @@ Swipe controls for volume are disabled Swipe controls for volume are enabled Swipe controls for Volume + Press-to-Swipe + Swipe controls are always active + Swipe controls require a long-press before activating + Vibrate on Press-to-Swipe + You\'ll get haptic feedback when activating Press-to-Swipe + You won\'t get haptic feedback when activating Press-to-Swipe + Overlay Timeout + How long the overlay is shown after changes (ms) + Overlay Text Size + Text size on the overlay + Overlay Background Transparency + Transparency value of the overlay background (0–255) + Swipe Magnitude Threshold + Minimum magnitude before a swipe is detected Tap to open our website ReVanced website Home ADS are hidden diff --git a/app/src/main/res/xml/revanced_prefs.xml b/app/src/main/res/xml/revanced_prefs.xml index c0e40954..7a3a492c 100644 --- a/app/src/main/res/xml/revanced_prefs.xml +++ b/app/src/main/res/xml/revanced_prefs.xml @@ -57,11 +57,12 @@ - - - - - + + + + + +