<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mingke Tu</title>
    <description>The latest articles on DEV Community by Mingke Tu (@disconfirmai).</description>
    <link>https://dev.to/disconfirmai</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4209707%2Fd8a35d54-8b25-4fb5-aa07-ce37b15a04a2.png</url>
      <title>DEV Community: Mingke Tu</title>
      <link>https://dev.to/disconfirmai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/disconfirmai"/>
    <language>en</language>
    <item>
      <title>Why Most Android Screen Time Blockers Fail at Midnight (And How We Fixed the Clock &amp; Uninstall Loopholes)</title>
      <dc:creator>Mingke Tu</dc:creator>
      <pubDate>Tue, 27 Oct 2026 10:00:35 +0000</pubDate>
      <link>https://dev.to/disconfirmai/why-most-android-screen-time-blockers-fail-at-midnight-and-how-we-fixed-the-clock-uninstall-1e44</link>
      <guid>https://dev.to/disconfirmai/why-most-android-screen-time-blockers-fail-at-midnight-and-how-we-fixed-the-clock-uninstall-1e44</guid>
      <description>&lt;p&gt;If you have ever set a screen time limit at 9 PM and watched it disappear at midnight, you already know the problem. Most Android blockers fail because the person who set the rule is also the person who can undo it, and willpower is lowest late at night. I built Ripple (v0.1.1), a free and local-only Android focus app, to close the three loopholes I kept hitting myself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three classic loopholes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Changing the system clock.&lt;/strong&gt; Sleep schedules and cooldown timers usually read wall-clock time. Open Settings, move the date forward a day, and the timer is over. Any blocker that trusts &lt;code&gt;System.currentTimeMillis()&lt;/code&gt; can be beaten in seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Scrolling feeds inside whitelisted apps.&lt;/strong&gt; Most people whitelist messaging apps so they stay reachable. Those apps now ship short-video feeds and stories, so a "messaging only" exception becomes a two-hour scroll session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Uninstalling or force-stopping the blocker.&lt;/strong&gt; If an urge hits at midnight, revoking Accessibility permission or uninstalling the blocker takes about five seconds. Friction that you can remove on demand decays to zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Ripple closes each one
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Monotonic clock anchoring
&lt;/h3&gt;

&lt;p&gt;Ripple measures every timer with &lt;code&gt;SystemClock.elapsedRealtime()&lt;/code&gt;, which keeps counting regardless of what the user sets in the date and time settings. The app anchors its state to this clock and re-anchors across reboots, so changing the phone's time does nothing to a running cooldown or a sleep lock.&lt;/p&gt;

&lt;h3&gt;
  
  
  Asymmetric cooldowns
&lt;/h3&gt;

&lt;p&gt;Tightening a rule is instant. Add an app to the block list, lower a daily limit, or extend sleep hours, and it applies immediately. Relaxing a rule is slow. Pausing a schedule, raising a limit, or unlocking uninstall requires waiting out a cooldown of 15 minutes by default, configurable up to 2 hours, and doubled for short-video apps. If a real emergency comes up, a friend or family member can generate a one-time unlock code so you can skip the wait.&lt;/p&gt;

&lt;h3&gt;
  
  
  Per-session caps on whitelisted apps
&lt;/h3&gt;

&lt;p&gt;Whitelisted messaging apps get a 30-minute per-session limit. You can still reply to people, but a built-in video feed cannot turn into a late-night loophole.&lt;/p&gt;

&lt;h3&gt;
  
  
  Uninstall and force-stop protection
&lt;/h3&gt;

&lt;p&gt;On Android 11 and newer, Ripple can pair with Wireless Debugging on the phone itself, with no PC and no root. It starts a small local daemon through &lt;code&gt;app_process&lt;/code&gt; running as UID 2000 (the shell user). That daemon locks the OS uninstall switch and restarts the blocker within about 2.5 seconds if it is swiped away from Recents. If you already use Shizuku, Ripple supports that route too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the design works
&lt;/h2&gt;

