# プロファイル・ページ・プラットフォーム機能の設計案

OS境界・データ構造・安全性の設計。画面と入力規則は [操作仕様](interaction-spec.md)、受入条件は [要件定義](requirements.md)、残る判断・実装差分・検証結果は [レビュー項目](review-register.md) に置く。以下のRegistry・Gateway等は論理責務の名前であり、同名クラスの存在を意味しない。未決案と現行実装は区別する。

画面の確認先は [図解ガイド](../design/visual-guide.html#profiles)。静的な図解であり、移動・認証・Privateの実動作を検証するものではない。

## 基本構造

表示言語はプロファイルやWorkspaceの設定ではなく、Android／AppCompatのアプリ別言語設定として管理する。保存用カテゴリ名・ページ名・分類キーへ翻訳を適用しない。Activityの再生成時は永続モデルを`WorkspaceRuntime`から再利用し、設定の言語選択階層だけを画面復元する。翻訳文言は`values/strings.xml`と`values-ja/strings.xml`へ置き、実行時の英語文字列置換や独自の言語設定ストアは使わない。

「個人・仕事・プライベートのモード切替」をアプリ全体の単一モードにしない。現在の画面を安定した PageId で指定し、各ページが一つのプロファイルを参照する。表示先のページ種別と、アクセス可否は別に判定する。

```text
Workspace（現在のAndroidユーザーで動くAV8）
  ProfileRegistry（OSから確認できたプロファイル群）
  PageRegistry（ページID・種別・対象プロファイル）
  ProfilePageOrder（各プロファイル内のスワイプ順）
  ProfileDestinations（下部のプロファイル切替）
  PrivateContainer（明示的に開く専用領域）
  UiSession（現在地・検索・編集・ダイアログ）
```

ProfileRegistry は2件固定の配列でも personal/work の二択でもない。OSからアクセスを許可されたものだけを登録する。別Androidユーザー・ゲストを、同じランチャー内の追加ページとして列挙する設計にはしない。

### 現行Androidとの対応

| 論理責務 | 現行実装 |
| --- | --- |
| プロファイル・アプリ・ショートカットの取得と起動 | `AppCatalog`。OS通知・状態更新の調整は`Av8LauncherActivity` |
| 設定・通知表示 | `SettingsScreen`の専用面。`BadgeSettings`の表示設定とOSアクセスを分離。`BadgeListener`は本文を保存しない |
| ウィジェット | `WidgetSupport`で提供元の列挙・再検証・実プレビュー。`WidgetPicker`で選択、`ResizableWidget`でサイズの下書きと確定を分離 |
| ページ縮小画像 | `PagePreviews`と`PageOverview`。実グリッド・RemoteViews、縦横比維持、公開のみ・一時メモリ・各50万画素以下 |
| プロファイル別ページ・カテゴリ・並び順 | `Workspace.Board`配下の`Page`・`Group`。カテゴリ色は`CategoryColors`の明暗ペア |
| 共有モデル・保存キュー | `WorkspaceRuntime`。公開配置は`WorkspaceStore`のAtomicFile・v2 JSON、v1を読み取り移行 |
| Private保存 | `PrivateWorkspaceStore`のAES-GCM＋Android Keystore。OSの解除完了前には復号・上書きしない |
| 編集・選択・ドラッグ | `UiSession`。現在プロファイル・ページ・検索入力と画面調整はActivityが所有しており、全UI状態を一クラスへ集約した実装ではない |
| 表示・下部ナビ・ページ遷移 | `BoardAdapter`、`LauncherDock.State`、`PageOverview`、`PageSlide`。表示状態が不変なら下部Viewを維持 |
| 通知・ウィジェット | `BadgeListener`、`AppWidgetHost`とActivity。ウィジェット操作を独立Gatewayへ分離した実装ではない |

## テキスト整理の実装境界

AUTO-01〜04の形式・適用・Undoの正本は[操作仕様](interaction-spec.md#app-auto-organize)。設定から現在の公開Boardだけを扱う軽量な補助機能。`AppOrganizer`はAndroid UI・ネットワークから独立し、入力テキストと現在のアプリ／カテゴリから、解釈可能な所属・箱内順序とスキップ数を導出する。`AppOrganizationFlow`が明示的なクリップボード操作・入力モーダル・最新一覧の照合を担当する。新しい設定ファイル形式・一時アプリID・LLM通信層・汎用インポート基盤は導入しない。

パッケージ／コンポーネントは外部入力なので、対象プロファイルの現在のAppCatalogに存在する通常アプリにだけ解決する。入力からOSコマンド・Intent・アプリ起動・APKインストール・ファイルパスを実行しない。Privateとピン留めショートカットは交換テキストにも共有Undoにも含めない。クリップボードは明示操作で読み書きし、入力全文・アプリ一覧を診断ログへ記録しない。アプリ名／カテゴリ名に含まれる識別子風の文字列や#を、別のアプリ行として出力しない。カテゴリ名は一行見出しとして扱う。

適用とUndoは現在モデルのコピーを作り、`WorkspaceRuntime.commitOrganization`の順序付き保存キューで一括保存する。成功まで表示モデルと既存Undoを置き換えない。処理中はプロセス共有の`organizing`で別の編集を抑止し、カタログ適用とウィジェット結果の確定を延期する。設定の適用前には最新一覧を取得し、書込み直前にもOSのアクセス可否を再確認する。書込み開始前に入力画面をHOME・停止・再生成で閉じたら中止し、遅い結果で設定を勝手に開き直さない。書込み開始後は保存結果を共有モデルへ反映し、破棄済みActivityへUI通知しない。コピー時点と適用時点の一致を要求しない方針と、処理中の更新競合防止は別問題である。

Undoは`WorkspaceRuntime.organizationUndo`にプロセス内・公開プロファイル別の1世代だけ保持する。保存モデル全体のバックアップではなく、アプリ配置と今回作成したカテゴリIDに限定し、色・ページ・ウィジェット等を巻き戻さない。パーサー／モデルのJVM試験、保存成功・失敗・アクセス喪失の試験と設定・クリップボード・停止・再生成の操作試験を追加した。テスト用fixtureの配置は実ファイルへ保存せず、別の実一覧ケースでAtomicFileへの保存と再生成を確認する。

## ナビゲーションと隔離の境界

一般の下部UI・スワイプ・ページ管理は操作仕様に集約する。モデル側ではプロファイルごとに独立したページ順序を持ち、別プロファイルのページやPrivateを混ぜない。追加公開プロファイルも同じ構造で扱う。

- 公開直通は選んだ2件までとし、残りは明示的なプロファイル選択から開く案（Q-11）。現在プロファイル内のページ一覧と、プロファイル選択を混同しない。データの件数を表示枠数に固定しない。
- Privateは公開直通へピン留めしない。設定のプライバシー欄に共通入口を置き、その有無から領域の存在を示さない。ページ一覧には混ぜない。非表示設定では一覧の行・件数・空き枠・プレビューを残さない。Privateのページ／ウィジェット管理操作は能力未確認のため表示しない。
- Private内は専用のカテゴリUIとし、公開画面への退出と「ロックして退出」を区別する。戻るによる退出とOSの自動ロックも区別し、私的なプレビューや位置を公開UIへ持ち出さない。未決点はQ-12。

## プロファイル別の扱い

| 対象 | 一覧への入口 | 利用不可時 | ウィジェット |
| --- | --- | --- | --- |
| 現在の通常ユーザー | 個人アプリ直通 | 一時取得失敗は再取得。削除とみなさない | OSが列挙・バインドを許可したもの |
| Managed / 仕事用 | 仕事アプリ直通とページ一覧 | ページを残し「一時停止」「停止解除」。取得不能は停止と断定しない | 管理側に許可された提供元のみ |
| Private | 専用コンテナへの明示的入口 | ロック時はアプリ・検索結果・プレビューを出さない。領域の非表示設定も尊重 | 対応可能と仮定しない。公的APIと実機で確認できるまで追加不可 |
| Clone / OEM固有 | OSがランチャーに公開する範囲で登録 | 未知の種類を仕事用として扱わない | 検出した能力に従う。未確認の機能は提供しない |
| 別Androidユーザー・ゲスト | AV8の同一画面へは統合しない | OS側のユーザー切替で扱う | そのユーザーで動くAV8の別Workspace |

Private、Cloneの有無は端末依存であり、この表は全端末で作成できるという保証ではない。公開APIを介して到達できないOEM独自機能は、種類名を追加するだけでは対応できない。

## 永続モデル

ホーム背景色はWorkspaceとは独立したappearance設定に任意のlight/darkペアと透明度を保持する。カテゴリ透明度とは別キー。RootのDrawableで壁紙の前に描画し、ページ遷移のスライド対象にしない。下部・左右のinsetは同Drawableで不透明に塗り、OSのedge-to-edgeでもナビゲーションバーが壁紙に埋もれない。背景編集の下書きはActivity内の単一オーバーレイの寿命に限定し、取消・dismiss・離脱で破棄する。

AppWidgetHostは共通host IDを使うため、`WorkspaceRuntime`が現在の監視所有者を記録する。HOME再入場で新Activityが監視を開始した後に旧Activityが停止しても、新しいホストの監視を解除しない。許可撤回はOSの提供元情報で再判定し、内容を隠して配置を維持する。試験もActivityScenarioの最初のインスタンスではなく、実際に前面にいるHOMEを使って保存状態を検証する。

| エンティティ | キーとデータ | 制約 |
| --- | --- | --- |
| ProfileRef | AV8内部ID、OS serialとの対応、OS種別、表示ラベル | enumの個人/仕事二択で識別しない。OS UserHandleは実行時に解決 |
| Page | PageId、ProfileRef、Apps / Widgets、表示ラベル | ページIDにプロファイル番号や配列位置を埋め込まない |
| ProfilePageOrder | ProfileRefごとのPageIdの順序 | 同一プロファイルだけで構成し、Privateを公開列へ混ぜない |
| ProfileDestination | ProfileRefの参照 | 切替UIの設定。ページ番号や固定2件配列と同一視しない |
| Collection | CollectionId、PageId、名前、順序、通常の表示状態、任意のライト／ダーク色ペア | 個人と仕事は定義も独立。同名を同一IDとして扱わない。透明度はカテゴリ別ではなく全体設定 |
| AppItem | ProfileRef＋ComponentName、CollectionId、順序、常時表示指定 | 同じGmailでもプロファイルが違えば別項目 |
| ShortcutItem | ProfileRef＋packageName＋shortcutId、所属と順序 | アプリ項目と別種。OSが現在許可するものだけを扱う |
| WidgetItem | PageId、ProfileRef、provider、ローカルappWidgetId、配置・サイズ | appWidgetIdを端末移行後も不変のIDとみなさない |

一つのアプリページは一つのプロファイルだけを表示する。アプリのカテゴリ間移動でProfileRefを変更しない。別プロファイルへのインストールやデータ移行は、アイコンのドラッグ移動とは別操作とする。

ウィジェットページも原則一つのProfileRefに所属させ、提供元と一致させる。ページがあることは提供元を利用できる保証にはならない。混在ウィジェットページは今回の設計範囲外。

OSのserialは同じ環境内で照合し、バックアップを別端末へ戻す際の本人確認には使わない。再作成された仕事用やPrivateに古い配置を自動で結び付けない。Privateの配置・カテゴリ名・色ペアは専用の暗号化保存へ隔離し、公開の平文JSONへ混入させない。Privateの画像キャッシュは残さず、OSの解除完了前には保存内容を復号・上書きしない。端末移行・バックアップと鍵消失時の復旧方針は未確定。

## 実行時状態と能力

種別や quietModeEnabled 一つから全UIを決めない。OSで観測した事実と、そこからAV8が計算した操作可能性を分ける。

```text
ProfileObservation
  presence: Present / Unknown / Removed
  access: Ready / Quiet / AuthenticationRequired / Unavailable
  privateContainerHidden: true / false / unknown
  generation: 更新世代

ResolvedCapabilities
  canListApps, canLaunchApps, canQueryShortcuts
  canListWidgetProviders, canBindProvider(provider)
  canRequestResume, canRequestLock, canOpenProfileSettings
```

能力はAPIレベル、既定HOME資格、OSの回答、管理ポリシーと操作結果から解決する。これら全てを一度に返す共通Android APIが存在するという意味ではない。未確認は許可済みと扱わず、要求拒否・認証待ちも結果として返す。

別に UiSession は `Browse / AppMenu / AppReorder / CollectionReorder / PagePicker / Dialog` を排他的に持つ。選択集合、ドラッグ、入力ロックはそこから導出し、ProfileObservation内へ混ぜない。OSからロック・停止が来たら、対象の編集・ダイアログ・ドラッグを中断してから表示を更新する。

通知は`BadgeListener`でプロファイル・パッケージ単位に保持し、本文はキャッシュしない。表示の設定とOSの許可を別に扱う。ショートカットはプロファイル・パッケージ・shortcutIdで区別する。受入条件はNTF/ACT/SCT、表示の決定は操作仕様のQ-09/10を参照する。

## 検索・停止・復帰の規則

`WorkspaceRuntime.open`は初回読込を共有し、読み込み済みモデルはディスクキューを経由せずmainへpostする。`CatalogRefreshGate`がActivityごとの取得を実行中1件＋後続1件へ集約する。`AppCatalog`はラベルを各アプリ1回取得してから文字列でソートする。公開アイコンのConstantStateだけをプロセス内LRU（128件）へ保持し、表示用Drawableは複製する。APKパス・構成変更・パッケージ通知で更新し、Privateはキャッシュ経路を通さない。アイコン読込中にmain側の無効化がロック待ちしないよう、供給処理は同期区間の外で行う。

`Av8Startup`ログは復帰からアプリセル描画まで、配置のキュー待ち／読込、一覧のキュー待ち／取得／UI配送待ち／適用、ウィジェット監視開始の所要msを分ける。アプリ名・ID・プロファイル名・Privateの内容は記録しない。描画ログはアプリセルのある画面に限り、OS側の画面点灯時間やプロセス生成時間そのものではない。[Androidの起動時間の区別](https://developer.android.com/topic/performance/issues/launch-time)に従い、コールド／ウォーム／既存画面への復帰を同じ測定として扱わない。性能の受入値は実機で確認する。

保護されたOSのプロファイル通知は公開受信、アプリ内のバッジ通知は非公開受信へ分離する。取得結果はUI適用直前にもQuiet／Unlockedを再確認し、停止中のキャッシュを表示しない。解除要求は要求中の観測と分離し、短い上限付きの再試行でOSの準備完了を待つ。一時的な空一覧をそのまま完全なアンインストールと扱わない。既存コンポーネントが有効なら配置は保持して再取得する。具体的な遅延と表示規則は操作仕様を参照する。

受信範囲は[Androidのブロードキャスト仕様](https://developer.android.com/develop/background-work/background-tasks/broadcasts)に沿って分離し、所属プロファイルの準備状態は[UserManager.isUserUnlocked](https://developer.android.com/reference/android/os/UserManager#isUserUnlocked(android.os.UserHandle))で確認する。

- 通常の検索対象は現在のアプリページのプロファイル。プロファイル切替時に検索を終了し、ページ・スクロールは実行中の前回位置へ戻す。検索入力は復元しない。復帰方針は操作仕様のQ-07/11。横断検索は将来の明示的機能とする。
- Privateの検索は専用コンテナの中だけ。ロック通知時は結果・アイコン・サジェストを撤去し、処理中の検索結果も更新世代で破棄する。
- 仕事用停止中もアプリ・ウィジェットページのIDと配置を保持する。仕事用ページは停止画面に切り替え、ウィジェットのRemoteViews内容を表示し続けない。解除要求の戻り値だけで復帰を確定しない。
- 一覧の取得失敗はRemovedではない。対象だけをUnavailableにし、個人へのフォールバックで仕事用アプリを代替起動しない。
- Privateのロック中は所属アプリ名・件数・カテゴリ名を公開UIに出さない。ロック前のスクリーンショットやページプレビューを保持しない。
- アプリ・ウィジェット・ショートカットの削除は、アクセス可能な完全な一覧やOSの確定した削除情報から判定する。ロック・停止を根拠に配置を剪定しない。

## Androidとの境界

OS依存処理と表示・保存の責任範囲を区別する。現行コードの担当は上の対応表のとおりであり、`ProfileGateway / ShortcutGateway / WidgetGateway`や汎用Repositoryは実装していない。将来分離する場合も、同名サービスを増やすこと自体を目的にせず、入力状態・許可・コマンド結果の境界を明確にする。

バージョン依存の呼び出しをこの境界へ閉じ込め、未対応の能力は理由付きで返す。登録時に使う識別子と、公開してよい表示情報も別に扱う。

## 必須の受入シナリオ

| シナリオ | 期待する結果 |
| --- | --- |
| 個人Gmailと仕事Gmailを別カテゴリへ配置 | 所属・順序・起動先が独立 |
| 仕事用で編集中に停止 | 編集を中止、配置は保持、停止画面へ |
| 解除で認証が必要／拒否される | 成功と断定せず、再操作できる |
| 仕事用一覧だけ取得失敗 | 個人と配置へ影響せず、一時利用不可として保持 |
| Private検索中にロック | 結果と進行中応答を破棄。公開画面に内容を出さない |
| Privateを非表示にする | 公開ナビの件数・サムネイルにも残さない |
| 公開対象を3件以上追加 | 直通2枠の外も一覧から到達可能。公開ページ順を型として固定しない |
| ウィジェットの管理許可が撤回 | 内容・操作を停止。アプリページと保存配置は消さない |
| プロファイル削除後に同種を再作成 | 別のProfileRefとして扱い、古い項目を自動起動しない |
| 保存データ復元・未知のページ種別 | 有効なページを維持し、未対応部分だけ復旧待ちにする |

これらは設計上の受入条件であり、全件の実施済みを示す表ではない。仕事用停止復帰、Privateの実PIN認証・再ロック・暗号化保存等の実施済み範囲はレビュー項目の検証結果を参照する。

## 根拠と未確認範囲

- [Android 15: Private space / Launcher apps](https://developer.android.com/about/versions/15/behavior-changes-all#private-space): HOME資格と権限、専用コンテナ、表示・ロックと検索の要件。上の隔離方針の根拠。
- [AOSP: Support multiple users](https://source.android.com/docs/devices/admin/multi-user): user/profileの区別とPrivate・Clone・OEM型。OEM依存機能を一律保証しない根拠。
- [AppWidgetManager](https://developer.android.com/reference/android/appwidget/AppWidgetManager): プロファイル指定による提供元列挙・バインド。
- [DevicePolicyManager.addCrossProfileWidgetProvider](https://developer.android.com/reference/android/app/admin/DevicePolicyManager#addCrossProfileWidgetProvider(android.content.ComponentName,java.lang.String)): 仕事用提供元の公開には管理側許可が関係する。

プロファイル内のページ並べ替え、設定からのPrivate入口、Private配置の暗号化保存、3件以上の公開プロファイル選択は実装済み。バックアップ・復旧、認証と管理ポリシーの網羅確認、ウィジェットのPrivate/OEM対応はレビュー・実機検証が必要。Private専用設定のAOSP認証Activityは非exportedのため、公開のセキュリティ設定へ委譲する（[Android 15 Settings manifest](https://android.googlesource.com/platform/packages/apps/Settings/+/refs/heads/android15-release/AndroidManifest.xml)）。非公開コンポーネントの起動や制限の迂回は行わない。
