feat: add four more service-icon options + on-device preview harness

Add four additional always-on service notification icon candidates, each
keeping the centred brand gem with a "background service / connected to
servers" motif around it:

- amethyst_service3: orbit ring with three server nodes
- amethyst_service4: two looping sync arrows (running)
- amethyst_service5: hub & spoke to five nodes (connected to relays)
- amethyst_service6: concentric broadcast waves

To compare all candidates on a real device, add a DEBUG-only helper
(ServiceIconPreviewNotifications) that posts one always-on-style ongoing
notification per icon (same channel style, ongoing/silent/low priority),
triggered from MainActivity.onCreate under BuildConfig.DEBUG.

NOTE: the preview harness (ServiceIconPreviewNotifications + the
MainActivity hook) is temporary scaffolding for the icon bake-off and is
meant to be removed once a design is chosen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kvjst6qVHuQ8rKD3LtvTzi
This commit is contained in:
Claude
2026-06-26 15:33:15 +00:00
parent bacb8a57ad
commit e1c152ada8
6 changed files with 304 additions and 0 deletions
@@ -0,0 +1,108 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service.notifications
// =====================================================================================
// TEMPORARY / DEBUG-ONLY — REMOVE BEFORE MERGE.
//
// Posts one always-on-style notification per candidate small-icon so the different
// "service icon" designs can be compared side by side in the status bar / shade on a
// real device. These mimic the real NotificationRelayService notification (same
// channel, ongoing, silent, low priority) but each uses a different drawable and a
// label naming the design.
//
// To remove the whole experiment later: delete this file, the four extra drawables
// (amethyst_service2..6), and the single ServiceIconPreviewNotifications.postAll(...)
// call in MainActivity.onCreate.
// =====================================================================================
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import androidx.annotation.DrawableRes
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.vitorpamplona.amethyst.R
object ServiceIconPreviewNotifications {
private const val CHANNEL_ID = "service_icon_preview"
private const val BASE_ID = 920_000
private data class Candidate(
@DrawableRes val icon: Int,
val label: String,
)
private val candidates =
listOf(
Candidate(R.drawable.amethyst_service, "1 · Hollow gem outline (current)"),
Candidate(R.drawable.amethyst_service2, "2 · Solid circle, gem cut-out"),
Candidate(R.drawable.amethyst_service3, "3 · Orbit + server nodes"),
Candidate(R.drawable.amethyst_service4, "4 · Sync arrows (running)"),
Candidate(R.drawable.amethyst_service5, "5 · Hub & spoke (connected)"),
Candidate(R.drawable.amethyst_service6, "6 · Broadcast waves"),
)
fun postAll(context: Context) {
val manager = NotificationManagerCompat.from(context)
if (!manager.areNotificationsEnabled()) return
ensureChannel(context)
candidates.forEachIndexed { index, candidate ->
val notification =
NotificationCompat
.Builder(context, CHANNEL_ID)
.setContentTitle("Service icon ${index + 1}")
.setContentText(candidate.label)
.setSmallIcon(candidate.icon)
.setOngoing(true)
.setSilent(true)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.build()
manager.notify(BASE_ID + index, notification)
}
}
fun clearAll(context: Context) {
val manager = NotificationManagerCompat.from(context)
candidates.indices.forEach { manager.cancel(BASE_ID + it) }
}
private fun ensureChannel(context: Context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val channel =
NotificationChannel(
CHANNEL_ID,
"Service icon preview (debug)",
NotificationManager.IMPORTANCE_LOW,
).apply {
description = "Temporary side-by-side preview of always-on service icons"
setShowBadge(false)
}
val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}
@@ -27,11 +27,13 @@ import androidx.activity.enableEdgeToEdge
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.debugState
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService
import com.vitorpamplona.amethyst.service.notifications.NotificationRelayService
import com.vitorpamplona.amethyst.service.notifications.ServiceIconPreviewNotifications
import com.vitorpamplona.amethyst.service.playback.composable.DEFAULT_MUTED_SETTING
import com.vitorpamplona.amethyst.service.playback.pip.BackgroundMedia
import com.vitorpamplona.amethyst.ui.navigation.findParameterValue
@@ -78,6 +80,13 @@ class MainActivity : AppCompatActivity() {
Log.d("ActivityLifecycle") { "MainActivity.onCreate $this" }
// TEMPORARY / DEBUG-ONLY — REMOVE BEFORE MERGE.
// Posts side-by-side preview notifications for the candidate always-on
// service icons so they can be compared on a real device.
if (BuildConfig.DEBUG) {
ServiceIconPreviewNotifications.postAll(this)
}
setContent {
StringResSetup()
AmethystTheme {
@@ -0,0 +1,30 @@
<!--
~ Status-bar icon (option) for the always-on relay-connection service.
~ Centred brand gem orbited by a ring of three "server" nodes, signalling a
~ background service quietly connected to a network of relays.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="512dp"
android:height="512dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#000000"
android:fillType="evenOdd"
android:pathData="M341.914,305.892C341.712,305.278,294.605,193.111,281.649,162c-15.775,0,-49.086,0,-72.585,0c-6.896,17.055,-24.698,60.526,-38.978,95.545c7.827,18.67,14.855,35.442,20.275,48.38l29.555,0a2.02,2.02 0,0,0,1.888,-2.373c-0.254,-5.968,-16.499,-27.66,-11.653,-51.125c1.292,-5.048,2.281,-10.178,3.694,-15.208a177.491,177.491 135,0,1,13.571,-35.141c0.898,-1.707,1.314,-2.473,3.382,-1.181c4.412,2.745,9.339,2.587,14.248,2.23c4.232,-0.92,6.292,-2.947,14.045,-0.887c1.534,0,3.028,0,4.596,0.14c3.727,0.294,7.219,1.181,9.46,4.555c2.443,3.687,2.837,7.856,2.443,12.117c-0.596,6.373,-0.173,12.308,5.221,16.801a58.083,58.083 0,0,0,6.06,4.04c2.675,1.718,5.906,2.465,8.198,4.846c1.354,1.402,2.153,2.9,1.564,4.898c-0.585,1.998,-2.153,2.94,-4.21,3.161c-5.21,0.567,-10.299,-0.493,-15.418,-1.111c-0.666,-0.081,-1.321,-0.132,-2.02,-0.173a11.583,11.583 0,0,1,-3.849,0.132l-1.203,0a26.961,26.961 0,0,0,-5.957,1.424c-4.828,1.759,-9.221,3.694,-14.914,3.595a4.364,4.364 0,0,0,-2.072,0.898c-4.463,3.967,-7.15,8.978,-4.916,16.297c6.119,17.096,21.82,58.337,28.569,76.139c-0.191,-12.058,-0.243,-33.647,-0.283,-44.126c26.942,0.04,68.25,0.018,71.543,0.018ZM251.093,219.689a5.696,5.795 90,1,0,11.591,0a5.696,5.795 90,1,0,-11.591,0Z" />
<path
android:strokeColor="#000000"
android:strokeWidth="16"
android:fillColor="#00000000"
android:strokeLineJoin="round"
android:pathData="M256,48A208,208 0,1,1 256,464A208,208 0,1,1 256,48Z" />
<path
android:fillColor="#000000"
android:pathData="M256,22A26,26 0,1,1 256,74A26,26 0,1,1 256,22Z" />
<path
android:fillColor="#000000"
android:pathData="M75.87,334A26,26 0,1,1 75.87,386A26,26 0,1,1 75.87,334Z" />
<path
android:fillColor="#000000"
android:pathData="M436.13,334A26,26 0,1,1 436.13,386A26,26 0,1,1 436.13,334Z" />
</vector>
@@ -0,0 +1,35 @@
<!--
~ Status-bar icon (option) for the always-on relay-connection service.
~ Centred brand gem encircled by two looping sync arrows, signalling a
~ background service that keeps continuously running and refreshing.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="512dp"
android:height="512dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#000000"
android:fillType="evenOdd"
android:pathData="M341.914,305.892C341.712,305.278,294.605,193.111,281.649,162c-15.775,0,-49.086,0,-72.585,0c-6.896,17.055,-24.698,60.526,-38.978,95.545c7.827,18.67,14.855,35.442,20.275,48.38l29.555,0a2.02,2.02 0,0,0,1.888,-2.373c-0.254,-5.968,-16.499,-27.66,-11.653,-51.125c1.292,-5.048,2.281,-10.178,3.694,-15.208a177.491,177.491 135,0,1,13.571,-35.141c0.898,-1.707,1.314,-2.473,3.382,-1.181c4.412,2.745,9.339,2.587,14.248,2.23c4.232,-0.92,6.292,-2.947,14.045,-0.887c1.534,0,3.028,0,4.596,0.14c3.727,0.294,7.219,1.181,9.46,4.555c2.443,3.687,2.837,7.856,2.443,12.117c-0.596,6.373,-0.173,12.308,5.221,16.801a58.083,58.083 0,0,0,6.06,4.04c2.675,1.718,5.906,2.465,8.198,4.846c1.354,1.402,2.153,2.9,1.564,4.898c-0.585,1.998,-2.153,2.94,-4.21,3.161c-5.21,0.567,-10.299,-0.493,-15.418,-1.111c-0.666,-0.081,-1.321,-0.132,-2.02,-0.173a11.583,11.583 0,0,1,-3.849,0.132l-1.203,0a26.961,26.961 0,0,0,-5.957,1.424c-4.828,1.759,-9.221,3.694,-14.914,3.595a4.364,4.364 0,0,0,-2.072,0.898c-4.463,3.967,-7.15,8.978,-4.916,16.297c6.119,17.096,21.82,58.337,28.569,76.139c-0.191,-12.058,-0.243,-33.647,-0.283,-44.126c26.942,0.04,68.25,0.018,71.543,0.018ZM251.093,219.689a5.696,5.795 90,1,0,11.591,0a5.696,5.795 90,1,0,-11.591,0Z" />
<path
android:strokeColor="#000000"
android:strokeWidth="18"
android:fillColor="#00000000"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M291.6,54.11A205,205 0,0,1,291.6,457.89" />
<path
android:strokeColor="#000000"
android:strokeWidth="18"
android:fillColor="#00000000"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M220.4,457.89A205,205 0,0,1,220.4,54.11" />
<path
android:fillColor="#000000"
android:pathData="M265.7,460.15L309.98,439.5L312.9,472.8Z" />
<path
android:fillColor="#000000"
android:pathData="M246.3,51.85L202.02,72.5L199.1,39.2Z" />
</vector>
@@ -0,0 +1,65 @@
<!--
~ Status-bar icon (option) for the always-on relay-connection service.
~ Centred brand gem as a hub with spokes out to five "server" nodes,
~ symbolising a background service connected to multiple relays.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="512dp"
android:height="512dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#000000"
android:fillType="evenOdd"
android:pathData="M333.688,301.115C333.505,300.56,290.909,199.133,279.193,171c-14.264,0,-44.387,0,-65.635,0c-6.235,15.422,-22.333,54.731,-35.246,86.397c7.077,16.883,13.432,32.049,18.334,43.748l26.725,0a1.827,1.827 0,0,0,1.707,-2.146c-0.23,-5.397,-14.92,-25.012,-10.538,-46.23c1.168,-4.565,2.063,-9.203,3.341,-13.752a160.497,160.497 135,0,1,12.271,-31.776c0.812,-1.544,1.188,-2.236,3.058,-1.068c3.989,2.482,8.445,2.339,12.883,2.016c3.826,-0.832,5.69,-2.665,12.7,-0.802c1.387,0,2.738,0,4.156,0.126c3.371,0.266,6.528,1.068,8.555,4.119c2.209,3.334,2.565,7.104,2.209,10.957c-0.539,5.763,-0.156,11.13,4.721,15.193a52.522,52.522 0,0,0,5.48,3.653c2.419,1.554,5.34,2.229,7.413,4.382c1.224,1.268,1.946,2.622,1.414,4.429c-0.529,1.807,-1.946,2.659,-3.806,2.858c-4.712,0.512,-9.313,-0.446,-13.942,-1.005c-0.602,-0.073,-1.195,-0.12,-1.827,-0.156a10.474,10.474 0,0,1,-3.48,0.12l-1.088,0a24.379,24.379 0,0,0,-5.387,1.288c-4.365,1.59,-8.338,3.341,-13.486,3.251a3.946,3.946 0,0,0,-1.873,0.812c-4.036,3.587,-6.465,8.119,-4.445,14.737c5.533,15.459,19.731,52.752,25.833,68.849c-0.173,-10.904,-0.22,-30.425,-0.256,-39.901c24.363,0.037,61.715,0.017,64.693,0.017ZM251.563,223.166a5.151,5.241 90,1,0,10.481,0a5.151,5.241 90,1,0,-10.481,0Z" />
<path
android:strokeColor="#000000"
android:strokeWidth="12"
android:fillColor="#00000000"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M256,138L256,46" />
<path
android:strokeColor="#000000"
android:strokeWidth="12"
android:fillColor="#00000000"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M368.22,219.54L455.72,191.11" />
<path
android:strokeColor="#000000"
android:strokeWidth="12"
android:fillColor="#00000000"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M325.36,351.46L379.43,425.89" />
<path
android:strokeColor="#000000"
android:strokeWidth="12"
android:fillColor="#00000000"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M186.64,351.46L132.57,425.89" />
<path
android:strokeColor="#000000"
android:strokeWidth="12"
android:fillColor="#00000000"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M143.78,219.54L56.28,191.11" />
<path
android:fillColor="#000000"
android:pathData="M256,22A24,24 0,1,1 256,70A24,24 0,1,1 256,22Z" />
<path
android:fillColor="#000000"
android:pathData="M455.72,167.11A24,24 0,1,1 455.72,215.11A24,24 0,1,1 455.72,167.11Z" />
<path
android:fillColor="#000000"
android:pathData="M379.43,401.89A24,24 0,1,1 379.43,449.89A24,24 0,1,1 379.43,401.89Z" />
<path
android:fillColor="#000000"
android:pathData="M132.57,401.89A24,24 0,1,1 132.57,449.89A24,24 0,1,1 132.57,401.89Z" />
<path
android:fillColor="#000000"
android:pathData="M56.28,167.11A24,24 0,1,1 56.28,215.11A24,24 0,1,1 56.28,167.11Z" />
</vector>
@@ -0,0 +1,57 @@
<!--
~ Status-bar icon (option) for the always-on relay-connection service.
~ Centred brand gem flanked by concentric signal waves, evoking a service
~ that is continuously transmitting to and receiving from relays.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="512dp"
android:height="512dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#000000"
android:fillType="evenOdd"
android:pathData="M324.548,295.808C324.387,295.317,286.802,205.823,276.465,181c-12.586,0,-39.165,0,-57.913,0c-5.502,13.608,-19.706,48.292,-31.1,76.233c6.245,14.897,11.852,28.278,16.177,38.601l23.581,0a1.612,1.612 0,0,0,1.506,-1.894c-0.203,-4.762,-13.164,-22.069,-9.298,-40.791c1.03,-4.028,1.82,-8.121,2.948,-12.134a141.615,141.615 135,0,1,10.828,-28.038c0.716,-1.362,1.048,-1.973,2.698,-0.942c3.52,2.19,7.451,2.064,11.368,1.779c3.376,-0.734,5.02,-2.352,11.206,-0.708c1.224,0,2.416,0,3.667,0.112c2.974,0.235,5.76,0.942,7.548,3.635c1.949,2.942,2.264,6.268,1.949,9.668c-0.476,5.085,-0.138,9.821,4.166,13.405a46.343,46.343 0,0,0,4.835,3.224c2.134,1.371,4.712,1.967,6.541,3.867c1.08,1.119,1.717,2.313,1.248,3.908c-0.467,1.594,-1.717,2.346,-3.359,2.522c-4.157,0.452,-8.218,-0.393,-12.301,-0.887c-0.531,-0.065,-1.054,-0.106,-1.612,-0.138a9.242,9.242 0,0,1,-3.071,0.106l-0.96,0a21.511,21.511 0,0,0,-4.753,1.136c-3.852,1.403,-7.357,2.948,-11.899,2.868a3.482,3.482 0,0,0,-1.653,0.716c-3.561,3.165,-5.704,7.164,-3.922,13.003c4.882,13.64,17.41,46.545,22.794,60.749c-0.153,-9.621,-0.194,-26.846,-0.226,-35.207c21.497,0.032,54.455,0.015,57.082,0.015ZM252.085,227.029a4.545,4.624 90,1,0,9.248,0a4.545,4.624 90,1,0,-9.248,0Z" />
<path
android:strokeColor="#000000"
android:strokeWidth="18"
android:fillColor="#00000000"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M141.09,159.58A150,150 0,0,1,370.91,159.58" />
<path
android:strokeColor="#000000"
android:strokeWidth="18"
android:fillColor="#00000000"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M370.91,352.42A150,150 0,0,1,141.09,352.42" />
<path
android:strokeColor="#000000"
android:strokeWidth="18"
android:fillColor="#00000000"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M106.62,130.66A195,195 0,0,1,405.38,130.66" />
<path
android:strokeColor="#000000"
android:strokeWidth="18"
android:fillColor="#00000000"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M405.38,381.34A195,195 0,0,1,106.62,381.34" />
<path
android:strokeColor="#000000"
android:strokeWidth="18"
android:fillColor="#00000000"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M72.15,101.73A240,240 0,0,1,439.85,101.73" />
<path
android:strokeColor="#000000"
android:strokeWidth="18"
android:fillColor="#00000000"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M439.85,410.27A240,240 0,0,1,72.15,410.27" />
</vector>