← Docs hub

등록 App ↔ DeviceAgent 인터페이스

구현 협의용 최신 문서: DeviceAgent ↔ 등록 App 구현 요청서. 아래는 이전 상세·Sample 자료다. 직접 구현 책임과 현재 취소·재시도 기준은 신규 요청서를 먼저 확인한다.

화면·복구 보강: 등록 단계별 완료 화면, Binder 단절·start 응답 유실·앱 재시작, 관련 연동 시험. 제품 제안에서는 전송 성공으로 완료 화면을 표시하지 않고, 상태 조회로 복구하며 녹음 start를 자동 반복하지 않는다.

등록 화면을 만드는 쪽과 DeviceAgent를 구현하는 쪽이 맞출 값이다. 등록 App은 제어 Bundle을 보내고 상태 Bundle을 받는다. 녹음·임베딩은 AI-Agent가 처리하며 원본 음성을 등록 App에서 보내지 않는다.

개인화 선택과 서버 관심사

등록 App의 setVoiceProfilePersonalization(enabled:Boolean)은 개인화 사용 동의 후 프로필 생성을 시작하는 명령이다. 관심사 목록을 저장하는 명령이 아니다.

단계 주체 → 대상 전달·처리
개인화 선택 등록 App → DeviceAgent → AI-Agent enabled를 최종 프로필 personalizationEnabled에 기록
등록 완료 AI-Agent → DeviceAgent → 등록 App·IoTAgent voiceProfileStateChanged(COMPLETED, voiceId). 등록 App은 완료 화면 표시
최초 관심사 등록 모바일 App → 샘플 Backend 완료 이벤트 후 프로필 목록 갱신, voiceId 선택, 관심사 HTTPS PUT
상시 관심사 변경 모바일 App → Backend 화자별 설정에서 같은 HTTPS PUT으로 전체 목록 교체
대화 중 조회 AI-Agent → DeviceAgent → IoTAgent → Backend getVoicePersonalization; 결과는 역방향. 등록 App에는 전달하지 않음

샘플 Backend는 기기 목록의 personalizationEnabled로 프로필을 확인한다. 동의하지 않은 프로필에 대한 관심사 저장은 거절하며, 과거 필드가 없는 프로필을 동의한 것으로 취급하지 않는다. 제품 계정 동의 연계는 별도다. COMPLETED가 LOCAL_TEST_ACK에서 나올 수 있으므로 실제 Cloud 저장 완료와 혼동하지 않는다.

서버 관심사 필드·실패·MQTT 명세

DeviceAgent 중심 구조

등록 App·AI-Agent Service와 DeviceAgent의 요청·결과 전달 구조

SVG 구조 그림 크게 보기 · 등록 App·AI-Agent ↔ DeviceAgent 전체 인터페이스 정의

등록 App은 화면과 사용자 입력을 담당하고, 등록·인식을 수행하는 서비스는 AI-Agent다. 아래 그림은 그중 등록 과정의 중계 순서다. DeviceAgent가 직접 녹음하거나 화자를 식별하는 구조는 아니다.

1. 주고받는 것

방향 수단 전달 값 처리 주체
DeviceAgent → 등록 App 명시적 Activity Intent requestId, 닉네임, 발음, 언어, 초기 개인화 값 등록 App이 화면 표시
등록 App → DeviceAgent sendModuleCommand(input, output) 시작·재녹음·취소·개인화 등 제어 Bundle DeviceAgent가 AI-Agent로 중계
DeviceAgent → 등록 App onModuleCallback(Bundle) voiceProfileStateChanged 등록 App이 진행/오류/완료 화면 갱신

요청의 반환값은 전달 수락 여부이고 최종 결과가 아니다. 등록 App은 output Bundle이 아니라 callback으로 화면을 갱신한다.

2. 화면 실행: DeviceAgent → 등록 App

