Skip to content

Refactor: state-aware ViewModel and app flow #37

Merged
TheZ3ro merged 17 commits into
osservatorionessuno:mainfrom
rocodes:states
Sep 29, 2025
Merged

Refactor: state-aware ViewModel and app flow #37
TheZ3ro merged 17 commits into
osservatorionessuno:mainfrom
rocodes:states

Conversation

@rocodes
Copy link
Copy Markdown
Contributor

@rocodes rocodes commented Sep 15, 2025

Ready for testing / review (cc @TheZ3ro).
It's....kind of a lot?

  • Define AppState and refactor Slideshow to avoid individual pages with individual logic, instead having a single SlideshowPage that displays the right content for a given state.
  • State logic (requisites, forward/back) are all controlled in the (new) ConfigurationViewModel.
  • In addition to the static checks it does in getState(), the viewmodel can also observe other components and be responsive to their changes: right now, there is one for adb connectivity status and one rough one for wifi connectivity status -- so for example disabling wifi mid-flow doesn't require a check on every page, it will emit the NeedsWifi state and the app will show the slideshow starting at wifi (and skipping any states it doesn't need again).
  • AdbViewModel is no longer a viewmodel. AdbManager.java was ported to Kotlin, and followup work can condense/combine some of the adb connector classes as @TheZ3ro suggested, but already this was getting too big so I mostly left them alone, except for replacing individual mutable data objects (isPair, isConnected, etc) with an AdbState stateflow. But there are a ton of adb service bootstrapping classes that can probably be cut down a lot.
  • Moves pairing service management into AdbManager (eventual preparation for consolidating adb related handling classes)

Still todo:

  • The autoconnect logic needs some further looking. If autoconnect fails the app requires re-pairing, which may be a regression compared to previous behaviour.
  • Tests (unit/integration): WIP, didn't want to block on that.

Other notes:
Maybe controversially, I decided on the configViewModel as a singleton scoped to the application's lifetime. My reasoning is that we don't want multiple instances of it either (we want one adbManager / adb instance, one wifi connectivity listener, etc), and we don't actually want to bake in view-specific functions (i.e. it's not an onButtonPressed handler bound to any specific screen), and we want one source of truth for the overall appstate. I think this is fine for our purposes, but it does mean that if we wanted screen-specific viewmodels, they should be a separate viewmodel object, and there should be no references to short-term ui components held by the configurationviewmodel, to avoid memory leaks. This is documented in comments.

Fixes #23
Fixes #11
Refs #12

@lsd-cat
Copy link
Copy Markdown
Member

lsd-cat commented Sep 15, 2025

Ci fails is my fault, hopefully improvements will come up in the next few days. This looks great and so much! I think we will benefit a lot from a more solid design here in the long term, and it will probably solve a lot of edge cases we have not even experienced yet! Thanks so much and looks forward to look into more detail and test it.

@rocodes rocodes mentioned this pull request Sep 15, 2025
@lsd-cat lsd-cat closed this Sep 16, 2025
@lsd-cat lsd-cat reopened this Sep 16, 2025
@lsd-cat
Copy link
Copy Markdown
Member

lsd-cat commented Sep 16, 2025

Sorry for the closing/reopening, I tried to trigger the updated CI, but Github always runs the workflow in the branch, so you'd have to rebase with the new one to see it hopefully passing. Thanks for all the great work!

Comment thread app/src/main/java/org/osservatorionessuno/bugbane/MainActivity.kt Outdated
…appstate.

Add ConfigurationViewModel to manage appstate flow.

Use ConfigurationViewModel in MainActivity and launch slideshow
only if state is not adbConnected.

Convert AdbViewModel to AdbManager class.
… for all permission pages (WifiPage, FinalPage, etc)
…onnectedFinishOnboarding state and move preferences checks to slideshowmanager
@rocodes
Copy link
Copy Markdown
Contributor Author

rocodes commented Sep 16, 2025

