关于 ping-orchestration-android-sdk
用于生成 Ping Orchestration Android SDK 认证集成代码或完整 Android 项目的技能,涵盖 Journey、DaVinci 和 OIDC Web 流程,并提供回调与收集器实现参考。
主要功能
- 01生成完整 Android 项目
- 02生成 Kotlin 集成文件
- 03配置 Journey、DaVinci 和 OIDC Web
- 04渲染回调与收集器
- 05校验 OAuth 和 Gradle 配置
适用场景
- 01创建 Android 认证示例应用
- 02为现有应用添加 Ping SDK 认证
- 03查阅 SDK 流程与回调指南
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ pingidentity/ ping-sdk-agent-skills --skill ping-orchestration-android-sdk安装后在 Claude Code 中输入/ping-orchestration-android-sdk确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
README.mdassets/Theme.kt.templatereferences/callback-reference.mdcommon-mistakes.md详细介绍
技能参数
不带参数调用时,运行向导。带参数调用时,生成内联内容。
无参数调用 — 向导模式
步骤 W1 — 确定意图,使用 AskUserQuestion:
"What would you like to do?"
Options:
A) Scaffold a new Android project — complete Gradle project with Ping SDK wired in
B) Add to an existing project — generate only the Kotlin files to drop in
C) Browse the reference guide — show the full SDK reference
D) Something else — let me describe what I need
- A/B → 进入步骤 W2 收集配置,然后生成
- C → 显示下方集成指南。停止。
- D → 提出后续自由文本问题,并据此分流。
步骤 W2 — 收集配置(仅限 A 和 B)。
在单个 AskUserQuestion 中询问所有必需参数。显示已有的默认值。所有必填字段都有值之前,不要生成。
| 参数 | 必填 | 默认值 | 描述 |
|---|---|---|---|
flowType | 是 | — | davinci · journey · oidc-web |
clientId | 是 | — | OAuth 2.0 客户端 ID |
discoveryEndpoint | 是 | — | 完整的 .well-known/openid-configuration URL |
redirectUri | 是 | com.example.myapp:/oauth2redirect | OAuth 2.0 重定向 URI(必须与清单方案匹配) |
scopes | 否 | openid profile email | 以空格分隔的 OAuth 2.0 作用域 |
serverUrl | 仅限 Journey | — | PingAM/AIC 基础 URL(末尾不能有 /) |
realm | 仅限 Journey | alpha | 身份验证域 |
cookieName | 仅限 Journey | iPlanetDirectoryPro | SSO Cookie 名称 |
journeyName | 仅限 Journey | Login | Journey 树名称 |
callbackTier | Journey/DaVinci | — | basic、standard 或 full(参见第 11 节) |
packageName | 是(仅限 A) | — | Android 包名,例如 com.example.myapp |
outputPath | 是(仅限 A) | — | 项目输出的绝对路径 |
验证规则:
redirectUri方案必须与build.gradle.kts中的manifestPlaceholders["appRedirectUriScheme"]匹配。discoveryEndpoint必须以/.well-known/openid-configuration结尾。serverUrl(Journey)必须以https://开头,且末尾不能有/。scopes必须包含openid。如果缺少,请将其添加到开头并发出警告。
步骤 W3 — 确认并生成。 在简短表格中总结各项值,询问“准备好生成了吗——这些信息正确吗?”,然后继续。
步骤 W4 — 生成后检查清单。 在报告完成之前,逐项检查:
-
Maven 仓库 —
settings.gradle.kts只需要google()+mavenCentral()。不要添加maven.forgerock.org(401)或maven.pingidentity.com。SDK 2.0.0+ 已在 Maven Central 上。 -
gradle.properties— 必须存在于项目根目录:android.useAndroidX=true android.enableJetifier=true org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 kotlin.code.style=official缺少此文件会导致构建因 AndroidX 类路径错误而失败。
-
gradlew包装器 — 不要包含在脚手架输出中;告知用户先在 Android Studio 中打开项目。 -
ExposedDropdownMenuBox锚点 — 使用Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable),导入androidx.compose.material3.MenuAnchorType。Material3 1.3.x 中不带参数的重载已弃用。 -
版本基线 —
agp = "8.9.1"、kotlin = "2.1.21"、compileSdk = 36、targetSdk = 36。AGP 8.9.1 需要 Kotlin 2.1.x(2.0.x 会触发 K2 崩溃FirIncompatibleClassExpressionChecker)。androidx.browser:1.9.0和androidx.core:1.17.0+需要 AGP 8.9.1+ 和compileSdk 36。 -
minSdk下限 — Journey:minSdk = 29。DaVinci/OIDC:minSdk = 28。 -
Ping Identity 品牌标识 — 始终应用。读取
assets/Theme.kt.template,写入ui/theme/PingTheme.kt,替换PLACEHOLDER_PACKAGE_NAME。将assets/ping_logo.png复制到app/src/main/res/drawable/。 -
res/values/themes.xml父主题 — 使用Theme.AppCompat.Light.NoActionBar,不要使用Theme.Material3.DayNight.NoActionBar。Material3 XML 主题需要依赖列表中没有的com.google.android.material库。Theme.AppCompat.Light.NoActionBar始终会通过activity-compose作为传递依赖提供。使用错误的父主题会导致构建时 AAPT 资源链接失败。 -
应用图标 — 绝不要引用
@mipmap/ic_launcher— 脚手架项目没有 mipmap 目录。在AndroidManifest.xml中,对android:icon和android:roundIcon都使用@drawable/ping_logo(已在步骤 7 中复制)。引用缺失的 mipmap 会导致 AAPT 资源链接失败,与上述主题错误相同。
Ping Orchestration Android SDK — 集成指南
流程类型
Journey(原生) — 在应用内组合 UI,使用回调,目标为 PingOne AIC / PingAM。
DaVinci — 类似 Journey,但目标为 PingOne DaVinci;使用收集器,不使用 serverUrl/realm/cookie。
OIDC Web — 通过 Custom Tabs 使用浏览器;配置重定向 URI intent filter。
模块映射
Artifact(com.pingidentity.sdks:<name>) | 添加时机 |
|---|---|
journey | Journey 流程 + 所有回调类型 |
davinci | DaVinci 流程 + 所有收集器类型 |
oidc | 独立的 OidcWebClient |
external-idp | 社交登录(IdpCollector、SelectIdpCallback) |
fido | FIDO2 / Passkeys |
protect | PingOne Protect 风险信号 |
binding | 设备绑定与签名 |
device-profile | 设备元数据 |
oath | TOTP/HOTP — artifact 是 oath,不是 mfa-oath |
push | 推送 MFA — artifact 是 push,不是 mfa-push |
recaptcha-enterprise | reCAPTCHA Enterprise |
1 — Gradle 依赖项
SDK 2.0.0+ 已发布到 Maven Central — 无需自定义仓库。
// settings.gradle.kts
dependencyResolutionManagement {
repositories { google(); mavenCentral() }
}
// app/build.gradle.kts
android {
compileSdk = 36
defaultConfig {
minSdk = 29
targetSdk = 36
manifestPlaceholders["appRedirectUriScheme"] = "com.example.myapp"
}
buildFeatures { buildConfig = true }
}
dependencies {
implementation("com.pingidentity.sdks:journey:2.0.0") // or :davinci:2.0.0
implementation("com.pingidentity.sdks:fido:2.0.0")
implementation("com.pingidentity.sdks:protect:2.0.0")
implementation("com.pingidentity.sdks:binding:2.0.0")
implementation("com.pingidentity.sdks:device-profile:2.0.0")
implementation("com.pingidentity.sdks:external-idp:2.0.0")
val composeBom = platform("androidx.compose:compose-bom:<bom_version>")
implementation(composeBom)
implementation("androidx.compose.material3:material3")
implementation("androidx.activity:activity-compose")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose")
implementation("androidx.navigation:navigation-compose")
}
gradle/libs.versions.toml 基线:agp = "8.9.1"、kotlin = "2.1.21"。
2 — AndroidManifest.xml — 重定向 URI
<activity android:name=".MainActivity" ...>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="${appRedirectUriScheme}" />
</intent-filter>
</activity>
3 — 节点类型(com.pingidentity.orchestrate)
sealed interface Node
// ContinueNode — more input needed; .next() to advance
// SuccessNode — authenticated; .session
// ErrorNode — server error (recoverable); .message: String
// FailureNode — unexpected error; .cause: Throwable (NON-OPTIONAL)
Node 不实现 Equatable — 在状态中使用 counter: Int 以强制重新组合。
4 — DaVinci 配置
import com.pingidentity.davinci.DaVinci
import com.pingidentity.davinci.module.Oidc
import com.pingidentity.logger.Logger
import com.pingidentity.logger.STANDARD // top-level extension — explicit import required
val daVinci = DaVinci {
logger = Logger.STANDARD
module(Oidc) {
clientId = BuildConfig.CLIENT_ID
discoveryEndpoint = BuildConfig.DISCOVERY_ENDPOINT
scopes = mutableSetOf("openid", "profile", "email")
redirectUri = BuildConfig.REDIRECT_URI
}
}
// Start: val node: Node = daVinci.start() — suspend
// Advance: val next: Node = continueNode.next() — suspend
// Session: val user = daVinci.user() — suspend, null if none
// Logout: user?.logout()
// Token: user?.accessToken() // Result<Token, OidcError>
5 — Journey 配置
import com.pingidentity.journey.Journey
import com.pingidentity.journey.module.Oidc
import com.pingidentity.logger.Logger
import com.pingidentity.logger.STANDARD // top-level extension
import com.pingidentity.journey.start // top-level extension — NOT a Journey member
import com.pingidentity.journey.user // top-level extension — NOT a Journey member
val journey = Journey {
logger = Logger.STANDARD
serverUrl = BuildConfig.SERVER_URL
realm = BuildConfig.REALM
cookie = BuildConfig.COOKIE_NAME
module(Oidc) {
clientId = BuildConfig.CLIENT_ID
discoveryEndpoint = BuildConfig.DISCOVERY_ENDPOINT
scopes = mutableSetOf("openid", "profile", "email")
redirectUri = BuildConfig.REDIRECT_URI
}
}
// Start: val node: Node = journey.start(BuildConfig.JOURNEY_NAME) — suspend
// Session: val user = journey.user() — suspend, null if none
// Logout: user?.logout()
// Token: user?.token() // Result<Token, OidcError>
以下四个符号都是顶层扩展 — 未导入时无法解析:
| 符号 | 导入 |
|---|---|
journey.start(name) | import com.pingidentity.journey.start |
journey.user() | import com.pingidentity.journey.user |
continueNode.callbacks | import com.pingidentity.journey.plugin.callbacks |
Logger.STANDARD | import com.pingidentity.logger.STANDARD |
6 — MVVM 状态与 ViewModel
data class AuthState(val node: Node? = null, val counter: Int = 0)
class AuthViewModel : ViewModel() {
val state = MutableStateFlow(AuthState())
val loading = MutableStateFlow(false)
fun start() { viewModelScope.launch {
loading.value = true
val node = journey.start(BuildConfig.JOURNEY_NAME)
state.update { AuthState(node = node, counter = it.counter + 1) }
loading.value = false
}}
fun next(continueNode: ContinueNode) { viewModelScope.launch {
loading.value = true
val node = continueNode.next()
state.update { AuthState(node = node, counter = it.counter + 1) }
loading.value = false
}}
fun refresh() { state.update { it.copy(counter = it.counter + 1) } }
fun logout(onCompleted: () -> Unit) { viewModelScope.launch {
journey.user()?.logout()
state.value = AuthState()
onCompleted()
}}
}
7 — 顶层身份验证屏幕
@Composable
fun AuthScreen(viewModel: AuthViewModel, onSuccess: () -> Unit) {
BackHandler { viewModel.start() } // restart rather than navigate back
val state by viewModel.state.collectAsState()
val loading by viewModel.loading.collectAsState()
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
when (val node = state.node) {
is ContinueNode -> CallbackNodeView(node, { viewModel.refresh() }) { viewModel.next(node) }
is FailureNode -> PingErrorCard(node.cause.message ?: "Unexpected error") { viewModel.start() }
is ErrorNode -> PingErrorCard(node.message) { viewModel.start() }
is SuccessNode -> LaunchedEffect(Unit) { onSuccess() } // LaunchedEffect prevents recomposition loop
null -> LaunchedEffect(Unit) { viewModel.start() }
}
if (loading) PingLoadingOverlay("Signing in…")
}
}
8 — DaVinci:收集器渲染
@Composable
fun ContinueNodeView(node: ContinueNode, onNodeUpdated: () -> Unit, onNext: (ContinueNode) -> Unit) {
var hasSubmittable = false
node.collectors.forEach { collector ->
when (collector) {
is TextCollector -> TextCollectorField(collector, onNodeUpdated)
is PasswordCollector -> PasswordCollectorField(collector, onNodeUpdated)
is LabelCollector -> Text(collector.content, style = MaterialTheme.typography.bodyMedium)
is SingleSelectCollector -> SingleSelectField(collector, onNodeUpdated)
is MultiSelectCollector -> MultiSelectField(collector, onNodeUpdated)
is PhoneNumberCollector -> PhoneNumberField(collector, onNodeUpdated)
is SubmitCollector -> { hasSubmittable = true; SubmitButton(collector) { onNext(node) } }
is FlowCollector -> { hasSubmittable = true; FlowButton(collector) { onNext(node) } }
is DeviceRegistrationCollector -> { hasSubmittable = true; DeviceRegistrationView(collector) { onNext(node) } }
is DeviceAuthenticationCollector -> { hasSubmittable = true; DeviceAuthView(collector) { onNext(node) } }
is FidoRegistrationCollector -> { hasSubmittable = true; FidoRegistrationView(collector) { onNext(node) } }
is FidoAuthenticationCollector -> { hasSubmittable = true; FidoAuthView(collector) { onNext(node) } }
is ProtectCollector -> ProtectCollectorView(collector) { onNext(node) }
is IdpCollector -> IdpButton(collector) { onNext(node) }
}
}
if (!hasSubmittable) { PingPrimaryButton("Next") { onNext(node) } }
}
收集器的关键属性:
| 收集器 | 关键属性 | 输入 |
|---|---|---|
TextCollector | label、value、required | collector.value = text |
PasswordCollector | label、value | 先调用 collector.validate(pwd);再执行 collector.value = pwd |
SubmitCollector / FlowCollector | label | 调用 onNext |
LabelCollector | content | 仅显示 |
SingleSelectCollector | label、options: List<Option>、value: String | collector.value = option.value |
MultiSelectCollector | label、options: List<Option>、value: List<String> | collector.value = listOf(...) |
PhoneNumberCollector | label、defaultCountryCode、countryCode、phoneNumber | 设置两个字段 |
DeviceRegistrationCollector / DeviceAuthenticationCollector | devices: List<Device> | 执行 collector.value = device,然后调用 onNext |
ProtectCollector | — | 执行 suspend collector.collect(),然后调用 onNext |
FidoRegistrationCollector | — | 执行 suspend collector.register(),然后调用 onNext |
FidoAuthenticationCollector | — | 执行 suspend collector.authenticate(),然后调用 onNext |
IdpCollector | label、type、iconUrl | 执行 suspend collector.authorize(redirectUri) |
9 — Journey:回调渲染
import com.pingidentity.journey.plugin.callbacks // extension property — required
@Composable
fun CallbackNodeView(node: ContinueNode, onNodeUpdated: () -> Unit, onNext: () -> Unit) {
var showNext = true
node.callbacks.forEach { callback ->
when (callback) {
is NameCallback -> NameField(callback, onNodeUpdated)
is PasswordCallback -> PasswordField(callback, onNodeUpdated)
is ValidatedUsernameCallback -> ValidatedUsernameField(callback, onNodeUpdated)
is ValidatedPasswordCallback -> ValidatedPasswordField(callback, onNodeUpdated)
is TextInputCallback -> TextInputField(callback, onNodeUpdated)
is TextOutputCallback -> Text(callback.message, style = MaterialTheme.typography.bodyMedium)
is ChoiceCallback -> ChoiceField(callback, onNodeUpdated)
is ConfirmationCallback -> { showNext = false; ConfirmationButtons(callback, onNext) }
is TermsAndConditionsCallback -> TermsCheckbox(callback, onNodeUpdated)
is KbaCreateCallback -> KbaField(callback, onNodeUpdated)
is BooleanAttributeInputCallback -> BooleanAttributeField(callback, onNodeUpdated)
is StringAttributeInputCallback -> StringAttributeField(callback, onNodeUpdated)
is PollingWaitCallback -> { showNext = false; PollingWaitView(callback, onNext) }
is DeviceProfileCallback -> { showNext = false; DeviceProfileView(callback, onNext) }
is DeviceBindingCallback -> { showNext = false; DeviceBindingView(callback, onNext) }
is FidoRegistrationCallback -> { showNext = false; FidoRegistrationView(callback, onNext) }
is FidoAuthenticationCallback -> { showNext = false; FidoAuthView(callback, onNext) }
is PingOneProtectInitializeCallback -> { showNext = false; ProtectInitView(callback, onNext) }
is PingOneProtectEvaluationCallback -> { showNext = false; ProtectEvalView(callback, onNext) }
is SelectIdpCallback -> { showNext = false; SelectIdpView(callback, onNext) }
}
}
if (showNext) { PingPrimaryButton("Next", onClick = onNext) }
}
回调参考(准确的名称、包名、属性名和挂起方法 — 猜错会导致编译错误):references/callback-reference.md
关键注意事项:ValidatedUsernameCallback → .username(不是 .value);TextInputCallback → .text;ChoiceCallback → .selectedIndex;TermsAndConditionsCallback → .accepted;DeviceProfileCallback.collect()、DeviceBindingCallback.bind()、PingOneProtectInitializeCallback.start()。
自动推进的回调 — 在 LaunchedEffect(callback) 中包装,并设置 showNext = false:PollingWaitCallback、DeviceProfileCallback、DeviceBindingCallback、FidoRegistrationCallback、FidoAuthenticationCallback、PingOneProtectInitializeCallback、PingOneProtectEvaluationCallback。
10 — OIDC Web 流程
import com.pingidentity.oidc.OidcWebClient
import com.pingidentity.oidc.module.Oidc
val webClient = OidcWebClient {
module(Oidc) {
clientId = BuildConfig.CLIENT_ID
discoveryEndpoint = BuildConfig.DISCOVERY_ENDPOINT
scopes = mutableSetOf("openid", "profile", "email")
redirectUri = BuildConfig.REDIRECT_URI
}
}
// In ViewModel:
webClient.authorize()
.onSuccess { user -> /* authenticated */ }
.onFailure { error -> /* handle OidcError */ }
val user = webClient.user() // null if no active session
authorize() 是挂起函数。它会启动 Custom Tabs,等待重定向,并将代码交换为令牌。
11 — Collector/Callback 层级
| 层级 | Journey 新增项 | DaVinci 新增项 | 额外模块 |
|---|---|---|---|
| 基础 | NameCallback、PasswordCallback、TextOutputCallback、ChoiceCallback | TextCollector、PasswordCollector、SubmitCollector、LabelCollector、FlowCollector | 无 |
| 标准 | + 已验证的回调、属性回调、ConfirmationCallback、TermsAndConditionsCallback、KbaCreateCallback、PollingWaitCallback,以及所有设备/FIDO/Protect/SelectIdp 回调 | + SingleSelectCollector、MultiSelectCollector、PhoneNumberCollector、设备/FIDO/Protect collectors | fido、protect、binding、device-profile、external-idp |
| 完整 | + IdpCallback、ReCaptchaEnterpriseCallback | + IdpCollector | + recaptcha-enterprise |
FIDO / Protect / device binding? No → Basic
Yes → Social login or reCAPTCHA? No → Standard Yes → Full
12 — 常见错误
请参阅 references/common-mistakes.md — 涵盖导入错误、回调属性名称错误、挂起方法名称错误、重组循环、Gradle/AGP 版本问题、AAPT 错误,以及品牌组件相关问题。
13 — MFA 模块
推送(FCM) — artifact com.pingidentity.sdks:push:
val pushClient = PushClient { }
pushClient.addCredentialFromUri(uri) // QR scan
pushClient.processNotification(remoteMessage.data) // FCM
pushClient.approveNotification(notificationId)
OATH(TOTP/HOTP) — artifact com.pingidentity.sdks:oath:
val oathClient = OathClient { }
oathClient.addCredentialFromUri(otpauthUri)
val code = oathClient.generateCode(credentialId)
Protect(独立):
Protect.config { isBehavioralDataCollection = true; envId = "api.pingone.com" }
Protect.init()
14 — ForgeRock → Ping 迁移
| 旧版 | Ping SDK |
|---|---|
org.forgerock:forgerock-auth | com.pingidentity.sdks:journey |
FRAuth.start() | journey.start("Login") |
FRUser.getCurrentUser() | journey.user() / daVinci.user() |
Node.getCallbacks() | continueNode.callbacks |
如需完整的迁移指南,请阅读 forgerock-to-ping-journey-migration skill。
15 — Ping Identity 视觉品牌设计
将 assets/Theme.kt.template 写入 ui/theme/PingTheme.kt(替换 PLACEHOLDER_PACKAGE_NAME)。将 assets/ping_logo.png 复制到 app/src/main/res/drawable/ping_logo.png。
品牌组件:PingPrimaryButton、PingHeaderView、PingTextFieldBranded(此名称可避免与 PingTextField 颜色常量冲突)、PingSecureField、PingErrorMessage、PingErrorCard、PingLoadingOverlay(必须是 Box(Modifier.fillMaxSize()) 中最上层的同级元素)。
登录屏幕:使用 Box(fillMaxSize) 包裹一个 Column(verticalScroll),顶部放置 PingHeaderView,下方放置 CallbackNodeView/CollectorNodeView,加载时叠加显示 PingLoadingOverlay。为 Checkbox/Switch 应用 PingRed 色调。