fix(app): the Pomodoro timer's permissions take effect as soon as they are given
All checks were successful
CI / check (push) Successful in 9m44s
CI / deploy (push) Successful in 14s

Found on the Pixel 9a with v0.2.1:

- Do Not Disturb while focusing: switching it on opens Android's list
  of apps with Do Not Disturb access; once the person allowed it there
  and came back, the switch was still off, saying the app may not, and
  had to be switched on again. The profile's sheet now looks again when
  the app comes back, and the switch goes on by itself once allowed.
- The timer's notification, posted while Android was still asking for
  leave to notify at the first start, showed nowhere until the timer
  next changed. It is now posted again as soon as notifications are
  allowed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-07 14:26:05 +02:00
commit c89ef05a20
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
4 changed files with 101 additions and 2 deletions

View file

@ -66,7 +66,26 @@ class MainActivity : FlutterActivity() {
checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED
) {
askedToNotify = true
requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), 1)
requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), NOTIFY_REQUEST)
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray,
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
// The timer's notification was posted while Android asked, and showed
// nowhere: now it may.
if (requestCode == NOTIFY_REQUEST &&
grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED
) {
PomodoroService.showAgain()
}
}
private companion object {
const val NOTIFY_REQUEST = 1
}
}

View file

@ -65,6 +65,11 @@ class PomodoroService : Service() {
appGone = false
}
/** Shows the timer again, as once notifications are allowed: posted before, it showed nowhere. */
fun showAgain() {
running?.repost()
}
/** Whether the timer was stopped here while the app was gone; forgets it. */
fun takeStoppedOutside(context: Context): Boolean {
val preferences = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)

View file

@ -102,8 +102,19 @@ class _ProfileSheetState() extends State<_ProfileSheet> {
late final _name = TextEditingController(text: widget.initial.name);
var _quietDenied = false;
// Asking may open the system's settings, where the person allows it and
// comes back: then the switch goes on by itself.
late final AppLifecycleListener _comingBack;
@override
void initState() {
super.initState();
_comingBack = AppLifecycleListener(onResume: _lookAgainAtQuiet);
}
@override
void dispose() {
_comingBack.dispose();
_name.dispose();
super.dispose();
}
@ -115,9 +126,21 @@ class _ProfileSheetState() extends State<_ProfileSheet> {
_change(_profile.copyWith(doNotDisturb: false));
return;
}
await _quiet(ask: true);
}
Future<void> _lookAgainAtQuiet() async {
if (_quietDenied) {
await _quiet(ask: false);
}
}
/// Keeps quiet in this profile if the person allows it, asking them when
/// [ask]; says so otherwise.
Future<void> _quiet({required bool ask}) async {
final allowed = await DependenciesScope.of(
context,
).pomodoro.mayKeepQuiet(ask: true);
).pomodoro.mayKeepQuiet(ask: ask);
if (mounted) {
setState(() {
_quietDenied = !allowed;

View file

@ -155,6 +155,58 @@ void main() {
expect(tester.widget<SwitchListTile>(quiet).value, isTrue);
});
testWidgets("allowed in the system's settings, keeping quiet switches on "
'by itself on coming back', (tester) async {
await showHome(tester);
await tester.tap(find.byTooltip('Pomodoro profiles'));
await tester.pumpAndSettle();
await tester.tap(find.byTooltip('Edit profile').first);
await tester.pumpAndSettle();
final quiet = find.widgetWithText(
SwitchListTile,
'Do Not Disturb while focusing',
);
await tester.ensureVisible(quiet);
await tester.pumpAndSettle();
await tester.tap(quiet);
await tester.pumpAndSettle();
expect(tester.widget<SwitchListTile>(quiet).value, isFalse);
// The person allows it in the system's settings, and comes back.
platform.allowsQuiet = true;
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive);
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed);
await tester.pumpAndSettle();
expect(tester.widget<SwitchListTile>(quiet).value, isTrue);
expect(find.textContaining('may not keep this device quiet'), findsNothing);
expect(platform.askedForQuiet, 1, reason: 'asked once, not again');
});
testWidgets('coming back without allowing it leaves keeping quiet off', (
tester,
) async {
await showHome(tester);
await tester.tap(find.byTooltip('Pomodoro profiles'));
await tester.pumpAndSettle();
await tester.tap(find.byTooltip('Edit profile').first);
await tester.pumpAndSettle();
final quiet = find.widgetWithText(
SwitchListTile,
'Do Not Disturb while focusing',
);
await tester.ensureVisible(quiet);
await tester.pumpAndSettle();
await tester.tap(quiet);
await tester.pumpAndSettle();
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive);
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed);
await tester.pumpAndSettle();
expect(tester.widget<SwitchListTile>(quiet).value, isFalse);
});
testWidgets('suggests setting up the top bar where it shows no icons, '
'until dismissed', (tester) async {
platform.suggestTopBar(suggest: true);