diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..152c03a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "files.associations": { + "*.gui_script": "lua", + "*.script": "lua", + "typeinfo": "cpp" + } +} \ No newline at end of file diff --git a/SIGW/ext.manifest b/SIGW/ext.manifest deleted file mode 100644 index 23bb57f..0000000 --- a/SIGW/ext.manifest +++ /dev/null @@ -1,2 +0,0 @@ -# C++ symbol in your extension -name: "MyExtension" diff --git a/SIGW/src/myextension.cpp b/SIGW/src/myextension.cpp deleted file mode 100644 index 3cb4010..0000000 --- a/SIGW/src/myextension.cpp +++ /dev/null @@ -1,112 +0,0 @@ -// myextension.cpp -// Extension lib defines -#define LIB_NAME "MyExtension" -#define MODULE_NAME "myextension" - -// include the Defold SDK -#include - -static int Reverse(lua_State* L) -{ - // The number of expected items to be on the Lua stack - // once this struct goes out of scope - DM_LUA_STACK_CHECK(L, 1); - - // Check and get parameter string from stack - char* str = (char*)luaL_checkstring(L, 1); - - // Reverse the string - int len = strlen(str); - for(int i = 0; i < len / 2; i++) { - const char a = str[i]; - const char b = str[len - i - 1]; - str[i] = b; - str[len - i - 1] = a; - } - - // Put the reverse string on the stack - lua_pushstring(L, str); - - // Return 1 item - return 1; -} - -// Functions exposed to Lua -static const luaL_reg Module_methods[] = -{ - {"reverse", Reverse}, - {0, 0} -}; - -static void LuaInit(lua_State* L) -{ - int top = lua_gettop(L); - - // Register lua names - luaL_register(L, MODULE_NAME, Module_methods); - - lua_pop(L, 1); - assert(top == lua_gettop(L)); -} - -dmExtension::Result AppInitializeMyExtension(dmExtension::AppParams* params) -{ - dmLogInfo("AppInitializeMyExtension\n"); - return dmExtension::RESULT_OK; -} - -dmExtension::Result InitializeMyExtension(dmExtension::Params* params) -{ - // Init Lua - LuaInit(params->m_L); - dmLogInfo("Registered %s Extension\n", MODULE_NAME); - return dmExtension::RESULT_OK; -} - -dmExtension::Result AppFinalizeMyExtension(dmExtension::AppParams* params) -{ - dmLogInfo("AppFinalizeMyExtension\n"); - return dmExtension::RESULT_OK; -} - -dmExtension::Result FinalizeMyExtension(dmExtension::Params* params) -{ - dmLogInfo("FinalizeMyExtension\n"); - return dmExtension::RESULT_OK; -} - -dmExtension::Result OnUpdateMyExtension(dmExtension::Params* params) -{ - dmLogInfo("OnUpdateMyExtension\n"); - return dmExtension::RESULT_OK; -} - -void OnEventMyExtension(dmExtension::Params* params, const dmExtension::Event* event) -{ - switch(event->m_Event) - { - case dmExtension::EVENT_ID_ACTIVATEAPP: - dmLogInfo("OnEventMyExtension - EVENT_ID_ACTIVATEAPP\n"); - break; - case dmExtension::EVENT_ID_DEACTIVATEAPP: - dmLogInfo("OnEventMyExtension - EVENT_ID_DEACTIVATEAPP\n"); - break; - case dmExtension::EVENT_ID_ICONIFYAPP: - dmLogInfo("OnEventMyExtension - EVENT_ID_ICONIFYAPP\n"); - break; - case dmExtension::EVENT_ID_DEICONIFYAPP: - dmLogInfo("OnEventMyExtension - EVENT_ID_DEICONIFYAPP\n"); - break; - default: - dmLogWarning("OnEventMyExtension - Unknown event id\n"); - break; - } -} - -// Defold SDK uses a macro for setting up extension entry points: -// -// DM_DECLARE_EXTENSION(symbol, name, app_init, app_final, init, update, on_event, final) - -// MyExtension is the C++ symbol that holds all relevant extension data. -// It must match the name field in the `ext.manifest` -DM_DECLARE_EXTENSION(MyExtension, LIB_NAME, AppInitializeMyExtension, AppFinalizeMyExtension, InitializeMyExtension, OnUpdateMyExtension, OnEventMyExtension, FinalizeMyExtension) diff --git a/assets/main.font b/assets/main.font new file mode 100644 index 0000000..95da1d7 --- /dev/null +++ b/assets/main.font @@ -0,0 +1,17 @@ +font: "/builtins/fonts/vera_mo_bd.ttf" +material: "/builtins/fonts/font.material" +size: 60 +antialias: 1 +alpha: 1.0 +outline_alpha: 0.0 +outline_width: 0.0 +shadow_alpha: 0.0 +shadow_blur: 0 +shadow_x: 0.0 +shadow_y: 0.0 +extra_characters: "" +output_format: TYPE_BITMAP +all_chars: false +cache_width: 0 +cache_height: 0 +render_mode: MODE_SINGLE_LAYER diff --git a/dmengine b/dmengine new file mode 100755 index 0000000..2f9f33f Binary files /dev/null and b/dmengine differ diff --git a/example/example.collection b/example/example.collection index ff3608f..55b8ec2 100644 --- a/example/example.collection +++ b/example/example.collection @@ -3,6 +3,21 @@ scale_along_z: 0 embedded_instances { id: "go" data: "components {\n" + " id: \"gui\"\n" + " component: \"/example/example.gui\"\n" + " position {\n" + " x: 0.0\n" + " y: 0.0\n" + " z: 0.0\n" + " }\n" + " rotation {\n" + " x: 0.0\n" + " y: 0.0\n" + " z: 0.0\n" + " w: 1.0\n" + " }\n" + "}\n" + "components {\n" " id: \"example\"\n" " component: \"/example/example.script\"\n" " position {\n" diff --git a/example/example.gui b/example/example.gui new file mode 100644 index 0000000..2abf299 --- /dev/null +++ b/example/example.gui @@ -0,0 +1,249 @@ +script: "/example/example.gui_script" +fonts { + name: "main" + font: "/assets/main.font" +} +background_color { + x: 0.0 + y: 0.0 + z: 0.0 + w: 0.0 +} +nodes { + position { + x: 540.0 + y: 170.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + size { + x: 900.0 + y: 160.0 + z: 0.0 + w: 1.0 + } + color { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + type: TYPE_BOX + blend_mode: BLEND_MODE_ALPHA + texture: "" + id: "login_btn" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_CENTER + adjust_mode: ADJUST_MODE_FIT + layer: "" + inherit_alpha: true + slice9 { + x: 0.0 + y: 0.0 + z: 0.0 + w: 0.0 + } + clipping_mode: CLIPPING_MODE_NONE + clipping_visible: true + clipping_inverted: false + alpha: 1.0 + template_node_child: false + size_mode: SIZE_MODE_MANUAL +} +nodes { + position { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + size { + x: 200.0 + y: 100.0 + z: 0.0 + w: 1.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + type: TYPE_TEXT + blend_mode: BLEND_MODE_ALPHA + text: "Manual Login\n" + "" + font: "main" + id: "text" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_CENTER + outline { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + shadow { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + adjust_mode: ADJUST_MODE_FIT + line_break: false + parent: "login_btn" + layer: "" + inherit_alpha: true + alpha: 1.0 + outline_alpha: 1.0 + shadow_alpha: 1.0 + template_node_child: false + text_leading: 1.0 + text_tracking: 0.0 +} +nodes { + position { + x: 540.0 + y: 427.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + size { + x: 900.0 + y: 160.0 + z: 0.0 + w: 1.0 + } + color { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + type: TYPE_BOX + blend_mode: BLEND_MODE_ALPHA + texture: "" + id: "silent_login_btn" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_CENTER + adjust_mode: ADJUST_MODE_FIT + layer: "" + inherit_alpha: true + slice9 { + x: 0.0 + y: 0.0 + z: 0.0 + w: 0.0 + } + clipping_mode: CLIPPING_MODE_NONE + clipping_visible: true + clipping_inverted: false + alpha: 1.0 + template_node_child: false + size_mode: SIZE_MODE_MANUAL +} +nodes { + position { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + size { + x: 200.0 + y: 100.0 + z: 0.0 + w: 1.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + type: TYPE_TEXT + blend_mode: BLEND_MODE_ALPHA + text: "Silent Login Btn" + font: "main" + id: "text1" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_CENTER + outline { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + shadow { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + adjust_mode: ADJUST_MODE_FIT + line_break: false + parent: "silent_login_btn" + layer: "" + inherit_alpha: true + alpha: 1.0 + outline_alpha: 1.0 + shadow_alpha: 1.0 + template_node_child: false + text_leading: 1.0 + text_tracking: 0.0 +} +material: "/builtins/materials/gui.material" +adjust_reference: ADJUST_REFERENCE_PARENT +max_nodes: 512 diff --git a/example/example.gui_script b/example/example.gui_script new file mode 100644 index 0000000..b4a4f51 --- /dev/null +++ b/example/example.gui_script @@ -0,0 +1,52 @@ +function init(self) + msg.post(".", "acquire_input_focus") + + if siwg then + siwg.set_callback(gpgs_callback) + end +end + +function on_message(self, message_id, message, sender) + +end + +function on_input(self, action_id, action) + if action_id == hash("touch") and action.pressed then + if gui.pick_node(gui.get_node("login_btn"), action.x, action.y) then + if siwg then + print("Login Pressed!") + siwg.login() + end + end + + if gui.pick_node(gui.get_node("silent_login_btn"), action.x, action.y) then + if siwg then + print("Login Pressed!") + siwg.silent_login() + end + end + end +end + +function gpgs_callback(self, message_id, message) + if message_id == siwg.MSG_SIGN_IN or message_id == siwg.MSG_SILENT_SIGN_IN then + if message.status == siwg.STATUS_SUCCESS then + print("SIWG ID: " .. siwg.get_id()) + print("SIWG DisplayName: " .. siwg.get_display_name()) + + if sys.get_config("siwg.client_id") then + print("SIWG id_token: ", siwg.get_id_token()) + print("SIWG auth_code: ", siwg.get_server_auth_code()) + end + + elseif message.status == siwg.STATUS_FAILED then + print("Status: FAILED") + print("Error: " .. message.error) + end + elseif message_id == siwg.MSG_SIGN_OUT then + print("SIWG Logged out!") + end + + print("SIWG is_logged_in: " .. tostring(siwg.is_logged_in())) +end + diff --git a/example/example.script b/example/example.script index 53b4483..d0f5843 100644 --- a/example/example.script +++ b/example/example.script @@ -1,7 +1,5 @@ function init(self) - local s = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - local reverse_s = myextension.reverse(s) - print(reverse_s) --> ZYXWVUTSRQPONMLKJIHGFEDCBAzyxwvutsrqponmlkjihgfedcba + msg.post(".", "acquire_input_focus") end function final(self) diff --git a/game.project b/game.project index d28c00d..4b02af4 100644 --- a/game.project +++ b/game.project @@ -5,12 +5,30 @@ main_collection = /example/example.collectionc shared_state = 1 [display] -width = 960 -height = 640 +width = 1080 +height = 1920 [project] -title = SIGW-Extension +title = SIWG-Extension [library] -include_dirs = SIGW +include_dirs = siwg + +[native_extension] +app_manifest = /generated.appmanifest + +[android] +input_method = HiddenInputField +immersive_mode = 1 +package = com.aterve.partydeck +debuggable = 1 + +[siwg] +app_id = 715670885775 +client_id = 715670885775-j3p3nr6ith9su03p2im8d2p752hptg56.apps.googleusercontent.com +request_server_auth_code = 1 +request_id_token = 1 + +[ios] +bundle_identifier = com.aterve.partydeck diff --git a/generated.appmanifest b/generated.appmanifest new file mode 100644 index 0000000..df2f660 --- /dev/null +++ b/generated.appmanifest @@ -0,0 +1,100 @@ +# App manifest generated Fri Nov 20 2020 09:28:33 GMT+0100 (Central European Standard Time) +# Settings: OpenGL +platforms: + x86_64-osx: + context: + excludeLibs: [] + excludeSymbols: [] + symbols: [] + libs: [] + frameworks: [] + linkFlags: [] + + x86_64-linux: + context: + excludeLibs: [] + excludeSymbols: [] + symbols: [] + libs: [] + linkFlags: [] + + js-web: + context: + excludeLibs: [] + excludeJsLibs: [] + excludeSymbols: [] + symbols: [] + libs: [] + linkFlags: [] + + wasm-web: + context: + excludeLibs: [] + excludeJsLibs: [] + excludeSymbols: [] + symbols: [] + libs: [] + linkFlags: [] + + x86-win32: + context: + excludeLibs: [] + excludeSymbols: [] + symbols: [] + libs: [] + linkFlags: [] + + x86_64-win32: + context: + excludeLibs: [] + excludeSymbols: [] + symbols: [] + libs: [] + linkFlags: [] + + armv7-android: + context: + excludeLibs: [] + excludeJars: [] + excludeSymbols: [] + symbols: [] + libs: [] + linkFlags: [] + jetifier: true + + arm64-android: + context: + excludeLibs: [] + excludeJars: [] + excludeSymbols: [] + symbols: [] + libs: [] + linkFlags: [] + jetifier: true + + armv7-ios: + context: + excludeLibs: [] + excludeSymbols: [] + symbols: [] + libs: [] + frameworks: [] + linkFlags: [] + + arm64-ios: + context: + excludeLibs: [] + excludeSymbols: [] + symbols: [] + libs: [] + frameworks: [] + linkFlags: [] + + x86_64-ios: + context: + excludeLibs: [] + excludeSymbols: [] + symbols: [] + libs: [] + frameworks: [] + linkFlags: [] \ No newline at end of file diff --git a/siwg/ext.manifest b/siwg/ext.manifest new file mode 100644 index 0000000..20f736e --- /dev/null +++ b/siwg/ext.manifest @@ -0,0 +1 @@ +name: SIWG \ No newline at end of file diff --git a/siwg/manifests/android/AndroidManifest.xml b/siwg/manifests/android/AndroidManifest.xml new file mode 100644 index 0000000..25f2762 --- /dev/null +++ b/siwg/manifests/android/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/siwg/manifests/android/build.gradle b/siwg/manifests/android/build.gradle new file mode 100644 index 0000000..80c1df7 --- /dev/null +++ b/siwg/manifests/android/build.gradle @@ -0,0 +1,6 @@ +dependencies { + // https://developers.google.com/android/guides/setup#split + implementation 'com.google.android.gms:play-services-base:17.5.0' + implementation 'com.google.android.gms:play-services-auth:18.1.0' + implementation 'com.google.android.gms:play-services-games:20.0.1' +} \ No newline at end of file diff --git a/siwg/src/com_aterve_siwg_SiwgJNI.h b/siwg/src/com_aterve_siwg_SiwgJNI.h new file mode 100644 index 0000000..ef2265d --- /dev/null +++ b/siwg/src/com_aterve_siwg_SiwgJNI.h @@ -0,0 +1,21 @@ + +#include +/* Header for class com_aterve_siwg_SiwgJNI */ + +#ifndef COM_ATERVE_SIWG_SIWGJNI_H +#define COM_ATERVE_SIWG_SIWGJNI_H +#ifdef __cplusplus +extern "C" { +#endif + /* + * Class: com_aterve_siwg_SiwgJNI + * Method: siwgAddToQueue_first_arg + * Signature: (ILjava/lang/String;I)V + */ + JNIEXPORT void JNICALL Java_com_aterve_siwg_SiwgJNI_siwgAddToQueue + (JNIEnv *, jclass, jint, jstring); + +#ifdef __cplusplus +} +#endif +#endif \ No newline at end of file diff --git a/siwg/src/java/com/aterve/siwg/SiwgJNI.java b/siwg/src/java/com/aterve/siwg/SiwgJNI.java new file mode 100644 index 0000000..fd09065 --- /dev/null +++ b/siwg/src/java/com/aterve/siwg/SiwgJNI.java @@ -0,0 +1,271 @@ +package com.aterve.siwg; + +import android.app.Activity; +import android.content.Intent; +import androidx.annotation.NonNull; +import android.util.Log; +import android.view.Gravity; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +import com.google.android.gms.auth.api.signin.GoogleSignIn; +import com.google.android.gms.auth.api.signin.GoogleSignInAccount; +import com.google.android.gms.auth.api.signin.GoogleSignInClient; +import com.google.android.gms.auth.api.signin.GoogleSignInOptions; +import com.google.android.gms.common.api.ApiException; +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.GoogleApiAvailability; + +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.OnFailureListener; +import com.google.android.gms.tasks.OnSuccessListener; +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.Continuation; + +import java.io.IOException; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.json.JSONException; + +public class SiwgJNI { + + private static final String TAG = "SiwgJNI"; + + // Request code used to invoke sign in user interactions. + private static final int RC_UNUSED = 5001; + private static final int RC_SIGN_IN = 9001; + + // + // Global Constants (Same as in sigw.h) + // + + private static final int MSG_SIGN_IN = 1; + private static final int MSG_SILENT_SIGN_IN = 2; + private static final int MSG_SIGN_OUT = 3; + private static final int MSG_GET_EVENTS = 11; + + private static final int STATUS_SUCCESS = 1; + private static final int STATUS_FAILED = 2; + private static final int STATUS_CONFLICT = 4; + + // + // Global Properties + // + + private Activity activity; + private String client_id; + private boolean is_request_id_token; + private boolean is_request_auth_code; + private boolean is_supported; + + // + // Client Handles + // + + private GoogleSignInAccount mSignedInAccount = null; + private GoogleSignInOptions mSignInOptions; + private GoogleSignInClient mGoogleSignInClient; + + // JNI Add to Queue + public static native void siwgAddToQueue(int msg, String json); + + // + // Constructor + // + + public SiwgJNI(Activity activity, boolean is_disk_active, boolean is_request_auth_code, boolean is_request_id_token, String client_id) { + this.activity = activity; + this.client_id = client_id; + this.is_request_auth_code = is_request_auth_code; + this.is_request_id_token = is_request_id_token; + + // Check if device supports google sign in + this.is_supported = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(activity) == ConnectionResult.SUCCESS; + + // Create GoogleSignIn Client + mGoogleSignInClient = GoogleSignIn.getClient(activity, getSignInOptions()); + } + + // + // Event Listeners + // + + private OnFailureListener newOnFailureListener(final int messageId, final String message) { + return new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception e) { + sendCallbackMessage(messageId, "status", STATUS_FAILED, "error", message); + } + }; + } + + private OnSuccessListener newOnSuccessListenerForIntent(final int requestCode) { + return new OnSuccessListener() { + @Override + public void onSuccess(Intent intent) { + activity.startActivityForResult(intent, requestCode); + } + }; + } + + // + // Callback Message Methods + // + + private void sendCallbackMessage(int msg, String key_1, int value_1) { + String message = null; + try { + JSONObject obj = new JSONObject(); + obj.put(key_1, value_1); + message = obj.toString(); + } catch (JSONException e) { + message = "{ \"error\": \"Error while converting callback message to JSON: " + e.getMessage() + "\" }"; + } + siwgAddToQueue(msg, message); + } + + private void sendCallbackMessage(int msg, String key_1, int value_1, String key_2, String value_2) { + String message = null; + try { + JSONObject obj = new JSONObject(); + obj.put(key_1, value_1); + obj.put(key_2, value_2); + message = obj.toString(); + } catch (JSONException e) { + message = "{ \"error\": \"Error while converting callback message to JSON: " + e.getMessage() + "\" }"; + } + siwgAddToQueue(msg, message); + } + + private void sendCallbackMessage(int msg, String key_1, int value_1, String key_2, int value_2, String key_3, String value_3) { + String message = null; + try { + JSONObject obj = new JSONObject(); + obj.put(key_1, value_1); + obj.put(key_2, value_2); + obj.put(key_3, value_3); + message = obj.toString(); + } catch (JSONException e) { + message = "{ \"error\": \"Error while converting callback message to JSON: " + e.getMessage() + "\" }"; + } + siwgAddToQueue(msg, message); + } + + private void onConnected(GoogleSignInAccount googleSignInAccount, final int msg) { + if (mSignedInAccount != googleSignInAccount) { + mSignedInAccount = googleSignInAccount; + sendCallbackMessage(msg, "status", STATUS_SUCCESS); + } + } + + private GoogleSignInOptions getSignInOptions() { + if (mSignInOptions == null) { + + // Create Google Sign in (Default) + GoogleSignInOptions.Builder builder = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN); + + if (is_request_id_token && client_id != null) { + builder.requestIdToken(client_id); + } + + if (is_request_auth_code && client_id != null) { + builder.requestServerAuthCode(client_id); + } + + mSignInOptions = builder.build(); + } + + return mSignInOptions; + } + + public void activityResult(int requestCode, int resultCode, Intent intent) { + if (requestCode == RC_SIGN_IN) { + if (intent != null) { + Task task = GoogleSignIn.getSignedInAccountFromIntent(intent); + + if (task.isSuccessful()) { + onConnected(task.getResult(), MSG_SIGN_IN); + } else { + sendCallbackMessage(MSG_SIGN_IN, "status", STATUS_FAILED, "error", "SIWG Sign-in Failed. Task Not Successfull!"); + } + + } else { + sendCallbackMessage(MSG_SIGN_IN, "status", STATUS_FAILED, "error", "SIWG Sign-in Failed. Intent does not exist!"); + } + } + } + + public void silentLogin() { + this.activity.runOnUiThread(new Runnable() { + @Override + public void run() { + + GoogleSignInAccount account = GoogleSignIn.getLastSignedInAccount(activity); + + if (GoogleSignIn.hasPermissions(account, getSignInOptions().getScopeArray())) { + onConnected(account, MSG_SILENT_SIGN_IN); + } else { + + mGoogleSignInClient.silentSignIn().addOnCompleteListener(activity, + new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if (task.isSuccessful()) { + onConnected(task.getResult(), MSG_SILENT_SIGN_IN); + } else { + sendCallbackMessage(MSG_SILENT_SIGN_IN, "status", STATUS_FAILED, "error", "SIWG Silent Sign-in Failed."); + } + } + }); + } + } + }); + } + + // + // JNI Passed Methods + // + + public void login() { + Intent intent = mGoogleSignInClient.getSignInIntent(); + this.activity.startActivityForResult(intent, RC_SIGN_IN); + } + + public void logout() { + mGoogleSignInClient.signOut().addOnCompleteListener(this.activity, + new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + mSignedInAccount = null; + sendCallbackMessage(MSG_SIGN_OUT, "status", STATUS_SUCCESS); + } + }); + } + + public String getDisplayName() { + return isLoggedIn() ? mSignedInAccount.getDisplayName() : null; + } + + public String getId() { + return isLoggedIn() ? mSignedInAccount.getId() : null; + } + + public String getIdToken() { + return isLoggedIn() ? mSignedInAccount.getIdToken() : null; + } + + public String getServerAuthCode() { + return isLoggedIn() ? mSignedInAccount.getServerAuthCode() : null; + } + + public boolean isLoggedIn() { + return mSignedInAccount != null; + } + + public boolean isSupported() { + return is_supported; + } + +} diff --git a/siwg/src/private_siwg_callback.h b/siwg/src/private_siwg_callback.h new file mode 100644 index 0000000..5a3d922 --- /dev/null +++ b/siwg/src/private_siwg_callback.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include "siwg.h" + +struct SIWG_callback +{ + SIWG_callback() : m_L(0), m_Callback(LUA_NOREF), m_Self(LUA_NOREF) {} + lua_State* m_L; + int m_Callback; + int m_Self; +}; + +struct CallbackData +{ + MESSAGE_ID msg; + char* json; +}; + +void siwg_set_callback(lua_State* L, int pos); +void siwg_callback_initialize(); +void siwg_callback_finalize(); +void siwg_callback_update(); +void siwg_add_to_queue(MESSAGE_ID msg, const char*json); \ No newline at end of file diff --git a/siwg/src/siwg.cpp b/siwg/src/siwg.cpp new file mode 100644 index 0000000..8075336 --- /dev/null +++ b/siwg/src/siwg.cpp @@ -0,0 +1,403 @@ +#define EXTENSION_NAME SIWG +#define LIB_NAME "SIWG" +#define MODULE_NAME "siwg" + +// include the Defold SDK +#include + +#if defined(DM_PLATFORM_ANDROID) + +#include + +#include "siwg.h" +#include "siwg_jni.h" +#include "private_siwg_callback.h" +#include "com_aterve_siwg_SiwgJNI.h" + +static bool luaL_checkbool(lua_State *L, int numArg) +{ + bool b = false; + if (lua_isboolean(L, numArg)) + { + b = lua_toboolean(L, numArg); + } + else + { + luaL_typerror(L, numArg, lua_typename(L, LUA_TBOOLEAN)); + } + return b; +} + +static bool luaL_checkboold(lua_State *L, int numArg, int def) +{ + int type = lua_type(L, numArg); + if (type != LUA_TNONE && type != LUA_TNIL) + { + return luaL_checkbool(L, numArg); + } + return def; +} + +static lua_Number luaL_checknumberd(lua_State *L, int numArg, lua_Number def) +{ + int type = lua_type(L, numArg); + if (type != LUA_TNONE && type != LUA_TNIL) + { + return luaL_checknumber(L, numArg); + } + return def; +} + +static char* luaL_checkstringd(lua_State *L, int numArg, const char* def) +{ + int type = lua_type(L, numArg); + if (type != LUA_TNONE && type != LUA_TNIL) + { + return (char*)luaL_checkstring(L, numArg); + } + return (char*)def; +} + +static lua_Number luaL_checktable_number(lua_State *L, int numArg, const char* field, lua_Number def) +{ + lua_Number result = def; + if(lua_istable(L, numArg)) + { + lua_getfield(L, numArg, field); + if(!lua_isnil(L, -1)) + { + result = luaL_checknumber(L, -1); + } + lua_pop(L, 1); + } + return result; +} + +static char* luaL_checktable_string(lua_State *L, int numArg, const char* field, char* def) +{ + char* result = def; + if(lua_istable(L, numArg)) + { + lua_getfield(L, numArg, field); + if(!lua_isnil(L, -1)) + { + result = (char*)luaL_checkstring(L, -1); + } + lua_pop(L, 1); + } + return result; +} + + +// +// JNI Method Executors +// + +// void method() +static int CallVoidMethod(jobject instance, jmethodID method) +{ + ThreadAttacher attacher; + JNIEnv *env = attacher.env; + env->CallVoidMethod(instance, method); + return 0; +} + +// string method() +static int CallStringMethod(lua_State* L, jobject instance, jmethodID method) +{ + DM_LUA_STACK_CHECK(L, 1); + ThreadAttacher attacher; + JNIEnv *env = attacher.env; + jstring return_value = (jstring)env->CallObjectMethod(instance, method); + if (return_value) + { + const char* cstr = env->GetStringUTFChars(return_value, 0); + lua_pushstring(L, cstr); + env->ReleaseStringUTFChars(return_value, cstr); + env->DeleteLocalRef(return_value); + } + else + { + lua_pushnil(L); + } + return 1; +} + +// boolean method() +static int CallBooleanMethod(lua_State* L, jobject instance, jmethodID method) +{ + DM_LUA_STACK_CHECK(L, 1); + ThreadAttacher attacher; + JNIEnv *env = attacher.env; + jboolean return_value = (jboolean)env->CallBooleanMethod(instance, method); + lua_pushboolean(L, JNI_TRUE == return_value); + return 1; +} + +// +// SIWG Handler +// + +struct SIWG +{ + jobject m_SiwgJNI; + jmethodID m_silentLogin; + jmethodID m_login; + jmethodID m_logout; + jmethodID m_activityResult; + jmethodID m_getDisplayName; + jmethodID m_getId; + jmethodID m_getIdToken; + jmethodID m_getServerAuthCode; + jmethodID m_isLoggedIn; + jmethodID m_isSupported; +}; + +static SIWG g_siwg; + +static int SiwgAuth_Login(lua_State* L) +{ + CallVoidMethod(g_siwg.m_SiwgJNI, g_siwg.m_login); + return 0; +} + +static int SiwgAuth_Logout(lua_State* L) +{ + CallVoidMethod(g_siwg.m_SiwgJNI, g_siwg.m_logout); + return 0; +} + +static int SiwgAuth_SilentLogin(lua_State* L) +{ + CallVoidMethod(g_siwg.m_SiwgJNI, g_siwg.m_silentLogin); + return 0; +} + +static int SiwgAuth_getDisplayName(lua_State* L) +{ + return CallStringMethod(L, g_siwg.m_SiwgJNI, g_siwg.m_getDisplayName); +} + +static int SiwgAuth_getId(lua_State* L) +{ + return CallStringMethod(L, g_siwg.m_SiwgJNI, g_siwg.m_getId); +} + +static int SiwgAuth_getIdToken(lua_State* L) +{ + return CallStringMethod(L, g_siwg.m_SiwgJNI, g_siwg.m_getIdToken); +} + +static int SiwgAuth_getServerAuthCode(lua_State* L) +{ + return CallStringMethod(L, g_siwg.m_SiwgJNI, g_siwg.m_getServerAuthCode); +} + +static int SiwgAuth_isLoggedIn(lua_State* L) +{ + return CallBooleanMethod(L, g_siwg.m_SiwgJNI, g_siwg.m_isLoggedIn); +} + +static int SiwgAuth_isSupported(lua_State* L) +{ + return CallBooleanMethod(L, g_siwg.m_SiwgJNI, g_siwg.m_isSupported); +} + +static int SiwgAuth_set_callback(lua_State* L) +{ + siwg_set_callback(L, 1); + return 0; +} + +// +// JNI Extension Methods +// + +static void OnActivityResult(void *env, void* activity, int32_t request_code, int32_t result_code, void* result) +{ + ThreadAttacher attacher; + JNIEnv *_env = attacher.env; + + _env->CallVoidMethod(g_siwg.m_SiwgJNI, g_siwg.m_activityResult, request_code, result_code, result); +} + +JNIEXPORT void JNICALL Java_com_aterve_siwg_SiwgJNI_siwgAddToQueue(JNIEnv * env, jclass cls, jint jmsg, jstring jjson) +{ + const char* json = env->GetStringUTFChars(jjson, 0); + siwg_add_to_queue((MESSAGE_ID)jmsg, json); + env->ReleaseStringUTFChars(jjson, json); +} + +// Functions exposed to Lua +static const luaL_reg Siwg_methods[] = +{ + {"is_supported", SiwgAuth_isSupported}, + {"login", SiwgAuth_Login}, + {"logout", SiwgAuth_Logout}, + {"silent_login", SiwgAuth_SilentLogin}, + {"get_display_name", SiwgAuth_getDisplayName}, + {"get_id", SiwgAuth_getId}, + {"get_id_token", SiwgAuth_getIdToken}, + {"get_server_auth_code", SiwgAuth_getServerAuthCode}, + {"is_logged_in", SiwgAuth_isLoggedIn}, + {"set_callback", SiwgAuth_set_callback}, + {0, 0} +}; + +dmExtension::Result AppInitializeSIWG(dmExtension::AppParams* params) +{ + dmLogInfo("[SIWG] Registered Extension!"); + return dmExtension::RESULT_OK; +} + +static void LuaInit(lua_State* L) +{ + DM_LUA_STACK_CHECK(L, 0); + luaL_register(L, MODULE_NAME, Siwg_methods); + +#define SETCONSTANT(name) \ + lua_pushnumber(L, (lua_Number) name); \ + lua_setfield(L, -2, #name); \ + + SETCONSTANT(MSG_SIGN_IN) + SETCONSTANT(MSG_SILENT_SIGN_IN) + SETCONSTANT(MSG_SIGN_OUT) + + SETCONSTANT(STATUS_SUCCESS) + SETCONSTANT(STATUS_FAILED) + SETCONSTANT(STATUS_CONFLICT) +#undef SETCONSTANT + + lua_pop(L, 1); +} + +// +// JNI Setup Methods +// + +static void InitJNIMethods(JNIEnv* env, jclass cls) +{ + //general + g_siwg.m_isSupported = env->GetMethodID(cls, "isSupported", "()Z"); + + //authorization + g_siwg.m_silentLogin = env->GetMethodID(cls, "silentLogin", "()V"); + g_siwg.m_login = env->GetMethodID(cls, "login", "()V"); + g_siwg.m_logout = env->GetMethodID(cls, "logout", "()V"); + g_siwg.m_isLoggedIn = env->GetMethodID(cls, "isLoggedIn", "()Z"); + g_siwg.m_getDisplayName = env->GetMethodID(cls, "getDisplayName", "()Ljava/lang/String;"); + g_siwg.m_getId = env->GetMethodID(cls, "getId", "()Ljava/lang/String;"); + g_siwg.m_getIdToken = env->GetMethodID(cls, "getIdToken", "()Ljava/lang/String;"); + g_siwg.m_getServerAuthCode = env->GetMethodID(cls, "getServerAuthCode", "()Ljava/lang/String;"); + + //private methods + g_siwg.m_activityResult = env->GetMethodID(cls, "activityResult", "(IILandroid/content/Intent;)V"); +} + + +static void CheckInitializationParams(const char* client_id, bool request_server_auth_code, bool request_id_token) +{ + bool is_empty_client_id = client_id == 0 || strlen(client_id) == 0; + + if (is_empty_client_id && request_server_auth_code) + { + dmLogError("'siwg.client_id' must be defined to request server auth code!"); + } + + if (is_empty_client_id && request_id_token) + { + dmLogError("'siwg.client_id' must be defined to request id token!"); + } +} + +static void InitializeJNI(const char* client_id, bool request_server_auth_code, bool request_id_token) +{ + CheckInitializationParams(client_id, request_server_auth_code > 0, request_id_token > 0); + dmLogInfo("[SIWG] InitJNI Checked Params"); + + ThreadAttacher attacher; + JNIEnv *env = attacher.env; + ClassLoader class_loader = ClassLoader(env); + jclass cls = class_loader.load("com.aterve.siwg.SiwgJNI"); + dmLogInfo("[SIWG] InitJNI Loaded Class"); + + InitJNIMethods(env, cls); + dmLogInfo("[SIWG] InitJNI Methods Initialized"); + + jmethodID jni_constructor = env->GetMethodID(cls, "", "(Landroid/app/Activity;ZZZLjava/lang/String;)V"); + jstring java_client_id = env->NewStringUTF(client_id); + + g_siwg.m_SiwgJNI = env->NewGlobalRef(env->NewObject(cls, jni_constructor, dmGraphics::GetNativeAndroidActivity(), true, request_server_auth_code, request_id_token, java_client_id)); + env->DeleteLocalRef(java_client_id); + dmLogInfo("[SIWG] InitJNI JNI Refrences Initialized"); +} + +// +// Extention Event Methods +// + +dmExtension::Result InitializeSIWG(dmExtension::Params* params) +{ + dmLogInfo("[SIWG] Initializing Extension..."); + + LuaInit(params->m_L); + int request_server_auth_code = dmConfigFile::GetInt(params->m_ConfigFile, "siwg.request_server_auth_code", 0); + int request_id_token = dmConfigFile::GetInt(params->m_ConfigFile, "siwg.request_id_token", 0); + const char* client_id = dmConfigFile::GetString(params->m_ConfigFile, "siwg.client_id", 0); + dmLogInfo("[SIWG] Loaded Settings"); + + InitializeJNI(client_id, request_server_auth_code > 0, request_id_token > 0); + + dmExtension::RegisterAndroidOnActivityResultListener(OnActivityResult); + dmLogInfo("[SIWG] Activity Result Listener Initialized"); + + siwg_callback_initialize(); + + dmLogInfo("[SIWG] Initialization Completed!"); + + return dmExtension::RESULT_OK; +} + +dmExtension::Result UpdateSIWG(dmExtension::Params* params) +{ + siwg_callback_update(); + return dmExtension::RESULT_OK; +} + +dmExtension::Result FinalizeSIWG(dmExtension::Params* params) +{ + dmLogInfo("[SIWG] Finalizing Extension..."); + + siwg_callback_finalize(); + + dmExtension::UnregisterAndroidOnActivityResultListener(OnActivityResult); + dmLogInfo("[SIWG] Activity Result Listener Unregistered"); + + return dmExtension::RESULT_OK; +} + +dmExtension::Result AppFinalizeSIWG(dmExtension::AppParams* params) +{ + return dmExtension::RESULT_OK; +} + +DM_DECLARE_EXTENSION(EXTENSION_NAME, LIB_NAME, AppInitializeSIWG, AppFinalizeSIWG, InitializeSIWG, UpdateSIWG, 0, FinalizeSIWG) + +#else + +dmExtension::Result InitializeSIWG(dmExtension::Params* params) +{ + dmLogInfo("[SIWG] Initializing Extension..."); + dmLogInfo("[SIWG] Initialization Failed! (Invalid OS Type)"); + return dmExtension::RESULT_OK; +} + +dmExtension::Result FinalizeSIWG(dmExtension::Params* params) +{ + dmLogInfo("[SIWG] Finalizing Extension..."); + return dmExtension::RESULT_OK; +} + +DM_DECLARE_EXTENSION(EXTENSION_NAME, LIB_NAME, 0, 0, InitializeSIWG, 0, 0, FinalizeSIWG) + +#endif \ No newline at end of file diff --git a/siwg/src/siwg.h b/siwg/src/siwg.h new file mode 100644 index 0000000..131ac0b --- /dev/null +++ b/siwg/src/siwg.h @@ -0,0 +1,17 @@ +#pragma once + +// Internal to the extension +enum MESSAGE_ID +{ + MSG_SIGN_IN = 1, + MSG_SILENT_SIGN_IN = 2, + MSG_SIGN_OUT = 3, +}; + +// Internal to the extension +enum STATUS +{ + STATUS_SUCCESS = 1, + STATUS_FAILED = 2, + STATUS_CONFLICT = 4, +}; diff --git a/siwg/src/siwg_callback.cpp b/siwg/src/siwg_callback.cpp new file mode 100644 index 0000000..3383cec --- /dev/null +++ b/siwg/src/siwg_callback.cpp @@ -0,0 +1,157 @@ +#if defined(DM_PLATFORM_IOS) || defined(DM_PLATFORM_ANDROID) +#include "private_siwg_callback.h" +#include + +static SIWG_callback m_callback; +static dmArray m_callbacksQueue; +static dmMutex::HMutex m_mutex; + +static void RegisterCallback(lua_State* L, int index) +{ + SIWG_callback *cbk = &m_callback; + if(cbk->m_Callback != LUA_NOREF) + { + dmScript::Unref(cbk->m_L, LUA_REGISTRYINDEX, cbk->m_Callback); + dmScript::Unref(cbk->m_L, LUA_REGISTRYINDEX, cbk->m_Self); + } + + cbk->m_L = dmScript::GetMainThread(L); + + luaL_checktype(L, index, LUA_TFUNCTION); + lua_pushvalue(L, index); + cbk->m_Callback = dmScript::Ref(L, LUA_REGISTRYINDEX); + + dmScript::GetInstance(L); + cbk->m_Self = dmScript::Ref(L, LUA_REGISTRYINDEX); +} + +static void UnregisterCallback() +{ + SIWG_callback *cbk = &m_callback; + if(cbk->m_Callback != LUA_NOREF) + { + dmScript::Unref(cbk->m_L, LUA_REGISTRYINDEX, cbk->m_Callback); + dmScript::Unref(cbk->m_L, LUA_REGISTRYINDEX, cbk->m_Self); + cbk->m_Callback = LUA_NOREF; + } +} + +static void siwg_invoke_callback(MESSAGE_ID type, char*json) +{ + SIWG_callback *cbk = &m_callback; + if(cbk->m_Callback == LUA_NOREF) + { + dmLogInfo("GPGS callback do not exist."); + return; + } + + lua_State* L = cbk->m_L; + int top = lua_gettop(L); + lua_rawgeti(L, LUA_REGISTRYINDEX, cbk->m_Callback); + lua_rawgeti(L, LUA_REGISTRYINDEX, cbk->m_Self); + lua_pushvalue(L, -1); + dmScript::SetInstance(L); + + if (!dmScript::IsInstanceValid(L)) + { + UnregisterCallback(); + dmLogError("Could not run GPGS callback because the instance has been deleted."); + lua_pop(L, 2); + } + else { + lua_pushnumber(L, type); + int count_table_elements = 1; + bool is_fail = false; + dmJson::Document doc; + dmJson::Result r = dmJson::Parse(json, &doc); + if (r == dmJson::RESULT_OK && doc.m_NodeCount > 0) { + char error_str_out[128]; + if (dmScript::JsonToLua(L, &doc, 0, error_str_out, sizeof(error_str_out)) < 0) { + dmLogError("Failed converting object JSON to Lua; %s", error_str_out); + is_fail = true; + } + } else { + dmLogError("Failed to parse JSON object(%d): (%s)", r, json); + is_fail = true; + } + dmJson::Free(&doc); + if (is_fail) { + lua_pop(L, 2); + assert(top == lua_gettop(L)); + return; + } + + int number_of_arguments = 3; + int ret = lua_pcall(L, number_of_arguments, 0, 0); + if(ret != 0) + { + dmLogError("Error running callback: %s", lua_tostring(L, -1)); + lua_pop(L, 1); + } + } + assert(top == lua_gettop(L)); +} + +void siwg_callback_initialize() +{ + m_mutex = dmMutex::New(); + dmLogInfo("[SIWG] Callback initialized"); +} + +void siwg_callback_finalize() +{ + dmMutex::Delete(m_mutex); + UnregisterCallback(); + dmLogInfo("[SIWG] Callback Unregistered"); +} + +void siwg_set_callback(lua_State* L, int pos) +{ + int type = lua_type(L, pos); + if (type == LUA_TNONE || type == LUA_TNIL) + { + UnregisterCallback(); + } + else + { + RegisterCallback(L, pos); + } +} + +void siwg_add_to_queue(MESSAGE_ID msg, const char*json) +{ + DM_MUTEX_SCOPED_LOCK(m_mutex); + + CallbackData data; + data.msg = msg; + data.json = json ? strdup(json) : NULL; + + if(m_callbacksQueue.Full()) + { + m_callbacksQueue.OffsetCapacity(1); + } + m_callbacksQueue.Push(data); +} + +void siwg_callback_update() +{ + if (m_callbacksQueue.Empty()) + { + return; + } + + DM_MUTEX_SCOPED_LOCK(m_mutex); + + for(uint32_t i = 0; i != m_callbacksQueue.Size(); ++i) + { + CallbackData* data = &m_callbacksQueue[i]; + siwg_invoke_callback(data->msg, data->json); + if(data->json) + { + free(data->json); + data->json = 0; + } + } + m_callbacksQueue.SetSize(0); +} +#endif \ No newline at end of file diff --git a/siwg/src/siwg_jni.h b/siwg/src/siwg_jni.h new file mode 100644 index 0000000..4ec345f --- /dev/null +++ b/siwg/src/siwg_jni.h @@ -0,0 +1,54 @@ +#pragma once + +#if defined(DM_PLATFORM_ANDROID) + +#include +#include + +struct ThreadAttacher { + JNIEnv *env; + bool has_attached; + ThreadAttacher() : env(NULL), has_attached(false) { + if (dmGraphics::GetNativeAndroidJavaVM()->GetEnv((void **)&env, JNI_VERSION_1_6) != JNI_OK) { + dmGraphics::GetNativeAndroidJavaVM()->AttachCurrentThread(&env, NULL); + has_attached = true; + } + } + ~ThreadAttacher() { + if (has_attached) { + if (env->ExceptionCheck()) { + env->ExceptionDescribe(); + } + env->ExceptionClear(); + dmGraphics::GetNativeAndroidJavaVM()->DetachCurrentThread(); + } + } +}; + +struct ClassLoader { + private: + JNIEnv *env; + jobject class_loader_object; + jmethodID find_class; + public: + ClassLoader(JNIEnv *env) : env(env) { + jclass activity_class = env->FindClass("android/app/NativeActivity"); + jmethodID get_class_loader = env->GetMethodID(activity_class, "getClassLoader", "()Ljava/lang/ClassLoader;"); + class_loader_object = env->CallObjectMethod(dmGraphics::GetNativeAndroidActivity(), get_class_loader); + jclass class_loader = env->FindClass("java/lang/ClassLoader"); + find_class = env->GetMethodID(class_loader, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;"); + env->DeleteLocalRef(activity_class); + env->DeleteLocalRef(class_loader); + } + ~ClassLoader() { + env->DeleteLocalRef(class_loader_object); + } + jclass load(const char *class_name) { + jstring str_class_name = env->NewStringUTF(class_name); + jclass loaded_class = (jclass)env->CallObjectMethod(class_loader_object, find_class, str_class_name); + env->DeleteLocalRef(str_class_name); + return loaded_class; + } +}; + +#endif \ No newline at end of file