5 Commits
Author SHA1 Message Date
TMCandClaude Fable 5.2 60d00afcfd release.ps1: keep Gradle's warnings from stopping a piped run
Gradle prints compiler warnings on its error stream. When the script's output was piped or
sent to a file, PowerShell 5.1 took the first such line for an error and stopped the build
at step 2 (seen on the first 0.3.1 attempt). The streams are now merged inside cmd, and
only the exit code decides whether the build failed.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 08:07:30 +01:00
TMC 74becdd355 Release 0.3.1 2026-11-12 08:02:33 +01:00
TMCandClaude Fable 5.2 4771d2928a Start the alphabet scrub the moment a hold is noticed; keep the selected letter clear of the thumb
Two reports from first use on the Pixel 9a (PRD AD-5, AD-6, AD-17).

Swipe up and hold was slow to hand over to the alphabet:
- After the hold was noticed, the drawer was sent to its open state through the state
  manager, which takes 600 ms, and the alphabet took the finger only after that, and then
  only on the next touch movement. A finger held perfectly still never started it.
- Now the alphabet takes the finger's position in the same call that notices the hold, and
  the drawer finishes opening under it by playing the swipe's own animation to its end in
  120 ms.
- A still finger counts as holding after 150 ms instead of 300 (MotionPauseDetector gains
  setForcePauseTimeout, reset by clear()), and the swipe has to travel 32dp instead of 72dp
  before a hold counts.
- Because the list now jumps at once, a finger that lifts within 200 ms of the hold being
  noticed is treated as an ordinary swipe that slowed down at its end: the scrub is undone
  and the list stays where it was.

The selected letter sat under the thumb:
- The wave's peak was 52dp beyond the finger, about 8 mm, and a thumb hides about 11 mm to
  each side of where it touches. It is now 104dp, and at least 96dp out from the edge.
- The peak may travel to within 36dp of the far edge (was 70% of the width), so the gap
  holds when the finger is in the middle of the screen.
- A taller wave is wider (a third of its reach), so the neighbouring letters bend around
  the thumb instead of vanishing under it.
- A soft dark spot behind the selected letter keeps it readable where it lands on an app
  name or a light wallpaper.

Checked on the Android 17 emulator with touches injected at millisecond timing (a monkey
script) and a thumb-sized shape drawn over the screenshots.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 08:02:20 +01:00
TMC 1f9116d666 Release 0.3.0 2026-11-12 00:05:16 +01:00
TMCandClaude Fable 5.2 4e48615b5a Show an app's latest notification under its name in the drawer list
PRD AD-11, the last buildable row of M4.

- FlexeraNotificationPreview keeps the text of the notifications that currently show a
  dot, fed by Launcher3's NotificationListener on its worker thread, and sets a second,
  smaller and dimmer line on a drawer row whenever the row's dot state is applied.
- It uses what the dots already use: the same notification access and the same filter
  for which notifications count. Without notification access nothing changes.
- PopupDataProvider now refreshes an app's icons on every posted notification, so a
  notification updated in place brings its new text to the row.
- Only rows of the drawer list take the second line (marked with a view tag); search
  result icons share the "all apps" display type but stay as they were.
- New setting under Settings > App drawer: Notification previews, on by default.

Checked on the Android 17 emulator (debug build): a test SMS showed under Messages,
a second one replaced it while the drawer was open, removing the notification put the
plain name back, and the setting switches the line off and on.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 00:05:08 +01:00
15 changed files with 358 additions and 49 deletions

No files matched your search