항목 값
package com.sk.airbot.voiceprofileregistration
class com.sk.airbot.voiceprofileregistration.VoiceProfileRegistrationActivity
action com.sk.airbot.voiceprofile.action.REGISTER
flags FLAG_ACTIVITY_NEW_TASK · FLAG_ACTIVITY_CLEAR_TOP

<a id="activity-flags"></a>

화면 실행 옵션(Flag)

Flag는 Android에 화면을 어떻게 열지 지정하는 옵션이다. 등록 데이터나 녹음 제어 명령이 아니다. Task는 뒤로가기로 이동하는 화면들의 묶음이다.

옵션 의미 등록 화면에서의 용도
FLAG_ACTIVITY_NEW_TASK 적합한 기존 Task가 있으면 활용하고, 없으면 새 Task를 만든다. 항상 새 화면을 만드는 뜻은 아니다. DeviceAgent의 서비스에서 등록 Activity를 열 때 사용한다.
FLAG_ACTIVITY_CLEAR_TOP 선택된 Task에 등록 Activity가 이미 있으면 그 위에 쌓인 다른 Activity를 종료한다. 기존 등록 화면으로 돌아오도록 한다.

두 옵션은 비트 OR로 함께 지정한다. 표의 가운데 점은 두 옵션을 나열한 표기다.

intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)

예를 들어 화면 순서가 등록 화면 → 다른 화면이면 등록 화면 위의 다른 화면을 정리한다. 등록 화면 자체의 재사용 여부는 Manifest의 launchMode와 다른 Flag에도 영향을 받는다. 기본 standard에서는 CLEAR_TOP으로 등록 Activity까지 종료하고 다시 만들 수 있으며, 재사용되는 경우 새 요청은 onNewIntent()로 전달된다.

이 옵션만으로 녹음 취소, 등록 상태 초기화, 중복 요청 방지가 처리되지는 않는다. 이 처리는 별도의 등록·취소 메서드와 requestId 확인으로 관리해야 한다. Activity 내부 화면 전환까지 자동으로 초기화하는 옵션도 아니다.

참고: Android 공식 문서: Task와 화면 스택.

Intent extra 타입 전달 조건 의미
requestId String 시작·취소 같은 등록 작업을 구분
nickname String 시작 사용자 표시 이름
locale String 시작 한국어 ko-KR, 영어 en-US. 아래 언어값 규약 참조
personalizationEnabled Boolean 시작 초기 개인화 값
command String 취소 시 cancel 기존 등록 화면에 취소 전달

원격 시작은 화면만 연다. 사용자가 시작 버튼을 눌러야 등록 App이 start Bundle을 보낸다. 원격 취소도 현재 Activity를 거친다. 다른 활성 요청이 있거나 Activity 실행에 실패한 경우의 원격 실패 응답은 보완 대상이다.

<a id="locale-contract"></a>

locale 언어값 규약

다음은 한·영 연동에서 송신 측이 사용할 값이다. 현재 코드의 허용 검사와는 구분한다.

항목 정의
키·타입 locale:String
한국어 ko-KR
영어 en-US
표기 언어 소문자 + 하이픈 + 지역 대문자. ko, en, ko_KR, 한국어로 대체하지 않음
전송 조건 시작 요청에 명시적으로 포함. 빈 문자열·공백·null을 보내지 않음
의미 등록 작업에 전달되는 언어 메타데이터. 이 필드만으로 기기의 언어 설정이나 STT/TTS 엔진을 변경하는 명령은 아님
다른 언어 이 한·영 연동 규약에는 정의하지 않음. 문자열이 수락된다고 해당 언어 기능이 지원되는 것은 아님

현재 구현의 기본값·검증 차이

