Skip to content

ADFA-3828 Fix 3 StrictMode violations#1247

Open
hal-eisen-adfa wants to merge 2 commits intostagefrom
ADFA-3828-StrictMode-violations-install-pre-create
Open

ADFA-3828 Fix 3 StrictMode violations#1247
hal-eisen-adfa wants to merge 2 commits intostagefrom
ADFA-3828-StrictMode-violations-install-pre-create

Conversation

@hal-eisen-adfa
Copy link
Copy Markdown
Collaborator

  1. WebServer socket tagging -- No more UntaggedSocketViolation from our WebServer (the ones showing are from OkHttp and JDI, outside our control)
  2. InstallationViewModel.checkStorageAndNotify() -- Now runs via withContext(Dispatchers.IO) inside a viewModelScope.launch block, no main-thread disk reads
  3. PermissionsFragment.startIdeSetup() -- Storage check now inside a coroutine scope, no synchronous main-thread call before the async block

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 25, 2026

Warning

Rate limit exceeded

@hal-eisen-adfa has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 41 minutes and 6 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 41 minutes and 6 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c1d539b7-a113-4003-8d79-4acec9e9be5a

📥 Commits

Reviewing files that changed from the base of the PR and between 92bf34f and 298ab0e.

📒 Files selected for processing (3)
  • app/src/main/java/com/itsaky/androidide/fragments/onboarding/PermissionsFragment.kt
  • app/src/main/java/com/itsaky/androidide/localWebServer/WebServer.kt
  • app/src/main/java/com/itsaky/androidide/viewmodel/InstallationViewModel.kt
📝 Walkthrough

Walkthrough

The PR refactors the IDE setup flow to improve coroutine handling and state management. startIdeSetup() is migrated to execute initial storage and tool checks within a viewLifecycleScope coroutine, replacing direct returns with coroutine-scoped exits. The checkStorageAndNotify method becomes a suspending function, and the web server gains TrafficStats tagging for network monitoring.

Changes

Cohort / File(s) Summary
Coroutine Scope Migration
app/src/main/java/com/itsaky/androidide/fragments/onboarding/PermissionsFragment.kt, app/src/main/java/com/itsaky/androidide/viewmodel/InstallationViewModel.kt
Refactored startIdeSetup to run storage/tool checks inside coroutines with lifecycle awareness. Early termination now uses return@launch. checkStorageAndNotify changed to a suspend function executing on Dispatchers.IO, eliminating nested coroutine launches.
Network Monitoring
app/src/main/java/com/itsaky/androidide/localWebServer/WebServer.kt
Added TrafficStats tagging (0xC0DE) around web server socket initialization and binding with tag cleanup afterward.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • #941: Modifies coroutine control-flow in InstallationViewModel.startIdeSetup and touches WebServer code, overlapping with refactoring patterns in this PR.
  • #1004: Modifies PermissionsFragment and adjusts how startIdeSetup is invoked to prevent race conditions during onboarding.

Suggested reviewers

  • itsaky-adfa
  • jatezzz

Poem

🐰 Coroutines dance in lifecycle's embrace,
Storage checks now suspend with grace,
Traffic stats tag the network flow,
Setup orchestration—watch it go!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the main objective of the PR: fixing three StrictMode violations, matching the changeset which addresses disk I/O and socket tagging issues.
Description check ✅ Passed The description is directly related to the changeset, detailing all three StrictMode violation fixes with specific implementation approaches matching the file-level changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ADFA-3828-StrictMode-violations-install-pre-create

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@app/src/main/java/com/itsaky/androidide/fragments/onboarding/PermissionsFragment.kt`:
- Around line 242-245: The call to viewModel.isSetupComplete() is performing
disk I/O on the main thread; change the call site in PermissionsFragment (inside
the viewLifecycleScope.launch) to perform the check on IO by using
withContext(Dispatchers.IO) { viewModel.isSetupComplete() } (or make
viewModel.isSetupComplete()/checkToolsIsInstalled() suspend and call it from
IO), then act on the result on the main thread (calling (activity as?
OnboardingActivity)?.tryNavigateToMainIfSetupIsCompleted() only after the IO
call returns). Also search other call sites of isSetupComplete(),
isPolicyRespected, and onUserIllegallyRequestedNextPage and plan to convert them
similarly if they run on the main thread.

In `@app/src/main/java/com/itsaky/androidide/localWebServer/WebServer.kt`:
- Around line 138-141: The TrafficStats tag is being set only during
ServerSocket creation (TrafficStats.setThreadStatsTag(0xC0DE)) then cleared
before the accept loop, so accepted client sockets remain untagged and I/O will
still trigger UntaggedSocketViolation; also an exception during ServerSocket()
or bind() can leak the tag. Fix by ensuring the tag is active for the lifetime
of accept/IO: either keep the tag set across the accept loop, or (preferably)
clear it after setup in a finally block and explicitly tag/untag each accepted
client socket using TrafficStats.tagSocket(clientSocket) and
TrafficStats.untagSocket(clientSocket) around the per-client I/O, and wrap the
ServerSocket creation/bind in try...finally to always call
TrafficStats.clearThreadStatsTag() if you choose to set the thread tag for
setup.

In `@app/src/main/java/com/itsaky/androidide/viewmodel/InstallationViewModel.kt`:
- Around line 63-71: checkToolsIsInstalled() performs a filesystem stat on the
main dispatcher, causing StrictMode disk-read violations; wrap the call in
withContext(Dispatchers.IO) inside the existing viewModelScope.launch, e.g. call
val installed = withContext(Dispatchers.IO) { checkToolsIsInstalled() } and then
branch on installed to call _state.update { InstallationComplete } (or switch
back to Main if you prefer to keep state mutation on the main thread); apply the
same pattern where PermissionsFragment.startIdeSetup calls
viewModel.isSetupComplete() after checkStorageAndNotify to avoid main-thread
disk reads.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e830d0ad-d53e-47f0-a97d-f9bd81b27238

📥 Commits

Reviewing files that changed from the base of the PR and between 9726e17 and 92bf34f.

📒 Files selected for processing (3)
  • app/src/main/java/com/itsaky/androidide/fragments/onboarding/PermissionsFragment.kt
  • app/src/main/java/com/itsaky/androidide/localWebServer/WebServer.kt
  • app/src/main/java/com/itsaky/androidide/viewmodel/InstallationViewModel.kt

Comment on lines +242 to +245
if (viewModel.isSetupComplete()) {
(activity as? OnboardingActivity)?.tryNavigateToMainIfSetupIsCompleted()
return@launch
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

isSetupComplete() still performs disk I/O on the main thread.

viewLifecycleScope.launch defaults to Dispatchers.Main.immediate, so viewModel.isSetupComplete() here runs on the main thread. Internally it calls checkToolsIsInstalled(), which invokes Environment.ANDROID_HOME.exists() — a filesystem call that StrictMode's DiskReadViolation will catch just like the storage check this PR is fixing. Consider running this check on Dispatchers.IO (or making isSetupComplete()/checkToolsIsInstalled() suspend) so the same violation pattern doesn't reappear two lines below the one you just fixed.

♻️ Sketch of one approach
-			if (viewModel.isSetupComplete()) {
+			val setupComplete = withContext(Dispatchers.IO) { viewModel.isSetupComplete() }
+			if (setupComplete) {
 				(activity as? OnboardingActivity)?.tryNavigateToMainIfSetupIsCompleted()
 				return@launch
 			}

Note: isSetupComplete() is also called from isPolicyRespected (line 348) and onUserIllegallyRequestedNextPage (line 353) on the main thread; those call sites would also benefit from the same treatment, though they're outside this PR's scope.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (viewModel.isSetupComplete()) {
(activity as? OnboardingActivity)?.tryNavigateToMainIfSetupIsCompleted()
return@launch
}
val setupComplete = withContext(Dispatchers.IO) { viewModel.isSetupComplete() }
if (setupComplete) {
(activity as? OnboardingActivity)?.tryNavigateToMainIfSetupIsCompleted()
return@launch
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/itsaky/androidide/fragments/onboarding/PermissionsFragment.kt`
around lines 242 - 245, The call to viewModel.isSetupComplete() is performing
disk I/O on the main thread; change the call site in PermissionsFragment (inside
the viewLifecycleScope.launch) to perform the check on IO by using
withContext(Dispatchers.IO) { viewModel.isSetupComplete() } (or make
viewModel.isSetupComplete()/checkToolsIsInstalled() suspend and call it from
IO), then act on the result on the main thread (calling (activity as?
OnboardingActivity)?.tryNavigateToMainIfSetupIsCompleted() only after the IO
call returns). Also search other call sites of isSetupComplete(),
isPolicyRespected, and onUserIllegallyRequestedNextPage and plan to convert them
similarly if they run on the main thread.

