Browse Source

Google && Facebook

baxi 2 tuần trước cách đây
mục cha
commit
41a14c7118

+ 4 - 0
native/engine/android/app/AndroidManifest.xml

@@ -5,6 +5,9 @@
   <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
   <application android:allowBackup="true" android:label="@string/app_name" android:usesCleartextTraffic="true" android:icon="@mipmap/ic_launcher" android:resizeableActivity="true">
     <meta-data android:name="android.app.lib_name" android:value="cocos"/>
+    <meta-data android:name="com.facebook.sdk.ApplicationId" android:value="@string/facebook_app_id"/>
+    <meta-data android:name="com.facebook.sdk.ClientToken" android:value="@string/facebook_client_token"/>
+    <meta-data android:name="com.facebook.sdk.AutoLogAppEventsEnabled" android:value="false"/>
     <activity android:name="com.cocos.game.AppActivity" android:screenOrientation="sensorLandscape" android:configChanges="orientation|keyboardHidden|screenSize|screenLayout|smallestScreenSize" android:label="@string/app_name" android:theme="@android:style/Theme.NoTitleBar.Fullscreen" android:launchMode="singleTask" android:exported="true">
       <intent-filter>
         <action android:name="android.intent.action.MAIN"/>
@@ -12,5 +15,6 @@
       </intent-filter>
     </activity>
     <activity android:name="com.cocos.lib.CocosEditBoxActivity" android:configChanges="orientation|keyboardHidden|screenSize|screenLayout|smallestScreenSize" android:screenOrientation="behind" android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen"/>
+    <provider android:name="com.facebook.FacebookContentProvider" android:authorities="com.facebook.app.FacebookContentProvider1056149470661855" android:exported="true"/>
   </application>
 </manifest>

+ 5 - 0
native/engine/android/app/build.gradle