경로 실제 동작
DeviceAgent 원격 시작 검증 trim 후 비어 있지 않고 길이 32자 이하인지 검사. ko-KR/en-US 목록 검사와 표기 정규화는 없음
DeviceAgent Activity Intent 작성 locale 누락 시 ko-KR 기본값을 지정하는 코드가 있으나, 원격 시작은 앞선 필수값 검사를 통과해야 함
등록 App에서 시작 버튼 Intent locale이 있으면 그대로 전달. 없으면 Locale.getDefault().toLanguageTag() 사용. 영어가 항상 en-US라는 보장은 없음
AI-Agent 세션 생성 locale이 빈 값/공백이면 ko-KR; 그 외 값은 그대로 세션에 보존
잘못된 값 응답 빈 값·길이 초과 원격 요청은 검증 거절 대상. 목록 밖 언어를 위한 전용 오류 코드·거절 처리는 현재 없음

따라서 송신 측은 위의 정확한 한·영 값을 보내야 한다. 수신 측에서도 이 두 값만 허용하도록 강제하려면 별도 코드 변경이 필요하다. 이번 문서 수정으로 검증 동작이 바뀌지는 않는다.

3. Proxy 연결과 공통 Bundle

연결: DeviceControlProxy.connect(context, callback, true, callbackClassName). 현재 callbackClassName은 등록 Activity의 전체 클래스명이다. 연결되면 getVoiceProfileEnrollmentState를 요청한다. 수신은 IDeviceControlProxyCallback.onModuleCallback(outdata)다.

등록 App의 모든 제어 요청은 다음 네 필드를 포함한다.

키 Android 타입 필수 값·의미
method String 필수 다음 표의 메서드 이름
contractVersion String 필수 voice-profile-v2
requestId String 필수 실행 Intent에서 받은 등록 작업 ID
issuedAtMs Long 현재 클라이언트 항상 전송 System.currentTimeMillis()

등록 App 클라이언트는 현재 expiresAtMs를 넣지 않는다. 원격 MQTT의 만료 검사와 UI 내부 등록 제어의 공통 필드를 혼동하지 않는다.

예시: 연결 후 현재 상태 조회

아래는 실제 클라이언트의 연결·송신 흐름을 간추린 예시다. context는 앱 Context, requestId는 실행 Intent에서 받은 값이다. 문자열 키는 읽기 쉽게 표시했으며 실제 구현에서는 VoiceProfileContract 상수를 사용한다.

val callback = object : DeviceControlProxy.IDeviceControlProxyCallback {
    override fun onServiceConnected() {
        val input = Bundle().apply {
            putString("method", "getVoiceProfileEnrollmentState")
            putString("contractVersion", "voice-profile-v2")
            putString("requestId", requestId)
            putLong("issuedAtMs", System.currentTimeMillis())
        }
        val service = DeviceControlProxy.getService() ?: return
        try {
            val result = service.sendModuleCommand(input, Bundle())
            if (result < 0) return // 전송 거절. 상태 수신으로 처리하지 않는다.
        } catch (error: android.os.RemoteException) {
            return // 연결 오류. 화면에서 재연결 안내가 필요하다.
        }
    }

    override fun onServiceDisconnected() {
        // 연결 해제 상태를 UI에 전달한다.
    }

    override fun onModuleCallback(outdata: Bundle?) {
        if (outdata?.getString("method") != "voiceProfileStateChanged") return
        if (outdata.getString("requestId") != requestId) return
        val state = outdata.getString("state") ?: return
        // state를 UI에 전달한다. 화면 갱신은 메인 스레드에서 처리한다.
    }
}
if (DeviceControlProxy.connected()) {
    DeviceControlProxy.setDeviceControlProxyCallback(callback)
    callback.onServiceConnected()
} else {
    DeviceControlProxy.connect(
        context.applicationContext, callback, true,
        "com.sk.airbot.voiceprofileregistration.VoiceProfileRegistrationActivity"
    )
}