I noticed there's a regression in the pairing flow so I'm going to put this back in draft mode while I fix it :( It looks like I got a little greedy with the autoconnect part.

@rocodes rocodes marked this pull request as draft September 16, 2025 18:21
@rocodes rocodes force-pushed the states branch 2 times, most recently from 868c03a to dab2009 Compare September 19, 2025 19:45
@rocodes
Copy link
Copy Markdown
Contributor Author

rocodes commented Sep 19, 2025

ok @TheZ3ro I think I'm ready for you now :)

@rocodes rocodes marked this pull request as ready for review September 19, 2025 19:45
AppState.NeedWirelessDebuggingAndPair, AppState.TryAutoConnect, AppState.AdbConnecting -> return SlideshowPageData(title = stringResource(R.string.slideshow_wireless_title),
description = stringResource(R.string.slideshow_wireless_description),
icon = Icons.Filled.Build,
buttonText = if (state == AppState.NeedWirelessDebuggingAndPair) stringResource(R.string.slideshow_wireless_button) else stringResource(R.string.notification_adb_pairing_working_title),
Copy link
Copy Markdown
Contributor Author

@rocodes rocodes Sep 19, 2025

Choose a reason for hiding this comment

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

the notification_adb_pairing_working_title isn't perfect (it should be something like "trying to connect to adb"), but just used this for now to avoid having a ton of very similar strings to translate.

text = when (appState.value) {
AppState.AdbScanning -> stringResource(R.string.home_scanning_button)
AppState.AdbConnected -> stringResource(R.string.home_scan_button)
AppState.TryAutoConnect, AppState.AdbConnecting -> stringResource(R.string.notification_adb_pairing_working_title)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

(as with above, notification_adb_pairing_working_title could be improved on here to ~ "trying to connect to adb" but just tried to reuse existing strings and avoid more translation burden until things are more finalized)

Comment thread app/src/main/java/org/osservatorionessuno/bugbane/utils/ConfigurationViewModel.kt Outdated
@TheZ3ro
Copy link
Copy Markdown
Member

TheZ3ro commented Sep 21, 2025

Apparently I've entered a state where I successfully pair with ADB but the state is not updated thus it is asking to perform another pairing. This happens both on a phisical device Pixel 4a and an emulator.

I'm trying to debug what is happening

image

@TheZ3ro
Copy link
Copy Markdown
Member

TheZ3ro commented Sep 21, 2025

Ok wow I've understood the problem here.

Basically, when you disable developer options "USB debugging" and "Wireless debugging" gets disabled.
Then during the Slideshow, we instruct the user to enable "Wireless debugging".
If "USB debugging" is not enabled, then pairing is successful but ADB-wireless doesn't seems to be able connect!
Turing it on makes everything smoothless!

image

Thanks @rocodes for the awesome PR!

@lsd-cat
Copy link
Copy Markdown
Member

lsd-cat commented Sep 21, 2025

I tested on both Android 11, 12 and 16 in the emulator and the behavior seems pretty solid and way more reliable than before! Thanks for all the effort in this. I didn't look at the code, but I was curious about this which seems a duplicate slide and that goes back automatically, it doesn't really affect the process but it seems like an extra click for the user
Screen_recording_20250921_182448.webm

@rocodes
Copy link
Copy Markdown
Contributor Author

rocodes commented Sep 21, 2025

Thank you both for your feedback :)

@lsd-cat: Thanks for the video. It looks like it's trying to autoconnect first (which is why the button text says "Pairing with adb" and then switches to "Pair"), then it fails and asks you to re-pair. If you're starting fresh (no prior Bugbane connections stored in Wireless Debugging) it shouldn't happen, but maybe there's a case I didn't catch, I'll try to repro.

@TheZ3ro: clarification about what you found, are you testing on a physical device by running the app while it's plugged and in usb debug mode, or are you seeing this issue even when you push the apk to the device via usb, then disconnect the phone, then run the app? I'll try to reproduce it as well, or if you have steps to reproduce the failed connection issue it would be helpful too.

I think I know what the last couple issues are, thanks for the testing and I'll see what I can do.

Add TryAutoConnect state to AppState and include adbManager logic to support it.
Add additional transitional adb and app states.
Improve wifi connectivity monitor checks.

Use checkState in AdbManager to report connection.
…ebuggingAndPair from autoconnect states

Check for wireless adb directly in configuration manager using content resolver. Include navigation to build number when launching settings.

Refactor ConfigurationManager to use contentobservers and listen for and emit configuration changes.

Refactor ConfigurationViewModel to combine stateflows of components and calculate state. Deprecate checkUpdateState since state is automatically emitted on value change.

Refactor slideshow manager so that it emits updates.
slideshowactivity and check onResume() for updated Slideshow state.

Keep ScanScreen in the backstack if permissions screen is re-launched from slideshow. Use renamed AppState AdbConnectedScanning.

SlideshowActivity: add logging, use AppState.hiddenStates() to filter onboarding screens.
Simplify MainActivity slideshow vs acquisition screen selection logic.
@rocodes rocodes force-pushed the states branch 2 times, most recently from 50cad6e to 37b15ce Compare September 26, 2025 04:41
@rocodes rocodes marked this pull request as ready for review September 26, 2025 04:41
@rocodes
Copy link
Copy Markdown
Contributor Author

rocodes commented Sep 26, 2025

okay, back after a brief intermission :)

I think I addressed all the review feedback:

  • if you're on android <= 14, you can connect with just wireless adb
  • build # is highlighted - this wasn't a regression, it doesn't currently highlight on main. I tested mostly on an older pixel running CalyxOS, but some manufacturers or roms might not be so obliging.

also

  • I cleaned up checkState a lot, i think it's more readable now
  • Everything* now emits a state via observers/listeners, as opposed to polling for changes (well, notifications permission cheats a bit but everything else)
  • Autoconnect should be more reliable- and I also added a state that is for when you've disabled wireless adb or adb but you just need to re-enable it, you don't need to pair again, because before if someone finished the onboarding flow successfully, then disabled developer options, the only option available was "enable wireless adb and pair", and it would launch the pairing service, which wasn't technically needed cause we could opportunistically autoconnect instead. Visually it's identical to the "Need Wireless Debug + Pair" slideshow screen, it just removes the "+ Pair" from the title and instructions.

@rocodes rocodes requested a review from TheZ3ro September 26, 2025 04:50
AppState.TryAutoConnect, AppState.AdbConnecting, -> return SlideshowPageData(title = stringResource(R.string.notification_channel_adb_pairing), //todo
description = stringResource(R.string.notification_adb_pairing_working_title),
icon = Icons.Filled.Build,
buttonText = "please wait", //todo
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

(I didn't know what the text should be here, so it's a placeholder - suggestions (+italian translation plz) welcome. The user may never see this screen, but it may show briefly depending how fast the user leaves the Settings menu after doing the wifi pairing, for example.)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks! I'd go for "Waiting for ADB pairing" and "In attesa dell'accoppiamento ADB" (@TheZ3ro yes Android uses "accoppiamento" a lot, see this).

I think I'd keep the button clickable though, for the following scenario: if the user exits the pairing process, or closes the application, than at the next start of the app it is still waiting for pairing, but the user has no way to get redirected again to the correct screen (though the app behavior seemed very solid, keeping the pairing service running or restarting it and allowing the user to complete the flow no matter in which state they interrupted).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

thank you :) added the text in 5f3f356, and also pushed a commit to make sure the pairing service notification is killed if the app is exited via the slideshow.

All that by way of saying, if you can get things into a state where the user is stuck on a screen that doesn't automatically resolve to a connection success or error (and then the pairing screen), that's a state machine bug that I should fix. The connecting / tryautoconnect states are always supposed to automatically resolve to a state where the user can take actions.

@lsd-cat
Copy link
Copy Markdown
Member

lsd-cat commented Sep 26, 2025

I did some tests on Android 11/12 and 15/16 and it seemed really solid to recover from weird conditions. I'll do a last round of tests today but it looks really good!

wireless is enabled.

Catch edge case where user has manually removed prior wireless pairing authorizations.

Skip autoconnect if we received a pairing exception.
@rocodes
Copy link
Copy Markdown
Contributor Author

rocodes commented Sep 28, 2025

I think I've addressed everything :) For followup work in another pr, I'm suggesting:

  • a simplification of some of the states to deduplicate what's somewhat shared between adbManager and the AppState. I realized that I could use another combiner and report a tuple of (AdbConnected, adbManager.whatever) to the scan screen and get rid of some of the appstate values. This should have no user-facing impacts but will be some code cleanup.
  • Improved ui feedback on the scan screen if/while a scan is being cancelled

}
}

configViewModel.adbManager.watchCommandOutput().observe(this) { output ->
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this can be removed since it's useless, wdyt?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yep I think it is a leftover from the libadb demo app we started with!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

sorry I was wrong, that is the listener on the output of the QuickForensics result.
maybe we can just clean that out

Copy link
Copy Markdown
Member

@TheZ3ro TheZ3ro left a comment

Choose a reason for hiding this comment

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

This is much much more reliable and elegant than the old canSkip Slideshow, I really like this approach. LGTM

@TheZ3ro TheZ3ro merged commit 9c58463 into osservatorionessuno:main Sep 29, 2025
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants