Skip to content

Commit ee1bd3a

Browse files
authored
Fix the status badge, the shortcut offer and the feed's first check (#895)
The status badge is the only route to the System status page, where the settings for opening Vector live, and a tick does not read as a button (#856). While the framework is active the tick now morphs into a gear for ten seconds every thirty; across those ten seconds the gear tosses a coin every two seconds and either turns once or stands still, because a wheel that starts and stops reads as something being operated while one that simply rotates becomes decoration. Every degree it moves was tossed for, including the one hint in thirty-two that does not move at all. Only the tick does this; the other states are reports, two of them urgent. The hint retires after five badge taps in a day and returns the next. A pinned shortcut does not follow the user to a launcher installed later, yet getPinnedShortcuts keeps reporting it, because the pin flag belongs to the shortcut rather than to the pair and only the active launcher may read the per-launcher sets (#883). The launchers that have pinned it are now recorded on this side, and a device running a launcher that is not among them is offered the shortcut again. Where nothing is recorded the current launcher is adopted, so no existing shortcut is declared missing, and a home screen resolving to the chooser or to nothing counts as unknown rather than as a mismatch. Opening Home tossed a coin, and four times in five it showed whatever was on disk — right for returning to Home, wrong for the first Home of a process, when the archive has had longest to go stale. The first now always revalidates; the toss governs only the visits after it.
1 parent e8bec6b commit ee1bd3a

5 files changed

Lines changed: 372 additions & 13 deletions

File tree

manager/src/main/kotlin/org/matrix/vector/manager/data/repository/LaunchShortcut.kt

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import android.os.Build
2222
import androidx.core.content.ContextCompat
2323
import java.util.UUID
2424
import org.matrix.vector.manager.BuildConfig
25+
import org.matrix.vector.manager.di.ServiceLocator
2526
import org.matrix.vector.manager.logE
2627
import org.matrix.vector.manager.logW
2728
import org.matrix.vector.manager.R
@@ -72,11 +73,68 @@ object LaunchShortcut {
7273
.onFailure { logW("actions: pin support query failed", it) }
7374
.getOrDefault(false)
7475

76+
/**
77+
* Whether *some* launcher on this device holds the shortcut.
78+
*
79+
* Not the same question as whether the reader can see it, which is [isPinnedHere]. The pin flag
80+
* is a property of the shortcut and not of the launcher that asked for it, so this stays true
81+
* for a launcher that has since been replaced.
82+
*/
7583
fun isPinned(context: Context): Boolean =
7684
runCatching { manager(context)?.pinnedShortcuts.orEmpty().any { it.id == ID } }
7785
.onFailure { logW("actions: pinned shortcut query failed", it) }
7886
.getOrDefault(false)
7987

88+
/**
89+
* Whether the shortcut is on the home screen the reader is actually looking at.
90+
*
91+
* Installing a different launcher does not carry pinned shortcuts across — the new one starts
92+
* with an empty home screen — but [isPinned] keeps saying yes, because the platform records the
93+
* pin on the shortcut rather than on the pair and only lets the active launcher read the
94+
* per-launcher sets. So the row offering to create one showed a tick over a home screen with no
95+
* Vector on it, and there was no way to ask for another: #883.
96+
*
97+
* The launchers that have pinned it are therefore remembered on this side, in
98+
* SettingsRepository. A device with nothing recorded is one that pinned the shortcut before this
99+
* was written, or by some route that never came back through [request]; rather than tell that
100+
* reader their shortcut is missing, the launcher they are on now is adopted as its owner, which
101+
* is almost certainly true and makes the *next* launcher change detectable.
102+
*/
103+
fun isPinnedHere(context: Context): Boolean {
104+
if (!isPinned(context)) return false
105+
// No answer is not a mismatch. A device whose default home cannot be resolved is not one we
106+
// may tell that its shortcut has gone.
107+
val launcher = currentLauncher(context) ?: return true
108+
val settings = ServiceLocator.settings
109+
val known = settings.shortcutLaunchers()
110+
if (known.isEmpty()) {
111+
settings.noteShortcutLauncher(launcher)
112+
return true
113+
}
114+
return launcher in known
115+
}
116+
117+
/**
118+
* The package drawing the home screen, or null when the device will not say which.
119+
*
120+
* Null covers two cases that must both be read as "do not know": the query failing, and it
121+
* resolving to the platform's own chooser, which is what a device with several launchers and no
122+
* default answers. Neither is evidence that the shortcut is somewhere the reader cannot see.
123+
*/
124+
fun currentLauncher(context: Context): String? =
125+
runCatching {
126+
context.packageManager
127+
.resolveActivity(
128+
Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME),
129+
PackageManager.MATCH_DEFAULT_ONLY,
130+
)
131+
?.activityInfo
132+
?.packageName
133+
?.takeIf { it != RESOLVER_PACKAGE }
134+
}
135+
.onFailure { logW("actions: current launcher query failed", it) }
136+
.getOrNull()
137+
80138
/**
81139
* Asks the launcher to pin the shortcut, calling [onPinned] if and when it does.
82140
*
@@ -90,7 +148,19 @@ object LaunchShortcut {
90148
if (!isParasitic(context)) return false
91149
val shortcut = build(context) ?: return false
92150
return runCatching {
93-
manager(context)?.requestPinShortcut(shortcut, callback(context, onPinned)) == true
151+
val confirmed =
152+
callback(context) {
153+
// Recorded here rather than when the request is made, because the launcher
154+
// may refuse or the reader may dismiss its dialog, and a launcher noted as
155+
// holding a shortcut it never took would suppress the offer for good. Read
156+
// again rather than captured: this runs after the launcher's own dialog,
157+
// which is long enough for the default home to have changed.
158+
currentLauncher(context)?.let {
159+
ServiceLocator.settings.noteShortcutLauncher(it)
160+
}
161+
onPinned()
162+
}
163+
manager(context)?.requestPinShortcut(shortcut, confirmed) == true
94164
}
95165
.onFailure { logE("actions: pin shortcut request failed", it) }
96166
.getOrDefault(false)
@@ -244,6 +314,9 @@ object LaunchShortcut {
244314
/** The synthesised entry every package has, used as the shortcut's publishing activity on Q+. */
245315
private const val APP_DETAILS_ACTIVITY = "android.app.AppDetailsActivity"
246316

317+
/** What CATEGORY_HOME resolves to when the device has several launchers and no default. */
318+
private const val RESOLVER_PACKAGE = "android"
319+
247320
/** Held by the system and by nothing installable, so only the platform can confirm a pin. */
248321
private const val CONFIRMATION_PERMISSION = "android.permission.CREATE_USERS"
249322

manager/src/main/kotlin/org/matrix/vector/manager/data/repository/SettingsRepository.kt

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package org.matrix.vector.manager.data.repository
22

33
import android.content.Context
44
import android.content.SharedPreferences
5+
import java.time.LocalDate
56
import kotlinx.coroutines.flow.MutableStateFlow
67
import kotlinx.coroutines.flow.StateFlow
78
import kotlinx.coroutines.flow.asStateFlow
@@ -323,6 +324,77 @@ class SettingsRepository(context: Context) {
323324
_launcherPromptDismissed.value = true
324325
}
325326

327+
/**
328+
* Which launchers are known to be holding a pinned Vector shortcut.
329+
*
330+
* The platform will not say. `ShortcutManager.getPinnedShortcuts` answers for *any* launcher at
331+
* once — the pin flag lives on the shortcut, not on the pair — and the per-launcher sets are
332+
* only readable by a caller that is itself the active launcher. So a device that pinned the
333+
* shortcut, then installed a different launcher, is told it already has one while its home
334+
* screen has nothing on it, which is what #883 reported.
335+
*
336+
* A set rather than a single package because pinning on a second launcher does not unpin the
337+
* first, and someone who keeps two and switches between them should not be offered a shortcut
338+
* they already have on both. What the set cannot represent is a shortcut *removed* from one of
339+
* several launchers holding it: nothing tells us which one lost it, and the platform still
340+
* reports the shortcut pinned. That row will read as done until the last copy is gone.
341+
*/
342+
fun shortcutLaunchers(): Set<String> =
343+
prefs.getStringSet("shortcut_launchers", emptySet()).orEmpty().toSet()
344+
345+
fun noteShortcutLauncher(packageName: String) {
346+
val known = shortcutLaunchers()
347+
if (packageName in known) return
348+
// A set of our own: `getStringSet` hands back the instance the preferences hold, which the
349+
// platform documents as not ours to modify.
350+
prefs.edit().putStringSet("shortcut_launchers", HashSet(known + packageName)).apply()
351+
}
352+
353+
// --- the status badge's own hint ----------------------------------------------------------
354+
355+
/**
356+
* How many times *today* the status badge was used to open System status.
357+
*
358+
* The badge is the only way to those settings, and nothing about a tick says so — #856. The
359+
* header answers that by having the tick turn into a gear now and then, and this is what stops
360+
* it: a reader who has opened the page several times today plainly knows where it is, and a gear
361+
* that keeps appearing after that is noise on the one part of the header whose job is to report
362+
* a state. How many is several is HomeViewModel's to say — this only counts.
363+
*
364+
* Counted per day rather than for good because the hint costs nothing to offer again and the
365+
* knowledge does fade — and because a count that only ever grows would retire the hint on the
366+
* strength of an afternoon spent on that page months ago. The day is stored beside the count and
367+
* a stale one reads as zero, so no reset has to run at midnight.
368+
*/
369+
private val _statusBadgeOpens = MutableStateFlow(statusBadgeOpensToday())
370+
val statusBadgeOpens: StateFlow<Int> = _statusBadgeOpens.asStateFlow()
371+
372+
fun noteStatusBadgeOpened() {
373+
val today = LocalDate.now().toEpochDay()
374+
// Against the stored day, not against the flow: a session left open across midnight holds
375+
// yesterday's count in memory, and adding to it would carry it into today.
376+
val next =
377+
if (prefs.getLong("status_badge_day", 0L) == today) _statusBadgeOpens.value + 1 else 1
378+
prefs.edit().putLong("status_badge_day", today).putInt("status_badge_opens", next).apply()
379+
_statusBadgeOpens.value = next
380+
}
381+
382+
/**
383+
* Re-reads the count against today's date.
384+
*
385+
* Called when Home is opened, which is the only moment the hint can start running again, and is
386+
* what lets a session that has crossed midnight — parasitically rare, since the host process is
387+
* killed constantly, but free to handle — offer it afresh.
388+
*/
389+
fun refreshStatusBadgeOpens() {
390+
_statusBadgeOpens.value = statusBadgeOpensToday()
391+
}
392+
393+
private fun statusBadgeOpensToday(): Int =
394+
if (prefs.getLong("status_badge_day", 0L) == LocalDate.now().toEpochDay())
395+
prefs.getInt("status_badge_opens", 0)
396+
else 0
397+
326398
// --- Logs ---
327399

328400
/**

0 commit comments

Comments
 (0)