Skip to content

ADFA-3829 Clear StrictMode violations on create-init flow#1277

Open
hal-eisen-adfa wants to merge 1 commit intostagefrom
ADFA-3829-StrictMode-violations-create-init
Open

ADFA-3829 Clear StrictMode violations on create-init flow#1277
hal-eisen-adfa wants to merge 1 commit intostagefrom
ADFA-3829-StrictMode-violations-create-init

Conversation

@hal-eisen-adfa
Copy link
Copy Markdown
Collaborator

Address 33 still-reproducible violations from the create-new-project log (the other 16 from legacy noActivity/emptyActivity/basicActivity templates were already removed by ADFA-3381).

Off-thread the heavy work:

  • TemplateListFragment.reloadTemplates: wrap ITemplateProvider.getInstance
    • getTemplates in withContext(Dispatchers.IO); pre-warm ITemplateWidgetViewProvider.getInstance so per-bind getInstance is a cache hit.
  • TemplateDetailsFragment.bindWithTemplate: invoke each parameter beforeCreateView on Dispatchers.IO before constructing the adapter so that getNewProjectName's File.exists loop runs off the UI thread.
  • Parameter.beforeCreateView: now one-shot per instance (reset on release) so the bind-time call is a no-op once pre-warmed.
  • TemplateWidgetViewProviderImpl.createTextField: per-keystroke ConstraintVerifier.verify (which stats the filesystem for EXISTS / FILE / DIRECTORY constraints) is now debounced and dispatched to IO via the view tree's lifecycleScope.
  • TemplateWidgetsListAdapter: lift ITemplateWidgetViewProvider.getInstance out of onBindViewHolder into the adapter's init.

Defer initialization:

  • JavaDebugAdapter.vmm: by lazy { Bootstrap.virtualMachineManager() } eliminates a 507ms NetworkViolation and 4 disk reads at field init.

Tag the JDWP listener socket:

  • ListenerState.startListening: wrap with TrafficStats.setThreadStatsTag / clearThreadStatsTag to clear UntaggedSocketViolation.

Whitelist vendor-framework call (not app-owned):

  • MIUI's NotificationManager.notify lazily inits EpFrameworkFactory via File.exists; rule + test added.

Address 33 still-reproducible violations from the create-new-project log
(the other 16 from legacy noActivity/emptyActivity/basicActivity templates
were already removed by ADFA-3381).

Off-thread the heavy work:
- TemplateListFragment.reloadTemplates: wrap ITemplateProvider.getInstance
  + getTemplates in withContext(Dispatchers.IO); pre-warm
  ITemplateWidgetViewProvider.getInstance so per-bind getInstance is a
  cache hit.
- TemplateDetailsFragment.bindWithTemplate: invoke each parameter
  beforeCreateView on Dispatchers.IO before constructing the adapter so
  that getNewProjectName's File.exists loop runs off the UI thread.
- Parameter.beforeCreateView: now one-shot per instance (reset on
  release) so the bind-time call is a no-op once pre-warmed.
- TemplateWidgetViewProviderImpl.createTextField: per-keystroke
  ConstraintVerifier.verify (which stats the filesystem for
  EXISTS / FILE / DIRECTORY constraints) is now debounced and dispatched
  to IO via the view tree's lifecycleScope.
- TemplateWidgetsListAdapter: lift ITemplateWidgetViewProvider.getInstance
  out of onBindViewHolder into the adapter's init.

Defer initialization:
- JavaDebugAdapter.vmm: by lazy { Bootstrap.virtualMachineManager() }
  eliminates a 507ms NetworkViolation and 4 disk reads at field init.

Tag the JDWP listener socket:
- ListenerState.startListening: wrap with TrafficStats.setThreadStatsTag
  / clearThreadStatsTag to clear UntaggedSocketViolation.

Whitelist vendor-framework call (not app-owned):
- MIUI's NotificationManager.notify lazily inits EpFrameworkFactory via
  File.exists; rule + test added.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

📝 Walkthrough

Release Notes

