Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,28 +25,15 @@ private AndroidKeyBridge()

public void init(Context context)
{
if (context == null) {
Log.w("AndroidKeyBridge", "Provided context is null, trying to get UnityPlayer.currentActivity");
try {
context = com.unity3d.player.UnityPlayer.currentActivity;
} catch (Exception e) {
Log.e("AndroidKeyBridge", "Failed to get UnityPlayer.currentActivity", e);
}
}

if (context == null) {
Log.e("AndroidKeyBridge", "Context is still null, cannot initialize AndroidKeyBridge");
return;
}

this.context = context;

if (masterKey == null || sharedPreferences == null) {
if (masterKey == null) {

try {
masterKey = new MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build();

sharedPreferences = EncryptedSharedPreferences.create(
context,
"secret_shared_prefs",
Expand All @@ -55,9 +42,9 @@ public void init(Context context)
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
);
} catch (GeneralSecurityException e){
Log.d("AndroidKeyBridge", "Encountered error when initializing AndroidKeyBridge: " + e.getMessage());
Log.d("Exception", e.getMessage());
} catch (IOException e){
Log.d("AndroidKeyBridge", "Encountered error when initializing AndroidKeyBridge: " + e.getMessage());
Log.d("Exception", e.getMessage());
}
}
}
Expand Down Expand Up @@ -87,17 +74,11 @@ public static String GetKeychainValue(String key)

private void RunSaveKeychainValue(String key, String value)
{
if (masterKey == null || sharedPreferences == null) {
init(context);
}
sharedPreferences.edit().putString(key, value).apply();
}

private String RunGetKeychainValue(String key)
{
if (masterKey == null || sharedPreferences == null) {
init(context);
}
return sharedPreferences.getString(key, "");
}
}
32 changes: 32 additions & 0 deletions Assets/Plugins/Android/AndroidKeyBridge.java.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

91 changes: 91 additions & 0 deletions Assets/Plugins/Android/GoogleSignInPlugin.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package xyz.sequence;

import android.content.Context;
import android.util.Log;

import androidx.annotation.NonNull;
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.GetCredentialException;
import androidx.credentials.exceptions.NoCredentialException;
import androidx.credentials.exceptions.GetCredentialCustomException;

import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption;
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential;

import java.security.SecureRandom;
import java.util.Base64;
import java.util.concurrent.Executors;

import com.unity3d.player.UnityPlayer;

public class GoogleSignInPlugin {
private static final String TAG = "SequenceGoogleSignIn";

public static void signIn(Context context, String clientId)
{
getCredentialAsync(context, clientId);
}

private static void getCredentialAsync(Context context, String clientId)
{
GetSignInWithGoogleOption googleIdOption = new GetSignInWithGoogleOption
.Builder(clientId)
.setNonce(generateNonce())
.build();

GetCredentialRequest request = new GetCredentialRequest.Builder().addCredentialOption(googleIdOption).build();

CredentialManager credentialManager = CredentialManager.create(context);
credentialManager.getCredentialAsync(
context,
request,
null,
Executors.newSingleThreadExecutor(),
new CredentialManagerCallback<GetCredentialResponse, GetCredentialException>() {
@Override
public void onResult(GetCredentialResponse getCredentialResponse) {
handleGetCredentialResponse(getCredentialResponse);
}

@Override
public void onError(@NonNull GetCredentialException e) {
Log.e(TAG, "Error getting credential", e);
if (e instanceof GetCredentialCustomException) {
GetCredentialCustomException customException = (GetCredentialCustomException) e;
Log.e(TAG, "Custom Exception Type: " + customException.getType());
}

UnityPlayer.UnitySendMessage("NativeGoogleSignInReceiver", "HandleError", "Error");
}
}
);
}

private static void handleGetCredentialResponse(GetCredentialResponse getCredentialResponse) {
Credential credential = getCredentialResponse.getCredential();
if (credential instanceof CustomCredential && GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL.equals(credential.getType())) {
try {
GoogleIdTokenCredential idTokenCredential = GoogleIdTokenCredential.createFrom(credential.getData());
String idToken = idTokenCredential.getIdToken();

UnityPlayer.UnitySendMessage("NativeGoogleSignInReceiver", "HandleIdToken", idToken);
} catch (Exception e) {
Log.e(TAG, "Failed to parse Google ID token response", e);
}
} else {
Log.e(TAG, "Unexpected credential type");
}
}

private static String generateNonce() {
SecureRandom random = new SecureRandom();
byte[] nonce = new byte[32];
random.nextBytes(nonce);
return Base64.getEncoder().encodeToString(nonce);
}
}
32 changes: 32 additions & 0 deletions Assets/Plugins/Android/GoogleSignInPlugin.java.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Assets/Plugins/Android/mainTemplate.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ apply plugin: 'com.android.library'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.security:security-crypto:1.1.0-alpha03'
implementation 'com.google.android.gms:play-services-auth:21.0.0'
implementation "androidx.credentials:credentials:1.2.0"
implementation "androidx.credentials:credentials-play-services-auth:1.2.0"
implementation "com.google.android.libraries.identity.googleid:googleid:1.1.0"
implementation "com.android.billingclient:billing:6.1.0"

**DEPS**}

Expand Down
111 changes: 96 additions & 15 deletions Packages/Sequence-Unity/Editor/AndroidDependencyManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Linq;
using Sequence.Config;
using System.IO;
using System.Text.RegularExpressions;

