This commit is contained in:
parent
cd173d80b8
commit
c35e6935b5
23 changed files with 188 additions and 85 deletions
55
.gitignore
vendored
55
.gitignore
vendored
|
|
@ -1,42 +1,19 @@
|
||||||
# --- Python ---
|
node_modules/
|
||||||
__pycache__/
|
.next/
|
||||||
*.py[cod]
|
|
||||||
.venv/
|
|
||||||
*.egg-info/
|
|
||||||
.pytest_cache/
|
|
||||||
.mypy_cache/
|
|
||||||
.ruff_cache/
|
|
||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
|
venv/
|
||||||
# --- Secrets & data ---
|
.venv/
|
||||||
.env
|
__pycache__/
|
||||||
.env.*
|
*.pyc
|
||||||
!.env.example
|
.gradle/
|
||||||
data/
|
target/
|
||||||
*.sqlite3
|
*.log
|
||||||
|
|
||||||
# --- Android / Gradle ---
|
|
||||||
android/.gradle/
|
|
||||||
android/.kotlin/
|
|
||||||
android/build/
|
|
||||||
android/app/build/
|
|
||||||
*.apk
|
|
||||||
*.aab
|
|
||||||
*.keystore
|
|
||||||
local.properties
|
|
||||||
|
|
||||||
# --- Desktop (Compose for Desktop) ---
|
|
||||||
desktop/.gradle/
|
|
||||||
desktop/.kotlin/
|
|
||||||
desktop/build/
|
|
||||||
desktop/app/build/
|
|
||||||
|
|
||||||
# --- IDE / OS ---
|
|
||||||
.idea/
|
|
||||||
.vscode/
|
|
||||||
*.swp
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.env
|
||||||
# --- Release signing (local-only, never commit) ---
|
*.key
|
||||||
android/keystores/*.jks
|
*.pem
|
||||||
|
*.jks
|
||||||
|
tsconfig.tsbuildinfo
|
||||||
|
instance/
|
||||||
|
captures/
|
||||||
|
|
|
||||||
BIN
.icon.png
Normal file
BIN
.icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
BIN
DesignReferenceShonar.png
Normal file
BIN
DesignReferenceShonar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
|
|
@ -16,9 +16,11 @@ import com.shonar.R
|
||||||
* Custom card pair in a decorated notification — the user-verified look, no
|
* Custom card pair in a decorated notification — the user-verified look, no
|
||||||
* system media template.
|
* system media template.
|
||||||
* Expanded: mic | "Recording" + filename | big timer, then X / pause /
|
* Expanded: mic | "Recording" + filename | big timer, then X / pause /
|
||||||
* check pills. Collapsed (shade row, lock screen): slim header-only card
|
* check pills. Collapsed (shade row, lock screen, heads-up): the same card
|
||||||
* (icon | titles | small timer, tap opens app) — the lock shade clips
|
* shrunk to one row — icon | titles | timer | compact X / pause / check —
|
||||||
* anything taller, so pills live only in the expanded card.
|
* so transport is always one tap away with no expanding (user-requested
|
||||||
|
* 2026-09-18). The collapsed slot clips anything taller, so the row stays
|
||||||
|
* single-line.
|
||||||
* DecoratedCustomViewStyle + CATEGORY_SERVICE: light system chrome around
|
* DecoratedCustomViewStyle + CATEGORY_SERVICE: light system chrome around
|
||||||
* the cards — never the dark media-player template.
|
* the cards — never the dark media-player template.
|
||||||
*
|
*
|
||||||
|
|
@ -109,6 +111,14 @@ object RecordingNotificationHelper {
|
||||||
fileName: String = "Shonar",
|
fileName: String = "Shonar",
|
||||||
confirmingDiscard: Boolean = false,
|
confirmingDiscard: Boolean = false,
|
||||||
alertOnce: Boolean = true,
|
alertOnce: Boolean = true,
|
||||||
|
// First post (startForeground banner) goes out plain: on several
|
||||||
|
// SystemUI builds a custom RemoteViews first-bind stalls ~10s
|
||||||
|
// before anything displays (traced 2026-09-18: post 17:03:17.4,
|
||||||
|
// first evaluation 17:03:27.5; plain-template recorder apps on the
|
||||||
|
// same phone display instantly). The 1s ticker swaps in the rich
|
||||||
|
// card, so the plain row lives ~1s. Stock template + timer-first
|
||||||
|
// text keeps title and timer visible in that window.
|
||||||
|
rich: Boolean = true,
|
||||||
): Notification {
|
): Notification {
|
||||||
val openApp = PendingIntent.getActivity(
|
val openApp = PendingIntent.getActivity(
|
||||||
ctx, 10,
|
ctx, 10,
|
||||||
|
|
@ -146,12 +156,33 @@ object RecordingNotificationHelper {
|
||||||
)
|
)
|
||||||
card.setOnClickPendingIntent(R.id.notif_icon, openApp)
|
card.setOnClickPendingIntent(R.id.notif_icon, openApp)
|
||||||
|
|
||||||
// Collapsed (shade row, lock screen): slim header-only card — the
|
// Collapsed (shade row, lock screen, heads-up): slim single row —
|
||||||
// lock shade clips anything taller. Expanded: full card with pills.
|
// the collapsed slot clips anything taller. Compact X / pause /
|
||||||
|
// check pills ride in the row so transport never needs expanding.
|
||||||
|
// Expanded: full card with big pills.
|
||||||
val small = RemoteViews(ctx.packageName, R.layout.notification_recording_small)
|
val small = RemoteViews(ctx.packageName, R.layout.notification_recording_small)
|
||||||
small.setTextViewText(R.id.notif_title_small, "Recording")
|
small.setTextViewText(R.id.notif_title_small, "Recording")
|
||||||
small.setTextViewText(R.id.notif_subtitle_small, fileName)
|
small.setTextViewText(R.id.notif_subtitle_small, fileName)
|
||||||
small.setTextViewText(R.id.notif_timer_small, elapsed)
|
small.setTextViewText(R.id.notif_timer_small, elapsed)
|
||||||
|
small.setImageViewResource(
|
||||||
|
R.id.notif_btn_pause_small,
|
||||||
|
if (paused) R.drawable.ic_notif_play else R.drawable.ic_notif_pause,
|
||||||
|
)
|
||||||
|
small.setOnClickPendingIntent(
|
||||||
|
R.id.notif_btn_discard_small,
|
||||||
|
svcIntent(ctx, RecordingService.ACTION_DISCARD_REQUEST, 31),
|
||||||
|
)
|
||||||
|
small.setOnClickPendingIntent(
|
||||||
|
R.id.notif_btn_pause_small,
|
||||||
|
svcIntent(
|
||||||
|
ctx,
|
||||||
|
if (paused) RecordingService.ACTION_RESUME else RecordingService.ACTION_PAUSE, 32,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
small.setOnClickPendingIntent(
|
||||||
|
R.id.notif_btn_save_small,
|
||||||
|
activitySaveIntent(ctx),
|
||||||
|
)
|
||||||
small.setOnClickPendingIntent(R.id.notif_small_root, openApp)
|
small.setOnClickPendingIntent(R.id.notif_small_root, openApp)
|
||||||
|
|
||||||
return NotificationCompat.Builder(ctx, CHANNEL_ID)
|
return NotificationCompat.Builder(ctx, CHANNEL_ID)
|
||||||
|
|
@ -177,17 +208,21 @@ object RecordingNotificationHelper {
|
||||||
// custom views — watches, Auto — get title/text + tap-to-open;
|
// custom views — watches, Auto — get title/text + tap-to-open;
|
||||||
// the confirm-delete card below keeps its No/Yes actions.)
|
// the confirm-delete card below keeps its No/Yes actions.)
|
||||||
// Slim header collapsed; full pills expanded. DecoratedCustomViewStyle
|
// Slim header collapsed; full pills expanded. DecoratedCustomViewStyle
|
||||||
// is REQUIRED: without it the system drops both custom views and
|
// is REQUIRED for the rich card: without it the system drops both
|
||||||
// the shade is stuck on the plain template (title/text/actions,
|
// custom views and the shade is stuck on the plain template
|
||||||
// expansion broken) — seen on targetSdk 35 / recent platform
|
// (title/text/actions, expansion broken) — seen on targetSdk 35 /
|
||||||
// versions. The header adds a small app icon next to the card's
|
// recent platform versions. The header adds a small app icon next
|
||||||
// own mic; that duplication is the price of the card rendering.
|
// to the card's own mic; that duplication is the price of the
|
||||||
.setCustomContentView(small)
|
// card rendering.
|
||||||
.setCustomBigContentView(card)
|
.apply {
|
||||||
// Heads-up uses this on the first post; without it the system
|
if (!rich) return@apply // plain first post: stock template only
|
||||||
// pops the plain template for ~5s until the shade row takes over.
|
setCustomContentView(small)
|
||||||
.setCustomHeadsUpContentView(small)
|
setCustomBigContentView(card)
|
||||||
.setStyle(NotificationCompat.DecoratedCustomViewStyle())
|
// Heads-up uses this on the first post; without it the system
|
||||||
|
// pops the plain template for ~5s until the shade row takes over.
|
||||||
|
setCustomHeadsUpContentView(small)
|
||||||
|
setStyle(NotificationCompat.DecoratedCustomViewStyle())
|
||||||
|
}
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -315,13 +315,15 @@ class RecordingService : Service() {
|
||||||
/**
|
/**
|
||||||
* Post a foreground notification immediately. Called at the top of
|
* Post a foreground notification immediately. Called at the top of
|
||||||
* onStartCommand so the banner appears instantly — no waiting for
|
* onStartCommand so the banner appears instantly — no waiting for
|
||||||
* MediaRecorder setup, file I/O, or DB access.
|
* MediaRecorder setup, file I/O, or DB access. Posts the PLAIN template
|
||||||
|
* first (custom RemoteViews first-bind stalls ~10s on some SystemUI
|
||||||
|
* builds); the 1s ticker swaps in the rich card.
|
||||||
*/
|
*/
|
||||||
private fun ensureForeground() {
|
private fun ensureForeground() {
|
||||||
try {
|
try {
|
||||||
val notif = RecordingNotificationHelper.build(
|
val notif = RecordingNotificationHelper.build(
|
||||||
this, RecordingSnapshot.Phase.RECORDING, 0L, displayFileName(),
|
this, RecordingSnapshot.Phase.RECORDING, 0L, displayFileName(),
|
||||||
alertOnce = false,
|
alertOnce = false, rich = false,
|
||||||
)
|
)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
startForeground(
|
startForeground(
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import com.shonar.settings.BuiltInSettings
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -47,6 +48,8 @@ import kotlinx.coroutines.withTimeoutOrNull
|
||||||
class StartRecordingActivity : Activity() {
|
class StartRecordingActivity : Activity() {
|
||||||
|
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
||||||
|
/** Set when the trampoline is TOP-resumed (uid counts as foreground). */
|
||||||
|
private val topResumed = kotlinx.coroutines.flow.MutableStateFlow(false)
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
@ -59,25 +62,33 @@ class StartRecordingActivity : Activity() {
|
||||||
finish()
|
finish()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Active session: mirror the in-app Stop button (save; the rename
|
|
||||||
// sheet opens on next app launch via RenamePendingHolder).
|
|
||||||
if (RecordingService.snapshot.value.phase != RecordingSnapshot.Phase.IDLE) {
|
|
||||||
RecordingService.command(this, RecordingService.ACTION_SAVE)
|
|
||||||
ShonarWidgetUpdater.refresh(this)
|
|
||||||
finish()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val app = application as ShonarApplication
|
val app = application as ShonarApplication
|
||||||
// Same consent gate as the in-app Record button. Bound the read
|
// Same consent gate as the in-app Record button. Bound the read
|
||||||
// so a wedged store degrades to opening the app, never a hang.
|
// so a wedged store degrades to opening the app, never a hang.
|
||||||
val consented = withTimeoutOrNull(CONSENT_WAIT_MS) {
|
val consentedDeferred = async {
|
||||||
runCatching {
|
withTimeoutOrNull(CONSENT_WAIT_MS) {
|
||||||
app.settingsManager.ensureLoaded()
|
runCatching {
|
||||||
app.settingsManager.bool(BuiltInSettings.CONSENT)
|
app.settingsManager.ensureLoaded()
|
||||||
}.getOrDefault(false)
|
app.settingsManager.bool(BuiltInSettings.CONSENT)
|
||||||
} == true
|
}.getOrDefault(false)
|
||||||
if (!consented) {
|
} == true
|
||||||
|
}
|
||||||
|
// Wait until TOP-resumed so the START/SAVE call below is
|
||||||
|
// foreground-originated. Sent from onCreate, the system stamps
|
||||||
|
// the intent FROM_BACKGROUND (activity not yet resumed) and
|
||||||
|
// defers the notification's interruption ~10s — traced
|
||||||
|
// on-device: FGS allowed +0.1s, first buzz +10.1s, every session.
|
||||||
|
// Bound: a missed resume signal must never strand the activity.
|
||||||
|
withTimeoutOrNull(RESUME_WAIT_MS) { topResumed.first { it } }
|
||||||
|
val consented = withTimeoutOrNull(CONSENT_WAIT_MS) { consentedDeferred.await() } == true
|
||||||
|
// Active session: mirror the in-app Stop button (save; the rename
|
||||||
|
// sheet opens on next app launch via RenamePendingHolder).
|
||||||
|
if (RecordingService.snapshot.value.phase != RecordingSnapshot.Phase.IDLE) {
|
||||||
|
RecordingService.command(
|
||||||
|
this@StartRecordingActivity, RecordingService.ACTION_SAVE,
|
||||||
|
)
|
||||||
|
} else if (!consented) {
|
||||||
startActivity(
|
startActivity(
|
||||||
Intent(this@StartRecordingActivity, MainActivity::class.java)
|
Intent(this@StartRecordingActivity, MainActivity::class.java)
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||||
|
|
@ -100,6 +111,18 @@ class StartRecordingActivity : Activity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
// Pre-29 has no top-resumed signal (and no background-FGS
|
||||||
|
// restriction); onResume is sufficient there.
|
||||||
|
if (android.os.Build.VERSION.SDK_INT < 29) topResumed.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTopResumedActivityChanged(isTopResumed: Boolean) {
|
||||||
|
super.onTopResumedActivityChanged(isTopResumed)
|
||||||
|
if (isTopResumed) topResumed.value = true
|
||||||
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
// singleInstance re-tap while a waiter is pending: the first waiter
|
// singleInstance re-tap while a waiter is pending: the first waiter
|
||||||
|
|
@ -115,5 +138,6 @@ class StartRecordingActivity : Activity() {
|
||||||
companion object {
|
companion object {
|
||||||
private const val TRAMPOLINE_WAIT_MS = 2_500L
|
private const val TRAMPOLINE_WAIT_MS = 2_500L
|
||||||
private const val CONSENT_WAIT_MS = 2_000L
|
private const val CONSENT_WAIT_MS = 2_000L
|
||||||
|
private const val RESUME_WAIT_MS = 2_000L
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,18 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Collapsed / lock-screen: slim single row (icon | titles | timer).
|
<!-- Collapsed / lock-screen / heads-up: slim single row (icon | titles |
|
||||||
No pills here — the lock shade constrains compact height and clips
|
timer | compact X / pause / check). One row only — the collapsed slot
|
||||||
anything taller. Full X / pause / check pills live in the expanded
|
clips anything taller — so the transport controls are always one tap
|
||||||
big card. Tapping the row opens the app. RemoteViews-safe views only. -->
|
away with no expanding needed. Full-size pills live in the expanded
|
||||||
|
big card. Tapping empty row space opens the app. RemoteViews-safe
|
||||||
|
views only. -->
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:id="@+id/notif_small_root"
|
android:id="@+id/notif_small_root"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:paddingTop="6dp"
|
android:paddingTop="4dp"
|
||||||
android:paddingBottom="6dp"
|
android:paddingBottom="4dp"
|
||||||
android:paddingStart="4dp"
|
android:paddingStart="4dp"
|
||||||
android:paddingEnd="4dp">
|
android:paddingEnd="4dp">
|
||||||
|
|
||||||
|
|
@ -34,7 +36,7 @@
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:text="Recording"
|
android:text="Recording"
|
||||||
android:textColor="@color/notif_text"
|
android:textColor="@color/notif_text"
|
||||||
android:textSize="14sp"
|
android:textSize="13sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
|
@ -45,7 +47,7 @@
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:text="Shonar"
|
android:text="Shonar"
|
||||||
android:textColor="@color/notif_text"
|
android:textColor="@color/notif_text"
|
||||||
android:textSize="12sp" />
|
android:textSize="11sp" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
|
@ -56,6 +58,36 @@
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:text="00:00"
|
android:text="00:00"
|
||||||
android:textColor="@color/notif_text"
|
android:textColor="@color/notif_text"
|
||||||
android:textSize="16sp"
|
android:textSize="14sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/notif_btn_discard_small"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:background="@drawable/notif_pill"
|
||||||
|
android:contentDescription="Discard"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:src="@drawable/ic_notif_close" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/notif_btn_pause_small"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_marginStart="6dp"
|
||||||
|
android:background="@drawable/notif_pill_accent"
|
||||||
|
android:contentDescription="Pause"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:src="@drawable/ic_notif_pause" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/notif_btn_save_small"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_marginStart="6dp"
|
||||||
|
android:background="@drawable/notif_pill"
|
||||||
|
android:contentDescription="Save"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:src="@drawable/ic_notif_check" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
|
||||||
|
|
@ -81,10 +81,20 @@ class RecordingNotificationLayoutTest {
|
||||||
for (id in listOf("notif_title_small", "notif_subtitle_small", "notif_timer_small")) {
|
for (id in listOf("notif_title_small", "notif_subtitle_small", "notif_timer_small")) {
|
||||||
assertTrue("collapsed layout missing @$id", xml.contains("@+id/$id"))
|
assertTrue("collapsed layout missing @$id", xml.contains("@+id/$id"))
|
||||||
}
|
}
|
||||||
assertTrue(
|
// User-requested 2026-09-18: collapsed row carries compact transport
|
||||||
"collapsed card must stay pill-free (lock shade clips taller cards)",
|
// pills (single row — the collapsed slot clips anything taller) so
|
||||||
"notif_btn_discard_small" !in xml && "notif_btn_pause_small" !in xml,
|
// recording controls never need expanding. Big-card ids must stay
|
||||||
)
|
// out of the small layout (exact-match: no substring confusion with
|
||||||
|
// the _small ids).
|
||||||
|
for (id in listOf("notif_btn_discard_small", "notif_btn_pause_small", "notif_btn_save_small")) {
|
||||||
|
assertTrue("collapsed layout missing @$id", xml.contains("@+id/$id"))
|
||||||
|
}
|
||||||
|
for (id in listOf("notif_btn_discard\"", "notif_btn_pause\"", "notif_btn_save\"")) {
|
||||||
|
assertTrue("small layout must not contain big-card @$id", !xml.contains("@+id/$id"))
|
||||||
|
}
|
||||||
|
assertTrue("must wire small pause button", src.contains("R.id.notif_btn_pause_small"))
|
||||||
|
assertTrue("must wire small save button", src.contains("R.id.notif_btn_save_small"))
|
||||||
|
assertTrue("must wire small discard button", src.contains("R.id.notif_btn_discard_small"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun startPath_postsForegroundNotificationOnlyOnce() {
|
@Test fun startPath_postsForegroundNotificationOnlyOnce() {
|
||||||
|
|
@ -143,6 +153,28 @@ class RecordingNotificationLayoutTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test fun firstPost_isPlainTemplateForInstantDisplay() {
|
||||||
|
// Custom RemoteViews first-bind stalls ~10s before anything displays
|
||||||
|
// on some SystemUI builds (traced on-device 2026-09-18), while plain
|
||||||
|
// stock-template posts display instantly. The startForeground banner
|
||||||
|
// must go out plain; the 1s ticker swaps in the rich card.
|
||||||
|
val src = helperSource()
|
||||||
|
assertTrue(
|
||||||
|
"build() must offer the plain-first-post flag",
|
||||||
|
src.contains("rich: Boolean"),
|
||||||
|
)
|
||||||
|
assertTrue(
|
||||||
|
"plain post must skip all three custom views",
|
||||||
|
src.contains("if (!rich)"),
|
||||||
|
)
|
||||||
|
val f = moduleDir().resolve("src/main/java/com/shonar/recording/RecordingService.kt")
|
||||||
|
assertTrue("service source missing: $f", f.exists())
|
||||||
|
assertTrue(
|
||||||
|
"startForeground banner must request the plain post",
|
||||||
|
f.readText().contains("rich = false"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test fun lockscreen_showsSimplifiedPublicVersion() {
|
@Test fun lockscreen_showsSimplifiedPublicVersion() {
|
||||||
// Secure lockscreen shows the single-icon template row instead of
|
// Secure lockscreen shows the single-icon template row instead of
|
||||||
// the full card (no doubled icons, no pills behind the lock).
|
// the full card (no doubled icons, no pills behind the lock).
|
||||||
|
|
|
||||||
1
android/local.properties
Normal file
1
android/local.properties
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
sdk.dir=/home/avi/.local/android-sdk
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue