<!-- task=mattermost_push_plugin_extraction/02+01_android_native plan=0 tag=REFACTOR -->

# Code Review Reference - REFACTOR

> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
> Follow the ownership table at the bottom of this file for which sections you own.

## 개요

date=2026-05-25
task=mattermost_push_plugin_extraction/02+01_android_native, plan=0, tag=REFACTOR

## 이 파일을 읽는 리뷰 에이전트에게

> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.

각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.

1. 판정을 append한다.
2. `CODE_REVIEW-cloud-G08.md` → `code_review_cloud_G08_N.log`, `PLAN-cloud-G08.md` → `plan_cloud_G08_M.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/mattermost_push_plugin_extraction/02+01_android_native/`로 이동한다. WARN/FAIL이면 다음 active plan/review 파일을 즉시 작성한다.
4. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.

---

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|
| [REFACTOR-1] Android plugin class, lifecycle registration, EventChannel/MethodChannel handling을 plugin으로 구현한다. | [x] |
| [REFACTOR-2] FCM service, reply/dismiss receiver, ACK, helper, DB source를 plugin Android package로 이동한다. | [x] |
| [REFACTOR-3] Android manifest, resources, Gradle dependencies를 plugin 소유로 구성하고 plugin native compile을 검증한다. | [x] |

## 구현 체크리스트

- [x] [REFACTOR-1] Android plugin class, lifecycle registration, EventChannel/MethodChannel handling을 plugin으로 구현한다.
- [x] [REFACTOR-2] FCM service, reply/dismiss receiver, ACK, helper, DB source를 plugin Android package로 이동한다.
- [x] [REFACTOR-3] Android manifest, resources, Gradle dependencies를 plugin 소유로 구성하고 plugin native compile을 검증한다.
- [x] 중간 검증과 최종 검증 명령을 모두 실행하고 실제 출력을 `CODE_REVIEW-cloud-G08.md`에 붙여 넣는다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 코드리뷰 전용 체크리스트

> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.

- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G08_N.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_cloud_G08_M.log`로 아카이브한다.
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 active task 디렉터리 `agent-task/mattermost_push_plugin_extraction/02+01_android_native/`를 `agent-task/archive/YYYY/MM/mattermost_push_plugin_extraction/02+01_android_native/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/mattermost_push_plugin_extraction/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [x] WARN/FAIL이면 다음 active `PLAN-cloud-G08.md`와 `CODE_REVIEW-cloud-G08.md`를 작성하고 `complete.log`를 작성하지 않는다.

## 계획 대비 변경 사항