namespace Sequence.Editor
{
Expand All @@ -17,7 +18,18 @@ namespace Sequence.Editor
public class AndroidDependencyManager : IPreprocessBuildWithReport
{
public const string SecureStoragePluginFilename = "AndroidKeyBridge.java";
public const string SequenceGoogleSignInPluginFilename = "GoogleSignInPlugin.java";
public const string GradleTemplateFilename = "mainTemplate.gradle";

private static string[] GradleDependencies = new[]
{
"androidx.security:security-crypto",
"com.google.android.gms:play-services-auth",
"androidx.credentials:credentials",
"androidx.credentials:credentials-play-services-auth",
"com.google.android.libraries.identity.googleid:googleid",
};

private const string RelevantDocsUrl =
"https://docs.sequence.xyz/sdk/unity/onboard/recovering-sessions#android";

Expand All @@ -26,38 +38,107 @@ public class AndroidDependencyManager : IPreprocessBuildWithReport
public void OnPreprocessBuild(BuildReport report)
{
#if UNITY_ANDROID
BuildTarget target = report.summary.platform;
SequenceConfig config = SequenceConfig.GetConfig();
var target = report.summary.platform;
var config = SequenceConfig.GetConfig();

CheckPlugin(SecureStoragePluginFilename, config.StoreSessionPrivateKeyInSecureStorage, target);
CheckPlugin(SequenceGoogleSignInPluginFilename, true, target);
CheckGradleTemplateDependencies();
AssetDatabase.Refresh();
#endif
}

[MenuItem("Sequence/Android Plugins")]
public static void Test()
{
CheckPlugin(SecureStoragePluginFilename, true, BuildTarget.Android);
CheckPlugin(SequenceGoogleSignInPluginFilename, true, BuildTarget.Android);
}

string[] files = Directory.GetFiles("Assets", SecureStoragePluginFilename, SearchOption.AllDirectories);
string pluginPath = files.FirstOrDefault();
private static void CheckPlugin(string fileName, bool enable, BuildTarget platform)
{
var existingFiles = Directory.GetFiles("Assets", fileName, SearchOption.AllDirectories);
var pluginPath = existingFiles.FirstOrDefault();

if (string.IsNullOrEmpty(pluginPath))
{
if (config.StoreSessionPrivateKeyInSecureStorage)
{
ShowWarning($"Secure Storage plugin '{SecureStoragePluginFilename}' not found in project. Please make sure you have imported it via Samples in Package Manager");
}
if (enable)
TryCopyPlugin(fileName);

return;
}

PluginImporter pluginImporter = AssetImporter.GetAtPath(pluginPath) as PluginImporter;
if (pluginImporter == null)
var pluginImporter = AssetImporter.GetAtPath(pluginPath) as PluginImporter;
if (!pluginImporter)
{
ShowWarning($"Unable to create {nameof(PluginImporter)} instance at path: {pluginPath}");
return;
}

pluginImporter.SetCompatibleWithPlatform(target, config.StoreSessionPrivateKeyInSecureStorage);
pluginImporter.SetCompatibleWithPlatform(platform, enable);
pluginImporter.SaveAndReimport();
Debug.Log(
$"Secure Storage plugin compatibility set to {config.StoreSessionPrivateKeyInSecureStorage} for path: {pluginPath}");
#endif

Debug.Log($"Plugin {fileName} compatibility set to {enable} for path: {pluginPath}");
}

private static void TryCopyPlugin(string fileName)
{
var targetPath = Path.Combine(Application.dataPath, "Plugins/Android", fileName);
var sourcePath = FindFileInPackages("Plugins/Android/" + fileName);

var directory = Path.GetDirectoryName(targetPath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
Directory.CreateDirectory(directory);

File.Copy(sourcePath, targetPath);
}

private static void CheckGradleTemplateDependencies()
{
var existingFiles = Directory.GetFiles("Assets", GradleTemplateFilename, SearchOption.AllDirectories);
var gradleFilePath = existingFiles.FirstOrDefault();

if (string.IsNullOrEmpty(gradleFilePath) || !File.Exists(gradleFilePath))
{
ShowWarning("mainTemplate.gradle does not exist. Trigger a build once to generate it.");
return;
}

var content = File.ReadAllText(gradleFilePath);
foreach (var dep in GradleDependencies)
{
if (!content.Contains(dep))
{
ShowWarning($"{GradleTemplateFilename} does not include '{dep}' as a dependency.");
}
}
}

private void ShowWarning(string warning)
private static void ShowWarning(string warning)
{
Debug.LogWarning(warning);
SequenceWarningPopup.ShowWindow(new List<string>() {warning}, RelevantDocsUrl);
}

private static string FindFileInPackages(string relativeFilePath)
{
var filePath = $"Packages/Sequence-Unity/{relativeFilePath}";
if (File.Exists(filePath))
return filePath;

var directories = Directory.GetDirectories("Library/PackageCache/", "xyz.0xsequence.waas-unity*", SearchOption.TopDirectoryOnly);
if (directories.Length > 0)
{
var packageDir = directories[0];
var packageJsonPath = Path.Combine(packageDir, relativeFilePath);
if (File.Exists(packageJsonPath))
{
return packageJsonPath;
}
}

ShowWarning("Plugin file not found: " + relativeFilePath);
return null;
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading