Guide: Enabling the Swipe-Up Gesture for Opening the App Drawer in Launcher3QuickStep
Original guide and fix discovered by @מחנה_ידידים, who posted it and asked for volunteers to translate it into English. This is that translation.
This guide documents the exact steps taken to fix a common issue in builds based on Launcher3QuickStep (AOSP) on devices with non-standard screen aspect ratios (like the Qin F21 Pro), where swiping up on the home screen doesn’t open the app drawer.
Root Cause
In stock Launcher3QuickStep, the swipe-up gesture that opens the app drawer is handled by a class called PortraitStatesTouchController, which gets registered inside QuickstepLauncher.createTouchControllers().
The problem: that registration is conditional on a DeviceProfile.isVerticalBarLayout() check. On devices with unusual aspect ratios (small or near-square screens), this function incorrectly returns true, which sends the code down a different branch — one meant for devices with a vertical hotseat, like tablets or foldables — where PortraitStatesTouchController never gets registered at all. End result: there’s no gesture wired up to open the drawer.
How to spot this yourself in another APK
# After decompiling with apktool:
grep -n "createTouchControllers" smali_classes2/com/android/launcher3/uioverrides/QuickstepLauncher.smali
Look inside that method for a call to DeviceProfile;->isVerticalBarLayout()Z. If the branch that adds PortraitStatesTouchController depends on that check, you’re looking at the same bug.
The Fix
Rather than “fixing” isVerticalBarLayout() — which is risky since it affects a lot of other UI and layout logic — I wrote a new, standalone controller that detects the swipe-up gesture on the home screen on its own and opens the drawer, with no dependency on the broken branch. It’s registered unconditionally, so it keeps working even if isVerticalBarLayout() still misbehaves.
Tools needed
Step 1 — Open the APK for editing
- Open MT Manager, find your APK, and tap it (a regular tap, not a long-press) to bring up the “Open with” menu.
- Select View.
- You’ll get a tree view of the APK’s structure, including
classes.dex,classes2.dex, etc. Each one can be drilled down all the way to the Smali package level. - Navigate into
classes2.dex(where the launcher code itself usually lives) →com/android/launcher3/uioverrides/touchcontrollers/.
Step 2 — Create the new class
Inside the touchcontrollers/ folder, create a new Smali file (long-press any existing file and pick “Add”) named exactly:
SimpleAppDrawerSwipeController.smali
Paste in the following:
.class public Lcom/android/launcher3/uioverrides/touchcontrollers/SimpleAppDrawerSwipeController;
.super Ljava/lang/Object;
.source "SimpleAppDrawerSwipeController.java"
# interfaces
.implements Lcom/android/launcher3/util/TouchController;
# instance fields
.field private mCanIntercept:Z
.field private mDownX:F
.field private mDownY:F
.field private final mLauncher:Lcom/android/launcher3/Launcher;
.field private final mSwipeSlop:F
# direct methods
.method public constructor <init>(Lcom/android/launcher3/Launcher;)V
.locals 2
.param p1, "launcher" # Lcom/android/launcher3/Launcher;
.line 20
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
.line 21
iput-object p1, p0, Lcom/android/launcher3/uioverrides/touchcontrollers/SimpleAppDrawerSwipeController;->mLauncher:Lcom/android/launcher3/Launcher;
.line 22
invoke-static {p1}, Landroid/view/ViewConfiguration;->get(Landroid/content/Context;)Landroid/view/ViewConfiguration;
move-result-object v0
.line 23
invoke-virtual {v0}, Landroid/view/ViewConfiguration;->getScaledTouchSlop()I
move-result v0
mul-int/lit8 v0, v0, 0x4
int-to-float v0, v0
iput v0, p0, Lcom/android/launcher3/uioverrides/touchcontrollers/SimpleAppDrawerSwipeController;->mSwipeSlop:F
.line 24
return-void
.end method
# virtual methods
.method public onControllerInterceptTouchEvent(Landroid/view/MotionEvent;)Z
.locals 6
.param p1, "ev" # Landroid/view/MotionEvent;
.line 28
invoke-virtual {p1}, Landroid/view/MotionEvent;->getActionMasked()I
move-result v0
.line 29
const/4 v1, 0x0
if-nez v0, :cond_1
.line 30
iget-object v2, p0, Lcom/android/launcher3/uioverrides/touchcontrollers/SimpleAppDrawerSwipeController;->mLauncher:Lcom/android/launcher3/Launcher;
sget-object v3, Lcom/android/launcher3/LauncherState;->NORMAL:Lcom/android/launcher3/LauncherState;
invoke-virtual {v2, v3}, Lcom/android/launcher3/Launcher;->isInState(Lcom/android/launcher3/statemanager/BaseState;)Z
move-result v2
iput-boolean v2, p0, Lcom/android/launcher3/uioverrides/touchcontrollers/SimpleAppDrawerSwipeController;->mCanIntercept:Z
.line 31
invoke-virtual {p1}, Landroid/view/MotionEvent;->getX()F
move-result v2
iput v2, p0, Lcom/android/launcher3/uioverrides/touchcontrollers/SimpleAppDrawerSwipeController;->mDownX:F
.line 32
invoke-virtual {p1}, Landroid/view/MotionEvent;->getY()F
move-result v2
iput v2, p0, Lcom/android/launcher3/uioverrides/touchcontrollers/SimpleAppDrawerSwipeController;->mDownY:F
.line 33
return v1
.line 36
:cond_1
iget-boolean v2, p0, Lcom/android/launcher3/uioverrides/touchcontrollers/SimpleAppDrawerSwipeController;->mCanIntercept:Z
if-nez v2, :cond_2
.line 37
return v1
.line 40
:cond_2
const/4 v2, 0x2
if-eq v0, v2, :cond_3
.line 41
return v1
.line 44
:cond_3
invoke-virtual {p1}, Landroid/view/MotionEvent;->getPointerCount()I
move-result v2
const/4 v3, 0x1
if-eq v2, v3, :cond_4
.line 45
return v1
.line 48
:cond_4
iget v2, p0, Lcom/android/launcher3/uioverrides/touchcontrollers/SimpleAppDrawerSwipeController;->mDownY:F
invoke-virtual {p1}, Landroid/view/MotionEvent;->getY()F
move-result v4
sub-float/2addr v2, v4
.line 49
invoke-virtual {p1}, Landroid/view/MotionEvent;->getX()F
move-result v4
iget v5, p0, Lcom/android/launcher3/uioverrides/touchcontrollers/SimpleAppDrawerSwipeController;->mDownX:F
sub-float/2addr v4, v5
invoke-static {v4}, Ljava/lang/Math;->abs(F)F
move-result v4
.line 51
iget v5, p0, Lcom/android/launcher3/uioverrides/touchcontrollers/SimpleAppDrawerSwipeController;->mSwipeSlop:F
cmpl-float v5, v2, v5
if-lez v5, :cond_5
cmpl-float v5, v2, v4
if-lez v5, :cond_5
.line 52
iget-object v5, p0, Lcom/android/launcher3/uioverrides/touchcontrollers/SimpleAppDrawerSwipeController;->mLauncher:Lcom/android/launcher3/Launcher;
invoke-virtual {v5}, Lcom/android/launcher3/Launcher;->getStateManager()Lcom/android/launcher3/statemanager/StateManager;
move-result-object v5
sget-object v0, Lcom/android/launcher3/LauncherState;->ALL_APPS:Lcom/android/launcher3/LauncherState;
invoke-virtual {v5, v0}, Lcom/android/launcher3/statemanager/StateManager;->goToState(Lcom/android/launcher3/statemanager/BaseState;)V
.line 53
const/4 v0, 0x1
return v0
.line 56
:cond_5
return v1
.end method
.method public onControllerTouchEvent(Landroid/view/MotionEvent;)Z
.locals 1
.param p1, "ev" # Landroid/view/MotionEvent;
.line 61
const/4 v0, 0x1
return v0
.end method
How it works, in short
onControllerInterceptTouchEventtracks every touch event. OnACTION_DOWN, it checks that the home screen is currently in theNORMALstate (i.e., not already in the app drawer or in overview) and stores the initial touch coordinates.- On
ACTION_MOVE, if the upward movement exceeds a threshold (4× the system’s touch-slop value) and is greater than the horizontal movement — meaning it’s a deliberate vertical swipe, not a drag between home screens — it callsgoToState(ALL_APPS)and returnstrue, telling theDragLayerto intercept the touch from other components. onControllerTouchEventjust consumes the rest of the gesture so nothing weird happens on screen while the transition to the drawer plays out.
Step 3 — Register the controller
Go back one folder (uioverrides/) and open QuickstepLauncher.smali. Use the editor’s search to find:
.method public createTouchControllers()[Lcom/android/launcher3/util/TouchController;
Inside this method, right after the first ArrayList.add() call (the one that adds DragController, which you’ll find right after getDragController()), insert these three lines:
invoke-virtual {v2, v3}, Ljava/util/ArrayList;->add(Ljava/lang/Object;)Z
new-instance v3, Lcom/android/launcher3/uioverrides/touchcontrollers/SimpleAppDrawerSwipeController;
invoke-direct {v3, p0}, Lcom/android/launcher3/uioverrides/touchcontrollers/SimpleAppDrawerSwipeController;-><init>(Lcom/android/launcher3/Launcher;)V
invoke-virtual {v2, v3}, Ljava/util/ArrayList;->add(Ljava/lang/Object;)Z
Before you paste this:
v2is the register holding theArrayList<TouchController>, andv3needs to be free at that exact point. Check your own file to confirm — see wherev3was last used above this point and where it’s used again below. Ifv3is already occupied, or the.localscount at the top of the method isn’t high enough for whatever register you pick, MT Manager will throw a Smali compile error on save. If that happens, just pick a free register and bump up.localsas needed.
Step 4 — Save and compile
Exit the Smali editor (usually via “Save” or the back arrow) and MT Manager will automatically recompile your changes into the .dex file. Any syntax errors will show up right there. If everything checks out, your APK is updated and ready to go.
Good luck!