remove Samsung Face Widget (signature-restricted, unusable by third-party apps)

Samsung's FACE_WIDGET permission requires platform signing, making it
impossible for non-Samsung apps to register lock screen face widgets.
The existing AppWidgetProvider already works via Good Lock LockStar and
will be natively supported on lock screens with One UI 8 / Android 16 QPR1.
This commit is contained in:
2026-02-23 20:57:29 -06:00
parent 65303df96a
commit f6ec5ba5de
8 changed files with 0 additions and 538 deletions

View File

@@ -2,8 +2,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="com.samsung.systemui.permission.FACE_WIDGET" />
<!-- AndroidTV support -->
<uses-feature android:name="android.software.leanback" android:required="false" />
@@ -100,20 +98,6 @@
android:permission="android.permission.BIND_REMOTEVIEWS"
android:exported="false" />
<!-- Samsung Lock Screen Face Widget -->
<receiver
android:name=".LockScreenWidgetReceiver"
android:permission="com.samsung.systemui.permission.FACE_WIDGET"
android:exported="true">
<intent-filter>
<action android:name="com.samsung.android.intent.action.REQUEST_SERVICEBOX_REMOTEVIEWS" />
</intent-filter>
</receiver>
<meta-data
android:name="com.samsung.systemui.facewidget.executable"
android:resource="@raw/facewidgets" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"

View File

