mirror of
https://github.com/tiennm99/loto.git
synced 2026-10-15 08:17:07 +00:00
feat(android): close behaviour gaps between the APK and the browser
Several behaviours diverged between the web build and the Capacitor wrapper, none of them reproducible on a desktop browser. - Declare VIBRATE. PlayerBoard calls navigator.vibrate() on every cell tap; a WebView app must hold the permission itself, where a browser holds it on the page's behalf. Taps were silent in the APK. - Hold a screen wake lock while numbers remain to be called. Auto-call advances on a timer with no touch input, so a round could run 15 minutes untouched — long enough for the display to sleep and the WebView to throttle the interval. Android drops the lock whenever the page hides, so wake-lock.js re-acquires on visibilitychange. - Handle back through OnBackPressedCallback. Android 16 (targetSdk 36) no longer calls onBackPressed() nor dispatches KEYCODE_BACK. Each open overlay pushes one history entry, so "the WebView can go back" means "an overlay is open": back closes the bingo modal or settings sheet, and only at the root does it confirm before quitting. Browsers get the overlay behaviour too. - Inset content by env(safe-area-inset-*) with viewport-fit=cover. targetSdk 36 forces edge-to-edge, so the WebView painted under the status bar and gesture nav. Also fixes the iOS PWA notch. - Pin the WebView's textZoom to 100 and add a "Cỡ chữ bảng" setting. The player card is a fixed 9-column grid that clips at large system font scales. Pinning the zoom overrides an accessibility control, so the app now owns an equivalent one; the two belong together. - Point the volume rocker at the media stream, so it works before the first clip plays. strings.xml also gains the exit-dialog strings and picks up the diacritics in app_name.
This commit is contained in:
1 parent
c6c839fd95
commit
3bb6b3d631
15 files changed
+790
-13
No files matched your search
@@ -3,6 +3,7 @@
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
@@ -38,6 +39,12 @@
|
||||
<!-- No INTERNET permission: app is fully offline. All assets bundled in
|
||||
APK; WebView serves from https://localhost which uses loopback. -->
|
||||
|
||||
<!-- Haptic feedback on cell taps. A WebView app must declare this itself
|
||||
for navigator.vibrate() to work — Chrome holds the permission on the
|
||||
browser's behalf, which is why the web build needs no equivalent.
|
||||
Normal permission: no runtime prompt, no data-safety impact. -->
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<!-- Mark hardware as optional so emulators (BlueStacks, NoxPlayer, AVDs)
|
||||
don't get filtered out. BlueStacks reports `faketouch` instead of
|
||||
`touchscreen` and lacks real telephony/GPS hardware. -->
|
||||
|
||||
@@ -1,5 +1,74 @@
|
||||
package com.miti99.loto;
|
||||
|
||||
import android.media.AudioManager;
|
||||
import android.os.Bundle;
|
||||
import android.webkit.WebView;
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import com.getcapacitor.Bridge;
|
||||
import com.getcapacitor.BridgeActivity;
|
||||
|
||||
public class MainActivity extends BridgeActivity {}
|
||||
public class MainActivity extends BridgeActivity {
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
WebView webView = webView();
|
||||
if (webView != null) {
|
||||
// The player card is a fixed 9-column grid, so the system font
|
||||
// scale (users routinely run 130-200%) clips two-digit numbers
|
||||
// out of their cells. Pinning the zoom is only defensible
|
||||
// because the app ships its own size control in Settings
|
||||
// ("Cỡ chữ bảng") as the accessible replacement.
|
||||
webView.getSettings().setTextZoom(100);
|
||||
}
|
||||
|
||||
// Calling numbers aloud is the app's whole job, so the volume rocker
|
||||
// should reach the media stream even before the first clip plays.
|
||||
setVolumeControlStream(AudioManager.STREAM_MUSIC);
|
||||
|
||||
// Android 16 (targetSdk 36) no longer calls onBackPressed() and no
|
||||
// longer dispatches KEYCODE_BACK, so back has to come through the
|
||||
// dispatcher. Each open overlay pushes one history entry (see
|
||||
// web/src/lib/overlay-history.js), which makes "the WebView can go
|
||||
// back" mean exactly "an overlay is open".
|
||||
getOnBackPressedDispatcher()
|
||||
.addCallback(
|
||||
this,
|
||||
new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
WebView wv = webView();
|
||||
if (wv != null && wv.canGoBack()) {
|
||||
wv.goBack();
|
||||
return;
|
||||
}
|
||||
confirmExit();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** Null-safe accessor — the bridge is built during super.onCreate(). */
|
||||
private WebView webView() {
|
||||
Bridge bridge = getBridge();
|
||||
return bridge == null ? null : bridge.getWebView();
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-call runs untouched for minutes at a time, so a stray back press
|
||||
* at the root would otherwise drop the caller out of a live round.
|
||||
*/
|
||||
private void confirmExit() {
|
||||
// Explicit AppCompat dialog theme: the activity runs on
|
||||
// AppTheme.NoActionBarLaunch (parent Theme.SplashScreen), which is
|
||||
// not guaranteed to be AppCompat-derived at dialog time.
|
||||
new AlertDialog.Builder(this, R.style.AppTheme_ExitDialog)
|
||||
.setTitle(R.string.exit_title)
|
||||
.setMessage(R.string.exit_message)
|
||||
.setNegativeButton(R.string.exit_cancel, null)
|
||||
.setPositiveButton(R.string.exit_confirm, (dialog, which) -> finish())
|
||||
.show();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,13 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<resources>
|
||||
<string name="app_name">Lo To</string>
|
||||
<string name="title_activity_main">Lo To</string>
|
||||
<string name="app_name">Lô tô</string>
|
||||
<string name="title_activity_main">Lô tô</string>
|
||||
<string name="package_name">com.miti99.loto</string>
|
||||
<string name="custom_url_scheme">com.miti99.loto</string>
|
||||
|
||||
<!-- Exit confirmation, shown when back is pressed with no overlay open. -->
|
||||
<string name="exit_title">Thoát Lô tô?</string>
|
||||
<string name="exit_message">Ván đang chơi sẽ được lưu lại.</string>
|
||||
<string name="exit_confirm">Thoát</string>
|
||||
<string name="exit_cancel">Ở lại</string>
|
||||
</resources>
|
||||
@@ -17,6 +17,21 @@
|
||||
|
||||
|
||||
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
|
||||
<!-- API 31+ system splash. Colour follows light/dark via
|
||||
values-night/colors.xml. -->
|
||||
<item name="windowSplashScreenBackground">@color/splash_background</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@mipmap/ic_launcher_foreground</item>
|
||||
<!-- Hand the activity a normal AppCompat theme once the splash is
|
||||
done; Theme.SplashScreen is not meant to stay applied, and
|
||||
AppCompat dialogs need an AppCompat-derived theme. -->
|
||||
<item name="postSplashScreenTheme">@style/AppTheme.NoActionBar</item>
|
||||
<!-- Compat path for API < 31; see drawable/splash.xml. -->
|
||||
<item name="android:background">@drawable/splash</item>
|
||||
</style>
|
||||
|
||||
<!-- Exit-confirmation dialog. Declared with an explicit AppCompat parent
|
||||
because the activity theme (Theme.SplashScreen) is not guaranteed to
|
||||
be AppCompat-derived, and AppCompat's AlertDialog requires one.
|
||||
DayNight so the dialog follows the app's light/dark setting. -->
|
||||
<style name="AppTheme.ExitDialog" parent="Theme.AppCompat.DayNight.Dialog.Alert" />
|
||||
</resources>
|
||||
Reference in new issue
Block a user