-
-
Notifications
You must be signed in to change notification settings - Fork 45
Connect Beta Backmerge #3163
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
Connect Beta Backmerge #3163
Conversation
📝 WalkthroughWalkthroughThis change introduces a comprehensive set of features and infrastructure for the "Connect" module within the application, focusing on job management, messaging, and payment workflows. It adds new activities, fragments, adapters, data models, and utility classes to support Connect job listing, job details, learning and delivery progress, messaging channels, chat functionality, and payment acknowledgment. The AndroidManifest is updated to register new activities and a broadcast receiver. Notification handling is enhanced to support Connect messaging and payment actions with actionable notifications. The codebase also includes new database helpers and network API integrations for managing messaging, jobs, and user data, along with UI updates, localization string changes, and resource additions. Sequence Diagram(s)1. Connect Messaging Notification and UI FlowsequenceDiagram
participant FCM as Firebase Cloud Messaging
participant Service as CommCareFirebaseMessagingService
participant MessageMgr as MessageManager
participant DB as ConnectMessagingDatabaseHelper
participant UI as ConnectMessagingActivity/Fragments
FCM->>Service: onMessageReceived(dataPayload)
Service->>MessageMgr: handleReceivedMessage/handleReceivedChannel
MessageMgr->>DB: Store message/channel
Service->>UI: Send local broadcast (MESSAGING_UPDATE_BROADCAST)
UI->>DB: Refresh UI with new messages/channels
2. Connect Job Claim and Progress UpdatesequenceDiagram
participant User as User
participant UI as ConnectDeliveryDetailsFragment
participant API as ApiConnect
participant DB as JobStoreManager
participant ConnectMgr as ConnectManager
User->>UI: Clicks "Claim Job" button
UI->>API: claimJob (opportunityId)
API-->>UI: Success/Failure
alt Success
UI->>DB: Update job status to "delivering"
UI->>ConnectMgr: setActiveJob(job)
UI->>UI: Navigate to delivery progress or downloading screen
else Failure
UI->>UI: Show error message
end
3. Connect Messaging: Send Message FlowsequenceDiagram
participant User as User
participant UI as ConnectMessageFragment
participant DB as ConnectMessagingDatabaseHelper
participant MessageMgr as MessageManager
participant API as ApiPersonalId
User->>UI: Types and sends message
UI->>DB: Store outgoing message (unconfirmed)
UI->>MessageMgr: sendMessage(message)
MessageMgr->>API: sendMessagingMessage
API-->>MessageMgr: Success/Failure
alt Success
MessageMgr->>DB: Mark message as confirmed
UI->>UI: Update chat UI
else Failure
UI->>UI: Show send failure message
end
4. Payment Acknowledgment via NotificationsequenceDiagram
participant Service as CommCareFirebaseMessagingService
participant User as User
participant Receiver as PaymentAcknowledgeReceiver
participant ConnectMgr as ConnectManager
participant DB as JobStoreManager
Service->>User: Show payment notification with "Yes"/"No" actions
User->>Receiver: Taps action button (broadcast intent)
Receiver->>ConnectMgr: Get job by opportunityId
Receiver->>ConnectMgr: updateDeliveryProgress(job)
ConnectMgr->>DB: Update job and payment status
Possibly related PRs
Suggested reviewers
✨ Finishing Touches
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
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: 70
🔭 Outside diff range comments (1)
app/src/org/commcare/connect/PersonalIdManager.java (1)
218-218:⚠️ Potential issueDuplicate call to registerUserData().
This method is already called in
init()when the user is logged in. SincecompleteSignin()is called after successful login, this seems redundant unless there's a specific scenario where user data changes between initialization and sign-in completion.Consider removing this duplicate call or add a comment explaining why it's necessary to call it again here.
🧹 Nitpick comments (62)
app/src/org/commcare/views/connect/CustomOtpView.java (1)
103-103: Stylistic: Cast formatting consistency
The cast(EditText)getChildAt(index - 1)omits a space after the type, whereas other casts in this file use the form(EditText) getChildAt(...). For readability and consistency, please align this with the existing style.app/src/org/commcare/utils/BiometricsHelper.java (1)
26-28: Add @SInCE tag to class Javadoc
The condensed class-level Javadoc improves readability; consider including an@sinceannotation or brief note on supported biometric types to aid future maintainers.app/src/org/commcare/connect/ConnectJobListLauncher.java (2)
8-8: Add space after comma in parameter list.
The signaturelaunchApp(ConnectJobRecord job, View view,boolean isAvailable)is missing a space after the comma beforeboolean, impacting readability and style consistency.
7-9: Document the interface contract.
Public interfaces benefit from Javadoc on both the interface and its method to clarify expected behavior and usage.app/res/menu/menu_connect.xml (1)
12-12: Ensure consistent icon tinting.
android:iconTintis applied only toaction_sync. Confirm ifaction_notificationalso needs tint or if styling should be moved to a theme/style resource.app/src/org/commcare/activities/connect/PersonalIdActivity.java (1)
12-12: Remove unused import.
The importorg.commcare.utils.CommCareNavControlleris not referenced in this class. Cleaning it up will reduce warnings.app/src/org/commcare/activities/LoginActivityUIController.java (1)
567-568: LGTM! Appropriate for staged rollout.Temporarily hiding the connect button during login aligns with the beta release strategy. The TODO comment clearly indicates this is intentional.
Consider enhancing the TODO comment with more specific details:
-///TODO: connect set to true after phase 4 merge +// TODO: Re-enable connect button visibility after phase 4 Connect features are fully merged and testedapp/src/org/commcare/connect/IConnectAppLauncher.java (1)
3-5: Add documentation to clarify the interface contract.While the interface is well-designed, consider adding Javadoc documentation to clarify:
- Parameter expectations (null handling, validation requirements)
- Expected behavior for invalid appId
- What the
isLearningparameter controls- Error handling approach (exceptions vs. silent failures)
+/** + * Interface for launching Connect apps with specified configuration. + */ public interface IConnectAppLauncher { + /** + * Launches a Connect app with the specified ID and mode. + * + * @param appId The ID of the app to launch (must not be null or empty) + * @param isLearning Whether to launch in learning mode + * @throws IllegalArgumentException if appId is invalid + */ void launchApp(String appId, boolean isLearning); }app/res/values-sw/strings.xml (1)
354-355: Consider renaming resource keys to reflect PersonalId
The string values now reference “PersonalId” but the resource keyconnect_id_enabledstill contains “connect”. For consistency and clarity, consider renaming it (and similar keys) topersonal_id_enabledand updating code references accordingly.app/src/org/commcare/interfaces/OnJobSelectionClick.java (1)
1-7: Add JavaDoc to clarify callback contract
This new interface would benefit from JavaDoc onOnJobSelectionClickand itsonClickparameters to improve readability and maintainability.app/src/org/commcare/fragments/SelectInstallModeFragment.java (1)
213-224: Temporary Connect button disabling needs proper tracking.The Connect button functionality is temporarily disabled with hard-coded
enabled = false. While the TODO comment references a "phase 4 merge," this temporary code should be properly tracked.Consider creating a proper issue to track re-enabling this functionality:
- ///TODO: connect uncomment after phase 4 merge -// boolean enabled = connectEnabled && PersonalIdManager.getInstance().isloggedIn(); - boolean enabled = false; + // TODO: Re-enable Connect button after Connect feature integration is complete + // Tracked in issue #[ISSUE_NUMBER] + boolean enabled = false; // connectEnabled && PersonalIdManager.getInstance().isloggedIn();Would you like me to help create a GitHub issue to track this work?
app/src/org/commcare/fragments/personalId/PersonalIdPhoneFragment.java (1)
147-159: Improved phone number parsing with proper validation.The enhanced
displayPhoneNumbermethod now usesphoneNumberHelperfor robust phone number parsing and validation, which is a significant improvement over the previous implementation. The validation checks for valid country code and national number extraction are good defensive programming practices.Consider these minor improvements:
- private void displayPhoneNumber(String fullPhoneNumber) { - - if(TextUtils.isEmpty(fullPhoneNumber))return; + private void displayPhoneNumber(String fullPhoneNumber) { + if (TextUtils.isEmpty(fullPhoneNumber)) { + return; + }Also consider adding logging for invalid phone number scenarios:
+ if (countryCodeFromFullPhoneNumber == -1 || nationPhoneNumberFromFullPhoneNumber == -1) { + Logger.log(LogTypes.TYPE_USER, "Invalid phone number format received: " + fullPhoneNumber); + }app/res/values/strings.xml (1)
559-561: Fix grammatical error in error message.The error message strings are well-named and support the Connect messaging feature. However, there's a grammatical inconsistency:
Apply this fix for grammatical consistency:
- <string name="connect_messaging_send_message_fail_msg">Fail to send message</string> + <string name="connect_messaging_send_message_fail_msg">Failed to send message</string>app/src/org/commcare/fragments/connectMessaging/ConnectMessageChatData.java (2)
5-13: Consider using more specific types and immutability.The field declarations are clean, but consider these improvements:
- Use
LocalDateTimeinstead ofjava.util.Datefor better thread safety and API- Consider making fields
finalif immutability is desired- Add documentation for the
typefield to clarify valid values
31-31: Potential Date mutability issue.The
getTimestamp()method returns the internalDateobject directly, which could allow external modification sinceDateis mutable.Consider defensive copying:
-public Date getTimestamp() { return timestamp; } +public Date getTimestamp() { return timestamp != null ? new Date(timestamp.getTime()) : null; }app/src/org/commcare/fragments/connectMessaging/ConnectMessageChannelConsentBottomSheet.java (1)
63-66: Consider handling decline operation failures.The decline button calls
updateChannelConsentbut ignores the callback result entirely. If the server update fails, the user won't be informed, and the local state might be inconsistent.Consider adding error handling for the decline operation:
MessageManager.updateChannelConsent(requireContext(), channel, success -> { - + if (!success) { + // Log the failure or show a subtle indication + // that the decline wasn't fully processed + } });app/src/org/commcare/fragments/connect/ConnectJobDetailBottomSheetDialogFragment.java (1)
30-45: Simplify bottom sheet configuration logic.The complex UI setup in
onViewCreatedwith nested null checks and API level branching could be simplified and made more readable.Consider extracting to a separate method:
@Override public void onViewCreated(@NotNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); -// setStyle(STYLE_NORMAL, R.style.TransparentBottomSheet); - view.post(() -> { - BottomSheetDialog dialog = (BottomSheetDialog) getDialog(); - if (dialog != null) { - FrameLayout bottomSheet = dialog.findViewById(com.google.android.material.R.id.design_bottom_sheet); - if (bottomSheet != null) { - BottomSheetBehavior<View> behavior = BottomSheetBehavior.from(bottomSheet); - behavior.setState(BottomSheetBehavior.STATE_EXPANDED); - ViewGroup.LayoutParams layoutParams = bottomSheet.getLayoutParams(); - layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; - bottomSheet.setLayoutParams(layoutParams); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - bottomSheet.setBackground(new ColorDrawable(getResources().getColor(R.color.transparent, null))); - } - } - } - }); + view.post(this::configureBottomSheetBehavior); } + + private void configureBottomSheetBehavior() { + BottomSheetDialog dialog = (BottomSheetDialog) getDialog(); + if (dialog == null) return; + + FrameLayout bottomSheet = dialog.findViewById(com.google.android.material.R.id.design_bottom_sheet); + if (bottomSheet == null) return; + + BottomSheetBehavior<View> behavior = BottomSheetBehavior.from(bottomSheet); + behavior.setState(BottomSheetBehavior.STATE_EXPANDED); + + ViewGroup.LayoutParams layoutParams = bottomSheet.getLayoutParams(); + layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; + bottomSheet.setLayoutParams(layoutParams); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + bottomSheet.setBackground(new ColorDrawable(getResources().getColor(R.color.transparent, null))); + } + }app/src/org/commcare/activities/connect/ConnectMessagingActivity.java (1)
52-63: Optimize navigation listener cleanup.The navigation listener cleanup is repeated in
onDestroy()when it could be simplified and made more robust.Simplify the cleanup logic:
@Override protected void onDestroy() { if (destinationListener != null) { - NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager() - .findFragmentById(R.id.nav_host_fragment_connect_messaging); - if (navHostFragment != null) { - NavController navController = navHostFragment.getNavController(); - navController.removeOnDestinationChangedListener(destinationListener); - } + if (controller != null) { + controller.removeOnDestinationChangedListener(destinationListener); + } destinationListener = null; } super.onDestroy(); }app/src/org/commcare/fragments/connectMessaging/ConnectMessageChannelListFragment.java (2)
87-89: Simplify complex instanceof chain.The lengthy instanceof check can be simplified for better readability.
- if(getActivity()!=null && getActivity() instanceof CommCareActivity && ((CommCareActivity)getActivity()).getSupportActionBar()!=null){ - ((CommCareActivity)getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(true); - } + if(getActivity() instanceof CommCareActivity) { + CommCareActivity activity = (CommCareActivity) getActivity(); + if(activity.getSupportActionBar() != null) { + activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + }
108-108: Break long navigation line for better readability.The long navigation line affects code readability and could be split across multiple lines.
- Navigation.findNavController(binding.rvChannel).navigate(channel.getConsented() ? getConnectMessageFragmentDirection(channel):getChannelConsetBottomSheetDirection(channel)); + NavDirections direction = channel.getConsented() + ? getConnectMessageFragmentDirection(channel) + : getChannelConsetBottomSheetDirection(channel); + Navigation.findNavController(binding.rvChannel).navigate(direction);app/src/org/commcare/adapters/ConnectProgressJobSummaryAdapter.java (1)
36-36: Optimize string concatenation for better performance.Using string concatenation with
+operator inonBindViewHoldercan impact scrolling performance. Consider usingStringBuilderor string formatting.- holder.tvPrimaryVisitCount.setText(String.valueOf(primaryVisit.getPaymentUnitAmount()) + "/" + String.valueOf(primaryVisit.getPaymentUnitMaxDaily())); + holder.tvPrimaryVisitCount.setText(getString(R.string.progress_format, + primaryVisit.getPaymentUnitAmount(), + primaryVisit.getPaymentUnitMaxDaily()));Add to strings.xml:
<string name="progress_format">%d/%d</string>app/src/org/commcare/fragments/connect/ConnectJobIntroFragment.java (2)
94-94: Address the TODO comment.The TODO comment indicates incomplete implementation for HQ username handling.
The TODO suggests expecting HQ username from server. This should be addressed before production release.
Do you want me to help implement the HQ username handling or create a tracking issue for this task?
171-181: Remove unused utility methods.These utility methods are not used and should be removed to reduce code clutter.
- private <T extends View> T findView(View parent, int id) { - return parent.findViewById(id); - } - - private void setText(TextView textView, String text) { - textView.setText(text); - } - - private void setVisibility(View view, int visibility) { - view.setVisibility(visibility); - }app/src/org/commcare/adapters/ChannelAdapter.java (2)
69-77: Optimize message processing for better performance.The current implementation iterates through all messages for each channel during binding, which could become expensive with large message histories. Consider pre-computing these values when storing messages or caching them in the channel record.
- Date lastDate = null; - int unread = 0; - for(ConnectMessagingMessageRecord message : channel.getMessages()) { - if(lastDate == null || lastDate.before(message.getTimeStamp())) { - lastDate = message.getTimeStamp(); - } - - if(!message.getUserViewed()) { - unread++; - } - } + Date lastDate = channel.getLastMessageDate(); + int unread = channel.getUnreadCount();Consider adding
getLastMessageDate()andgetUnreadCount()methods toConnectMessagingChannelRecordthat are updated when messages are added/modified.
34-37: Use DiffUtil for better RecyclerView performance.The current implementation uses
notifyDataSetChanged()which refreshes the entire list. Consider usingDiffUtilornotifyItemChanged()for more efficient updates.public void setChannels(List<ConnectMessagingChannelRecord> channels) { this.channels = channels; - notifyDataSetChanged(); + // Consider implementing DiffUtil.Callback for efficient updates + notifyDataSetChanged(); // TODO: Replace with DiffUtil }app/src/org/commcare/fragments/connectMessaging/ConnectMessageFragment.java (2)
42-42: Consider reducing API call frequency.A 30-second interval for periodic API calls may be too frequent and could impact battery life and network usage. Consider increasing the interval or implementing a more intelligent polling strategy.
-private static final int INTERVAL = 30000; +private static final int INTERVAL = 60000; // Consider 1 minute or longerAlternatively, consider using WebSocket connections or Firebase real-time updates for more efficient messaging.
195-199: Optimize message viewed status updates.The current implementation updates each message's viewed status individually in the UI thread during refresh. Consider batch updates or background processing.
- if (!message.getUserViewed()) { - message.setUserViewed(true); - ConnectMessagingDatabaseHelper.storeMessagingMessage(context, message); - } + // Consider batching these updates or doing them in background + if (!message.getUserViewed()) { + message.setUserViewed(true); + // Add to batch update list instead of individual updates + }app/src/org/commcare/fragments/connect/ConnectLearningProgressFragment.java (1)
152-154: Ensure proper string formatting.When setting text with dynamic values, ensure proper formatting to handle edge cases.
- TextView textView = view.findViewById(R.id.connect_learn_cert_score); - String text=getString(R.string.your_score, job.getAssessmentScore()); - textView.setText(text); + TextView textView = view.findViewById(R.id.connect_learn_cert_score); + String text = getString(R.string.your_score, job.getAssessmentScore()); + textView.setText(text);app/src/org/commcare/fragments/connect/ConnectDeliveryDetailsFragment.java (1)
116-153: Consider adding more specific error messages.The error handling in the API callback uses generic messages. Consider providing more specific feedback to users based on the error type.
For example:
- For
processFailure: Check the response code and provide specific messages (e.g., "Job already claimed" for 409, "Job not found" for 404)- For
processTokenUnavailableError: "Please log in again to claim this job"- For network failures: "Please check your internet connection and try again"
app/src/org/commcare/adapters/ConnectDeliveryProgressReportAdapter.java (4)
21-21: Make the field private and final for better encapsulation.The
deliveryProgressListfield should be private and final to follow proper encapsulation principles.- List<ConnectDeliveryDetails> deliveryProgressList; + private final List<ConnectDeliveryDetails> deliveryProgressList;
22-22: Make the field private and final for better encapsulation.The
deliveryItemOnClickListenerfield should be private and final to follow proper encapsulation principles.- DeliveryItemOnClickListener deliveryItemOnClickListener; + private final DeliveryItemOnClickListener deliveryItemOnClickListener;
38-47: Avoid using @SuppressLint("SetTextI18n") for legitimate concatenation.The
@SuppressLint("SetTextI18n")annotation is used here, but the code appears to be setting text values appropriately. Consider removing this annotation if it's not necessary, or ensure that proper string formatting is used where needed.Review if this suppression is actually needed. If you're just setting individual text values (not concatenating), this annotation may be unnecessary.
45-47: Remove unnecessary String.valueOf() calls.These calls to
String.valueOf()are redundant sincesetText()can handle various data types directly.- holder.binding.tvDeliveryTitle.setText(String.valueOf(connectDeliveryDetails.getDeliveryName())); - holder.binding.tvApproved.setText(String.valueOf(connectDeliveryDetails.getApprovedCount())); - holder.binding.tvDeliveryTotalAmount.setText(String.valueOf(connectDeliveryDetails.getTotalAmount())); + holder.binding.tvDeliveryTitle.setText(connectDeliveryDetails.getDeliveryName()); + holder.binding.tvApproved.setText(String.valueOf(connectDeliveryDetails.getApprovedCount())); + holder.binding.tvDeliveryTotalAmount.setText(connectDeliveryDetails.getTotalAmount());app/src/org/commcare/fragments/connect/ConnectUnlockFragment.java (1)
95-97: Improve exception handling in JSON parsing loop.The catch block only logs the exception but continues processing other items, which is good. However, consider whether partial failures should be reported to the user.
Consider adding a counter for failed items and potentially showing a summary to the user if multiple parsing failures occur.
app/src/org/commcare/fragments/connect/ConnectDeliveryListFragment.java (2)
165-169: Consider using DiffUtil for better performance.The
updateDeliveriesmethod usesnotifyDataSetChanged()which redraws the entire list. For better performance, consider usingDiffUtilto only update changed items.public void updateDeliveries(List<ConnectJobDeliveryRecord> newDeliveries) { - filteredDeliveries.clear(); - filteredDeliveries.addAll(newDeliveries); - notifyDataSetChanged(); + DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DeliveryDiffCallback(filteredDeliveries, newDeliveries)); + filteredDeliveries.clear(); + filteredDeliveries.addAll(newDeliveries); + diffResult.dispatchUpdatesTo(this); }You would need to implement a
DiffCallbackclass for comparing delivery records.
119-122: Consider extracting color resources to improve maintainability.The hardcoded color references could be extracted to improve theme consistency and maintainability.
Consider defining semantic color names for selected/unselected states that can adapt to different themes.
app/src/org/commcare/fragments/connect/ConnectDeliveryProgressDeliveryFragment.java (5)
94-107: Improve string concatenation performance.The repeated string concatenation in the loop could be inefficient for large datasets. Consider using
StringBuilder.+ StringBuilder completedTextBuilder = new StringBuilder(completedText); for (int unitIndex = 0; unitIndex < job.getPaymentUnits().size(); unitIndex++) { ConnectPaymentUnitRecord unit = job.getPaymentUnits().get(unitIndex); int count = 0; String stringKey = Integer.toString(unit.getUnitId()); if (paymentCounts.containsKey(stringKey)) { count = paymentCounts.get(stringKey); } - completedText = String.format(Locale.getDefault(), "%s\n%s: %d", completedText, unit.getName(), count); + completedTextBuilder.append(String.format(Locale.getDefault(), "\n%s: %d", unit.getName(), count)); } + completedText = completedTextBuilder.toString();
117-118: Consider using more specific Map types.Using
HashMap<String, HashMap<String, Integer>>is fine, but consider if using more specific types or creating a custom class would improve readability.Consider creating a dedicated class for tracking delivery counts by payment type and status for better type safety and readability.
135-136: Use computeIfAbsent for cleaner HashMap operations.The HashMap operations can be simplified using Java 8+ methods.
- int count = typeCounts.containsKey(status) ? typeCounts.get(status) : 0; - typeCounts.put(status, count + 1); + typeCounts.merge(status, 1, Integer::sum);
146-147: Simplify HashMap access pattern.The HashMap access pattern can be simplified.
- HashMap<String, Integer> statusCounts = paymentTypeAndStatusCounts.containsKey(unitIdKey) ? paymentTypeAndStatusCounts.get(unitIdKey) : new HashMap<>(); + HashMap<String, Integer> statusCounts = paymentTypeAndStatusCounts.getOrDefault(unitIdKey, new HashMap<>());
149-150: Use getOrDefault for cleaner code.Similar to the previous comment, this can be simplified.
- int totalApproved = statusCounts.containsKey("approved") ? statusCounts.get("approved") : 0; + int totalApproved = statusCounts.getOrDefault("approved", 0);app/src/org/commcare/adapters/ConnectMessageAdapter.java (1)
28-31: Consider using DiffUtil for better performance when updating messages.Using
notifyDataSetChanged()refreshes the entire list. For better performance, especially with large message lists or frequent updates, consider implementing DiffUtil to calculate and apply only the actual changes.Would you like me to generate a DiffUtil implementation for this adapter?
app/src/org/commcare/fragments/connect/ConnectDeliveryProgressFragment.java (1)
197-197: Fix method naming to follow Java conventions.Method name should use camelCase.
- private void jobCardDataHandle(View view, ConnectJobRecord job) { + private void jobCardDataHandling(View view, ConnectJobRecord job) {Also update the method call on line 193:
- jobCardDataHandle(view,job); + jobCardDataHandling(view, job);app/src/org/commcare/connect/database/ConnectMessagingDatabaseHelper.java (2)
93-93: Fix incorrect comment - this deletes channels, not payments.The comment appears to be copy-pasted from payment-related code.
- //Delete payments that are no longer available + //Delete channels that are no longer available
167-167: Fix incorrect comment - this deletes messages, not payments.The comment appears to be copy-pasted from payment-related code.
- //Delete payments that are no longer available + //Delete messages that are no longer availableapp/src/org/commcare/fragments/connect/ConnectJobsListsFragment.java (2)
103-106: Remove unnecessary UI element setupThe refresh button is configured with a click listener but then immediately hidden. This creates unnecessary UI element initialization.
Either remove the refresh button setup entirely or keep it visible if it's intended for use:
- ImageView refreshButton = view.findViewById(R.id.connect_jobs_refresh); - refreshButton.setOnClickListener(v -> refreshData()); - refreshButton.setVisibility(View.GONE);
357-360: Consider reversing sort order for better UXThe current sorting shows oldest jobs first. For job lists, users typically expect to see the most recently accessed items first.
- Collections.sort(learnApps, (job1, job2) -> job1.getLastAccessed().compareTo(job2.getLastAccessed())); - Collections.sort(deliverApps, (job1, job2) -> job1.getLastAccessed().compareTo(job2.getLastAccessed())); - Collections.sort(reviewLearnApps, (job1, job2) -> job1.getLastAccessed().compareTo(job2.getLastAccessed())); - Collections.sort(finishedItems, (job1, job2) -> job1.getLastAccessed().compareTo(job2.getLastAccessed())); + Collections.sort(learnApps, (job1, job2) -> job2.getLastAccessed().compareTo(job1.getLastAccessed())); + Collections.sort(deliverApps, (job1, job2) -> job2.getLastAccessed().compareTo(job1.getLastAccessed())); + Collections.sort(reviewLearnApps, (job1, job2) -> job2.getLastAccessed().compareTo(job1.getLastAccessed())); + Collections.sort(finishedItems, (job1, job2) -> job2.getLastAccessed().compareTo(job1.getLastAccessed()));app/src/org/commcare/fragments/connect/ConnectResultsSummaryListFragment.java (1)
259-274: Simplify dialog margin setupThe manual margin calculation and padding setup is overly complex. Consider using standard dialog styling or layout XML.
- // Set dialog to match_parent and add 10dp horizontal margin - WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(); - layoutParams.copyFrom(dialog.getWindow().getAttributes()); - layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT; - - // Convert 10dp to pixels - int marginInDp = 10; - float density = context.getResources().getDisplayMetrics().density; - int marginInPx = (int) (marginInDp * density); - - layoutParams.horizontalMargin = marginInPx; - dialog.getWindow().setAttributes(layoutParams); - - dialog.getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT); - dialog.getWindow().getDecorView().setPadding(marginInPx, 0, marginInPx, 0); + // Set dialog width with margins + int marginInPx = (int) (10 * context.getResources().getDisplayMetrics().density); + dialog.getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT); + dialog.getWindow().getDecorView().setPadding(marginInPx, 0, marginInPx, 0);Alternatively, define margins in the dialog layout XML file directly.
app/src/org/commcare/adapters/JobListConnectHomeAppsAdapter.java (2)
31-31: Make context field finalSince the context is provided in the constructor and shouldn't change, declare it as final.
-private Context mContext; +private final Context mContext;
50-50: Add spacing around operatorsFollow Java coding conventions for readability.
-if(viewType==NON_CORRUPT_JOB_VIEW){ +if (viewType == NON_CORRUPT_JOB_VIEW) {app/src/org/commcare/fragments/connect/ConnectDownloadingFragment.java (1)
62-64: Simplify instanceof pattern matchingSince you're already using instanceof pattern matching, you can avoid the redundant cast.
Activity activity = getActivity(); -if(activity instanceof ConnectActivity connectActivity) { - connectActivity.setBackButtonAndActionBarState(enabled); +if (activity instanceof ConnectActivity connectActivity) { + connectActivity.setBackButtonAndActionBarState(enabled); }Note: Just fixing the spacing here since the pattern variable is already being used correctly.
app/src/org/commcare/activities/connect/ConnectActivity.java (3)
64-66: Simplify null check and assignmentUse a more concise approach for null-safe string assignment.
-if(opportunityId == null) { - opportunityId = ""; -} +opportunityId = opportunityId != null ? opportunityId : "";Or even better with modern Java:
-if(opportunityId == null) { - opportunityId = ""; -} +opportunityId = Strings.nullToEmpty(opportunityId);
161-161: Use isEmpty() instead of size() > 0For better readability and potential performance benefits.
-if(ConnectMessagingDatabaseHelper.getUnviewedMessages(this).size() > 0) { +if(!ConnectMessagingDatabaseHelper.getUnviewedMessages(this).isEmpty()) {
86-89: Consider adding documentation for notification redirection flowThe logic for handling notification redirects and navigation to the unlock fragment is complex. Consider adding comments to explain the flow.
} else if (!Strings.isNullOrEmpty(redirectionAction)) { Logger.log("ConnectActivity", "Redirecting to unlock fragment"); - //Entering from a notification, so we may need to initialize + // When entering from a notification: + // 1. Initialize ConnectManager if not already initialized + // 2. Navigate to unlock fragment which will handle authentication + // 3. Pass redirection parameters for post-unlock navigation ConnectManager.init(this);app/src/org/commcare/services/CommCareFirebaseMessagingService.java (2)
57-60: Consider expanding error handling for invalid FCM actions.The current implementation only logs invalid FCM actions without any error handling or user notification. Consider adding proper error handling or fallback behavior for the
INVALIDcase.Also applies to: 88-96
157-162: Add logging when suppressing notifications.When a message is ignored due to unknown consented channel, the method returns silently. Consider adding more context to the log message to help with debugging.
if(message == null) { - Logger.log(LogTypes.TYPE_FCM, "Ignoring message without known consented channel: " + - payloadData.get(ConnectMessagingMessageRecord.META_MESSAGE_ID)); + Logger.log(LogTypes.TYPE_FCM, "Suppressing notification for message without known consented channel. Message ID: " + + payloadData.get(ConnectMessagingMessageRecord.META_MESSAGE_ID) + ", Channel ID: " + channelId); //End now to avoid showing a notification return; }app/src/org/commcare/connect/MessageManager.java (2)
323-323: Remove debug logging statement.Debug log statements should not be left in production code.
sendMessage(context, message, success -> { - Log.d("Check", Boolean.toString(success)); + // Message send attempt completed });
370-370: Use proper log type constant for logging.The logger is being used with a string literal instead of a proper log type constant.
-Logger.log("Messaging", "Tried to send message but no encryption key"); +Logger.log(LogTypes.TYPE_ERROR_ASSERTION, "Tried to send message but no encryption key for channel: " + message.getChannelId());app/src/org/commcare/models/connect/ConnectLoginJobListModel.java (2)
29-29: Add explicit access modifier for consistencyThe
jobfield is missing an access modifier, making it package-private by default. For consistency with other fields and proper encapsulation:- ConnectJobRecord job; + private ConnectJobRecord job;
241-250: Fix toString() implementation issuesThe toString() method has two issues:
- Incorrect class name reference
- Missing quotes around id field which could cause formatting issues
@Override public String toString() { - return "AddListItem{" + + return "ConnectLoginJobListModel{" + "name='" + name + '\'' + - ", id=" + id + + ", id='" + id + '\'' + ", date=" + date + ", description='" + description + '\'' + ", organization='" + organization + '\'' + '}'; }app/src/org/commcare/activities/StandardHomeActivityUIController.java (1)
64-64: Add explicit access modifier- List<ConnectDeliveryPaymentSummaryInfo> deliveryPaymentInfoList = new ArrayList<>(); + private List<ConnectDeliveryPaymentSummaryInfo> deliveryPaymentInfoList = new ArrayList<>();app/src/org/commcare/connect/ConnectManager.java (1)
1-658: Consider splitting ConnectManager into focused componentsThis class violates the Single Responsibility Principle with 650+ lines handling:
- User management
- Job management
- App launching
- Network API calls
- Database operations
- UI updates
Consider refactoring into focused managers:
ConnectUserManager- User authentication and managementConnectJobManager- Job state and progressConnectNetworkManager- API interactionsConnectAppManager- App installation and launchingThis would improve testability, maintainability, and make the codebase more modular.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (3)
app/res/drawable/icon_chevron_left_brand.pngis excluded by!**/*.pngapp/res/drawable/icon_chevron_left_primary.pngis excluded by!**/*.pngapp/res/font/roboto_medium.ttfis excluded by!**/*.ttf
📒 Files selected for processing (73)
app/AndroidManifest.xml(8 hunks)app/build.gradle(2 hunks)app/res/drawable-night/ic_connect_message_large.xml(1 hunks)app/res/drawable/ic_connect_payment_revert.xml(0 hunks)app/res/layout-land/home_screen.xml(1 hunks)app/res/layout/fragment_connect_delivery_progress.xml(2 hunks)app/res/layout/item_channel.xml(1 hunks)app/res/menu/menu_connect.xml(1 hunks)app/res/values-fr/strings.xml(5 hunks)app/res/values-pt/strings.xml(5 hunks)app/res/values-sw/strings.xml(5 hunks)app/res/values-ti/strings.xml(5 hunks)app/res/values/dimens.xml(1 hunks)app/res/values/strings.xml(2 hunks)app/src/org/commcare/CommCareApplication.java(4 hunks)app/src/org/commcare/CommCareNoficationManager.java(3 hunks)app/src/org/commcare/activities/LoginActivity.java(3 hunks)app/src/org/commcare/activities/LoginActivityUIController.java(1 hunks)app/src/org/commcare/activities/StandardHomeActivity.java(7 hunks)app/src/org/commcare/activities/StandardHomeActivityUIController.java(4 hunks)app/src/org/commcare/activities/connect/ConnectActivity.java(1 hunks)app/src/org/commcare/activities/connect/ConnectMessagingActivity.java(1 hunks)app/src/org/commcare/activities/connect/PersonalIdActivity.java(1 hunks)app/src/org/commcare/adapters/ChannelAdapter.java(1 hunks)app/src/org/commcare/adapters/ConnectDeliveryProgressReportAdapter.java(1 hunks)app/src/org/commcare/adapters/ConnectMessageAdapter.java(1 hunks)app/src/org/commcare/adapters/ConnectProgressJobSummaryAdapter.java(1 hunks)app/src/org/commcare/adapters/JobListConnectHomeAppsAdapter.java(1 hunks)app/src/org/commcare/connect/ConnectJobListLauncher.java(1 hunks)app/src/org/commcare/connect/ConnectManager.java(1 hunks)app/src/org/commcare/connect/IConnectAppLauncher.java(1 hunks)app/src/org/commcare/connect/MessageManager.java(1 hunks)app/src/org/commcare/connect/PersonalIdManager.java(5 hunks)app/src/org/commcare/connect/database/ConnectDatabaseHelper.java(3 hunks)app/src/org/commcare/connect/database/ConnectJobUtils.java(2 hunks)app/src/org/commcare/connect/database/ConnectMessagingDatabaseHelper.java(1 hunks)app/src/org/commcare/connect/network/ApiPersonalId.java(7 hunks)app/src/org/commcare/connect/network/ConnectSsoHelper.java(1 hunks)app/src/org/commcare/fragments/SelectInstallModeFragment.java(1 hunks)app/src/org/commcare/fragments/connect/ConnectDeliveryDetailsFragment.java(1 hunks)app/src/org/commcare/fragments/connect/ConnectDeliveryListFragment.java(1 hunks)app/src/org/commcare/fragments/connect/ConnectDeliveryProgressDeliveryFragment.java(1 hunks)app/src/org/commcare/fragments/connect/ConnectDeliveryProgressFragment.java(1 hunks)app/src/org/commcare/fragments/connect/ConnectDownloadingFragment.java(1 hunks)app/src/org/commcare/fragments/connect/ConnectJobDetailBottomSheetDialogFragment.java(1 hunks)app/src/org/commcare/fragments/connect/ConnectJobIntroFragment.java(1 hunks)app/src/org/commcare/fragments/connect/ConnectJobsListsFragment.java(1 hunks)app/src/org/commcare/fragments/connect/ConnectLearningProgressFragment.java(1 hunks)app/src/org/commcare/fragments/connect/ConnectResultsListFragment.java(1 hunks)app/src/org/commcare/fragments/connect/ConnectResultsSummaryListFragment.java(1 hunks)app/src/org/commcare/fragments/connect/ConnectUnlockFragment.java(1 hunks)app/src/org/commcare/fragments/connectMessaging/ConnectMessageChannelConsentBottomSheet.java(1 hunks)app/src/org/commcare/fragments/connectMessaging/ConnectMessageChannelListFragment.java(1 hunks)app/src/org/commcare/fragments/connectMessaging/ConnectMessageChatData.java(1 hunks)app/src/org/commcare/fragments/connectMessaging/ConnectMessageFragment.java(1 hunks)app/src/org/commcare/fragments/personalId/PersonalIdBackupCodeFragment.java(1 hunks)app/src/org/commcare/fragments/personalId/PersonalIdMessageFragment.java(3 hunks)app/src/org/commcare/fragments/personalId/PersonalIdPhoneFragment.java(2 hunks)app/src/org/commcare/fragments/personalId/PersonalIdPhoneVerificationFragment.java(2 hunks)app/src/org/commcare/interfaces/OnJobSelectionClick.java(1 hunks)app/src/org/commcare/models/connect/ConnectLoginJobListModel.java(1 hunks)app/src/org/commcare/network/HttpCalloutTask.java(1 hunks)app/src/org/commcare/services/CommCareFirebaseMessagingService.java(2 hunks)app/src/org/commcare/services/PaymentAcknowledgeReceiver.java(1 hunks)app/src/org/commcare/tasks/ManageKeyRecordTask.java(1 hunks)app/src/org/commcare/utils/BiometricsHelper.java(3 hunks)app/src/org/commcare/utils/CommCareNavigationController.kt(1 hunks)app/src/org/commcare/utils/PhoneNumberHelper.java(1 hunks)app/src/org/commcare/views/connect/CustomOtpView.java(1 hunks)app/src/org/commcare/views/connect/LinearProgressBar.java(0 hunks)app/unit-tests/src/org/commcare/CommCareTestApplication.java(5 hunks)app/unit-tests/src/org/commcare/android/tests/processing/FormStorageTest.java(4 hunks)commcare-support-library/src/main/java/org/commcare/commcaresupportlibrary/CommCareLauncher.java(1 hunks)
💤 Files with no reviewable changes (2)
- app/res/drawable/ic_connect_payment_revert.xml
- app/src/org/commcare/views/connect/LinearProgressBar.java
🧰 Additional context used
🧠 Learnings (7)
📓 Common learnings
Learnt from: shubham1g5
PR: dimagi/commcare-android#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.
app/src/org/commcare/activities/connect/ConnectMessagingActivity.java (1)
Learnt from: shubham1g5
PR: dimagi/commcare-android#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.
app/src/org/commcare/fragments/connectMessaging/ConnectMessageFragment.java (1)
Learnt from: pm-dimagi
PR: dimagi/commcare-android#2956
File: app/src/org/commcare/fragments/connectMessaging/ConnectMessageChannelListFragment.java:58-60
Timestamp: 2025-02-19T15:15:01.935Z
Learning: Error handling for message retrieval in ConnectMessageChannelListFragment's retrieveMessages callback is not required as per user preference.
app/src/org/commcare/fragments/connect/ConnectUnlockFragment.java (1)
Learnt from: OrangeAndGreen
PR: dimagi/commcare-android#3108
File: app/src/org/commcare/fragments/connect/ConnectUnlockFragment.java:62-64
Timestamp: 2025-06-04T19:17:21.213Z
Learning: In ConnectUnlockFragment.java, the user prefers to let getArguments() potentially throw NullPointerException rather than adding null checks, as the arguments are required for proper navigation flow and their absence indicates a programming error that should fail fast.
app/src/org/commcare/activities/connect/ConnectActivity.java (1)
Learnt from: shubham1g5
PR: dimagi/commcare-android#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.
app/src/org/commcare/fragments/connect/ConnectDeliveryProgressFragment.java (1)
Learnt from: shubham1g5
PR: dimagi/commcare-android#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.
app/src/org/commcare/connect/ConnectManager.java (1)
Learnt from: shubham1g5
PR: dimagi/commcare-android#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.
🧬 Code Graph Analysis (8)
app/src/org/commcare/connect/network/ConnectSsoHelper.java (1)
app/src/org/commcare/connect/workers/ConnectHeartbeatWorker.kt (1)
context(12-28)
app/src/org/commcare/fragments/connectMessaging/ConnectMessageChannelConsentBottomSheet.java (3)
app/src/org/commcare/connect/database/ConnectDatabaseHelper.java (1)
ConnectDatabaseHelper(35-135)app/src/org/commcare/connect/MessageManager.java (1)
MessageManager(34-373)app/src/org/commcare/connect/database/ConnectMessagingDatabaseHelper.java (1)
ConnectMessagingDatabaseHelper(22-195)
app/src/org/commcare/activities/StandardHomeActivity.java (1)
app/src/org/commcare/connect/ConnectManager.java (1)
ConnectManager(68-658)
app/src/org/commcare/activities/LoginActivity.java (1)
app/src/org/commcare/connect/ConnectManager.java (1)
ConnectManager(68-658)
app/src/org/commcare/adapters/ConnectDeliveryProgressReportAdapter.java (1)
app/src/org/commcare/android/database/connect/models/ConnectDeliveryDetails.java (1)
ConnectDeliveryDetails(3-79)
app/unit-tests/src/org/commcare/CommCareTestApplication.java (1)
app/src/org/commcare/connect/database/ConnectDatabaseHelper.java (1)
ConnectDatabaseHelper(35-135)
app/src/org/commcare/connect/database/ConnectMessagingDatabaseHelper.java (3)
app/src/org/commcare/models/database/SqlStorage.java (1)
SqlStorage(44-813)app/src/org/commcare/connect/database/ConnectDatabaseHelper.java (1)
ConnectDatabaseHelper(35-135)app/src/org/commcare/utils/DimensionUtils.kt (1)
convertDpToPixel(7-11)
app/src/org/commcare/connect/PersonalIdManager.java (4)
app/src/org/commcare/activities/connect/ConnectActivity.java (1)
ConnectActivity(39-251)app/src/org/commcare/utils/CrashUtil.java (1)
CrashUtil(13-59)app/src/org/commcare/connect/workers/ConnectHeartbeatWorker.kt (1)
context(12-28)app/src/org/commcare/connect/ConnectManager.java (1)
ConnectManager(68-658)
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: Lint Code Base
🔇 Additional comments (80)
app/src/org/commcare/utils/BiometricsHelper.java (2)
51-53: Use defined constant for biometric authenticator
Switching toStrongBiometricimproves clarity over a numeric literal.
72-74: Use defined constant for biometric authenticator
InvokingconfigureBiometricwithStrongBiometricinstead of an explicit value enhances maintainability.app/res/layout/fragment_connect_delivery_progress.xml (1)
76-76: LGTM! Improved button sizing flexibility.Changing from fixed height (35dp) to
wrap_contentallows the buttons to adapt dynamically to content size, improving accessibility and following Material Design guidelines for adaptive layouts.Also applies to: 87-87
app/res/layout-land/home_screen.xml (1)
31-31: LGTM! Fixed missing layout constraint.Adding the vertical constraint properly positions the
connect_alert_tileCardView relative toconnect_jobs_last_update, preventing layout issues in landscape orientation.app/src/org/commcare/fragments/personalId/PersonalIdBackupCodeFragment.java (1)
70-70: LGTM! Improved code organization.Moving the
isRecoveryassignment toconfigureUiByModecentralizes recovery mode determination within the UI configuration logic, improving code organization and following the single responsibility principle.app/build.gradle (2)
93-98: LGTM! Good dependency organization.Relocating the Google Play Services dependencies to eliminate duplicates and group them with other core dependencies improves build file organization and maintainability.
628-628: LGTM! Minor comment cleanup.Good catch fixing the comment syntax.
commcare-support-library/src/main/java/org/commcare/commcaresupportlibrary/CommCareLauncher.java (1)
16-16: LGTM! Appropriate intent flags for app launching.The combination of
FLAG_ACTIVITY_CLEAR_TOPandFLAG_ACTIVITY_NEW_TASKis appropriate for launching CommCare from external contexts, ensuring proper activity stack management and task isolation.app/src/org/commcare/utils/PhoneNumberHelper.java (1)
95-108: LGTM! Well-implemented utility method.The new
getNationalNumbermethod follows the same consistent pattern as the existinggetCountryCodemethod, with proper error handling and appropriate return type. The implementation correctly validates the phone number before extracting the national number.app/res/values-sw/strings.xml (12)
357-357: Localization update correct
362-362: Localization update correct
365-366: Localization update correct
410-410: Localization update correct
416-416: Localization update correct
419-419: Localization update correct
434-434: Localization update correct
437-437: Localization update correct
439-439: Localization update correct
442-442: Localization update correct
515-519: Localization update correct
522-523: Localization update correctapp/res/values-pt/strings.xml (11)
354-355: Localization update correct
358-358: Localization update correct
366-367: Localization update correct
392-392: Localization update correct
419-419: Localization update correct
422-422: Localization update correct
436-436: Localization update correct
439-439: Localization update correct
442-442: Localization update correct
531-535: Localization update correct
537-538: Localization update correctapp/res/drawable-night/ic_connect_message_large.xml (1)
1-19: Night-mode vector drawable addition looks good
The 128 dp icon with white fill is appropriate for dark themes and aligns with existing Connect messaging assets.app/src/org/commcare/CommCareNoficationManager.java (1)
43-43: LGTM - Notification channel addition looks good.The new messaging notification channel follows the established pattern and appropriately uses
IMPORTANCE_MAXfor high-priority Connect messaging notifications.Also applies to: 200-203
app/src/org/commcare/connect/database/ConnectJobUtils.java (2)
62-64: LGTM - Clean delegation method.The static convenience method properly delegates to
JobStoreManager.storeJobswith appropriate parameter passing.
227-237: Excellent improvement to transaction handling.The updated transaction management properly wraps database operations in a try-finally block, ensuring transactions are always ended and only marked successful after all operations complete. This prevents transaction leaks and ensures data consistency.
app/res/values-fr/strings.xml (1)
353-354: LGTM - Consistent terminology update.The changes consistently replace "ConnectID" with "PersonalId" across French localization strings, aligning with the rebranding effort mentioned in the PR description.
Also applies to: 356-357, 362-362, 365-366, 410-410, 416-416, 419-419, 432-432, 435-435, 437-437, 440-440, 513-522, 576-576
app/src/org/commcare/tasks/ManageKeyRecordTask.java (1)
349-349: LGTM - Method signature updated to match parent class.The
doHttpRequest()method signature correctly adds the new exception declarations (TokenDeniedException,TokenUnavailableException) to align with the abstract method in the parent class. The implementation remains unchanged, which is appropriate as these exceptions will be handled by the parent class's error handling logic.app/res/values-ti/strings.xml (1)
343-522: LGTM! Systematic rebranding from ConnectID to PersonalId is well-executed.The translation updates consistently replace "ConnectID" references with "PersonalId" across all authentication, registration, and login flows. This aligns with the broader rebranding effort mentioned in the PR summary.
app/src/org/commcare/network/HttpCalloutTask.java (1)
149-149:Details
✅ Verification successful
Enhanced error handling for token-based authentication.
The addition of
TokenDeniedExceptionandTokenUnavailableExceptionto the abstract method signature properly supports the Connect feature's token authentication flow. The base class already handles these exceptions appropriately in thedoTaskBackgroundmethod.Verify that all subclasses of
HttpCalloutTaskhave been updated to handle these new exceptions:
🏁 Script executed:
#!/bin/bash # Description: Find all classes extending HttpCalloutTask and check if they handle the new exceptions # Expected: All subclasses should either handle these exceptions or let them propagate # Find all classes extending HttpCalloutTask ast-grep --pattern 'class $_ extends HttpCalloutTask { $$$ }' # Also check for implementations of doHttpRequest method ast-grep --pattern 'doHttpRequest() throws $$$'Length of output: 115
Let’s try a more robust search using ripgrep to locate all subclasses and their
doHttpRequestimplementations:
🏁 Script executed:
#!/bin/bash # Search for all classes extending HttpCalloutTask echo "🔍 Subclasses of HttpCalloutTask:" rg --glob '*.java' -n 'class .* extends HttpCalloutTask' # Search for all doHttpRequest method definitions echo -e "\n🔍 Implementations of doHttpRequest():" rg --glob '*.java' -n 'doHttpRequest\s*\('Length of output: 1039
All HttpCalloutTask subclasses correctly updated
- ManageKeyRecordTask (app/src/org/commcare/tasks/ManageKeyRecordTask.java) now declares
throws IOException, TokenDeniedException, TokenUnavailableExceptionas required.- ManageKeyRecordTaskFake (app/unit-tests/src/org/commcare/ManageKeyRecordTaskFake.java) declares only
IOException, which is a valid, narrower override of the base method’s signature.No further changes are needed—subclasses appropriately handle the new token-related exceptions.
app/src/org/commcare/fragments/personalId/PersonalIdPhoneVerificationFragment.java (2)
183-184: Good improvement in lifecycle management.Moving the timer start and broadcast receiver registration to
onResume()is better practice as it ensures these operations are properly synchronized with the fragment's visible state.
254-254: Good defensive programming practice.Setting
otpRequestTimeonly when a validprimaryPhoneis present prevents potential issues with the resend timer logic.app/src/org/commcare/connect/database/ConnectDatabaseHelper.java (2)
43-51: Verify the database rekey logic and security implications.The enhanced rekey logic is more efficient by only rekeying when necessary, but there are potential security and logic concerns:
- Security: The method always stores the received passphrase locally, even if rekeying fails
- Logic: The conditional check
!remotePassphrase.equals(localPassphrase)might not handle null values properlyConsider these improvements:
public static void handleReceivedDbPassphrase(Context context, String remotePassphrase) { ConnectDatabaseUtils.storeConnectDbPassphrase(context, remotePassphrase, false); try { - //Rekey the DB if the remote passphrase is different than local - String localPassphrase = ConnectDatabaseUtils.getConnectDbEncodedPassphrase(context, true); - if (connectDatabase != null && connectDatabase.isOpen() && !remotePassphrase.equals(localPassphrase)) { + String localPassphrase = ConnectDatabaseUtils.getConnectDbEncodedPassphrase(context, true); + if (connectDatabase != null && connectDatabase.isOpen() && + !Objects.equals(remotePassphrase, localPassphrase)) { DatabaseConnectOpenHelper.rekeyDB(connectDatabase, remotePassphrase); FirebaseAnalyticsUtil.reportRekeyedDatabase(); + //Only store the received passphrase after successful rekey + ConnectDatabaseUtils.storeConnectDbPassphrase(context, remotePassphrase, true); + } else if (Objects.equals(remotePassphrase, localPassphrase)) { + //Passphrases match, safe to mark as local + ConnectDatabaseUtils.storeConnectDbPassphrase(context, remotePassphrase, true); } - - //Store the received passphrase as what's in use locally - ConnectDatabaseUtils.storeConnectDbPassphrase(context, remotePassphrase, true);Verify that failed rekey operations don't leave the database in an inconsistent state.
84-85: Good addition of legacy usage tracking.The logging statement helps track legacy database access patterns, which is valuable for monitoring the migration to newer database access methods. The inclusion of encryption status in the log message provides useful context.
app/src/org/commcare/connect/network/ConnectSsoHelper.java (1)
117-118: Good formatting improvement for method signature readability.The line break for the exception declarations improves the readability of the method signature, especially given the length of the parameter list.
app/src/org/commcare/fragments/personalId/PersonalIdMessageFragment.java (2)
50-51: Proper ViewModel integration for session data management.Good use of activity-scoped ViewModel to access
PersonalIdSessionData. This follows the correct pattern for sharing data between fragments within the same activity lifecycle.
143-146: Well-integrated orphaned account recovery case.The new case for
PERSONALID_RECOVERY_ACCOUNT_ORPHANEDis properly integrated into the existing switch statement. SettingaccountExiststofalseand navigating to the backup code screen follows the expected recovery flow pattern.app/res/values/strings.xml (1)
920-920: LGTM! Terminology update aligns with ConnectID rebranding.The change from "Connect ID" to "Personal ID" is consistent with the broader terminology updates mentioned in the PR summary and aligns with the ConnectID rebranding effort.
app/src/org/commcare/activities/LoginActivity.java (3)
40-40: LGTM! Import is required for ConnectManager functionality.The import is correctly added to support the ConnectManager method calls used in the login flow.
149-149: LGTM! Correctly migrates to ConnectManager API.The replacement of the deprecated PersonalIdManager method with
ConnectManager.wasAppLaunchedFromConnect(presetAppId)properly integrates the new Connect management functionality. The method correctly determines if the app was launched from Connect for auto-login behavior.
414-416: LGTM! Enables Connect auto-login functionality.The call to
ConnectManager.checkAutoLoginAndOverridePasswordcorrectly integrates Connect-specific auto-login behavior. The method parameters are properly passed and the password override logic enables seamless login for Connect-managed apps.app/unit-tests/src/org/commcare/android/tests/processing/FormStorageTest.java (3)
4-4: LGTM: Import reorganization improves readability.The import reorganization with proper grouping and spacing follows Java best practices.
Also applies to: 5-14, 18-18
51-398: LGTM: Formatting improvements enhance maintainability.The consistent formatting of the
completeHistoryOfExternalizableClasseslist improves readability and maintainability. The addition of Connect-related classes aligns with the PR objectives for the Connect module integration.
409-409: LGTM: Variable naming improvements.The variable name changes (
extClassesInPfandclassName) are more descriptive and improve code clarity.Also applies to: 414-414
app/src/org/commcare/activities/StandardHomeActivity.java (3)
4-4: LGTM: Imports properly added for Connect integration.The new imports are correctly added and necessary for the Connect job progress functionality.
Also applies to: 14-15, 25-25
129-129: LGTM: UI update placement is appropriate.Calling
updateConnectProgress()after data operations ensures the UI reflects the latest Connect job status.
114-114:Details
❌ Incorrect review comment
Consider async execution to avoid blocking sync flow.
The
updateConnectJobProgress()call is synchronous in the sync button flow, but the actual progress update is asynchronous. Consider if this could create timing issues or impact user experience during sync operations.
🏁 Script executed:
#!/bin/bash # Description: Check if updateConnectJobProgress affects sync performance by examining sync flow timing # Look for other blocking operations in syncButtonPressed method ast-grep --pattern 'void syncButtonPressed() { $$$ }' # Check ConnectManager.updateDeliveryProgress implementation for timing concerns rg -A 10 "updateDeliveryProgress.*Context.*ConnectJobRecord"Length of output: 4330
🏁 Script executed:
#!/bin/bash # Locate updateConnectJobProgress implementation to inspect its behavior rg -n "updateConnectJobProgress" -A 20 app/src/org/commcare/activities/StandardHomeActivity.javaLength of output: 1387
No blocking in sync flow—updateConnectJobProgress is already async
The call to
updateConnectJobProgress()simply enqueues an API request viaApiConnect.getDeliveries(…)with anIApiCallbackand returns immediately. The UI gets updated in the callback (uiController.updateConnectProgress()), so it doesn’t blocksyncButtonPressed(). No code changes required.Likely an incorrect or invalid review comment.
app/src/org/commcare/utils/CommCareNavigationController.kt (3)
1-7: LGTM: Proper Kotlin file structure and imports.The file annotation, package declaration, and imports are correctly structured for a navigation utility.
9-21: Excellent defensive navigation implementation.The
navigateSafelyextension provides comprehensive safety checks to prevent navigation crashes. The logic correctly validates:
- Non-null direction and current destination
- Valid navigation action for the direction
- Non-zero destination ID
- Existence of destination node in the current or parent graph
This implementation will prevent common navigation errors in complex UI flows.
23-25: LGTM: Simple and clear nullable Int extension.The
orEmptyextension provides a clean way to handle nullable integers with a sensible default. The naming is consistent with similar Kotlin standard library functions.app/src/org/commcare/fragments/connectMessaging/ConnectMessageChatData.java (2)
1-4: LGTM: Proper package and import structure.The package declaration and Date import are correctly structured.
24-63: LGTM: Standard getter/setter implementation.The getter and setter methods follow Java conventions and provide proper encapsulation.
app/src/org/commcare/adapters/ConnectProgressJobSummaryAdapter.java (1)
38-43: Excellent division by zero handling.Good practice preventing division by zero errors with proper bounds checking.
app/src/org/commcare/CommCareApplication.java (1)
388-388: LGTM - Proper crash reporting integration.Good addition of user data registration for crash reporting after session cleanup.
app/unit-tests/src/org/commcare/CommCareTestApplication.java (2)
91-98: LGTM - Proper WorkManager test setup.The WorkManager test initialization with synchronous executor and debug logging is correctly configured for testing environment. This ensures deterministic test execution.
285-290: LGTM - Comprehensive test cleanup.Good addition of Connect database teardown and app sandbox cleanup to prevent test interference. The cleanup order is appropriate - app teardown first, then database cleanup.
app/src/org/commcare/connect/PersonalIdManager.java (3)
124-124: Good integration of crash reporting.Registering user data with Crashlytics during initialization will help with debugging user-specific issues.
422-434: Appropriate delegation to ConnectManager.The refactoring to use
ConnectManagerfor job progress updates aligns well with the centralized architecture approach.
445-450:Details
✅ Verification successful
Verify intent extras handling in ConnectActivity.
The method passes "info" and "buttons" as intent extras. Ensure that
ConnectActivityproperly handles these extras to navigate to the correct fragment.
🏁 Script executed:
#!/bin/bash # Description: Verify ConnectActivity handles the intent extras properly # Search for intent handling in ConnectActivity ast-grep --pattern 'getIntent().getBooleanExtra("info", $_)' # Also check for "buttons" extra handling ast-grep --pattern 'getIntent().getBooleanExtra("buttons", $_)'Length of output: 542
ConnectActivity correctly handles “info” and “buttons” extras
Verified that in app/src/org/commcare/activities/connect/ConnectActivity.java:
- Line 70:
if (getIntent().getBooleanExtra("info", false)) { … }- Line 76:
boolean buttons = getIntent().getBooleanExtra("buttons", true);- Line 97:
bundle.putBoolean("buttons", getIntent().getBooleanExtra("buttons", true));No changes needed.
app/src/org/commcare/fragments/connect/ConnectDeliveryDetailsFragment.java (1)
163-180: Good job state management.The method properly updates the job status in the database and navigates to the appropriate screen based on app installation status.
app/src/org/commcare/adapters/ConnectDeliveryProgressReportAdapter.java (1)
52-62: Consider using modern switch expression syntax consistently.The switch expression is well-implemented and uses modern Java syntax. However, ensure this is compatible with the project's minimum API level requirements.
The logic for handling remaining days with different text based on time remaining is well thought out and user-friendly.
app/src/org/commcare/fragments/connect/ConnectUnlockFragment.java (2)
62-63: Null safety for navigation arguments.Based on the retrieved learning, the user prefers to let
getArguments()potentially throw NullPointerException for required arguments. However, consider adding defensive programming to handle edge cases gracefully.The approach aligns with the established pattern in the codebase where arguments are required for proper navigation flow.
181-186: Navigation implementation looks solid.The navigation logic properly handles back stack management and uses NavOptions for a clean navigation experience.
The fragment redirection logic is well-implemented with proper back stack clearing and destination selection.
app/src/org/commcare/fragments/connect/ConnectDeliveryListFragment.java (2)
33-39: Good use of constants for filter identifiers.The use of static final constants for filter identifiers and the FILTERS array is a good practice for maintainability and reducing magic strings.
60-61: Verify argument retrieval pattern.The fragment uses
ConnectDeliveryListFragmentArgs.fromBundle()which suggests Safe Args is being used. This is a good practice for type-safe navigation.app/src/org/commcare/fragments/connect/ConnectDeliveryProgressDeliveryFragment.java (2)
77-79: Handle division by zero properly.The code handles the case where
totalis 0, but the ternary operator could be made more explicit for clarity.The division by zero handling is correct, defaulting to 100% when there are no total visits.
166-170: Lambda expression implementation looks good.The adapter initialization with the lambda for navigation is well-implemented and follows modern Android patterns.
app/src/org/commcare/fragments/connect/ConnectDeliveryProgressFragment.java (1)
375-383: 🛠️ Refactor suggestionAdd type safety checks before casting fragments.
Direct casting without instanceof checks could cause ClassCastException.
public void refresh() { for (Fragment fragment : fragmentList) { if (fragment instanceof ConnectDeliveryProgressDeliveryFragment) { ((ConnectDeliveryProgressDeliveryFragment) fragment).updateView(); } else if (fragment instanceof ConnectResultsSummaryListFragment) { ((ConnectResultsSummaryListFragment) fragment).updateView(); } + // Consider adding an interface that both fragments implement instead of casting } }Likely an incorrect or invalid review comment.
app/src/org/commcare/adapters/JobListConnectHomeAppsAdapter.java (1)
100-101:⚠️ Potential issueAdd null check after date parsing
The parse method could return null, which would cause a NullPointerException.
Date date = inputFormat.parse(dateStr); +if (date == null) { + return null; +} return outputFormat.format(date);Likely an incorrect or invalid review comment.
app/src/org/commcare/activities/connect/ConnectActivity.java (1)
85-85: Use isEmpty() for string checkThe
Strings.isNullOrEmpty()method is more readable and efficient than negating.Good use of Guava's string utility for null-safe empty checking!
| <item | ||
| android:id="@+id/action_notification" | ||
| android:icon="@drawable/ic_connect_messaging_base" | ||
| android:title="Notification" |
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.
Externalize hard-coded strings.
The android:title attributes use literal text ("Notification", "Menu"). Move these into strings.xml for proper localization support.
Also applies to: 13-13
🤖 Prompt for AI Agents
In app/res/menu/menu_connect.xml at lines 7 and 13, the android:title attributes
use hard-coded string literals ("Notification" and "Menu"). To support
localization, move these string values into the strings.xml resource file and
reference them here using the @string/ resource syntax instead of literal text.
| stopResendTimer(); | ||
| try { | ||
| activity.unregisterReceiver(smsBroadcastReceiver); | ||
| } catch (IllegalArgumentException e) { | ||
| e.printStackTrace(); | ||
|
|
||
| } |
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.
🛠️ Refactor suggestion
Address the empty catch block.
The empty catch block for IllegalArgumentException when unregistering the broadcast receiver is a code smell and makes debugging difficult.
Consider logging the exception or adding a comment explaining why it's safe to ignore:
try {
activity.unregisterReceiver(smsBroadcastReceiver);
} catch (IllegalArgumentException e) {
-
+ // Receiver was not registered or already unregistered - safe to ignore
+ Logger.log(LogTypes.TYPE_WARNING, "SMS receiver was not registered: " + e.getMessage());
}🤖 Prompt for AI Agents
In
app/src/org/commcare/fragments/personalId/PersonalIdPhoneVerificationFragment.java
around lines 191 to 196, the catch block for IllegalArgumentException is empty,
which obscures potential issues. To fix this, add a log statement inside the
catch block to record the exception details or include a comment explaining why
it is safe to ignore this exception, ensuring better maintainability and easier
debugging.
| public void updateConnectJobProgress() { | ||
| ConnectJobRecord job = ConnectManager.getActiveJob(); | ||
| if(job != null && job.getStatus() == ConnectJobRecord.STATUS_DELIVERING) { | ||
| ConnectManager.updateDeliveryProgress(this, job, success -> { | ||
| if (success) { | ||
| uiController.updateConnectProgress(); | ||
| } | ||
| }); | ||
| } | ||
| } |
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.
🛠️ Refactor suggestion
Verify null safety and error handling in async callback.
The method correctly checks for null job and specific status, but the async callback doesn't handle potential exceptions from uiController.updateConnectProgress().
Consider adding try-catch around the UI update:
public void updateConnectJobProgress() {
ConnectJobRecord job = ConnectManager.getActiveJob();
if(job != null && job.getStatus() == ConnectJobRecord.STATUS_DELIVERING) {
ConnectManager.updateDeliveryProgress(this, job, success -> {
if (success) {
- uiController.updateConnectProgress();
+ try {
+ uiController.updateConnectProgress();
+ } catch (Exception e) {
+ Log.w(TAG, "Error updating Connect progress UI", e);
+ }
}
});
}
}📝 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.
| public void updateConnectJobProgress() { | |
| ConnectJobRecord job = ConnectManager.getActiveJob(); | |
| if(job != null && job.getStatus() == ConnectJobRecord.STATUS_DELIVERING) { | |
| ConnectManager.updateDeliveryProgress(this, job, success -> { | |
| if (success) { | |
| uiController.updateConnectProgress(); | |
| } | |
| }); | |
| } | |
| } | |
| public void updateConnectJobProgress() { | |
| ConnectJobRecord job = ConnectManager.getActiveJob(); | |
| if (job != null && job.getStatus() == ConnectJobRecord.STATUS_DELIVERING) { | |
| ConnectManager.updateDeliveryProgress(this, job, success -> { | |
| if (success) { | |
| try { | |
| uiController.updateConnectProgress(); | |
| } catch (Exception e) { | |
| Log.w(TAG, "Error updating Connect progress UI", e); | |
| } | |
| } | |
| }); | |
| } | |
| } |
🤖 Prompt for AI Agents
In app/src/org/commcare/activities/StandardHomeActivity.java around lines 282 to
291, the async callback in updateConnectJobProgress does not handle exceptions
that might be thrown by uiController.updateConnectProgress(). To fix this, wrap
the call to uiController.updateConnectProgress() inside a try-catch block within
the success lambda to catch and handle any exceptions, preventing crashes and
improving robustness.
| public ConnectMessageChatData(int type, String message, String userName, Date timestamp, boolean isMessageRead) { | ||
| this.type = type; | ||
| this.message = message; | ||
| this.userName = userName; | ||
| this.timestamp = timestamp; | ||
| this.isMessageRead = isMessageRead; | ||
| } |
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.
🛠️ Refactor suggestion
Incomplete initialization: countUnread field not set in constructor.
The constructor doesn't initialize the countUnread field, which will default to 0. If this is intentional, consider documenting this behavior or adding it as a parameter.
Consider updating the constructor:
-public ConnectMessageChatData(int type, String message, String userName, Date timestamp, boolean isMessageRead) {
+public ConnectMessageChatData(int type, String message, String userName, Date timestamp, boolean isMessageRead, int countUnread) {
this.type = type;
this.message = message;
this.userName = userName;
this.timestamp = timestamp;
this.isMessageRead = isMessageRead;
+ this.countUnread = countUnread;
}Or add a comment explaining why countUnread defaults to 0.
📝 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.
| public ConnectMessageChatData(int type, String message, String userName, Date timestamp, boolean isMessageRead) { | |
| this.type = type; | |
| this.message = message; | |
| this.userName = userName; | |
| this.timestamp = timestamp; | |
| this.isMessageRead = isMessageRead; | |
| } | |
| public ConnectMessageChatData(int type, String message, String userName, Date timestamp, boolean isMessageRead, int countUnread) { | |
| this.type = type; | |
| this.message = message; | |
| this.userName = userName; | |
| this.timestamp = timestamp; | |
| this.isMessageRead = isMessageRead; | |
| this.countUnread = countUnread; | |
| } |
🤖 Prompt for AI Agents
In app/src/org/commcare/fragments/connectMessaging/ConnectMessageChatData.java
around lines 15 to 21, the constructor does not initialize the countUnread
field, which defaults to 0. To fix this, either add countUnread as a parameter
to the constructor and initialize it accordingly, or add a comment explaining
that countUnread intentionally defaults to 0 to clarify the design decision.
| MessageManager.updateChannelConsent(requireContext(), channel, success -> { | ||
| if(success) { | ||
| NavDirections directions = ConnectMessageChannelConsentBottomSheetDirections | ||
| .actionChannelConsentToConnectMessageFragment(channel.getChannelId()); | ||
| NavHostFragment.findNavController(this).navigate(directions); | ||
| } else { | ||
| Context context = getContext(); | ||
| if(context != null) { | ||
| Toast.makeText(context, getString(R.string.connect_messaging_channel_consent_failure_msg), Toast.LENGTH_SHORT).show(); | ||
| } | ||
|
|
||
| NavHostFragment.findNavController(this).popBackStack(); | ||
| } | ||
| }); |
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.
🛠️ Refactor suggestion
Handle potential UI state issues with async callback.
The callback from MessageManager.updateChannelConsent may execute after the fragment is destroyed, potentially causing issues with UI operations like navigation and toast display.
Consider adding lifecycle checks:
MessageManager.updateChannelConsent(requireContext(), channel, success -> {
+ if (!isAdded() || isDetached()) {
+ return;
+ }
if(success) {
NavDirections directions = ConnectMessageChannelConsentBottomSheetDirections
.actionChannelConsentToConnectMessageFragment(channel.getChannelId());
NavHostFragment.findNavController(this).navigate(directions);
} else {
Context context = getContext();
if(context != null) {
Toast.makeText(context, getString(R.string.connect_messaging_channel_consent_failure_msg), Toast.LENGTH_SHORT).show();
}
NavHostFragment.findNavController(this).popBackStack();
}
});📝 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.
| MessageManager.updateChannelConsent(requireContext(), channel, success -> { | |
| if(success) { | |
| NavDirections directions = ConnectMessageChannelConsentBottomSheetDirections | |
| .actionChannelConsentToConnectMessageFragment(channel.getChannelId()); | |
| NavHostFragment.findNavController(this).navigate(directions); | |
| } else { | |
| Context context = getContext(); | |
| if(context != null) { | |
| Toast.makeText(context, getString(R.string.connect_messaging_channel_consent_failure_msg), Toast.LENGTH_SHORT).show(); | |
| } | |
| NavHostFragment.findNavController(this).popBackStack(); | |
| } | |
| }); | |
| MessageManager.updateChannelConsent(requireContext(), channel, success -> { | |
| if (!isAdded() || isDetached()) { | |
| return; | |
| } | |
| if (success) { | |
| NavDirections directions = ConnectMessageChannelConsentBottomSheetDirections | |
| .actionChannelConsentToConnectMessageFragment(channel.getChannelId()); | |
| NavHostFragment.findNavController(this).navigate(directions); | |
| } else { | |
| Context context = getContext(); | |
| if (context != null) { | |
| Toast.makeText(context, getString(R.string.connect_messaging_channel_consent_failure_msg), Toast.LENGTH_SHORT).show(); | |
| } | |
| NavHostFragment.findNavController(this).popBackStack(); | |
| } | |
| }); |
🤖 Prompt for AI Agents
In
app/src/org/commcare/fragments/connectMessaging/ConnectMessageChannelConsentBottomSheet.java
around lines 44 to 57, the async callback from
MessageManager.updateChannelConsent may run after the fragment is destroyed,
leading to crashes when performing UI operations. To fix this, add a check to
ensure the fragment is still added and not detached before executing navigation
or showing a toast. Use methods like isAdded() or
getViewLifecycleOwner().getLifecycle().getCurrentState() to verify the
fragment's lifecycle state before proceeding with UI updates inside the
callback.
| if(!Strings.isNullOrEmpty(opportunityId)) { | ||
| int jobId = Integer.parseInt(opportunityId); | ||
| ConnectJobRecord job = ConnectJobUtils.getCompositeJob(requireContext(), jobId); | ||
| if(job != null) { | ||
| ConnectManager.setActiveJob(job); | ||
| } | ||
| } |
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.
🛠️ Refactor suggestion
Add null check for opportunityId parsing.
The code parses opportunityId as an integer without validating that it's a valid integer string, which could cause a NumberFormatException.
if(!Strings.isNullOrEmpty(opportunityId)) {
- int jobId = Integer.parseInt(opportunityId);
- ConnectJobRecord job = ConnectJobUtils.getCompositeJob(requireContext(), jobId);
- if(job != null) {
- ConnectManager.setActiveJob(job);
- }
+ try {
+ int jobId = Integer.parseInt(opportunityId);
+ ConnectJobRecord job = ConnectJobUtils.getCompositeJob(requireContext(), jobId);
+ if(job != null) {
+ ConnectManager.setActiveJob(job);
+ }
+ } catch (NumberFormatException e) {
+ Logger.exception("Invalid opportunity ID format: " + opportunityId, e);
+ }
}📝 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.
| if(!Strings.isNullOrEmpty(opportunityId)) { | |
| int jobId = Integer.parseInt(opportunityId); | |
| ConnectJobRecord job = ConnectJobUtils.getCompositeJob(requireContext(), jobId); | |
| if(job != null) { | |
| ConnectManager.setActiveJob(job); | |
| } | |
| } | |
| if(!Strings.isNullOrEmpty(opportunityId)) { | |
| try { | |
| int jobId = Integer.parseInt(opportunityId); | |
| ConnectJobRecord job = ConnectJobUtils.getCompositeJob(requireContext(), jobId); | |
| if(job != null) { | |
| ConnectManager.setActiveJob(job); | |
| } | |
| } catch (NumberFormatException e) { | |
| Logger.exception("Invalid opportunity ID format: " + opportunityId, e); | |
| } | |
| } |
🤖 Prompt for AI Agents
In app/src/org/commcare/fragments/connect/ConnectUnlockFragment.java around
lines 155 to 161, the code parses opportunityId to an integer without validating
if it is a valid integer string, risking a NumberFormatException. Add a
try-catch block around the Integer.parseInt call to catch NumberFormatException
and handle it gracefully, such as by logging the error or skipping the parsing
if invalid. This ensures the app does not crash due to invalid input.
| String descriptionText = delivery.getReason(); | ||
| if (Strings.isNullOrEmpty(descriptionText)) { | ||
| if (delivery.getFlags() != null) { | ||
| List<String> flagStrings = new ArrayList<>(); | ||
| for (ConnectJobDeliveryFlagRecord flag : delivery.getFlags()) { | ||
| flagStrings.add(flag.getDescription()); | ||
| } | ||
| descriptionText = String.join(", ", flagStrings); | ||
| } else { | ||
| descriptionText = ""; | ||
| } | ||
| } | ||
| reasonText.setText(descriptionText); |
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.
🛠️ Refactor suggestion
Handle potential null pointer in flag processing.
The code checks if delivery.getFlags() is not null but doesn't handle the case where individual flags might be null.
if (delivery.getFlags() != null) {
List<String> flagStrings = new ArrayList<>();
for (ConnectJobDeliveryFlagRecord flag : delivery.getFlags()) {
- flagStrings.add(flag.getDescription());
+ if (flag != null && flag.getDescription() != null) {
+ flagStrings.add(flag.getDescription());
+ }
}
descriptionText = String.join(", ", flagStrings);
} else {📝 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.
| String descriptionText = delivery.getReason(); | |
| if (Strings.isNullOrEmpty(descriptionText)) { | |
| if (delivery.getFlags() != null) { | |
| List<String> flagStrings = new ArrayList<>(); | |
| for (ConnectJobDeliveryFlagRecord flag : delivery.getFlags()) { | |
| flagStrings.add(flag.getDescription()); | |
| } | |
| descriptionText = String.join(", ", flagStrings); | |
| } else { | |
| descriptionText = ""; | |
| } | |
| } | |
| reasonText.setText(descriptionText); | |
| String descriptionText = delivery.getReason(); | |
| if (Strings.isNullOrEmpty(descriptionText)) { | |
| if (delivery.getFlags() != null) { | |
| List<String> flagStrings = new ArrayList<>(); | |
| for (ConnectJobDeliveryFlagRecord flag : delivery.getFlags()) { | |
| if (flag != null && flag.getDescription() != null) { | |
| flagStrings.add(flag.getDescription()); | |
| } | |
| } | |
| descriptionText = String.join(", ", flagStrings); | |
| } else { | |
| descriptionText = ""; | |
| } | |
| } | |
| reasonText.setText(descriptionText); |
🤖 Prompt for AI Agents
In app/src/org/commcare/fragments/connect/ConnectDeliveryListFragment.java
around lines 194 to 206, the code iterates over delivery.getFlags() without
checking if individual flag elements are null, which can cause a
NullPointerException. To fix this, add a null check inside the loop to skip any
null flags before calling flag.getDescription(), ensuring safe processing of the
flags list.
| builder.append(chat.getMessage()); | ||
| MarkupUtil.setMarkdown(binding.tvChatMessage, builder, new SpannableStringBuilder()); | ||
|
|
||
| binding.tvUserName.setText(DateUtils.formatDateTime(chat.getTimestamp(), DateUtils.FORMAT_HUMAN_READABLE_SHORT)); |
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.
🛠️ Refactor suggestion
Field name tvUserName is misleading when displaying timestamp.
The TextView field named tvUserName is actually displaying a formatted timestamp. This naming inconsistency could confuse future maintainers.
Consider renaming the TextView in the layout files from tvUserName to tvTimestamp to accurately reflect its purpose.
Also applies to: 63-63
🤖 Prompt for AI Agents
In app/src/org/commcare/adapters/ConnectMessageAdapter.java at lines 46 and 63,
the TextView field named tvUserName is misleading because it displays a
formatted timestamp. Rename this field to tvTimestamp in both the Java code and
the corresponding layout XML files to accurately reflect its purpose and avoid
confusion for future maintainers.
|
|
||
| public static final int LEFTVIEW = 0; | ||
| public static final int RIGHTVIEW = 1; | ||
| List<ConnectMessageChatData> messages; |
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.
🛠️ Refactor suggestion
Make the messages field private for better encapsulation.
The field should be private to prevent external modification and maintain proper encapsulation.
- List<ConnectMessageChatData> messages;
+ private List<ConnectMessageChatData> messages;📝 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.
| List<ConnectMessageChatData> messages; | |
| private List<ConnectMessageChatData> messages; |
🤖 Prompt for AI Agents
In app/src/org/commcare/adapters/ConnectMessageAdapter.java at line 22, change
the visibility of the messages field from package-private to private by adding
the private modifier. This will restrict direct access to the field from outside
the class and improve encapsulation.
| public static class LeftViewHolder extends RecyclerView.ViewHolder { | ||
| ItemChatLeftViewBinding binding; | ||
|
|
||
| public LeftViewHolder(ItemChatLeftViewBinding binding) { | ||
| super(binding.getRoot()); | ||
| this.binding = binding; | ||
| } | ||
|
|
||
| public void bind(ConnectMessageChatData chat) { | ||
| SpannableStringBuilder builder = new SpannableStringBuilder(); | ||
| builder.append(chat.getMessage()); | ||
| MarkupUtil.setMarkdown(binding.tvChatMessage, builder, new SpannableStringBuilder()); | ||
|
|
||
| binding.tvUserName.setText(DateUtils.formatDateTime(chat.getTimestamp(), DateUtils.FORMAT_HUMAN_READABLE_SHORT)); | ||
| } | ||
| } | ||
|
|
||
| public static class RightViewHolder extends RecyclerView.ViewHolder { | ||
| ItemChatRightViewBinding binding; | ||
|
|
||
| public RightViewHolder(ItemChatRightViewBinding binding) { | ||
| super(binding.getRoot()); | ||
| this.binding = binding; | ||
| } | ||
|
|
||
| public void bind(ConnectMessageChatData chat) { | ||
| SpannableStringBuilder builder = new SpannableStringBuilder(); | ||
| builder.append(chat.getMessage()); | ||
| MarkupUtil.setMarkdown(binding.tvChatMessage, builder, new SpannableStringBuilder()); | ||
|
|
||
| binding.tvUserName.setText(DateUtils.formatDateTime(chat.getTimestamp(), DateUtils.FORMAT_HUMAN_READABLE_SHORT)); | ||
| } | ||
| } |
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.
🛠️ Refactor suggestion
Eliminate code duplication between LeftViewHolder and RightViewHolder.
Both ViewHolders have identical bind methods. Consider creating an abstract base ViewHolder to share the common logic.
+ public abstract static class BaseMessageViewHolder extends RecyclerView.ViewHolder {
+ protected abstract TextView getMessageTextView();
+ protected abstract TextView getTimestampTextView();
+
+ public BaseMessageViewHolder(View itemView) {
+ super(itemView);
+ }
+
+ public void bind(ConnectMessageChatData chat) {
+ SpannableStringBuilder builder = new SpannableStringBuilder();
+ builder.append(chat.getMessage());
+ MarkupUtil.setMarkdown(getMessageTextView(), builder, new SpannableStringBuilder());
+ getTimestampTextView().setText(DateUtils.formatDateTime(chat.getTimestamp(), DateUtils.FORMAT_HUMAN_READABLE_SHORT));
+ }
+ }
- public static class LeftViewHolder extends RecyclerView.ViewHolder {
+ public static class LeftViewHolder extends BaseMessageViewHolder {
ItemChatLeftViewBinding binding;
public LeftViewHolder(ItemChatLeftViewBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
- public void bind(ConnectMessageChatData chat) {
- SpannableStringBuilder builder = new SpannableStringBuilder();
- builder.append(chat.getMessage());
- MarkupUtil.setMarkdown(binding.tvChatMessage, builder, new SpannableStringBuilder());
-
- binding.tvUserName.setText(DateUtils.formatDateTime(chat.getTimestamp(), DateUtils.FORMAT_HUMAN_READABLE_SHORT));
+ @Override
+ protected TextView getMessageTextView() {
+ return binding.tvChatMessage;
+ }
+
+ @Override
+ protected TextView getTimestampTextView() {
+ return binding.tvUserName;
}
}
- public static class RightViewHolder extends RecyclerView.ViewHolder {
+ public static class RightViewHolder extends BaseMessageViewHolder {
ItemChatRightViewBinding binding;
public RightViewHolder(ItemChatRightViewBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
- public void bind(ConnectMessageChatData chat) {
- SpannableStringBuilder builder = new SpannableStringBuilder();
- builder.append(chat.getMessage());
- MarkupUtil.setMarkdown(binding.tvChatMessage, builder, new SpannableStringBuilder());
-
- binding.tvUserName.setText(DateUtils.formatDateTime(chat.getTimestamp(), DateUtils.FORMAT_HUMAN_READABLE_SHORT));
+ @Override
+ protected TextView getMessageTextView() {
+ return binding.tvChatMessage;
+ }
+
+ @Override
+ protected TextView getTimestampTextView() {
+ return binding.tvUserName;
}
}📝 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.
| public static class LeftViewHolder extends RecyclerView.ViewHolder { | |
| ItemChatLeftViewBinding binding; | |
| public LeftViewHolder(ItemChatLeftViewBinding binding) { | |
| super(binding.getRoot()); | |
| this.binding = binding; | |
| } | |
| public void bind(ConnectMessageChatData chat) { | |
| SpannableStringBuilder builder = new SpannableStringBuilder(); | |
| builder.append(chat.getMessage()); | |
| MarkupUtil.setMarkdown(binding.tvChatMessage, builder, new SpannableStringBuilder()); | |
| binding.tvUserName.setText(DateUtils.formatDateTime(chat.getTimestamp(), DateUtils.FORMAT_HUMAN_READABLE_SHORT)); | |
| } | |
| } | |
| public static class RightViewHolder extends RecyclerView.ViewHolder { | |
| ItemChatRightViewBinding binding; | |
| public RightViewHolder(ItemChatRightViewBinding binding) { | |
| super(binding.getRoot()); | |
| this.binding = binding; | |
| } | |
| public void bind(ConnectMessageChatData chat) { | |
| SpannableStringBuilder builder = new SpannableStringBuilder(); | |
| builder.append(chat.getMessage()); | |
| MarkupUtil.setMarkdown(binding.tvChatMessage, builder, new SpannableStringBuilder()); | |
| binding.tvUserName.setText(DateUtils.formatDateTime(chat.getTimestamp(), DateUtils.FORMAT_HUMAN_READABLE_SHORT)); | |
| } | |
| } | |
| // New abstract base to share bind(...) logic | |
| public abstract static class BaseMessageViewHolder extends RecyclerView.ViewHolder { | |
| protected abstract TextView getMessageTextView(); | |
| protected abstract TextView getTimestampTextView(); | |
| public BaseMessageViewHolder(View itemView) { | |
| super(itemView); | |
| } | |
| public void bind(ConnectMessageChatData chat) { | |
| SpannableStringBuilder builder = new SpannableStringBuilder(); | |
| builder.append(chat.getMessage()); | |
| MarkupUtil.setMarkdown(getMessageTextView(), builder, new SpannableStringBuilder()); | |
| getTimestampTextView().setText( | |
| DateUtils.formatDateTime(chat.getTimestamp(), DateUtils.FORMAT_HUMAN_READABLE_SHORT) | |
| ); | |
| } | |
| } | |
| public static class LeftViewHolder extends BaseMessageViewHolder { | |
| ItemChatLeftViewBinding binding; | |
| public LeftViewHolder(ItemChatLeftViewBinding binding) { | |
| super(binding.getRoot()); | |
| this.binding = binding; | |
| } | |
| @Override | |
| protected TextView getMessageTextView() { | |
| return binding.tvChatMessage; | |
| } | |
| @Override | |
| protected TextView getTimestampTextView() { | |
| return binding.tvUserName; | |
| } | |
| } | |
| public static class RightViewHolder extends BaseMessageViewHolder { | |
| ItemChatRightViewBinding binding; | |
| public RightViewHolder(ItemChatRightViewBinding binding) { | |
| super(binding.getRoot()); | |
| this.binding = binding; | |
| } | |
| @Override | |
| protected TextView getMessageTextView() { | |
| return binding.tvChatMessage; | |
| } | |
| @Override | |
| protected TextView getTimestampTextView() { | |
| return binding.tvUserName; | |
| } | |
| } |
🤖 Prompt for AI Agents
In app/src/org/commcare/adapters/ConnectMessageAdapter.java between lines 33 and
65, the LeftViewHolder and RightViewHolder classes have identical bind methods
causing code duplication. To fix this, create an abstract base ViewHolder class
that holds the common bind method implementation, then have LeftViewHolder and
RightViewHolder extend this base class and only keep their specific binding
references. This will consolidate the shared logic and eliminate duplication.
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.
Review continued from previous batch...
| @Override | ||
| public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, | ||
| Bundle savedInstanceState) { | ||
| ConnectJobRecord job = ConnectManager.getActiveJob(); |
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.
Add null safety check for active job.
The code assumes ConnectManager.getActiveJob() returns a non-null value, but this might not always be the case.
ConnectJobRecord job = ConnectManager.getActiveJob();
+if (job == null) {
+ // Handle the case where no active job exists
+ getActivity().finish();
+ return null;
+}Also applies to: 193-193
🤖 Prompt for AI Agents
In app/src/org/commcare/fragments/connect/ConnectDeliveryProgressFragment.java
at lines 76 and 193, add a null check after calling
ConnectManager.getActiveJob() to ensure the returned job is not null before
using it. If the job is null, handle this case appropriately, such as by
returning early or showing an error message, to prevent potential
NullPointerExceptions.
| View view = viewStateAdapter.createFragment(position).getView(); | ||
| if(view != null) { | ||
| pager.getLayoutParams().height = view.getMeasuredHeight(); | ||
| pager.requestLayout(); | ||
| } |
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.
ViewPager height adjustment logic may fail due to view not being measured.
Getting the view from a fragment and immediately accessing its measured height is unreliable because the view might not be laid out yet.
Remove this height adjustment logic or defer it until the view is properly laid out using a ViewTreeObserver.
🤖 Prompt for AI Agents
In app/src/org/commcare/fragments/connect/ConnectDeliveryProgressFragment.java
around lines 142 to 146, the current code tries to adjust the ViewPager height
by immediately accessing the fragment view's measured height, which may be zero
if the view is not yet laid out. To fix this, remove the direct height
adjustment here and instead defer it by adding a
ViewTreeObserver.OnGlobalLayoutListener to the fragment's view. In that
listener, update the ViewPager height once the view has been laid out and then
remove the listener to avoid repeated calls.
| } catch (Exception e) { | ||
| //Ignore exception, happens if we leave the page before API call finishes | ||
| } | ||
| } |
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.
Avoid catching all exceptions - be more specific.
Catching all exceptions can mask real errors. Consider catching specific exceptions or checking if the fragment is still attached.
- try {
+ if (isAdded() && getView() != null) {
updateUpdatedDate(new Date());
updateConnectWarningMessage(getView());
updatePaymentConfirmationTile(getContext(), false);
viewStateAdapter.refresh();
- } catch (Exception e) {
- //Ignore exception, happens if we leave the page before API call finishes
}📝 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.
| } catch (Exception e) { | |
| //Ignore exception, happens if we leave the page before API call finishes | |
| } | |
| } | |
| if (isAdded() && getView() != null) { | |
| updateUpdatedDate(new Date()); | |
| updateConnectWarningMessage(getView()); | |
| updatePaymentConfirmationTile(getContext(), false); | |
| viewStateAdapter.refresh(); | |
| } |
🤖 Prompt for AI Agents
In app/src/org/commcare/fragments/connect/ConnectDeliveryProgressFragment.java
around lines 315 to 318, the catch block currently catches all exceptions which
can hide real errors. Modify the catch block to catch only specific exceptions
that are expected, such as those related to the fragment lifecycle or API call
cancellation. Alternatively, before handling the exception, check if the
fragment is still attached to avoid acting on a detached fragment. This will
prevent masking unexpected errors while safely handling the known case.
| for (ConnectMessagingMessageRecord connectMessagingMessageRecord : getMessagingMessagesAll(context)) { | ||
| ConnectMessagingChannelRecord connectMessagingChannelRecord = channelMap.get(connectMessagingMessageRecord.getChannelId()); | ||
| connectMessagingChannelRecord.getMessages().add(connectMessagingMessageRecord); | ||
| } |
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.
🛠️ Refactor suggestion
Optimize database queries to avoid N+1 pattern.
Loading all messages for all channels is inefficient. Consider loading only the latest message per channel for preview generation.
Instead of loading all messages, modify the query to fetch only the latest message per channel:
// Add a method to get only latest messages per channel
public static Map<String, ConnectMessagingMessageRecord> getLatestMessagesPerChannel(Context context) {
// Implementation to fetch only latest message per channel
}🤖 Prompt for AI Agents
In app/src/org/commcare/connect/database/ConnectMessagingDatabaseHelper.java
around lines 31 to 34, the current code loads all messages for all channels
causing an N+1 query problem. To fix this, create a new method that queries and
returns only the latest message per channel, then update the code to use this
method instead of loading all messages. This will optimize database access by
reducing the number of queries and data loaded.
| public static List<ConnectMessagingChannelRecord> getMessagingChannels(Context context) { | ||
| List<ConnectMessagingChannelRecord> channels = ConnectDatabaseHelper.getConnectStorage(context, ConnectMessagingChannelRecord.class) | ||
| .getRecordsForValues(new String[]{}, new Object[]{}); | ||
|
|
||
| Map<String, ConnectMessagingChannelRecord> channelMap = new HashMap<>(); | ||
| for (ConnectMessagingChannelRecord c : channels) { | ||
| channelMap.put(c.getChannelId(), c); | ||
| } | ||
| for (ConnectMessagingMessageRecord connectMessagingMessageRecord : getMessagingMessagesAll(context)) { | ||
| ConnectMessagingChannelRecord connectMessagingChannelRecord = channelMap.get(connectMessagingMessageRecord.getChannelId()); | ||
| connectMessagingChannelRecord.getMessages().add(connectMessagingMessageRecord); | ||
| } | ||
|
|
||
| for(ConnectMessagingChannelRecord channel : channels) { | ||
| List<ConnectMessagingMessageRecord> messages = channel.getMessages(); | ||
| ConnectMessagingMessageRecord lastMessage = messages.size() > 0 ? | ||
| messages.get(messages.size() - 1) : null; | ||
| SpannableString preview; | ||
| if(!channel.getConsented()) { | ||
| preview = new SpannableString(context.getString(R.string.connect_messaging_channel_list_unconsented)); | ||
| } else if(lastMessage != null) { | ||
|
|
||
| String trimmed = lastMessage.getMessage().split("\n")[0]; | ||
| int maxLength = 25; | ||
| if(trimmed.length() > maxLength) { | ||
| trimmed = trimmed.substring(0, maxLength - 3) + "..."; | ||
| } | ||
| preview = new SpannableString(lastMessage.getIsOutgoing()? " "+trimmed:trimmed); | ||
| if(lastMessage.getIsOutgoing()){ | ||
| Drawable drawable = lastMessage.getConfirmed() ? ContextCompat.getDrawable(context, R.drawable.ic_connect_message_read) : ContextCompat.getDrawable(context, R.drawable.ic_connect_message_unread); | ||
| float lineHeight = DimensionUtils.INSTANCE.convertDpToPixel(14); | ||
| drawable.setBounds(0,0,(int) lineHeight, (int) lineHeight); | ||
| preview.setSpan(new ImageSpan(drawable), 0, 1, SPAN_EXCLUSIVE_EXCLUSIVE); | ||
| } | ||
| } else { | ||
| preview = new SpannableString(""); | ||
| } | ||
|
|
||
| channel.setPreview(preview); | ||
| } | ||
|
|
||
| return channels; | ||
| } | ||
|
|
||
| public static ConnectMessagingChannelRecord getMessagingChannel(Context context, String channelId) { | ||
| List<ConnectMessagingChannelRecord> channels = ConnectDatabaseHelper.getConnectStorage(context, ConnectMessagingChannelRecord.class) | ||
| .getRecordsForValues(new String[]{ConnectMessagingChannelRecord.META_CHANNEL_ID}, | ||
| new Object[]{channelId}); | ||
|
|
||
| if(channels.size() > 0) { | ||
| return channels.get(0); | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| public static void storeMessagingChannel(Context context, ConnectMessagingChannelRecord channel) { | ||
| ConnectMessagingChannelRecord existing = getMessagingChannel(context, channel.getChannelId()); | ||
| if(existing != null) { | ||
| channel.setID(existing.getID()); | ||
| } | ||
|
|
||
| ConnectDatabaseHelper.getConnectStorage(context, ConnectMessagingChannelRecord.class).write(channel); | ||
| } | ||
|
|
||
| public static void storeMessagingChannels(Context context, List<ConnectMessagingChannelRecord> channels, boolean pruneMissing) { | ||
| SqlStorage<ConnectMessagingChannelRecord> storage = ConnectDatabaseHelper.getConnectStorage(context, ConnectMessagingChannelRecord.class); | ||
|
|
||
| List<ConnectMessagingChannelRecord> existingList = getMessagingChannels(context); | ||
|
|
||
| //Delete payments that are no longer available | ||
| Vector<Integer> recordIdsToDelete = new Vector<>(); | ||
| for (ConnectMessagingChannelRecord existing : existingList) { | ||
| boolean stillExists = false; | ||
| for (ConnectMessagingChannelRecord incoming : channels) { | ||
| if (existing.getChannelId().equals(incoming.getChannelId())) { | ||
| incoming.setID(existing.getID()); | ||
|
|
||
| incoming.setChannelCreated(existing.getChannelCreated()); | ||
|
|
||
| if(!incoming.getAnsweredConsent()) { | ||
| incoming.setAnsweredConsent(existing.getAnsweredConsent()); | ||
| } | ||
|
|
||
| if(existing.getKey().length() > 0) { | ||
| incoming.setKey(existing.getKey()); | ||
| } | ||
|
|
||
| stillExists = true; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| if (!stillExists && pruneMissing) { | ||
| //Mark the delivery for deletion | ||
| //Remember the ID so we can delete them all at once after the loop | ||
| recordIdsToDelete.add(existing.getID()); | ||
| } | ||
| } | ||
|
|
||
| if (pruneMissing) { | ||
| storage.removeAll(recordIdsToDelete); | ||
| } | ||
|
|
||
| //Now insert/update deliveries | ||
| for (ConnectMessagingChannelRecord incomingRecord : channels) { | ||
| storage.write(incomingRecord); | ||
| } | ||
| } | ||
|
|
||
| public static List<ConnectMessagingMessageRecord> getMessagingMessagesAll(Context context) { | ||
| return ConnectDatabaseHelper.getConnectStorage(context, ConnectMessagingMessageRecord.class) | ||
| .getRecordsForValues(new String[]{}, new Object[]{}); | ||
| } | ||
|
|
||
| public static List<ConnectMessagingMessageRecord> getMessagingMessagesForChannel(Context context, String channelId) { | ||
| return ConnectDatabaseHelper.getConnectStorage(context, ConnectMessagingMessageRecord.class) | ||
| .getRecordsForValues(new String[]{ ConnectMessagingMessageRecord.META_MESSAGE_CHANNEL_ID }, new Object[]{channelId}); | ||
| } | ||
|
|
||
| public static List<ConnectMessagingMessageRecord> getUnviewedMessages(Context context) { | ||
| return ConnectDatabaseHelper.getConnectStorage(context, ConnectMessagingMessageRecord.class) | ||
| .getRecordsForValues(new String[]{ ConnectMessagingMessageRecord.META_MESSAGE_USER_VIEWED }, new Object[]{false}); | ||
| } | ||
|
|
||
| public static void storeMessagingMessage(Context context, ConnectMessagingMessageRecord message) { | ||
| SqlStorage<ConnectMessagingMessageRecord> storage = ConnectDatabaseHelper.getConnectStorage(context, ConnectMessagingMessageRecord.class); | ||
|
|
||
| List<ConnectMessagingMessageRecord> existingList = getMessagingMessagesForChannel(context, message.getChannelId()); | ||
| for (ConnectMessagingMessageRecord existing : existingList) { | ||
| if(existing.getMessageId().equals(message.getMessageId())) { | ||
| message.setID(existing.getID()); | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| storage.write(message); | ||
| } | ||
|
|
||
| public static void storeMessagingMessages(Context context, List<ConnectMessagingMessageRecord> messages, boolean pruneMissing) { | ||
| SqlStorage<ConnectMessagingMessageRecord> storage = ConnectDatabaseHelper.getConnectStorage(context, ConnectMessagingMessageRecord.class); | ||
|
|
||
| List<ConnectMessagingMessageRecord> existingList = getMessagingMessagesAll(context); | ||
|
|
||
| //Delete payments that are no longer available | ||
| Vector<Integer> recordIdsToDelete = new Vector<>(); | ||
| for (ConnectMessagingMessageRecord existing : existingList) { | ||
| boolean stillExists = false; | ||
| for (ConnectMessagingMessageRecord incoming : messages) { | ||
| if (existing.getMessageId().equals(incoming.getMessageId())) { | ||
| incoming.setID(existing.getID()); | ||
| stillExists = true; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| if (!stillExists && pruneMissing) { | ||
| //Mark the delivery for deletion | ||
| //Remember the ID so we can delete them all at once after the loop | ||
| recordIdsToDelete.add(existing.getID()); | ||
| } | ||
| } | ||
|
|
||
| if (pruneMissing) { | ||
| storage.removeAll(recordIdsToDelete); | ||
| } | ||
|
|
||
| //Now insert/update deliveries | ||
| for (ConnectMessagingMessageRecord incomingRecord : messages) { | ||
| storage.write(incomingRecord); | ||
| } | ||
| } |
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.
🛠️ Refactor suggestion
Consider adding error handling for database operations.
Database operations can fail but there's no error handling throughout this class. Consider wrapping database calls in try-catch blocks and logging errors appropriately.
Example pattern:
public static List<ConnectMessagingChannelRecord> getMessagingChannels(Context context) {
try {
// existing implementation
} catch (Exception e) {
Logger.exception("Failed to get messaging channels", e);
return new ArrayList<>(); // Return empty list on failure
}
}🤖 Prompt for AI Agents
In app/src/org/commcare/connect/database/ConnectMessagingDatabaseHelper.java
from lines 23 to 194, add error handling for all database operations by wrapping
calls to ConnectDatabaseHelper and SqlStorage methods in try-catch blocks. In
each catch block, log the exception using a Logger or similar mechanism and
return a safe default value such as an empty list or null where appropriate.
This will prevent unhandled exceptions from crashing the app and provide useful
error logs for debugging.
| import android.content.Context; | ||
| import android.util.Log; | ||
| import android.view.View; | ||
| import android.view.ViewTreeObserver; | ||
| import android.widget.TextView; | ||
|
|
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.
Remove duplicate import statements
These imports are duplicated and will cause compilation errors:
-import android.content.Context;
-import android.util.Log;
-import android.view.View;
-import android.view.ViewTreeObserver;
-import android.widget.TextView;
-📝 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.
| import android.content.Context; | |
| import android.util.Log; | |
| import android.view.View; | |
| import android.view.ViewTreeObserver; | |
| import android.widget.TextView; | |
| // (Lines 12–17 have been removed; duplicate imports are gone) |
🤖 Prompt for AI Agents
In app/src/org/commcare/activities/StandardHomeActivityUIController.java between
lines 12 and 17, there are duplicate import statements that will cause
compilation errors. Review the import section and remove any repeated import
lines so that each import appears only once.
| public static void updateLearningProgress(Context context, ConnectJobRecord job, ConnectActivityCompleteListener listener) { | ||
| ConnectUserRecord user = getUser(context); | ||
| ApiConnect.getLearnProgress(context, user, job.getJobId(), new IApiCallback() { | ||
| private static void reportApiCall(boolean success) { | ||
| FirebaseAnalyticsUtil.reportCccApiLearnProgress(success); | ||
| } | ||
|
|
||
| @Override | ||
| public void processSuccess(int responseCode, InputStream responseData) { | ||
| try { | ||
| String responseAsString = new String(StreamsUtil.inputStreamToByteArray(responseData)); | ||
| if (responseAsString.length() > 0) { | ||
| //Parse the JSON | ||
| JSONObject json = new JSONObject(responseAsString); | ||
|
|
||
| String key = "completed_modules"; | ||
| JSONArray modules = json.getJSONArray(key); | ||
| List<ConnectJobLearningRecord> learningRecords = new ArrayList<>(modules.length()); | ||
| for (int i = 0; i < modules.length(); i++) { | ||
| JSONObject obj = (JSONObject) modules.get(i); | ||
| ConnectJobLearningRecord record = ConnectJobLearningRecord.fromJson(obj, job.getJobId()); | ||
| learningRecords.add(record); | ||
| } | ||
| job.setLearnings(learningRecords); | ||
| job.setCompletedLearningModules(learningRecords.size()); | ||
|
|
||
| key = "assessments"; | ||
| JSONArray assessments = json.getJSONArray(key); | ||
| List<ConnectJobAssessmentRecord> assessmentRecords = new ArrayList<>(assessments.length()); | ||
| for (int i = 0; i < assessments.length(); i++) { | ||
| JSONObject obj = (JSONObject) assessments.get(i); | ||
| ConnectJobAssessmentRecord record = ConnectJobAssessmentRecord.fromJson(obj, job.getJobId()); | ||
| assessmentRecords.add(record); | ||
| } | ||
| job.setAssessments(assessmentRecords); | ||
|
|
||
| ConnectJobUtils.updateJobLearnProgress(context, job); | ||
| } | ||
| } catch (IOException | JSONException | ParseException e) { | ||
| Logger.exception("Parsing return from learn_progress request", e); | ||
| } | ||
|
|
||
| reportApiCall(true); | ||
| listener.connectActivityComplete(true); | ||
| } | ||
|
|
||
| @Override | ||
| public void processFailure(int responseCode, @Nullable InputStream errorResponse) { | ||
| Logger.log("ERROR", String.format(Locale.getDefault(), "Failed: %d", responseCode)); | ||
| reportApiCall(false); | ||
| listener.connectActivityComplete(false); | ||
| } | ||
|
|
||
| @Override | ||
| public void processNetworkFailure() { | ||
| Logger.log("ERROR", "Failed (network)"); | ||
| reportApiCall(false); | ||
| listener.connectActivityComplete(false); | ||
| } | ||
|
|
||
| @Override | ||
| public void processTokenUnavailableError() { | ||
| ConnectNetworkHelper.handleTokenUnavailableException(context); | ||
| reportApiCall(false); | ||
| listener.connectActivityComplete(false); | ||
| } | ||
|
|
||
| @Override | ||
| public void processTokenRequestDeniedError() { | ||
| ConnectNetworkHelper.handleTokenDeniedException(); | ||
| reportApiCall(false); | ||
| listener.connectActivityComplete(false); | ||
| } | ||
|
|
||
| @Override | ||
| public void processOldApiError() { | ||
| ConnectNetworkHelper.showOutdatedApiError(context); | ||
| reportApiCall(false); | ||
| listener.connectActivityComplete(false); | ||
| } | ||
| }); |
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.
🛠️ Refactor suggestion
Refactor duplicate callback error handling code
The IApiCallback implementations have significant code duplication in error handling. This violates the DRY principle and makes maintenance difficult.
Consider creating a base callback class or using a template method pattern:
private abstract static class BaseApiCallback implements IApiCallback {
protected final Context context;
protected final ConnectActivityCompleteListener listener;
protected final String analyticsEvent;
BaseApiCallback(Context context, ConnectActivityCompleteListener listener, String analyticsEvent) {
this.context = context;
this.listener = listener;
this.analyticsEvent = analyticsEvent;
}
protected abstract void handleSuccess(int responseCode, InputStream responseData) throws Exception;
@Override
public void processSuccess(int responseCode, InputStream responseData) {
try {
handleSuccess(responseCode, responseData);
reportApiCall(true);
listener.connectActivityComplete(true);
} catch (Exception e) {
Logger.exception("Error processing response", e);
reportApiCall(false);
listener.connectActivityComplete(false);
}
}
@Override
public void processFailure(int responseCode, InputStream errorResponse) {
Logger.log("ERROR", String.format(Locale.getDefault(), "Failed: %d", responseCode));
reportApiCall(false);
listener.connectActivityComplete(false);
}
// Similar implementations for other error methods...
private void reportApiCall(boolean success) {
FirebaseAnalyticsUtil.reportEvent(analyticsEvent, success);
}
}Also applies to: 481-601, 604-656
🤖 Prompt for AI Agents
In app/src/org/commcare/connect/ConnectManager.java from lines 398 to 478,
refactor the IApiCallback anonymous class to remove duplicated error handling
code by creating a base callback class that implements IApiCallback and
centralizes common error handling logic. Move the success processing logic to an
abstract method in the base class that subclasses override. Ensure all error
callbacks log errors, report analytics, and notify the listener consistently in
the base class. Apply the same refactoring pattern to similar callback
implementations in lines 481-601 and 604-656.
| private static ConnectManager getInstance() { | ||
| if (manager == null) { | ||
| manager = new ConnectManager(); | ||
| } | ||
|
|
||
| return manager; | ||
| } |
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.
Singleton implementation is not thread-safe
The current implementation could create multiple instances in a multi-threaded environment, which defeats the purpose of a singleton and could lead to state inconsistencies.
- private static ConnectManager getInstance() {
- if (manager == null) {
- manager = new ConnectManager();
- }
-
- return manager;
+ private static ConnectManager getInstance() {
+ if (manager == null) {
+ synchronized (ConnectManager.class) {
+ if (manager == null) {
+ manager = new ConnectManager();
+ }
+ }
+ }
+ return manager;
}Alternatively, use the initialization-on-demand holder pattern for better performance:
private static class SingletonHolder {
private static final ConnectManager INSTANCE = new ConnectManager();
}
private static ConnectManager getInstance() {
return SingletonHolder.INSTANCE;
}🤖 Prompt for AI Agents
In app/src/org/commcare/connect/ConnectManager.java around lines 91 to 97, the
singleton getInstance method is not thread-safe and may create multiple
instances in concurrent scenarios. Refactor the singleton implementation to use
the initialization-on-demand holder pattern by creating a private static inner
class holding the single ConnectManager instance and modifying getInstance to
return that instance. This ensures thread safety and lazy initialization without
synchronization overhead.
| public static String generatePassword() { | ||
| int passwordLength = 20; | ||
|
|
||
| String charSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_!.?"; | ||
| StringBuilder password = new StringBuilder(); | ||
| for (int i = 0; i < passwordLength; i++) { | ||
| password.append(charSet.charAt(new Random().nextInt(charSet.length()))); | ||
| } | ||
|
|
||
| return password.toString(); |
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.
Use SecureRandom for cryptographically secure password generation
Using java.util.Random for password generation is not cryptographically secure and could lead to predictable passwords.
+import java.security.SecureRandom;
public static String generatePassword() {
int passwordLength = 20;
+ SecureRandom secureRandom = new SecureRandom();
String charSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_!.?";
StringBuilder password = new StringBuilder();
for (int i = 0; i < passwordLength; i++) {
- password.append(charSet.charAt(new Random().nextInt(charSet.length())));
+ password.append(charSet.charAt(secureRandom.nextInt(charSet.length())));
}
return password.toString();
}📝 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.
| public static String generatePassword() { | |
| int passwordLength = 20; | |
| String charSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_!.?"; | |
| StringBuilder password = new StringBuilder(); | |
| for (int i = 0; i < passwordLength; i++) { | |
| password.append(charSet.charAt(new Random().nextInt(charSet.length()))); | |
| } | |
| return password.toString(); | |
| import java.security.SecureRandom; | |
| public static String generatePassword() { | |
| int passwordLength = 20; | |
| SecureRandom secureRandom = new SecureRandom(); | |
| String charSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_!.?"; | |
| StringBuilder password = new StringBuilder(); | |
| for (int i = 0; i < passwordLength; i++) { | |
| password.append(charSet.charAt(secureRandom.nextInt(charSet.length()))); | |
| } | |
| return password.toString(); | |
| } |
🤖 Prompt for AI Agents
In app/src/org/commcare/connect/ConnectManager.java around lines 327 to 336,
replace the use of java.util.Random with java.security.SecureRandom for
generating the password. Initialize a SecureRandom instance once and use it to
select characters from the charSet to ensure the password is generated in a
cryptographically secure manner.
| private static ConnectJobRecord activeJob = null; | ||
|
|
||
|
|
||
| public static boolean wasAppLaunchedFromConnect(String appId) { | ||
| String primed = primedAppIdForAutoLogin; | ||
| primedAppIdForAutoLogin = null; | ||
| return primed != null && primed.equals(appId); | ||
| } | ||
|
|
||
| public static void setActiveJob(ConnectJobRecord job) { | ||
| activeJob = job; | ||
| } | ||
|
|
||
| public static ConnectJobRecord getActiveJob() { | ||
| return activeJob; | ||
| } |
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.
🛠️ Refactor suggestion
Inconsistent singleton pattern - static fields accessed directly
Static fields activeJob and primedAppIdForAutoLogin are accessed directly instead of through the singleton instance. This breaks encapsulation and makes the singleton pattern inconsistent.
Consider making these fields non-static and accessing them through the singleton instance:
- private static ConnectJobRecord activeJob = null;
+ private ConnectJobRecord activeJob = null;
- public static void setActiveJob(ConnectJobRecord job) {
- activeJob = job;
+ public static void setActiveJob(ConnectJobRecord job) {
+ getInstance().activeJob = job;
}
- public static ConnectJobRecord getActiveJob() {
- return activeJob;
+ public static ConnectJobRecord getActiveJob() {
+ return getInstance().activeJob;
}Apply the same pattern to primedAppIdForAutoLogin.
📝 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.
| private static ConnectJobRecord activeJob = null; | |
| public static boolean wasAppLaunchedFromConnect(String appId) { | |
| String primed = primedAppIdForAutoLogin; | |
| primedAppIdForAutoLogin = null; | |
| return primed != null && primed.equals(appId); | |
| } | |
| public static void setActiveJob(ConnectJobRecord job) { | |
| activeJob = job; | |
| } | |
| public static ConnectJobRecord getActiveJob() { | |
| return activeJob; | |
| } | |
| // remove static modifier to encapsulate in singleton instance | |
| private ConnectJobRecord activeJob = null; | |
| public static void setActiveJob(ConnectJobRecord job) { | |
| getInstance().activeJob = job; | |
| } | |
| public static ConnectJobRecord getActiveJob() { | |
| return getInstance().activeJob; | |
| } |
🤖 Prompt for AI Agents
In app/src/org/commcare/connect/ConnectManager.java around lines 166 to 181, the
static fields activeJob and primedAppIdForAutoLogin are accessed directly,
breaking the singleton pattern and encapsulation. To fix this, make these fields
non-static and access them via the singleton instance of ConnectManager. Update
all methods that use these fields to reference them through the singleton
instance rather than directly as static fields.
… CCCT-1317-app-crash-otp
Backup code error when not matching criteria
|
@damagatchi retest this please |
Handling a JSON Exception properly.
Crashing on JSONExceptions or unknown errors by default No generic toast message about not being able to reach server.
…nto dv/personalid_api_error_handling
…ents can display in UI (instead of toast messages).
correct error handling
Correct error handling for network requests
|
@damagatchi retest this please |
Compile issue solved due to string not present in default locale
Relaying on default focus behavior when Next button pressed (from first to second backup code input).
Handling Next and Done keys in Backup Code Fragment
Cancelling config workflow when device isn't compatible
|
@damagatchi retest this please |
Backmerge for latest changes as part for beta release.