-
-
Notifications
You must be signed in to change notification settings - Fork 45
CCCT-1877 Phone Verification Crash (Empty Session Data) #3437
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
CCCT-1877 Phone Verification Crash (Empty Session Data) #3437
Conversation
Made PersonalIdSessionDataViewModel lifecycle-aware to restore PersonalIdSessionData on process death.
Tweaked our logic for the case that the OTP fallback was used before process death.
Ran Ktlint on PersonalIdSessionData.kt.
📝 WalkthroughWalkthroughThis PR migrates PersonalIdSessionData persistence from in-memory ViewModel fields to AndroidX SavedStateHandle to survive process death. It adds Serializable support and a new otpFallbackUsedBeforeProcessDeath flag to the PersonalIdSessionData model. PersonalIdPhoneVerificationFragment is updated to conditionally initialize OTP state on first creation only and leverage the persisted fallback flag when applying OTP fallback logic. Sequence DiagramsequenceDiagram
actor User
participant Fragment as PersonalIdPhoneVerificationFragment
participant ViewModel as PersonalIdSessionDataViewModel
participant SavedState as SavedStateHandle
participant OTPMgr as OTP Manager
User->>Fragment: Initial load / OTP attempt
Fragment->>ViewModel: Check otpAttempts
alt First creation (otpAttempts == 0)
Fragment->>Fragment: setupInitialState()
else Process death recovery
Fragment->>Fragment: Skip setupInitialState
Note over Fragment: Preserve OTP state from before death
end
User->>Fragment: OTP attempt fails
Fragment->>ViewModel: getOtpFallbackUsedBeforeProcessDeath()
SavedState->>ViewModel: Retrieve persisted flag
alt Attempt 2 OR flag already set
Fragment->>OTPMgr: Create with fallback enabled
Fragment->>ViewModel: setOtpFallbackUsedBeforeProcessDeath(true)
SavedState->>SavedState: Persist flag
else First attempt, no prior fallback
Fragment->>OTPMgr: Create without fallback
Fragment->>ViewModel: setOtpFallbackUsedBeforeProcessDeath(false)
SavedState->>SavedState: Persist flag
end
rect rgb(200, 220, 240)
Note over Fragment,SavedState: Process death scenario
Fragment--xFragment: Process killed
Fragment->>ViewModel: onCreate (reconstructed)
SavedState->>ViewModel: Restore PersonalIdSessionData + flags
Fragment->>ViewModel: Read restored state & flag
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes
Possibly related PRs
Suggested labels
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (1)
app/src/org/commcare/fragments/personalId/PersonalIdPhoneVerificationFragment.java (1)
167-172: Avoid re‑requesting OTP on recreation, but consider restoring UI state separatelyGating
setupInitialState()onpersonalIdSessionData.getOtpAttempts() == 0is a good way to prevent sending a new “initial” OTP when the fragment is recreated after process death or configuration changes.One side effect is that, when
otpAttempts > 0, you now skipupdateVerificationMessage()and the initial button state setup driven bysetupInitialState(), so the recreated UI may not show the “last 4 digits” text or initial disabled state until the user interacts again.If that matters UX‑wise, consider splitting
setupInitialState()into something like:
setupInitialUiState()(updates label, button state, clears errors), andrequestInitialOtp(),and call only the UI part unconditionally while keeping
requestInitialOtp()behind theotpAttempts == 0check.Based on learnings, this fragment already cleans up view binding in
onDestroyView(), so factoring out UI initialization shouldn’t pose leak risks.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
app/build.gradle(1 hunks)app/src/org/commcare/activities/connect/viewmodel/PersonalIdSessionDataViewModel.java(1 hunks)app/src/org/commcare/android/database/connect/models/PersonalIdSessionData.kt(2 hunks)app/src/org/commcare/fragments/personalId/PersonalIdPhoneVerificationFragment.java(3 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: shubham1g5
Repo: dimagi/commcare-android PR: 0
File: :0-0
Timestamp: 2025-05-08T11:08:18.530Z
Learning: PR #3048 "Phase 4 Connect PR" introduces a substantial feature called "Connect" to the CommCare Android app, which includes messaging, job management, delivery tracking, payment processing, authentication flows, and learning modules. It follows a modern architecture using Navigation Components with three navigation graphs, segregated business logic in Manager classes, and proper database persistence.
📚 Learning: 2025-04-21T15:02:17.492Z
Learnt from: shubham1g5
Repo: dimagi/commcare-android PR: 3042
File: app/src/org/commcare/fragments/BreadcrumbBarViewModel.java:50-55
Timestamp: 2025-04-21T15:02:17.492Z
Learning: ViewModels should not store View or Activity references as this can cause memory leaks. Unlike Fragments with setRetainInstance(true), ViewModels don't have automatic view detachment mechanisms. When migrating from Fragments to ViewModels, view references should be replaced with data-only state in the ViewModel.
Applied to files:
app/src/org/commcare/activities/connect/viewmodel/PersonalIdSessionDataViewModel.java
📚 Learning: 2025-03-10T08:16:59.436Z
Learnt from: shubham1g5
Repo: dimagi/commcare-android PR: 2949
File: app/src/org/commcare/fragments/connectId/ConnectIDSecondaryPhoneNumber.java:58-59
Timestamp: 2025-03-10T08:16:59.436Z
Learning: All fragments using view binding should implement proper cleanup in onDestroyView() by setting binding to null to prevent memory leaks.
Applied to files:
app/src/org/commcare/fragments/personalId/PersonalIdPhoneVerificationFragment.java
🧬 Code graph analysis (1)
app/src/org/commcare/fragments/personalId/PersonalIdPhoneVerificationFragment.java (1)
app/src/org/commcare/utils/PersonalIDAuthService.kt (1)
activity(10-44)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Lint Code Base
🔇 Additional comments (5)
app/build.gradle (1)
119-123: SavedStateHandle dependency addition is appropriateAdding
androidx.lifecycle:lifecycle-viewmodel-savedstateis consistent with the newSavedStateHandle‑basedPersonalIdSessionDataViewModeland is the right artifact for process‑death persistence. No issues from this change alone.Please confirm that
$lifecycle_versionmatches the rest of your lifecycle components and that your existing CI/instrumentation suite covers the Personal ID flow with this new dependency enabled.app/src/org/commcare/activities/connect/viewmodel/PersonalIdSessionDataViewModel.java (1)
5-30: SavedStateHandle integration is good; verify ViewModel factory wiringUsing
SavedStateHandlewith a dedicated key here is a good fit for persistingPersonalIdSessionData, and making the modelSerializablesupports this usage.One thing to double‑check:
PersonalIdSessionDataViewModelnow only has aSavedStateHandleconstructor. Unless the hostingActivityoverridesgetDefaultViewModelProviderFactory()to return aSavedStateViewModelFactory/AbstractSavedStateViewModelFactory(or similar), a plainnew ViewModelProvider(requireActivity())call (as inPersonalIdPhoneVerificationFragment) will fail at runtime because there is no no‑arg constructor.If that default factory is already in place, this looks solid; if not, you’ll need to switch to a SavedState‑aware factory at the call site or via the Activity’s default factory.
Based on learnings, this ViewModel correctly keeps only data (no View/Activity references); please just confirm that the default
ViewModelProvider.Factoryfor the Personal ID flow is a SavedState‑aware one so theSavedStateHandleconstructor is actually used.app/src/org/commcare/android/database/connect/models/PersonalIdSessionData.kt (1)
4-46: Serializable + process‑death flag onPersonalIdSessionDataare appropriateMaking
PersonalIdSessionDataSerializableand addingotpFallbackUsedBeforeProcessDeathcleanly support persisting OTP session state throughSavedStateHandle. All fields are primitives/Strings, so they’re safe forSerializable, and the default value keeps existing callers backward‑compatible.Please confirm that this object is only serialized for in‑memory/saved‑state purposes (and not for long‑term disk/network persistence); if it ever becomes a persisted schema, we may want to be more deliberate about versioning.
app/src/org/commcare/fragments/personalId/PersonalIdPhoneVerificationFragment.java (2)
143-145: Fallback decision now correctly accounts for prior fallback usageIncluding
personalIdSessionData.getOtpFallbackUsedBeforeProcessDeath()in theuseOtpFallbackcalculation ensures that, after a process death, you continue using the Twilio/Personal ID fallback if it was already in use, rather than silently reverting to Firebase. This matches the intended behavior around OTP retries and looks correct.
373-393: PersistingotpFallbackUsedBeforeProcessDeathinsidesetupOtpManageris well‑placedUpdating
personalIdSessionData.setOtpFallbackUsedBeforeProcessDeath(true/false)in the same method that chooses the OTP backend keeps the persisted flag tightly coupled to the actual runtime choice. This ensuresinitOtpManager()can reliably restore the correct backend after process death, without needing extra bookkeeping elsewhere.
app/src/org/commcare/activities/connect/viewmodel/PersonalIdSessionDataViewModel.java
Show resolved
Hide resolved
app/src/org/commcare/fragments/personalId/PersonalIdPhoneVerificationFragment.java
Outdated
Show resolved
Hide resolved
…nto bug/CCCT-1877-no-session-data-for-phone-verification-crash
Moved simple UI state data out of PersonalIdSessionData.kt.
Added a check for if the fragment was restored when setting up the OTP manager.
app/src/org/commcare/fragments/personalId/PersonalIdPhoneVerificationFragment.java
Outdated
Show resolved
Hide resolved
| // Always fallback to Twilio (via Personal ID) if this is the second attempt in the session | ||
| // to send the user an OTP. Note that "otpFallbackUsed" may be true if this fragment was | ||
| // restored after process death. | ||
| Boolean useOtpFallback = (personalIdSessionData.getOtpAttempts() == 1 && !fragmentRestored) || otpFallbackUsed; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@conroy-ricketts Now we are storing the personalIdSessionData in SavedStateHandle of ViewModel, only checking the
personalIdSessionData.getOtpAttempts() > 1 should work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Solely checking for the number of otpAttempts would lead to a logical error in our code currently. More details here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On second thought, we may actually be able to simplify this a lot because this function is only called in onCreate(). I'm going to tweak this and do more testing
| public void onSaveInstanceState(@NonNull Bundle outState) { | ||
| super.onSaveInstanceState(outState); | ||
| outState.putString(KEY_PHONE, primaryPhone); | ||
| outState.putBoolean(KEY_OTP_FALLBACK_USED, otpFallbackUsed); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@conroy-ricketts Is this required as we have started storing PersonalIdSessionData with persistance data? I think we should use ViewModel only for persistence storage instead using both ViewModel and Fragment's savedInstance to have clear code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Think PersonalIdSessionData is a shared view model across multiple fragments and it will be confusing for it to have temporary fragment UI state from multiple fragments. Instead it would be more concrete for it to only have key data points that define the UI state and business logic itself on these signup fragments. We can potentially have a view model per fragment as well in addition to PersonalIdSessionDataViewModel to do this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah that is correct if storing any fragment UI state but I am here proposing to use only otpAttempts, which is already present, to calculate the PersonalIdPhoneVerificationFragment UI
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would lead us back to the issue discussed in this thread
Refactored PersonalIdPhoneVerificationFragment to restore UI state after process death, and simplified some logic surrounding the OTP method.
|
@shubham1g5 @Jignesh-dimagi In |
app/src/org/commcare/fragments/personalId/PersonalIdPhoneVerificationFragment.java
Outdated
Show resolved
Hide resolved
app/src/org/commcare/fragments/personalId/PersonalIdPhoneVerificationFragment.java
Show resolved
Hide resolved
app/src/org/commcare/fragments/personalId/PersonalIdPhoneVerificationFragment.java
Show resolved
Hide resolved
app/src/org/commcare/fragments/personalId/PersonalIdPhoneVerificationFragment.java
Show resolved
Hide resolved
Removed default vaule for lastOtpMethod.
Added null check before parsing the OTP request time string.
shubham1g5
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to go and great work on troubleshooting and implementing the fix here, my sense is we might need to do this kind of background/re-creation testing with all our screens as I think we are not really storing state in Connect side of code anywhere, but it does feel specially important for OTP screen and I am glad to have this fixed.
CCCT-1877
Technical Summary
This crash is happening because the user's session data is null when trying to get the phone number.
Here's why the session data is null for these users: They are putting the app in the background, and the Android OS, under memory pressure, decides to terminate the app's process to reclaim resources. Then, the entire
PersonalIdSessionDataViewModelinstance gets wiped from memory. When the users return to the app and thePersonalIdPhoneVerificationFragmentis recreated, a new emptyPersonalIdSessionDataViewModelis created because the old one was destroyed during process death and thePersonalIdSessionDatainside the view model was never re-initialized.I assume that the reason users are backgrounding the app in the first place is to read the OTP from the text message that gets sent to them.
Here is the easiest way I found to reproduce the crash:
Here's a video demonstrating those reproduction steps above:
Screen_Recording_20251126_160510_CommCare.Debug.mp4
I think the best way to fix this crash is to make
PersonalIdSessionDataViewModellifecycle-aware.I considered kicking the user back to the start of the Personal ID signup flow (or even the activity they were on just before they started the Personal ID signup flow) to avoid the crash, but if you really think about this, the user will likely see the crash again because they are likely to background the app again to read the OTP text message.
We could use the phone number that is saved to the
savedInstanceStatebundle inPersonalIdPhoneVerificationFragment, but the app will still crash because we also need the session data for getting/settingotpAttempts, getting theotpFallback, and getting thesmsMethod.Note that since the user is coming back to the same screen after process death, we also need to tweak some logic in the
PersonalIdPhoneVerificationFragmentbecause of our OTP fallback implementation or else we will have more logical errors.Let me know what y'alls thoughts are on this!
Safety Assurance
Safety story
I verified that, with these changes, the app no longer crashes.
I also verified that I'm still able to successfully complete the entire Personal ID signup flow with these changes - the app behaves as expected, especially when the OTP fallback is used.
QA Plan
QA should follow the reproduction steps I listed above and ensure that the app does not crash. QA should also background and freground the app at various states of otp-recreations (before sending code, after sending code, after verifying otp , after resending code once and twice) to check that we restore successfully and able to verify otp in each of those scenarios.
Also, QA should follow the test notes that I outlined here to ensure that there are no regressions.