<?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: luis</title>
    <description>The latest articles on DEV Community by luis (@lui61140).</description>
    <link>https://dev.to/lui61140</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3953405%2Ff747572a-0953-4db3-8e4c-81cf0fd62979.jpg</url>
      <title>DEV Community: luis</title>
      <link>https://dev.to/lui61140</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lui61140"/>
    <language>en</language>
    <item>
      <title>Why I removed AccessibilityService from my Android app (and finally got into Play Store)</title>
      <dc:creator>luis</dc:creator>
      <pubDate>Wed, 27 May 2026 02:09:58 +0000</pubDate>
      <link>https://dev.to/lui61140/why-i-removed-accessibilityservice-from-my-android-app-and-finally-got-into-play-store-4i52</link>
      <guid>https://dev.to/lui61140/why-i-removed-accessibilityservice-from-my-android-app-and-finally-got-into-play-store-4i52</guid>
      <description>&lt;p&gt;After 4 rejections from Google Play, I deleted 387 lines of working Kotlin code &lt;br&gt;
and a service my app supposedly needed.&lt;/p&gt;

&lt;p&gt;The app got approved 2 days later.&lt;/p&gt;

&lt;p&gt;This is the story of how my "essential" feature turned out to be optional, why &lt;br&gt;
Google Play's AccessibilityService policy is brutal but fair, and what I learned &lt;br&gt;
about scope creep on solo projects.&lt;/p&gt;
&lt;h2&gt;
  
  
  The app
&lt;/h2&gt;

&lt;p&gt;I'm a solo founder. I built &lt;strong&gt;Soccialstopper&lt;/strong&gt;, a screen time app for Android &lt;br&gt;
with one design rule: treat the user like an adult, not a kid being punished.&lt;/p&gt;

&lt;p&gt;No hard locks. No red "YOU'VE EXCEEDED YOUR LIMIT" warnings. Just a gentle &lt;br&gt;
floating bubble when you hit your daily limit, and a "Calm Days" garden that &lt;br&gt;
pauses (instead of resetting to zero) when you slip.&lt;/p&gt;

&lt;p&gt;Built with Flutter, Kotlin for native Android services.&lt;/p&gt;
&lt;h2&gt;
  
  
  The detection problem
&lt;/h2&gt;

&lt;p&gt;A screen time app needs to know which app the user has open. On Android, there &lt;br&gt;
are two ways to do this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Latency&lt;/th&gt;
&lt;th&gt;Permission&lt;/th&gt;
&lt;th&gt;Privacy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;UsageStatsManager&lt;/code&gt; (polling)&lt;/td&gt;
&lt;td&gt;~800ms&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;PACKAGE_USAGE_STATS&lt;/code&gt; — simple toggle&lt;/td&gt;
&lt;td&gt;Low concern&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;AccessibilityService&lt;/code&gt; (events)&lt;/td&gt;
&lt;td&gt;&amp;lt;100ms&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;BIND_ACCESSIBILITY_SERVICE&lt;/code&gt; — sensitive&lt;/td&gt;
&lt;td&gt;High concern&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I implemented &lt;strong&gt;both&lt;/strong&gt;. UsageStatsManager as the primary path, &lt;br&gt;
AccessibilityService as an "advanced" option for users who wanted real-time &lt;br&gt;
detection.&lt;/p&gt;

&lt;p&gt;It worked. Locally. Then I submitted to Play Store.&lt;/p&gt;
&lt;h2&gt;
  
  
  The rejections
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Issue found: Missing prominent disclosure
We were unable to approve your app because we could not locate prominent
disclosure of your use of the AccessibilityService API in your app.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Google's AccessibilityService policy is strict for apps that don't directly &lt;br&gt;
help people with disabilities (&lt;code&gt;IsAccessibilityTool&lt;/code&gt;). You need:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Prominent in-app disclosure&lt;/strong&gt; before requesting the permission&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A demo video&lt;/strong&gt; uploaded with each submission&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Listed usage&lt;/strong&gt; in the Play Store description&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Justification&lt;/strong&gt; that no other API can achieve the same result&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I tried compliance: added a disclosure screen, updated my listing copy. Got &lt;br&gt;
rejected again. And again.&lt;/p&gt;
&lt;h2&gt;
  
  
  The audit that changed everything
