Skip to content
Open
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
3 changes: 2 additions & 1 deletion gleap/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
<application>
<activity
android:name="io.gleap.GleapMainActivity"
android:configChanges="orientation|screenSize"
android:configChanges="orientation|screenSize|keyboard|keyboardHidden|screenLayout|smallestScreenSize|uiMode|navigation|density"
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The activity now claims to handle a very broad set of configChanges (including density and uiMode). When these are listed, Android will not recreate the Activity and will also not automatically re-apply updated resources for those configuration changes, which can cause incorrect theming/scaling after dark-mode changes or display-size/density changes. Consider narrowing this list to only the minimum needed to prevent the keyboard-related recreation (and leave other changes to the default recreation behavior).

Suggested change
android:configChanges="orientation|screenSize|keyboard|keyboardHidden|screenLayout|smallestScreenSize|uiMode|navigation|density"
android:configChanges="orientation|screenSize|keyboard|keyboardHidden"

Copilot uses AI. Check for mistakes.
android:windowSoftInputMode="adjustResize"
android:theme="@style/AppTheme.TranslucentBG" />
</application>

Expand Down
75 changes: 57 additions & 18 deletions gleap/src/main/java/io/gleap/GleapMainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
Expand Down Expand Up @@ -197,6 +198,11 @@ public void handleOnBackPressed() {

this.requestWindowFeature(Window.FEATURE_NO_TITLE);
try {
// SOFT_INPUT_ADJUST_RESIZE is deprecated on API 30+ but still honoured
// for apps that target SDK < 35 (this SDK targets 33). Setting it
// unconditionally ensures the window shrinks for the keyboard on every
// device – including Android 16 / Samsung One UI 8.0 – so the WebView
// content remains visible without needing a separate IME-insets handler.
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
if (getSupportActionBar() != null) {
getSupportActionBar().hide();
Expand All @@ -214,25 +220,24 @@ public void handleOnBackPressed() {

final FrameLayout webViewContainer = findViewById(R.id.webview_container);

// Handle system-bar insets (status bar, navigation bar) so the
// WebView content is not drawn behind them. We intentionally
// do NOT include IME insets here: the keyboard resize is fully
// handled by SOFT_INPUT_ADJUST_RESIZE which shrinks the window
// itself. Adding IME padding on top of that would double-shift
// the content -- the root cause of the blank-screen-on-keyboard
// bug on Android 16 / Samsung One UI 8.0.
ViewCompat.setOnApplyWindowInsetsListener(webViewContainer,
(view, insets) -> {

// status + navigation bars (stable system bars)
Insets bars = insets.getInsets(WindowInsetsCompat.Type.systemBars());

// on Android 11+ this is the real keyboard height;
// on older versions it’s 0 – but then the IME is reported as a system‑bar inset
Insets ime = insets.getInsets(WindowInsetsCompat.Type.ime());

int topInset = bars.top; // keep the status‑bar height
int bottomInset = Math.max(bars.bottom, ime.bottom); // choose the larger of nav‑bar or IME

view.setPadding(view.getPaddingLeft(),
topInset,
bars.top,
view.getPaddingRight(),
bottomInset);
bars.bottom);

return insets; // DON’T consume – let child views see the same insets
return insets;
});

int backgroundColor = Color.parseColor(GleapConfig.getInstance().getBackgroundColor());
Expand Down Expand Up @@ -285,9 +290,19 @@ public void invoke() {
@Override
public void run() {
url += GleapURLGenerator.generateURL();
initBrowser();
if (savedInstanceState != null) {
webView.restoreState(savedInstanceState);
// Activity was recreated (e.g. process killed in background).
// Set up WebView clients/settings but skip loading a new URL;
// restoreState will navigate back to the previous page.
initBrowserSettings();
android.webkit.WebBackForwardList restored = webView.restoreState(savedInstanceState);
if (restored == null) {
// restoreState failed — fall back to a full reload.
webView.loadUrl(url);
Comment on lines +299 to +301
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the savedInstanceState != null path you call initBrowserSettings() (which sets the WebView INVISIBLE) and then webView.restoreState(...), but if restoreState succeeds there is no native code path that hides R.id.loader / shows the WebView (the only hide/show is triggered by the JS "ping" message). If the restored page does not re-emit that ping, the UI can remain stuck in an infinite loader/blank state after process recreation. Consider explicitly showing the WebView / hiding the loader after a successful restore, or triggering a reload/timeout fallback when the ping isn’t received.

Suggested change
if (restored == null) {
// restoreState failed — fall back to a full reload.
webView.loadUrl(url);
if (restored == null || restored.getSize() == 0) {
// restoreState failed — fall back to a full reload.
webView.loadUrl(url);
} else {
// Successfully restored a previous page. Ensure that the
// WebView is visible and the loader is hidden, since the
// restored content might not re-emit the JS "ping".
webView.setVisibility(View.VISIBLE);
View loader = findViewById(R.id.loader);
if (loader != null) {
loader.setVisibility(View.GONE);
}

Copilot uses AI. Check for mistakes.
}
} else {
// Fresh start — initialise everything and load the URL.
initBrowser();
}
}
});
Expand All @@ -305,7 +320,20 @@ protected void onSaveInstanceState(Bundle outState) {
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
webView.restoreState(savedInstanceState);
// Do NOT call webView.restoreState() here. The posted runnable in
// onCreate already handles restoration after the WebView clients are
// wired up via initBrowserSettings(). Calling restoreState() here
// would consume the Bundle's WebView state before that runnable
// executes, forcing an unnecessary full URL reload every time.
}

@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// Intentionally empty — we declared the relevant configChanges in the
// manifest so the system delivers them here instead of recreating the
// activity. This keeps the WebView (and its chat state) alive when the
// soft keyboard appears/disappears or the screen layout changes.
}

@Override
Expand Down Expand Up @@ -359,7 +387,12 @@ protected void onDestroy() {
super.onDestroy();
}

private void initBrowser() {
/**
* Configure WebView settings, clients and JS bridge without loading a URL.
* Call this when restoring from saved state so the WebView is properly
* wired up before {@code restoreState()} navigates to the previous page.
*/
private void initBrowserSettings() {
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setDomStorageEnabled(true);
Expand Down Expand Up @@ -443,10 +476,16 @@ public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathC
}
}
});
webView.loadUrl(url);
webView.setVisibility(View.INVISIBLE);
settings.setUseWideViewPort(true);
settings.setLoadWithOverviewMode(true);
}

/**
* Full browser initialisation: configure settings, clients and load the URL.
* Used on a fresh start (no saved instance state).
*/
private void initBrowser() {
initBrowserSettings();
webView.loadUrl(url);
}

public void askForPermission(String origin, String androidPermission, String webkitPermission, int requestCode) {
Expand Down