흐름은 Proxy 연결 → 조회 Bundle 송신 → DeviceAgent가 AI-Agent로 중계 → 상태 콜백 수신 → UI 반영이다. sendModuleCommand()의 0 이상 반환값은 전송 수락이지 등록 완료가 아니다. 실제 상태는 콜백의 state로 확인한다. 예시는 흐름 설명용이며 화면의 오류 표시와 연결 수명 관리는 생략했다. 기존 클라이언트는 수신 값을 Listener로 전달하며, 위 예시는 현재 작업의 requestId를 확인할 위치도 함께 보여준다.

<a id="ui-commands"></a>

4. 버튼별 요청과 기대 결과

추가 입력은 3절 공통 Bundle에 붙인다. 모든 키는 실제 문자열이며 구현에서는 각 모듈의 Contract 상수를 사용한다.

UI 동작 method 추가 키 : 타입 받는 결과·주의
연결/화면 복귀 getVoiceProfileEnrollmentState 없음 현재 상태. 해당 세션 없음은 WAITING_START
등록 시작 startVoiceProfileEnrollment nickname:String, :String, locale:String, userId:String RECORDING_SET 또는 거절 상태. userId는 기존 호환 값이며 등록 식별 키로 사용 안 함
다시 녹음 retryVoiceProfileSet setIndex:Int 현재 세트 재시작. 임의 세트 선택 API 아님
나가기 확인창 pauseVoiceProfileEnrollmentForExit setIndex:Int 현재 별도 상태 ACK 없음. pause callback을 기다리면 안 됨
나가기 확정/원격 취소 cancelVoiceProfileEnrollment reason:String CANCELLED. 요청 ID 불일치/세션 없음은 무응답 가능
개인화 선택 저장 setVoiceProfilePersonalization enabled:Boolean 로컬 프로필 생성 시작. personalizationEnabled 키로 보내지 않음
생성 재시도 retryVoiceProfileCreation 없음 상태 callback. 세트가 비워진 실패 경로에서는 무동작 가능
전달 재시도 retryVoiceProfileDelivery 없음 기존 pending 프로필 전달. 새 프로필 생성 아님
완료 화면 닫기 closeVoiceProfileRegistration 없음 완료 프로필을 삭제하지 않음. 별도 close ACK를 기다리지 않음

nickname은 trim 후 1~40자이며 경로 구분자·제어 문자 금지다. 발음 보조 필드를 입력받거나 전달하지 않는다.

4.1 즉시 반환값: 전송 단계

호출 형식은 sendModuleCommand(input: Bundle, output: Bundle): Int다. 등록 제어는 두 번째 Bundle에서 결과를 읽는 방식이 아니라 별도 상태 콜백을 받는다.

값/상황 현재 의미 등록 App 처리
0 DeviceAgent가 요청 중계 경로를 수행함 상태 콜백을 확인한다. AI-Agent의 실행·완료를 보장하지 않음
-1 등록 요청의 공통 형식 검사 실패 contractVersion·requestId 등 입력 확인
-2 호출자 허용 검사 실패 호출 앱·권한 구성 확인
-3 IoTAgent 원격 요청에서 등록 Activity 실행 실패 원격 화면 실행 경로의 오류. 등록 App의 일반 제어 반환값과 구분
Service가 null Binder 연결 안 됨 클라이언트 send()는 false 반환. 재연결 후 상태 조회
RemoteException Binder 통신 예외 클라이언트 send()는 false 반환. 성공으로 표시하지 않음

현재 DeviceAgentVoiceProfileClient.send()는 Int를 그대로 노출하지 않고 0 이상이면 true, 음수·연결 실패·통신 예외이면 false로 변환한다. 따라서 UI에서 -1/-2를 구별하려면 클라이언트 반환형도 확장해야 한다. 문서에 적힌 오류 코드만으로 현재 UI가 세부 오류를 구분하는 것은 아니다.

4.2 비동기 결과: 버튼별 상태와 파라미터

모든 상태 결과는 onModuleCallback(Bundle)의 method="voiceProfileStateChanged"로 받는다. 요청 메서드마다 별도의 ...Response가 오는 구조가 아니다. 공통 필드의 타입·의미는 상태 콜백 필드를 따른다.

