Files
TMCandClaude Fable 5.2 8aba905d9c Thumb-friendly drawer: compact list, Niagara-style alphabet, dark backdrop; QR code on About
Requests from the owner on 12 November 2026, after using 0.3.1 on the Pixel 9a, with a
photo of Niagara Launcher's alphabet as the reference.

Drawer list
- Rows are 50dp with 34dp icons and 15sp names (were 64dp, 46dp, 17sp); letter headers are
  34dp. A row grows with a large system font so a notification preview still fits.

Alphabet
- Letters sit 20dp apart at most (was 30dp). The strip keeps a stretch of edge as tall as
  26 letters, low on the edge with its end 40dp above the navigation bar, and centres the
  letters that exist in it. It used to spread over the whole edge, which put "A" out of a
  thumb's reach.
- The wave follows Niagara's: letters keep their size, the wave is wide (104dp) so about
  five letters on each side of the finger curve around the thumb, and the selected letter
  is shown large in a light bubble just beyond the wave's peak. This replaces the enlarged
  letter on a dark spot from 0.3.1, which collided with its neighbours at this spacing.
- The list dims a little during any scrub, more where the wave lies over it.

Backdrop
- The drawer's scrim is black at 78% in every theme (AllAppsState). Lawnchair's grey at 40%
  left a light grey behind white names over a light wallpaper. LawnchairScrimView judges the
  status bar icons by the scrim's own colour again, since that colour is now always dark.

Scrub from the home screen (PRD AD-10)
- On by default, the owner's decision; this closes the PRD's open question.
- It opens the drawer in 120 ms instead of the state manager's 600 ms, like swipe up and hold.
- A touch that lifts within 200 ms was a flick and opens the plain list.

Fix: undoing a scrub that was not meant
- 0.3.1 put the list back to "where it was", read from the list's rows. A closed drawer has
  been told to return to the top but lays its rows out only when it opens, so the rows
  still reported the place the drawer was last left at, and a quick swipe reopened it
  there. The list now goes where a plainly opened drawer stands.

About
- A QR code for the release page, generated on the phone with ZXing core (new dependency),
  black on white in every theme, with the address under it.

Checked on the Android 17 emulator with timed touches, a thumb-sized shape over the
screenshots, a 1.6 font scale, and by decoding the QR code from a screenshot.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 09:00:22 +01:00

575 lines
20 KiB
Groovy

import app.cash.licensee.SpdxId
import com.android.build.gradle.api.AndroidBasePlugin
import org.jetbrains.kotlin.gradle.dsl.JvmDefaultMode
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.android.library) apply false
alias(libs.plugins.android.test) apply false
alias(libs.plugins.androidx.baselineprofile)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.google.ksp)
alias(libs.plugins.google.protobuf)
alias(libs.plugins.cash.licensee)
alias(libs.plugins.rikka.refine)
alias(libs.plugins.diffplug.spotless)
}
allprojects {
plugins.withType(AndroidBasePlugin).configureEach {
android {
buildToolsVersion "37.0.0"
compileSdk {
version = release(37) {
minorApiLevel = 2
}
}
defaultConfig {
minSdk = 26
targetSdk = 37
vectorDrawables.useSupportLibrary = true
}
lint {
abortOnError true
checkReleaseBuilds false
}
compileOptions {
sourceCompatibility = libs.versions.jdkRelease.get()
targetCompatibility = libs.versions.jdkRelease.get()
}
}
dependencies {
implementation libs.androidx.core.ktx
implementation libs.androidx.core.animation
}
}
plugins.withId('com.google.protobuf') {
protobuf {
// Configure the protoc executable
protoc {
artifact = libs.protobuf.protoc.get().toString()
}
generateProtoTasks {
all().configureEach { task ->
task.builtins {
java {
option "lite"
}
}
}
}
}
dependencies {
implementation libs.protobuf.javalite
}
}
plugins.withType(JavaBasePlugin).configureEach {
java {
sourceCompatibility = libs.versions.jdkRelease.get()
targetCompatibility = libs.versions.jdkRelease.get()
}
}
tasks.withType(KotlinCompile).configureEach {
compilerOptions {
jvmDefault = JvmDefaultMode.ENABLE
jvmTarget = JvmTarget.fromTarget(libs.versions.jdkRelease.get())
}
}
ext {
FRAMEWORK_PREBUILTS_DIR = "$rootDir/prebuilts/libs"
addFrameworkJar = { String name ->
def frameworkJar = new File(FRAMEWORK_PREBUILTS_DIR, name)
if (!frameworkJar.exists()) {
throw new IllegalArgumentException("Framework jar path ${frameworkJar.path} doesn't exist")
}
gradle.projectsEvaluated {
tasks.withType(JavaCompile).configureEach {
// Make sure the frameworkJar is always first in the classpath.
classpath = files(frameworkJar, classpath)
}
tasks.withType(KotlinCompile).configureEach {
// Make sure the frameworkJar is always first in the classpath.
libraries.setFrom files(frameworkJar, libraries)
}
}
dependencies {
compileOnly files(frameworkJar)
}
}
compileOnlyCommonJars = {
dependencies {
compileOnly fileTree(dir: FRAMEWORK_PREBUILTS_DIR, include: 'SystemUI-core-16.jar')
compileOnly fileTree(dir: FRAMEWORK_PREBUILTS_DIR, include: 'SystemUI-statsd-16.jar')
compileOnly fileTree(dir: FRAMEWORK_PREBUILTS_DIR, include: 'WindowManager-Shell-16.jar')
compileOnly projects.compatLib
compileOnly projects.compatLib.compatLibVQ
compileOnly projects.compatLib.compatLibVR
compileOnly projects.compatLib.compatLibVS
compileOnly projects.compatLib.compatLibVT
compileOnly projects.compatLib.compatLibVU
compileOnly projects.compatLib.compatLibVV
compileOnly projects.compatLib.compatLibVBaklava
}
}
}
}
final def buildCommit = providers.exec {
commandLine('git', 'rev-parse', '--short=7', 'HEAD')
}.standardOutput.asText.get().trim()
final def ciBuild = System.getenv("CI") == "true"
final def ciRef = System.getenv("GITHUB_REF") ?: ""
final def ciRunNumber = System.getenv("GITHUB_RUN_NUMBER") ?: ""
final def isReleaseBuild = ciBuild
&& (ciRef.endsWith("-alpha")
|| ciRef.endsWith("-beta")
|| ciRef.endsWith("-rc")
|| ciRef.endsWith("-stable"))
final def devReleaseName = ciBuild ? "Dev.(#${ciRunNumber})" : "Dev.(${buildCommit})"
final def version = "16"
final def releaseName = "Beta 1"
final def versionDisplayName = "${version}.${isReleaseBuild ? releaseName : devReleaseName}"
final def majorVersion = versionDisplayName.split("\\.")[0]
// Flexera Launcher: the app has its own version, kept in flexera/version.properties. The updater
// (app.flexera.update.FlexeraUpdater) compares these numbers with the latest published release.
final def flexeraVersionProperties = new Properties()
file("flexera/version.properties").withInputStream { flexeraVersionProperties.load(it) }
final def flexeraVersionName = flexeraVersionProperties.getProperty("versionName").trim()
final def flexeraVersionParts = flexeraVersionName.tokenize('.').collect { it as int }
if (flexeraVersionParts.size() != 3 || flexeraVersionParts[1] > 99 || flexeraVersionParts[2] > 99) {
throw new GradleException("flexera/version.properties: versionName must be major.minor.patch with minor and patch below 100, got '${flexeraVersionName}'")
}
final def flexeraVersionCode = flexeraVersionParts[0] * 10000 + flexeraVersionParts[1] * 100 + flexeraVersionParts[2]
final def quickstepMinSdk = "35"
final def quickstepMaxSdk = "36"
android {
namespace "com.android.launcher3"
defaultConfig {
/*
* Lawnchair Launcher 16.0 Beta 1.0
* see https://docs.lawnchair.app/developers/standards/development-workflow#versioning-scheme
*/
// Flexera Launcher: its own version instead of Lawnchair's 16_00_02_01_00 / "16.Dev.(hash)".
// MAJOR_VERSION is what the settings home shows after the app name, so it gets the full version.
versionCode flexeraVersionCode
versionName flexeraVersionName
buildConfigField "String", "VERSION_DISPLAY_NAME", "\"${flexeraVersionName} (${buildCommit})\""
buildConfigField "String", "MAJOR_VERSION", "\"${flexeraVersionName}\""
buildConfigField "String", "COMMIT_HASH", "\"${buildCommit}\""
buildConfigField "boolean", "ENABLE_AUTO_INSTALLS_LAYOUT", "false"
manifestPlaceholders.quickstepMinSdk = quickstepMinSdk
manifestPlaceholders.quickstepMaxSdk = quickstepMaxSdk
buildConfigField "int", "QUICKSTEP_MIN_SDK", quickstepMinSdk
buildConfigField "int", "QUICKSTEP_MAX_SDK", quickstepMaxSdk
}
androidResources {
generateLocaleConfig true
}
buildFeatures {
aidl true
buildConfig true
resValues true
}
packagingOptions.resources.excludes += [
"**/*.proto",
"**/*.bin",
"**/*.java",
"**/*.properties",
"**/*.version",
"**/*.*_module",
"com/**",
"google/**",
"kotlin/**",
"kotlinx/**",
"okhttp3/**",
"META-INF/services/**",
"META-INF/com/**",
"META-INF/licenses/**",
"META-INF/AL2.0",
"META-INF/LGPL2.1",
]
// Load all proguard configs from AOSP
def proguardFilesFromAosp = allprojects.collect { proj ->
[
'proguard.flags',
'proguard_flags.flags',
'proguard_plugins.flags'
].collect { proj.file(it) }
}.flatten().findAll { it.exists() }.toArray()
def releaseSigning
try {
def keystoreProperties = new Properties()
keystoreProperties.load(rootProject.file("keystore.properties").newInputStream())
releaseSigning = signingConfigs.create("release") {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile rootProject.file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
}
} catch (Exception ignored) {
releaseSigning = signingConfigs.debug
}
buildTypes {
all {
signingConfig releaseSigning
pseudoLocalesEnabled true
}
debug {
applicationIdSuffix ".debug"
// Flexera Launcher: debug builds always use the debug key, so the release key in
// keystore.properties signs releases only.
signingConfig signingConfigs.debug
resValue("string", "derived_app_name", "Flexera Launcher (Debug)")
manifestPlaceholders.quickstepMinSdk = "0"
manifestPlaceholders.quickstepMaxSdk = "100000"
buildConfigField "int", "QUICKSTEP_MIN_SDK", "0"
buildConfigField "int", "QUICKSTEP_MAX_SDK", "100000"
}
release {
resValue("string", "derived_app_name", "Flexera Launcher")
minifyEnabled true
shrinkResources true
pseudoLocalesEnabled false
proguardFiles proguardFilesFromAosp + "proguard.pro"
}
}
compileOptions {
coreLibraryDesugaringEnabled true
}
dependenciesInfo {
includeInApk = false
includeInBundle = false
}
// See: https://developer.android.com/studio/build/build-variants#flavor-dimensions
flavorDimensions += ["app", "recents", "channel"]
productFlavors {
lawn {
dimension "app"
}
withQuickstep {
dimension "recents"
minSdk = 26
}
github {
applicationId 'com.sema.flexeralauncher'
dimension "channel"
}
nightly {
applicationId 'com.sema.flexeralauncher.nightly'
dimension "channel"
}
play {
applicationId "com.sema.flexeralauncher.play"
dimension "channel"
isDefault true
}
configureEach {
resValue("string", "launcher_component", "${applicationId}/app.lawnchair.LawnchairLauncher")
}
}
sourceSets {
main {
res.srcDirs = ['res']
// Lawnchair-TODO: Don't include Compose Disabled, but there's has to be another way of making this NOT long
java.srcDirs = ['src', 'src_plugins', 'compose/facade/core', 'compose/facade/enabled', 'compose/features']
kotlin.directories.addAll('src', 'src_plugins', 'compose/facade/core', 'compose/facade/enabled', 'compose/features')
manifest.srcFile 'AndroidManifest-common.xml'
proto {
srcDirs = ['protos/', 'quickstep/protos_overrides/']
}
}
lawn {
java.srcDirs = ['src_flags', 'src_shortcuts_overrides', 'lawnchair/src', 'tests/shared']
kotlin.directories.addAll('src_flags', 'src_shortcuts_overrides', 'lawnchair/src', 'tests/shared')
aidl.srcDirs = ['lawnchair/aidl']
res.srcDirs = ['lawnchair/res', 'platform_frameworks_libs_systemui/animationlib/res']
manifest.srcFile "lawnchair/AndroidManifest.xml"
assets {
srcDirs 'lawnchair/assets'
}
proto {
srcDirs = ['lawnchair/protos/']
}
}
github {
manifest.srcFile "github/AndroidManifest.xml"
}
nightly {
manifest.srcFile "nightly/AndroidManifest.xml"
res.srcDirs = ["nightly/res"]
}
lawnWithQuickstepGithub {
manifest.srcFile "quickstep/AndroidManifest-launcher.xml"
// Flexera Launcher: branding and default overrides. This variant-level source set
// outranks 'lawn' and 'main', so nothing upstream has to be edited.
res.srcDirs = ['flexera/res']
java.srcDirs = ['flexera/src']
kotlin.directories.add('flexera/src')
}
// Flexera Launcher: manifest overrides (permissions). Build-type manifests outrank
// flavor and library manifests, so what is declared there always wins.
debug {
manifest.srcFile "flexera/AndroidManifest.xml"
}
release {
manifest.srcFile "flexera/AndroidManifest.xml"
}
lawnWithQuickstepNightly {
manifest.srcFile "quickstep/AndroidManifest-launcher.xml"
}
lawnWithQuickstepPlay {
manifest.srcFile "quickstep/AndroidManifest-launcher.xml"
}
withQuickstep {
res.srcDirs = ['quickstep/res', 'quickstep/recents_ui_overrides/res']
java.srcDirs = ['quickstep/src', 'quickstep/dagger', 'quickstep/recents_ui_overrides/src', 'quickstep/src_protolog']
kotlin.directories.addAll('quickstep/src', 'quickstep/dagger', 'quickstep/recents_ui_overrides/src', 'quickstep/src_protolog')
manifest.srcFile "quickstep/AndroidManifest.xml"
}
}
}
androidComponents {
onVariants(selector().all()) { variant ->
def channel = variant.productFlavors.last().second
variant.outputs.forEach { output ->
output.outputFileName.set("FlexeraLauncher.${output.versionName.get()}.$channel.${variant.buildType}.apk")
}
// Lawnchair-TODO: A better way to sync proto file to the IDE preventing missing or outdated result. Please remove if this is the best solution we have right now
def capitalizedVariantName = variant.name.substring(0, 1).toUpperCase(java.util.Locale.ROOT) + variant.name.substring(1)
def generatedProtoJavaDir = layout.buildDirectory
.dir("generated/java/generate${capitalizedVariantName}Proto/java")
.get()
.asFile
variant.sources.java.addStaticSourceDirectory(generatedProtoJavaDir.absolutePath)
}
}
baselineProfile {
saveInSrc = true
mergeIntoMain = true
automaticGenerationDuringBuild = false
}
composeCompiler {
stabilityConfigurationFiles.addAll(
layout.projectDirectory.file("compose_compiler_config.conf"),
)
reportsDestination = layout.buildDirectory.dir("compose_build_reports")
}
addFrameworkJar('framework-16.jar')
dependencies {
implementation projects.iconloaderlib
implementation projects.searchuilib
implementation projects.animationlib
implementation projects.msdllib
implementation projects.contextualeducationlib
implementation projects.viewcapturelib
implementation projects.displaylib
implementation projects.mechanics
// Recents lib dependency
withQuickstepCompileOnly projects.hiddenApi
withQuickstepImplementation projects.shared
withQuickstepImplementation projects.animation
withQuickstepImplementation projects.unfold
withQuickstepImplementation projects.log
withQuickstepCompileOnly projects.plugin
withQuickstepImplementation projects.plugincore
withQuickstepCompileOnly projects.common
withQuickstepCompileOnly projects.utils
// QuickSwitch Compat
withQuickstepImplementation projects.compatLib
withQuickstepImplementation projects.compatLib.compatLibVQ
withQuickstepImplementation projects.compatLib.compatLibVR
withQuickstepImplementation projects.compatLib.compatLibVS
withQuickstepImplementation projects.compatLib.compatLibVT
withQuickstepImplementation projects.compatLib.compatLibVU
withQuickstepImplementation projects.compatLib.compatLibVV
withQuickstepImplementation projects.compatLib.compatLibVBaklava
withQuickstepImplementation projects.wmshell
withQuickstepImplementation projects.flags
withQuickstepImplementation projects.dagger
withQuickstepImplementation projects.concurrent
// withQuickstepImplementation projects.compose
withQuickstepImplementation projects.modules.widgetpicker
implementation libs.androidx.dynamicanimation
implementation fileTree(dir: FRAMEWORK_PREBUILTS_DIR, include: 'SystemUI-statsd-16.jar')
implementation fileTree(dir: FRAMEWORK_PREBUILTS_DIR, include: 'WindowManager-Shell-16.jar')
withQuickstepCompileOnly fileTree(dir: FRAMEWORK_PREBUILTS_DIR, include: 'framework-16.jar')
baselineProfile projects.baselineProfile
coreLibraryDesugaring libs.android.desugarJdkLibs
implementation libs.androidx.profileinstaller
baselineProfile projects.baselineProfile
implementation libs.androidx.recyclerview
implementation libs.androidx.preference.ktx
implementation libs.javax.inject
implementation libs.kotlinx.coroutines.android
implementation libs.kotlinx.serialization.json
implementation libs.chickenhook.restrictionbypass
implementation libs.rikka.refine.runtime
implementation libs.androidx.activity.compose
implementation libs.androidx.constraintlayout
implementation libs.androidx.datastore.preferences
implementation platform(libs.compose.bom)
implementation libs.compose.ui
implementation libs.compose.ui.util
implementation libs.compose.ui.graphics
implementation libs.bundles.graphics
debugImplementation libs.compose.ui.tooling
implementation libs.compose.ui.tooling.preview
implementation libs.compose.ui.google.fonts
implementation libs.compose.foundation
implementation libs.compose.material.icons
implementation libs.compose.runtime.livedata
implementation libs.compose.material3
implementation libs.compose.material3.windowSizeClass
implementation libs.androidx.activity.compose
implementation libs.androidx.lifecycle.viewmodel.compose
implementation libs.androidx.navigation.compose
implementation libs.androidx.constraintlayout
implementation libs.androidx.palette.ktx
implementation libs.androidx.slice.core
implementation libs.bundles.accompanist
implementation libs.google.material
implementation libs.material.motion.compose
implementation libs.kdrag0n.colorkt
implementation libs.coil.compose
implementation libs.xdrop.fuzzywuzzy
// Flexera Launcher: encodes the QR code on the About screen. Nothing else of it is used.
implementation libs.zxing.core
implementation libs.bundles.opto
implementation libs.androidx.datastore.preferences
implementation libs.bundles.retrofit
implementation libs.bundles.room
ksp libs.room.compiler
implementation libs.libsu.service
implementation libs.airbnb.lottie
// Compose drag and drop library
implementation libs.reorderable
// Smartspacer
implementation(libs.smartspacer.sdk) {
exclude group: "com.github.skydoves", module: "balloon"
}
implementation libs.hoko.blur
implementation libs.androidx.window
ksp libs.dagger.compiler
implementation libs.dagger.hilt.android
ksp libs.dagger.hilt.compiler
implementation libs.google.guava
implementation libs.androidx.exifinterface
debugImplementation libs.leakcanary.android
debugImplementation libs.leakcanary.androidstudio
}
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
arg("dagger.hilt.disableModulesHaveInstallInCheck", "true")
}
spotless {
java {
// LC-Note: Don't spotless AOSP code in compatlib
// target("compatLib/**/src/**/*.java")
googleJavaFormat().aosp()
removeUnusedImports()
}
kotlin {
target("lawnchair/src/**/*.kt")
ktlint(libs.ktlint.get().version).customRuleSets([
libs.composeRules.get().toString()
]).editorConfigOverride([
"ktlint_compose_compositionlocal-allowlist": "disabled",
"ktlint_compose_lambda-param-event-trailing": "disabled",
"ktlint_compose_content-slot-reused": "disabled",
"ktlint_compose_state-param-check": "disabled",
])
}
}
licensee {
allow(SpdxId.Apache_20)
allow(SpdxId.BSD_3_Clause)
allow(SpdxId.GPL_20_or_later)
allowDependency(libs.libsu.core)
allowDependency(libs.libsu.service)
allowUrl("https://github.com/patrykmichalik/opto/blob/master/LICENSE")
allowUrl("https://github.com/RikkaApps/HiddenApiRefinePlugin/blob/main/LICENSE")
allowUrl("https://opensource.org/licenses/mit-license.php")
bundleAndroidAsset = true
androidAssetReportPath = 'licenses.json'
}