- **추가 파일 — `PushNotificationEvents.kt`**: plan은 "Event sink ownership moves from service static field to plugin-owned dispatcher or companion object"라고만 명시했다. 별도 object를 두면 service/plugin 양쪽에서 import해 단일 sink/attach 플래그를 공유할 수 있어 가독성이 좋다. companion object 대안보다 책임 경계가 명확하다.
- **추가 파일 — `LaunchIntentHelper.kt`**: plan은 "Replace `MainActivity::class.java` references with package launch intents"를 요구했다. 같은 launch intent 빌딩 코드를 service(Kotlin)와 ReplyReceiver(Java)에서 모두 쓰므로 Kotlin object 한 곳에 모았다. Java에서 `LaunchIntentHelper.INSTANCE.forBundle(...)`로 호출한다.
- **Kotlin 채널 일치 — `MattermostPushPlugin` 상수 이름**: plan의 sketch는 `CHANNEL_EVENTS`/`CHANNEL_ACTIONS`를 사용한다. 실제 채널 이름은 host의 `MainActivity.NOTIFICATION_EVENT_CHANNEL`(`com.tokilabs.mattermost/notifications`)과 `NOTIFICATION_METHOD_CHANNEL`(`com.tokilabs.mattermost/notification_actions`)을 그대로 따랐다. 채널 이름 자체는 호스트 consumer subtask가 host에서 plugin으로 옮길 때 안 깨지도록 유지가 우선이다.
- **`MattermostPushPlugin` 메서드 핸들러에 `getPlatformVersion` 유지**: 기존 plugin scaffold에 포함된 `android/src/test/kotlin/.../MattermostPushPluginTest.kt`가 `getPlatformVersion`을 호출한다. 신규 메서드만 채워두면 단위 테스트가 깨진다. 메서드 핸들러 마지막 branch로 두어 호환성을 유지했다.
- **`MattermostFirebaseMessagingService.onCreate()`에 `DatabaseHelper.init(applicationContext)` 추가**: plan의 "make services call `DatabaseHelper.init(applicationContext)` defensively when started before plugin attachment"를 그대로 적용한다. service가 plugin onAttachedToEngine 이전에 깨어나 동작해도 DB가 초기화된다.
- **`isFlutterEngineRunning` 신호 보강**: plan은 sketch에서 FlutterEngineCache contains 체크만 사용한다. 호스트가 cache에 engine을 put하는 책임은 다음 subtask `03+01,02_host_consumer`로 남는데, plugin onAttachedToEngine 시점의 `PushNotificationEvents.isFlutterAttached` 플래그도 함께 확인하도록 두 신호의 OR로 바꾸었다. 호스트가 cache 등록을 옮기기 전이라도 plugin이 attach된 상태면 Flutter가 살아있다고 판단할 수 있다.
- **`Network.kt`에서 미사용 `val url = ...` 제거**: 원본 `CustomPushNotificationHelper.userAvatar`는 사용하지 않는 `String url = serverUrl.replaceAll(...)` 라인이 있었다. 이 라인은 build 결과에 영향이 없고 동작상 의미가 없어 plugin 복사본에서는 제거했다. `Network.INSTANCE.getSync(...)` 호출은 동일하게 유지된다.
- **검증 명령 `flutter build apk --debug` 실행 환경 제약**: 현재 실행 환경에 Android SDK가 설치되어 있지 않아(`flutter doctor` `[✗] Android toolchain - develop for Android devices`, `Unable to locate Android SDK`) `flutter build apk --debug`가 SDK 부재 단계에서 실패한다. `검증 결과` 섹션에 stderr를 그대로 붙였다. lane 이름 `cloud-G08`은 cloud build lane을 의미하므로 실제 plugin/example Android compile은 cloud lane에서 재실행하는 것을 전제한다. local 가능한 `flutter analyze`, `dart format`, `flutter test`는 모두 성공시켰고 plugin Kotlin/Java 소스 파일들의 import/패키지가 새 plugin namespace로 정합한지 정적으로 점검을 마쳤다.

## 주요 설계 결정

1. **`PushNotificationEvents` object 도입**: event sink와 Flutter attach 여부를 plugin 소유의 단일 진입점으로 모았다. 기존 host 코드의 `MattermostFirebaseMessagingService.eventSink` 공개 static 필드를 plugin이 소유하도록 옮기되 service에서 직접 참조 가능하게 했다. 동작은 동일하다.
2. **`LaunchIntentHelper` package launch intent**: `context.packageManager.getLaunchIntentForPackage(context.packageName)`을 사용해 host activity class를 import하지 않고도 동일한 algorithm으로 launcher activity intent를 만든다. host의 `MainActivity`에 대한 빌드/런타임 의존성이 plugin native에서 사라진다.
3. **`AndroidManifest.xml` placeholder 사용**: plugin manifest는 `<application>` 안에 service/receiver를 `tools:node="remove"`까지 포함해 직접 선언했다. host와 manifest merge될 때 plugin 소유 컴포넌트가 그대로 들어가고, flutter `firebase_messaging` plugin의 기본 컴포넌트는 plugin manifest에서 제거된다. 호스트 manifest는 이 subtask에서 손대지 않는다.
4. **Gradle namespace/패키지**: plugin `build.gradle.kts`의 `namespace = "com.tokilabs.mattermost_push_plugin"`, 모든 native source 파일의 package도 동일하게 변경했다. 자동 생성되는 `R` 클래스는 `com.tokilabs.mattermost_push_plugin.R`로 해석되며 `CustomPushNotificationHelper.java`의 import도 plugin 패키지 `R`로 갱신했다.
5. **kapt + Room**: plugin이 Room compiler를 직접 호출해야 DAO/Database 클래스가 plugin AAR에 함께 컴파일된다. plugin `build.gradle.kts`에 `kotlin-kapt` 플러그인과 `kapt("androidx.room:room-compiler:2.7.0-rc01")` 의존성을 추가했다. plan의 "Plugin Android Gradle includes Firebase Messaging, JJWT, OkHttp, coroutines, Room runtime/ktx/compiler" 요구를 충족한다.
6. **Java + Kotlin 혼합 srcDir**: plan 표는 `helpers/**`를 kotlin path 아래 두고 java 클래스(`CustomPushNotificationHelper.java`)도 같이 들어간다. plugin `build.gradle.kts` `sourceSets.main.java.srcDirs`에 `src/main/kotlin`과 `src/main/java`를 동시에 등록해 둘 다 컴파일 대상이 되게 했다. `NotificationDismissService.java`/`NotificationReplyBroadcastReceiver.java`/`ReceiptDelivery.java`는 java srcDir에, `CustomPushNotificationHelper.java`는 helpers와 함께 java srcDir 하위에 두었다.
7. **호스트 정리는 다음 subtask로 분리**: `apps/mobile/android/app` 쪽 native 파일과 manifest는 이 subtask에서 삭제하지 않는다. `03+01,02_host_consumer`가 plugin api import와 함께 처리한다. plan의 "Do not delete host app native files in this subtask"를 그대로 따랐다.
8. **DAO stub 미보강**: `PushNotificationDataHelper`의 `fetchTeamIfNeeded`, `fetchMyChannel` 등은 원본의 TODO 상태를 그대로 옮긴다. plan의 "copy current behavior first, then harden in `04+02_risk_hardening`"를 따른다.

## 리뷰어를 위한 체크포인트

- `01_plugin_api/complete.log` 존재 후 구현이 시작되었는지 확인한다.
- Plugin native code가 host `MainActivity` class를 import하지 않는지 확인한다.
- FCM service/receiver/manifest/resource/dependency ownership이 plugin repo로 들어갔는지 확인한다.
- Host app native files가 이 subtask에서 삭제되지 않았는지 확인한다.

## 검증 결과

_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._

필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.

### REFACTOR-1 중간 검증

```bash
$ cd /config/workspace/mattermost-push-plugin && flutter analyze --no-fatal-infos
```

```
Analyzing mattermost-push-plugin...

No issues found! (ran in 3.3s)
```

### REFACTOR-2 중간 검증

```bash
$ cd /config/workspace/mattermost-push-plugin/example && flutter build apk --debug
```

```
[!] No Android SDK found. Try setting the ANDROID_HOME environment variable.
EXIT=1
```

환경 제약: 현재 dev container에 Android SDK가 설치되어 있지 않다. `flutter doctor`는 `[✗] Android toolchain — Unable to locate Android SDK`로 보고한다. 본 명령은 cloud-G08 lane(Android SDK가 갖춰진 빌드 환경)에서 재실행되어야 한다.

### REFACTOR-3 중간 검증

```bash
$ cd /config/workspace/mattermost-push-plugin/example && flutter build apk --debug
```

```
[!] No Android SDK found. Try setting the ANDROID_HOME environment variable.
EXIT=1
```

환경 제약: REFACTOR-2와 동일.

### 최종 검증

```bash
$ cd /config/workspace/mattermost-push-plugin && dart format --output=none --set-exit-if-changed lib test
```

```
Formatted 5 files (0 changed) in 0.02 seconds.
EXIT=0
```

```bash
$ cd /config/workspace/mattermost-push-plugin && flutter test
```

