SystemUI: Add double tap to sleep gesture

Author: Roman Birg <roman@cyngn.com>
Date:   Sun Nov 23 06:54:06 2014 -0800

    SystemUI: double tap to sleep improvements

    * Make it more reliable
    * Add it to keyguard
    * Add a content observer to not always query Settings.System on every
    touch event

    Change-Id: I292c4d9d9f810843590b7a9ec6e15b99ac44009d
    Signed-off-by: Roman Birg <roman@cyngn.com>

Author: Adnan Begovic <adnan@cyngn.com>
Date:   Wed Nov 11 12:05:59 2015 -0800

    fw: Move DOUBLE_TAP_SLEEP_GESTURE to CMSettings.

    Change-Id: I8274b7c241cef6835a1114a702e68c95b6d2e036

Author: Zhao Wei Liew <zhaoweiliew@gmail.com>
Date:   Fri Oct 7 08:56:25 2016 +0800

    SystemUI: Use Tuner API for CM settings

    Get rid of all the excess code by implementing TunerService.Tunable
    and observing any changes made to the settings through it.

    Remove UserContentObserver as the Tuner API handles user switches.

    Also remove some unused imports that were introduced in earlier
    CM commits, but were never removed since.

    Change-Id: Iecafafabdaec82b3b3c72293bea865de48f0e90a

Author: Altaf-Mahdi <altaf.mahdi@gmail.com>
Date:   Wed Nov 11 16:07:49 2015 -0500

    Double tap to sleep anywhere on the lock screen [1/3]

    Change-Id: I7dd46f3fafeb2e629974c0f32083d4d9774fb1de
    [neo: Using Tuner API.]
    Signed-off-by: Pranav Vashi <neobuddy89@gmail.com>

Author: dianlujitao <dianlujitao@lineageos.org>
Date:   Thu Feb 27 12:57:07 2020 +0800

    SystemUI: Don't sleep on double tap below status bar

    Change-Id: Ic64c29eae63e96f34dc37cda355401b7e2cd2d39

Author: Arian <arian.kulmer@web.de>
Date:   2020-12-27 14:35:33 +0100

    NotificationPanelViewController: Fix DT2S gesture handling

    Change-Id: I9f2d3c63d397c998bea5137f747a9ad95ae2c746

Author:     maxwen <max.weninger@gmail.com>
AuthorDate: Mon Nov 18 00:29:15 2019 +0100

    SystemUI: use DOUBLE_TAP_TO_WAKE setting also for wake from aod

    Co-authored-by: Michael W <baddaemon87@gmail.com>
    Change-Id: Ib51fdeaaa9a1b18b79f4f311c65565352d909a72

Author:     Dhina17 <dhinalogu@gmail.com>
AuthorDate: Tue Sep 12 11:08:28 2023 +0530

    SystemUI: Move DT2S from PulsingGestureListener

    PulsingGestureListener should handle the touch events in the
    dozing state (AOD/ambient).
    Our DT2S should work in non-dozing mode so move out from here.

    Test:
    Double tap to sleep on QQS status bar works fine as before.

    Change-Id: Ia79df2eb73e54d8b0dcb1d10b18539f4e88a0a18

Author:     Dhina17 <dhinalogu@gmail.com>
AuthorDate: Mon Sep 11 16:24:24 2023 +0530

    SystemUI: Pass touch events to pulsing gesture listener only if dozing

    PulsingGestureListener should handle touch events only in the
    dozing state (AOD/ambient).

    Otherwise lockscreen DT2S and AOD DT2W will be in conflict.

    Explanation:
    Enable AOD, DT2W and DT2S in the display settings.

    In the scenerio, touch events are passed to
    first NotificationPanelView then NotificationShadeWindowView.

    So first NotificationPanelView(Controller) detects the double tap
    and put the screen to sleep (hence dozing = true).
    Then as the NotificationShadeWindowView(Controller) detects the double tap at dozing,
    it wakes up from the AOD immediately [from PulsingGestureListener].

    so it won't let the screen to sleep on double tap the lockscreen
    when AOD and DT2W are available.

    PulsingGestureListener is supposed to receive touch events only in
    the dozing state, so it won't cause this conflict.

    Test:
    - Enable AOD, DT2W and DT2S in the display settings
    - Double tap in the lockscreen, it will go to AOD screen.

    Change-Id: I6ede700e690ad4c37fea64419cfe8a53d64a9e9a