&lt;/h2&gt;

&lt;p&gt;Before trying compliance for the 4th time, I did a real audit of how &lt;br&gt;
AccessibilityService was actually used in my codebase. Two questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;What does it do that UsageStatsManager doesn't?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What user-visible features break if I remove it?&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Result: my AccessibilityService class was 387 lines of Kotlin that duplicated &lt;br&gt;
work the UsageStatsMonitorService was already doing. The Dart-side helpers &lt;br&gt;
were literal stubs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;startAccessibilityService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;apps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kDebugMode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;debugPrint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'[BlockLogicService] Starting...'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// This method is now handled by updating shared preferences.&lt;/span&gt;
  &lt;span class="c1"&gt;// The accessibility service reads from shared preferences.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;stopAccessibilityService&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kDebugMode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;debugPrint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'[BlockLogicService] Stopping requested'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Empty stubs with &lt;code&gt;debugPrint&lt;/code&gt;. The "feature" was already dead code &lt;br&gt;
that I'd never noticed because the app worked fine without it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The only real difference&lt;/strong&gt;: 700ms of detection latency. For a digital &lt;br&gt;
wellness app, that's imperceptible to the user.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I removed
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;// AndroidManifest.xml
&lt;span class="gd"&gt;- &amp;lt;service
-     android:name=".SocialStopperAccessibilityService"
-     android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
-     android:exported="true"&amp;gt;
-     &amp;lt;intent-filter&amp;gt;
-         &amp;lt;action android:name="android.accessibilityservice.AccessibilityService" /&amp;gt;
-     &amp;lt;/intent-filter&amp;gt;
-     &amp;lt;meta-data
-         android:name="android.accessibilityservice"
-         android:resource="@xml/accessibility_service_config" /&amp;gt;
- &amp;lt;/service&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;Deleted &lt;code&gt;SocialStopperAccessibilityService.kt&lt;/code&gt; (387 lines)&lt;/li&gt;
&lt;li&gt;Deleted &lt;code&gt;accessibility_service_config.xml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Removed 2 method channels from &lt;code&gt;MainActivity.kt&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Removed 2 stub methods from &lt;code&gt;BlockLogicService.dart&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Removed an entire "advanced" view from the setup wizard&lt;/li&gt;
&lt;li&gt;Removed 8 localization keys in 3 languages&lt;/li&gt;
&lt;li&gt;Bumped versionCode&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total: ~600 lines of code gone. Zero user-visible functionality lost.&lt;/p&gt;
&lt;h2&gt;
  
  
  Verifying the AAB
&lt;/h2&gt;

&lt;p&gt;Before resubmitting, I verified the compiled AAB really had no trace of the &lt;br&gt;
permission:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unzip &lt;span class="nt"&gt;-q&lt;/span&gt; app-release.aab &lt;span class="s2"&gt;"base/manifest/AndroidManifest.xml"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; /tmp/aab
&lt;span class="nb"&gt;cat&lt;/span&gt; /tmp/aab/base/manifest/AndroidManifest.xml | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'\0'&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-aoiE&lt;/span&gt; &lt;span class="s1"&gt;'BIND_ACCESSIBILITY|accessibilityservice'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Empty output. The AAB only declared the permissions I actually needed:&lt;br&gt;
&lt;code&gt;INTERNET&lt;/code&gt;, &lt;code&gt;PACKAGE_USAGE_STATS&lt;/code&gt;, &lt;code&gt;POST_NOTIFICATIONS&lt;/code&gt;, &lt;code&gt;FOREGROUND_SERVICE&lt;/code&gt;, &lt;br&gt;
&lt;code&gt;SYSTEM_ALERT_WINDOW&lt;/code&gt;, and a few others.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Play Console trap
&lt;/h2&gt;

&lt;p&gt;Even with the clean AAB uploaded, &lt;strong&gt;the rejection kept coming&lt;/strong&gt;. Why?&lt;/p&gt;