Comment on lines +138 to +141
TrafficStats.setThreadStatsTag(0xC0DE)
serverSocket = ServerSocket().apply { reuseAddress = true }
serverSocket.bind(InetSocketAddress(config.bindName, config.port))
TrafficStats.clearThreadStatsTag()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Android TrafficStats setThreadStatsTag: are sockets returned by ServerSocket.accept() tagged with the current thread's stats tag at the time of accept, or only sockets created via new Socket() while the tag is set? Does clearing the thread tag before calling accept() leave accepted client sockets untagged and still trigger StrictMode UntaggedSocketViolation on read/write?

💡 Result:

Sockets returned by ServerSocket.accept are automatically tagged with the current thread's stats tag (set via TrafficStats.setThreadStatsTag) at the time of accept. This happens because the underlying libcore.io.BlockGuardOs.accept method explicitly calls tagSocket on the newly accepted FileDescriptor if it is an inet socket, using the thread's current tag. Sockets created via new Socket are similarly tagged automatically during their creation in BlockGuardOs.socket. Clearing the thread tag (via TrafficStats.clearThreadStatsTag) before calling accept will cause the accepted sockets to be untagged (tag == -1), which triggers StrictMode UntaggedSocketViolation on subsequent read/write operations, as the tagSocket call in accept uses the current (cleared/untagged) thread tag, and StrictMode checks for this during tagging.

Citations:


Tagging window defeats the fix for client sockets, and lacks exception safety.

Two actionable issues:

  1. Coverage. TrafficStats.setThreadStatsTag() tags sockets at the moment they are bound, connected, or accepted on the thread with the tag active. Here, you clear the tag at line 141—immediately before the accept loop (lines 144–217). Every Socket returned by serverSocket.accept() will be created with the tag already cleared, inheriting an untagged state (tag == -1). These client sockets are where the bulk of I/O happens (reads/writes on their InputStreams/OutputStreams), so UntaggedSocketViolation will still fire on those operations. The listening socket is tagged, but that is not where the violation typically occurs.
    Fix: Either (a) keep the tag set throughout the entire accept loop, (b) re-apply the tag before each accept(), or (c) explicitly tag each accepted socket with TrafficStats.tagSocket(clientSocket) and untag it when done.

  2. Exception safety. If ServerSocket() or bind() throws, line 141 never executes and the thread retains tag 0xC0DE, leaking it into any subsequent socket operations. Wrap the setup in try { ... } finally { TrafficStats.clearThreadStatsTag() }.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/itsaky/androidide/localWebServer/WebServer.kt` around
lines 138 - 141, The TrafficStats tag is being set only during ServerSocket
creation (TrafficStats.setThreadStatsTag(0xC0DE)) then cleared before the accept
loop, so accepted client sockets remain untagged and I/O will still trigger
UntaggedSocketViolation; also an exception during ServerSocket() or bind() can
leak the tag. Fix by ensuring the tag is active for the lifetime of accept/IO:
either keep the tag set across the accept loop, or (preferably) clear it after
setup in a finally block and explicitly tag/untag each accepted client socket
using TrafficStats.tagSocket(clientSocket) and
TrafficStats.untagSocket(clientSocket) around the per-client I/O, and wrap the
ServerSocket creation/bind in try...finally to always call
TrafficStats.clearThreadStatsTag() if you choose to set the thread tag for
setup.

return
}
if (viewModel.isSetupCompleteAsync()) {
(activity as? OnboardingActivity)?.tryNavigateToMainIfSetupIsCompleted()
Copy link
Copy Markdown
Contributor

@itsaky-adfa itsaky-adfa Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be a bit cautious here and wrap the startActivity part in tryNavigatetoToMainIfSetupIsCompleted in a withContext(Dispatchers.Main.immediate) { ... } here.

But as CodeRabbit suggested, tryNavigatetoToMainIfSetupIsCompleted may still perform disk IO when it calls isSetupComplete.

A better alternative would be to just do this :

if (isSetupCompleteAsync()) {
  (activity as? OnboardingActivity)?.apply {
    startActivity(Intent(this, MainActivity::class.java))
    finish()
  }
}

Or maybe OnboardingActivity can have a helper function to just do the navigation which can then be shared at both call sites.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants