Guide: Enabling the Swipe-Up Gesture for Opening the App Drawer in Launcher3QuickStep

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

  1. Open MT Manager, find your APK, and tap it (a regular tap, not a long-press) to bring up the “Open with” menu.
  2. Select View.
  3. 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.
  4. 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

  • onControllerInterceptTouchEvent tracks every touch event. On ACTION_DOWN, it checks that the home screen is currently in the NORMAL state (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 calls goToState(ALL_APPS) and returns true, telling the DragLayer to intercept the touch from other components.
  • onControllerTouchEvent just 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: v2 is the register holding the ArrayList<TouchController>, and v3 needs to be free at that exact point. Check your own file to confirm — see where v3 was last used above this point and where it’s used again below. If v3 is already occupied, or the .locals count 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 .locals as 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!

7 Likes

Anyone have the updated apk itself?

once i update it does it now have a home screen with certain apps and if i want to access all apps i have to swipe up like a regular launcher or is it still have tons of apps also on the main screen so i have the same thing twice?