diff --git a/android-plugin/app/src/main/AndroidManifest.xml b/android-plugin/app/src/main/AndroidManifest.xml index 3eabdf3d..586dc70f 100644 --- a/android-plugin/app/src/main/AndroidManifest.xml +++ b/android-plugin/app/src/main/AndroidManifest.xml @@ -8,6 +8,12 @@ + + + requestKeepAlive()); + LinearLayout.LayoutParams allowParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); + allowParams.topMargin = dp(6); + contentLayout.addView(allowBackgroundButton, allowParams); + LinearLayout buttons = new LinearLayout(this); buttons.setOrientation(LinearLayout.HORIZONTAL); LinearLayout.LayoutParams buttonsParams = new LinearLayout.LayoutParams( @@ -514,6 +544,73 @@ public final class ServerControlActivity extends Activity { statusView.setTextColor(color); } + // ---- keeping the server alive ---------------------------------------------------------- + + private static final int NOTIFICATION_PERMISSION_REQUEST = 40614; + + private boolean notificationsAllowed() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return true; + return checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) + == PackageManager.PERMISSION_GRANTED; + } + + private boolean batteryExempt() { + PowerManager power = getSystemService(PowerManager.class); + return power != null && power.isIgnoringBatteryOptimizations(getPackageName()); + } + + private void refreshKeepAlive() { + if (keepAliveView == null) return; + boolean notifications = notificationsAllowed(); + boolean exempt = batteryExempt(); + keepAliveView.setText("Keep alive: notification " + (notifications ? "allowed" : "NOT allowed") + + ", battery exemption " + (exempt ? "granted" : "NOT granted")); + keepAliveView.setTextColor(notifications && exempt ? COLOR_PASS : COLOR_WARN); + if (allowBackgroundButton != null) allowBackgroundButton.setEnabled(!(notifications && exempt)); + } + + private void requestKeepAlive() { + // One at a time, and in this order: the notification prompt is a system dialog of ours, + // while the battery one leaves the app entirely - so it goes second, and coming back from + // it re-runs the other branch through onResume. + if (!notificationsAllowed() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + requestPermissions(new String[] { Manifest.permission.POST_NOTIFICATIONS }, + NOTIFICATION_PERMISSION_REQUEST); + return; + } + if (!batteryExempt()) { + try { + startActivity(new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, + Uri.parse("package:" + getPackageName()))); + } catch (RuntimeException missingDialog) { + // Some builds ship without that dialog; the list is the honest fallback rather than + // a button that does nothing. + Log.w(TAG, "no battery-optimization dialog; opening the list", missingDialog); + startActivity(new Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)); + } + return; + } + setStatus("Already kept alive: the notification is allowed and the battery exemption is granted.", + COLOR_PASS); + } + + @Override + protected void onResume() { + super.onResume(); + refreshKeepAlive(); + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] results) { + super.onRequestPermissionsResult(requestCode, permissions, results); + if (requestCode != NOTIFICATION_PERMISSION_REQUEST) return; + refreshKeepAlive(); + // The denial is worth saying out loud: the server still runs, but its notification is gone, + // and a service the system cannot show is a service it may stop. + if (!notificationsAllowed()) + setStatus("Notifications are denied, so the server has no visible notification. " + + "Android may stop it in the background.", COLOR_WARN); + } private TextView makeText(String text, int sizeSp, int color, boolean bold) { TextView view = new TextView(this); view.setText(text);