Author: Bruno Martins <bgcngm@gmail.com>
Date:   Fri Oct 13 14:04:00 2023 +0100

    SystemUI: Respect status bar DT2S gesture defaults

    Feature can be disabled by default, but if additionally the preference
    is hidden in Settings, it doesn't allow TunerService to set the proper
    default.

    Change-Id: I12b37335fba12cf4d5564f1aa19c5e4cf19f8775

[Pig]: Forward port to R
[bgcngm]: Forward port to S

Co-authored-by: Michael W <baddaemon87@gmail.com>
Change-Id: I7489204e348906dcf6e34fa04f2121974c22ddb9
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index cf7c3e0..4532385 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -56,6 +56,7 @@
 import android.annotation.Nullable;
 import android.app.StatusBarManager;
 import android.content.ContentResolver;
+import android.content.Context;
 import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.graphics.Color;
@@ -64,12 +65,14 @@
 import android.graphics.Region;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.PowerManager;
 import android.os.Trace;
 import android.os.UserManager;
 import android.provider.Settings;
 import android.util.IndentingPrintWriter;
 import android.util.Log;
 import android.util.MathUtils;
+import android.view.GestureDetector;
 import android.view.HapticFeedbackConstants;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
@@ -305,6 +308,7 @@
             new ShadeHeadsUpChangedListener();
     private final ConfigurationListener mConfigurationListener = new ConfigurationListener();
     private final SettingsChangeObserver mSettingsChangeObserver;
+    private final ContentObserver mDoubleTapToSleepObserver;
     private final StatusBarStateListener mStatusBarStateListener = new StatusBarStateListener();
     private final NotificationPanelView mView;
     private final VibratorHelper mVibratorHelper;
@@ -493,6 +497,9 @@
     private final NotificationShadeDepthController mDepthController;
     private final NavigationBarController mNavigationBarController;
     private final int mDisplayId;
+    private final PowerManager mPowerManager;
+    private boolean mDoubleTapToSleepEnabled;
+    private GestureDetector mDoubleTapGesture;
 
     private final KeyguardIndicationController mKeyguardIndicationController;
     private int mHeadsUpInset;
@@ -771,7 +778,9 @@
             SplitShadeStateController splitShadeStateController,
             PowerInteractor powerInteractor,
             KeyguardClockPositionAlgorithm keyguardClockPositionAlgorithm,