@@ -1,238 +0,0 @@
package com.agentui.desktop
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
import android.view.View
import android.widget.RemoteViews
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONObject
import java.util.concurrent.TimeUnit
/**
* Samsung Face Widget receiver for the lock screen / AOD.
*
* Samsung's proprietary system sends REQUEST_SERVICEBOX_REMOTEVIEWS
* and expects RESPONSE_SERVICEBOX_REMOTEVIEWS back with RemoteViews
* for both the lock screen ("origin") and AOD ("aod").
*/
class LockScreenWidgetReceiver : BroadcastReceiver() {
companion object {
private const val TAG = "AgentUI.FaceWidget"
private const val ACTION_REQUEST =
"com.samsung.android.intent.action.REQUEST_SERVICEBOX_REMOTEVIEWS"
private const val ACTION_RESPONSE =
"com.samsung.android.intent.action.RESPONSE_SERVICEBOX_REMOTEVIEWS"
private const val PAGE_ID = "agent_ui_transcript"
private val STATUS_COLORS = mapOf(
"idle" to 0xFF6b7280.toInt(),
"thinking" to 0xFF60a5fa.toInt(),
"reading" to 0xFF22d3ee.toInt(),
"writing" to 0xFF4ade80.toInt(),
"toolUse" to 0xFFfbbf24.toInt(),
"permissionRequest" to 0xFFfb923c.toInt(),
"interrupted" to 0xFFf87171.toInt(),
"error" to 0xFFf87171.toInt(),
"sessionStart" to 0xFF60a5fa.toInt(),
"sessionEnd" to 0xFF6b7280.toInt()
)
// Dimmed versions for AOD
private val AOD_STATUS_COLORS = mapOf(
"idle" to 0xFF3b3f47.toInt(),
"thinking" to 0xFF304f7a.toInt(),
"reading" to 0xFF116670.toInt(),
"writing" to 0xFF256b40.toInt(),
"toolUse" to 0xFF7a5f12.toInt(),
"permissionRequest" to 0xFF7a491e.toInt(),
"interrupted" to 0xFF7a3838.toInt(),
"error" to 0xFF7a3838.toInt(),
"sessionStart" to 0xFF304f7a.toInt(),
"sessionEnd" to 0xFF3b3f47.toInt()
)
private val client = OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(5, TimeUnit.SECONDS)
.build()
}
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != ACTION_REQUEST) return
val pageId = intent.getStringExtra("pageId")
if (pageId != PAGE_ID) return
Log.d(TAG, "Face widget update requested for pageId=$pageId")
// Fetch data on a background thread to avoid ANR
val pendingResult = goAsync()
Thread {
try {
val terminals = fetchTerminals(context)
val lockViews = buildLockScreenViews(context, terminals)
val aodViews = buildAodViews(context, terminals)
val response = Intent(ACTION_RESPONSE).apply {
setPackage("com.android.systemui")
putExtra("package", context.packageName)
putExtra("pageId", PAGE_ID)
putExtra("show", true)
putExtra("origin", lockViews)
putExtra("aod", aodViews)
}
context.sendBroadcast(response)
Log.d(TAG, "Face widget response sent with ${terminals.size} terminals")
} catch (e: Exception) {
Log.e(TAG, "Failed to build face widget", e)
// Send empty response so widget doesn't get stuck
val response = Intent(ACTION_RESPONSE).apply {
setPackage("com.android.systemui")
putExtra("package", context.packageName)
putExtra("pageId", PAGE_ID)
putExtra("show", true)
putExtra("origin", buildEmptyLockScreenViews(context))
putExtra("aod", buildEmptyAodViews(context))
}
context.sendBroadcast(response)
} finally {
pendingResult.finish()
}
}.start()
}
private fun buildLockScreenViews(
context: Context,
terminals: List<FaceWidgetTerminal>
): RemoteViews {
val views = RemoteViews(context.packageName, R.layout.face_widget_lockscreen)
val statusText = if (terminals.isEmpty()) "offline"
else "${terminals.size} agent${if (terminals.size > 1) "s" else ""}"
views.setTextViewText(R.id.fw_status, statusText)
if (terminals.isEmpty()) {
views.setViewVisibility(R.id.fw_empty, View.VISIBLE)
} else {
views.setViewVisibility(R.id.fw_empty, View.GONE)
}
// Lockscreen terminal slot IDs
val slotIds = listOf(
Triple(R.id.fw_terminal_1, R.id.fw_dot_1, R.id.fw_name_1),
Triple(R.id.fw_terminal_2, R.id.fw_dot_2, R.id.fw_name_2),
Triple(R.id.fw_terminal_3, R.id.fw_dot_3, R.id.fw_name_3)
)
for (i in slotIds.indices) {
val (container, dot, name) = slotIds[i]
if (i < terminals.size) {
val t = terminals[i]
views.setViewVisibility(container, View.VISIBLE)
views.setTextColor(dot, t.statusColor)
views.setTextViewText(name, "T${t.index} ${t.agent} ${t.status}")
} else {
views.setViewVisibility(container, View.GONE)
}
}
return views
}
private fun buildAodViews(
context: Context,
terminals: List<FaceWidgetTerminal>
): RemoteViews {
val views = RemoteViews(context.packageName, R.layout.face_widget_aod)
if (terminals.isEmpty()) {
views.setViewVisibility(R.id.fw_aod_empty, View.VISIBLE)
} else {
views.setViewVisibility(R.id.fw_aod_empty, View.GONE)
}
val slotIds = listOf(
Triple(R.id.fw_aod_terminal_1, R.id.fw_aod_dot_1, R.id.fw_aod_name_1),
Triple(R.id.fw_aod_terminal_2, R.id.fw_aod_dot_2, R.id.fw_aod_name_2),
Triple(R.id.fw_aod_terminal_3, R.id.fw_aod_dot_3, R.id.fw_aod_name_3)
)
for (i in slotIds.indices) {
val (container, dot, name) = slotIds[i]
if (i < terminals.size) {
val t = terminals[i]
views.setViewVisibility(container, View.VISIBLE)
val aodColor = AOD_STATUS_COLORS[t.status] ?: AOD_STATUS_COLORS["idle"]!!
views.setTextColor(dot, aodColor)
views.setTextViewText(name, "T${t.index} ${t.agent}")
} else {
views.setViewVisibility(container, View.GONE)
}
}
return views
}
private fun buildEmptyLockScreenViews(context: Context): RemoteViews {
return RemoteViews(context.packageName, R.layout.face_widget_lockscreen)
}
private fun buildEmptyAodViews(context: Context): RemoteViews {
return RemoteViews(context.packageName, R.layout.face_widget_aod)
}
private fun fetchTerminals(context: Context): List<FaceWidgetTerminal> {
val apiBase = ServerConfig.apiBaseUrl(context) ?: return emptyList()
try {
val url = "$apiBase/session-state"
val req = Request.Builder().url(url).build()
val resp = client.newCall(req).execute()
if (!resp.isSuccessful) return emptyList()
val json = JSONObject(resp.body?.string() ?: "{}")
val registry = json.optJSONArray("registry") ?: return emptyList()
val agents = json.optJSONObject("agents")
val result = mutableListOf<FaceWidgetTerminal>()
for (i in 0 until minOf(registry.length(), 3)) { // Max 3 for face widget
val entry = registry.getJSONObject(i)
val agentName = entry.optString("agent", "")
val alive = entry.optBoolean("alive", false)
val agentState = agents?.optJSONObject(agentName)
val status = agentState?.optString("status", if (alive) "idle" else "closed")
?: if (alive) "idle" else "closed"
val statusColor = STATUS_COLORS[status] ?: STATUS_COLORS["idle"]!!
result.add(
FaceWidgetTerminal(
index = i + 1,
agent = agentName,
status = status,
statusColor = statusColor
)
)
}
return result
} catch (e: Exception) {
Log.w(TAG, "Failed to fetch terminals for face widget", e)
return emptyList()
}
}
private data class FaceWidgetTerminal(
val index: Int,
val agent: String,
val status: String,
val statusColor: Int
)
}

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#ff171717" />
<corners android:radius="22dp" />
</shape>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#99010101" />
<corners android:radius="22dp" />
</shape>

View File