+4 -1
View File
@@ -170,7 +170,10 @@ if ($SkipBuild) {
Get-ChildItem (Join-Path $Root 'build\kotlin') -Directory -Filter "compile${Variant}Kotlin*" -ErrorAction SilentlyContinue |
Remove-Item -Recurse -Force
$env:JAVA_HOME = $JavaHome
cmd /c ".\gradlew.bat assemble$Variant --no-build-cache --console=plain"
# "2>&1" inside cmd: Gradle prints compiler warnings on its error stream. If this script's
# output is piped or sent to a file, PowerShell would otherwise take the first such line
# for an error and stop the build. Only the exit code says whether the build failed.
cmd /c ".\gradlew.bat assemble$Variant --no-build-cache --console=plain 2>&1"
if ($LASTEXITCODE -ne 0) { throw "The build failed." }
}
if (-not (Test-Path $apk)) { throw "Expected the APK at $apk, but it is not there." }
+14 -7
View File
@@ -21,15 +21,22 @@
<!-- Letters never sit further apart than this, however few there are. -->
<dimen name="flexera_wave_max_spacing">30dp</dimen>
<dimen name="flexera_wave_vertical_margin">16dp</dimen>
<!-- How far before the finger the wave's peak sits, so the thumb never covers the letter. -->
<dimen name="flexera_wave_finger_gap">52dp</dimen>
<!-- How far beyond the finger the wave's peak sits, so the thumb never covers the selected
letter. A thumb hides about 11 mm (70dp) to each side of the point it touches; the
letter's centre has to clear that by its own half width and a margin. At 52dp, the
first value, the letter sat under the thumb. -->
<dimen name="flexera_wave_finger_gap">104dp</dimen>
<!-- The smallest bulge, used when the finger is right on the edge. -->
<dimen name="flexera_wave_min_reach">60dp</dimen>
<!-- How far up and down the edge the bulge spreads. -->
<dimen name="flexera_wave_width">56dp</dimen>
<dimen name="flexera_wave_min_reach">96dp</dimen>
<!-- How far up and down the edge the bulge spreads, at the least. A taller wave is wider
(WaveAlphabetView.WAVE_WIDTH_SHARE_OF_REACH). -->
<dimen name="flexera_wave_width">68dp</dimen>
<!-- The wave's peak never comes closer than this to the opposite edge of the screen. -->
<dimen name="flexera_wave_far_margin">36dp</dimen>
<!-- Swipe up and hold (PRD AD-17): a pause only counts once the swipe has travelled this far. -->
<dimen name="flexera_hold_min_swipe">72dp</dimen>
<!-- Swipe up and hold (PRD AD-17): a pause only counts once the swipe has travelled this far.
Enough to tell a hold from a hesitant start, short enough that a small swipe counts. -->
<dimen name="flexera_hold_min_swipe">32dp</dimen>
<!-- Soft shadow behind white drawer text, for readability over light wallpaper. -->
<dimen name="flexera_drawer_text_shadow_radius">3dp</dimen>
+4
View File
@@ -2,4 +2,8 @@
<resources>
<!-- The wave alphabet view inside the drawer (app.flexera.drawer.WaveAlphabetView). -->
<item name="flexera_wave_alphabet" type="id" />
<!-- View tag that marks an app row of the drawer list (FlexeraDrawer.styleAppRow). -->
<item name="flexera_drawer_row" type="id" />
<!-- View tag on a drawer row: set while the row shows a notification preview under the app name. -->
<item name="flexera_notification_preview" type="id" />
</resources>
+2
View File
@@ -40,6 +40,8 @@
<!-- Read by screen readers as the name of the alphabet strip, followed by the current letter -->
<string name="flexera_alphabet_description">Alphabet</string>
<!-- App drawer settings (app.flexera.ui.FlexeraDrawerSettings) -->
<string name="flexera_notification_previews">Notification previews</string>
<string name="flexera_notification_previews_description">Show an app\'s latest notification under its name in the app list. Works when notification dots are on (Settings, General).</string>
<string name="flexera_alphabet_heading">Alphabet</string>
<string name="flexera_alphabet_on_left">Alphabet on the left edge</string>
<string name="flexera_alphabet_on_left_description">For left-handed use. The letters normally sit on the right edge.</string>
@@ -87,6 +87,9 @@ object FlexeraDrawer {
@JvmStatic
fun styleAppRow(icon: BubbleTextView) {
val res = icon.resources
// Other icons share the "all apps" display type (search results, for one); this tag
// tells list rows apart from them.
icon.setTag(R.id.flexera_drawer_row, true)
icon.layoutParams.height = res.getDimensionPixelSize(R.dimen.flexera_drawer_row_height)
icon.compoundDrawablePadding = res.getDimensionPixelSize(R.dimen.flexera_drawer_row_icon_gap)
icon.setTextSize(TypedValue.COMPLEX_UNIT_PX, res.getDimension(R.dimen.flexera_drawer_row_text_size))
@@ -197,12 +200,19 @@ object FlexeraDrawer {
appsView: ActivityAllAppsContainerView<*>,
dragLayer: BaseDragLayer<*>,
ev: MotionEvent,
) = scrubFromSwipe(appsView, dragLayer, ev.x, ev.y)
/** The same, for a finger position ([x], [y]) in the drag layer's coordinates. */
@JvmStatic
fun scrubFromSwipe(
appsView: ActivityAllAppsContainerView<*>,
dragLayer: BaseDragLayer<*>,
x: Float,
y: Float,
) {
val wave = waveAlphabet(appsView) ?: return
val offset = layoutOffset(wave, dragLayer)
val x = ev.x - offset.x
val y = ev.y - offset.y
if (wave.isScrubbing) wave.moveScrub(x, y) else wave.beginScrub(x, y)
if (wave.isScrubbing) wave.moveScrub(x - offset.x, y - offset.y) else wave.beginScrub(x - offset.x, y - offset.y)
}
/**
@@ -242,6 +252,15 @@ object FlexeraDrawer {
waveAlphabet(appsView)?.endScrub()
}
/**
* Ends a scrub started by [scrubFromSwipe] and undoes it: the list goes back to where it
* stood, so the drawer opens the way an ordinary swipe up leaves it.
*/
@JvmStatic
fun cancelScrubFromSwipe(appsView: ActivityAllAppsContainerView<*>) {
waveAlphabet(appsView)?.cancelScrub()
}
/**
* A letter header. It carries its letter so that the adapter's diffing notices when the
* header at a position changes, for example after an app is installed or removed.
@@ -0,0 +1,132 @@
package app.flexera.drawer
import android.app.Notification
import android.service.notification.StatusBarNotification
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.TextUtils
import android.text.style.ForegroundColorSpan
import android.text.style.RelativeSizeSpan
import app.lawnchair.preferences.PreferenceManager
import com.android.launcher3.BubbleTextView
import com.android.launcher3.R
import com.android.launcher3.dot.DotInfo
import com.android.launcher3.model.data.ItemInfo
import java.util.concurrent.ConcurrentHashMap
/**
* Inline notification previews (PRD AD-11): in the drawer list, a row whose app has
* notifications shows the latest one under the app name, smaller and dimmer.
*
* It rides on what Launcher3 already has for notification dots. The same notification access
* feeds it, the same filter decides which notifications count, and a row is refreshed whenever
* its dot is. Without notification access there are no dots and no previews.
*
* Launcher3's listener hands each notification to [onPosted], [onRemoved] or [onFullRefresh] on
* its worker thread; the rows read the stored text on the main thread through [apply].
*/
object FlexeraNotificationPreview {
private class Preview(val text: String, val postTime: Long)
/** Latest text per notification key, for the notifications that currently show a dot. */
private val previews = ConcurrentHashMap<String, Preview>()
private const val MAX_LENGTH = 140
/** The preview line's text size as a share of the app name's. */
private const val TEXT_SCALE = 0.78f
/** White at 70%, so the app name stays the stronger of the two lines. */
private const val TEXT_COLOR = 0xB3FFFFFF.toInt()
@JvmStatic
fun onPosted(sbn: StatusBarNotification) {
val text = previewText(sbn)
if (text == null) previews.remove(sbn.key) else previews[sbn.key] = Preview(text, sbn.postTime)
}
@JvmStatic
fun onRemoved(sbn: StatusBarNotification) {
previews.remove(sbn.key)
}
@JvmStatic
fun onFullRefresh(active: List<StatusBarNotification>) {
previews.clear()
active.forEach(::onPosted)
}
/**
* Sets [icon]'s text for [info]: the app name, plus the latest notification under it when
* [dot] says the app has any. Called for drawer rows whenever their dot state is applied.
*/
@JvmStatic
fun apply(icon: BubbleTextView, info: ItemInfo, dot: DotInfo?) {
// Only rows of the drawer list have room for a second line.
if (icon.getTag(R.id.flexera_drawer_row) == null) return
val name = info.title ?: return
val preview = if (dot != null && isEnabled(icon)) latest(dot) else null
if (preview == null) {
if (icon.getTag(R.id.flexera_notification_preview) != null) {
// The row was showing a preview, for this app earlier or for another app before
// the view was reused. Back to the plain one-line name.
icon.setTag(R.id.flexera_notification_preview, null)
icon.setLines(1)
icon.text = name
}
return
}
// The text is set afresh every time. Binding a row puts the plain name back first, so
// what the row showed before says nothing about what it shows now.
// Keep a long app name on its own line, so the preview is never pushed off the row.
val width = icon.width - icon.compoundPaddingLeft - icon.compoundPaddingRight
val firstLine = if (width > 0) {
TextUtils.ellipsize(name, icon.paint, width.toFloat(), TextUtils.TruncateAt.END)
} else {
name
}
val label = SpannableStringBuilder(firstLine).append('\n')
val start = label.length
label.append(preview)
label.setSpan(RelativeSizeSpan(TEXT_SCALE), start, label.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
label.setSpan(ForegroundColorSpan(TEXT_COLOR), start, label.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
icon.setLines(2)
icon.ellipsize = TextUtils.TruncateAt.END
icon.text = label
icon.setTag(R.id.flexera_notification_preview, true)
}
private fun isEnabled(icon: BubbleTextView): Boolean =
PreferenceManager.getInstance(icon.context).flexeraNotificationPreviews.get()
/** The text of the newest of the app's notifications that has any text. */
private fun latest(dot: DotInfo): String? {
var newest: Preview? = null
for (key in dot.notificationKeys) {
val candidate = previews[key.notificationKey] ?: continue
if (newest == null || candidate.postTime > newest.postTime) newest = candidate
}
return newest?.text
}
/** "Title: text" in one line, or whichever of the two the notification has. */
private fun previewText(sbn: StatusBarNotification): String? {
val extras = sbn.notification.extras ?: return null
val title = extras.getCharSequence(Notification.EXTRA_TITLE).oneLine()
val text = extras.getCharSequence(Notification.EXTRA_TEXT).oneLine()
val combined = when {
title.isEmpty() -> text
text.isEmpty() -> title
else -> "$title: $text"
}
return combined.take(MAX_LENGTH).ifEmpty { null }
}
private fun CharSequence?.oneLine(): String =
this?.toString()?.replace('\n', ' ')?.trim().orEmpty()
}
@@ -6,7 +6,9 @@ import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RadialGradient
import android.graphics.Rect
import android.graphics.Shader
import android.graphics.Typeface
import android.os.Build
import android.os.Bundle
@@ -37,9 +39,9 @@ import kotlin.math.roundToInt
*
* Only letters that have apps are shown. Touching the strip and sliding along it jumps the list
* to the letter under the finger, with a haptic tick on every change. While the finger is down,
* the letters near it bulge away from the edge in a wave, and the wave reaches further the
* further the finger moves in from the edge, so the current letter always sits beside the thumb
* instead of under it.
* the letters near it bulge away from the edge in a wave. The wave's peak, where the current
* letter is, stays a thumb's width beyond the finger however far in from the edge the finger
* is, so the letter always sits beside the thumb instead of under it.
*
* The strip sits on the right edge unless the owner moves it to the left (PRD AD-15).
*
@@ -69,6 +71,7 @@ class WaveAlphabetView(
private val fingerGap = res.getDimension(R.dimen.flexera_wave_finger_gap)
private val minReach = res.getDimension(R.dimen.flexera_wave_min_reach)
private val waveWidth = res.getDimension(R.dimen.flexera_wave_width)
private val farMargin = res.getDimension(R.dimen.flexera_wave_far_margin)
private val letterSize = res.getDimension(R.dimen.flexera_wave_letter_size)
/** The section name Launcher3 gives the work profile's info card, an "i" in a circle. */
@@ -91,6 +94,22 @@ class WaveAlphabetView(
}
private val boldTypeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
/**
* A soft dark spot behind the selected letter. Out over the list the letter can land on an
* app name, and on a light wallpaper white on white; the spot keeps it readable on both.
* Drawn as a circle of radius 1 and scaled to size, so nothing is allocated while drawing.
*/
private val backdropPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
shader = RadialGradient(
0f,
0f,
1f,
intArrayOf(BACKDROP_COLOR, BACKDROP_COLOR, Color.TRANSPARENT),
floatArrayOf(0f, BACKDROP_SOLID_SHARE, 1f),
Shader.TileMode.CLAMP,
)
}
private var letters: List<Letter> = emptyList()
private var letterYs = FloatArray(0)
private var stripTop = 0f
@@ -106,6 +125,10 @@ class WaveAlphabetView(
private var fingerY = 0f
private var selected = -1
/** Where the list stood when the scrub began (first visible row and its offset), for [cancelScrub]. */
private var listPositionBefore = RecyclerView.NO_POSITION
private var listOffsetBefore = 0
/** 0 when the strip is at rest, 1 when the wave is fully out. Animated on touch down and up. */
private var press = 0f
private var pressAnimator: ValueAnimator? = null
@@ -225,12 +248,16 @@ class WaveAlphabetView(
// Where the letters rest, and which way "away from the edge" points.
val baseX = if (onLeft) edgeInset else width - edgeInset
val inward = if (onLeft) 1f else -1f
// The wave's peak sits a fixed gap before the finger, so it reaches further out the
// further the finger is from the edge.
// The wave's peak sits a fixed gap beyond the finger, on the side away from the edge,
// which is the side the thumb leaves uncovered. So the wave reaches further out the
// further the finger is from the edge, up to nearly the whole width of the screen.
val fingerDistance = (fingerX - baseX) * inward
val maxReach = width * 0.7f
val maxReach = max(minReach + 1f, width - edgeInset - farMargin)
val fullReach = (fingerDistance + fingerGap).coerceIn(minReach, maxReach)
val reach = fullReach * press
// A taller wave is also a wider one. It keeps its shape that way, and the letters next
// to the selected one bend around the thumb instead of vanishing under it.
val spread = max(waveWidth, fullReach * WAVE_WIDTH_SHARE_OF_REACH)
// Darken the list under the wave where its letters cross icons and names. From the
// right edge that only happens once the wave reaches well into the screen, as it does
@@ -243,7 +270,7 @@ class WaveAlphabetView(
for (i in letters.indices) {
val y = letterYs[i]
val nearness = if (press > 0f) bell((y - fingerY) / waveWidth) else 0f
val nearness = if (press > 0f) bell((y - fingerY) / spread) else 0f
val isSelected = i == selected && press > 0f
val scale = 1f + (if (isSelected) SELECTED_GROWTH else NEAR_GROWTH) * nearness * press
@@ -256,6 +283,15 @@ class WaveAlphabetView(
}
val x = baseX + inward * reach * nearness
if (isSelected) {
val radius = paint.textSize * BACKDROP_RADIUS_IN_LETTER_SIZES
backdropPaint.alpha = (press * 255).toInt()
canvas.save()
canvas.translate(x, y)
canvas.scale(radius, radius)
canvas.drawCircle(0f, 0f, 1f, backdropPaint)
canvas.restore()
}
// Centre the glyph on the letter's line.
val baseline = y - (paint.descent() + paint.ascent()) / 2f
canvas.drawText(letters[i].label, x, baseline, paint)
@@ -312,10 +348,35 @@ class WaveAlphabetView(
if (letters.isEmpty()) return
scrubbing = true
selected = -1
rememberListPosition()
animatePress(1f, PRESS_IN_MS)
moveScrub(x, y)
}
private fun rememberListPosition() {
listPositionBefore = RecyclerView.NO_POSITION
val rv = appsView.activeRecyclerView ?: return
val layoutManager = rv.layoutManager as? LinearLayoutManager ?: return
val position = layoutManager.findFirstVisibleItemPosition()
if (position == RecyclerView.NO_POSITION) return
listPositionBefore = position
listOffsetBefore = (layoutManager.findViewByPosition(position)?.top ?: rv.paddingTop) - rv.paddingTop
}
/**
* Ends the scrub and puts the list back where it stood before it, as if the scrub had not
* happened. For a scrub that turns out to have been started by mistake.
*/
fun cancelScrub() {
if (!scrubbing) return
endScrub()
if (listPositionBefore == RecyclerView.NO_POSITION) return
val rv = appsView.activeRecyclerView ?: return
rv.stopScroll()
(rv.layoutManager as? LinearLayoutManager)
?.scrollToPositionWithOffset(listPositionBefore, listOffsetBefore)
}
/** Moves the finger of a scrub started with [beginScrub]. */
fun moveScrub(x: Float, y: Float) {
if (!scrubbing || letters.isEmpty()) return
@@ -465,6 +526,14 @@ class WaveAlphabetView(
/** How much its neighbours grow, tapering with distance from the finger. */
const val NEAR_GROWTH = 0.5f
/** The spot behind the selected letter: black at 60%, solid to this share of its radius. */
const val BACKDROP_COLOR = 0x99000000.toInt()
const val BACKDROP_SOLID_SHARE = 0.55f
const val BACKDROP_RADIUS_IN_LETTER_SIZES = 1.15f
/** The wave's spread up and down the edge, as a share of how far it reaches out. */
const val WAVE_WIDTH_SHARE_OF_REACH = 0.33f
/** How dark the list gets under a wave that lies fully over it (0 = unchanged, 1 = black). */
const val SCRUB_DIM = 0.55f
@@ -49,6 +49,11 @@ fun FlexeraDrawerSettings(modifier: Modifier = Modifier) {
label = stringResource(id = R.string.suggestion_pref_screen_title),
destination = Predictions,
)
SwitchPreference(
label = stringResource(id = R.string.flexera_notification_previews),
description = stringResource(id = R.string.flexera_notification_previews_description),
adapter = prefs.flexeraNotificationPreviews.getAdapter(),
)
SwitchPreference(
label = stringResource(id = R.string.pref_all_apps_remember_position_title),
description = stringResource(id = R.string.pref_all_apps_remember_position_description),
+1 -1
View File
@@ -2,4 +2,4 @@
# build.gradle reads this and derives the Android version code from it
# (major * 10000 + minor * 100 + patch), so the name and the code can never disagree.
# flexera/release.ps1 raises it for every release.
versionName=0.2.0
versionName=0.3.1
@@ -220,6 +220,9 @@ class PreferenceManager @Inject constructor(
// Flexera Launcher: whether a slide along the home screen's edge opens the drawer at a letter (PRD AD-10).
val flexeraScrubFromHome = BoolPref("pref_flexeraScrubFromHome", false)
// Flexera Launcher: whether drawer rows show the app's latest notification under its name (PRD AD-11).
val flexeraNotificationPreviews = BoolPref("pref_flexeraNotificationPreviews", true, recreate)
override fun close() {
TODO("Not yet implemented")
}
@@ -17,6 +17,7 @@
package com.android.launcher3.uioverrides.touchcontrollers;
import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE;
import static com.android.app.animation.Interpolators.DECELERATE;
import static com.android.launcher3.LauncherAnimUtils.SCRIM_COLORS;
import static com.android.launcher3.LauncherAnimUtils.newSingleUseCancelListener;
import static com.android.launcher3.LauncherState.ALL_APPS;
@@ -35,6 +36,7 @@ import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.graphics.PointF;
import android.os.SystemClock;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
@@ -81,10 +83,23 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch
private boolean mDidTouchStartInNavBar;
// Flexera Launcher: swipe up and hold hands the same touch to the wave alphabet (PRD AD-17).
// "Started" is set when the pause is detected and the drawer begins to finish opening;
// "ready" is set once it is fully open and the alphabet can take the finger's position.
// "Started" is set when the pause is detected: the alphabet takes the finger at once, while
// the drawer finishes opening under it. "Drawer open" is set when that has finished.
private boolean mFlexeraScrubStarted;
private boolean mFlexeraScrubReady;
private boolean mFlexeraDrawerOpen;
// Where the finger was at the last drag event, in the drag layer's coordinates.
private float mFlexeraLastX;
private float mFlexeraLastY;
// How long the drawer takes to finish opening once a hold is detected. Short, because the
// alphabet is already following the finger and the list has to be readable behind it.
private static final long FLEXERA_HOLD_OPEN_MS = 120;
// A finger that has not moved for this long is holding. Swipe to recents waits twice as
// long; here a false alarm only opens the drawer the swipe was opening anyway.
private static final long FLEXERA_HOLD_STILL_MS = 150;
// A finger that lifts sooner than this after the hold was noticed was not holding; see
// onDragEnd. Too short a time to have read the letter and meant it.
private static final long FLEXERA_MIN_HOLD_MS = 200;
private long mFlexeraScrubStartTime;
private boolean mStartedOverview;
private boolean mReachedOverview;
// The last recorded displacement before we reached overview.
@@ -177,9 +192,10 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch
// Flexera Launcher: a pause during an ordinary swipe up to the drawer starts a scrub.
mFlexeraScrubStarted = false;
mFlexeraScrubReady = false;
mFlexeraDrawerOpen = false;
if (flexeraHoldToScrub()) {
mMotionPauseDetector.setOnMotionPauseListener(this::onFlexeraHoldDetected);
mMotionPauseDetector.setForcePauseTimeout(FLEXERA_HOLD_STILL_MS);
}
if (mFromState == NORMAL && mToState == HINT_STATE) {
@@ -219,8 +235,15 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch
if (mFlexeraScrubStarted) {
// Flexera Launcher: lifting the finger ends the scrub and leaves the drawer open at
// the chosen letter. If the drawer is still opening, its end callback finishes up.
app.flexera.drawer.FlexeraDrawer.endScrubFromSwipe(mLauncher.getAppsView());
if (mFlexeraScrubReady) {
if (SystemClock.uptimeMillis() - mFlexeraScrubStartTime < FLEXERA_MIN_HOLD_MS) {
// The finger left almost as soon as the hold was noticed, so it was not
// holding: an ordinary swipe slowed down at its end. Open the drawer the way
// that swipe would have, with the list where it was.
app.flexera.drawer.FlexeraDrawer.cancelScrubFromSwipe(mLauncher.getAppsView());
} else {
app.flexera.drawer.FlexeraDrawer.endScrubFromSwipe(mLauncher.getAppsView());
}
if (mFlexeraDrawerOpen) {
onSwipeInteractionCompleted(ALL_APPS);
}
} else if (mStartedOverview) {
@@ -288,26 +311,34 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch
}
/**
* Flexera Launcher: the finger paused part-way through a swipe up. Finish opening the drawer
* and, once it is open, let the same touch scrub the wave alphabet. This mirrors
* {@link #onMotionPauseDetected()}, which does the same hand-over to Overview.
* Flexera Launcher: the finger paused part-way through a swipe up. From this moment the same
* touch scrubs the wave alphabet, with no wait: the alphabet takes the finger where it is,
* and the drawer finishes opening under it.
*/
private void onFlexeraHoldDetected() {
if (mCurrentAnimation == null || mFlexeraScrubStarted || !flexeraHoldToScrub()) {
return;
}
mCurrentAnimation.getTarget().addListener(newSingleUseCancelListener(() ->
mLauncher.getStateManager().goToState(ALL_APPS, true, forSuccessCallback(() -> {
mFlexeraScrubReady = true;
if (!mDetector.isDraggingState()) {
// The finger lifted while the drawer was still opening.
onSwipeInteractionCompleted(ALL_APPS);
}
}))));
mCurrentAnimation.getTarget().removeListener(mClearStateOnCancelListener);
mCurrentAnimation.dispatchOnCancel();
mFlexeraScrubStarted = true;
mFlexeraScrubStartTime = SystemClock.uptimeMillis();
app.flexera.drawer.FlexeraDrawer.scrubFromSwipe(
mLauncher.getAppsView(), mLauncher.getDragLayer(), mFlexeraLastX, mFlexeraLastY);
// Play the swipe's own animation to its end, as lifting the finger would, only faster.
// Going through the state manager instead would take its full 600 ms opening time.
mCurrentAnimation.setEndAction(() -> {
mFlexeraDrawerOpen = true;
if (!mDetector.isDraggingState()) {
// The finger lifted while the drawer was still opening.
onSwipeInteractionCompleted(ALL_APPS);
}
});
ValueAnimator anim = mCurrentAnimation.getAnimationPlayer();
anim.setFloatValues(
Utilities.boundToRange(mCurrentAnimation.getProgressFraction(), 0f, 1f), 1f);
anim.setDuration(FLEXERA_HOLD_OPEN_MS).setInterpolator(DECELERATE);
mCurrentAnimation.dispatchOnStart();
anim.start();
VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC);
}
@@ -326,13 +357,15 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch
@Override
public boolean onDrag(float yDisplacement, float xDisplacement, MotionEvent event) {
// Flexera Launcher: remembered so that a hold, which can be noticed between two touch
// events, knows where the finger is.
mFlexeraLastX = event.getX();
mFlexeraLastY = event.getY();
if (mFlexeraScrubStarted) {
// Flexera Launcher: the swipe has become a scrub; the finger now moves the alphabet,
// not the drawer. Until the drawer has finished opening there is nothing to move yet.
if (mFlexeraScrubReady) {
app.flexera.drawer.FlexeraDrawer.scrubFromSwipe(
mLauncher.getAppsView(), mLauncher.getDragLayer(), event);
}
// not the drawer.
app.flexera.drawer.FlexeraDrawer.scrubFromSwipe(
mLauncher.getAppsView(), mLauncher.getDragLayer(), mFlexeraLastX, mFlexeraLastY);
return true;
}
if (mStartedOverview) {
@@ -76,6 +76,9 @@ public class MotionPauseDetector {
private boolean mDisallowPause;
// Time at which speed became < mSpeedSlow (only used if mMakePauseHarderToTrigger == true).
private long mSlowStartTime;
// Flexera Launcher: how long without motion counts as a pause. A gesture may shorten it
// (see setForcePauseTimeout); clear() puts the stock value back.
private long mForcePauseTimeoutMs = FORCE_PAUSE_TIMEOUT;
public MotionPauseDetector(Context context) {
this(context, false);
@@ -122,6 +125,15 @@ public class MotionPauseDetector {
mIsTrackpadGesture = isTrackpadGesture;
}
/**
* Flexera Launcher: sets how long the finger must stay still before that alone counts as a
* pause, for the gesture in progress. Swipe up and hold (PRD AD-17) uses a shorter time
* than the stock 300 ms, so the alphabet answers a hold without a noticeable wait.
*/
public void setForcePauseTimeout(long timeoutMs) {
mForcePauseTimeoutMs = timeoutMs;
}
/**
* @param disallowPause If true, we will not detect any pauses until this is set to false again.
*/
@@ -153,7 +165,7 @@ public class MotionPauseDetector {
? TEST_HARNESS_TRIGGER_TIMEOUT
: mMakePauseHarderToTrigger
? HARDER_TRIGGER_TIMEOUT
: FORCE_PAUSE_TIMEOUT;
: mForcePauseTimeoutMs;
mForcePauseTimeout.setAlarm(timeoutMs);
float newVelocity = mVelocityProvider.addMotionEvent(ev, ev.getPointerId(pointerIndex));
if (mPreviousVelocity != null) {
@@ -262,6 +274,7 @@ public class MotionPauseDetector {
mIsTrackpadGesture = false;
mIsPaused = mHasEverBeenPaused = false;
mSlowStartTime = 0;
mForcePauseTimeoutMs = FORCE_PAUSE_TIMEOUT;
mForcePauseTimeout.cancelAlarm();
}
@@ -1413,6 +1413,11 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
setContentDescription(itemInfo.contentDescription);
}
}
// Flexera Launcher: a row in the drawer list shows its app's latest notification
// under the app name (PRD AD-11).
if (mDisplay == DISPLAY_ALL_APPS) {
app.flexera.drawer.FlexeraNotificationPreview.apply(this, itemInfo, mDotInfo);
}
}
}
@@ -129,13 +129,23 @@ public class NotificationListener extends NotificationListenerService {
switch (message.what) {
case MSG_NOTIFICATION_POSTED: {
StatusBarNotification sbn = (StatusBarNotification) message.obj;
mUiHandler.obtainMessage(notificationIsValidForUI(sbn)
// Flexera Launcher: keep the text for the drawer's notification previews in step
// with the dots, before the rows are told to refresh (PRD AD-11).
boolean validForUi = notificationIsValidForUI(sbn);
if (validForUi) {
app.flexera.drawer.FlexeraNotificationPreview.onPosted(sbn);
} else {
app.flexera.drawer.FlexeraNotificationPreview.onRemoved(sbn);
}
mUiHandler.obtainMessage(validForUi
? MSG_NOTIFICATION_POSTED : MSG_NOTIFICATION_REMOVED,
toKeyPair(sbn)).sendToTarget();
return true;
}
case MSG_NOTIFICATION_REMOVED: {
StatusBarNotification sbn = (StatusBarNotification) message.obj;
// Flexera Launcher: drop the notification's preview text with its dot.
app.flexera.drawer.FlexeraNotificationPreview.onRemoved(sbn);
mUiHandler.obtainMessage(MSG_NOTIFICATION_REMOVED,
toKeyPair(sbn)).sendToTarget();
@@ -158,6 +168,8 @@ public class NotificationListener extends NotificationListenerService {
} else {
activeNotifications = new ArrayList<>();
}
// Flexera Launcher: rebuild the preview texts from the same list as the dots.
app.flexera.drawer.FlexeraNotificationPreview.onFullRefresh(activeNotifications);
mUiHandler.obtainMessage(message.what, activeNotifications).sendToTarget();
return true;
@@ -102,9 +102,11 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan
dotInfo = new DotInfo();
mPackageUserToDotInfos.put(postedPackageUserKey, dotInfo);
}
if (dotInfo.addOrUpdateNotificationKey(notificationKey)) {
updateNotificationDots(postedPackageUserKey::equals);
}
// Flexera Launcher: refresh the app's icons on every posted notification, not only when
// the dot or its count changes. A notification that is updated in place keeps its dot
// but brings new text for the drawer's preview line (PRD AD-11).
dotInfo.addOrUpdateNotificationKey(notificationKey);
updateNotificationDots(postedPackageUserKey::equals);
}
@Override