Performance & Thread Safety Improvements:

  • Off-threaded template loading and parameter initialization to IO dispatcher in TemplateListFragment and TemplateDetailsFragment, reducing main thread blocking during the create-new-project flow
  • Lazy-initialized JDI VirtualMachineManager in JavaDebugAdapter to defer expensive initialization (507ms network call + 4 disk reads) until first use
  • Debounced constraint validation (150ms) with async IO processing in TemplateWidgetViewProviderImpl to reduce per-keystroke filesystem operations
  • Moved ITemplateWidgetViewProvider instance creation out of adapter bind loop in TemplateWidgetsListAdapter to eliminate redundant initialization
  • Pre-warmed ITemplateWidgetViewProvider before adapter binding in TemplateListFragment to ensure subsequent bind-time getInstance calls are cache hits

Strict Mode Compliance:

  • Added thread stats tagging in ListenerState.startListening for proper socket accounting
  • Implemented one-shot execution guarantee for Parameter.beforeCreateView to prevent duplicate off-thread operations
  • Whitelisted vendor-specific MIUI NotificationManager.notify disk read path (EpFrameworkFactory lazy initialization)

Risks & Best Practices Considerations:

  • Lazy initialization of Bootstrap.virtualMachineManager() defers potential initialization failures until first debug session; ensure adequate error handling in debug flow
  • Debounced validation in TextFieldWidget may delay error feedback to users; 150ms debounce should provide acceptable UX
  • Async parameter initialization uses lifecycleScope correctly, but ensure all parameter plugins properly implement beforeCreateView callbacks with IO-safety
  • One-shot parameter.beforeCreateView execution requires proper reset() calls on parameter reuse; review parameter lifecycle management to prevent stale state
  • Whitelist rule for MIUI EpFrameworkFactory is vendor-specific; monitor for similar patterns in other device manufacturers

Walkthrough

This PR introduces asynchronous template widget initialization with debounced input validation, adds a strict-mode whitelist rule for MIUI enterprise JAR checks, and applies performance optimizations to debug initialization and network traffic accounting.

Changes

Template Async Initialization & Debounced Validation

Layer / File(s) Summary
API Contracts
templates-api/src/main/java/com/itsaky/androidide/templates/parameters.kt
Parameter.beforeCreateView() now enforces one-time invocation via a beforeCreateViewInvoked flag; release() resets this state to prevent reuse artifacts.
Core Validation Behavior
templates-impl/src/main/java/com/itsaky/androidide/templates/impl/TemplateWidgetViewProviderImpl.kt
TextFieldWidget text changes are now debounced (150ms) and validated asynchronously on Dispatchers.IO, with fallback to synchronous validation when no lifecycle owner is available.
Adapter Caching
app/src/main/java/com/itsaky/androidide/adapters/TemplateWidgetsListAdapter.kt
TemplateWidgetsListAdapter caches the ITemplateWidgetViewProvider singleton as a class property to avoid repeated lookups during view creation.
Fragment Pre-Creation Hooks
app/src/main/java/com/itsaky/androidide/fragments/TemplateDetailsFragment.kt
bindWithTemplate launches a lifecycle-scoped coroutine that invokes each ParameterWidget.beforeCreateView() on Dispatchers.IO before attaching the adapter.
Fragment Async Loading & Pre-warming
app/src/main/java/com/itsaky/androidide/fragments/TemplateListFragment.kt
reloadTemplates() now runs template loading and warning collection on Dispatchers.IO, pre-warms ITemplateWidgetViewProvider to avoid disk reads during binding, then updates UI on the main thread with guards to ensure binding still exists.

Strict Mode MIUI Enterprise Whitelist

Layer / File(s) Summary
Whitelist Rule
app/src/main/java/com/itsaky/androidide/app/strictmode/WhitelistEngine.kt
Adds a new DiskReadViolation whitelist rule that permits violations on MIUI devices when the stack traces through java.io.File.exists()miui.enterprise.EpFrameworkFactory.isEnterpriseJarExists()EpFrameworkFactory.get() during notification initialization.
Test Coverage
app/src/androidTest/kotlin/com/itsaky/androidide/app/strictmode/WhitelistRulesTest.kt
New test allow_DiskRead_on_MiuiEpFrameworkFactoryIsEnterpriseJarExists asserts that the enterprise JAR existence check stack trace is allowed.

Performance Optimizations

