新增 AutofillBridge.java:隐形原生 EditText(低透明度伪可见)设置 autofillHints,支持填充建议与保存提示,commit 采用反射调用规避符号解析问题 新增 AutofillBridge.cs:拦截 InputField 点击改由原生框承接输入,屏幕坐标换算、文本实时同步回显;登录邮箱/密码框接入 登录点击时先 commit 触发保存密码再隐藏;失焦/键盘完成时软隐藏(视图保留树上供快照采集),hide 防重入修复 NPE;proguard-user.txt 添加 keep 规则防 R8 剥离 状态:待真机验证填充与保存提示,已加 autofill 会话诊断日志
324 lines
14 KiB
Java
324 lines
14 KiB
Java
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);
|
||
}
|
||
}
|
||
}
|