How I Built an Android “Don’t Touch” Mode That Survives Flutter Process Death
“I will not touch my phone for 30 minutes” often lasts until the next unconscious unlock. I built an Android-only “Don’t Touch Mode” for Yominder. The user chooses a duration. Every time they unlock the phone during that session, one of 64 character lines plays locally. It does not block access; it makes the action noticeable. The hard part was not detecting a tap. It was keeping the behavior alive when the Flutter process was gone, stopping at the correct time, and restoring state after a reboot. Product behavior - Starts now for 30 min, 1h, 1.5h, 2h, 3h, 6h, 12h, or 1 day - Reacts on every unlock; cooldown is zero - Randomly chooses from eight characters and eight lines each - Stops automatically at an absolute end time - Can be stopped immediately from its foreground notification - Does not lock the user out of the phone - Android only The last constraint is deliberate. Emergency access matters more than making the focus mechanism impossible to bypass. Why ACTION_USER_PRESENT SCREEN_ON fires when the display turns on, including cases where the user only checks the time. The behavior I wanted was closer to “the user authenticated and entered the phone.” Android's ACTION_USER_PRESENT is the better event for that definition. The foreground service dynamically registers the receiver: val newReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { if (intent.action == Intent.ACTION_USER_PRESENT) { onUnlocked() } } } registerReceiver( newReceiver, IntentFilter(Intent.ACTION_USER_PRESENT) ) I rejected an Accessibility Service approach. It would request a much stronger capability than the feature needs and add store-policy and user-trust costs. Split ownership at the process boundary Flutter owns configuration and audio preparation. Android owns time-critical and process-independent behavior. Flutter settings --MethodChannel--> Android configuration | | | +--> AlarmManager stop alarm v Foreground Service | v ACTION_USER_PRESENT | v Pre-generated local audio --> Kotlin MediaPlayer Starting Flutter on unlock would add latency and fail when the process cannot be launched quickly. The Kotlin service plays a pre-generated local file directly with MediaPlayer . mediaPlayer = MediaPlayer().apply { setDataSource(path) setOnCompletionListener { player -> player.release() } prepare() start() } No network call occurs at unlock time. That makes the interaction fast and independent of connectivity. Why not a Dart timer? A Dart timer is not a durable scheduler. The app can be killed, the isolate can stop, and the device can reboot. The app stores an absolute end timestamp and schedules a one-shot Android alarm: alarmManager.setExactAndAllowWhileIdle( AlarmManager.RTC_WAKEUP, endAtMillis, stopPendingIntent ) If exact-alarm permission is unavailable, the implementation catches the security failure and falls back to setAndAllowWhileIdle . The tradeoff is a potentially later stop instead of a crash. Restore from facts, not transient state The saved facts are: enabled, absolute end time, and local audio paths. On boot, app replacement, time change, timezone change, or exact-alarm permission change, the receiver reevaluates those facts. Disabled --select duration--> Active Active --unlock-----------> Active (play a random line) Active --end alarm--------> Disabled Active --notification-----> Disabled Active --reboot, time left> Active Active --restore, expired-> Disabled The session is one-shot rather than a repeating daily schedule. That reduces ambiguous state and makes “now for 30 minutes” easy to recover. The foreground notification is part of the UX Android requires a visible notification for the service. Instead of treating it as noise, the app uses it to show that the mode is active and provide an escape hatch. This matters because the mode is intentionally annoying. A user who enabled it by mistake must be able to stop it without hunting through settings. Random audio, no character setting The app has eight characters with eight warning lines each. It picks a random file on every unlock. There is no “warning character” preference. That creates variety and avoids another settings concept. The tradeoff is that users cannot exclude a voice they dislike. I chose lower configuration cost for the first release and will measure feedback before adding filtering. Known limitations - iOS is not supported. - The mode nudges; it does not enforce a block. - The foreground notification remains visible. - Vendor battery optimizations may affect behavior. - Without exact-alarm permission, automatic stopping may be delayed. - Preparing 64 local files has storage and setup cost. Yominder combines spoken encouragement, authorized custom voice IDs, and this Android focus nudge. https://yominder.app/?utm_source=devto&utm_medium=article&utm_campaign=dont_touch_architecture Top comments (0)
Comments
No comments yet. Start the discussion.