Skip to content

[DX-2601] fix: chrome custom tabs #59

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 24, 2024
Merged
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
53 changes: 19 additions & 34 deletions Source/Immutable/Immutable_UPL_Android.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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.** { *; }
Expand All @@ -22,44 +24,27 @@
<action android:name="android.support.customtabs.action.CustomTabsService" />
</intent>
</addElements>
<addElements tag="application">
<activity
android:name="com.immutable.unreal.ImmutableActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboard|keyboardHidden"
android:exported="false"
android:launchMode="singleTask"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
</addElements>
</androidManifestUpdates>
<gameActivityImportAdditions>
<insert>
import com.immutable.unreal.ImmutableAndroid;
</insert>
</gameActivityImportAdditions>
<gameActivityImplementsAdditions>
<insert>
ImmutableAndroid.Callback,
</insert>
</gameActivityImplementsAdditions>
<gameActivityOnCreateAdditions>
<insert>
Uri uri = getIntent().getData();
if (uri != null) {
String deeplink = uri.toString();
handleDeepLink(uri.toString());
}
</insert>
</gameActivityOnCreateAdditions>
<gameActivityOnResumeAdditions>
<insert>
Uri uri = getIntent().getData();
if (uri != null) {
String deeplink = uri.toString();
handleDeepLink(deeplink);
}
</insert>
</gameActivityOnResumeAdditions>
<gameActivityClassAdditions>
<insert>
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);
}
</insert>
</gameActivityClassAdditions>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Activity> context;
private final AtomicReference<CustomTabsSession> 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<ResolveInfo> resolvedList = packageManager.queryIntentServices(serviceIntent, 0);
List<String> 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;
}
}

}
127 changes: 127 additions & 0 deletions Source/Immutable/Private/Immutable/Android/Java/ImmutableActivity.java
Original file line number Diff line number Diff line change
@@ -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);
}
}

Loading