요청 method 실행 조건·추가 입력 수신 상태와 확인 필드 응답이 없거나 실패할 수 있는 경우
getVoiceProfileEnrollmentState 공통 Bundle만 전송 현재 state, setIndex, progress, terminal; 해당 작업이 없으면 WAITING_START Binder 미연결·중계 실패 시 수신 보장 없음
startVoiceProfileEnrollment nickname:String, :String, locale:String, 기존 userId:String 녹음 시작은 RECORDING_SET; 거절은 reasonCode와 state로 판정 동일 요청 재전송이 안전한 중복 제거로 처리되지 않음
retryVoiceProfileSet 같은 requestId의 비종료 세션. UI가 setIndex:Int 전송 RECORDING_SET, 현재 세트의 setIndex, progress Service는 전달받은 setIndex로 세트를 선택하지 않음. 세션 없음·ID 불일치·terminal이면 무응답
pauseVoiceProfileEnrollmentForExit 같은 requestId이며 RECORDING_SET; UI가 setIndex:Int 전송 별도 상태 콜백/ACK 없음 조건 불일치 시 무동작. PAUSED 같은 enum을 기다리지 않음
cancelVoiceProfileEnrollment reason:String, 같은 requestId의 세션 CANCELLED, reasonCode="USER_CANCELLED", terminal=true Service는 입력 reason을 그대로 돌려주지 않음. 세션 없음·ID 불일치면 무응답
setVoiceProfilePersonalization enabled:Boolean; 같은 requestId이며 PERSONALIZATION_SETTINGS CREATING_LOCAL_PROFILE → 생성·전달 상태; 최종 성공은 COMPLETED와 voiceId 단계 불일치·필요 세트 부족 시 무응답 가능
retryVoiceProfileCreation 같은 requestId와 필요한 녹음 세트 보유 CREATING_LOCAL_PROFILE 이후 상태; 품질 실패는 SET_REJECTED, 생성 실패는 PROFILE_FAILED 실패 후 세트가 이미 비워졌으면 재시도가 실행되지 않을 수 있음
retryVoiceProfileDelivery 같은 requestId의 pending 프로필 존재 PENDING_DEVICE_DELIVERY → COMPLETED 또는 DELIVERY_FAILED; 실패는 reasonCode 확인 pending 없음·ID 불일치 시 무응답
closeVoiceProfileRegistration 같은 requestId의 세션 비종료 세션이면 취소 처리로 CANCELLED; 이미 종료된 세션이면 별도 close ACK 없음 세션 없음·ID 불일치 시 무응답. 완료 프로필 삭제 명령이 아님

COMPLETED에 필요한 voiceId는 Bundle에는 있지만 현재 등록 App의 상태 모델에는 복사되지 않는다. DELIVERY_FAILED는 저장·동기화 실패 경로이며 단순히 음성 녹음 실패를 뜻하지 않는다. 무응답 경로에는 현재 공통 타임아웃 응답 규약이 없으므로, 전송 true만으로 화면을 완료 처리해서는 안 된다.

실제 송신 예제

val input = Bundle().apply {
    putString("method", "startVoiceProfileEnrollment")
    putString("contractVersion", "voice-profile-v2")
    putString("requestId", requestId) // Intent에서 받은 값
    putLong("issuedAtMs", System.currentTimeMillis())
    putString("nickname", "Niky")
    putString("locale", "ko-KR")
    putString("userId", "") // 기존 클라이언트 호환 필드
}
val transportResult = DeviceControlProxy.getService()
    ?.sendModuleCommand(input, Bundle())
// 수락 여부만 확인. 녹음/완료 화면은 상태 callback으로 전환한다.

<a id="state-fields"></a>

5. 상태 수신: DeviceAgent → 등록 App

