Skip to content

Commit 07469cc

Browse files
authored
fix: chrome custom tabs (#59)
1 parent 136e73f commit 07469cc

File tree

6 files changed

+325
-149
lines changed

6 files changed

+325
-149
lines changed

Source/Immutable/Immutable_UPL_Android.xml

Lines changed: 19 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
-dontwarn com.immutable.unreal
1010
-keep class com.immutable.unreal.** { *; }
1111
-keep interface com.immutable.unreal.** { *; }
12-
-keep public class com.immutable.unreal.ImmutableAndroid.** { public protected *; }
12+
-keep public class com.immutable.unreal.ImmutableActivity { public protected *; }
13+
-keep public class com.immutable.unreal.CustomTabsController { public protected *; }
14+
-keep public class com.immutable.unreal.RedirectActivity { public protected *; }
1315

1416
-dontwarn androidx.**
1517
-keep class androidx.** { *; }
@@ -22,44 +24,27 @@
2224
<action android:name="android.support.customtabs.action.CustomTabsService" />
2325
</intent>
2426
</addElements>
27+
<addElements tag="application">
28+
<activity
29+
android:name="com.immutable.unreal.ImmutableActivity"
30+
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboard|keyboardHidden"
31+
android:exported="false"
32+
android:launchMode="singleTask"
33+
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
34+
</addElements>
2535
</androidManifestUpdates>
26-
<gameActivityImportAdditions>
27-
<insert>
28-
import com.immutable.unreal.ImmutableAndroid;
29-
</insert>
30-
</gameActivityImportAdditions>
31-
<gameActivityImplementsAdditions>
32-
<insert>
33-
ImmutableAndroid.Callback,
34-
</insert>
35-
</gameActivityImplementsAdditions>
36-
<gameActivityOnCreateAdditions>
37-
<insert>
38-
Uri uri = getIntent().getData();
39-
if (uri != null) {
40-
String deeplink = uri.toString();
41-
handleDeepLink(uri.toString());
42-
}
43-
</insert>
44-
</gameActivityOnCreateAdditions>
45-
<gameActivityOnResumeAdditions>
46-
<insert>
47-
Uri uri = getIntent().getData();
48-
if (uri != null) {
49-
String deeplink = uri.toString();
50-
handleDeepLink(deeplink);
51-
}
52-
</insert>
53-
</gameActivityOnResumeAdditions>
5436
<gameActivityClassAdditions>
5537
<insert>
56-
public native void handleDeepLink(String Deeplink);
38+
public static native void handleDeepLink(String Deeplink);
5739

58-
public native void handleOnCustomTabsDismissed(String Url);
40+
public static native void handleOnCustomTabsDismissed(String Url);
5941

60-
@Override
61-
public void onCustomTabsDismissed(String Url) {
62-
handleOnCustomTabsDismissed(Url);
42+
public static void onDeeplinkResult(String url) {
43+
handleDeepLink(url);
44+
}
45+
46+
public static void onCustomTabsDismissed(String url) {
47+
handleOnCustomTabsDismissed(url);
6348
}
6449
</insert>
6550
</gameActivityClassAdditions>
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package com.immutable.unreal;
2+
3+
import android.app.Activity;
4+
import android.content.ActivityNotFoundException;
5+
import android.content.ComponentName;
6+
import android.content.Context;
7+
import android.content.Intent;
8+
import android.content.pm.PackageManager;
9+
import android.content.pm.ResolveInfo;
10+
import android.graphics.Insets;
11+
import android.net.Uri;
12+
import android.os.Build;
13+
import android.util.DisplayMetrics;
14+
import android.view.WindowInsets;
15+
import android.view.WindowMetrics;
16+
17+
import androidx.annotation.NonNull;
18+
import androidx.browser.customtabs.CustomTabsCallback;
19+
import androidx.browser.customtabs.CustomTabsClient;
20+
import androidx.browser.customtabs.CustomTabsIntent;
21+
import androidx.browser.customtabs.CustomTabsService;
22+
import androidx.browser.customtabs.CustomTabsServiceConnection;
23+
import androidx.browser.customtabs.CustomTabsSession;
24+
25+
import java.lang.ref.WeakReference;
26+
import java.util.ArrayList;
27+
import java.util.List;
28+
import java.util.concurrent.CountDownLatch;
29+
import java.util.concurrent.TimeUnit;
30+
import java.util.concurrent.atomic.AtomicReference;
31+
32+
public class CustomTabsController extends CustomTabsServiceConnection {
33+
private static final long MAX_WAIT_TIME_SECONDS = 1;
34+
35+
private final WeakReference<Activity> context;
36+
private final AtomicReference<CustomTabsSession> session;
37+
private final CountDownLatch sessionLatch;
38+
private final String preferredPackage;
39+
private final CustomTabsCallback callback;
40+
41+
private boolean didTryToBindService;
42+
43+
public CustomTabsController(@NonNull Activity context, CustomTabsCallback callback) {
44+
this.context = new WeakReference<>(context);
45+
this.session = new AtomicReference<>();
46+
this.sessionLatch = new CountDownLatch(1);
47+
this.callback = callback;
48+
this.preferredPackage = getPreferredCustomTabsPackage(context);
49+
}
50+
51+
// Get all apps that can support Custom Tabs Service
52+
// i.e. services that can handle ACTION_CUSTOM_TABS_CONNECTION intents
53+
private String getPreferredCustomTabsPackage(@NonNull Activity context) {
54+
PackageManager packageManager = context.getPackageManager();
55+
Intent serviceIntent = new Intent();
56+
serviceIntent.setAction(CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION);
57+
List<ResolveInfo> resolvedList = packageManager.queryIntentServices(serviceIntent, 0);
58+
List<String> packageNames = new ArrayList<>();
59+
for (ResolveInfo info : resolvedList) {
60+
if (info.serviceInfo != null) {
61+
packageNames.add(info.serviceInfo.packageName);
62+
}
63+
}
64+
if (packageNames.size() > 0) {
65+
// Get the preferred Custom Tabs package
66+
return CustomTabsClient.getPackageName(context, packageNames);
67+
} else {
68+
return null;
69+
}
70+
}
71+
72+
@Override
73+
public void onCustomTabsServiceConnected(@NonNull ComponentName componentName, @NonNull CustomTabsClient client) {
74+
client.warmup(0L);
75+
CustomTabsSession customTabsSession = client.newSession(callback);
76+
session.set(customTabsSession);
77+
sessionLatch.countDown();
78+
}
79+
80+
@Override
81+
public void onServiceDisconnected(ComponentName componentName) {
82+
session.set(null);
83+
}
84+
85+
public void bindService() {
86+
Context context = this.context.get();
87+
didTryToBindService = false;
88+
if (context != null && preferredPackage != null) {
89+
didTryToBindService = true;
90+
CustomTabsClient.bindCustomTabsService(context, preferredPackage, this);
91+
}
92+
}
93+
94+
public void unbindService() {
95+
Context context = this.context.get();
96+
if (didTryToBindService && context != null) {
97+
context.unbindService(this);
98+
didTryToBindService = false;
99+
}
100+
}
101+
102+
public void launch(@NonNull final Uri uri) {
103+
final Activity context = this.context.get();
104+
if (context == null) {
105+
// Custom tabs context is no longer valid
106+
return;
107+
}
108+
109+
if (preferredPackage == null) {
110+
// Could not get the preferred Custom Tab browser, so launch URL in any browser
111+
context.startActivity(new Intent(Intent.ACTION_VIEW, uri));
112+
} else {
113+
// Running in a different thread to prevent doing too much work on main thread
114+
new Thread(() -> {
115+
try {
116+
launchCustomTabs(context, uri);
117+
} catch (ActivityNotFoundException ex) {
118+
// Failed to launch Custom Tab browser, so launch in browser
119+
context.startActivity(new Intent(Intent.ACTION_VIEW, uri));
120+
}
121+
}).start();
122+
}
123+
}
124+
125+
private void launchCustomTabs(Activity context, Uri uri) {
126+
bindService();
127+
try {
128+
boolean ignored = sessionLatch.await(MAX_WAIT_TIME_SECONDS, TimeUnit.SECONDS);
129+
} catch (InterruptedException ignored) {
130+
}
131+
132+
final CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder(session.get())
133+
.setInitialActivityHeightPx(getCustomTabsHeight(context))
134+
.setShareState(CustomTabsIntent.SHARE_STATE_OFF);
135+
final Intent intent = builder.build().intent;
136+
intent.setData(uri);
137+
context.startActivity(intent);
138+
}
139+
140+
private int getCustomTabsHeight(Activity context) {
141+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
142+
WindowMetrics windowMetrics = context.getWindowManager().getCurrentWindowMetrics();
143+
Insets insets = windowMetrics.getWindowInsets()
144+
.getInsetsIgnoringVisibility(WindowInsets.Type.systemBars());
145+
return windowMetrics.getBounds().height() - insets.top - insets.bottom;
146+
} else {
147+
DisplayMetrics displayMetrics = new DisplayMetrics();
148+
context.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
149+
return displayMetrics.heightPixels;
150+
}
151+
}
152+
153+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package com.immutable.unreal;
2+
3+
import android.app.Activity;
4+
import android.content.Context;
5+
import android.content.Intent;
6+
import android.net.Uri;
7+
import android.os.Build;
8+
import android.os.Bundle;
9+
import android.os.Handler;
10+
import android.os.Looper;
11+
import android.util.Log;
12+
13+
import androidx.annotation.NonNull;
14+
import androidx.annotation.Nullable;
15+
import androidx.browser.customtabs.CustomTabsCallback;
16+
17+
import com.epicgames.unreal.GameActivity;
18+
19+
public class ImmutableActivity extends Activity {
20+
private static final String EXTRA_URI = "extra_uri";
21+
private static final String EXTRA_INTENT_LAUNCHED = "extra_intent_launched";
22+
23+
private boolean customTabsLaunched = false;
24+
private CustomTabsController customTabsController;
25+
26+
public static void startActivity(Activity context, String url) {
27+
Intent intent = new Intent(context, ImmutableActivity.class);
28+
intent.putExtra(EXTRA_URI, Uri.parse(url));
29+
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
30+
context.startActivity(intent);
31+
}
32+
33+
@Override
34+
protected void onCreate(@Nullable Bundle savedInstanceState) {
35+
super.onCreate(savedInstanceState);
36+
if (savedInstanceState != null) {
37+
customTabsLaunched = savedInstanceState.getBoolean(EXTRA_INTENT_LAUNCHED, false);
38+
}
39+
}
40+
41+
@Override
42+
protected void onNewIntent(Intent intent) {
43+
super.onNewIntent(intent);
44+
setIntent(intent);
45+
}
46+
47+
@Override
48+
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
49+
Intent resultData = resultCode == RESULT_CANCELED ? new Intent() : data;
50+
onDeeplinkResult(resultData);
51+
finish();
52+
}
53+
54+
@Override
55+
protected void onSaveInstanceState(@NonNull Bundle outState) {
56+
super.onSaveInstanceState(outState);
57+
outState.putBoolean(EXTRA_INTENT_LAUNCHED, customTabsLaunched);
58+
}
59+
60+
@Override
61+
protected void onResume() {
62+
super.onResume();
63+
Intent authenticationIntent = getIntent();
64+
if (!customTabsLaunched && authenticationIntent.getExtras() == null) {
65+
// This activity was launched in an unexpected way
66+
finish();
67+
return;
68+
} else if (!customTabsLaunched) {
69+
// Haven't launched custom tabs
70+
customTabsLaunched = true;
71+
launchCustomTabs();
72+
return;
73+
}
74+
onDeeplinkResult(authenticationIntent);
75+
finish();
76+
}
77+
78+
@Override
79+
protected void onDestroy() {
80+
super.onDestroy();
81+
if (customTabsController != null) {
82+
customTabsController.unbindService();
83+
customTabsController = null;
84+
}
85+
}
86+
87+
public native void handleDeepLink(String Deeplink);
88+
89+
public native void handleOnCustomTabsDismissed(String Url);
90+
91+
private void launchCustomTabs() {
92+
Bundle extras = getIntent().getExtras();
93+
final Uri uri = extras.getParcelable(EXTRA_URI);
94+
customTabsController = new CustomTabsController(this, new CustomTabsCallback() {
95+
@Override
96+
public void onNavigationEvent(int navigationEvent, @Nullable Bundle extras) {
97+
if (navigationEvent == CustomTabsCallback.TAB_HIDDEN/* && callbackInstance != null */) {
98+
// Adding some delay before calling onCustomTabsDismissed as sometimes this gets called
99+
// before the PKCE deeplink is triggered (by 100ms). This means PKCEResponseDelegate will be
100+
// set to null before the SDK can use it to notify the consumer of the PKCE result.
101+
// See UImmutablePassport::HandleOnPKCEDismissed and UImmutablePassport::OnDeepLinkActivated
102+
final Handler handler = new Handler(Looper.getMainLooper());
103+
handler.postDelayed(new Runnable() {
104+
@Override
105+
public void run() {
106+
GameActivity.onCustomTabsDismissed(uri.toString());
107+
}
108+
}, 1000);
109+
}
110+
}
111+
});
112+
customTabsController.bindService();
113+
customTabsController.launch(uri);
114+
}
115+
116+
private void onDeeplinkResult(@Nullable Intent intent) {
117+
if (intent != null && intent.getData() != null) {
118+
GameActivity.onDeeplinkResult(intent.getData().toString());
119+
}
120+
}
121+
122+
public interface Callback {
123+
void onCustomTabsDismissed(String url);
124+
void onDeeplinkResult(String url);
125+
}
126+
}
127+

0 commit comments

Comments
 (0)