-            NaturalScrollingSettingObserver naturalScrollingSettingObserver) {
+            NaturalScrollingSettingObserver naturalScrollingSettingObserver,
+            PowerManager powerManager,
+            Context context) {
         keyguardStateController.addCallback(new KeyguardStateController.Callback() {
             @Override
             public void onKeyguardFadingAwayChanged() {
@@ -917,6 +926,25 @@
         });
         mBottomAreaShadeAlphaAnimator.setDuration(160);
         mBottomAreaShadeAlphaAnimator.setInterpolator(Interpolators.ALPHA_OUT);
+        mPowerManager = powerManager;
+        mDoubleTapGesture = new GestureDetector(context,
+                new GestureDetector.SimpleOnGestureListener() {
+            @Override
+            public boolean onDoubleTap(MotionEvent e) {
+                if (mPowerManager != null) {
+                    mPowerManager.goToSleep(e.getEventTime());
+                }
+                return true;
+            }
+        });
+        mDoubleTapToSleepObserver = new ContentObserver(handler) {
+            @Override
+            public void onChange(boolean selfChange) {
+                mDoubleTapToSleepEnabled = Settings.System.getInt(mContentResolver,
+                        Settings.System.DOUBLE_TAP_SLEEP_GESTURE,
+                        0) != 0;
+            }
+        };
         mConversationNotificationManager = conversationNotificationManager;
         mAuthController = authController;
         mLockIconViewController = lockIconViewController;
@@ -4595,6 +4623,10 @@
                 mStatusBarStateListener.onStateChanged(mStatusBarStateController.getState());
             }
             mConfigurationController.addCallback(mConfigurationListener);
+            mContentResolver.registerContentObserver(Settings.System.getUriFor(
+                    Settings.System.DOUBLE_TAP_SLEEP_GESTURE), false,
+                    mDoubleTapToSleepObserver);
+            mDoubleTapToSleepObserver.onChange(true);
             // Theme might have changed between inflating this view and attaching it to the
             // window, so
             // force a call to onThemeChanged
@@ -4606,6 +4638,7 @@
 
         @Override
         public void onViewDetachedFromWindow(View v) {
+            mContentResolver.unregisterContentObserver(mDoubleTapToSleepObserver);
             mContentResolver.unregisterContentObserver(mSettingsChangeObserver);
             mFragmentService.getFragmentHostManager(mView)
                     .removeTagListener(QS.TAG, mQsController.getQsFragmentListener());
@@ -4996,6 +5029,10 @@
                 return false;
             }
 
+            if (mDoubleTapToSleepEnabled && !mPulsing && !mDozing) {
+                mDoubleTapGesture.onTouchEvent(event);
+            }
+
             // Make sure the next touch won't the blocked after the current ends.
             if (event.getAction() == MotionEvent.ACTION_UP
                     || event.getAction() == MotionEvent.ACTION_CANCEL) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index e577178..d38ba7a 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -150,6 +150,9 @@
             };
     private final SystemClock mClock;
 