AI-Agent가 만든 상태를 DeviceAgent가 복사해 등록 App(CALLER_VOICE_PROFILE_REGISTRATION=8)과 IoTAgent(CALLER_IOT=2)에 전달한다.

예시: 1번째 녹음 시작을 두 앱에 알림

AI-Agent가 requestId="vp-example-001" 작업의 녹음을 시작했다고 가정한다. 아래 값은 설명용 예시이며 실행 로그가 아니다.

AI-Agent: 상태 Bundle 생성
  method = "voiceProfileStateChanged"
  contractVersion = "voice-profile-v2"
  requestId = "vp-example-001"
  stateSequence = 2L
  state = "RECORDING_SET"
  setIndex = 1, setCount = 5, progress = 0
  reasonCode = "", terminal = false, nickname = "Niky"
          |
          v
DeviceAgent: 값은 바꾸지 않고 대상별 Bundle로 복사하여 콜백 전달
          |
          +--> 등록 App [대상 번호 8]
          |      onModuleCallback(Bundle)
          |      같은 requestId 확인 → 1/5 녹음 중 화면 표시
          |
          +--> IoTAgent [대상 번호 2]
                 onModuleCallback(Bundle)
                 원격 요청 측에 전달할 상태 이벤트로 처리

CALLER_VOICE_PROFILE_REGISTRATION=8과 CALLER_IOT=2는 DeviceAgent 내부의 콜백 수신 대상 번호다. 녹음 상태 enum이나 requestId가 아니며 위 상태 Bundle에 추가로 넣는 필드도 아니다. 두 앱은 같은 requestId·stateSequence·state를 받지만 사용하는 목적이 다르다. 등록 App은 기기 화면을 갱신하고, IoTAgent는 원격 상태 전달을 담당한다.

두 수신처의 처리 완료나 동시 수신까지 보장하는 ACK는 아니다. 현재 IoTAgent의 외부 결과 전달은 로컬 시험 Bridge 경로이며, 이 콜백 수신만으로 제품 AWS IoT Core 발행이나 모바일 수신이 완료됐다고 판단하면 안 된다. 상태 Bundle에 원본 음성이나 임베딩은 포함하지 않는다.

Bundle 키 타입 포함 조건 의미
method String 항상 voiceProfileStateChanged
contractVersion String 항상 voice-profile-v2
requestId String 항상 화면의 작업 ID와 비교
stateSequence Long 항상 상태 순번
state String 항상 AI-Agent 상태 enum 이름
setIndex Int 항상 현재 세트 번호
setCount Int 항상 전체 세트 수, 현재 5
progress Int 항상 진행률 0~100
reasonCode String 항상 실패 사유, 정상은 빈 문자열 가능
terminal Boolean 항상 등록 작업 종료 여부
nickname String 항상 표시 이름, 초기 조회는 빈 문자열 가능
voiceId String 생성 후 등록된 화자 키. COMPLETED에 필요
{
  "method": "voiceProfileStateChanged",
  "contractVersion": "voice-profile-v2",
  "requestId": "vp-example-001",
  "stateSequence": 2,
  "state": "RECORDING_SET",
  "setIndex": 1,
  "setCount": 5,
  "progress": 20,
  "reasonCode": "",
  "terminal": false,
  "nickname": "Niky"
}

위 JSON은 Bundle 값 설명용이며 Binder에 JSON 문자열을 넣는 예제가 아니다. 상태 Bundle에는 success가 없다.

현재 등록 App의 VoiceProfileStateUpdate 변환은 상태·순번·세트·진행률·사유·terminal을 읽는다. nickname/voiceId는 Bundle에 있어도 이 UI 모델에는 옮기지 않는다. 등록 App에서 완료 ID를 직접 사용할 기능을 추가한다면 수신 모델도 확장해야 한다.

<a id="ui-state"></a>

6. 화면 판정과 예외

수신/상황 등록 App 처리
다른 requestId 현재 화면을 덮어쓰지 않음
이전 이하 stateSequence 오래된 이벤트 무시. Service 순번 역전은 송신측 보완 필요
SET_REJECTED 재녹음 표시; 생성 품질 실패는 1세트부터 다시 녹음
PERSONALIZATION_SETTINGS 개인화 사용 여부 선택. 발음 수정 화면 아님
CREATING_LOCAL_PROFILE 생성 진행
PENDING_DEVICE_DELIVERY DeviceAgent 전달 대기
PROFILE_FAILED/DELIVERY_FAILED reasonCode·terminal·실제 재시도 가능 경로로 화면 결정
COMPLETED 완료 화면. 로컬 시험 ACK인지 제품 Cloud ACK인지는 별도 확인
CANCELLED 취소 화면/종료. 생성 중 늦은 완료 방어는 미완성
Binder 연결 끊김 등록 성공으로 간주하지 않음. 재연결 후 상태 조회

DeviceAgent는 등록 제어를 CALLER_LLM=1로 전달하고 ModuleCmdCallbackPolicy의 상수 허용 목록을 맞춰야 한다. 현재 클라이언트는 sendModuleCommand 반환값 >=0을 수락으로 취급한다. -1/-2/-3은 분기별 오류이며 최종 등록 결과 enum이 아니다.

7. 클라이언트가 실제로 수신·적용하는 순서

DeviceAgentVoiceProfileClient와 VoiceProfileUiState의 현재 처리다. 표의 기본값은 누락을 허용하자는 권장 사양이 아니라 수신 구현의 동작이다. 송신자는 앞의 필드 명세대로 값을 모두 전달해야 한다.

순서 검사·변환 결과
1 method가 voiceProfileStateChanged인지 다른 method는 화면 상태로 처리하지 않음
2 requestId 존재, 알려진 stage 문자열인지 없거나 알 수 없는 상태이면 무시
3 Bundle → 상태 객체 setIndex 기본 1, setCount 기본 5, progress 기본 0, reasonCode 기본 빈 문자열, stateSequence 기본 -1, terminal 기본 false
4 UI의 requestId와 일치하는지 다른 등록 요청의 이벤트 무시
5 stateSequence가 현재 값보다 큰지, UI가 이미 terminal인지 같거나 작은 순번 및 종료 후 이벤트 무시
6 화면 표시 범위 보정 setCount 최소 1, setIndex는 1..setCount, progress는 0..100으로 제한

클라이언트 자체는 callback의 contractVersion을 별도로 검증하지 않는다. DeviceAgent 검증을 통과한 메시지를 받는 구조다. 또한 callback의 nickname·voiceId는 현재 UI 상태 객체로 복사하지 않는다. 화면에서 이 값의 변경까지 반영된다고 가정하면 안 된다.

예: 같은 요청에서 순번 12의 VALIDATING_SET을 먼저 적용한 뒤 순번 11의 RECORDING_SET이 도착해도 녹음 화면으로 돌아가지 않는다. 반대로 서비스 재시작으로 순번이 초기화되면 기존 UI가 새 이벤트를 거부할 수 있어, 재연결만으로 세션 복구가 완성되지는 않는다.

8. 연결·나가기·타임아웃