&lt;p&gt;The Accessibility Services declaration I'd filled out earlier in App Content &lt;br&gt;
was still active. Play Console detects the permission in any active artifact &lt;br&gt;
across any track. I had old AABs with the permission still active in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Closed Testing Alpha → v20 active (with AccessibilityService)&lt;/li&gt;
&lt;li&gt;Internal Testing → v18 active (with AccessibilityService)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The clean v22 AAB was uploaded but &lt;strong&gt;inactive&lt;/strong&gt; because I hadn't created &lt;br&gt;
releases in those tracks.&lt;/p&gt;

&lt;p&gt;The fix: create a release in EVERY track using the clean AAB. Once no active &lt;br&gt;
release had the permission, the declaration in App Content became removable, &lt;br&gt;
and the rejection cycle finally broke.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Scope creep on solo projects is sneaky.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Six months ago, when I added AccessibilityService, it felt like the "right" &lt;br&gt;
technical choice. Real-time detection is objectively better than polling. &lt;br&gt;
But "better" doesn't always mean "necessary."&lt;/p&gt;

&lt;p&gt;The 700ms difference was a feature for me as a developer. To users, it was &lt;br&gt;
invisible. I built it for myself, not for them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Google's strictness on AccessibilityService is actually fair.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The API is dangerous. An accessibility service can read everything on screen. &lt;br&gt;
Google's policy forces you to ask: &lt;em&gt;do you really need this, or is there a &lt;br&gt;
narrower API that works?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For most "monitor what's happening" use cases, the narrower API is &lt;br&gt;
&lt;code&gt;UsageStatsManager&lt;/code&gt;. Use it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Play Console state is sticky.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Cleaning your code isn't enough. You also have to clean up old artifacts in &lt;br&gt;
all tracks, and remove answered policy declarations. Those forms persist &lt;br&gt;
based on what's &lt;em&gt;active&lt;/em&gt;, not what's &lt;em&gt;latest&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Sometimes the right fix is "delete code", not "add code".&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I spent a week trying to write a compliant disclosure flow. The actual fix &lt;br&gt;
took 2 hours: delete the service, delete its references, ship.&lt;/p&gt;
&lt;h2&gt;
  
  
  The code
&lt;/h2&gt;

&lt;p&gt;If you're building something similar, the boring &lt;code&gt;UsageStatsManager&lt;/code&gt; polling &lt;br&gt;
approach is enough for 99% of screen time / digital wellness use cases. The &lt;br&gt;
800ms latency feels real-time in practice.&lt;/p&gt;

&lt;p&gt;Here's the core pattern:&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="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;getCurrentForegroundApp&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?&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;end&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="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;begin&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;10_000&lt;/span&gt; &lt;span class="c1"&gt;// 10 second window&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;events&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;usageStatsManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;queryEvents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&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;event&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UsageEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Event&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;lastForegroundPackage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasNextEvent&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getNextEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;eventType&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;UsageEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MOVE_TO_FOREGROUND&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;lastForegroundPackage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;packageName&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;lastForegroundPackage&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Poll this every ~800ms, compare against the user's app list, accumulate time, &lt;br&gt;
trigger your notification when the limit is hit. No accessibility, no &lt;br&gt;
disclosure forms, no rejections.&lt;/p&gt;

&lt;h2&gt;
  
  
  The app
&lt;/h2&gt;

&lt;p&gt;If you're curious or want to try it: &lt;br&gt;
&lt;a href="https://play.google.com/store/apps/details?id=com.lfbo.soccialstopper&amp;amp;pcampaignid=web_share" rel="noopener noreferrer"&gt;Soccialstopper on Google Play&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Built with Flutter, free, no signup.&lt;/p&gt;

&lt;p&gt;Happy to answer any questions about the build, the Play Store policy &lt;br&gt;
navigation, or the Flutter + Kotlin native services setup.&lt;/p&gt;

</description>
      <category>android</category>
      <category>flutter</category>
      <category>mobile</category>
      <category>indiedev</category>
    </item>
  </channel>
</rss>