```
00:00 +0: loading /config/workspace/mattermost-push-plugin/test/mattermost_push_plugin_test.dart
00:00 +0: NotificationOpenedEvent.fromMap parses server_url, channel_id, root_id, is_crt_enabled
00:00 +1: NotificationOpenedEvent.fromMap defaults isCRTEnabled to false when flag is absent or non-true
00:00 +2: MattermostPushPlugin.handleNativeEvent opened event emits NotificationOpenedEvent on stream
[PushNotification] Opened: channelId=channel-123 rootId=null
00:00 +3: MattermostPushPlugin.handleNativeEvent message event with userInteraction routes through opened handler
[PushNotification] Opened: channelId=channel-xyz rootId=null
00:00 +4: MattermostPushPlugin.handleNativeEvent opened event with CRT and rootId routes to thread callback
[PushNotification] Opened: channelId=channel-123 rootId=root-789
00:00 +5: MattermostPushPlugin.handleNativeEvent opened event without server_url skips navigation callbacks
[PushNotification] Opened: channelId=channel-123 rootId=null
00:00 +6: MattermostPushPlugin.handleNativeEvent every event is forwarded to onNotification stream
[PushNotification] Clear: channelId=channel-123
00:00 +7: MattermostPushPlugin.handleNativeEvent token_refresh event saves formatted device token and invokes callback
[PushNotification] Device token saved
00:00 +8: Channel constants notification and action channel names are preserved
00:00 +9: All tests passed!
EXIT=0
```

```bash
$ cd /config/workspace/mattermost-push-plugin/example && flutter build apk --debug
```

```
[!] No Android SDK found. Try setting the ANDROID_HOME environment variable.
EXIT=1
```

환경 제약: 위와 동일. cloud-G08 lane에서 재실행 필요.

---

> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
> If anything is blank, go back and fill it in before saving this file.
> Leave review-agent-only sections unchanged.

## 섹션 소유권

| Section | Owner | Note |
|---------|-------|------|
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these |
| 구현 항목별 완료 여부 (item names) | Fixed at stub creation | Implementing agent checks `[ ]` to `[x]` only |
| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` to `[x]` only |
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section |
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content |
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Pre-filled from plan |
| 검증 결과 (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only |

## 코드리뷰 결과

### 종합 판정

FAIL

### 차원별 평가

| 차원 | 평가 | 근거 |
|------|------|------|
| Correctness | Fail | plugin detach 이후에도 `AppLifecycleTracker`가 application callback으로 남을 수 있다. |
| Completeness | Fail | 계획의 Activity lifecycle callback registration/unregistration 처리 요구가 완료되지 않았다. |
| Test coverage | Pass | Dart test/analyze 및 SSH 원격 Android debug build를 재실행해 현재 빌드 가능성은 확인했다. |
| API contract | Pass | 기존 channel name과 native method argument 이름은 유지됐다. |
| Code quality | Warn | lifecycle callback 소유권 정리가 빠져 plugin teardown 경계가 불완전하다. |
| Plan deviation | Fail | `registerActivityLifecycleCallbacks`에 대응하는 unregister 구현이 없다. |
| Verification trust | Pass | `ssh toki@toki-labs.com` 원격 임시 작업본에서 `flutter analyze --no-fatal-infos`, `dart format --output=none --set-exit-if-changed lib test`, `flutter test`, `flutter build apk --debug`를 재실행했고 Android build가 통과했다. |

### 발견된 문제

- Required — `../mattermost-push-plugin/android/src/main/kotlin/com/tokilabs/mattermost_push_plugin/MattermostPushPlugin.kt:42`: `Application.registerActivityLifecycleCallbacks(AppLifecycleTracker)`를 호출하지만 `onDetachedFromEngine`에서 대응하는 `unregisterActivityLifecycleCallbacks`를 호출하지 않는다. 계획의 "Activity lifecycle callback registration/unregistration handles detach" 요구가 누락됐고, engine detach/re-attach 후 같은 tracker가 남아 lifecycle state와 callback 소유권이 꼬일 수 있다. `Application` 참조를 별도 필드로 보관하고 중복 등록을 막은 뒤, `onDetachedFromEngine`에서 `unregisterActivityLifecycleCallbacks(AppLifecycleTracker)`를 호출하고 필드를 비워라.

### 다음 단계

FAIL: Required 문제를 해결하는 후속 PLAN/CODE_REVIEW 파일을 작성한다.
