diff --git a/Source/Immutable/Immutable_UPL_Android.xml b/Source/Immutable/Immutable_UPL_Android.xml
index ea9a9cf6..816c4246 100644
--- a/Source/Immutable/Immutable_UPL_Android.xml
+++ b/Source/Immutable/Immutable_UPL_Android.xml
@@ -9,7 +9,9 @@
-dontwarn com.immutable.unreal
-keep class com.immutable.unreal.** { *; }
-keep interface com.immutable.unreal.** { *; }
- -keep public class com.immutable.unreal.ImmutableAndroid.** { public protected *; }
+ -keep public class com.immutable.unreal.ImmutableActivity { public protected *; }
+ -keep public class com.immutable.unreal.CustomTabsController { public protected *; }
+ -keep public class com.immutable.unreal.RedirectActivity { public protected *; }
-dontwarn androidx.**
-keep class androidx.** { *; }
@@ -22,44 +24,27 @@
+
+
+
-
-
- import com.immutable.unreal.ImmutableAndroid;
-
-
-
-
- ImmutableAndroid.Callback,
-
-
-
-
- Uri uri = getIntent().getData();
- if (uri != null) {
- String deeplink = uri.toString();
- handleDeepLink(uri.toString());
- }
-
-
-
-
- Uri uri = getIntent().getData();
- if (uri != null) {
- String deeplink = uri.toString();
- handleDeepLink(deeplink);
- }
-
-
- public native void handleDeepLink(String Deeplink);
+ public static native void handleDeepLink(String Deeplink);
- public native void handleOnCustomTabsDismissed(String Url);
+ public static native void handleOnCustomTabsDismissed(String Url);
- @Override
- public void onCustomTabsDismissed(String Url) {
- handleOnCustomTabsDismissed(Url);
+ public static void onDeeplinkResult(String url) {
+ handleDeepLink(url);
+ }
+
+ public static void onCustomTabsDismissed(String url) {
+ handleOnCustomTabsDismissed(url);
}
diff --git a/Source/Immutable/Private/Immutable/Android/Java/CustomTabsController.java b/Source/Immutable/Private/Immutable/Android/Java/CustomTabsController.java
new file mode 100644
index 00000000..49584a95
--- /dev/null
+++ b/Source/Immutable/Private/Immutable/Android/Java/CustomTabsController.java
@@ -0,0 +1,153 @@
+package com.immutable.unreal;
+
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.Insets;
+import android.net.Uri;
+import android.os.Build;
+import android.util.DisplayMetrics;
+import android.view.WindowInsets;
+import android.view.WindowMetrics;
+
+import androidx.annotation.NonNull;
+import androidx.browser.customtabs.CustomTabsCallback;
+import androidx.browser.customtabs.CustomTabsClient;
+import androidx.browser.customtabs.CustomTabsIntent;
+import androidx.browser.customtabs.CustomTabsService;
+import androidx.browser.customtabs.CustomTabsServiceConnection;
+import androidx.browser.customtabs.CustomTabsSession;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class CustomTabsController extends CustomTabsServiceConnection {
+ private static final long MAX_WAIT_TIME_SECONDS = 1;
+
+ private final WeakReference context;
+ private final AtomicReference session;
+ private final CountDownLatch sessionLatch;
+ private final String preferredPackage;
+ private final CustomTabsCallback callback;
+
+ private boolean didTryToBindService;
+
+ public CustomTabsController(@NonNull Activity context, CustomTabsCallback callback) {
+ this.context = new WeakReference<>(context);
+ this.session = new AtomicReference<>();
+ this.sessionLatch = new CountDownLatch(1);
+ this.callback = callback;
+ this.preferredPackage = getPreferredCustomTabsPackage(context);
+ }
+
+ // Get all apps that can support Custom Tabs Service
+ // i.e. services that can handle ACTION_CUSTOM_TABS_CONNECTION intents
+ private String getPreferredCustomTabsPackage(@NonNull Activity context) {
+ PackageManager packageManager = context.getPackageManager();
+ Intent serviceIntent = new Intent();
+ serviceIntent.setAction(CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION);
+ List resolvedList = packageManager.queryIntentServices(serviceIntent, 0);
+ List packageNames = new ArrayList<>();
+ for (ResolveInfo info : resolvedList) {
+ if (info.serviceInfo != null) {
+ packageNames.add(info.serviceInfo.packageName);
+ }
+ }
+ if (packageNames.size() > 0) {
+ // Get the preferred Custom Tabs package
+ return CustomTabsClient.getPackageName(context, packageNames);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public void onCustomTabsServiceConnected(@NonNull ComponentName componentName, @NonNull CustomTabsClient client) {
+ client.warmup(0L);
+ CustomTabsSession customTabsSession = client.newSession(callback);
+ session.set(customTabsSession);
+ sessionLatch.countDown();
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName componentName) {
+ session.set(null);
+ }
+
+ public void bindService() {
+ Context context = this.context.get();
+ didTryToBindService = false;
+ if (context != null && preferredPackage != null) {
+ didTryToBindService = true;
+ CustomTabsClient.bindCustomTabsService(context, preferredPackage, this);
+ }
+ }
+
+ public void unbindService() {
+ Context context = this.context.get();
+ if (didTryToBindService && context != null) {
+ context.unbindService(this);
+ didTryToBindService = false;
+ }
+ }
+
+ public void launch(@NonNull final Uri uri) {
+ final Activity context = this.context.get();
+ if (context == null) {
+ // Custom tabs context is no longer valid
+ return;
+ }
+
+ if (preferredPackage == null) {
+ // Could not get the preferred Custom Tab browser, so launch URL in any browser
+ context.startActivity(new Intent(Intent.ACTION_VIEW, uri));
+ } else {
+ // Running in a different thread to prevent doing too much work on main thread
+ new Thread(() -> {
+ try {
+ launchCustomTabs(context, uri);
+ } catch (ActivityNotFoundException ex) {
+ // Failed to launch Custom Tab browser, so launch in browser
+ context.startActivity(new Intent(Intent.ACTION_VIEW, uri));
+ }
+ }).start();
+ }
+ }
+
+ private void launchCustomTabs(Activity context, Uri uri) {
+ bindService();
+ try {
+ boolean ignored = sessionLatch.await(MAX_WAIT_TIME_SECONDS, TimeUnit.SECONDS);
+ } catch (InterruptedException ignored) {
+ }
+
+ final CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder(session.get())
+ .setInitialActivityHeightPx(getCustomTabsHeight(context))
+ .setShareState(CustomTabsIntent.SHARE_STATE_OFF);
+ final Intent intent = builder.build().intent;
+ intent.setData(uri);
+ context.startActivity(intent);
+ }
+
+ private int getCustomTabsHeight(Activity context) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ WindowMetrics windowMetrics = context.getWindowManager().getCurrentWindowMetrics();
+ Insets insets = windowMetrics.getWindowInsets()
+ .getInsetsIgnoringVisibility(WindowInsets.Type.systemBars());
+ return windowMetrics.getBounds().height() - insets.top - insets.bottom;
+ } else {
+ DisplayMetrics displayMetrics = new DisplayMetrics();
+ context.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
+ return displayMetrics.heightPixels;
+ }
+ }
+
+}
diff --git a/Source/Immutable/Private/Immutable/Android/Java/ImmutableActivity.java b/Source/Immutable/Private/Immutable/Android/Java/ImmutableActivity.java
new file mode 100644
index 00000000..c81d0c4d
--- /dev/null
+++ b/Source/Immutable/Private/Immutable/Android/Java/ImmutableActivity.java
@@ -0,0 +1,127 @@
+package com.immutable.unreal;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.browser.customtabs.CustomTabsCallback;
+
+import com.epicgames.unreal.GameActivity;
+
+public class ImmutableActivity extends Activity {
+ private static final String EXTRA_URI = "extra_uri";
+ private static final String EXTRA_INTENT_LAUNCHED = "extra_intent_launched";
+
+ private boolean customTabsLaunched = false;
+ private CustomTabsController customTabsController;
+
+ public static void startActivity(Activity context, String url) {
+ Intent intent = new Intent(context, ImmutableActivity.class);
+ intent.putExtra(EXTRA_URI, Uri.parse(url));
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ context.startActivity(intent);
+ }
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState != null) {
+ customTabsLaunched = savedInstanceState.getBoolean(EXTRA_INTENT_LAUNCHED, false);
+ }
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ setIntent(intent);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ Intent resultData = resultCode == RESULT_CANCELED ? new Intent() : data;
+ onDeeplinkResult(resultData);
+ finish();
+ }
+
+ @Override
+ protected void onSaveInstanceState(@NonNull Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putBoolean(EXTRA_INTENT_LAUNCHED, customTabsLaunched);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ Intent authenticationIntent = getIntent();
+ if (!customTabsLaunched && authenticationIntent.getExtras() == null) {
+ // This activity was launched in an unexpected way
+ finish();
+ return;
+ } else if (!customTabsLaunched) {
+ // Haven't launched custom tabs
+ customTabsLaunched = true;
+ launchCustomTabs();
+ return;
+ }
+ onDeeplinkResult(authenticationIntent);
+ finish();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (customTabsController != null) {
+ customTabsController.unbindService();
+ customTabsController = null;
+ }
+ }
+
+ public native void handleDeepLink(String Deeplink);
+
+ public native void handleOnCustomTabsDismissed(String Url);
+
+ private void launchCustomTabs() {
+ Bundle extras = getIntent().getExtras();
+ final Uri uri = extras.getParcelable(EXTRA_URI);
+ customTabsController = new CustomTabsController(this, new CustomTabsCallback() {
+ @Override
+ public void onNavigationEvent(int navigationEvent, @Nullable Bundle extras) {
+ if (navigationEvent == CustomTabsCallback.TAB_HIDDEN/* && callbackInstance != null */) {
+ // Adding some delay before calling onCustomTabsDismissed as sometimes this gets called
+ // before the PKCE deeplink is triggered (by 100ms). This means PKCEResponseDelegate will be
+ // set to null before the SDK can use it to notify the consumer of the PKCE result.
+ // See UImmutablePassport::HandleOnPKCEDismissed and UImmutablePassport::OnDeepLinkActivated
+ final Handler handler = new Handler(Looper.getMainLooper());
+ handler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ GameActivity.onCustomTabsDismissed(uri.toString());
+ }
+ }, 1000);
+ }
+ }
+ });
+ customTabsController.bindService();
+ customTabsController.launch(uri);
+ }
+
+ private void onDeeplinkResult(@Nullable Intent intent) {
+ if (intent != null && intent.getData() != null) {
+ GameActivity.onDeeplinkResult(intent.getData().toString());
+ }
+ }
+
+ public interface Callback {
+ void onCustomTabsDismissed(String url);
+ void onDeeplinkResult(String url);
+ }
+}
+
diff --git a/Source/Immutable/Private/Immutable/Android/Java/ImmutableAndroid.java b/Source/Immutable/Private/Immutable/Android/Java/ImmutableAndroid.java
deleted file mode 100644
index 378648e6..00000000
--- a/Source/Immutable/Private/Immutable/Android/Java/ImmutableAndroid.java
+++ /dev/null
@@ -1,112 +0,0 @@
-package com.immutable.unreal;
-
-import android.app.Activity;
-import android.content.ComponentName;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.graphics.Insets;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
-import android.util.DisplayMetrics;
-import android.view.WindowInsets;
-import android.view.WindowMetrics;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.browser.customtabs.CustomTabsCallback;
-import androidx.browser.customtabs.CustomTabsClient;
-import androidx.browser.customtabs.CustomTabsIntent;
-import androidx.browser.customtabs.CustomTabsService;
-import androidx.browser.customtabs.CustomTabsServiceConnection;
-import androidx.browser.customtabs.CustomTabsSession;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class ImmutableAndroid {
- private static CustomTabsServiceConnection customTabsServiceConnection;
-
- private static int getCustomTabsHeight(Activity context) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- WindowMetrics windowMetrics = context.getWindowManager().getCurrentWindowMetrics();
- Insets insets = windowMetrics.getWindowInsets()
- .getInsetsIgnoringVisibility(WindowInsets.Type.systemBars());
- return windowMetrics.getBounds().height() - insets.top - insets.bottom;
- } else {
- DisplayMetrics displayMetrics = new DisplayMetrics();
- context.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
- return displayMetrics.heightPixels;
- }
- }
-
- public static void launchUrl(final Activity context, final String url) {
- // Get all apps that can support Custom Tabs Service
- // i.e. services that can handle ACTION_CUSTOM_TABS_CONNECTION intents
- PackageManager packageManager = context.getPackageManager();
- Intent serviceIntent = new Intent();
- serviceIntent.setAction(CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION);
- List resolvedList = packageManager.queryIntentServices(serviceIntent, 0);
- List packageNames = new ArrayList<>();
- for (ResolveInfo info : resolvedList) {
- if (info.serviceInfo != null) {
- packageNames.add(info.serviceInfo.packageName);
- }
- }
-
- if (packageNames.size() > 0) {
- // Get the preferred Custom Tabs package
- String customTabsPackageName = CustomTabsClient.getPackageName(context, packageNames);
- if (customTabsPackageName == null) {
- // Could not get the preferred Custom Tab browser, so launch URL in any browser
- context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
- } else {
- customTabsServiceConnection = new CustomTabsServiceConnection() {
- @Override
- public void onServiceDisconnected(ComponentName name) {
- customTabsServiceConnection = null;
- }
-
- @Override
- public void onCustomTabsServiceConnected(@NonNull ComponentName name, @NonNull CustomTabsClient client) {
- CustomTabsSession session = client.newSession(new CustomTabsCallback() {
- @Override
- public void onNavigationEvent(int navigationEvent, @Nullable Bundle extras) {
- if (context instanceof Callback && navigationEvent == CustomTabsCallback.TAB_HIDDEN) {
- // Adding some delay before calling onCustomTabsDismissed as sometimes this gets called
- // before the PKCE deeplink is triggered (by 100ms). This means PKCEResponseDelegate will be
- // set to null before the SDK can use it to notify the consumer of the PKCE result.
- // See UImmutablePassport::HandleOnPKCEDismissed and UImmutablePassport::OnDeepLinkActivated
- final Handler handler = new Handler(Looper.getMainLooper());
- handler.postDelayed(new Runnable() {
- @Override
- public void run() {
- ((Callback) context).onCustomTabsDismissed(url);
- }
- }, 1000);
- }
- }
- });
- // Need to set the session to get custom tabs to show as a bottom sheet
- CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder(session)
- .setInitialActivityHeightPx(getCustomTabsHeight(context))
- .setUrlBarHidingEnabled(true)
- .build();
- customTabsIntent.launchUrl(context, Uri.parse(url));
- }
- };
- CustomTabsClient.bindCustomTabsService(context, customTabsPackageName, customTabsServiceConnection);
- }
- } else {
- // Custom Tabs not supported by any browser on the device so launch URL in any browser
- context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
- }
- }
-
- public interface Callback {
- void onCustomTabsDismissed(String url);
- }
-}
\ No newline at end of file
diff --git a/Source/Immutable/Private/Immutable/Android/Java/RedirectActivity.java b/Source/Immutable/Private/Immutable/Android/Java/RedirectActivity.java
new file mode 100644
index 00000000..4e1c6ac3
--- /dev/null
+++ b/Source/Immutable/Private/Immutable/Android/Java/RedirectActivity.java
@@ -0,0 +1,23 @@
+package com.immutable.unreal;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+
+// Activity which handles deeplinks
+public class RedirectActivity extends Activity {
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Intent intent = new Intent(this, ImmutableActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ if (getIntent() != null) {
+ intent.setData(getIntent().getData());
+ }
+ startActivity(intent);
+ finish();
+ }
+}
+
diff --git a/Source/Immutable/Private/Immutable/ImmutablePassport.cpp b/Source/Immutable/Private/Immutable/ImmutablePassport.cpp
index 5959be6c..954a3ca0 100644
--- a/Source/Immutable/Private/Immutable/ImmutablePassport.cpp
+++ b/Source/Immutable/Private/Immutable/ImmutablePassport.cpp
@@ -981,7 +981,7 @@ void UImmutablePassport::HandleOnLoginPKCEDismissed()
IMTBL_LOG("Handle On Login PKCE Dismissed");
OnPKCEDismissed = nullptr;
- if (!completingPKCE)
+ if (!completingPKCE && !bIsLoggedIn)
{
// User hasn't entered all required details (e.g. email address) into
// Passport yet
@@ -1023,8 +1023,8 @@ void UImmutablePassport::LaunchAndroidUrl(FString Url)
if (JNIEnv *Env = FAndroidApplication::GetJavaEnv())
{
jstring jurl = Env->NewStringUTF(TCHAR_TO_UTF8(*Url));
- jclass jimmutableAndroidClass = FAndroidApplication::FindJavaClass("com/immutable/unreal/ImmutableAndroid");
- static jmethodID jlaunchUrl = FJavaWrapper::FindStaticMethod(Env, jimmutableAndroidClass, "launchUrl", "(Landroid/app/Activity;Ljava/lang/String;)V", false);
+ jclass jimmutableAndroidClass = FAndroidApplication::FindJavaClass("com/immutable/unreal/ImmutableActivity");
+ static jmethodID jlaunchUrl = FJavaWrapper::FindStaticMethod(Env, jimmutableAndroidClass, "startActivity", "(Landroid/app/Activity;Ljava/lang/String;)V", false);
CallJniStaticVoidMethod(Env, jimmutableAndroidClass, jlaunchUrl, FJavaWrapper::GameActivityThis, jurl);
}