&lt;p&gt;Each fix removes one cheap exit. The monotonic clock removes the time trick, the asymmetric cooldown makes late-night relaxing costly, the session cap shrinks the whitelist loophole, and the shell daemon protects the app itself. Together they keep a rule you set in the morning alive at midnight.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Project page: &lt;a href="https://ripplefocus.github.io/en/" rel="noopener noreferrer"&gt;https://ripplefocus.github.io/en/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Comparison with AppBlock and StayFocused: &lt;a href="https://ripplefocus.github.io/en/vs-appblock-stayfocused/" rel="noopener noreferrer"&gt;https://ripplefocus.github.io/en/vs-appblock-stayfocused/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Latest release (APK): &lt;a href="https://github.com/ripplefocus/ripplefocus.github.io/releases/latest" rel="noopener noreferrer"&gt;https://github.com/ripplefocus/ripplefocus.github.io/releases/latest&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feedback from developers and digital minimalists is welcome, especially on edge cases for the clock and uninstall protections.&lt;/p&gt;

</description>
      <category>android</category>
      <category>productivity</category>
      <category>kotlin</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Building a Self-Healing Android App Blocker Using On-Device Wireless Debugging (No PC or Root)</title>
      <dc:creator>Mingke Tu</dc:creator>
      <pubDate>Tue, 27 Oct 2026 03:45:19 +0000</pubDate>
      <link>https://dev.to/disconfirmai/building-a-self-healing-android-app-blocker-using-on-device-wireless-debugging-no-pc-or-root-4f69</link>
      <guid>https://dev.to/disconfirmai/building-a-self-healing-android-app-blocker-using-on-device-wireless-debugging-no-pc-or-root-4f69</guid>
      <description>&lt;p&gt;When I tried using standard screen-time apps on Android to stop scrolling short-form video late at night, I ran into a predictable problem. Setting a 90-minute daily limit at 10:00 AM is easy. Keeping that limit intact at 12:30 AM takes much more than a pop-up dialog.&lt;/p&gt;

&lt;p&gt;On Android, bypassing a typical app blocker takes about five seconds:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Tap "Ignore limit for today" if the app is polite enough to offer one.&lt;/li&gt;
&lt;li&gt;Open &lt;strong&gt;Settings &amp;gt; Accessibility&lt;/strong&gt;, toggle off the blocker's service, or long-press the launcher icon and tap &lt;strong&gt;Uninstall&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Change the system clock forward by a few hours to skip a cooldown timer, or change the date to reset the daily quota.&lt;/li&gt;
&lt;li&gt;On aggressive OEM ROMs like OPPO ColorOS, Xiaomi HyperOS, or vivo OriginOS, swipe the app card away in Recents so the system kills its &lt;code&gt;AccessibilityService&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I built &lt;strong&gt;Ripple (涟漪)&lt;/strong&gt; (&lt;a href="https://ripplefocus.github.io/en/" rel="noopener noreferrer"&gt;English page&lt;/a&gt; / &lt;a href="https://github.com/ripplefocus/ripplefocus.github.io/releases" rel="noopener noreferrer"&gt;APK Releases&lt;/a&gt;) to close each of those escape hatches without requiring root access, a desktop computer, or a paid subscription.&lt;/p&gt;

&lt;p&gt;This post walks through the engineering choices behind Ripple: enforcing asymmetric rule changes on Android's monotonic clock, closing whitelist loopholes, running a self-healing &lt;code&gt;shell&lt;/code&gt; (UID 2000) daemon via Android 11+ on-device wireless debugging, and fixing a subtle &lt;code&gt;uint32&lt;/code&gt; overflow in &lt;code&gt;AndroidPubkey&lt;/code&gt; that initially caused half of all generated RSA keys to fail pairing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxu382pw1p6saexppcore.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxu382pw1p6saexppcore.png" alt="Ripple Overview" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Asymmetric Rules and Monotonic Cooldowns (&lt;code&gt;SystemClock.elapsedRealtime()&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;In Ripple, rule changes follow an asymmetric model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tightening a rule takes effect immediately&lt;/strong&gt;: Lowering a daily limit from 90 minutes to 45 minutes, extending a nightly focus schedule, or removing an app from the focus whitelist happens with zero delay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relaxing a rule requires waiting through a cooldown timer&lt;/strong&gt;: Adding time to an app, skipping tonight's sleep schedule, pausing protection, or uninstalling Ripple starts a countdown (15 minutes by default, configurable up to 2 hours, and automatically doubled for short-video apps like TikTok, YouTube, and Douyin). Users can also set a &lt;strong&gt;Friend / Parent Unlock Code&lt;/strong&gt; so a family member or parent can unlock it immediately when needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why &lt;code&gt;System.currentTimeMillis()&lt;/code&gt; Fails for Cooldowns
&lt;/h3&gt;

