diff --git a/Assets/Plugins/Android/AutofillBridge.java b/Assets/Plugins/Android/AutofillBridge.java
new file mode 100644
index 0000000..e362194
--- /dev/null
+++ b/Assets/Plugins/Android/AutofillBridge.java
@@ -0,0 +1,323 @@
+package com.photonmatrix.autofill;
+
+import android.app.Activity;
+import android.graphics.Color;
+import android.text.Editable;
+import android.text.InputFilter;
+import android.text.InputType;
+import android.text.TextWatcher;
+import android.view.Gravity;
+import android.view.inputmethod.InputMethodManager;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.FrameLayout;
+import com.unity3d.player.UnityPlayer;
+
+/**
+ * 隐形原生输入桥(Android 谷歌密码自动填充)
+ *
+ * 原理:在 Unity InputField 的屏幕位置上叠加一个视觉隐形(低透明度、无背景)的原生 EditText,
+ * 通过 autofillHints 让谷歌密码管理器识别该输入框,从而提供填充建议和"保存密码"提示。
+ * 输入内容通过 TextWatcher 实时回传给 Unity 侧同步显示。
+ */
+public class AutofillBridge {
+ private static final String TAG = "[AUTOFILL-BRIDGE]";
+ private static final String RECEIVER_GO_NAME = "AutofillBridgeReceiver";
+
+ private static EditText sEditText;
+ private static EditText sCompanionUsername; // 密码页配套的隐形用户名框(用于成对保存账号密码)
+
+ /**
+ * 显示隐形输入桥
+ * @param hint autofillHints 值:"emailAddress" / "username" / "password"
+ * @param initialText 初始文本(Unity InputField 当前值)
+ * @param x 屏幕像素坐标(Android 屏幕坐标系,y 从顶部开始)
+ * @param y 同上
+ * @param w 宽(像素)
+ * @param h 高(像素)
+ * @param maxLength 最大字符数(0 表示不限制)
+ * @param companionUsername 密码输入时配套的账号(非空则额外挂一个隐形用户名框,用于成对保存)
+ */
+ public static void show(final String hint, final String initialText,
+ final int x, final int y, final int w, final int h,
+ final int maxLength, final String companionUsername) {
+ final Activity activity = UnityPlayer.currentActivity;
+ if (activity == null || activity.isFinishing()) {
+ return;
+ }
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ showInternal(activity, hint, initialText, x, y, w, h, maxLength, companionUsername);
+ }
+ });
+ }
+
+ private static void showInternal(Activity activity, String hint, String initialText,
+ int x, int y, int w, int h,
+ int maxLength, String companionUsername) {
+ try {
+ hideInternal(activity, false);
+
+ final EditText editText = new EditText(activity);
+ editText.setSingleLine(true);
+ editText.setBackground(null); // 去背景,视觉隐形
+ editText.setAlpha(0.08f); // 低透明度"伪可见":完全隐藏时 autofill 服务不生效
+ editText.setHint(null);
+ editText.setTextColor(Color.TRANSPARENT);
+ editText.setCursorVisible(false);
+
+ // 输入类型与 autofillHints
+ editText.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_YES);
+ editText.setAutofillHints(hint);
+ if ("password".equals(hint)) {
+ editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
+ editText.setImeOptions(android.view.inputmethod.EditorInfo.IME_ACTION_DONE);
+ } else if ("emailAddress".equals(hint)) {
+ editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS);
+ editText.setImeOptions(android.view.inputmethod.EditorInfo.IME_ACTION_NEXT);
+ } else {
+ editText.setInputType(InputType.TYPE_CLASS_TEXT);
+ editText.setImeOptions(android.view.inputmethod.EditorInfo.IME_ACTION_DONE);
+ }
+
+ if (maxLength > 0) {
+ editText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(maxLength)});
+ }
+
+ if (initialText != null && !initialText.isEmpty()) {
+ editText.setText(initialText);
+ editText.setSelection(initialText.length());
+ }
+
+ // 文本变化实时回传 Unity(含 autofill 填充的值)
+ editText.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (sEditText == editText) {
+ UnityPlayer.UnitySendMessage(RECEIVER_GO_NAME, "OnNativeTextChanged", s.toString());
+ }
+ }
+ });
+
+ // IME 完成键:软隐藏(保留视图供登录时 commit 快照采集)
+ editText.setOnEditorActionListener(new android.widget.TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(android.widget.TextView v, int actionId, android.view.KeyEvent event) {
+ softHide();
+ return false;
+ }
+ });
+
+ // 失焦时软隐藏(兜底);isFocusable 检查防递归
+ editText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (!hasFocus && sEditText == v && v.isFocusable()) {
+ softHide();
+ }
+ }
+ });
+
+ // 位置:与 Unity InputField 屏幕矩形重合
+ ViewGroup root = (ViewGroup) activity.getWindow().getDecorView();
+ FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
+ Math.max(w, 1), Math.max(h, 1));
+ lp.gravity = Gravity.TOP | Gravity.START;
+ root.addView(editText, lp);
+ editText.setTranslationX(x);
+ editText.setTranslationY(y);
+ root.requestLayout();
+
+ sEditText = editText;
+
+ // 密码场景:配套一个隐形"用户名"框,让保存的凭据账号密码成对
+ if (companionUsername != null && !companionUsername.isEmpty()) {
+ EditText companion = new EditText(activity);
+ companion.setSingleLine(true);
+ companion.setBackground(null);
+ companion.setAlpha(0.03f);
+ companion.setTextColor(Color.TRANSPARENT);
+ companion.setCursorVisible(false);
+ companion.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_YES);
+ companion.setAutofillHints(View.AUTOFILL_HINT_USERNAME);
+ companion.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS);
+ companion.setText(companionUsername);
+ companion.setFocusable(false); // 不参与焦点/填充请求,仅参与保存快照
+ companion.setClickable(false);
+ ViewGroup.LayoutParams clp = new FrameLayout.LayoutParams(Math.max(w, 1), Math.max(h, 1));
+ root.addView(companion, clp);
+ companion.setTranslationX(x);
+ companion.setTranslationY(y);
+ sCompanionUsername = companion;
+ }
+
+ // 请求焦点并弹出软键盘
+ editText.setFocusableInTouchMode(true);
+ editText.requestFocus();
+ InputMethodManager imm = (InputMethodManager) activity.getSystemService(Activity.INPUT_METHOD_SERVICE);
+ if (imm != null) {
+ editText.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (sEditText == editText) {
+ imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);
+ }
+ }
+ }, 100);
+ }
+
+ // autofill 会话诊断:确认系统是否建立了填充会话(1.5 秒后打印状态)
+ editText.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ if (sEditText != editText) return;
+ java.lang.reflect.Method getter = activity.getClass().getMethod("getAutofillManager");
+ Object afm = getter.invoke(activity);
+ if (afm != null) {
+ Object enabled = afm.getClass().getMethod("isEnabled").invoke(afm);
+ Object filled = afm.getClass().getMethod("hasFilledSession").invoke(afm);
+ android.util.Log.d(TAG, "autofill 诊断: enabled=" + enabled
+ + ", hasFilledSession(会话已建立)=" + filled
+ + ", viewFocused=" + editText.isFocused()
+ + ", viewShown=" + editText.isShown()
+ + ", hints=" + java.util.Arrays.toString(editText.getAutofillHints()));
+ } else {
+ android.util.Log.d(TAG, "autofill 诊断: getAutofillManager() 为 null");
+ }
+ } catch (Throwable t) {
+ android.util.Log.e(TAG, "autofill 诊断失败", t);
+ }
+ }
+ }, 1500);
+ } catch (Throwable t) {
+ android.util.Log.e(TAG, "show failed", t);
+ }
+ }
+
+ /**
+ * 隐藏输入桥
+ * @param commitToAutofill 隐藏前是否触发 autofill 保存
+ */
+ public static void hide(final boolean commitToAutofill) {
+ final Activity activity = UnityPlayer.currentActivity;
+ if (activity == null) {
+ return;
+ }
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ hideInternal(activity, commitToAutofill);
+ }
+ });
+ }
+
+ private static void hideInternal(Activity activity, boolean commitToAutofill) {
+ try {
+ // commit 必须在移除视图之前:保存快照采集需要输入框仍在视图树中
+ if (commitToAutofill) {
+ commitInternal(activity);
+ }
+ // 先置空引用再移除,防止移除过程中触发的失焦回调重入 hide 导致 NPE
+ EditText editText = sEditText;
+ EditText companion = sCompanionUsername;
+ sEditText = null;
+ sCompanionUsername = null;
+
+ InputMethodManager imm = (InputMethodManager) activity.getSystemService(Activity.INPUT_METHOD_SERVICE);
+ View focus = activity.getCurrentFocus();
+ if (imm != null && focus != null) {
+ imm.hideSoftInputFromWindow(focus.getWindowToken(), 0);
+ }
+ ViewGroup root = (ViewGroup) activity.getWindow().getDecorView();
+ if (editText != null) {
+ root.removeView(editText);
+ }
+ if (companion != null) {
+ root.removeView(companion);
+ }
+ } catch (Throwable t) {
+ android.util.Log.e(TAG, "hide failed", t);
+ }
+ }
+
+ /**
+ * 软隐藏:收起键盘并解除交互(不可聚焦/不可点击,点击穿透回 Unity),
+ * 但视图保留在视图树上——登录 commit 时 autofill 保存快照仍能采集到输入字段
+ */
+ public static void softHide() {
+ final Activity activity = UnityPlayer.currentActivity;
+ if (activity == null) {
+ return;
+ }
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ softHideInternal(activity);
+ }
+ });
+ }
+
+ private static void softHideInternal(Activity activity) {
+ try {
+ EditText editText = sEditText;
+ if (editText == null) {
+ return;
+ }
+ InputMethodManager imm = (InputMethodManager) activity.getSystemService(Activity.INPUT_METHOD_SERVICE);
+ if (imm != null) {
+ imm.hideSoftInputFromWindow(editText.getWindowToken(), 0);
+ }
+ editText.setFocusable(false);
+ editText.setFocusableInTouchMode(false);
+ editText.setClickable(false);
+ editText.setLongClickable(false);
+ editText.clearFocus();
+ } catch (Throwable t) {
+ android.util.Log.e(TAG, "soft hide failed", t);
+ }
+ }
+
+ /**
+ * 触发 autofill 保存流程(登录成功时调用,弹出"保存密码"提示)
+ * 注意:需在输入桥仍挂载在视图树时调用,登录点击时先 commit 再 hide
+ */
+ public static void commit() {
+ final Activity activity = UnityPlayer.currentActivity;
+ if (activity == null) {
+ return;
+ }
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ commitInternal(activity);
+ }
+ });
+ }
+
+ private static void commitInternal(Activity activity) {
+ try {
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+ // 反射调用 getAutofillManager().commit(),避免部分 android.jar 变体缺少该符号导致编译失败
+ java.lang.reflect.Method getter = activity.getClass().getMethod("getAutofillManager");
+ final Object afm = getter.invoke(activity);
+ if (afm != null) {
+ java.lang.reflect.Method commit = afm.getClass().getMethod("commit");
+ commit.invoke(afm);
+ }
+ }
+ } catch (Throwable t) {
+ android.util.Log.e(TAG, "commit failed", t);
+ }
+ }
+}
diff --git a/Assets/Plugins/Android/AutofillBridge.java.meta b/Assets/Plugins/Android/AutofillBridge.java.meta
new file mode 100644
index 0000000..d25e84d
--- /dev/null
+++ b/Assets/Plugins/Android/AutofillBridge.java.meta
@@ -0,0 +1,32 @@
+fileFormatVersion: 2
+guid: 981a182736820804d8310023dc57abc7
+PluginImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ iconMap: {}
+ executionOrder: {}
+ defineConstraints: []
+ isPreloaded: 0
+ isOverridable: 0
+ isExplicitlyReferenced: 0
+ validateReferences: 1
+ platformData:
+ - first:
+ Android: Android
+ second:
+ enabled: 1
+ settings: {}
+ - first:
+ Any:
+ second:
+ enabled: 0
+ settings: {}
+ - first:
+ Editor: Editor
+ second:
+ enabled: 0
+ settings:
+ DefaultValueInitialized: true
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Plugins/Android/proguard-user.txt b/Assets/Plugins/Android/proguard-user.txt
index 0e2456a..2e2f83e 100644
--- a/Assets/Plugins/Android/proguard-user.txt
+++ b/Assets/Plugins/Android/proguard-user.txt
@@ -4,6 +4,9 @@
# 保留 FilePicker 插件类(通过 AndroidJavaClass 反射调用,R8 无法检测到使用)
-keep class com.unity.filepicker.** { *; }
+# 保留谷歌自动填充隐形输入桥(通过 AndroidJavaClass 反射调用,R8 无法检测到使用)
+-keep class com.photonmatrix.autofill.** { *; }
+
# 保留 ffmpeg-kit 全部类(R8 混淆后 ffmpeg-kit 内部 native 调用会失效)
-keep class com.arthenica.ffmpegkit.** { *; }
-keep class com.arthenica.ffmpegkit.**$* { *; }
diff --git a/Assets/Scripts/UI/Components/AutofillBridge.cs b/Assets/Scripts/UI/Components/AutofillBridge.cs
new file mode 100644
index 0000000..3d3c9a7
--- /dev/null
+++ b/Assets/Scripts/UI/Components/AutofillBridge.cs
@@ -0,0 +1,184 @@
+using UnityEngine;
+using UnityEngine.UI;
+
+namespace Kill.UI.Components
+{
+ ///
+ /// autofill 语义类型(对应 Android autofillHints)
+ ///
+ public enum AutofillHint
+ {
+ EmailAddress, // emailAddress
+ Username, // username
+ Password, // password
+ }
+
+ ///
+ /// 隐形原生输入桥(Android 谷歌密码自动填充)
+ ///
+ /// 用法:AutofillBridge.Attach(inputField, AutofillHint.Password)
+ /// Attach 后会在 InputField 上叠一层透明按钮拦截点击:点击不再激活 Unity 自带键盘,
+ /// 而是在 InputField 的屏幕位置弹出带 autofillHints 的隐形原生 EditText,
+ /// 由谷歌密码管理器提供填充建议;输入内容实时同步回 Unity InputField 显示。
+ /// 登录成功时调用 Commit() 触发"保存密码"提示。
+ ///
+ public static class AutofillBridge
+ {
+ private const string JavaClass = "com.photonmatrix.autofill.AutofillBridge";
+ private const string ReceiverGoName = "AutofillBridgeReceiver";
+
+ private static AndroidJavaClass sJava;
+ private static GameObject sReceiverGo;
+ private static InputField sActiveField;
+
+ /// 密码页配套的账号(保存凭据时账号密码成对)
+ public static string CompanionUsername { get; set; } = "";
+
+ public static bool Supported =>
+ !Application.isEditor && Application.platform == RuntimePlatform.Android;
+
+ ///
+ /// 绑定 InputField:叠加透明按钮拦截点击,改由原生输入桥承接
+ ///
+ public static void Attach(InputField field, AutofillHint hint)
+ {
+ if (field == null || !Supported) return;
+ EnsureReceiver();
+
+ var blockerGo = new GameObject(field.name + "_AutofillBlocker", typeof(RectTransform), typeof(Image), typeof(Button));
+ var blockerRt = blockerGo.GetComponent();
+ var fieldRt = field.transform as RectTransform;
+ blockerRt.SetParent(fieldRt.parent, false);
+ // 复制 InputField 的布局,完全重合
+ blockerRt.anchorMin = fieldRt.anchorMin;
+ blockerRt.anchorMax = fieldRt.anchorMax;
+ blockerRt.pivot = fieldRt.pivot;
+ blockerRt.anchoredPosition = fieldRt.anchoredPosition;
+ blockerRt.sizeDelta = fieldRt.sizeDelta;
+ blockerRt.localScale = Vector3.one;
+ blockerRt.SetAsLastSibling(); // 保持在 InputField 之上,优先接收点击
+
+ var img = blockerGo.GetComponent();
+ img.color = new Color(0, 0, 0, 0); // 全透明,仅用于接收射线
+
+ var btn = blockerGo.GetComponent