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