&lt;p&gt;A common mistake in productivity apps is storing a cooldown target timestamp using wall-clock time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Vulnerable to Settings &amp;gt; Date &amp;amp; Time changes&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;unlockAtWallMs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;currentTimeMillis&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;cooldownDurationMs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a user (or a teenager whose parent set up the phone) turns off automatic network time and moves the clock forward by 30 minutes, a wall-clock timer expires immediately.&lt;/p&gt;

&lt;p&gt;Instead, Ripple anchors active cooldowns to Android's monotonic boot clock, &lt;code&gt;SystemClock.elapsedRealtime()&lt;/code&gt;, which counts milliseconds since boot including deep sleep and cannot be altered by user time settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;nowElapsed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SystemClock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;elapsedRealtime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;nowWall&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;currentTimeMillis&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We persist both &lt;code&gt;(startedElapsedMs, startedWallMs, durationMs)&lt;/code&gt;. While the device remains booted, remaining time is computed strictly from &lt;code&gt;SystemClock.elapsedRealtime() - startedElapsedMs&lt;/code&gt;. If the user reboots the phone mid-cooldown (which resets &lt;code&gt;elapsedRealtime&lt;/code&gt; to zero), we detect the boot count / monotonic reset and prevent any negative jump or forward clock jump from crediting unelapsed time.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Closing the Whitelist Loophole and Protecting System Essentials
&lt;/h2&gt;

&lt;p&gt;During a nightly focus schedule (Ripple defaults to &lt;code&gt;23:30–07:00&lt;/code&gt; on fresh installs), blocking everything except a few allowed apps creates two practical problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Messaging App Video-Feed Loophole&lt;/strong&gt;: If you whitelist a messaging app like WeChat or Telegram so you do not miss urgent messages, you also gain access to its built-in short-video feed or channels. To solve this, Ripple limits the focus whitelist to at most 5 apps and applies a &lt;strong&gt;per-session usage cap&lt;/strong&gt; (30 minutes by default) to each whitelisted app during focus hours. You have plenty of time to reply to messages, after which the app is paused for the rest of that focus session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero False Positives on System Essentials&lt;/strong&gt;: An app blocker should never stop an incoming phone call, an emergency SMS, a morning alarm, or a payment screen at a subway gate. Ripple permanently exempts system essentials without consuming whitelist slots:

&lt;ul&gt;
&lt;li&gt;Phone dialer and active call screens&lt;/li&gt;
&lt;li&gt;SMS / Messaging&lt;/li&gt;
&lt;li&gt;Clock and Alarms (full-screen ringing alarm activities are explicitly whitelisted so morning alarms always work)&lt;/li&gt;
&lt;li&gt;Calendar, System Camera (resolved dynamically via &lt;code&gt;MediaStore.ACTION_IMAGE_CAPTURE&lt;/code&gt;), Input Methods (IME), System File / Photo Pickers (&lt;code&gt;documentsui&lt;/code&gt; and MediaProvider picker), TalkBack, and default NFC Wallets.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  3. Two-Tier Anti-Uninstall Architecture
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Tier 1: Basic Protection (No PC, No Pairing)
&lt;/h3&gt;