상황 현재 구현 완료 여부를 판단할 때 주의할 점
Proxy 연결 성공 listener에 연결 알림 후 getVoiceProfileEnrollmentState 요청 연결 성공과 등록 상태 수신은 별도
연결 끊김 UI connected 값을 false로 변경 자동 등록 재시작·서버 동기화·세션 복원을 보장하지 않음
버튼 요청 sendModuleCommand(...) >= 0을 Boolean으로 반환; 미연결·RemoteException은 false 일반 버튼 처리의 여러 경로가 반환값을 사용하지 않음. 전송 실패 안내·재시도 보완 필요
녹음 중 나가기 창 표시 pauseVoiceProfileEnrollmentForExit 송신 후 확인창 별도 pause 완료 ACK 없음
나가기 창에서 계속하기 창을 닫고 RECORDING_SET이면 retryVoiceProfileSet 송신 중단한 세트를 다시 녹음하는 요청
나가기 확정 cancelVoiceProfileEnrollment(reason=USER_EXIT_CONFIRMED) 후 즉시 finishAndRemoveTask CANCELLED callback을 기다리지 않고 닫음. 화면 종료가 Service 캡처 종료의 증거는 아님
모바일에서 취소 연결 후 cancel(...MOBILE_CANCELLED)의 반환이 true이면 화면 종료 true는 전송 단계 결과이며 취소 완료 ACK가 아님
WAITING_START 5분 타이머 만료 시 UI_STATE_TIMEOUT으로 취소 요청 후 화면 종료 Live 무발화 5초와 다른 등록 UI 전용 값
COMPLETED 60초 타이머 만료 시 취소 요청 후 화면 종료 완료 화면 자동 닫기 동작; 완료 프로필 삭제 요청이 아님
녹음·생성·전달 중 이 Activity의 상태별 종료 타이머 없음 Service 캡처 제한과 별개. 생성·전달 무응답은 무기한 화면 대기 가능

오류 표시와 확인할 화면

reasonCode 현재 오류 문구 분류 확인할 버튼 동작
NO_SPEECH / TOO_SHORT / INSUFFICIENT_SPEECH 음성 부족 안내 SET_REJECTED의 다시 녹음 → retryVoiceProfileSet
TOO_QUIET 작은 음량 안내 같은 세트 재녹음
MULTI_SPEAKER / INCONSISTENT_SPEAKER 여러 화자 안내 현재 Service가 지정한 세트부터 재녹음
그 외 일반 녹음 오류 안내 stage가 PROFILE_FAILED이면 생성 재시도, DELIVERY_FAILED이면 전달 재시도

오류 문구는 reasonCode, 버튼 행동은 stage에 따라 결정된다. 동일 오류 문구가 나온다고 같은 재시도 method를 보내는 것은 아니다.

화면 연동 시험표

아래는 실기기에서 추가 확인할 조건이며 완료된 시험 결과는 아니다.

번호 재현 조건 확인할 결과
UI-01 다른 요청·역순·중복 callback 주입 현재 화면이 바뀌지 않음
UI-02 녹음 오류 후 다시 녹음 retryVoiceProfileSet의 requestId 유지, 새 RECORDING_SET 수신
UI-03 연결 끊긴 상태에서 버튼 누름 전송 실패와 화면 대기를 기록; 성공으로 표시하지 않음
UI-04 녹음 중 나가기 확정 UI 종료와 별도로 Service 캡처 정지·CANCELLED·마이크 복구 확인
UI-05 같은 UI를 둔 채 Service 재시작 순번 초기화로 callback이 막히는지 확인
UI-06 WAITING_START 5분 / COMPLETED 60초 경과 UI_STATE_TIMEOUT 요청 및 화면 종료 확인

구현 근거·검증 범위

2026-09-18 SoC 작업본의 DeviceAgentVoiceProfileClient.kt, DeviceAgent.java, ModuleCmdCallbackPolicy, AI-Agent VoiceProfileIpcContract.kt를 대조했다. 문서·정적 검토이며 새 실기기 시험 결과는 아니다.

Keyboard shortcuts

⌘K / Ctrl+KOpen command palette
/Focus search
g hGo to home
g pGo to projects
g sGo to sessions
j / kNext / prev row (tables)
?Show this help
EscClose dialogs

Structured queries

Mix key:value filters with free text in the palette:

type:sessionOnly session pages
project:llm-wikiFilter by project name (substring)
model:claudeFilter by model name (substring)
date:>2026-03-01Sessions after a date
date:<2026-04-01Sessions before a date
tags:rustPages mentioning a tag/topic
sort:dateSort results by date (newest first)

Example: type:session project:llm-wiki date:>2026-04 sort:date