killapp/Assets/Plugins/Android/AutofillBridge.java
“虞渠成” 6fa5a6c175 feat(login): 新增谷歌密码自动填充的隐形原生输入桥
新增 AutofillBridge.java:隐形原生 EditText(低透明度伪可见)设置 autofillHints,支持填充建议与保存提示,commit 采用反射调用规避符号解析问题

新增 AutofillBridge.cs:拦截 InputField 点击改由原生框承接输入,屏幕坐标换算、文本实时同步回显;登录邮箱/密码框接入

登录点击时先 commit 触发保存密码再隐藏;失焦/键盘完成时软隐藏(视图保留树上供快照采集),hide 防重入修复 NPE;proguard-user.txt 添加 keep 规则防 R8 剥离

状态:待真机验证填充与保存提示,已加 autofill 会话诊断日志
2026-09-11 09:46:33 +08:00

324 lines
14 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}
}
}