Match Agent app picker to stub launcher list

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Iliyan Malchev
2026-03-21 23:47:43 -07:00
parent 7a19620340
commit faa0756a6b
3 changed files with 113 additions and 21 deletions

View File

@@ -2,10 +2,14 @@ package com.openai.codex.agent
import android.app.Activity
import android.app.AlertDialog
import android.graphics.drawable.Drawable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.EditText
import android.widget.ImageView
import android.widget.Spinner
import android.widget.TextView
import com.openai.codex.bridge.SessionExecutionSettings
@@ -83,9 +87,21 @@ class CreateSessionDialogController(
}
fun updatePackageSummary() {
packageSummary.text = selectedPackage?.let { app ->
"${app.label} (${app.packageName})"
} ?: "No target app selected. This will start an Agent-anchored session."
val app = selectedPackage
if (app == null) {
packageSummary.text = "No target app selected. This will start an Agent-anchored session."
packageSummary.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, null, null)
return
}
packageSummary.text = "${app.label} (${app.packageName})"
packageSummary.setCompoundDrawablesRelativeWithIntrinsicBounds(
resizeIcon(app.icon),
null,
null,
null,
)
packageSummary.compoundDrawablePadding =
activity.resources.getDimensionPixelSize(android.R.dimen.app_icon_size) / 4
}
fun updateEffortOptions(requestedEffort: String?) {
@@ -157,25 +173,49 @@ class CreateSessionDialogController(
val apps = InstalledAppCatalog.listInstalledApps(activity, sessionController)
if (apps.isEmpty()) {
AlertDialog.Builder(activity)
.setMessage("No installed target packages are available.")
.setMessage("No launchable target apps are available.")
.setPositiveButton(android.R.string.ok, null)
.show()
return
}
val labels = apps.map { app ->
buildString {
append(app.label)
append(" (")
append(app.packageName)
append(")")
if (!app.eligibleTarget) {
append(" — unavailable")
}
val adapter = object : ArrayAdapter<InstalledApp>(
activity,
R.layout.list_item_installed_app,
apps,
) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
return bindAppRow(position, convertView, parent)
}
}.toTypedArray()
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
return bindAppRow(position, convertView, parent)
}
private fun bindAppRow(position: Int, convertView: View?, parent: ViewGroup): View {
val row = convertView ?: LayoutInflater.from(context)
.inflate(R.layout.list_item_installed_app, parent, false)
val app = getItem(position) ?: return row
val iconView = row.findViewById<ImageView>(R.id.installed_app_icon)
val titleView = row.findViewById<TextView>(R.id.installed_app_title)
val subtitleView = row.findViewById<TextView>(R.id.installed_app_subtitle)
iconView.setImageDrawable(app.icon ?: activity.getDrawable(android.R.drawable.sym_def_app_icon))
titleView.text = app.label
subtitleView.text = if (app.eligibleTarget) {
app.packageName
} else {
"${app.packageName} — unavailable"
}
row.isEnabled = app.eligibleTarget
titleView.isEnabled = app.eligibleTarget
subtitleView.isEnabled = app.eligibleTarget
iconView.alpha = if (app.eligibleTarget) 1f else 0.5f
row.alpha = if (app.eligibleTarget) 1f else 0.6f
return row
}
}
AlertDialog.Builder(activity)
.setTitle("Choose package")
.setItems(labels) { _, which ->
.setTitle("Choose app")
.setAdapter(adapter) { _, which ->
val app = apps[which]
if (!app.eligibleTarget) {
AlertDialog.Builder(activity)
@@ -184,11 +224,18 @@ class CreateSessionDialogController(
)
.setPositiveButton(android.R.string.ok, null)
.show()
return@setItems
return@setAdapter
}
onSelected(app)
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
private fun resizeIcon(icon: Drawable?): Drawable? {
val sizedIcon = icon?.constantState?.newDrawable()?.mutate() ?: return null
val iconSize = activity.resources.getDimensionPixelSize(android.R.dimen.app_icon_size)
sizedIcon.setBounds(0, 0, iconSize, iconSize)
return sizedIcon
}
}

View File

@@ -1,12 +1,14 @@
package com.openai.codex.agent
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
data class InstalledApp(
val packageName: String,
val label: String,
val icon: Drawable?,
val eligibleTarget: Boolean,
)
@@ -22,17 +24,23 @@ object InstalledAppCatalog {
sessionController: AgentSessionController,
): List<InstalledApp> {
val pm = context.packageManager
val installedApplications = pm.getInstalledApplications(PackageManager.MATCH_ALL)
val launcherIntent = Intent(Intent.ACTION_MAIN)
.addCategory(Intent.CATEGORY_LAUNCHER)
val appsByPackage = linkedMapOf<String, InstalledApp>()
installedApplications.forEach { applicationInfo ->
pm.queryIntentActivities(launcherIntent, 0).forEach { resolveInfo ->
val applicationInfo = resolveInfo.activityInfo?.applicationInfo ?: return@forEach
val packageName = applicationInfo.packageName.takeIf(String::isNotBlank) ?: return@forEach
if (packageName in excludedPackages) {
return@forEach
}
val label = applicationInfo.loadLabel(pm)?.toString().orEmpty().ifBlank { packageName }
if (packageName in appsByPackage) {
return@forEach
}
val label = resolveInfo.loadLabel(pm)?.toString().orEmpty().ifBlank { packageName }
appsByPackage[packageName] = InstalledApp(
packageName = packageName,
label = label,
icon = resolveInfo.loadIcon(pm),
eligibleTarget = sessionController.canStartSessionForTarget(packageName),
)
}

View File

@@ -0,0 +1,37 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="56dp"
android:orientation="horizontal"
android:paddingHorizontal="20dp"
android:paddingVertical="12dp">
<ImageView
android:id="@+id/installed_app_icon"
android:layout_width="40dp"
android:layout_height="40dp"
android:contentDescription="@null"
android:importantForAccessibility="no" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/installed_app_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="@+id/installed_app_subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textAppearance="?android:attr/textAppearanceSmall" />
</LinearLayout>
</LinearLayout>