+    private GestureDetector mQQSGestureHandler;
+    private final QQSGestureListener mQQSGestureListener;
+
     @ExperimentalCoroutinesApi
     @Inject
     public NotificationShadeWindowViewController(
@@ -188,7 +191,8 @@
             AlternateBouncerInteractor alternateBouncerInteractor,
             Lazy<JavaAdapter> javaAdapter,
             Lazy<AlternateBouncerDependencies> alternateBouncerDependencies,
-            BouncerViewBinder bouncerViewBinder) {
+            BouncerViewBinder bouncerViewBinder,
+            QQSGestureListener qqsGestureListener) {
         mLockscreenShadeTransitionController = transitionController;
         mFalsingCollector = falsingCollector;
         mStatusBarStateController = statusBarStateController;
@@ -218,6 +222,7 @@
         mPrimaryBouncerInteractor = primaryBouncerInteractor;
         mAlternateBouncerInteractor = alternateBouncerInteractor;
         mQuickSettingsController = quickSettingsController;
+        mQQSGestureListener = qqsGestureListener;
 
         // This view is not part of the newly inflated expanded status bar.
         mBrightnessMirror = mView.findViewById(R.id.brightness_mirror_container);
@@ -286,6 +291,8 @@
         mStackScrollLayout = mView.findViewById(R.id.notification_stack_scroller);
         mPulsingWakeupGestureHandler = new GestureDetector(mView.getContext(),
                 mPulsingGestureListener);
+        mQQSGestureHandler = new GestureDetector(mView.getContext(),
+                mQQSGestureListener);
         if (mFeatureFlagsClassic.isEnabled(LOCKSCREEN_WALLPAPER_DREAM_ENABLED)) {
             mDreamingWakeupGestureHandler = new GestureDetector(mView.getContext(),
                     mLockscreenHostedDreamGestureListener);
@@ -355,7 +362,12 @@
                 }
 
                 mFalsingCollector.onTouchEvent(ev);
-                mPulsingWakeupGestureHandler.onTouchEvent(ev);
+                mQQSGestureHandler.onTouchEvent(ev);
+                // Pass touch events to the pulsing gesture listener only if it's dozing,
+                // otherwise lockscreen DT2S and AOD DT2W will conflict.
+                if (mStatusBarStateController.isDozing()) {
+                    mPulsingWakeupGestureHandler.onTouchEvent(ev);
+                }
 
                 if (mGlanceableHubContainerController.onTouchEvent(ev)) {
                     return logDownDispatch(ev, "dispatched to glanceable hub container", true);
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QQSGestureListener.kt b/packages/SystemUI/src/com/android/systemui/shade/QQSGestureListener.kt
new file mode 100644
index 0000000..15db267
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/QQSGestureListener.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2023 The LineageOS Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade
+
+import android.content.Context
+import android.database.ContentObserver
+import android.os.PowerManager
+import android.provider.Settings
+import android.view.GestureDetector
+import android.view.MotionEvent
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import javax.inject.Inject
+
+@SysUISingleton
+class QQSGestureListener @Inject constructor(
+        private val context: Context,
+        private val falsingManager: FalsingManager,
+        private val powerManager: PowerManager,
+        private val statusBarStateController: StatusBarStateController,
+) : GestureDetector.SimpleOnGestureListener() {
+
+    private var doubleTapToSleepEnabled = false
+    private val quickQsOffsetHeight: Int
+
+    init {
+        val contentObserver = object : ContentObserver(null) {
+            override fun onChange(selfChange: Boolean) {
+                doubleTapToSleepEnabled = Settings.System.getInt(
+                        context.contentResolver, Settings.System.DOUBLE_TAP_SLEEP_GESTURE,
+                        0) != 0
+            }
+        }
+        context.contentResolver.registerContentObserver(
+                Settings.System.getUriFor(Settings.System.DOUBLE_TAP_SLEEP_GESTURE),
+                false, contentObserver)
+        contentObserver.onChange(true)
+
+        quickQsOffsetHeight = context.resources.getDimensionPixelSize(
+                com.android.internal.R.dimen.quick_qs_offset_height)
+    }
+
+    override fun onDoubleTapEvent(e: MotionEvent): Boolean {
+        // Go to sleep on double tap the QQS status bar
+        if (e.actionMasked == MotionEvent.ACTION_UP &&
+                !statusBarStateController.isDozing &&
+                doubleTapToSleepEnabled &&
+                e.getY() < quickQsOffsetHeight &&
+                !falsingManager.isFalseDoubleTap
+        ) {
+            powerManager.goToSleep(e.getEventTime())
+            return true
+        }
+        return false
+    }
+
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index f8771b2..b4e997d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -757,7 +757,8 @@
                 new ResourcesSplitShadeStateController(),
                 mPowerInteractor,
                 mKeyguardClockPositionAlgorithm,
-                mNaturalScrollingSettingObserver);
+                mNaturalScrollingSettingObserver,
+                mContext);
         mNotificationPanelViewController.initDependencies(
                 mCentralSurfaces,
                 null,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
index 960fd59..4bcccb8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -137,6 +137,7 @@
     private val notificationLaunchAnimationRepository = NotificationLaunchAnimationRepository()
     private val notificationLaunchAnimationInteractor =
         NotificationLaunchAnimationInteractor(notificationLaunchAnimationRepository)
+    @Mock private lateinit var qqsGestureListener: QQSGestureListener
 
     private lateinit var fakeClock: FakeSystemClock
     private lateinit var interactionEventHandlerCaptor: ArgumentCaptor<InteractionEventHandler>
@@ -208,7 +209,8 @@
                 alternateBouncerInteractor,
                 { mock(JavaAdapter::class.java) },
                 { mock(AlternateBouncerDependencies::class.java) },
-                mock(BouncerViewBinder::class.java)
+                mock(BouncerViewBinder::class.java),
+                qqsGestureListener
             )
         underTest.setupExpandedStatusBar()
         underTest.setDragDownHelper(dragDownHelper)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
index 59fe813..a503f6e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
@@ -123,6 +123,7 @@
     @Mock private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
     @Mock lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor
     @Mock lateinit var alternateBouncerInteractor: AlternateBouncerInteractor
+    @Mock private lateinit var qqsGestureListener: QQSGestureListener
     @Captor
     private lateinit var interactionEventHandlerCaptor: ArgumentCaptor<InteractionEventHandler>
 
@@ -197,7 +198,8 @@
                 alternateBouncerInteractor,
                 { Mockito.mock(JavaAdapter::class.java) },
                 { Mockito.mock(AlternateBouncerDependencies::class.java) },
-                mock()
+                mock(),
+                qqsGestureListener
             )
 
         controller.setupExpandedStatusBar()