Skip to content

Commit 20c84e2

Browse files
authored
Support both non-edge-to-edge and edge-to-edge #377
ref DEV-3001
2 parents 3e25b24 + 719997d commit 20c84e2

File tree

8 files changed

+306
-12
lines changed

8 files changed

+306
-12
lines changed

example/capacitor/src/pages/Home.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,10 @@ function AuthgearDemo() {
252252
modalPresentationStyle: "fullScreen",
253253
isInspectable: true,
254254
},
255+
android: {
256+
actionBarBackgroundColor: 0x00ffffff,
257+
actionBarButtonTintColor: 0xff000000,
258+
},
255259
})
256260
: undefined,
257261
isSSOEnabled,

example/reactnative/src/screens/MainScreen.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,8 @@ const HomeScreen: React.FC = () => {
387387
},
388388
android: {
389389
wechatRedirectURI,
390+
actionBarBackgroundColor: 0x00ffffff,
391+
actionBarButtonTintColor: 0xff000000,
390392
},
391393
sendWechatAuthRequest,
392394
})

packages/authgear-capacitor/android/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ dependencies {
5252
implementation fileTree(dir: 'libs', include: ['*.jar'])
5353
implementation project(':capacitor-android')
5454
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
55+
implementation 'androidx.core:core:1.12.0'
5556
implementation 'androidx.browser:browser:1.2.0'
5657
implementation "androidx.biometric:biometric:1.2.0-alpha05"
5758
// NOTE(backup): Please search NOTE(backup) before you update security-crypto or tink-android.

packages/authgear-capacitor/android/src/main/java/com/authgear/capacitor/WebKitWebViewActivity.java

Lines changed: 148 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,41 @@
77
import android.app.Activity;
88
import android.content.Context;
99
import android.content.Intent;
10+
import android.graphics.Bitmap;
1011
import android.graphics.drawable.ColorDrawable;
1112
import android.graphics.drawable.Drawable;
1213
import android.net.Uri;
1314
import android.os.Build;
1415
import android.os.Bundle;
1516
import android.os.Message;
17+
import android.util.DisplayMetrics;
18+
import android.util.TypedValue;
1619
import android.view.Menu;
1720
import android.view.MenuItem;
21+
import android.view.View;
22+
import android.view.ViewGroup;
1823
import android.webkit.ValueCallback;
1924
import android.webkit.WebChromeClient;
2025
import android.webkit.WebResourceRequest;
2126
import android.webkit.WebSettings;
2227
import android.webkit.WebView;
2328
import android.webkit.WebViewClient;
29+
import android.widget.FrameLayout;
2430

2531
import androidx.annotation.ColorInt;
2632
import androidx.annotation.DrawableRes;
2733
import androidx.annotation.NonNull;
2834
import androidx.annotation.Nullable;
2935
import androidx.appcompat.app.ActionBar;
3036
import androidx.appcompat.app.AppCompatActivity;
37+
import androidx.appcompat.widget.Toolbar;
3138
import androidx.core.content.res.ResourcesCompat;
39+
import androidx.core.graphics.Insets;
3240
import androidx.core.graphics.drawable.DrawableCompat;
41+
import androidx.core.util.TypedValueCompat;
42+
import androidx.core.view.OnApplyWindowInsetsListener;
43+
import androidx.core.view.ViewCompat;
44+
import androidx.core.view.WindowInsetsCompat;
3345

3446
public class WebKitWebViewActivity extends AppCompatActivity {
3547

@@ -38,7 +50,11 @@ public class WebKitWebViewActivity extends AppCompatActivity {
3850
private static final int MENU_ID_CANCEL = 1;
3951
private static final int TAG_FILE_CHOOSER = 1;
4052

53+
private FrameLayout mRootFrameLayout;
54+
private Toolbar mToolbar;
55+
private FrameLayout mToolbarFrameLayout;
4156
private WebView mWebView;
57+
private Insets mLastSeenInsets;
4258
private Uri result;
4359
private StartActivityHandles<ValueCallback<Uri[]>> handles = new StartActivityHandles<>();
4460

@@ -85,6 +101,19 @@ private MyWebViewClient(WebKitWebViewActivity activity) {
85101
this.activity = activity;
86102
}
87103

104+
@Override
105+
public void onPageStarted(WebView view, String url, Bitmap favicon) {
106+
super.onPageStarted(view, url, favicon);
107+
// onPageStarted is not always called, but when it is called, it is called before
108+
// onPageFinished.
109+
// Therefore, we put the edge-to-edge handling here hoping that
110+
// the safe area insets can be set as soon as possible.
111+
112+
view.evaluateJavascript(USERSCRIPT_USER_SELECT_NONE, null);
113+
this.activity.handleNonEdgeToEdge();
114+
this.activity.handleEdgeToEdge();
115+
}
116+
88117
@Override
89118
public void onPageFinished(WebView view, String url) {
90119
super.onPageFinished(view, url);
@@ -96,6 +125,8 @@ public void onPageFinished(WebView view, String url) {
96125
// The caveat is that the script is run in the main frame only.
97126
// But we do not actually use iframes so it does not matter.
98127
view.evaluateJavascript(USERSCRIPT_USER_SELECT_NONE, null);
128+
this.activity.handleNonEdgeToEdge();
129+
this.activity.handleEdgeToEdge();
99130
}
100131

101132
@TargetApi(Build.VERSION_CODES.N)
@@ -199,18 +230,85 @@ public static Intent createIntent(Context ctx, Options options) {
199230
return intent;
200231
}
201232

233+
private float getActionBarSizeInDp() {
234+
float actionBarSizeInDp = 44f;
235+
TypedValue tv = new TypedValue();
236+
if (this.getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true)) {
237+
int actionBarSizeInPx = TypedValue.complexToDimensionPixelSize(tv.data, this.getResources().getDisplayMetrics());
238+
actionBarSizeInDp = TypedValueCompat.pxToDp((float) actionBarSizeInPx, this.getResources().getDisplayMetrics());
239+
}
240+
return actionBarSizeInDp;
241+
}
242+
243+
private void applyInsetsToWebView(Insets safeAreaInsets) {
244+
float actionBarSizeInDp = this.getActionBarSizeInDp();
245+
DisplayMetrics displayMetrics = this.getResources().getDisplayMetrics();
246+
float actionBarSizeInPx = TypedValueCompat.dpToPx(actionBarSizeInDp, displayMetrics);
247+
float top = TypedValueCompat.pxToDp((float) safeAreaInsets.top + actionBarSizeInPx, displayMetrics);
248+
float right = TypedValueCompat.pxToDp((float) safeAreaInsets.right, displayMetrics);
249+
float bottom = TypedValueCompat.pxToDp((float) safeAreaInsets.bottom, displayMetrics);
250+
float left = TypedValueCompat.pxToDp((float) safeAreaInsets.left, displayMetrics);
251+
252+
String safeAreaJs =
253+
"document.documentElement.style.setProperty('--safe-area-inset-top', '" + top + "px');\n" +
254+
"document.documentElement.style.setProperty('--safe-area-inset-right', '" + right + "px');\n" +
255+
"document.documentElement.style.setProperty('--safe-area-inset-bottom', '" + bottom + "px');\n" +
256+
"document.documentElement.style.setProperty('--safe-area-inset-left', '" + left + "px');";
257+
258+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
259+
this.mWebView.evaluateJavascript(safeAreaJs, null);
260+
}
261+
}
262+
263+
private void handleNonEdgeToEdge() {
264+
// In non edge-to-edge, the insets listener is not called.
265+
// So we have to apply the insets here.
266+
Insets insets = this.mLastSeenInsets == null ? Insets.NONE : this.mLastSeenInsets;
267+
this.applyInsetsToWebView(insets);
268+
}
269+
270+
private void handleEdgeToEdge() {
271+
// In edge-to-edge, we ask the system to invoke the insets listener.
272+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
273+
this.mRootFrameLayout.requestApplyInsets();
274+
}
275+
}
276+
202277
@Override
203278
protected void onCreate(@Nullable Bundle savedInstanceState) {
204279
super.onCreate(savedInstanceState);
205280

281+
this.mRootFrameLayout = new FrameLayout(this);
282+
this.mRootFrameLayout.setLayoutParams(new ViewGroup.LayoutParams(
283+
ViewGroup.LayoutParams.MATCH_PARENT,
284+
ViewGroup.LayoutParams.MATCH_PARENT
285+
));
286+
287+
this.mToolbarFrameLayout = new FrameLayout(this);
288+
this.mToolbarFrameLayout.setLayoutParams(new ViewGroup.LayoutParams(
289+
ViewGroup.LayoutParams.MATCH_PARENT,
290+
ViewGroup.LayoutParams.WRAP_CONTENT
291+
));
292+
293+
float actionBarSizeInDp = this.getActionBarSizeInDp();
294+
295+
this.mToolbar = new Toolbar(this);
296+
this.mToolbar.setLayoutParams(new FrameLayout.LayoutParams(
297+
ViewGroup.LayoutParams.MATCH_PARENT,
298+
(int) TypedValueCompat.dpToPx(actionBarSizeInDp, this.getResources().getDisplayMetrics())
299+
));
300+
this.setSupportActionBar(this.mToolbar);
301+
206302
Options options = this.getOptions();
207303

208304
// Do not show title.
209305
getSupportActionBar().setDisplayShowTitleEnabled(false);
210306

211307
// Configure navigation bar background color.
212308
if (options.actionBarBackgroundColor != null) {
213-
getSupportActionBar().setBackgroundDrawable(new ColorDrawable(options.actionBarBackgroundColor));
309+
ColorDrawable colorDrawable = new ColorDrawable(options.actionBarBackgroundColor);
310+
getSupportActionBar().setBackgroundDrawable(colorDrawable);
311+
this.mToolbarFrameLayout.setBackgroundDrawable(colorDrawable);
214312
}
215313

216314
// Show back button.
@@ -225,17 +323,62 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
225323

226324
// Configure web view.
227325
this.mWebView = new WebView(this);
326+
this.mWebView.setLayoutParams(new FrameLayout.LayoutParams(
327+
ViewGroup.LayoutParams.MATCH_PARENT,
328+
ViewGroup.LayoutParams.MATCH_PARENT
329+
));
228330
this.mWebView.getSettings().setSupportMultipleWindows(true);
229331
this.mWebView.getSettings().setDomStorageEnabled(true);
230-
this.setContentView(this.mWebView);
231332
this.mWebView.setWebViewClient(new MyWebViewClient(this));
232333
this.mWebView.setWebChromeClient(new MyWebChromeClient(this));
233334
WebSettings webSettings = this.mWebView.getSettings();
234335
webSettings.setJavaScriptEnabled(true);
235336

236-
if (savedInstanceState == null) {
237-
this.mWebView.loadUrl(options.url.toString());
238-
}
337+
this.mRootFrameLayout.addView(this.mWebView);
338+
this.mRootFrameLayout.addView(this.mToolbarFrameLayout);
339+
this.mToolbarFrameLayout.addView(this.mToolbar);
340+
this.setContentView(this.mRootFrameLayout);
341+
342+
ViewCompat.setOnApplyWindowInsetsListener(this.mRootFrameLayout, new OnApplyWindowInsetsListener() {
343+
344+
@Override
345+
public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) {
346+
Insets safeAreaInsets = insets.getInsets(
347+
WindowInsetsCompat.Type.systemBars() |
348+
WindowInsetsCompat.Type.displayCutout() |
349+
WindowInsetsCompat.Type.ime()
350+
);
351+
352+
WebKitWebViewActivity.this.mLastSeenInsets = safeAreaInsets;
353+
354+
ViewGroup.MarginLayoutParams toolbarParams = (ViewGroup.MarginLayoutParams) mToolbar.getLayoutParams();
355+
toolbarParams.setMargins(
356+
safeAreaInsets.left,
357+
safeAreaInsets.top,
358+
safeAreaInsets.right,
359+
0
360+
);
361+
362+
WebKitWebViewActivity.this.applyInsetsToWebView(safeAreaInsets);
363+
364+
return WindowInsetsCompat.CONSUMED;
365+
}
366+
});
367+
368+
this.mRootFrameLayout.post(new Runnable() {
369+
@Override
370+
public void run() {
371+
// We want the content view to draw at least once before loading the URL.
372+
//
373+
// In non edge-to-edge, the insets listener is never called so mLastSeenInsets is null.
374+
//
375+
// In edge-to-edge, the insets listener will be called at least once in the first draw,
376+
// so by the time onPageStart / onPageFinished is called, mLastSeenInsets is not null.
377+
if (savedInstanceState == null) {
378+
WebKitWebViewActivity.this.mWebView.loadUrl(options.url.toString());
379+
}
380+
}
381+
});
239382
}
240383

241384
@Override
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
<resources>
2-
<style name="AuthgearTheme" parent="Theme.AppCompat.DayNight.DarkActionBar">
2+
<style name="AuthgearTheme" parent="Theme.AppCompat.Light.NoActionBar">
33
</style>
44
</resources>

packages/authgear-react-native/android/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ dependencies {
121121
//noinspection GradleDynamicVersion
122122
implementation 'com.facebook.react:react-native:+' // From node_modules
123123

124+
implementation 'androidx.core:core:1.12.0'
124125
implementation 'androidx.browser:browser:1.2.0'
125126
implementation "androidx.biometric:biometric:1.2.0-alpha03"
126127
// NOTE(backup): Please search NOTE(backup) before you update security-crypto or tink-android.

0 commit comments

Comments
 (0)