Skip to content
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

Referral Notifications - Background Sync #2680

Merged
merged 55 commits into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
f865524
Add FCM Data Syncer
avazirna Jul 7, 2023
8fc36d8
Assess current state of the app
avazirna Jul 7, 2023
2500bb5
Alert user about pending sync
avazirna Jul 7, 2023
13c1ca4
Block unsafe events when a background sync is underway
avazirna Jul 9, 2023
491e08d
Trigger post login pending sync
avazirna Jul 10, 2023
b363687
Publish background sync updates
avazirna Jul 10, 2023
8652d7b
Disable pending sync
avazirna Jul 10, 2023
076adaa
Cancel unnecessary sync
avazirna Jul 10, 2023
1080733
Finalize background sync
avazirna Jul 12, 2023
e5317c7
Skip certain restore file elements from being parsed
avazirna Jul 13, 2023
7f137fa
Refactor
avazirna Jul 14, 2023
13cf532
Make FCM Message data model externalizable
avazirna Jul 14, 2023
d72ed7c
Store FCM Message data for post form submission sync
avazirna Jul 14, 2023
1b529f3
Trigger sync post form submission
avazirna Jul 14, 2023
106b465
Trigger background sync
avazirna Jul 14, 2023
11516b7
Skip fixtures
avazirna Jul 26, 2023
1194304
Remove skip parsing logic
avazirna Jul 26, 2023
e6a6ea6
Lint
avazirna Aug 4, 2023
c61d023
Refactor
avazirna Aug 4, 2023
722b24f
Trigger broadcast about pending sync
avazirna Aug 4, 2023
78439ad
Refactor
avazirna Aug 4, 2023
d2e4af1
Broadcast sync complete to CommCare
avazirna Aug 7, 2023
d683391
Enable Data update receiver when background sync is enabled
avazirna Aug 7, 2023
2f0f48b
Broadcast data update for background syncs
avazirna Aug 7, 2023
0bc0b04
Refactor
avazirna Aug 11, 2023
a13afe8
Revert
avazirna Aug 14, 2023
0c2f463
Rebuild session after sync
avazirna Aug 14, 2023
308a828
Make post login sync non-user triggered
avazirna Aug 14, 2023
7ca96a8
Remove unnecessary lock
avazirna Aug 14, 2023
51510e4
Upload unsent forms
avazirna Aug 16, 2023
6e0d9d7
Update tests
avazirna Aug 21, 2023
7ed33cf
Lint
avazirna Aug 21, 2023
71a7306
Update FCM string resource keys
avazirna Aug 22, 2023
dfbe5ec
Add flag to monitor sync complete broadcast registration
avazirna Aug 22, 2023
1c84b3a
Revert
avazirna Aug 22, 2023
c0412b5
Restart activity after background sync
avazirna Aug 23, 2023
e699380
Lint
avazirna Aug 23, 2023
cfcd5dc
Refactor
avazirna Aug 23, 2023
6d42c01
Lint
avazirna Aug 23, 2023
414cccb
Set ForegroundInfo for expedited work
avazirna Aug 24, 2023
c7a70c1
Remove observer after work completion
avazirna Aug 24, 2023
cd27032
Revert
avazirna Aug 30, 2023
b9525ed
Nit
avazirna Aug 30, 2023
68e05d0
Refactor
avazirna Aug 30, 2023
415bc8b
Set activities that should restart after sync
avazirna Aug 31, 2023
f6416f3
Refactor
avazirna Aug 31, 2023
bb99c16
Refactor
avazirna Aug 31, 2023
30e2de6
Handle malformed dates
avazirna Aug 31, 2023
e1daf72
Refactor
avazirna Aug 31, 2023
ff10165
Refactor
avazirna Sep 4, 2023
be12087
Merge shared preferences post-form-submission-sync and background-syn…
avazirna Sep 4, 2023
edfce31
Log exception
avazirna Sep 4, 2023
65cc28e
Resolve merge conflicts
avazirna Oct 3, 2023
f1fbb2a
Fix unit test
avazirna Oct 3, 2023
d96060d
Android 13 notification change
avazirna Oct 3, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 8 additions & 9 deletions app/src/org/commcare/activities/HomeScreenBaseActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import static org.commcare.activities.DispatchActivity.EXIT_AFTER_FORM_SUBMISSION;
import static org.commcare.activities.DispatchActivity.EXIT_AFTER_FORM_SUBMISSION_DEFAULT;
import static org.commcare.activities.DispatchActivity.REBUILD_SESSION;
import static org.commcare.activities.DispatchActivity.SESSION_ENDPOINT_ARGUMENTS_BUNDLE;
import static org.commcare.activities.DispatchActivity.SESSION_ENDPOINT_ARGUMENTS_LIST;
import static org.commcare.activities.DispatchActivity.SESSION_ENDPOINT_ID;
Expand Down Expand Up @@ -172,7 +171,8 @@ public abstract class HomeScreenBaseActivity<T> extends SyncCapableCommCareActiv
protected boolean showCommCareUpdateMenu = false;
private static final int MAX_CC_UPDATE_CANCELLATION = 3;