@@ -107,6 +107,11 @@ android {
 }
 
 dependencies {
+    implementation 'androidx.credentials:credentials:1.6.0'
+    implementation 'androidx.credentials:credentials-play-services-auth:1.6.0'
+    implementation 'com.google.android.libraries.identity.googleid:googleid:1.2.0'
+    implementation 'com.facebook.android:facebook-share:18.3.0'
+    implementation 'com.facebook.android:facebook-login:18.3.0'
     implementation fileTree(dir: '../libs', include: ['*.jar','*.aar'])
     implementation fileTree(dir: 'libs', include: ['*.jar','*.aar'])
     implementation fileTree(dir: "${COCOS_ENGINE_PATH}/cocos/platform/android/java/libs", include: ['*.jar'])

+ 5 - 1
native/engine/android/app/proguard-rules.pro

@@ -41,6 +41,10 @@
 -dontwarn android.net.http.SslError
 -dontwarn android.webkit.WebViewClient
 
+# SDK Manager: Facebook BEGIN
+-keep class com.cocos.plugin.** { *; }
+# SDK Manager: Facebook END
+
 # This is generated automatically by the Android Gradle plugin.
 -dontwarn android.hardware.BatteryState
 -dontwarn android.hardware.lights.Light
@@ -51,4 +55,4 @@
 -dontwarn android.hardware.lights.LightsRequest$Builder
 -dontwarn android.hardware.lights.LightsRequest
 -dontwarn android.net.ssl.SSLSockets
--dontwarn android.os.VibratorManager
+-dontwarn android.os.VibratorManager

+ 502 - 0
native/engine/android/app/src/com/cocos/plugin/FBWrapper.java

@@ -0,0 +1,502 @@
+package com.cocos.plugin;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.cocos.lib.CocosHelper;
+import com.cocos.lib.JsbBridgeWrapper;
+import com.cocos.service.SDKWrapper;
+import com.facebook.AccessToken;
+import com.facebook.CallbackManager;
+import com.facebook.FacebookCallback;
+import com.facebook.FacebookException;
+import com.facebook.FacebookSdk;
+import com.facebook.appevents.AppEventsLogger;
+import com.facebook.login.LoginManager;
+import com.facebook.login.LoginResult;
+import com.facebook.share.Sharer;
+import com.facebook.share.model.ShareHashtag;
+import com.facebook.share.model.ShareLinkContent;
+import com.facebook.share.widget.ShareDialog;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.lang.ref.WeakReference;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Currency;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Pattern;
+
+public final class FBWrapper implements SDKWrapper.SDKInterface {
+    private static final String REQUEST_EVENT = "facebook.request";
+    private static final String RESPONSE_EVENT = "facebook.response";
+    private static final int MAX_EVENT_PARAMETERS = 25;
+    private static final int MAX_EVENT_PARAMETER_LENGTH = 100;
+    private static final Pattern EVENT_NAME_PATTERN =
+            Pattern.compile("^[A-Za-z0-9][A-Za-z0-9 _-]{1,39}$");
+
+    private WeakReference<Activity> activityRef = new WeakReference<>(null);
+    private CallbackManager callbackManager;
+    private ShareDialog shareDialog;
+    private AppEventsLogger appEventsLogger;
+    private JsbBridgeWrapper.OnScriptEventListener requestListener;
+    private String initializationError;
+    private String pendingLoginRequestId;
+    private String pendingShareRequestId;
+
+    @Override
+    public void init(Context context) {
+        if (!(context instanceof Activity)) {
+            initializationError = "Facebook SDK 初始化需要 Activity Context。";
+            return;
+        }
+
+        Activity activity = (Activity) context;
+        activityRef = new WeakReference<>(activity);
+        registerBridgeListener();
+
+        try {
+            if (!FacebookSdk.isInitialized()) {
+                throw new IllegalStateException(
+                        "Facebook SDK 尚未初始化,请检查 Manifest 中的 App ID、Client Token 和 AutoInit 配置。"
+                );
+            }
+
+            callbackManager = CallbackManager.Factory.create();
+            appEventsLogger = AppEventsLogger.newLogger(context.getApplicationContext());
+            registerLoginCallback();
+            registerShareCallback(activity);
+            initializationError = null;
+        } catch (Throwable error) {
+            initializationError = safeMessage(error);
+        }
+    }
+
+    private void registerBridgeListener() {
+        requestListener = this::handleRequest;
+        JsbBridgeWrapper.getInstance().addScriptEventListener(REQUEST_EVENT, requestListener);
+    }
+
+    private void registerLoginCallback() {
+        LoginManager.getInstance().registerCallback(
+                callbackManager,
+                new FacebookCallback<LoginResult>() {
+                    @Override
+                    public void onSuccess(LoginResult result) {
+                        String requestId = takePendingLoginRequestId();
+                        if (requestId == null) return;
+
+                        AccessToken token = result != null ? result.getAccessToken() : null;
+                        if (token == null || token.getToken() == null || token.getToken().isEmpty()) {
+                            dispatchError(requestId, "login", "EMPTY_ACCESS_TOKEN", "Facebook 登录成功但未返回 Access Token。");
+                            return;
+                        }
+
+                        JSONObject data = new JSONObject();
+                        putQuietly(data, "accessToken", token.getToken());
+                        putQuietly(data, "userId", token.getUserId());
+                        dispatchSuccess(requestId, "login", false, data);
+                    }
+
+                    @Override
+                    public void onCancel() {
+                        String requestId = takePendingLoginRequestId();
+                        if (requestId != null) {
+                            dispatchSuccess(requestId, "login", true, new JSONObject());
+                        }
+                    }
+
+                    @Override
+                    public void onError(FacebookException error) {
+                        String requestId = takePendingLoginRequestId();
+                        if (requestId != null) {
+                            dispatchError(requestId, "login", "LOGIN_ERROR", safeMessage(error));
+                        }
+                    }
+                }
+        );
+    }
+
+    private void registerShareCallback(Activity activity) {
+        shareDialog = new ShareDialog(activity);
+        shareDialog.registerCallback(
+                callbackManager,
+                new FacebookCallback<Sharer.Result>() {
+                    @Override
+                    public void onSuccess(Sharer.Result result) {
+                        String requestId = takePendingShareRequestId();
+                        if (requestId == null) return;
+
+                        JSONObject data = new JSONObject();
+                        if (result != null && result.getPostId() != null) {
+                            putQuietly(data, "postId", result.getPostId());
+                        }
+                        dispatchSuccess(requestId, "share", false, data);
+                    }
+
+                    @Override
+                    public void onCancel() {
+                        String requestId = takePendingShareRequestId();
+                        if (requestId != null) {
+                            dispatchSuccess(requestId, "share", true, new JSONObject());
+                        }
+                    }
+
+                    @Override
+                    public void onError(FacebookException error) {
+                        String requestId = takePendingShareRequestId();
+                        if (requestId != null) {
+                            dispatchError(requestId, "share", "SHARE_ERROR", safeMessage(error));
+                        }
+                    }
+                }
+        );
+    }
+
+    private void handleRequest(String rawRequest) {
+        String requestId = "";
+        String method = "unknown";
+        try {
+            JSONObject request = new JSONObject(rawRequest == null ? "{}" : rawRequest);
+            requestId = request.optString("requestId", "").trim();
+            method = request.optString("method", "").trim();
+            JSONObject params = request.optJSONObject("params");
+            if (params == null) params = new JSONObject();
+
+            if (requestId.isEmpty() || method.isEmpty()) {
+                throw new IllegalArgumentException("Facebook 请求缺少 requestId 或 method。 ");
+            }
+
+            if ("init".equals(method)) {
+                handleInit(requestId);
+                return;
+            }
+            if (initializationError != null) {
+                dispatchError(requestId, method, "NOT_INITIALIZED", initializationError);
+                return;
+            }
+
+            switch (method) {
+                case "login":
+                    handleLogin(requestId, params);
+                    break;
+                case "logout":
+                    handleLogout(requestId);
+                    break;
+                case "share":
+                    handleShare(requestId, params);
+                    break;
+                case "logEvent":
+                    handleLogEvent(requestId, params);
+                    break;
+                case "logPurchase":
+                    handleLogPurchase(requestId, params);
+                    break;
+                case "flushEvents":
+                    handleFlushEvents(requestId);
+                    break;
+                default:
+                    dispatchError(requestId, method, "UNKNOWN_METHOD", "不支持的 Facebook 方法:" + method);
+                    break;
+            }
+        } catch (Throwable error) {
+            dispatchError(requestId, method, "INVALID_REQUEST", safeMessage(error));
+        }
+    }
+
+    private void handleInit(String requestId) {
+        if (initializationError != null) {
+            dispatchError(requestId, "init", "INIT_ERROR", initializationError);
+            return;
+        }
+        JSONObject data = new JSONObject();
+        putQuietly(data, "sdkVersion", FacebookSdk.getSdkVersion());
+        dispatchSuccess(requestId, "init", false, data);
+    }
+
+    private void handleLogin(String requestId, JSONObject params) {
+        if (pendingLoginRequestId != null) {
+            dispatchError(requestId, "login", "LOGIN_IN_PROGRESS", "已有 Facebook 登录请求正在进行。");
+            return;
+        }
+
+        final Activity activity = activityRef.get();
+        if (activity == null || activity.isFinishing()) {
+            dispatchError(requestId, "login", "ACTIVITY_UNAVAILABLE", "Android Activity 不可用。");
+            return;
+        }
+
+        final List<String> permissions = parsePermissions(params.optJSONArray("permissions"));
+        pendingLoginRequestId = requestId;
+        activity.runOnUiThread(() -> {
+            try {
+                LoginManager.getInstance().logInWithReadPermissions(activity, permissions);
+            } catch (Throwable error) {
+                pendingLoginRequestId = null;
+                dispatchError(requestId, "login", "LOGIN_START_ERROR", safeMessage(error));
+            }
+        });
+    }
+
+    private void handleLogout(String requestId) {
+        final Activity activity = activityRef.get();
+        if (activity == null || activity.isFinishing()) {
+            dispatchError(requestId, "logout", "ACTIVITY_UNAVAILABLE", "Android Activity 不可用。");
+            return;
+        }
+        activity.runOnUiThread(() -> {
+            try {
+                LoginManager.getInstance().logOut();
+                dispatchSuccess(requestId, "logout", false, new JSONObject());
+            } catch (Throwable error) {
+                dispatchError(requestId, "logout", "LOGOUT_ERROR", safeMessage(error));
+            }
+        });
+    }
+
+    private void handleShare(String requestId, JSONObject params) {
+        if (pendingShareRequestId != null) {
+            dispatchError(requestId, "share", "SHARE_IN_PROGRESS", "已有 Facebook 分享请求正在进行。");
+            return;
+        }
+
+        final Activity activity = activityRef.get();
+        if (activity == null || activity.isFinishing() || shareDialog == null) {
+            dispatchError(requestId, "share", "ACTIVITY_UNAVAILABLE", "Android Activity 或分享对话框不可用。");
+            return;
+        }
+
+        final String url = params.optString("url", "").trim();
+        Uri uri = Uri.parse(url);
+        if (!"https".equalsIgnoreCase(uri.getScheme()) || uri.getHost() == null) {
+            dispatchError(requestId, "share", "INVALID_SHARE_URL", "Facebook 分享地址必须是有效的 HTTPS URL。");
+            return;
+        }
+
+        ShareLinkContent.Builder builder = new ShareLinkContent.Builder().setContentUrl(uri);
+        String quote = params.optString("quote", "").trim();
+        if (!quote.isEmpty()) builder.setQuote(quote);
+        String hashtag = params.optString("hashtag", "").trim();
+        if (!hashtag.isEmpty()) {
+            if (!hashtag.startsWith("#")) hashtag = "#" + hashtag;
+            builder.setShareHashtag(new ShareHashtag.Builder().setHashtag(hashtag).build());
+        }
+        final ShareLinkContent content = builder.build();
+        if (!shareDialog.canShow(content, ShareDialog.Mode.AUTOMATIC)) {
+            dispatchError(requestId, "share", "SHARE_UNAVAILABLE", "当前设备无法显示 Facebook 链接分享对话框。");
+            return;
+        }
+
+        pendingShareRequestId = requestId;
+        activity.runOnUiThread(() -> {
+            try {
+                shareDialog.show(content, ShareDialog.Mode.AUTOMATIC);
+            } catch (Throwable error) {
+                pendingShareRequestId = null;
+                dispatchError(requestId, "share", "SHARE_START_ERROR", safeMessage(error));
+            }
+        });
+    }
+
+    private void handleLogEvent(String requestId, JSONObject params) {
+        runAppEventOnUiThread(requestId, "logEvent", () -> {
+            String eventName = params.optString("eventName", "").trim();
+            validateEventName(eventName);
+            Bundle eventParams = toEventBundle(params.optJSONObject("parameters"));
+            if (params.has("valueToSum") && !params.isNull("valueToSum")) {
+                appEventsLogger.logEvent(eventName, params.getDouble("valueToSum"), eventParams);
+            } else {
+                appEventsLogger.logEvent(eventName, eventParams);
+            }
+        });
+    }
+
+    private void handleLogPurchase(String requestId, JSONObject params) {
+        runAppEventOnUiThread(requestId, "logPurchase", () -> {
+            Object amountValue = params.opt("amount");
+            if (!(amountValue instanceof Number)) {
+                throw new IllegalArgumentException("Facebook 购买事件 amount 必须是数字。");
+            }
+            BigDecimal amount = new BigDecimal(String.valueOf(amountValue));
+            if (amount.signum() < 0) {
+                throw new IllegalArgumentException("Facebook 购买事件 amount 不能小于 0。");
+            }
+            String currencyCode = params.optString("currency", "").trim().toUpperCase(Locale.US);
+            if (!currencyCode.matches("^[A-Z]{3}$")) {
+                throw new IllegalArgumentException("Facebook 购买事件 currency 必须是三位 ISO 4217 货币代码。");
+            }
+            Currency currency = Currency.getInstance(currencyCode);
+            Bundle eventParams = toEventBundle(params.optJSONObject("parameters"));
+            appEventsLogger.logPurchase(amount, currency, eventParams);
+        });
+    }
+
+    private void handleFlushEvents(String requestId) {
+        runAppEventOnUiThread(requestId, "flushEvents", () -> appEventsLogger.flush());
+    }
+
+    private void runAppEventOnUiThread(String requestId, String method, ThrowingRunnable action) {
+        final Activity activity = activityRef.get();
+        if (activity == null || activity.isFinishing() || appEventsLogger == null) {
+            dispatchError(requestId, method, "APP_EVENTS_UNAVAILABLE", "Facebook AppEventsLogger 不可用。");
+            return;
+        }
+        activity.runOnUiThread(() -> {
+            try {
+                action.run();
+                JSONObject data = new JSONObject();
+                putQuietly(data, "accepted", true);
+                dispatchSuccess(requestId, method, false, data);
+            } catch (Throwable error) {
+                dispatchError(requestId, method, "APP_EVENT_ERROR", safeMessage(error));
+            }
+        });
+    }
+
+    private static Bundle toEventBundle(JSONObject parameters) throws JSONException {
+        Bundle bundle = new Bundle();
+        if (parameters == null) return bundle;
+        if (parameters.length() > MAX_EVENT_PARAMETERS) {
+            throw new IllegalArgumentException("Facebook 单个事件最多允许 25 个参数。");
+        }
+
+        Iterator<String> keys = parameters.keys();
+        while (keys.hasNext()) {
+            String key = keys.next();
+            validateEventName(key);
+            Object value = parameters.get(key);
+            if (value instanceof String) {
+                String stringValue = (String) value;
+                if (stringValue.length() > MAX_EVENT_PARAMETER_LENGTH) {
+                    throw new IllegalArgumentException("Facebook 事件参数值不能超过 100 个字符:" + key);
+                }
+                bundle.putString(key, stringValue);
+            } else if (value instanceof Byte || value instanceof Short || value instanceof Integer || value instanceof Long) {
+                bundle.putLong(key, ((Number) value).longValue());
+            } else if (value instanceof Float || value instanceof Double) {
+                bundle.putDouble(key, ((Number) value).doubleValue());
+            } else {
+                throw new IllegalArgumentException("Facebook 事件参数只允许 string 或 number:" + key);
+            }
+        }
+        return bundle;
+    }
+
+    private static void validateEventName(String value) {
+        if (!EVENT_NAME_PATTERN.matcher(value).matches()) {
+            throw new IllegalArgumentException("Facebook 事件名和参数名必须为 2-40 位字母、数字、空格、_ 或 -。");
+        }
+    }
+
+    private static List<String> parsePermissions(JSONArray values) {
+        List<String> permissions = new ArrayList<>();
+        if (values != null) {
+            for (int index = 0; index < values.length(); index++) {
+                String permission = values.optString(index, "").trim();
+                if (!permission.isEmpty() && !permissions.contains(permission)) {
+                    permissions.add(permission);
+                }
+            }
+        }
+        if (permissions.isEmpty()) {
+            permissions.add("public_profile");
+            permissions.add("email");
+        }
+        return permissions;
+    }
+
+    private synchronized String takePendingLoginRequestId() {
+        String requestId = pendingLoginRequestId;
+        pendingLoginRequestId = null;
+        return requestId;
+    }
+
+    private synchronized String takePendingShareRequestId() {
+        String requestId = pendingShareRequestId;
+        pendingShareRequestId = null;
+        return requestId;
+    }
+
+    private void dispatchSuccess(String requestId, String method, boolean cancelled, JSONObject data) {
+        JSONObject response = new JSONObject();
+        putQuietly(response, "requestId", requestId);
+        putQuietly(response, "method", method);
+        putQuietly(response, "success", true);
+        putQuietly(response, "cancelled", cancelled);
+        putQuietly(response, "data", data == null ? new JSONObject() : data);
+        dispatchResponse(response);
+    }
+
+    private void dispatchError(String requestId, String method, String code, String message) {
+        JSONObject error = new JSONObject();
+        putQuietly(error, "code", code);
+        putQuietly(error, "message", message);
+
+        JSONObject response = new JSONObject();
+        putQuietly(response, "requestId", requestId == null ? "" : requestId);
+        putQuietly(response, "method", method == null ? "unknown" : method);
+        putQuietly(response, "success", false);
+        putQuietly(response, "cancelled", false);
+        putQuietly(response, "error", error);
+        dispatchResponse(response);
+    }
+
+    private void dispatchResponse(JSONObject response) {
+        final String payload = response.toString();
+        CocosHelper.runOnGameThread(
+                () -> JsbBridgeWrapper.getInstance().dispatchEventToScript(RESPONSE_EVENT, payload)
+        );
+    }
+
+    private static void putQuietly(JSONObject object, String key, Object value) {
+        try {
+            object.put(key, value);
+        } catch (JSONException ignored) {
+        }
+    }
+
+    private static String safeMessage(Throwable error) {
+        if (error == null) return "未知错误";
+        String message = error.getMessage();
+        return message == null || message.trim().isEmpty()
+                ? error.getClass().getSimpleName()
+                : message;
+    }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (callbackManager != null) {
+            callbackManager.onActivityResult(requestCode, resultCode, data);
+        }
+    }
+
+    @Override
+    public void onDestroy() {
+        if (requestListener != null) {
+            JsbBridgeWrapper.getInstance().removeScriptEventListener(REQUEST_EVENT, requestListener);
+            requestListener = null;
+        }
+        if (callbackManager != null) {
+            LoginManager.getInstance().unregisterCallback(callbackManager);
+        }
+        pendingLoginRequestId = null;
+        pendingShareRequestId = null;
+        callbackManager = null;
+        shareDialog = null;
+        appEventsLogger = null;
+        activityRef.clear();
+    }
+
+    private interface ThrowingRunnable {
+        void run() throws Exception;
+    }
+}