@@ -1,124 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="12dp"
android:background="@drawable/face_widget_bg_aod">
<!-- Minimal AOD layout: title + terminal count -->
<TextView
android:id="@+id/fw_aod_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Agent UI"
android:textColor="#555588"
android:textSize="11sp"
android:fontFamily="monospace"
android:paddingBottom="4dp" />
<!-- Terminal 1 -->
<LinearLayout
android:id="@+id/fw_aod_terminal_1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingTop="1dp"
android:paddingBottom="1dp"
android:visibility="gone">
<TextView
android:id="@+id/fw_aod_dot_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="\u25CF"
android:textColor="#444466"
android:textSize="7sp"
android:paddingEnd="4dp" />
<TextView
android:id="@+id/fw_aod_name_1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#777777"
android:textSize="9sp"
android:fontFamily="monospace"
android:maxLines="1"
android:ellipsize="end" />
</LinearLayout>
<!-- Terminal 2 -->
<LinearLayout
android:id="@+id/fw_aod_terminal_2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingTop="1dp"
android:paddingBottom="1dp"
android:visibility="gone">
<TextView
android:id="@+id/fw_aod_dot_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="\u25CF"
android:textColor="#444466"
android:textSize="7sp"
android:paddingEnd="4dp" />
<TextView
android:id="@+id/fw_aod_name_2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#777777"
android:textSize="9sp"
android:fontFamily="monospace"
android:maxLines="1"
android:ellipsize="end" />
</LinearLayout>
<!-- Terminal 3 -->
<LinearLayout
android:id="@+id/fw_aod_terminal_3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingTop="1dp"
android:paddingBottom="1dp"
android:visibility="gone">
<TextView
android:id="@+id/fw_aod_dot_3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="\u25CF"
android:textColor="#444466"
android:textSize="7sp"
android:paddingEnd="4dp" />
<TextView
android:id="@+id/fw_aod_name_3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#777777"
android:textSize="9sp"
android:fontFamily="monospace"
android:maxLines="1"
android:ellipsize="end" />
</LinearLayout>
<!-- Empty state -->
<TextView
android:id="@+id/fw_aod_empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="No agents"
android:textColor="#444444"
android:textSize="9sp"
android:fontFamily="monospace"
android:gravity="center" />
</LinearLayout>

View File

@@ -1,140 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="12dp"
android:background="@drawable/face_widget_bg_dark">
<!-- Title bar -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingBottom="6dp">
<TextView
android:id="@+id/fw_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Agent UI"
android:textColor="#8888FF"
android:textSize="12sp"
android:fontFamily="monospace" />
<TextView
android:id="@+id/fw_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#888888"
android:textSize="10sp"
android:fontFamily="monospace" />
</LinearLayout>
<!-- Terminal 1 -->
<LinearLayout
android:id="@+id/fw_terminal_1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingTop="2dp"
android:paddingBottom="2dp"
android:visibility="gone">
<TextView
android:id="@+id/fw_dot_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="\u25CF"
android:textColor="#6b7280"
android:textSize="8sp"
android:paddingEnd="4dp" />
<TextView
android:id="@+id/fw_name_1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#DDDDDD"
android:textSize="10sp"
android:fontFamily="monospace"
android:maxLines="1"
android:ellipsize="end" />
</LinearLayout>
<!-- Terminal 2 -->
<LinearLayout
android:id="@+id/fw_terminal_2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingTop="2dp"
android:paddingBottom="2dp"
android:visibility="gone">
<TextView
android:id="@+id/fw_dot_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="\u25CF"
android:textColor="#6b7280"
android:textSize="8sp"
android:paddingEnd="4dp" />
<TextView
android:id="@+id/fw_name_2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#DDDDDD"
android:textSize="10sp"
android:fontFamily="monospace"
android:maxLines="1"
android:ellipsize="end" />
</LinearLayout>
<!-- Terminal 3 -->
<LinearLayout
android:id="@+id/fw_terminal_3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingTop="2dp"
android:paddingBottom="2dp"
android:visibility="gone">
<TextView
android:id="@+id/fw_dot_3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="\u25CF"
android:textColor="#6b7280"
android:textSize="8sp"
android:paddingEnd="4dp" />
<TextView
android:id="@+id/fw_name_3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#DDDDDD"
android:textSize="10sp"
android:fontFamily="monospace"
android:maxLines="1"
android:ellipsize="end" />
</LinearLayout>
<!-- Empty state -->
<TextView
android:id="@+id/fw_empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="No agents active"
android:textColor="#666666"
android:textSize="10sp"
android:fontFamily="monospace"
android:gravity="center" />
</LinearLayout>

View File

@@ -1,7 +0,0 @@
{
"agent_ui_transcript": {
"menuInSetting": 1,
"labelResNameInSetting": "face_widget_label",
"actionDetailSetting": "com.agentui.desktop.FACE_WIDGET_SETTINGS"
}
}

View File

@@ -2,5 +2,4 @@
<string name="app_name">Agent UI</string>
<string name="main_activity_title">Agent UI</string>
<string name="widget_description">Shows recent transcript messages from Agent UI</string>
<string name="face_widget_label">Agent UI Terminals</string>
</resources>