&lt;p&gt;Most users only need a 15-minute speed bump when an urge hits. Basic Protection takes about one minute to set up on the phone by enabling &lt;strong&gt;Accessibility Service&lt;/strong&gt; and &lt;strong&gt;Device Administrator&lt;/strong&gt; (&lt;code&gt;DeviceAdminReceiver&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;When active, &lt;code&gt;ProtectionMatcher&lt;/code&gt; inspects window state changes in the Accessibility service to detect self-sabotage paths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Long-pressing Ripple on the home screen to open the system uninstall dialog&lt;/li&gt;
&lt;li&gt;Opening Android Settings &amp;gt; App Info for Ripple to tap &lt;strong&gt;Uninstall&lt;/strong&gt; or &lt;strong&gt;Force Stop&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Opening Ripple's Storage page to tap &lt;strong&gt;Clear Storage / Clear Data&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Navigating to Ripple's Accessibility toggle or the Device Admin deactivation screen&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When any of these screens is entered, Ripple immediately navigates back to the home screen and displays a calm dark-mode screen stating which action was just intercepted and how long the cooldown timer is.&lt;/p&gt;

&lt;p&gt;Crucially, &lt;code&gt;ProtectionMatcher&lt;/code&gt; checks the target package name and specific installer view nodes so that opening App Info for other apps (for example, clearing cache in a browser) or installing an in-place APK update for Ripple is never falsely blocked.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tier 2: Advanced Protection via On-Device Wireless Debugging (&lt;code&gt;ripple_ss&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Basic Protection has two limitations on modern Android phones:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If the user connects the phone to a computer or navigates fast enough on certain custom ROMs, Device Admin can still be deactivated.&lt;/li&gt;
&lt;li&gt;On aggressive OEM skins (ColorOS, HyperOS/MIUI, OriginOS), swiping Ripple away from the Recents screen force-stops the app process and tears down its &lt;code&gt;AccessibilityService&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Traditionally, surviving task killers or locking uninstallation at the system level required connecting the phone to a PC with USB debugging (&lt;code&gt;adb shell&lt;/code&gt;) or installing a separate third-party helper like Shizuku.&lt;/p&gt;

&lt;p&gt;Starting with &lt;strong&gt;Android 11 (API 30)&lt;/strong&gt;, Android includes built-in &lt;strong&gt;Wireless Debugging&lt;/strong&gt;. Because &lt;code&gt;adbd&lt;/code&gt; listens on the device's local interface, an app can pair with &lt;code&gt;adbd&lt;/code&gt; directly on the phone (&lt;code&gt;127.0.0.1&lt;/code&gt;), open a local ADB connection, and spawn a helper process running under the &lt;code&gt;shell&lt;/code&gt; user (&lt;code&gt;UID 2000&lt;/code&gt;) via &lt;code&gt;app_process&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In Ripple, this is an optional feature under &lt;strong&gt;Settings &amp;gt; Anti-Uninstall &amp;gt; Advanced Protection&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The user follows a 4-step guide to enable Developer Options and Wireless Debugging, then taps &lt;strong&gt;Pair device with pairing code&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Ripple posts a notification with an inline text input (&lt;code&gt;RemoteInput&lt;/code&gt;) so the user can type the 6-digit pairing code directly from the notification shade without closing Android's pairing dialog (closing the dialog invalidates the pairing code).&lt;/li&gt;
&lt;li&gt;Once paired, Ripple connects to &lt;code&gt;127.0.0.1:&amp;lt;connectPort&amp;gt;&lt;/code&gt;, grants itself &lt;code&gt;android.permission.WRITE_SECURE_SETTINGS&lt;/code&gt;, and starts a lightweight JVM daemon named &lt;code&gt;ripple_ss&lt;/code&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;CLASSPATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;pm path com.slowisle.app | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;: &lt;span class="nt"&gt;-f2-&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
app_process /system/bin &lt;span class="nt"&gt;--nice-name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ripple_ss &lt;span class="se"&gt;\&lt;/span&gt;
  com.slowisle.app.selfstart.Server &amp;lt;bridgeToken&amp;gt; &amp;lt;wakeToken&amp;gt; &amp;lt;userId&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running as &lt;code&gt;UID 2000&lt;/code&gt; (&lt;code&gt;shell&lt;/code&gt;), &lt;code&gt;ripple_ss&lt;/code&gt; does three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;System-Level Uninstall Lock&lt;/strong&gt;: Calls &lt;code&gt;DevicePolicyManager.setBlockUninstallForUser&lt;/code&gt; so the package manager rejects uninstallation even before any UI appears. When the user legitimately finishes the cooldown timer under "I want to uninstall Ripple", Ripple releases this lock and verifies the unlocked state via &lt;code&gt;DevicePolicyManager.isUninstallBlocked&lt;/code&gt; before launching the system uninstaller.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strong Binder Reference + &lt;code&gt;DeathRecipient&lt;/code&gt; + 5s Heartbeat&lt;/strong&gt;: &lt;code&gt;ripple_ss&lt;/code&gt; passes an &lt;code&gt;IBinder&lt;/code&gt; back to the main app process through a &lt;code&gt;ContentProvider.call()&lt;/code&gt; handshake. If the user swipes Ripple away from Recents on ColorOS 14, &lt;code&gt;ripple_ss&lt;/code&gt; detects process death (and missing &lt;code&gt;enabled_accessibility_services&lt;/code&gt;), re-writes the Accessibility setting, and brings the service back in &lt;strong&gt;about 2.5 seconds&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reboot Self-Recovery&lt;/strong&gt;: After a phone reboot, once the user unlocks the screen, Ripple uses its granted &lt;code&gt;WRITE_SECURE_SETTINGS&lt;/code&gt; permission to restore Accessibility and reconnects to &lt;code&gt;ripple_ss&lt;/code&gt; within about a minute.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  4. The Bug That Broke 50% of Wireless Debugging Pairings: &lt;code&gt;AndroidPubkey&lt;/code&gt; &lt;code&gt;n0inv&lt;/code&gt; Overflow
&lt;/h2&gt;

&lt;p&gt;While building the built-in wireless debugging client, we ran into a baffling bug: &lt;strong&gt;pairing succeeded on the first try about half the time, and failed silently after the SPAKE2 / TLS handshake the other half of the time.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When pairing over Android 11+ wireless debugging:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The client and &lt;code&gt;adbd&lt;/code&gt; establish a TLS 1.3 connection authenticated with SPAKE2 using the 6-digit pairing code.&lt;/li&gt;
&lt;li&gt;Inside that encrypted channel, the client sends a &lt;code&gt;PeerInfo&lt;/code&gt; structure containing its 2048-bit RSA public key encoded in AOSP's custom binary &lt;code&gt;RSAPublicKey&lt;/code&gt; format (&lt;code&gt;libcrypto_utils/android_pubkey.cpp&lt;/code&gt;), Base64-encoded.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;adbd&lt;/code&gt; decodes the struct and saves the public key to &lt;code&gt;/data/misc/adb/adb_keys&lt;/code&gt; so subsequent &lt;code&gt;AdbConnection&lt;/code&gt; handshakes on the connect port are trusted.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;AOSP's 524-byte &lt;code&gt;RSAPublicKey&lt;/code&gt; struct looks like this in C (&lt;code&gt;system/core/libcrypto_utils/include/crypto_utils/android_pubkey.h&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;RSAPublicKey&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;modulus_size_words&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 64 for 2048-bit RSA&lt;/span&gt;
    &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;n0inv&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;              &lt;span class="c1"&gt;// Precomputed Montgomery parameter: -1 / n[0] mod 2^32&lt;/span&gt;
    &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;              &lt;span class="c1"&gt;// Modulus as little-endian byte array&lt;/span&gt;
    &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;rr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;             &lt;span class="c1"&gt;// R^2 mod N as little-endian byte array&lt;/span&gt;
    &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;exponent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;           &lt;span class="c1"&gt;// 65537&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;RSAPublicKey&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the second field: &lt;code&gt;uint32_t n0inv&lt;/code&gt;, which is $-N^{-1} \bmod 2^{32}$, an unsigned 32-bit integer in the range $[1, 2^{32} - 1]$ (since any RSA modulus $N$ is odd).&lt;/p&gt;

&lt;p&gt;In Kotlin/Java, we compute &lt;code&gt;n0inv&lt;/code&gt; using &lt;code&gt;BigInteger&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;r32&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BigInteger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ZERO&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setBit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// 2^32&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;n0inv&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r32&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;modInverse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;n0inv&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r32&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subtract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n0inv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;// Result is in [1, 2^32 - 1]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In our initial port, we wrote &lt;code&gt;n0inv&lt;/code&gt; into the little-endian &lt;code&gt;ByteBuffer&lt;/code&gt; using &lt;code&gt;BigInteger.intValueExact()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// BUG: throws ArithmeticException whenever bit 31 of n0inv is 1!&lt;/span&gt;
&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;putInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n0inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;intValueExact&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is why that failed 50% of the time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;n0inv&lt;/code&gt; is an &lt;strong&gt;unsigned&lt;/strong&gt; 32-bit value uniformly distributed across odd integers in $[1, 2^{32} - 1]$.&lt;/li&gt;
&lt;li&gt;Java's &lt;code&gt;int&lt;/code&gt; is &lt;strong&gt;signed&lt;/strong&gt; (&lt;code&gt;[-2^31, 2^31 - 1]&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Whenever a newly generated RSA key had $n_0^{-1}$ such that &lt;code&gt;n0inv &amp;gt;= 0x80000000&lt;/code&gt; (which happens for &lt;strong&gt;50% of all random RSA moduli&lt;/strong&gt;), &lt;code&gt;n0inv.intValueExact()&lt;/code&gt; threw &lt;code&gt;ArithmeticException: BigInteger out of int range&lt;/code&gt; because the positive &lt;code&gt;BigInteger&lt;/code&gt; required 33 bits in two's-complement signed representation!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix is a one-line change from &lt;code&gt;intValueExact()&lt;/code&gt; to &lt;code&gt;toInt()&lt;/code&gt; (or &lt;code&gt;intValue()&lt;/code&gt;), which preserves the exact lower 32 bits in two's-complement representation when written by &lt;code&gt;ByteBuffer.putInt()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;r32&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BigInteger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ZERO&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setBit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;n0inv&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r32&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;modInverse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;n0inv&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r32&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subtract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n0inv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// toInt() preserves all 32 bits of the uint32_t value without throwing on &amp;gt;= 2^31&lt;/span&gt;
&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;putInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n0inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toInt&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once we switched &lt;code&gt;intValueExact()&lt;/code&gt; to &lt;code&gt;toInt()&lt;/code&gt; (and added a unit test covering RSA moduli across both halves of the &lt;code&gt;uint32&lt;/code&gt; range), wireless debugging pairing became 100% reliable.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Try Ripple
&lt;/h2&gt;

&lt;p&gt;Ripple &lt;code&gt;v0.1.1&lt;/code&gt; is out today. It is free, has no ads, requires no account, and stores all rules and usage statistics locally on your device.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;English Overview &amp;amp; Guide&lt;/strong&gt;: &lt;a href="https://ripplefocus.github.io/en/" rel="noopener noreferrer"&gt;https://ripplefocus.github.io/en/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chinese Homepage&lt;/strong&gt;: &lt;a href="https://ripplefocus.github.io/" rel="noopener noreferrer"&gt;https://ripplefocus.github.io/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Download APK (GitHub Releases)&lt;/strong&gt;: &lt;a href="https://github.com/ripplefocus/ripplefocus.github.io/releases/latest" rel="noopener noreferrer"&gt;https://github.com/ripplefocus/ripplefocus.github.io/releases/latest&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;(Note: &lt;code&gt;v0.1.1&lt;/code&gt; is out today, and the Android app interface now supports full English and Simplified Chinese.)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>flutter</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Building DisconfirmAI: An AI investment research workspace that looks for counter-evidence first</title>
      <dc:creator>Mingke Tu</dc:creator>
      <pubDate>Tue, 27 Oct 2026 01:22:06 +0000</pubDate>
      <link>https://dev.to/disconfirmai/building-disconfirmai-an-ai-investment-research-workspace-that-looks-for-counter-evidence-first-5bhk</link>
      <guid>https://dev.to/disconfirmai/building-disconfirmai-an-ai-investment-research-workspace-that-looks-for-counter-evidence-first-5bhk</guid>
      <description>&lt;p&gt;When you use a general-purpose LLM to analyze a stock or read an earnings report, it tends to agree with whatever premise you put in the prompt. If you ask why a company looks undervalued, it will assemble a convincing bull case. In financial research, that habit is risky.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://disconfirmai.com/en" rel="noopener noreferrer"&gt;DisconfirmAI&lt;/a&gt; around a simple rule from Karl Popper and value investing: before building conviction in a thesis, look for the specific evidence that would prove it wrong.&lt;/p&gt;

&lt;p&gt;Here is how the workspace is structured:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Primary-Source Daily Reading
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://disconfirmai.com/en/reading" rel="noopener noreferrer"&gt;Daily Reading&lt;/a&gt; feed curates AI industry and market developments from primary sources, keeping original links and publication dates. Editorial annotations use three fixed labels:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;[Background]&lt;/strong&gt; for factual context&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;[Doubt]&lt;/strong&gt; when a claim relies only on vendor self-reporting without independent benchmarks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;[Editor's Note]&lt;/strong&gt; for editorial commentary&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. SEC Filing &amp;amp; 13F Research Separated by Evidence Level
&lt;/h3&gt;

&lt;p&gt;In the &lt;a href="https://disconfirmai.com/en/features/research" rel="noopener noreferrer"&gt;Research&lt;/a&gt; workflow, questions about earnings releases, multi-year financials, and SEC 13F institutional holdings (calculated strictly by share-count changes rather than market-value fluctuations) are split into three sections: &lt;strong&gt;Verified&lt;/strong&gt;, &lt;strong&gt;Inferred&lt;/strong&gt;, and &lt;strong&gt;Doubtful&lt;/strong&gt;. It does not output buy/sell recommendations or price targets.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Investment Framework &amp;amp; Quiet Scheduled Radar
&lt;/h3&gt;

&lt;p&gt;In &lt;a href="https://disconfirmai.com/en/features/system" rel="noopener noreferrer"&gt;Investment Framework&lt;/a&gt;, you write down your circle of competence, entry rules, and the exact conditions that would force you to revise or exit a position. &lt;a href="https://disconfirmai.com/en/features/radar" rel="noopener noreferrer"&gt;Radar&lt;/a&gt; scans those conditions on a schedule. When nothing material changes, it stays quiet and logs &lt;code&gt;scanned-and-empty&lt;/code&gt; in the audit trail.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Open AI Industry Databases &amp;amp; 23 Free Valuation Tools
&lt;/h3&gt;

&lt;p&gt;Everything below is open in the browser without registration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://disconfirmai.com/en/ai100" rel="noopener noreferrer"&gt;AI 100 Ranking&lt;/a&gt; and &lt;a href="https://disconfirmai.com/en/ai-industry-map" rel="noopener noreferrer"&gt;AI Industry Map&lt;/a&gt; covering 328 public and private AI companies with valuation sources.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://disconfirmai.com/en/ai-capital" rel="noopener noreferrer"&gt;AI Capital Database&lt;/a&gt; tracking 291 AI-native companies with $100M+ in funding and 1,205 investment firms.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://disconfirmai.com/en/tools" rel="noopener noreferrer"&gt;23 Native Investing Tools&lt;/a&gt; including Value Trap Review, Business Quality Check, Damodaran DCF, Buffett Intrinsic Value, Abnormal Selling Pressure Check, and a 38-metric financial analysis handbook.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you analyze tech or AI companies or build financial research workflows, I would love your feedback on the tools and the falsification approach: &lt;a href="https://disconfirmai.com/en" rel="noopener noreferrer"&gt;https://disconfirmai.com/en&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>fintech</category>
      <category>investing</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