Layer / File(s) Summary
Debug VM Lazy Init
lsp/java/src/main/java/com/itsaky/androidide/lsp/java/debug/JavaDebugAdapter.kt
virtualMachineManager is now lazily initialized instead of eagerly created at construction, deferring JDI setup until first use.
JDWP Listener Instrumentation
lsp/java/src/main/java/com/itsaky/androidide/lsp/java/debug/ListenerState.kt
connector.startListening() is now wrapped with TrafficStats.setThreadStatsTag() and clearThreadStatsTag() to enable traffic-stats accounting for the JDWP listener socket.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • appdevforall/CodeOnTheGo#803: Adds a related DiskReadViolation whitelist rule for a different vendor-specific stack trace (Oplus UIFirst) in the same WhitelistEngine and WhitelistRulesTest files.
  • appdevforall/CodeOnTheGo#1147: Modifies TemplateListFragment and TemplateDetailsFragment template UI loading logic, overlapping with the async initialization refactoring in this PR.
  • appdevforall/CodeOnTheGo#1259: Adds vendor-specific StrictMode whitelist rules and tests in WhitelistEngine and WhitelistRulesTest, following the same pattern as the MIUI enterprise JAR whitelist in this PR.

Suggested reviewers

  • itsaky-adfa
  • jomen-adfa
  • jatezzz

Poem