public static boolean safeToTriggerBackgroundSyncOnResume = true;
// This is to trigger a background sync after a form submission,
public static boolean shouldTriggerBackgroundSync = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

should be private boolean shouldTriggerBackgroundSync


// This is to restore the selected entity when restarting EntityDetailActivity after a
// background sync
Expand Down Expand Up @@ -353,10 +353,9 @@ private boolean doLoginLaunchChecksInOrder() {

// In case a sync request from FCM was made while the user was logged out, this will
// trigger a blocking sync
if (HiddenPreferences.isPendingSyncRequestFromServer() &&
HiddenPreferences.getPendingSyncRequestFromServerTime()>SyncDetailCalculations.getLastSyncTime()) {
HiddenPreferences.setPendingSyncRequestFromServer(false);
if (HiddenPreferences.isPendingSyncRequestFromServerForUser()) {
sendFormsOrSync(false);

return true;
}

Expand Down Expand Up @@ -405,7 +404,7 @@ private boolean showUpdateInfoForm() {
private void clearOneTimeLoginActionFlags() {
HiddenPreferences.setPostUpdateSyncNeeded(false);
HiddenPreferences.clearInterruptedSSD();
HiddenPreferences.setPendingSyncRequestFromServer(false);
HiddenPreferences.clearPendingSyncRequestFromServerForUser();
}

private boolean tryRestoringFormFromSessionExpiration() {
Expand Down Expand Up @@ -1232,7 +1231,7 @@ private void formEntry(int formDefId, FormRecord r, String headerTitle,
boolean isRestartAfterSessionExpiration) {

// Block any background syncs during a form entry
safeToTriggerBackgroundSyncOnResume = false;
shouldTriggerBackgroundSync = false;

Logger.log(LogTypes.TYPE_FORM_ENTRY, "Form Entry Starting|" +
(r.getInstanceID() == null ? "" : r.getInstanceID() + "|") +
Expand Down Expand Up @@ -1301,14 +1300,14 @@ public void onResumeSessionSafe() {
}

// In case a Sync was blocked because of a form entry, trigger now if it's safe
if (HiddenPreferences.isPostFormSubmissionSyncNeeded() && safeToTriggerBackgroundSyncOnResume) {
if (HiddenPreferences.isPostFormSubmissionSyncNeeded() && shouldTriggerBackgroundSync) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Won't shouldTriggerBackgroundSync will always be false here when coming back from From Entry ? If so, how does the sync gets initiated after form submission ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@shubham1g5 Not necessarily, when we open a form onActivityResultSessionSafe from HomeScreenBaseActivity in called, this then triggers the logic to start a new Form Entry session, and here we set shouldTriggerBackgroundSync to false, so by the time onResumeSessionSafe is called, shouldTriggerBackgroundSync is false and the sync is not triggered. Also, during onResumeSessionSafe, shouldTriggerBackgroundSync is reset to true, which means that when onResumeSessionSafe is called again, the sync can be triggered (if necessary), unless a new Form Entry Session is starts.

Copy link
Contributor

Choose a reason for hiding this comment

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

which means that when onResumeSessionSafe is called again, the sync can be triggered (if necessary), unless a new Form Entry Session is starts.

But that means that sync won't get triggered immediately after user completes the form but only when user comes back to home screen one more time.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@shubham1g5 I think we are supposed to go back to HomeScreenBaseActivity after a form submission to determine the next step. Besides, we start a FormEntry activity for result in HomeScreenBaseActivity. Are there other scenarios that I'm missing here?

Copy link
Contributor

Choose a reason for hiding this comment

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

I am considering the scenario where a user go through this navigation -

  1. Home Screen -> Start -> Module -> Form Entry
  2. Sync request from server
  3. Form Entry Complete
  4. Home Screen (no eof navigation) but no sync
  5. User goes out and back into the home screen -> Sync gets triggered.

Is my interpretaion of sync behaviour in step 4 and 5 right based on the current code ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why wouldn't there be a sync in number 4? After a form submission, we would navigate through HomeSreenBaseActivity and shouldTriggerBackgroundSync would be true, so a sync would be triggered

Copy link
Contributor

@shubham1g5 shubham1g5 Aug 31, 2023

Choose a reason for hiding this comment

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

I see, My understanding is that HomeSreenBaseActivity doesn't reinitialise when onActivityResult gets called on the activity as there might already be a live instance of that activity in the background. But I might be wrong here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I think so too, but why does it need to be reinitialized for the sync to occur?

Copy link
Contributor

Choose a reason for hiding this comment

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

shouldTriggerBackgroundSync is set to false in startFormEntry and will remain false without HomeSreenBaseActivity getting reinitialised after step 3 above.

dataSyncer.syncData(HiddenPreferences.getPostFormSubmissionSyncNeededFCMMessageData());
}

// reset these
redirectedInOnCreate = false;
sessionNavigationProceedingAfterOnResume = false;
safeToTriggerBackgroundSyncOnResume = true;
shouldTriggerBackgroundSync = true;
}

private void attemptDispatchHomeScreen() {
Expand Down
46 changes: 31 additions & 15 deletions app/src/org/commcare/preferences/HiddenPreferences.java
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public class HiddenPreferences {
private static final long NO_OF_HOURS_TO_WAIT_TO_RESUME_BACKGROUND_WORK = 36;

// This is to be used by CommCareFirebaseMessagingService to schedule a sync after the next Login
public final static String PENDING_SYNC_REQUEST_FROM_SERVER = "pending-sync-request-from-server";
public final static String PENDING_SYNC_REQUEST_FROM_SERVER = "pending-sync-request-from-server-";
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: can just call it this BACKGROUND_SYNC_PENDING and accordingly rename the related methods that access the preference.


private static final String ENABLE_ANDROID_WINDOW_SECURE_FLAG = "cc-enable-android-window-secure-flag";

Expand All @@ -110,6 +110,12 @@ public class HiddenPreferences {
private static final String POST_FOR_SUBMISSION_SYNC_NEEDED_FCM_MESSAGE_DATA = "post-form-submission-sync-needed-fcm-message-data";
private static final String ENABLE_BACKGROUND_SYNC = "cc-enable-background-sync";

/**
* The domain name in the application profile file comes in the <domain>.commcarehq.org form,
* this is standard across the different HQ servers. This constant is to store that suffix and
* be used to remove it form the user domain name to match how the domain represented in the backend
*/
public static final String USER_DOMAIN_SERVER_URL_SUFFIX = ".commcarehq.org";

/**
* @return How many seconds should a user session remain open before expiring?
Expand Down Expand Up @@ -272,6 +278,18 @@ public static String getUserDomain() {
return prefs.getString(USER_DOMAIN_SUFFIX, null);
}

public static String getUserDomainWithoutServerUrl() {
String userDomain = getUserDomain();
if (userDomain == null){
return null;
}

if (userDomain.contains(USER_DOMAIN_SERVER_URL_SUFFIX)){
return userDomain.replace(USER_DOMAIN_SERVER_URL_SUFFIX, "");
}
return userDomain;
}

public static void setPostUpdateSyncNeeded(boolean b) {
CommCareApplication.instance().getCurrentApp().getAppPreferences().edit()
.putBoolean(POST_UPDATE_SYNC_NEEDED, b).apply();
Expand Down Expand Up @@ -565,30 +583,28 @@ public static void markRawMediaCleanUpComplete() {
.edit().putBoolean(RAW_MEDIA_CLEANUP_COMPLETE, true).apply();
}

public static boolean isPendingSyncRequestFromServer() {
public static boolean isPendingSyncRequestFromServerForUser() {
Copy link
Contributor

Choose a reason for hiding this comment

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

  1. this method should ideally take username as a method parameter so that we don't cause session exceptions in here.
  2. Confirming that loggedusername is not the full username like - [email protected] ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  1. This is to check if there is a pending sync for the current logged in user, it's triggered right after a successful login. I can rename it to make this more clear.
  2. Confirmed.

Copy link
Contributor

Choose a reason for hiding this comment

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

I understand, although my suggestion is to avoid hidden session exception if in future we call this method from other places. Supplying username to this method will ensure that caller of this method will handle implications of session not being available at that time.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's a good point

String loggedUsername = CommCareApplication.instance().getSession().getLoggedInUser().getUsername();
return PreferenceManager.getDefaultSharedPreferences(CommCareApplication.instance())
.getBoolean(PENDING_SYNC_REQUEST_FROM_SERVER, false);
.contains(PENDING_SYNC_REQUEST_FROM_SERVER + loggedUsername + "@"+ getUserDomainWithoutServerUrl());
}
public static void setPendingSyncRequestFromServer(boolean syncNeeded) {
public static void setPendingSyncRequestFromServerForUser(FCMMessageData fcmMessageData) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: can rename all methods for BACKGROUND_SYNC_PENDING as well - setBackgroundSyncPending(), isBackgroundSyncPending() and clearBackgroundSyncPending()

PreferenceManager.getDefaultSharedPreferences(CommCareApplication.instance()).edit()
.putBoolean(PENDING_SYNC_REQUEST_FROM_SERVER, syncNeeded)
.putBoolean(PENDING_SYNC_REQUEST_FROM_SERVER + fcmMessageData.getUsername() + "@"+ fcmMessageData.getDomain(), true)
.apply();
}

public static boolean isFlagSecureEnabled() {
return DeveloperPreferences.doesPropertyMatch(ENABLE_ANDROID_WINDOW_SECURE_FLAG, PrefValues.NO, PrefValues.YES);
}
public static long getPendingSyncRequestFromServerTime() {
return PreferenceManager.getDefaultSharedPreferences(CommCareApplication.instance())
.getLong(PENDING_SYNC_REQUEST_FROM_SERVER_TIME, 0);
}

public static void setPendingSyncRequestFromServerTime(long requestTime) {
public static void clearPendingSyncRequestFromServerForUser() {
String loggedUsername = CommCareApplication.instance().getSession().getLoggedInUser().getUsername();
PreferenceManager.getDefaultSharedPreferences(CommCareApplication.instance()).edit()
.putLong(PENDING_SYNC_REQUEST_FROM_SERVER_TIME, requestTime)
.remove(PENDING_SYNC_REQUEST_FROM_SERVER + loggedUsername + "@"+ getUserDomainWithoutServerUrl())
.apply();
}

public static boolean isFlagSecureEnabled() {
return DeveloperPreferences.doesPropertyMatch(ENABLE_ANDROID_WINDOW_SECURE_FLAG, PrefValues.NO, PrefValues.YES);
}

public static boolean isPostFormSubmissionSyncNeeded() {
return CommCareApplication.instance().getCurrentApp().getAppPreferences()
.getBoolean(POST_FOR_SUBMISSION_SYNC_NEEDED, false);
Expand Down
30 changes: 4 additions & 26 deletions app/src/org/commcare/sync/FirebaseMessagingDataSyncer.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package org.commcare.sync;


import static org.commcare.utils.FirebaseMessagingUtil.removeServerUrlFromUserDomain;

import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
Expand Down Expand Up @@ -53,7 +51,7 @@ public FirebaseMessagingDataSyncer(Context context) {
this.context = context;
}

private List<String> syncSafeActivities = Arrays.asList(new String[]{"FormEntryActivity"});
private List<String> syncUnsafeActivities = Arrays.asList(new String[]{"FormEntryActivity"});
private CommCareTask currentTask = null;
private PinnedNotificationWithProgress<DataPullTask.PullTaskResult> mPinnedNotificationProgress = null;

Expand All @@ -65,21 +63,9 @@ public FirebaseMessagingDataSyncer(Context context) {
* 2) Ensure that the sync is only triggered for the intended 'recipient' of the message
*/
public void syncData(FCMMessageData fcmMessageData) {
// Abort if FCM Message data is null
if (fcmMessageData == null){
disablePendingSyncs();
return;
}

if (!CommCareApplication.isSessionActive()) {
// There is no active session at the moment, proceed accordingly
// TODO: Decide whether to only trigger the Sync when the 'recipient' of the message logs in
// or anyone, in case multiple users are sharing the same device
// TODO: Decide whether to check if when there is no active session, the recipient has ever
// logged in the device, before scheduling a sync post login
HiddenPreferences.setPendingSyncRequestFromServer(true);
HiddenPreferences.setPendingSyncRequestFromServerTime(fcmMessageData.getCreationTime().getMillis());

HiddenPreferences.setPendingSyncRequestFromServerForUser(fcmMessageData);
return;
}
// Retrieve the current User
Expand Down Expand Up @@ -199,14 +185,14 @@ protected void onPostExecute(ResultAndError<PullTaskResult> resultAndErrorMessag
private boolean checkUserAndDomain(User user, String payloadUsername, String payloadDomain) {
if(payloadUsername != null && payloadDomain != null){
String loggedInUsername = user.getUsername();
String userDomain = removeServerUrlFromUserDomain(HiddenPreferences.getUserDomain());
String userDomain = HiddenPreferences.getUserDomainWithoutServerUrl();
return payloadUsername.equalsIgnoreCase(loggedInUsername) && payloadDomain.equalsIgnoreCase(userDomain);
}
return false;
}

private boolean isCurrentActivitySyncSafe() {
return !syncSafeActivities.contains(CommCareApplication.currentActivityName);
return !syncUnsafeActivities.contains(CommCareApplication.currentActivityName);
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of setting the currentActivityName in CommCareApplication, you can directly have a variable to set backgroundSyncSafe that you can update from FormEntryActivity . This way every activity is deciding whether it can handle background sync instead of having to maintain a centralised list of classes in this class.

}

// This method is responsible for informing the User about a pending sync and scheduling
Expand All @@ -233,14 +219,6 @@ public void startBlockingForTask(int id) {
}
mPinnedNotificationProgress = new PinnedNotificationWithProgress(context,
"sync.communicating.title","sync.progress.starting", -1);

// Disable any pending sync
disablePendingSyncs();
}

private void disablePendingSyncs() {
HiddenPreferences.setPendingSyncRequestFromServer(false);
HiddenPreferences.setPostFormSubmissionSyncNeeded(false);
}

@Override
Expand Down
5 changes: 5 additions & 0 deletions app/src/org/commcare/tasks/DataPullTask.java
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,11 @@ private void onSuccessfulSync() {

Logger.log(LogTypes.TYPE_USER, "User Sync Successful|" + username);
updateCurrentUser(password);

// Disable pending background syncs
HiddenPreferences.clearPendingSyncRequestFromServerForUser();
HiddenPreferences.setPostFormSubmissionSyncNeeded(false);

this.publishProgress(PROGRESS_DONE);
}

Expand Down
18 changes: 0 additions & 18 deletions app/src/org/commcare/utils/FirebaseMessagingUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,6 @@ public class FirebaseMessagingUtil {
public static final String FCM_TOKEN = "fcm_token";
public static final String FCM_TOKEN_TIME = "fcm_token_time";

/**
* The domain name in the application profile file comes in the <domain>.commcarehq.org form,
* this is standard across the different HQ servers. This constant is to store that suffix and
* be used to remove it form the user domain name to match how the domain represented in the backend
*/
public static final String USER_DOMAIN_SERVER_URL_SUFFIX = ".commcarehq.org";

public static String getFCMToken() {
return PreferenceManager
.getDefaultSharedPreferences(CommCareApplication.instance())
Expand All @@ -43,17 +36,6 @@ public static void updateFCMToken(String newToken) {
sharedPreferences.edit().putLong(FCM_TOKEN_TIME,System.currentTimeMillis()).apply();
}

public static String removeServerUrlFromUserDomain(String userDomain) {
if (userDomain == null){
return null;
}

if (userDomain.contains(USER_DOMAIN_SERVER_URL_SUFFIX)){
return userDomain.replace(USER_DOMAIN_SERVER_URL_SUFFIX, "");
}
return userDomain;
}

public static void verifyToken() {
// TODO: Enable FCM in debug mode
if(!BuildConfig.DEBUG){
Expand Down