Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60d00afcfd | ||
|
|
74becdd355 | ||
|
|
4771d2928a | ||
|
|
1f9116d666 | ||
|
|
4e48615b5a |
No files matched your search
+4
-1
@@ -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." }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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),
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
+58
-25
@@ -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
|
||||
|
||||
Reference in new issue
Block a user