+ 313 - 0
native/engine/android/app/src/com/cocos/plugin/GoogleWrapper.java

@@ -0,0 +1,313 @@
+package com.cocos.plugin;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.CancellationSignal;
+import android.util.Log;
+
+import androidx.credentials.ClearCredentialStateRequest;
+import androidx.credentials.Credential;
+import androidx.credentials.CredentialManager;
+import androidx.credentials.CredentialManagerCallback;
+import androidx.credentials.CustomCredential;
+import androidx.credentials.GetCredentialRequest;
+import androidx.credentials.GetCredentialResponse;
+import androidx.credentials.exceptions.ClearCredentialException;
+import androidx.credentials.exceptions.GetCredentialCancellationException;
+import androidx.credentials.exceptions.GetCredentialException;
+
+import com.cocos.lib.CocosHelper;
+import com.cocos.lib.JsbBridgeWrapper;
+import com.cocos.service.SDKWrapper;
+import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption;
+import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.lang.ref.WeakReference;
+import java.util.concurrent.Executor;
+
+public final class GoogleWrapper implements SDKWrapper.SDKInterface {
+    private static final String REQUEST_EVENT = "google.request";
+    private static final String RESPONSE_EVENT = "google.response";
+    private static final String GOOGLE_CLIENT_ID_SUFFIX = ".apps.googleusercontent.com";
+
+    private WeakReference<Activity> activityRef = new WeakReference<>(null);
+    private CredentialManager credentialManager;
+    private JsbBridgeWrapper.OnScriptEventListener requestListener;
+    private CancellationSignal loginCancellationSignal;
+    private String webClientId;
+    private String pendingLoginRequestId;
+
+    @Override
+    public void init(Context context) {
+        if (!(context instanceof Activity)) {
+            return;
+        }
+
+        Activity activity = (Activity) context;
+        activityRef = new WeakReference<>(activity);
+        credentialManager = CredentialManager.create(activity.getApplicationContext());
+        requestListener = this::handleRequest;
+        JsbBridgeWrapper.getInstance().addScriptEventListener(REQUEST_EVENT, requestListener);
+    }
+
+    private void handleRequest(String rawRequest) {
+        String requestId = "";
+        String method = "unknown";
+        try {
+            JSONObject request = new JSONObject(rawRequest == null ? "{}" : rawRequest);
+            requestId = request.optString("requestId", "").trim();
+            method = request.optString("method", "").trim();
+            JSONObject params = request.optJSONObject("params");
+            if (params == null) params = new JSONObject();
+
+            if (requestId.isEmpty() || method.isEmpty()) {
+                throw new IllegalArgumentException("Google 请求缺少 requestId 或 method。");
+            }
+
+            switch (method) {
+                case "init":
+                    handleInit(requestId, params);
+                    break;
+                case "login":
+                    handleLogin(requestId);
+                    break;
+                case "logout":
+                    handleLogout(requestId);
+                    break;
+                default:
+                    dispatchError(requestId, method, "UNKNOWN_METHOD", "不支持的 Google 方法:" + method);
+                    break;
+            }
+        } catch (Throwable error) {
+            dispatchError(requestId, method, "INVALID_REQUEST", safeMessage(error));
+        }
+    }
+
+    private void handleInit(String requestId, JSONObject params) {
+        if (credentialManager == null || activityRef.get() == null) {
+            dispatchError(requestId, "init", "INIT_ERROR", "Google Credential Manager 初始化失败。");
+            return;
+        }
+
+        String clientId = params.optString("webClientId", "").trim();
+        if (clientId.isEmpty() || !clientId.endsWith(GOOGLE_CLIENT_ID_SUFFIX)) {
+            dispatchError(
+                    requestId,
+                    "init",
+                    "INVALID_CLIENT_ID",
+                    "Google 登录必须配置 Web OAuth Client ID。"
+            );
+            return;
+        }
+
+        webClientId = clientId;
+        Log.i("cocos", "》》》》》》》》 clientID: " + clientId);
+        JSONObject data = new JSONObject();
+        putQuietly(data, "initialized", true);
+        dispatchSuccess(requestId, "init", false, data);
+    }
+
+    private synchronized void handleLogin(String requestId) {
+        if (webClientId == null || webClientId.isEmpty()) {
+            dispatchError(requestId, "login", "NOT_INITIALIZED", "Google 登录尚未初始化。");
+            return;
+        }
+        if (pendingLoginRequestId != null) {
+            dispatchError(requestId, "login", "LOGIN_IN_PROGRESS", "已有 Google 登录请求正在进行。");
+            return;
+        }
+
+        final Activity activity = activityRef.get();
+        if (activity == null || activity.isFinishing()) {
+            dispatchError(requestId, "login", "ACTIVITY_UNAVAILABLE", "Android Activity 不可用。");
+            return;
+        }
+
+        pendingLoginRequestId = requestId;
+        loginCancellationSignal = new CancellationSignal();
+        final CancellationSignal cancellationSignal = loginCancellationSignal;
+        final GetSignInWithGoogleOption googleOption =
+                new GetSignInWithGoogleOption.Builder(webClientId).build();
+        final GetCredentialRequest credentialRequest = new GetCredentialRequest.Builder()
+                .addCredentialOption(googleOption)
+                .build();
+
+        activity.runOnUiThread(() -> credentialManager.getCredentialAsync(
+                activity,
+                credentialRequest,
+                cancellationSignal,
+                mainExecutor(activity),
+                new CredentialManagerCallback<GetCredentialResponse, GetCredentialException>() {
+                    @Override
+                    public void onResult(GetCredentialResponse result) {
+                        String activeRequestId = takePendingLoginRequestId();
+                        if (activeRequestId == null) return;
+                        try {
+                            Credential credential = result.getCredential();
+                            if (!(credential instanceof CustomCredential)
+                                    || !GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL.equals(
+                                    credential.getType())) {
+                                throw new IllegalStateException("Google 返回了不支持的 Credential 类型。");
+                            }
+
+                            GoogleIdTokenCredential googleCredential =
+                                    GoogleIdTokenCredential.createFrom(
+                                            ((CustomCredential) credential).getData()
+                                    );
+                            String idToken = googleCredential.getIdToken();
+                            if (idToken == null || idToken.isEmpty()) {
+                                throw new IllegalStateException("Google 登录未返回 ID Token。");
+                            }
+
+                            JSONObject data = new JSONObject();
+                            putQuietly(data, "idToken", idToken);
+                            putQuietly(data, "email", googleCredential.getId());
+                            putQuietly(data, "displayName", googleCredential.getDisplayName());
+                            putQuietly(data, "givenName", googleCredential.getGivenName());
+                            putQuietly(data, "familyName", googleCredential.getFamilyName());
+                            if (googleCredential.getProfilePictureUri() != null) {
+                                putQuietly(
+                                        data,
+                                        "profilePictureUrl",
+                                        googleCredential.getProfilePictureUri().toString()
+                                );
+                            }
+                            dispatchSuccess(activeRequestId, "login", false, data);
+                        } catch (Throwable error) {
+                            Log.e("cocos", "》》》》》》》》 Google Login onResult: ", error);
+                            dispatchError(
+                                    activeRequestId,
+                                    "login",
+                                    "INVALID_ID_TOKEN",
+                                    safeMessage(error)
+                            );
+                        }
+                    }
+
+                    @Override
+                    public void onError(GetCredentialException error) {
+                        Log.e("cocos", "》》》》》》》》 Google Login onError: ", error);
+                        String activeRequestId = takePendingLoginRequestId();
+                        if (activeRequestId == null) return;
+                        if (error instanceof GetCredentialCancellationException) {
+                            dispatchSuccess(activeRequestId, "login", true, new JSONObject());
+                            return;
+                        }
+                        dispatchError(
+                                activeRequestId,
+                                "login",
+                                "LOGIN_ERROR",
+                                safeMessage(error)
+                        );
+                    }
+                }
+        ));
+    }
+
+    private void handleLogout(String requestId) {
+        final Activity activity = activityRef.get();
+        if (activity == null || activity.isFinishing() || credentialManager == null) {
+            dispatchError(requestId, "logout", "ACTIVITY_UNAVAILABLE", "Android Activity 不可用。");
+            return;
+        }
+
+        credentialManager.clearCredentialStateAsync(
+                new ClearCredentialStateRequest(),
+                null,
+                mainExecutor(activity),
+                new CredentialManagerCallback<Void, ClearCredentialException>() {
+                    @Override
+                    public void onResult(Void result) {
+                        dispatchSuccess(requestId, "logout", false, new JSONObject());
+                    }
+
+                    @Override
+                    public void onError(ClearCredentialException error) {
+                        dispatchError(
+                                requestId,
+                                "logout",
+                                "LOGOUT_ERROR",
+                                safeMessage(error)
+                        );
+                    }
+                }
+        );
+    }
+
+    private synchronized String takePendingLoginRequestId() {
+        String requestId = pendingLoginRequestId;
+        pendingLoginRequestId = null;
+        loginCancellationSignal = null;
+        return requestId;
+    }
+
+    private static Executor mainExecutor(Activity activity) {
+        return command -> activity.runOnUiThread(command);
+    }
+
+    private void dispatchSuccess(String requestId, String method, boolean cancelled, JSONObject data) {
+        JSONObject response = new JSONObject();
+        putQuietly(response, "requestId", requestId);
+        putQuietly(response, "method", method);
+        putQuietly(response, "success", true);
+        putQuietly(response, "cancelled", cancelled);
+        putQuietly(response, "data", data == null ? new JSONObject() : data);
+        dispatchResponse(response);
+    }
+
+    private void dispatchError(String requestId, String method, String code, String message) {
+        JSONObject error = new JSONObject();
+        putQuietly(error, "code", code);
+        putQuietly(error, "message", message);
+
+        JSONObject response = new JSONObject();
+        putQuietly(response, "requestId", requestId == null ? "" : requestId);
+        putQuietly(response, "method", method == null ? "unknown" : method);
+        putQuietly(response, "success", false);
+        putQuietly(response, "cancelled", false);
+        putQuietly(response, "error", error);
+        dispatchResponse(response);
+    }
+
+    private void dispatchResponse(JSONObject response) {
+        final String payload = response.toString();
+        CocosHelper.runOnGameThread(
+                () -> JsbBridgeWrapper.getInstance().dispatchEventToScript(RESPONSE_EVENT, payload)
+        );
+    }
+
+    private static void putQuietly(JSONObject object, String key, Object value) {
+        if (value == null) return;
+        try {
+            object.put(key, value);
+        } catch (JSONException ignored) {
+        }
+    }
+
+    private static String safeMessage(Throwable error) {
+        if (error == null) return "未知错误";
+        String message = error.getMessage();
+        return message == null || message.trim().isEmpty()
+                ? error.getClass().getSimpleName()
+                : message;
+    }
+
+    @Override
+    public synchronized void onDestroy() {
+        if (loginCancellationSignal != null) {
+            loginCancellationSignal.cancel();
+            loginCancellationSignal = null;
+        }
+        pendingLoginRequestId = null;
+        if (requestListener != null) {
+            JsbBridgeWrapper.getInstance().removeScriptEventListener(REQUEST_EVENT, requestListener);
+            requestListener = null;
+        }
+        credentialManager = null;
+        webClientId = null;
+        activityRef.clear();
+    }
+}

+ 6 - 0
native/engine/android/app/src/main/assets/service.json

@@ -0,0 +1,6 @@
+{
+  "serviceClasses": [
+    "com.cocos.plugin.FBWrapper",
+    "com.cocos.plugin.GoogleWrapper"
+  ]
+}

+ 4 - 0
native/engine/android/res/values/strings.xml

@@ -1,2 +1,6 @@
 <resources>
+    <!--   更新FB_APP_ID 记得更新AndroidManifest FacebookContentProvider-->
+    <string name="facebook_app_id" translatable="false">1056149470661855</string>
+    <string name="fb_login_protocol_scheme" translatable="false">fb1056149470661855</string>
+    <string name="facebook_client_token" translatable="false">b3c949e52128cc10879901661efb2ca5</string>
 </resources>