🐰 Widgets wake to async calls,
Validation waits with patient grace,
MIUI's jars now have their place,
Debug sleeps till summoned slow,
Threads and coroutines steal the show!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.43% 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 'ADFA-3829 Clear StrictMode violations on create-init flow' clearly and concisely summarizes the main change: addressing StrictMode violations during project creation, which matches the primary focus of all the code changes across multiple files.
Description check ✅ Passed The description comprehensively explains the changes across all modified files, detailing specific optimizations (off-threading work, deferred initialization, socket tagging) and their rationale for clearing StrictMode violations.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ADFA-3829-StrictMode-violations-create-init

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: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/java/com/itsaky/androidide/app/strictmode/WhitelistEngine.kt`:
- Around line 256-260: The current matchAdjacentFrames invocation that looks for
classAndMethod("java.io.File","exists") followed by the three MIUI frames should
be narrowed to require the downstream NotificationManager call path as well;
update the matcher that builds this rule (the matchAdjacentFrames call) to also
require, in order, a classAndMethod("android.app.NotificationManager","notify")
or classAndMethod("android.app.NotificationManager","notifyAsUser") frame after
(or before, as appropriate to call order) the MIUI EpFrameworkFactory frames so
the rule only triggers when the MIUI chain is part of a notification delivery
path.

In
`@app/src/main/java/com/itsaky/androidide/fragments/TemplateDetailsFragment.kt`:
- Around line 173-183: The coroutine launched in
viewLifecycleOwner.lifecycleScope.launch can overlap older runs and assign the
wrong adapter; add a Job field (e.g., widgetsBindJob) on the fragment, cancel
widgetsBindJob?.cancel() before starting a new
viewLifecycleOwner.lifecycleScope.launch, store the returned Job in
widgetsBindJob, do the Dispatchers.IO work (template.widgets.forEach /
ParameterWidget.beforeCreateView) inside that coroutine, and only after the job
is the active one assign binding.widgets.adapter =
TemplateWidgetsListAdapter(template.widgets) (also keep the existing _binding
null-check) so stale/cancelled jobs cannot overwrite the adapter.

In `@app/src/main/java/com/itsaky/androidide/fragments/TemplateListFragment.kt`:
- Around line 125-166: The reloadTemplates coroutine can run multiple concurrent
jobs and older ones may overwrite newer UI state; add a cancellable Job property
(e.g., private var reloadJob: Job? = null) on TemplateListFragment, cancel any
existing reloadJob before starting a new
viewLifecycleOwner.lifecycleScope.launch in reloadTemplates, then assign the
launched Job to reloadJob so only the latest job updates
adapter/binding/warnings; keep the existing _binding check and UI update logic
unchanged.

In
`@lsp/java/src/main/java/com/itsaky/androidide/lsp/java/debug/ListenerState.kt`:
- Around line 48-53: Preserve and restore any existing thread stats tag around
the JDWP listener setup: before calling
TrafficStats.setThreadStatsTag(JDWP_LISTENER_SOCKET_TAG) read and save the
current tag with TrafficStats.getThreadStatsTag(), then call
TrafficStats.setThreadStatsTag(...), run connector.startListening(args) and in
the finally block restore the saved tag by calling
TrafficStats.setThreadStatsTag(savedTag) instead of
TrafficStats.clearThreadStatsTag(); reference the
TrafficStats.getThreadStatsTag, TrafficStats.setThreadStatsTag,
TrafficStats.clearThreadStatsTag calls and the connector.startListening(args)
invocation in ListenerState.kt to implement this change.

In `@templates-api/src/main/java/com/itsaky/androidide/templates/parameters.kt`:
- Around line 189-203: The one-shot guard in beforeCreateView currently does a
non-atomic check/set on beforeCreateViewInvoked allowing races; make the
check-and-set atomic (e.g., replace the boolean with an AtomicBoolean and use
compareAndSet, or protect the check/set and invocation with a synchronized
block) so that beforeCreateViewInvoked is set and actionBeforeCreateView is
invoked exactly once across threads; update the beforeCreateView method to
atomically test-and-set before invoking actionBeforeCreateView(this).

In
`@templates-impl/src/main/java/com/itsaky/androidide/templates/impl/TemplateWidgetViewProviderImpl.kt`:
- Around line 179-190: The validation branch toggles root.isErrorEnabled and
only sets root.error when err != null, leaving stale error text when validation
passes; in both the lifecycle-owner branch and the fallback
(ConstraintVerifier.verify) branch inside TemplateWidgetViewProviderImpl
(references: root, root.isErrorEnabled, root.error,
ConstraintVerifier.verify(value, constraints = param.constraints), value,
param.constraints) ensure that when err == null you explicitly clear the error
by setting root.error = null and set root.isErrorEnabled = false so no stale
error text remains after successful validation.
🪄 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: c0c09bb1-f570-4b8d-9de5-78aa7da5a6c0

📥 Commits

Reviewing files that changed from the base of the PR and between 69e03bf and f8a133b.

📒 Files selected for processing (9)
  • app/src/androidTest/kotlin/com/itsaky/androidide/app/strictmode/WhitelistRulesTest.kt
  • app/src/main/java/com/itsaky/androidide/adapters/TemplateWidgetsListAdapter.kt
  • app/src/main/java/com/itsaky/androidide/app/strictmode/WhitelistEngine.kt
  • app/src/main/java/com/itsaky/androidide/fragments/TemplateDetailsFragment.kt
  • app/src/main/java/com/itsaky/androidide/fragments/TemplateListFragment.kt
  • lsp/java/src/main/java/com/itsaky/androidide/lsp/java/debug/JavaDebugAdapter.kt
  • lsp/java/src/main/java/com/itsaky/androidide/lsp/java/debug/ListenerState.kt
  • templates-api/src/main/java/com/itsaky/androidide/templates/parameters.kt
  • templates-impl/src/main/java/com/itsaky/androidide/templates/impl/TemplateWidgetViewProviderImpl.kt

Comment on lines +256 to +260
matchAdjacentFrames(
classAndMethod("java.io.File", "exists"),
classAndMethod("miui.enterprise.EpFrameworkFactory", "isEnterpriseJarExists"),
classAndMethod("miui.enterprise.EpFrameworkFactory", "get"),
)
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 | ⚡ Quick win

Tighten this rule to the NotificationManager call path.

Right now this matcher allows any stack containing the 3-frame MIUI chain, even if it is not triggered from notification delivery. Narrow it by also requiring the downstream NotificationManager.notify/notifyAsUser anchor in order.

Suggested narrowing
-				matchAdjacentFrames(
-					classAndMethod("java.io.File", "exists"),
-					classAndMethod("miui.enterprise.EpFrameworkFactory", "isEnterpriseJarExists"),
-					classAndMethod("miui.enterprise.EpFrameworkFactory", "get"),
-				)
+				matchAdjacentFramesInOrder(
+					listOf(
+						listOf(
+							classAndMethod("java.io.File", "exists"),
+							classAndMethod("miui.enterprise.EpFrameworkFactory", "isEnterpriseJarExists"),
+							classAndMethod("miui.enterprise.EpFrameworkFactory", "get"),
+						),
+						listOf(
+							anyOf(
+								classAndMethod("android.app.NotificationManager", "notifyAsUser"),
+								classAndMethod("android.app.NotificationManager", "notify"),
+							),
+						),
+					),
+				)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/itsaky/androidide/app/strictmode/WhitelistEngine.kt`
around lines 256 - 260, The current matchAdjacentFrames invocation that looks
for classAndMethod("java.io.File","exists") followed by the three MIUI frames
should be narrowed to require the downstream NotificationManager call path as
well; update the matcher that builds this rule (the matchAdjacentFrames call) to
also require, in order, a
classAndMethod("android.app.NotificationManager","notify") or
classAndMethod("android.app.NotificationManager","notifyAsUser") frame after (or
before, as appropriate to call order) the MIUI EpFrameworkFactory frames so the
rule only triggers when the MIUI chain is part of a notification delivery path.

Comment on lines +173 to +183
viewLifecycleOwner.lifecycleScope.launch {
withContext(Dispatchers.IO) {
template.widgets.forEach { widget ->
if (widget is ParameterWidget<*>) {
widget.parameter.beforeCreateView()
}
}
}
_binding ?: return@launch
binding.widgets.adapter = TemplateWidgetsListAdapter(template.widgets)
}
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 | ⚡ Quick win

Prevent stale adapter assignment from overlapping bind jobs

Line 173 launches a new coroutine per template update, but previous jobs are not canceled. A slower old job can complete later and assign widgets for the wrong template.

Suggested fix
+import kotlinx.coroutines.Job
...
 class TemplateDetailsFragment : ... {
+    private var bindTemplateJob: Job? = null
...
     private fun bindWithTemplate(template: Template<*>?) {
         template ?: return
         binding.title.text = template.templateNameStr

-        viewLifecycleOwner.lifecycleScope.launch {
+        bindTemplateJob?.cancel()
+        bindTemplateJob = viewLifecycleOwner.lifecycleScope.launch {
             withContext(Dispatchers.IO) {
                 template.widgets.forEach { widget ->
                     if (widget is ParameterWidget<*>) {
                         widget.parameter.beforeCreateView()
                     }
                 }
             }
             _binding ?: return@launch
             binding.widgets.adapter = TemplateWidgetsListAdapter(template.widgets)
         }
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/itsaky/androidide/fragments/TemplateDetailsFragment.kt`
around lines 173 - 183, The coroutine launched in
viewLifecycleOwner.lifecycleScope.launch can overlap older runs and assign the
wrong adapter; add a Job field (e.g., widgetsBindJob) on the fragment, cancel
widgetsBindJob?.cancel() before starting a new
viewLifecycleOwner.lifecycleScope.launch, store the returned Job in
widgetsBindJob, do the Dispatchers.IO work (template.widgets.forEach /
ParameterWidget.beforeCreateView) inside that coroutine, and only after the job
is the active one assign binding.widgets.adapter =
TemplateWidgetsListAdapter(template.widgets) (also keep the existing _binding
null-check) so stale/cancelled jobs cannot overwrite the adapter.

Comment on lines +125 to +166
viewLifecycleOwner.lifecycleScope.launch {
val (templates, warnings) = withContext(Dispatchers.IO) {
val provider = ITemplateProvider.getInstance(reload = true)
// Pre-warm the widget view provider so per-bind getInstance() in
// TemplateWidgetsListAdapter doesn't trigger a disk read on the UI thread.
ITemplateWidgetViewProvider.getInstance()
val templates = provider.getTemplates().filterIsInstance<ProjectTemplate>()
val warnings = (provider as? TemplateProviderImpl)?.warnings.orEmpty()
templates to warnings
}

if (warnings.isNotEmpty()) {
requireActivity().flashError(
warnings.joinToString(System.lineSeparator()) { w ->
requireContext().getString(w.resId, *w.args.toTypedArray())
}
)
_binding ?: return@launch

adapter =
TemplateListAdapter(
templates = templates,
onClick = { template, _ ->
viewModel.template.value = template
viewModel.setScreen(MainViewModel.SCREEN_TEMPLATE_DETAILS)
},
onLongClick = { template, itemView ->
template.tooltipTag?.let { tag ->
TooltipManager.showIdeCategoryTooltip(
context = requireContext(),
anchorView = itemView,
tag = tag
)
}
},
)
binding.list.adapter = adapter
updateSpanCount()

if (warnings.isNotEmpty()) {
requireActivity().flashError(
warnings.joinToString(System.lineSeparator()) { w ->
requireContext().getString(w.resId, *w.args.toTypedArray())
}
)
}
}
}
}
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 | ⚡ Quick win

Cancel previous reload coroutine to avoid out-of-order UI updates

Line 125 starts a new reload every time reloadTemplates() is called, but prior jobs remain active. Older jobs can finish after newer ones and overwrite the list/warnings with stale data.

Suggested fix
+import kotlinx.coroutines.Job
...
 class TemplateListFragment : ... {
+    private var reloadJob: Job? = null
...
     private fun reloadTemplates() {
         _binding ?: return
         log.debug("Reloading templates...")

-        viewLifecycleOwner.lifecycleScope.launch {
+        reloadJob?.cancel()
+        reloadJob = viewLifecycleOwner.lifecycleScope.launch {
             val (templates, warnings) = withContext(Dispatchers.IO) {
                 ...
             }
             _binding ?: return@launch
             ...
         }
     }
 }
📝 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
viewLifecycleOwner.lifecycleScope.launch {
val (templates, warnings) = withContext(Dispatchers.IO) {
val provider = ITemplateProvider.getInstance(reload = true)
// Pre-warm the widget view provider so per-bind getInstance() in
// TemplateWidgetsListAdapter doesn't trigger a disk read on the UI thread.
ITemplateWidgetViewProvider.getInstance()
val templates = provider.getTemplates().filterIsInstance<ProjectTemplate>()
val warnings = (provider as? TemplateProviderImpl)?.warnings.orEmpty()
templates to warnings
}
if (warnings.isNotEmpty()) {
requireActivity().flashError(
warnings.joinToString(System.lineSeparator()) { w ->
requireContext().getString(w.resId, *w.args.toTypedArray())
}
)
_binding ?: return@launch
adapter =
TemplateListAdapter(
templates = templates,
onClick = { template, _ ->
viewModel.template.value = template
viewModel.setScreen(MainViewModel.SCREEN_TEMPLATE_DETAILS)
},
onLongClick = { template, itemView ->
template.tooltipTag?.let { tag ->
TooltipManager.showIdeCategoryTooltip(
context = requireContext(),
anchorView = itemView,
tag = tag
)
}
},
)
binding.list.adapter = adapter
updateSpanCount()
if (warnings.isNotEmpty()) {
requireActivity().flashError(
warnings.joinToString(System.lineSeparator()) { w ->
requireContext().getString(w.resId, *w.args.toTypedArray())
}
)
}
}
}
}
reloadJob?.cancel()
reloadJob = viewLifecycleOwner.lifecycleScope.launch {
val (templates, warnings) = withContext(Dispatchers.IO) {
val provider = ITemplateProvider.getInstance(reload = true)
// Pre-warm the widget view provider so per-bind getInstance() in
// TemplateWidgetsListAdapter doesn't trigger a disk read on the UI thread.
ITemplateWidgetViewProvider.getInstance()
val templates = provider.getTemplates().filterIsInstance<ProjectTemplate>()
val warnings = (provider as? TemplateProviderImpl)?.warnings.orEmpty()
templates to warnings
}
_binding ?: return@launch
adapter =
TemplateListAdapter(
templates = templates,
onClick = { template, _ ->
viewModel.template.value = template
viewModel.setScreen(MainViewModel.SCREEN_TEMPLATE_DETAILS)
},
onLongClick = { template, itemView ->
template.tooltipTag?.let { tag ->
TooltipManager.showIdeCategoryTooltip(
context = requireContext(),
anchorView = itemView,
tag = tag
)
}
},
)
binding.list.adapter = adapter
updateSpanCount()
if (warnings.isNotEmpty()) {
requireActivity().flashError(
warnings.joinToString(System.lineSeparator()) { w ->
requireContext().getString(w.resId, *w.args.toTypedArray())
}
)
}
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/itsaky/androidide/fragments/TemplateListFragment.kt`
around lines 125 - 166, The reloadTemplates coroutine can run multiple
concurrent jobs and older ones may overwrite newer UI state; add a cancellable
Job property (e.g., private var reloadJob: Job? = null) on TemplateListFragment,
cancel any existing reloadJob before starting a new
viewLifecycleOwner.lifecycleScope.launch in reloadTemplates, then assign the
launched Job to reloadJob so only the latest job updates
adapter/binding/warnings; keep the existing _binding check and UI update logic
unchanged.

Comment on lines +48 to +53
TrafficStats.setThreadStatsTag(JDWP_LISTENER_SOCKET_TAG)
val address = try {
connector.startListening(args)
} finally {
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 | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Android TrafficStats getThreadStatsTag clearThreadStatsTag API

💡 Result:

The Android TrafficStats class provides methods for per-thread network traffic tagging to track socket traffic statistics. getThreadStatsTag: Returns the active tag (int) used for accounting Socket traffic from the current thread. Only one tag per thread is supported. Added in API level 14 [1][2]. clearThreadStatsTag: Clears the active thread stats tag for the current thread. Added in API level 8 [1][2][3]. These work with setThreadStatsTag(int tag) to set the tag (API level 8) [1][2], and tagSocket(Socket) to apply it to sockets. Tags help profile app network usage by category [4][5]. Example usage for HTTP requests [5]: TrafficStats.setThreadStatsTag(0xF00D); try { // Make network request } finally { TrafficStats.clearThreadStatsTag(); } Official documentation confirms these are stable APIs as of Android 15+ (current as of 2026) [1][2].

Citations:


🌐 Web query:

TrafficStats getThreadStatsTag() return value no tag -1

💡 Result:

In Android's TrafficStats API, getThreadStatsTag returns the active thread stats tag used for accounting socket traffic from the current thread. The return value of -1 indicates no tag is set for the thread, as this is the default initial value in the ThreadLocal storage (UidTag.tag = -1).[1][2] This differs from TrafficStats.UNSUPPORTED (-1), which is returned by network statistic methods when the device does not support the requested statistic.[3][4][5][6] Official docs describe getThreadStatsTag as returning the "active tag" without specifying -1 explicitly as "no tag," but source code confirms -1 as the unset/default state, and developers use clearThreadStatsTag to explicitly reset to this state.[4][1]

Citations:


Preserve and restore any pre-existing thread stats tag

clearThreadStatsTag() always resets to the unset state (−1), which erases any existing tag on pooled threads. Save the previous tag via getThreadStatsTag() and restore it in the finally block to prevent cross-call traffic attribution drift.

💡 Suggested patch
 fun startListening(): String {
-	TrafficStats.setThreadStatsTag(JDWP_LISTENER_SOCKET_TAG)
+	val previousTag = TrafficStats.getThreadStatsTag()
+	TrafficStats.setThreadStatsTag(JDWP_LISTENER_SOCKET_TAG)
 	val address = try {
 		connector.startListening(args)
 	} finally {
-		TrafficStats.clearThreadStatsTag()
+		if (previousTag == -1) {
+			TrafficStats.clearThreadStatsTag()
+		} else {
+			TrafficStats.setThreadStatsTag(previousTag)
+		}
 	}
 	listenAddress = address
 	return address
 }
📝 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
TrafficStats.setThreadStatsTag(JDWP_LISTENER_SOCKET_TAG)
val address = try {
connector.startListening(args)
} finally {
TrafficStats.clearThreadStatsTag()
}
fun startListening(): String {
val previousTag = TrafficStats.getThreadStatsTag()
TrafficStats.setThreadStatsTag(JDWP_LISTENER_SOCKET_TAG)
val address = try {
connector.startListening(args)
} finally {
if (previousTag == -1) {
TrafficStats.clearThreadStatsTag()
} else {
TrafficStats.setThreadStatsTag(previousTag)
}
}
listenAddress = address
return address
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lsp/java/src/main/java/com/itsaky/androidide/lsp/java/debug/ListenerState.kt`
around lines 48 - 53, Preserve and restore any existing thread stats tag around
the JDWP listener setup: before calling
TrafficStats.setThreadStatsTag(JDWP_LISTENER_SOCKET_TAG) read and save the
current tag with TrafficStats.getThreadStatsTag(), then call
TrafficStats.setThreadStatsTag(...), run connector.startListening(args) and in
the finally block restore the saved tag by calling
TrafficStats.setThreadStatsTag(savedTag) instead of
TrafficStats.clearThreadStatsTag(); reference the
TrafficStats.getThreadStatsTag, TrafficStats.setThreadStatsTag,
TrafficStats.clearThreadStatsTag calls and the connector.startListening(args)
invocation in ListenerState.kt to implement this change.

Comment on lines +189 to 203
private var beforeCreateViewInvoked = false

/**
* Called before the layout for this widget is created.
* Called before the layout for this widget is created. The action registered via
* [doBeforeCreateView] is invoked at most once per parameter instance — callers
* may pre-invoke this off the UI thread (e.g. before binding a RecyclerView) so
* that the bind-time call is a no-op and avoids triggering disk reads on the
* main thread.
*/
open fun beforeCreateView() {
if (beforeCreateViewInvoked) {
return
}
beforeCreateViewInvoked = true
this.actionBeforeCreateView?.invoke(this)
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 | ⚡ Quick win

Guard one-shot beforeCreateView() with synchronization

Line 199-Line 203 performs a non-atomic check/set on beforeCreateViewInvoked. Concurrent callers can still execute the action twice, violating the one-shot guarantee.

Suggested fix
-    private var beforeCreateViewInvoked = false
+    private var beforeCreateViewInvoked = false

     open fun beforeCreateView() {
-        if (beforeCreateViewInvoked) {
-            return
-        }
-        beforeCreateViewInvoked = true
-        this.actionBeforeCreateView?.invoke(this)
+        val action = lock.withLock {
+            if (beforeCreateViewInvoked) return
+            beforeCreateViewInvoked = true
+            actionBeforeCreateView
+        }
+        action?.invoke(this)
     }
📝 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
private var beforeCreateViewInvoked = false
/**
* Called before the layout for this widget is created.
* Called before the layout for this widget is created. The action registered via
* [doBeforeCreateView] is invoked at most once per parameter instance — callers
* may pre-invoke this off the UI thread (e.g. before binding a RecyclerView) so
* that the bind-time call is a no-op and avoids triggering disk reads on the
* main thread.
*/
open fun beforeCreateView() {
if (beforeCreateViewInvoked) {
return
}
beforeCreateViewInvoked = true
this.actionBeforeCreateView?.invoke(this)
private var beforeCreateViewInvoked = false
/**
* Called before the layout for this widget is created. The action registered via
* [doBeforeCreateView] is invoked at most once per parameter instance — callers
* may pre-invoke this off the UI thread (e.g. before binding a RecyclerView) so
* that the bind-time call is a no-op and avoids triggering disk reads on the
* main thread.
*/
open fun beforeCreateView() {
val action = lock.withLock {
if (beforeCreateViewInvoked) return
beforeCreateViewInvoked = true
actionBeforeCreateView
}
action?.invoke(this)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@templates-api/src/main/java/com/itsaky/androidide/templates/parameters.kt`
around lines 189 - 203, The one-shot guard in beforeCreateView currently does a
non-atomic check/set on beforeCreateViewInvoked allowing races; make the
check-and-set atomic (e.g., replace the boolean with an AtomicBoolean and use
compareAndSet, or protect the check/set and invocation with a synchronized
block) so that beforeCreateViewInvoked is set and actionBeforeCreateView is
invoked exactly once across threads; update the beforeCreateView method to
atomically test-and-set before invoking actionBeforeCreateView(this).

Comment on lines +179 to +190
root.isErrorEnabled = err != null
if (err != null) {
root.error = err
}
} ?: run {
// Fall back to synchronous validation when no lifecycle owner is attached
// (e.g., previews/tests). Production attaches a fragment lifecycle.
val err = ConstraintVerifier.verify(value, constraints = param.constraints)
root.isErrorEnabled = err != null
if (err != null) {
root.error = err
}
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 | ⚡ Quick win

Clear error text when validation passes

At Line 179-Line 190, isErrorEnabled is toggled, but root.error is only set on failure. Clear it on success to avoid stale error state.

Suggested fix
-          root.isErrorEnabled = err != null
-          if (err != null) {
-            root.error = err
-          }
+          root.isErrorEnabled = err != null
+          root.error = err
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@templates-impl/src/main/java/com/itsaky/androidide/templates/impl/TemplateWidgetViewProviderImpl.kt`
around lines 179 - 190, The validation branch toggles root.isErrorEnabled and
only sets root.error when err != null, leaving stale error text when validation
passes; in both the lifecycle-owner branch and the fallback
(ConstraintVerifier.verify) branch inside TemplateWidgetViewProviderImpl
(references: root, root.isErrorEnabled, root.error,
ConstraintVerifier.verify(value, constraints = param.constraints), value,
param.constraints) ensure that when err == null you explicitly clear the error
by setting root.error = null and set root.isErrorEnabled = false so no stale
error text remains after successful validation.

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.

1 participant