Skip to content

Commit 776d7e4

Browse files
authored
Merge pull request #151 from immutable/fix/android-custom-tabs
[DX-2565] [DX-2564] fix: android chrome custom tabs
2 parents d6aede0 + cc0b9c2 commit 776d7e4

File tree

10 files changed

+333
-108
lines changed

10 files changed

+333
-108
lines changed

sample/Assets/Plugins/Android/AndroidManifest.xml

+12-2
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,23 @@
1717
<action android:name="android.intent.action.MAIN" />
1818
<category android:name="android.intent.category.LAUNCHER" />
1919
</intent-filter>
20-
<intent-filter>
20+
<meta-data android:name="unityplayer.UnityActivity" android:value="true" />
21+
</activity>
22+
<activity
23+
android:name="com.immutable.unity.ImmutableActivity"
24+
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboard|keyboardHidden"
25+
android:exported="false"
26+
android:launchMode="singleTask"
27+
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
28+
<activity
29+
android:name="com.immutable.unity.RedirectActivity"
30+
android:exported="true" >
31+
<intent-filter android:autoVerify="true">
2132
<action android:name="android.intent.action.VIEW" />
2233
<category android:name="android.intent.category.DEFAULT" />
2334
<category android:name="android.intent.category.BROWSABLE" />
2435
<data android:host="callback" android:scheme="imxsample" />
2536
</intent-filter>
26-
<meta-data android:name="unityplayer.UnityActivity" android:value="true" />
2737
</activity>
2838
</application>
2939
</manifest>

sample/Assets/Scripts/AuthenticatedScript.cs

+1
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ public async void GetAddress()
179179

180180
public async void Logout()
181181
{
182+
ShowOutput("Logging out...");
182183
#if UNITY_ANDROID || UNITY_IPHONE || (UNITY_STANDALONE_OSX && !UNITY_EDITOR_OSX)
183184
await passport.LogoutPKCE();
184185
#else

sample/ProjectSettings/ProjectSettings.asset

+1-1
Original file line numberDiff line numberDiff line change
@@ -792,7 +792,7 @@ PlayerSettings:
792792
allowUnsafeCode: 0
793793
useDeterministicCompilation: 1
794794
enableRoslynAnalyzers: 1
795-
selectedPlatform: 0
795+
selectedPlatform: 2
796796
additionalIl2CppArgs:
797797
scriptingRuntimeVersion: 1
798798
gcIncremental: 1

src/Packages/Passport/Runtime/Assets/ImmutableAndroid.androidlib/src/main/AndroidManifest.xml

+9-1
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,13 @@
44
<intent>
55
<action android:name="android.support.customtabs.action.CustomTabsService" />
66
</intent>
7-
</queries>
7+
</queries>
8+
<application>
9+
<activity
10+
android:name="com.immutable.unity.ImmutableActivity"
11+
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboard|keyboardHidden"
12+
android:exported="false"
13+
android:launchMode="singleTask"
14+
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
15+
</application>
816
</manifest>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package com.immutable.unity;
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package com.immutable.unity;
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+
10+
import androidx.annotation.NonNull;
11+
import androidx.annotation.Nullable;
12+
import androidx.browser.customtabs.CustomTabsCallback;
13+
14+
public class ImmutableActivity extends Activity {
15+
private static final String EXTRA_URI = "extra_uri";
16+
private static final String EXTRA_INTENT_LAUNCHED = "extra_intent_launched";
17+
18+
private static Callback callbackInstance;
19+
20+
private boolean customTabsLaunched = false;
21+
private CustomTabsController customTabsController;
22+
23+
public static void startActivity(Context context, String url, Callback callback) {
24+
callbackInstance = callback;
25+
26+
Intent intent = new Intent(context, ImmutableActivity.class);
27+
intent.putExtra(EXTRA_URI, Uri.parse(url));
28+
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
29+
context.startActivity(intent);
30+
}
31+
32+
@Override
33+
protected void onCreate(@Nullable Bundle savedInstanceState) {
34+
super.onCreate(savedInstanceState);
35+
if (savedInstanceState != null) {
36+
customTabsLaunched = savedInstanceState.getBoolean(EXTRA_INTENT_LAUNCHED, false);
37+
}
38+
}
39+
40+
@Override
41+
protected void onNewIntent(Intent intent) {
42+
super.onNewIntent(intent);
43+
setIntent(intent);
44+
}
45+
46+
@Override
47+
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
48+
Intent resultData = resultCode == RESULT_CANCELED ? new Intent() : data;
49+
onDeeplinkResult(resultData);
50+
finish();
51+
}
52+
53+
@Override
54+
protected void onSaveInstanceState(@NonNull Bundle outState) {
55+
super.onSaveInstanceState(outState);
56+
outState.putBoolean(EXTRA_INTENT_LAUNCHED, customTabsLaunched);
57+
}
58+
59+
@Override
60+
protected void onResume() {
61+
super.onResume();
62+
Intent authenticationIntent = getIntent();
63+
if (!customTabsLaunched && authenticationIntent.getExtras() == null) {
64+
// This activity was launched in an unexpected way
65+
finish();
66+
return;
67+
} else if (!customTabsLaunched) {
68+
// Haven't launched custom tabs
69+
customTabsLaunched = true;
70+
launchCustomTabs();
71+
return;
72+
}
73+
onDeeplinkResult(authenticationIntent);
74+
finish();
75+
}
76+
77+
@Override
78+
protected void onDestroy() {
79+
super.onDestroy();
80+
if (customTabsController != null) {
81+
customTabsController.unbindService();
82+
customTabsController = null;
83+
}
84+
}
85+
86+
private void launchCustomTabs() {
87+
Bundle extras = getIntent().getExtras();
88+
Uri uri;
89+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
90+
uri = extras.getParcelable(EXTRA_URI, Uri.class);
91+
} else {
92+
uri = extras.getParcelable(EXTRA_URI);
93+
}
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+
callbackInstance.onCustomTabsDismissed(uri.toString());
99+
}
100+
}
101+
});
102+
customTabsController.bindService();
103+
customTabsController.launch(uri);
104+
}
105+
106+
private void onDeeplinkResult(@Nullable Intent intent) {
107+
if (callbackInstance != null && intent != null && intent.getData() != null) {
108+
callbackInstance.onDeeplinkResult(intent.getData().toString());
109+
}
110+
}
111+
112+
public interface Callback {
113+
void onCustomTabsDismissed(String url);
114+
void onDeeplinkResult(String url);
115+
}
116+
}
117+

0 commit comments

Comments
 (0)