verificationLauncher = registerForActivityResult(
+ new ActivityResultContracts.StartActivityForResult(),
+ result -> {
+ if (result.getResultCode() == Activity.RESULT_OK) {
+ ConnectDownloadingFragment connectDownloadFragment = getConnectDownloadFragment();
+ if (connectDownloadFragment != null) {
+ connectDownloadFragment.onSuccessfulVerification();
+ }
+ }
+ });
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.screen_connect);
+ setTitle(getString(R.string.connect_title));
+ getIntentData();
+ updateBackButton();
+
+ destinationListener = FirebaseAnalyticsUtil.getDestinationChangeListener();
+
+ NavHostFragment host = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment_connect);
+ navController = host.getNavController();
+ navController.addOnDestinationChangedListener(destinationListener);
+
+ if (getIntent().getBooleanExtra("info", false)) {
+ ConnectJobRecord job = ConnectManager.getActiveJob();
+ int fragmentId = job.getStatus() == ConnectJobRecord.STATUS_DELIVERING ?
+ R.id.connect_job_delivery_progress_fragment :
+ R.id.connect_job_learning_progress_fragment;
+
+ boolean buttons = getIntent().getBooleanExtra("buttons", true);
+
+ Bundle bundle = new Bundle();
+ bundle.putBoolean("showLaunch", buttons);
+
+ NavOptions options = new NavOptions.Builder()
+ .setPopUpTo(navController.getGraph().getStartDestinationId(), true)
+ .build();
+ navController.navigate(fragmentId, bundle, options);
+ } else if (redirectionAction != null) {
+ ConnectManager.init(this);
+ ConnectManager.unlockConnect(this, success -> {
+ if (success) {
+ getJobDetails();
+ }
+ });
+ }
+ }
+
+ /**
+ * Returns the fragment ID based on the redirection action.
+ *
+ * This method determines which fragment should be displayed based on the value of the redirectionAction.
+ * It maps specific actions to their corresponding fragment IDs.
+ *
+ * @return The ID of the fragment to be displayed.
+ */
+ private int getFragmentId() {
+ int fragmentId;
+ if (redirectionAction.equals(CCC_OPPORTUNITY_SUMMARY_PAGE)) {
+ fragmentId = R.id.connect_job_intro_fragment;
+ } else if (redirectionAction.equals(CCC_LEARN_PROGRESS)) {
+ fragmentId = R.id.connect_job_learning_progress_fragment;
+ } else {
+ fragmentId = R.id.connect_job_delivery_progress_fragment;
+ }
+ return fragmentId;
+ }
+
+ private void getIntentData() {
+ redirectionAction = getIntent().getStringExtra("action");
+ opportunityId = getIntent().getStringExtra("opportunity_id");
+ }
+
+ /**
+ * Sets the fragment redirection based on the redirection action.
+ *
+ * This method determines the fragment to be displayed using the getFragmentId() method,
+ * prepares a bundle with additional data, and navigates to the appropriate fragment.
+ */
+ private void setFragmentRedirection(boolean ApiSuccess) {
+ if (ApiSuccess) {
+ int fragmentId = getFragmentId();
+
+ boolean buttons = getIntent().getBooleanExtra("buttons", true);
+ Bundle bundle = new Bundle();
+ bundle.putBoolean("showLaunch", buttons);
+
+ // Set the tab position in the bundle based on the redirection action
+ if (redirectionAction.equals(CCC_DELIVERY_PROGRESS)) {
+ bundle.putString("tabPosition", "0");
+ } else if (redirectionAction.equals(CCC_PAYMENTS)) {
+ bundle.putString("tabPosition", "1");
+ }
+
+ NavOptions options = new NavOptions.Builder()
+ .setPopUpTo(navController.getGraph().getStartDestinationId(), true)
+ .build();
+ navController.navigate(fragmentId, bundle, options);
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (backButtonEnabled) {
+ super.onBackPressed();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ if (destinationListener != null) {
+ NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager()
+ .findFragmentById(R.id.nav_host_fragment_connect);
+ if (navHostFragment != null) {
+ NavController navController = navHostFragment.getNavController();
+ navController.removeOnDestinationChangedListener(destinationListener);
+ }
+ destinationListener = null;
+ }
+
+ super.onDestroy();
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ ConnectManager.handleFinishedActivity(this, requestCode, resultCode, intent);
+ super.onActivityResult(requestCode, resultCode, intent);
+ }
+
+ @Override
+ public CustomProgressDialog generateProgressDialog(int taskId) {
+ if (waitDialogEnabled) {
+ return CustomProgressDialog.newInstance(null, getString(R.string.please_wait), taskId);
+ }
+
+ return null;
+ }
+
+ public void setBackButtonEnabled(boolean enabled) {
+ backButtonEnabled = enabled;
+ }
+
+ public void setWaitDialogEnabled(boolean enabled) {
+ waitDialogEnabled = enabled;
+ }
+
+ private void updateBackButton() {
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayShowHomeEnabled(isBackEnabled());
+ actionBar.setDisplayHomeAsUpEnabled(isBackEnabled());
+ }
+ }
+
+ @Override
+ protected boolean shouldShowBreadcrumbBar() {
+ return false;
+ }
+
+ @Override
+ public ResourceEngineListener getReceiver() {
+ return getConnectDownloadFragment();
+ }
+
+ @Nullable
+ private ConnectDownloadingFragment getConnectDownloadFragment() {
+ NavHostFragment navHostFragment =
+ (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment_connect);
+ Fragment currentFragment =
+ navHostFragment.getChildFragmentManager().getPrimaryNavigationFragment();
+ if (currentFragment instanceof ConnectDownloadingFragment) {
+ return (ConnectDownloadingFragment) currentFragment;
+ }
+ return null;
+ }
+
+ public void startAppValidation() {
+ Intent i = new Intent(this, CommCareVerificationActivity.class);
+ i.putExtra(CommCareVerificationActivity.KEY_LAUNCH_FROM_SETTINGS, true);
+ verificationLauncher.launch(i);
+ }
+
+ public void getJobDetails() {
+ ApiConnect.getConnectOpportunities(ConnectActivity.this, new IApiCallback() {
+ @Override
+ public void processSuccess(int responseCode, InputStream responseData) {
+ try {
+ String responseAsString = new String(StreamsUtil.inputStreamToByteArray(responseData));
+ if (responseAsString.length() > 0) {
+ //Parse the JSON
+ JSONArray json = new JSONArray(responseAsString);
+ List jobs = new ArrayList<>(json.length());
+ for (int i = 0; i < json.length(); i++) {
+ JSONObject obj = (JSONObject) json.get(i);
+ ConnectJobRecord job = ConnectJobRecord.fromJson(obj);
+ jobs.add(job);
+ if (job.getJobId() == Integer.parseInt(opportunityId)) {
+ ConnectManager.setActiveJob(job);
+ }
+ }
+ ConnectDatabaseHelper.storeJobs(ConnectActivity.this, jobs, true);
+ setFragmentRedirection(true);
+ }
+ } catch (IOException | JSONException | ParseException e) {
+ setFragmentRedirection(false);
+ Toast.makeText(ConnectActivity.this, R.string.connect_job_list_api_failure, Toast.LENGTH_SHORT).show();
+ Logger.exception("Parsing return from Opportunities request", e);
+ }
+ }
+
+ @Override
+ public void processFailure(int responseCode, IOException e) {
+ setFragmentRedirection(false);
+ Toast.makeText(ConnectActivity.this, R.string.connect_job_list_api_failure, Toast.LENGTH_SHORT).show();
+ Logger.log("ERROR", String.format(Locale.getDefault(), "Opportunities call failed: %d", responseCode));
+ }
+
+ @Override
+ public void processNetworkFailure() {
+ setFragmentRedirection(false);
+ Toast.makeText(ConnectActivity.this, R.string.recovery_network_unavailable, Toast.LENGTH_SHORT).show();
+ Logger.log("ERROR", "Failed (network)");
+ }
+
+ @Override
+ public void processOldApiError() {
+ setFragmentRedirection(false);
+ Toast.makeText(ConnectActivity.this, R.string.connect_job_list_api_failure, Toast.LENGTH_SHORT).show();
+ ConnectNetworkHelper.showOutdatedApiError(ConnectActivity.this);
+ }
+ });
+ }
+}
diff --git a/app/src/org/commcare/activities/connect/ConnectIdPhoneVerificationActivityUiController.java b/app/src/org/commcare/activities/connect/ConnectIdPhoneVerificationActivityUiController.java
index 4814bdfc5..e1fa74d4a 100644
--- a/app/src/org/commcare/activities/connect/ConnectIdPhoneVerificationActivityUiController.java
+++ b/app/src/org/commcare/activities/connect/ConnectIdPhoneVerificationActivityUiController.java
@@ -74,9 +74,8 @@ public void requestInputFocus() {
public String getCode() {
return codeInput.getText().toString();
}
-
public void setCode(String code) {
- codeInput.setText(code);
+ codeInput.setText(code);
}
public void setErrorMessage(String message) {
diff --git a/app/src/org/commcare/activities/connect/ConnectIdRecoveryDecisionActivityUiController.java b/app/src/org/commcare/activities/connect/ConnectIdRecoveryDecisionActivityUiController.java
index 3492a43c5..7efc1c831 100644
--- a/app/src/org/commcare/activities/connect/ConnectIdRecoveryDecisionActivityUiController.java
+++ b/app/src/org/commcare/activities/connect/ConnectIdRecoveryDecisionActivityUiController.java
@@ -99,9 +99,8 @@ public String getCountryCode() {
public String getPhoneNumber() {
return phoneInput.getText().toString();
}
-
public void setPhoneNumber(String num) {
- phoneInput.setText(num);
+ phoneInput.setText(num);
}
public void setButton1Enabled(boolean enabled) {
diff --git a/app/src/org/commcare/adapters/ConnectJobAdapter.java b/app/src/org/commcare/adapters/ConnectJobAdapter.java
new file mode 100644
index 000000000..d2b6d37fc
--- /dev/null
+++ b/app/src/org/commcare/adapters/ConnectJobAdapter.java
@@ -0,0 +1,413 @@
+package org.commcare.adapters;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import org.commcare.connect.ConnectDatabaseHelper;
+import org.commcare.connect.ConnectManager;
+import org.commcare.connect.IConnectAppLauncher;
+import org.commcare.android.database.connect.models.ConnectJobRecord;
+import org.commcare.dalvik.R;
+import org.commcare.fragments.connect.ConnectJobsListsFragmentDirections;
+
+import java.util.Date;
+import java.util.List;
+
+import androidx.annotation.NonNull;
+import androidx.navigation.NavDirections;
+import androidx.navigation.Navigation;
+import androidx.recyclerview.widget.RecyclerView;
+
+public class ConnectJobAdapter extends RecyclerView.Adapter {
+ private static final int ViewTypeHeader = 1;
+ private static final int ViewTypeLearning = 2;
+ private static final int ViewTypeClaimed = 3;
+ private static final int ViewTypeEmpty = 4;
+ private static final int ViewTypeAvailable = 5;
+ private static final int ViewTypeEnded = 6;
+
+ private Context parentContext;
+ private final boolean showAvailable;
+ private final IConnectAppLauncher launcher;
+
+ public ConnectJobAdapter(boolean showAvailable, IConnectAppLauncher appLauncher) {
+ this.showAvailable = showAvailable;
+ this.launcher = appLauncher;
+ }
+
+ @Override
+ public int getItemCount() {
+ if(showAvailable) {
+ //1 section, no header
+ int numAvailable = ConnectDatabaseHelper.getAvailableJobs(parentContext).size();
+ return numAvailable > 0 ? numAvailable : 1;
+ }
+
+ //3 sections, each with a header and at least 1 row (for placeholder)
+
+ List training = ConnectDatabaseHelper.getTrainingJobs(parentContext);
+ int numTrainingRows = training.size() > 0 ? training.size() : 1;
+
+ List claimed = ConnectDatabaseHelper.getDeliveryJobs(parentContext);
+ int numClaimedRows = claimed.size() > 0 ? claimed.size() : 1;
+
+ List finished = ConnectDatabaseHelper.getFinishedJobs(parentContext);
+ int numFinishedRows = finished.size() > 0 ? finished.size() : 1;
+
+ return numTrainingRows + numClaimedRows + numFinishedRows + 3; //3 here is for headers
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ if(showAvailable) {
+ int numAvailable = ConnectDatabaseHelper.getAvailableJobs(parentContext).size();
+ if(numAvailable == 0) {
+ return ViewTypeEmpty;
+ }
+
+ return ViewTypeAvailable;
+ }
+
+ List training = ConnectDatabaseHelper.getTrainingJobs(parentContext);
+ int numTraining = training.size() > 0 ? training.size() : 1;
+ int totalTrainingRows = numTraining + 1;
+
+ List claimed = ConnectDatabaseHelper.getDeliveryJobs(parentContext);
+ int numClaimed = claimed.size() > 0 ? claimed.size() : 1;
+ int totalTrainingPlusClaimedRows = numTraining + numClaimed + 2;
+
+ if(position == 0 || position == totalTrainingRows || position == totalTrainingPlusClaimedRows) {
+ return ViewTypeHeader;
+ }
+
+ if(position < totalTrainingRows) {
+ return training.size() == 0 ? ViewTypeEmpty : ViewTypeLearning;
+ }
+
+ if(position < totalTrainingPlusClaimedRows) {
+ return claimed.size() == 0 ? ViewTypeEmpty : ViewTypeClaimed;
+ }
+
+ List finished = ConnectDatabaseHelper.getFinishedJobs(parentContext);
+
+ return finished.size() == 0 ? ViewTypeEmpty : ViewTypeEnded;
+ }
+
+ @NonNull
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ parentContext = parent.getContext();
+ switch(viewType) {
+ case ViewTypeClaimed, ViewTypeLearning -> {
+ View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.connect_claimed_job_item, parent, false);
+ return new ConnectJobAdapter.ClaimedJobViewHolder(view);
+ }
+ case ViewTypeEnded -> {
+ View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.connect_claimed_job_item, parent, false);
+ return new ConnectJobAdapter.EndedJobViewHolder(view);
+ }
+ case ViewTypeEmpty -> {
+ View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.connect_empty_job_list_item, parent, false);
+ return new ConnectJobAdapter.EmptyJobListViewHolder(view);
+ }
+ case ViewTypeHeader -> {
+ View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.connect_job_list_header_item, parent, false);
+ return new ConnectJobAdapter.JobHeaderViewHolder(view);
+ }
+ case ViewTypeAvailable -> {
+ View view = LayoutInflater.from(parentContext).inflate(R.layout.connect_available_job_item, parent, false);
+ return new ConnectJobAdapter.AvailableJobViewHolder(view);
+ }
+ }
+
+ throw new RuntimeException("Not ready for requested viewType");
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+ if(holder instanceof ConnectJobAdapter.AvailableJobViewHolder availableHolder) {
+ ConnectJobRecord job = ConnectDatabaseHelper.getAvailableJobs(parentContext).get(position);
+
+ availableHolder.newText.setVisibility(job.getIsNew() ? View.VISIBLE : View.GONE);
+ availableHolder.titleText.setText(job.getTitle());
+ availableHolder.descriptionText.setText(job.getShortDescription());
+
+ availableHolder.visitsText.setText(parentContext.getString(R.string.connect_job_visits,
+ job.getMaxPossibleVisits(), job.getDaysRemaining()));
+
+ availableHolder.continueImage.setOnClickListener(v ->
+ launchJobInfo(job, availableHolder.continueImage));
+ }
+ else if(holder instanceof ConnectJobAdapter.ClaimedJobViewHolder claimedHolder) {
+ List training = ConnectDatabaseHelper.getTrainingJobs(parentContext);
+ boolean isTraining = position - 1 < training.size();
+ ConnectJobRecord job;
+ if (isTraining) {
+ job = training.get(position - 1);
+ } else {
+ int numTraining = training.size() > 0 ? training.size() : 1;
+ job = ConnectDatabaseHelper.getDeliveryJobs(parentContext).get(position - numTraining - 2);
+ }
+
+ claimedHolder.titleText.setText(job.getTitle());
+
+ int percent = getJobPercentage(job, isTraining);
+ claimedHolder.progressBar.setProgress(percent);
+ claimedHolder.progressBar.setMax(100);
+
+ claimedHolder.descriptionText.setVisibility(View.VISIBLE);
+ claimedHolder.descriptionText.setText(getStatusDescription(job, isTraining, percent));
+
+ String fromStr = "";
+ if(job.getProjectStartDate().after(new Date())) {
+ fromStr = parentContext.getString(R.string.connect_job_remaining_from,
+ ConnectManager.formatDate(job.getProjectStartDate()));
+ }
+ String remaining = parentContext.getString(R.string.connect_job_remaining, job.getDaysRemaining(), fromStr);
+ claimedHolder.remainingText.setVisibility(View.VISIBLE);
+ claimedHolder.remainingText.setText(remaining);
+
+ claimedHolder.progressBar.setVisibility(View.VISIBLE);
+ claimedHolder.progressImage.setVisibility(View.VISIBLE);
+
+ claimedHolder.infoImage.setVisibility(View.VISIBLE);
+ claimedHolder.infoImage.setOnClickListener(v -> {
+ launchJobInfo(job, claimedHolder.continueImage);
+ });
+
+ claimedHolder.continueImage.setVisibility(View.VISIBLE);
+ claimedHolder.continueImage.setOnClickListener(v -> {
+ launchActiveAppForJob(job, claimedHolder.continueImage);
+ });
+ }
+ else if(holder instanceof ConnectJobAdapter.EndedJobViewHolder endedHolder) {
+ List training = ConnectDatabaseHelper.getTrainingJobs(parentContext);
+ int numTraining = training.size() > 0 ? training.size() : 1;
+ List claimed = ConnectDatabaseHelper.getDeliveryJobs(parentContext);
+ int numClaimed = claimed.size() > 0 ? claimed.size() : 1;
+ int totalTrainingPlusClaimedRows = numTraining + numClaimed + 2;
+ int endedIndex = position - 1 - totalTrainingPlusClaimedRows;
+
+ ConnectJobRecord job = ConnectDatabaseHelper.getFinishedJobs(parentContext).get(endedIndex);
+
+ endedHolder.titleText.setText(job.getTitle());
+
+ boolean isTraining = job.getStatus() == ConnectJobRecord.STATUS_LEARNING;
+ int percent = getJobPercentage(job, isTraining);
+ endedHolder.descriptionText.setVisibility(View.VISIBLE);
+ endedHolder.descriptionText.setText(getStatusDescription(job, isTraining, percent));
+
+ String endedText = parentContext.getString(R.string.connect_job_completed, ConnectManager.formatDate(job.getProjectEndDate()));
+ endedHolder.remainingText.setVisibility(View.VISIBLE);
+ endedHolder.remainingText.setText(endedText);
+
+ endedHolder.progressBar.setVisibility(View.GONE);
+ endedHolder.progressImage.setVisibility(View.GONE);
+
+ endedHolder.infoImage.setVisibility(View.GONE);
+
+ endedHolder.continueImage.setVisibility(View.VISIBLE);
+ endedHolder.continueImage.setOnClickListener(v -> {
+ launchJobInfo(job, endedHolder.continueImage);
+ });
+ }
+ else if(holder instanceof ConnectJobAdapter.EmptyJobListViewHolder emptyHolder) {
+ int textResource = position == 1 ? R.string.connect_job_none_training : R.string.connect_job_none_active;
+ if(showAvailable) {
+ textResource = R.string.connect_job_none_available;
+ }
+
+ emptyHolder.image.setVisibility(!showAvailable && position == 1 ? View.GONE : View.VISIBLE);
+ emptyHolder.titleText.setText(parentContext.getString(textResource));
+ }
+ else if(holder instanceof ConnectJobAdapter.JobHeaderViewHolder headerHolder) {
+ int textId = R.string.connect_job_ended;
+ if(position == 0) {
+ textId = R.string.connect_job_training;
+ }
+ else {
+ List training = ConnectDatabaseHelper.getTrainingJobs(parentContext);
+ int numTraining = training.size() > 0 ? training.size() : 1;
+ int totalTrainingRows = numTraining + 1;
+ if(position == totalTrainingRows) {
+ textId = R.string.connect_job_claimed;
+ }
+ }
+
+ headerHolder.titleText.setText(parentContext.getString(textId));
+ }
+ }
+
+ int getJobPercentage(ConnectJobRecord job, boolean isTraining) {
+ int percent = job.getPercentComplete();
+ if (isTraining) {
+ //NOTE: leaving other code here for now in case API changfes to give back the modules array
+ int completed = job.getCompletedLearningModules();//0;
+// for (ConnectJobLearningModule module: job.getLearningModules()) {
+// if(module.getCompletedDate() != null) {
+// completed++;
+// }
+// }
+
+ int numModules = job.getNumLearningModules();// job.getLearningModules().length;
+ percent = numModules > 0 ? (100 * completed / numModules) : 100;
+ }
+
+ return percent;
+ }
+
+ private String getStatusDescription(ConnectJobRecord job, boolean isTraining, int percent) {
+ String description;
+ boolean finished = job.isFinished();
+ if (isTraining) {
+ //Started learning
+ if(percent >= 100) {
+ //Finished learning
+ if (job.passedAssessment()) {
+ description = parentContext.getString(finished ?
+ R.string.connect_job_passed_assessment :
+ R.string.connect_job_training_complete);
+ } else {
+ description = parentContext.getString(finished ?
+ R.string.connect_job_assessment_not_completed :
+ R.string.connect_job_needs_assessment);
+ }
+ } else {
+ description = parentContext.getString(R.string.connect_job_learning_not_completed);
+ }
+ } else if(finished) {
+ description = parentContext.getString(R.string.connect_job_visits_completed, job.getCompletedVisits());
+ } else if(job.getIsUserSuspended()) {
+ description =parentContext.getString(R.string.suspended) ;
+ }
+ else {
+ String learningOrJob = parentContext.getString(isTraining ? R.string.connect_job_learning : R.string.connect_job);
+ description = parentContext.getString(R.string.connect_job_training_progress, learningOrJob, percent);
+ }
+
+ return description;
+ }
+
+ private void launchActiveAppForJob(ConnectJobRecord job, View view) {
+ ConnectManager.setActiveJob(job);
+
+ boolean isLearning = job.getStatus() == ConnectJobRecord.STATUS_LEARNING;
+ String appId = isLearning ? job.getLearnAppInfo().getAppId() : job.getDeliveryAppInfo().getAppId();
+
+ if(ConnectManager.isAppInstalled(appId)) {
+ launcher.launchApp(appId, isLearning);
+ }
+ else {
+ int textId = isLearning ? R.string.connect_downloading_learn : R.string.connect_downloading_delivery;
+ String title = parentContext.getString(textId);
+ Navigation.findNavController(view).navigate(ConnectJobsListsFragmentDirections.actionConnectJobsListFragmentToConnectDownloadingFragment(title, isLearning, true));
+ }
+ }
+
+ private void launchJobInfo(ConnectJobRecord job, View view) {
+ ConnectManager.setActiveJob(job);
+
+ NavDirections directions;
+ switch(job.getStatus()) {
+ case ConnectJobRecord.STATUS_AVAILABLE,
+ ConnectJobRecord.STATUS_AVAILABLE_NEW -> {
+ directions = ConnectJobsListsFragmentDirections.actionConnectJobsListFragmentToConnectJobIntroFragment();
+ }
+ case ConnectJobRecord.STATUS_LEARNING -> {
+ directions = ConnectJobsListsFragmentDirections.actionConnectJobsListFragmentToConnectJobLearningProgressFragment();
+ }
+ case ConnectJobRecord.STATUS_DELIVERING -> {
+ directions = ConnectJobsListsFragmentDirections.actionConnectJobsListFragmentToConnectJobDeliveryProgressFragment();
+ }
+ default -> {
+ throw new RuntimeException(String.format("Unexpected job status: %d", job.getStatus()));
+ }
+ }
+
+ Navigation.findNavController(view).navigate(directions);
+ }
+
+ public static class AvailableJobViewHolder extends RecyclerView.ViewHolder {
+ final TextView newText;
+ final TextView titleText;
+ final TextView descriptionText;
+ final TextView visitsText;
+ final ImageView continueImage;
+ public AvailableJobViewHolder(@NonNull View itemView) {
+ super(itemView);
+
+ newText = itemView.findViewById(R.id.new_label);
+ titleText = itemView.findViewById(R.id.title_label);
+ descriptionText = itemView.findViewById(R.id.description_label);
+ visitsText = itemView.findViewById(R.id.visits_label);
+ continueImage = itemView.findViewById(R.id.button);
+ }
+ }
+
+ public static class ClaimedJobViewHolder extends RecyclerView.ViewHolder {
+ final ProgressBar progressBar;
+ final ImageView progressImage;
+ final TextView titleText;
+ final TextView descriptionText;
+ final TextView remainingText;
+ final ImageView infoImage;
+ final ImageView continueImage;
+ public ClaimedJobViewHolder(@NonNull View itemView) {
+ super(itemView);
+
+ progressBar = itemView.findViewById(R.id.progress_bar);
+ progressImage = itemView.findViewById(R.id.progress_image);
+ titleText = itemView.findViewById(R.id.title_label);
+ descriptionText = itemView.findViewById(R.id.description_label);
+ remainingText = itemView.findViewById(R.id.remaining_label);
+ infoImage = itemView.findViewById(R.id.button_info);
+ continueImage = itemView.findViewById(R.id.button_go);
+ }
+ }
+
+ public static class EndedJobViewHolder extends RecyclerView.ViewHolder {
+ final ProgressBar progressBar;
+ final ImageView progressImage;
+ final TextView titleText;
+ final TextView descriptionText;
+ final TextView remainingText;
+ final ImageView infoImage;
+ final ImageView continueImage;
+ public EndedJobViewHolder(@NonNull View itemView) {
+ super(itemView);
+
+ progressBar = itemView.findViewById(R.id.progress_bar);
+ progressImage = itemView.findViewById(R.id.progress_image);
+ titleText = itemView.findViewById(R.id.title_label);
+ descriptionText = itemView.findViewById(R.id.description_label);
+ remainingText = itemView.findViewById(R.id.remaining_label);
+ infoImage = itemView.findViewById(R.id.button_info);
+ continueImage = itemView.findViewById(R.id.button_go);
+ }
+ }
+
+ public static class JobHeaderViewHolder extends RecyclerView.ViewHolder {
+ final TextView titleText;
+ public JobHeaderViewHolder(@NonNull View itemView) {
+ super(itemView);
+
+ titleText = itemView.findViewById(R.id.title_label);
+ }
+ }
+
+ public static class EmptyJobListViewHolder extends RecyclerView.ViewHolder {
+ final ImageView image;
+ final TextView titleText;
+ public EmptyJobListViewHolder(@NonNull View itemView) {
+ super(itemView);
+
+ image = itemView.findViewById(R.id.empty_image);
+ titleText = itemView.findViewById(R.id.title_label);
+ }
+ }
+}
diff --git a/app/src/org/commcare/android/database/connect/models/ConnectJobDeliveryRecord.java b/app/src/org/commcare/android/database/connect/models/ConnectJobDeliveryRecord.java
index e14e9f06f..be1b3b8a4 100644
--- a/app/src/org/commcare/android/database/connect/models/ConnectJobDeliveryRecord.java
+++ b/app/src/org/commcare/android/database/connect/models/ConnectJobDeliveryRecord.java
@@ -3,12 +3,14 @@
import java.io.Serializable;
import java.text.ParseException;
import java.util.Date;
+import java.util.Locale;
import org.commcare.connect.network.ConnectNetworkHelper;
import org.commcare.android.storage.framework.Persisted;
import org.commcare.models.framework.Persisting;
import org.commcare.modern.database.Table;
import org.commcare.modern.models.MetaField;
+import org.commcare.utils.CrashUtil;
import org.json.JSONException;
import org.json.JSONObject;
@@ -71,21 +73,32 @@ public ConnectJobDeliveryRecord() {
}
public static ConnectJobDeliveryRecord fromJson(JSONObject json, int jobId) throws JSONException, ParseException {
- ConnectJobDeliveryRecord delivery = new ConnectJobDeliveryRecord();
- delivery.jobId = jobId;
- delivery.lastUpdate = new Date();
-
- delivery.deliveryId = json.has(META_ID) ? json.getInt(META_ID) : -1;
- delivery.date = json.has(META_DATE) ? ConnectNetworkHelper.convertUTCToDate(json.getString(META_DATE)): new Date();
- delivery.status = json.has(META_STATUS) ? json.getString(META_STATUS) : "";
- delivery.unitName = json.has(META_UNIT_NAME) ? json.getString(META_UNIT_NAME) : "";
- delivery.slug = json.has(META_SLUG) ? json.getString(META_SLUG) : "";
- delivery.entityId = json.has(META_ENTITY_ID) ? json.getString(META_ENTITY_ID) : "";
- delivery.entityName = json.has(META_ENTITY_NAME) ? json.getString(META_ENTITY_NAME) : "";
-
- delivery.reason = json.has(META_REASON) && !json.isNull(META_REASON) ? json.getString(META_REASON) : "";
-
- return delivery;
+ int deliveryId = -1;
+ String dateString = "(error)";
+ try {
+ ConnectJobDeliveryRecord delivery = new ConnectJobDeliveryRecord();
+ delivery.jobId = jobId;
+ delivery.lastUpdate = new Date();
+
+ deliveryId = json.has(META_ID) ? json.getInt(META_ID) : -1;
+ delivery.deliveryId = deliveryId;
+ dateString = json.getString(META_DATE);
+ delivery.date = ConnectNetworkHelper.convertUTCToDate(dateString);
+ delivery.status = json.has(META_STATUS) ? json.getString(META_STATUS) : "";
+ delivery.unitName = json.has(META_UNIT_NAME) ? json.getString(META_UNIT_NAME) : "";
+ delivery.slug = json.has(META_SLUG) ? json.getString(META_SLUG) : "";
+ delivery.entityId = json.has(META_ENTITY_ID) ? json.getString(META_ENTITY_ID) : "";
+ delivery.entityName = json.has(META_ENTITY_NAME) ? json.getString(META_ENTITY_NAME) : "";
+
+ delivery.reason = json.has(META_REASON) && !json.isNull(META_REASON) ? json.getString(META_REASON) : "";
+
+ return delivery;
+ }
+ catch(Exception e) {
+ String message = String.format(Locale.getDefault(), "Error parsing delivery %d: date = '%s'", deliveryId, dateString);
+ CrashUtil.reportException(new Exception(message, e));
+ return null;
+ }
}
public int getDeliveryId() { return deliveryId; }
diff --git a/app/src/org/commcare/connect/ConnectIdWorkflows.java b/app/src/org/commcare/connect/ConnectIdWorkflows.java
index b4d7b8e8d..6ff3cf625 100644
--- a/app/src/org/commcare/connect/ConnectIdWorkflows.java
+++ b/app/src/org/commcare/connect/ConnectIdWorkflows.java
@@ -1,6 +1,7 @@
package org.commcare.connect;
import android.app.Activity;
+import android.content.Context;
import android.content.Intent;
import org.commcare.activities.CommCareActivity;
@@ -40,7 +41,7 @@ public static void reset() {
forgotPassword = false;
forgotPin = false;
}
-
+
public static void beginRegistration(CommCareActivity> parent, ConnectManager.ConnectIdStatus status, ConnectManager.ConnectActivityCompleteListener callback) {
parentActivity = parent;
listener = callback;
@@ -371,7 +372,7 @@ public static boolean handleFinishedActivity(CommCareActivity> activity, int r
} else {
if (intent != null) {
forgotPin = intent.getBooleanExtra(ConnectConstants.WRONG_PIN, false);
- nextRequestCode = ConnectTask.CONNECT_REGISTRATION_WRONG_PIN;
+ nextRequestCode = ConnectTask.CONNECT_REGISTRATION_WRONG_PIN;
} else {
nextRequestCode = ConnectTask.CONNECT_REGISTRATION_ALTERNATE_PHONE;
}
@@ -426,7 +427,7 @@ public static boolean handleFinishedActivity(CommCareActivity> activity, int r
}
} else {
if (intent != null) {
- nextRequestCode = ConnectTask.CONNECT_RECOVERY_WRONG_PIN;
+ nextRequestCode = ConnectTask.CONNECT_RECOVERY_WRONG_PIN;
}
}
}
diff --git a/app/src/org/commcare/connect/ConnectManager.java b/app/src/org/commcare/connect/ConnectManager.java
index bcae7ec61..1e9b7b93c 100644
--- a/app/src/org/commcare/connect/ConnectManager.java
+++ b/app/src/org/commcare/connect/ConnectManager.java
@@ -1,39 +1,75 @@
package org.commcare.connect;
+import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.view.View;
import android.widget.TextView;
+import android.widget.Toast;
+
+import org.commcare.AppUtils;
import androidx.constraintlayout.widget.ConstraintLayout;
+import androidx.work.BackoffPolicy;
+import androidx.work.Constraints;
+import androidx.work.ExistingPeriodicWorkPolicy;
+import androidx.work.NetworkType;
+import androidx.work.PeriodicWorkRequest;
+import androidx.work.WorkManager;
import org.commcare.activities.CommCareActivity;
+import org.commcare.android.database.app.models.UserKeyRecord;
import org.commcare.android.database.connect.models.ConnectAppRecord;
+import org.commcare.android.database.connect.models.ConnectJobAssessmentRecord;
+import org.commcare.android.database.connect.models.ConnectJobDeliveryRecord;
+import org.commcare.android.database.connect.models.ConnectJobLearningRecord;
+import org.commcare.android.database.connect.models.ConnectJobPaymentRecord;
+import org.commcare.android.database.connect.models.ConnectJobRecord;
import org.commcare.android.database.connect.models.ConnectLinkedAppRecord;
import org.commcare.android.database.connect.models.ConnectUserRecord;
+import org.commcare.CommCareApplication;
+import org.commcare.android.database.global.models.ApplicationRecord;
+import org.commcare.commcaresupportlibrary.CommCareLauncher;
+import org.commcare.connect.network.ApiConnect;
import org.commcare.connect.network.ApiConnectId;
import org.commcare.connect.network.ConnectNetworkHelper;
import org.commcare.connect.network.ConnectSsoHelper;
import org.commcare.connect.network.IApiCallback;
+import org.commcare.connect.workers.ConnectHeartbeatWorker;
+import org.commcare.core.encryption.CryptUtil;
import org.commcare.core.network.AuthInfo;
import org.commcare.dalvik.R;
+import org.commcare.engine.resource.ResourceInstallUtils;
+import org.commcare.google.services.analytics.FirebaseAnalyticsUtil;
+import org.commcare.models.encryption.ByteEncrypter;
import org.commcare.preferences.AppManagerDeveloperPreferences;
+import org.commcare.tasks.ResourceEngineListener;
+import org.commcare.tasks.templates.CommCareTask;
+import org.commcare.tasks.templates.CommCareTaskConnector;
import org.commcare.utils.CrashUtil;
import org.commcare.views.dialogs.StandardAlertDialog;
import org.javarosa.core.io.StreamsUtil;
import org.javarosa.core.services.Logger;
+import org.javarosa.core.util.PropertyUtils;
+import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.text.DateFormat;
+import java.text.ParseException;
import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
import java.util.Date;
+import java.util.List;
import java.util.Locale;
import java.util.Random;
import java.util.concurrent.TimeUnit;
+import javax.crypto.SecretKey;
+
import androidx.annotation.Nullable;
/**
@@ -42,6 +78,12 @@
* @author dviggiano
*/
public class ConnectManager {
+ private static final String CONNECT_WORKER = "connect_worker";
+ private static final long PERIODICITY_FOR_HEARTBEAT_IN_HOURS = 4;
+ private static final long BACKOFF_DELAY_FOR_HEARTBEAT_RETRY = 5 * 60 * 1000L; // 5 mins
+ private static final String CONNECT_HEARTBEAT_REQUEST_NAME = "connect_hearbeat_periodic_request";
+ private static final int APP_DOWNLOAD_TASK_ID = 4;
+
public static int getFailureAttempt() {
return ConnectManager.getInstance().failedPinAttempts;
}
@@ -71,6 +113,8 @@ public interface ConnectActivityCompleteListener {
private CommCareActivity> parentActivity;
private ConnectActivityCompleteListener loginListener;
+ private String primedAppIdForAutoLogin = null;
+
//Singleton, private constructor
private ConnectManager() {
}
@@ -104,6 +148,33 @@ public static void init(CommCareActivity> parent) {
}
}
+ private static void scheduleHearbeat() {
+ if (AppManagerDeveloperPreferences.isConnectIdEnabled()) {
+ Constraints constraints = new Constraints.Builder()
+ .setRequiredNetworkType(NetworkType.CONNECTED)
+ .setRequiresBatteryNotLow(true)
+ .build();
+
+ PeriodicWorkRequest heartbeatRequest =
+ new PeriodicWorkRequest.Builder(ConnectHeartbeatWorker.class,
+ PERIODICITY_FOR_HEARTBEAT_IN_HOURS,
+ TimeUnit.HOURS)
+ .addTag(CONNECT_WORKER)
+ .setConstraints(constraints)
+ .setBackoffCriteria(
+ BackoffPolicy.EXPONENTIAL,
+ BACKOFF_DELAY_FOR_HEARTBEAT_RETRY,
+ TimeUnit.MILLISECONDS)
+ .build();
+
+ WorkManager.getInstance(CommCareApplication.instance()).enqueueUniquePeriodicWork(
+ CONNECT_HEARTBEAT_REQUEST_NAME,
+ ExistingPeriodicWorkPolicy.REPLACE,
+ heartbeatRequest
+ );
+ }
+ }
+
public static void setParent(CommCareActivity> parent) {
getInstance().parentActivity = parent;
}
@@ -123,6 +194,10 @@ public static String formatDate(Date date) {
return dateFormat.format(date);
}
+ public static String formatDateTime(Date date) {
+ return SimpleDateFormat.getDateTimeInstance().format(date);
+ }
+
public static boolean shouldShowSecondaryPhoneConfirmationTile(Context context) {
boolean show = false;
@@ -193,7 +268,7 @@ private static void completeSignin() {
ConnectManager instance = getInstance();
instance.connectStatus = ConnectIdStatus.LoggedIn;
- //scheduleHearbeat(); //When Connect is ready
+ scheduleHearbeat();
CrashUtil.registerConnectUser();
if(instance.loginListener != null) {
@@ -216,8 +291,29 @@ public static void forgetUser() {
manager.loginListener = null;
}
+ public static ConnectJobRecord setConnectJobForApp(Context context, String appId) {
+ ConnectJobRecord job = null;
+
+ ConnectAppRecord appRecord = getAppRecord(context, appId);
+ if(appRecord != null) {
+ job = ConnectDatabaseHelper.getJob(context, appRecord.getJobId());
+ }
+
+ setActiveJob(job);
+
+ return job;
+ }
+
+ private ConnectJobRecord activeJob = null;
private int failedPinAttempts = 0;
+ public static void setActiveJob(ConnectJobRecord job) {
+ ConnectManager.getInstance().activeJob = job;
+ }
+ public static ConnectJobRecord getActiveJob() {
+ return ConnectManager.getInstance().activeJob;
+ }
+
public static void unlockConnect(CommCareActivity> parent, ConnectActivityCompleteListener listener) {
if(manager.connectStatus == ConnectIdStatus.LoggedIn) {
ConnectIdWorkflows.unlockConnect(parent, success -> {
@@ -253,6 +349,9 @@ public static void handleConnectButtonPress(CommCareActivity> parent, ConnectA
listener.connectActivityComplete(success);
});
}
+ case LoggedIn -> {
+ goToConnectJobsList();
+ }
}
}
@@ -260,6 +359,20 @@ public static void verifySecondaryPhone(CommCareActivity> parent, ConnectActiv
ConnectIdWorkflows.beginSecondaryPhoneVerification(parent, listener);
}
+ public static void goToConnectJobsList() {
+ ConnectTask task = ConnectTask.CONNECT_MAIN;
+ Intent i = new Intent(manager.parentActivity, task.getNextActivity());
+ manager.parentActivity.startActivity(i);
+ }
+
+ public static void goToActiveInfoForJob(Activity activity, boolean allowProgression) {
+ ConnectTask task = ConnectTask.CONNECT_JOB_INFO;
+ Intent i = new Intent(activity, task.getNextActivity());
+ i.putExtra("info", true);
+ i.putExtra("buttons", allowProgression);
+ activity.startActivity(i);
+ }
+
public static void forgetAppCredentials(String appId, String userId) {
ConnectLinkedAppRecord record = ConnectDatabaseHelper.getAppData(manager.parentActivity, appId, userId);
if (record != null) {
@@ -432,6 +545,140 @@ public static AuthInfo.TokenAuth getTokenCredentialsForApp(String appId, String
return null;
}
+ public static boolean isAppInstalled(String appId) {
+ boolean installed = false;
+ ArrayList apps = AppUtils.
+ getInstalledAppRecords();
+ for (ApplicationRecord app : apps) {
+ if (appId.equals(app.getUniqueId())) {
+ installed = true;
+ break;
+ }
+ }
+ return installed;
+ }
+
+ private boolean downloading = false;
+ private ResourceEngineListener downloadListener = null;
+ public static void downloadAppOrResumeUpdates(String installUrl, ResourceEngineListener listener) {
+ ConnectManager instance = getInstance();
+ instance.downloadListener = listener;
+ if(!instance.downloading) {
+ instance.downloading = true;
+ //Start a new download
+ ResourceInstallUtils.startAppInstallAsync(false, APP_DOWNLOAD_TASK_ID, new CommCareTaskConnector() {
+ @Override
+ public void connectTask(CommCareTask task) {
+
+ }
+
+ @Override
+ public void startBlockingForTask(int id) {
+
+ }
+
+ @Override
+ public void stopBlockingForTask(int id) {
+ instance.downloading = false;
+ }
+
+ @Override
+ public void taskCancelled() {
+
+ }
+
+ @Override
+ public ResourceEngineListener getReceiver() {
+ return instance.downloadListener;
+ }
+
+ @Override
+ public void startTaskTransition() {
+
+ }
+
+ @Override
+ public void stopTaskTransition(int taskId) {
+
+ }
+
+ @Override
+ public void hideTaskCancelButton() {
+
+ }
+ }, installUrl);
+ }
+ }
+
+ public static void launchApp(Context context, boolean isLearning, String appId) {
+ CommCareApplication.instance().closeUserSession();
+
+ String appType = isLearning ? "Learn" : "Deliver";
+ FirebaseAnalyticsUtil.reportCccAppLaunch(appType, appId);
+
+ getInstance().primedAppIdForAutoLogin = appId;
+
+ CommCareLauncher.launchCommCareForAppId(context, appId);
+ }
+
+ public static boolean wasAppLaunchedFromConnect(String appId) {
+ String primed = getInstance().primedAppIdForAutoLogin;
+ getInstance().primedAppIdForAutoLogin = null;
+ return primed != null && primed.equals(appId);
+ }
+
+ public static String checkAutoLoginAndOverridePassword(Context context, String appId, String username,
+ String passwordOrPin, boolean appLaunchedFromConnect, boolean uiInAutoLogin) {
+ if (isUnlocked()) {
+ if(appLaunchedFromConnect) {
+ //Configure some things if we haven't already
+ ConnectLinkedAppRecord record = ConnectDatabaseHelper.getAppData(context,
+ appId, username);
+ if (record == null) {
+ record = prepareConnectManagedApp(context, appId, username);
+ }
+
+ passwordOrPin = record.getPassword();
+ } else if(uiInAutoLogin) {
+ String seatedAppId = CommCareApplication.instance().getCurrentApp().getUniqueId();
+ passwordOrPin = ConnectManager.getStoredPasswordForApp(seatedAppId, username);
+ }
+ }
+
+ return passwordOrPin;
+ }
+
+ public static ConnectLinkedAppRecord prepareConnectManagedApp(Context context, String appId, String username) {
+ //Create app password
+ String password = generatePassword();
+
+ //Store ConnectLinkedAppRecord (note worker already linked)
+ ConnectLinkedAppRecord appRecord = ConnectDatabaseHelper.storeApp(context, appId, username, true, password, true);
+
+ //Store UKR
+ SecretKey newKey = CryptUtil.generateSemiRandomKey();
+ String sandboxId = PropertyUtils.genUUID().replace("-", "");
+ Date now = new Date();
+
+ Calendar cal = Calendar.getInstance();
+ cal.setTime(now);
+ cal.add(Calendar.YEAR, -10); //Begin ten years ago
+ Date fromDate = cal.getTime();
+
+ cal = Calendar.getInstance();
+ cal.setTime(now);
+ cal.add(Calendar.YEAR, 10); //Expire in ten years
+ Date toDate = cal.getTime();
+
+ UserKeyRecord ukr = new UserKeyRecord(username, UserKeyRecord.generatePwdHash(password),
+ ByteEncrypter.wrapByteArrayWithString(newKey.getEncoded(), password),
+ fromDate, toDate, sandboxId);
+
+ CommCareApplication.instance().getCurrentApp().getStorage(UserKeyRecord.class).write(ukr);
+
+ return appRecord;
+ }
+
public static void getRemoteDbPassphrase(Context context, ConnectUserRecord user) {
ApiConnectId.fetchDbPassphrase(context, user, new IApiCallback() {
@Override
@@ -468,6 +715,236 @@ public void processOldApiError() {
});
}
+ public static void updateJobProgress(Context context, ConnectJobRecord job, ConnectActivityCompleteListener listener) {
+ switch (job.getStatus()) {
+ case ConnectJobRecord.STATUS_LEARNING -> {
+ updateLearningProgress(context, job, listener);
+ }
+ case ConnectJobRecord.STATUS_DELIVERING -> {
+ updateDeliveryProgress(context, job, listener);
+ }
+ default -> {
+ listener.connectActivityComplete(true);
+ }
+ }
+ }
+
+ public static void updateLearningProgress(Context context, ConnectJobRecord job, ConnectActivityCompleteListener listener) {
+ ApiConnect.getLearnProgress(context, 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 learningRecords = new ArrayList<>(modules.length());
+ for(int i=0; i assessmentRecords = new ArrayList<>(assessments.length());
+ for(int i=0; i 0) {
+ //Parse the JSON
+ JSONObject json = new JSONObject(responseAsString);
+
+ boolean updatedJob = false;
+ String key = "max_payments";
+ if(json.has(key)) {
+ job.setMaxVisits(json.getInt(key));
+ updatedJob = true;
+ }
+
+ key = "end_date";
+ if(json.has(key)) {
+ job.setProjectEndDate(ConnectNetworkHelper.parseDate(json.getString(key)));
+ updatedJob = true;
+ }
+
+ key = "payment_accrued";
+ if(json.has(key)) {
+ job.setPaymentAccrued(json.getInt(key));
+ updatedJob = true;
+ }
+
+ key = "is_user_suspended";
+ if(json.has(key)) {
+ job.setIsUserSuspended(json.getBoolean(key));
+ updatedJob = true;
+ }
+
+ if(updatedJob) {
+ job.setLastDeliveryUpdate(new Date());
+ ConnectDatabaseHelper.upsertJob(context, job);
+ }
+
+ List deliveries = new ArrayList<>(json.length());
+ key = "deliveries";
+ if(json.has(key)) {
+ JSONArray array = json.getJSONArray(key);
+ for (int i = 0; i < array.length(); i++) {
+ JSONObject obj = (JSONObject)array.get(i);
+ ConnectJobDeliveryRecord delivery = ConnectJobDeliveryRecord.fromJson(obj, job.getJobId());
+ if(delivery != null) {
+ //Note: Ignoring faulty deliveries (non-fatal exception logged)
+ deliveries.add(delivery);
+ }
+ }
+
+ //Store retrieved deliveries
+ ConnectDatabaseHelper.storeDeliveries(context, deliveries, job.getJobId(), true);
+
+ job.setDeliveries(deliveries);
+ }
+
+ List payments = new ArrayList<>();
+ key = "payments";
+ if(json.has(key)) {
+ JSONArray array = json.getJSONArray(key);
+ for (int i = 0; i < array.length(); i++) {
+ JSONObject obj = (JSONObject)array.get(i);
+ payments.add(ConnectJobPaymentRecord.fromJson(obj, job.getJobId()));
+ }
+
+ ConnectDatabaseHelper.storePayments(context, payments, job.getJobId(), true);
+
+ job.setPayments(payments);
+ }
+ }
+ } catch (IOException | JSONException | ParseException e) {
+ Logger.exception("Parsing return from delivery progress request", e);
+ success = false;
+ }
+
+ reportApiCall(success);
+ listener.connectActivityComplete(success);
+ }
+
+ @Override
+ public void processFailure(int responseCode, IOException e) {
+ Logger.log("ERROR", String.format(Locale.getDefault(), "Delivery progress call 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 processOldApiError() {
+ ConnectNetworkHelper.showOutdatedApiError(context);
+ reportApiCall(false);
+ listener.connectActivityComplete(false);
+ }
+ });
+ }
+
+ public static void updatePaymentConfirmed(Context context, final ConnectJobPaymentRecord payment, boolean confirmed, ConnectActivityCompleteListener listener) {
+ ApiConnect.setPaymentConfirmed(context, payment.getPaymentId(), confirmed, new IApiCallback() {
+ private void reportApiCall(boolean success) {
+ FirebaseAnalyticsUtil.reportCccApiPaymentConfirmation(success);
+ }
+
+ @Override
+ public void processSuccess(int responseCode, InputStream responseData) {
+ payment.setConfirmed(confirmed);
+ ConnectDatabaseHelper.storePayment(context, payment);
+
+ //No need to report to user
+ reportApiCall(true);
+ listener.connectActivityComplete(true);
+ }
+
+ @Override
+ public void processFailure(int responseCode, IOException e) {
+ Toast.makeText(context, R.string.connect_payment_confirm_failed, Toast.LENGTH_SHORT).show();
+ reportApiCall(false);
+ listener.connectActivityComplete(false);
+ }
+
+ @Override
+ public void processNetworkFailure() {
+ Toast.makeText(context, R.string.connect_payment_confirm_failed, Toast.LENGTH_SHORT).show();
+ reportApiCall(false);
+ listener.connectActivityComplete(false);
+ }
+
+ @Override
+ public void processOldApiError() {
+ ConnectNetworkHelper.showOutdatedApiError(context);
+ reportApiCall(false);
+ listener.connectActivityComplete(false);
+ }
+ });
+ }
+
public static String generatePassword() {
int passwordLength = 20;
@@ -479,4 +956,4 @@ public static String generatePassword() {
return password.toString();
}
-}
\ No newline at end of file
+}
diff --git a/app/src/org/commcare/connect/ConnectTask.java b/app/src/org/commcare/connect/ConnectTask.java
index f9fce9c83..87b4bad68 100644
--- a/app/src/org/commcare/connect/ConnectTask.java
+++ b/app/src/org/commcare/connect/ConnectTask.java
@@ -1,5 +1,6 @@
package org.commcare.connect;
+import org.commcare.activities.connect.ConnectActivity;
import org.commcare.activities.connect.ConnectIdConsentActivity;
import org.commcare.activities.connect.ConnectIdBiometricUnlockActivity;
import org.commcare.activities.connect.ConnectIdMessageActivity;
@@ -60,7 +61,7 @@ public enum ConnectTask {
CONNECT_BIOMETRIC_ENROLL_FAIL(ConnectConstants.ConnectIdTaskIdOffset + 22,
ConnectIdMessageActivity.class),
CONNECT_MAIN(ConnectConstants.ConnectIdTaskIdOffset + 23,
- null), //NOTE: Will be ConnectActivity.class when ready
+ ConnectActivity.class),
CONNECT_REGISTRATION_CONFIGURE_PIN(ConnectConstants.ConnectIdTaskIdOffset + 24,
ConnectIdPinActivity.class),
CONNECT_REGISTRATION_CONFIRM_PIN(ConnectConstants.ConnectIdTaskIdOffset + 25,
@@ -82,7 +83,7 @@ public enum ConnectTask {
CONNECT_UNLOCK_VERIFY_ALT_PHONE(ConnectConstants.ConnectIdTaskIdOffset + 33,
ConnectIdPhoneVerificationActivity.class),
CONNECT_JOB_INFO(ConnectConstants.ConnectIdTaskIdOffset + 34,
- null), //NOTE: Will be ConnectActivity.class when ready
+ ConnectActivity.class),
CONNECT_REGISTRATION_CHANGE_PIN(ConnectConstants.ConnectIdTaskIdOffset + 35,
ConnectIdPinActivity.class),
CONNECT_UNLOCK_ALT_PHONE_CHANGE(ConnectConstants.ConnectIdTaskIdOffset + 36,
diff --git a/app/src/org/commcare/connect/IConnectAppLauncher.java b/app/src/org/commcare/connect/IConnectAppLauncher.java
new file mode 100644
index 000000000..bb088de73
--- /dev/null
+++ b/app/src/org/commcare/connect/IConnectAppLauncher.java
@@ -0,0 +1,5 @@
+package org.commcare.connect;
+
+public interface IConnectAppLauncher {
+ void launchApp(String appId, boolean isLearning);
+}
diff --git a/app/src/org/commcare/connect/network/ApiConnect.java b/app/src/org/commcare/connect/network/ApiConnect.java
new file mode 100644
index 000000000..5f636509b
--- /dev/null
+++ b/app/src/org/commcare/connect/network/ApiConnect.java
@@ -0,0 +1,148 @@
+package org.commcare.connect.network;
+
+import android.content.Context;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Multimap;
+
+import org.commcare.CommCareApplication;
+import org.commcare.connect.ConnectConstants;
+import org.commcare.connect.ConnectDatabaseHelper;
+import org.commcare.android.database.connect.models.ConnectLinkedAppRecord;
+import org.commcare.core.network.AuthInfo;
+import org.commcare.dalvik.BuildConfig;
+import org.commcare.dalvik.R;
+import org.commcare.preferences.HiddenPreferences;
+import org.commcare.preferences.ServerUrls;
+import org.javarosa.core.io.StreamsUtil;
+import org.javarosa.core.services.Logger;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Locale;
+
+public class ApiConnect {
+ private static final String API_VERSION_CONNECT = "1.0";
+
+ public static boolean getConnectOpportunities(Context context, IApiCallback handler) {
+ if (ConnectNetworkHelper.isBusy()) {
+ return false;
+ }
+
+ ConnectSsoHelper.retrieveConnectTokenAsync(context, token -> {
+ if(token == null) {
+ return;
+ }
+
+ String url = context.getString(R.string.ConnectOpportunitiesURL, BuildConfig.CCC_HOST);
+ Multimap params = ArrayListMultimap.create();
+
+ ConnectNetworkHelper.get(context, url, API_VERSION_CONNECT, token, params, false, handler);
+ });
+
+ return true;
+ }
+
+ public static boolean startLearnApp(Context context, int jobId, IApiCallback handler) {
+ if (ConnectNetworkHelper.isBusy()) {
+ return false;
+ }
+
+ ConnectSsoHelper.retrieveConnectTokenAsync(context, token -> {
+ if(token == null) {
+ return;
+ }
+
+ String url = context.getString(R.string.ConnectStartLearningURL, BuildConfig.CCC_HOST);
+ HashMap params = new HashMap<>();
+ params.put("opportunity", String.format(Locale.getDefault(), "%d", jobId));
+
+ ConnectNetworkHelper.post(context, url, API_VERSION_CONNECT, token, params, true, false, handler);
+ });
+
+ return true;
+ }
+
+ public static boolean getLearnProgress(Context context, int jobId, IApiCallback handler) {
+ if (ConnectNetworkHelper.isBusy()) {
+ return false;
+ }
+
+ ConnectSsoHelper.retrieveConnectTokenAsync(context, token -> {
+ if(token == null) {
+ return;
+ }
+
+ String url = context.getString(R.string.ConnectLearnProgressURL, BuildConfig.CCC_HOST, jobId);
+ Multimap params = ArrayListMultimap.create();
+
+ ConnectNetworkHelper.get(context, url, API_VERSION_CONNECT, token, params, false, handler);
+ });
+
+ return true;
+ }
+
+ public static boolean claimJob(Context context, int jobId, IApiCallback handler) {
+ if (ConnectNetworkHelper.isBusy()) {
+ return false;
+ }
+
+ ConnectSsoHelper.retrieveConnectTokenAsync(context, token -> {
+ if(token == null) {
+ return;
+ }
+
+ String url = context.getString(R.string.ConnectClaimJobURL, BuildConfig.CCC_HOST, jobId);
+ HashMap params = new HashMap<>();
+
+ ConnectNetworkHelper.post(context, url, API_VERSION_CONNECT, token, params, false, false, handler);
+ });
+
+ return true;
+ }
+
+ public static boolean getDeliveries(Context context, int jobId, IApiCallback handler) {
+ if (ConnectNetworkHelper.isBusy()) {
+ return false;
+ }
+
+ ConnectSsoHelper.retrieveConnectTokenAsync(context, token -> {
+ if(token == null) {
+ return;
+ }
+
+ String url = context.getString(R.string.ConnectDeliveriesURL, BuildConfig.CCC_HOST, jobId);
+ Multimap params = ArrayListMultimap.create();
+
+ ConnectNetworkHelper.get(context, url, API_VERSION_CONNECT, token, params, false, handler);
+ });
+
+ return true;
+ }
+
+ public static boolean setPaymentConfirmed(Context context, String paymentId, boolean confirmed, IApiCallback handler) {
+ if (ConnectNetworkHelper.isBusy()) {
+ return false;
+ }
+
+ ConnectSsoHelper.retrieveConnectTokenAsync(context, token -> {
+ if(token == null) {
+ return;
+ }
+
+ String url = context.getString(R.string.ConnectPaymentConfirmationURL, BuildConfig.CCC_HOST, paymentId);
+
+ HashMap params = new HashMap<>();
+ params.put("confirmed", confirmed ? "true" : "false");
+
+ ConnectNetworkHelper.post(context, url, API_VERSION_CONNECT, token, params, true, false, handler);
+ });
+
+ return true;
+ }
+}
diff --git a/app/src/org/commcare/connect/network/ApiConnectId.java b/app/src/org/commcare/connect/network/ApiConnectId.java
index 63fa74dfd..40414e495 100644
--- a/app/src/org/commcare/connect/network/ApiConnectId.java
+++ b/app/src/org/commcare/connect/network/ApiConnectId.java
@@ -102,6 +102,19 @@ public static AuthInfo.TokenAuth retrieveHqTokenApi(Context context, String hqUs
return null;
}
+ public static ConnectNetworkHelper.PostResult makeHeartbeatRequestSync(Context context) {
+ String url = context.getString(R.string.ConnectHeartbeatURL);
+ HashMap params = new HashMap<>();
+ String token = FirebaseMessagingUtil.getFCMToken();
+ if(token != null) {
+ params.put("fcm_token", token);
+ boolean useFormEncoding = true;
+ return ConnectNetworkHelper.postSync(context, url, API_VERSION_CONNECT_ID, retrieveConnectIdTokenSync(context), params, useFormEncoding, true);
+ }
+
+ return new ConnectNetworkHelper.PostResult(-1, null, null);
+ }
+
public static AuthInfo.TokenAuth retrieveConnectIdTokenSync(Context context) {
AuthInfo.TokenAuth connectToken = ConnectManager.getConnectToken();
if (connectToken != null) {
@@ -324,7 +337,7 @@ public static boolean requestRecoveryOtpSecondary(Context context, String phone,
}
public static boolean requestVerificationOtpSecondary(Context context, String username, String password,
- IApiCallback callback) {
+ IApiCallback callback) {
int urlId = R.string.ConnectVerifySecondaryURL;
AuthInfo authInfo = new AuthInfo.ProvidedAuth(username, password, false);
@@ -375,7 +388,7 @@ public static boolean confirmRecoveryOtpSecondary(Context context, String phone,
}
public static boolean confirmVerificationOtpSecondary(Context context, String username, String password,
- String token, IApiCallback callback) {
+ String token, IApiCallback callback) {
int urlId = R.string.ConnectVerifyConfirmSecondaryOTPURL;
AuthInfo authInfo = new AuthInfo.ProvidedAuth(username, password, false);
diff --git a/app/src/org/commcare/connect/network/ConnectNetworkHelper.java b/app/src/org/commcare/connect/network/ConnectNetworkHelper.java
index e44189b6b..5e3c3e16a 100644
--- a/app/src/org/commcare/connect/network/ConnectNetworkHelper.java
+++ b/app/src/org/commcare/connect/network/ConnectNetworkHelper.java
@@ -88,31 +88,22 @@ public static Date parseDate(String dateStr) throws ParseException {
private static final SimpleDateFormat utcFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault());
- public static Date convertUTCToDate(String utcDateString) {
+ public static Date convertUTCToDate(String utcDateString) throws ParseException {
utcFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
- Date date = null;
- try {
- date = utcFormat.parse(utcDateString);
- } catch (ParseException e) {
- e.printStackTrace();
- }
-
- return date;
+ return utcFormat.parse(utcDateString);
}
public static Date convertDateToLocal(Date utcDate) {
utcFormat.setTimeZone(TimeZone.getDefault());
- Date date = null;
try {
String localDateString = utcFormat.format(utcDate);
- date = utcFormat.parse(localDateString);
- } catch (ParseException e) {
- e.printStackTrace();
+ return utcFormat.parse(localDateString);
+ }
+ catch (ParseException e) {
+ return utcDate;
}
-
- return date;
}
public static String getCallInProgress() {
@@ -288,7 +279,7 @@ private static HashMap getContentHeadersForXFormPost(RequestBody
}
public PostResult getSync(Context context, String url, AuthInfo authInfo, boolean background,
- Multimap params) {
+ Multimap params) {
if(!background) {
setCallInProgress(url);
showProgressDialog(context);
diff --git a/app/src/org/commcare/connect/network/ConnectNetworkService.kt b/app/src/org/commcare/connect/network/ConnectNetworkService.kt
new file mode 100644
index 000000000..1fdff56a7
--- /dev/null
+++ b/app/src/org/commcare/connect/network/ConnectNetworkService.kt
@@ -0,0 +1,14 @@
+package org.commcare.connect.network
+
+import okhttp3.ResponseBody
+import retrofit2.Call
+import retrofit2.http.Body
+import retrofit2.http.POST
+
+interface ConnectNetworkService {
+
+ @POST("users/heartbeat")
+ fun makeHeartbeatRequest(
+ @Body body: HeartBeatBody
+ ): Call?
+}
diff --git a/app/src/org/commcare/connect/network/ConnectNetworkServiceFactory.kt b/app/src/org/commcare/connect/network/ConnectNetworkServiceFactory.kt
new file mode 100644
index 000000000..1afa705f2
--- /dev/null
+++ b/app/src/org/commcare/connect/network/ConnectNetworkServiceFactory.kt
@@ -0,0 +1,31 @@
+package org.commcare.connect.network
+
+import okhttp3.OkHttpClient
+import org.commcare.connect.ConnectManager
+import org.commcare.core.network.AuthenticationInterceptor
+import org.commcare.core.network.ModernHttpRequester
+import org.commcare.network.HttpUtils
+import retrofit2.Retrofit
+import java.util.concurrent.TimeUnit
+
+object ConnectNetworkServiceFactory {
+
+ private const val CONNECT_ID_BASE_URL = "https://connectid.dimagi.com/"
+
+ private val authInterceptor = AuthenticationInterceptor()
+
+ private val httpClient = OkHttpClient.Builder()
+ .connectTimeout(ModernHttpRequester.CONNECTION_TIMEOUT.toLong(), TimeUnit.MILLISECONDS)
+ .readTimeout(ModernHttpRequester.CONNECTION_SO_TIMEOUT.toLong(), TimeUnit.MILLISECONDS)
+ .addInterceptor(authInterceptor)
+
+ private val connectIdRetrofit = Retrofit.Builder()
+ .baseUrl(CONNECT_ID_BASE_URL)
+ .client(httpClient.build())
+ .build()
+
+ fun createConnectIdNetworkSerive(): ConnectNetworkService {
+ authInterceptor.setCredential(HttpUtils.getCredential(ConnectManager.getConnectToken()))
+ return connectIdRetrofit.create(ConnectNetworkService::class.java)
+ }
+}
diff --git a/app/src/org/commcare/connect/network/ConnectSsoHelper.java b/app/src/org/commcare/connect/network/ConnectSsoHelper.java
index f18757be5..e428e4b73 100644
--- a/app/src/org/commcare/connect/network/ConnectSsoHelper.java
+++ b/app/src/org/commcare/connect/network/ConnectSsoHelper.java
@@ -90,4 +90,10 @@ public static AuthInfo.TokenAuth retrieveHqSsoTokenSync(Context context, String
return hqTokenAuth;
}
-}
\ No newline at end of file
+
+ public static void retrieveConnectTokenAsync(Context context, TokenCallback callback) {
+ TokenTask task = new TokenTask(context, null, false, callback);
+
+ task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+}
diff --git a/app/src/org/commcare/connect/network/HeartBeatBody.kt b/app/src/org/commcare/connect/network/HeartBeatBody.kt
new file mode 100644
index 000000000..03a721827
--- /dev/null
+++ b/app/src/org/commcare/connect/network/HeartBeatBody.kt
@@ -0,0 +1,3 @@
+package org.commcare.connect.network
+
+data class HeartBeatBody(val fcmToken: String)
diff --git a/app/src/org/commcare/connect/workers/ConnectHeartbeatWorker.kt b/app/src/org/commcare/connect/workers/ConnectHeartbeatWorker.kt
new file mode 100644
index 000000000..32c3e3d09
--- /dev/null
+++ b/app/src/org/commcare/connect/workers/ConnectHeartbeatWorker.kt
@@ -0,0 +1,31 @@
+package org.commcare.connect.workers
+
+import android.content.Context
+import androidx.work.CoroutineWorker
+import androidx.work.WorkerParameters
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import org.commcare.connect.ConnectManager
+import org.commcare.connect.network.ApiConnectId
+
+class ConnectHeartbeatWorker(context: Context, workerParams: WorkerParameters) :
+ CoroutineWorker(context, workerParams) {
+
+ override suspend fun doWork(): Result {
+ return withContext(Dispatchers.IO) {
+ if (!ConnectManager.isUnlocked()) {
+ return@withContext Result.failure()
+ }
+
+ //NOTE: Using trad'l code route instead until we can get the commented code to work
+ //val connectNetworkService = ConnectNetworkServiceFactory.createConnectIdNetworkSerive()
+ //val fcmToken = FirebaseMessagingUtil.getFCMToken();
+ //val requestBody = HeartBeatBody(fcmToken)
+ //val response = connectNetworkService.makeHeartbeatRequest(requestBody)!!.execute()
+ //return@withContext if (response.isSuccessful) Result.success() else Result.failure()
+
+ val result = ApiConnectId.makeHeartbeatRequestSync(applicationContext);
+ return@withContext if (result.responseCode in 200..299) Result.success() else Result.failure()
+ }
+ }
+}
diff --git a/app/src/org/commcare/engine/resource/ResourceInstallUtils.java b/app/src/org/commcare/engine/resource/ResourceInstallUtils.java
index 603008d9d..7bfbf3f49 100644
--- a/app/src/org/commcare/engine/resource/ResourceInstallUtils.java
+++ b/app/src/org/commcare/engine/resource/ResourceInstallUtils.java
@@ -1,9 +1,13 @@
package org.commcare.engine.resource;
+import android.content.Context;
+import android.content.Intent;
import android.content.SharedPreferences;
import org.commcare.CommCareApp;
import org.commcare.CommCareApplication;
+import org.commcare.activities.PromptApkUpdateActivity;
+import org.commcare.activities.TargetMismatchErrorActivity;
import org.commcare.android.database.global.models.ApplicationRecord;
import org.commcare.core.network.CaptivePortalRedirectException;
import org.commcare.engine.resource.installers.SingleAppInstallation;
@@ -26,6 +30,7 @@
import org.commcare.utils.SessionUnavailableException;
import org.commcare.views.notifications.NotificationActionButtonInfo;
import org.javarosa.core.services.Logger;
+import org.javarosa.core.services.locale.Localization;
import org.javarosa.core.util.PropertyUtils;
import java.net.MalformedURLException;
@@ -145,6 +150,29 @@ public static void handleAppInstallResult(ResourceEngineTask resourceEngineTask,
}
}
+ /**
+ * Shows Apk update prompt to user
+ * @param context current context
+ * @param versionRequired apk version required
+ * @param versionAvailable apk version available
+ */
+ public static void showApkUpdatePrompt(Context context, String versionRequired, String versionAvailable) {
+ String versionMismatch = Localization.get("install.version.mismatch", new String[]{versionRequired, versionAvailable});
+ Intent intent = new Intent(context, PromptApkUpdateActivity.class);
+ intent.putExtra(PromptApkUpdateActivity.REQUIRED_VERSION, versionRequired);
+ intent.putExtra(PromptApkUpdateActivity.CUSTOM_PROMPT_TITLE, versionMismatch);
+ context.startActivity(intent);
+ }
+
+ /**
+ * Show target mismatch error during CC App installation
+ * @param context current context
+ */
+ public static void showTargetMismatchError(Context context) {
+ Intent intent = new Intent(context, TargetMismatchErrorActivity.class);
+ context.startActivity(intent);
+ }
+
/**
* @return Version from profile in the app's upgrade table; -1 if upgrade
* profile not found.
diff --git a/app/src/org/commcare/fragments/SelectInstallModeFragment.java b/app/src/org/commcare/fragments/SelectInstallModeFragment.java
index d7c2dbc36..68844dcb9 100644
--- a/app/src/org/commcare/fragments/SelectInstallModeFragment.java
+++ b/app/src/org/commcare/fragments/SelectInstallModeFragment.java
@@ -9,6 +9,7 @@
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
+import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
@@ -18,6 +19,7 @@
import org.commcare.CommCareNoficationManager;
import org.commcare.activities.CommCareActivity;
import org.commcare.activities.CommCareSetupActivity;
+import org.commcare.connect.ConnectManager;
import org.commcare.android.nsd.MicroNode;
import org.commcare.android.nsd.NSDDiscoveryTools;
import org.commcare.android.nsd.NsdServiceListener;
@@ -47,6 +49,8 @@ public class SelectInstallModeFragment extends Fragment implements NsdServiceLis
private TextView mErrorMessageView;
private RectangleButtonWithText mViewErrorButton;
private View mViewErrorContainer;
+ private Button mConnectButton;
+ private TextView mOrText;
private ArrayList mLocalApps = new ArrayList<>();
@Override
@@ -71,6 +75,9 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa
TextView setupMsg = view.findViewById(R.id.str_setup_message);
setupMsg.setText(Localization.get("install.barcode.top"));
+ mConnectButton = view.findViewById(R.id.connect_login_button);
+ mOrText = view.findViewById(R.id.login_or);
+
TextView setupMsg2 = view.findViewById(R.id.str_setup_message_2);
setupMsg2.setText(Localization.get("install.barcode.bottom"));
@@ -190,4 +197,17 @@ public void showOrHideErrorMessage() {
}
}
}
+
+ public void updateConnectButton(boolean connectEnabled, View.OnClickListener listener) {
+ if(mConnectButton != null) {
+ boolean enabled = connectEnabled && ConnectManager.shouldShowConnectButton();
+
+ if (enabled) {
+ mConnectButton.setOnClickListener(listener);
+ }
+
+ mConnectButton.setVisibility(enabled ? View.VISIBLE : View.GONE);
+ mOrText.setVisibility(enabled ? View.VISIBLE : View.GONE);
+ }
+ }
}
diff --git a/app/src/org/commcare/fragments/connect/ConnectDeliveryDetailsFragment.java b/app/src/org/commcare/fragments/connect/ConnectDeliveryDetailsFragment.java
new file mode 100644
index 000000000..769f0b1a4
--- /dev/null
+++ b/app/src/org/commcare/fragments/connect/ConnectDeliveryDetailsFragment.java
@@ -0,0 +1,163 @@
+package org.commcare.fragments.connect;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.commcare.android.database.connect.models.ConnectPaymentUnitRecord;
+import org.commcare.connect.ConnectDatabaseHelper;
+import org.commcare.connect.ConnectManager;
+import org.commcare.connect.network.ConnectNetworkHelper;
+import org.commcare.android.database.connect.models.ConnectJobRecord;
+import org.commcare.android.database.global.models.ApplicationRecord;
+import org.commcare.connect.network.ApiConnect;
+import org.commcare.connect.network.IApiCallback;
+import org.commcare.dalvik.R;
+import org.commcare.google.services.analytics.FirebaseAnalyticsUtil;
+import org.commcare.utils.MultipleAppsUtil;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Date;
+
+import androidx.fragment.app.Fragment;
+import androidx.navigation.NavDirections;
+import androidx.navigation.Navigation;
+
+/**
+ * Fragment for showing delivery details for a Connect job
+ *
+ * @author dviggiano
+ */
+public class ConnectDeliveryDetailsFragment extends Fragment {
+ public ConnectDeliveryDetailsFragment() {
+ // Required empty public constructor
+ }
+
+ public static ConnectDeliveryDetailsFragment newInstance() {
+ return new ConnectDeliveryDetailsFragment();
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ ConnectJobRecord job = ConnectManager.getActiveJob();
+ getActivity().setTitle(job.getTitle());
+
+ View view = inflater.inflate(R.layout.fragment_connect_delivery_details, container, false);
+
+ TextView textView = view.findViewById(R.id.connect_delivery_total_visits_text);
+ int maxPossibleVisits = job.getMaxPossibleVisits();
+ int daysRemaining = job.getDaysRemaining();
+ textView.setText(getString(R.string.connect_delivery_max_visits, maxPossibleVisits, daysRemaining));
+
+ textView = view.findViewById(R.id.connect_delivery_max_daily_text);
+ textView.setText(getString(R.string.connect_delivery_max_daily_visits, job.getMaxDailyVisits()));
+
+ textView = view.findViewById(R.id.connect_delivery_budget_text);
+ String paymentText = "";
+ if(job.isMultiPayment()) {
+ //List payment units
+ paymentText = getString(R.string.connect_delivery_earn_multi);
+ for(int i=0; i 0) {
+ //Single payment unit
+ String moneyValue = job.getMoneyString(job.getPaymentUnits().get(0).getAmount());
+ paymentText = getString(R.string.connect_delivery_earn_single, moneyValue);
+ }
+
+ textView.setText(paymentText);
+
+ boolean expired = daysRemaining < 0;
+ textView = view.findViewById(R.id.connect_delivery_action_title);
+ textView.setText(expired ? R.string.connect_delivery_expired : R.string.connect_delivery_ready_to_claim);
+
+ textView = view.findViewById(R.id.connect_delivery_action_details);
+ textView.setText(expired ? R.string.connect_delivery_expired_detailed :
+ R.string.connect_delivery_ready_to_claim_detailed);
+
+ boolean jobClaimed = job.getStatus() == ConnectJobRecord.STATUS_DELIVERING;
+ boolean installed = false;
+ for (ApplicationRecord app : MultipleAppsUtil.appRecordArray()) {
+ if (job.getDeliveryAppInfo().getAppId().equals(app.getUniqueId())) {
+ installed = true;
+ break;
+ }
+ }
+ final boolean appInstalled = installed;
+
+ int buttonTextId = jobClaimed ? (appInstalled ? R.string.connect_delivery_go : R.string.connect_delivery_get_app) : R.string.connect_delivery_claim;
+
+ Button button = view.findViewById(R.id.connect_delivery_button);
+ button.setText(buttonTextId);
+ button.setEnabled(!expired);
+ button.setOnClickListener(v -> {
+ if(jobClaimed) {
+ proceedAfterJobClaimed(button, job, appInstalled);
+ } else {
+ //Claim job
+ ApiConnect.claimJob(getContext(), job.getJobId(), new IApiCallback() {
+ @Override
+ public void processSuccess(int responseCode, InputStream responseData) {
+ proceedAfterJobClaimed(button, job, appInstalled);
+ reportApiCall(true);
+ }
+
+ @Override
+ public void processFailure(int responseCode, IOException e) {
+ Toast.makeText(getContext(), "Connect: error claming job", Toast.LENGTH_SHORT).show();
+ reportApiCall(false);
+ }
+
+ @Override
+ public void processNetworkFailure() {
+ ConnectNetworkHelper.showNetworkError(getContext());
+ reportApiCall(false);
+ }
+
+ @Override
+ public void processOldApiError() {
+ ConnectNetworkHelper.showOutdatedApiError(getContext());
+ reportApiCall(false);
+ }
+ });
+ }
+ });
+
+ return view;
+ }
+
+ private void reportApiCall(boolean success) {
+ FirebaseAnalyticsUtil.reportCccApiClaimJob(success);
+ }
+
+ private void proceedAfterJobClaimed(Button button, ConnectJobRecord job, boolean installed) {
+ job.setStatus(ConnectJobRecord.STATUS_DELIVERING);
+ ConnectDatabaseHelper.upsertJob(getContext(), job);
+
+ NavDirections directions;
+ if (installed) {
+ directions = ConnectDeliveryDetailsFragmentDirections
+ .actionConnectJobDeliveryDetailsFragmentToConnectJobDeliveryProgressFragment();
+ } else {
+ String title = getString(R.string.connect_downloading_delivery);
+ directions = ConnectDeliveryDetailsFragmentDirections
+ .actionConnectJobDeliveryDetailsFragmentToConnectDownloadingFragment(title, false, false);
+ }
+
+ Navigation.findNavController(button).navigate(directions);
+ }
+}
diff --git a/app/src/org/commcare/fragments/connect/ConnectDeliveryProgressDeliveryFragment.java b/app/src/org/commcare/fragments/connect/ConnectDeliveryProgressDeliveryFragment.java
new file mode 100644
index 000000000..b4ce6f7dc
--- /dev/null
+++ b/app/src/org/commcare/fragments/connect/ConnectDeliveryProgressDeliveryFragment.java
@@ -0,0 +1,227 @@
+package org.commcare.fragments.connect;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import org.commcare.activities.CommCareActivity;
+import org.commcare.android.database.connect.models.ConnectJobDeliveryRecord;
+import org.commcare.android.database.connect.models.ConnectPaymentUnitRecord;
+import org.commcare.connect.ConnectManager;
+import org.commcare.android.database.connect.models.ConnectJobRecord;
+import org.commcare.dalvik.R;
+import org.commcare.views.dialogs.StandardAlertDialog;
+import org.javarosa.core.services.locale.Localization;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Locale;
+
+import androidx.fragment.app.Fragment;
+import androidx.navigation.Navigation;
+
+public class ConnectDeliveryProgressDeliveryFragment extends Fragment {
+ private View view;
+ private boolean showLearningLaunch = true;
+ private boolean showDeliveryLaunch = true;
+
+ private Button launchButton;
+ public ConnectDeliveryProgressDeliveryFragment() {
+ // Required empty public constructor
+ }
+
+ public static ConnectDeliveryProgressDeliveryFragment newInstance(boolean showLearningLaunch, boolean showDeliveryLaunch) {
+ ConnectDeliveryProgressDeliveryFragment fragment = new ConnectDeliveryProgressDeliveryFragment();
+ fragment.showLearningLaunch = showLearningLaunch;
+ fragment.showDeliveryLaunch = showDeliveryLaunch;
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ view = inflater.inflate(R.layout.fragment_connect_progress_delivery, container, false);
+
+ launchButton = view.findViewById(R.id.connect_progress_button);
+ launchButton.setVisibility(showDeliveryLaunch ? View.VISIBLE : View.GONE);
+
+ launchButton.setOnClickListener(v -> {
+ launchDeliveryApp(launchButton);
+ });
+
+ Button reviewButton = view.findViewById(R.id.connect_progress_review_button);
+ reviewButton.setVisibility(showLearningLaunch ? View.VISIBLE : View.GONE);
+ reviewButton.setOnClickListener(v -> {
+ launchLearningApp(reviewButton);
+ });
+
+ updateView();
+
+ return view;
+ }
+
+ private void launchLearningApp(Button button) {
+ ConnectJobRecord job = ConnectManager.getActiveJob();
+ if(ConnectManager.isAppInstalled(job.getLearnAppInfo().getAppId())) {
+ ConnectManager.launchApp(getContext(), true, job.getLearnAppInfo().getAppId());
+ getActivity().finish();
+ }
+ else {
+ String title = getString(R.string.connect_downloading_learn);
+ Navigation.findNavController(button).navigate(ConnectDeliveryProgressFragmentDirections.actionConnectJobDeliveryProgressFragmentToConnectDownloadingFragment(title, true, true));
+ }
+ }
+
+ private void launchDeliveryApp(Button button) {
+ ConnectJobRecord job = ConnectManager.getActiveJob();
+ if(ConnectManager.isAppInstalled(job.getDeliveryAppInfo().getAppId())) {
+ ConnectManager.launchApp(getContext(), false, job.getDeliveryAppInfo().getAppId());
+ getActivity().finish();
+ }
+ else {
+ String title = getString(R.string.connect_downloading_delivery);
+ Navigation.findNavController(button).navigate(ConnectDeliveryProgressFragmentDirections.actionConnectJobDeliveryProgressFragmentToConnectDownloadingFragment(title, false, true));
+ }
+ }
+
+ public void updateView() {
+ ConnectJobRecord job = ConnectManager.getActiveJob();
+ if(job == null || view == null) {
+ return;
+ }
+
+ int completed = job.getCompletedVisits();
+ int total = job.getMaxVisits();
+ int percent = total > 0 ? (100 * completed / total) : 100;
+
+ ProgressBar progress = view.findViewById(R.id.connect_progress_progress_bar);
+ progress.setProgress(percent);
+ progress.setMax(100);
+
+ TextView textView = view.findViewById(R.id.connect_progress_progress_text);
+ textView.setText(String.format(Locale.getDefault(), "%d%%", percent));
+
+ launchButton.setEnabled(!job.getIsUserSuspended());
+
+ textView = view.findViewById(R.id.connect_progress_status_text);
+ String completedText = getString(R.string.connect_progress_status, completed, total);
+ if(job.isMultiPayment() && completed > 0) {
+ //Get counts for each type
+ Hashtable paymentCounts = job.getDeliveryCountsPerPaymentUnit(false);
+
+ //Add a line for each payment unit
+ 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);
+ }
+ }
+
+ textView.setText(completedText);
+
+ int totalVisitCount = job.getDeliveries().size();
+ int dailyVisitCount = job.numberOfDeliveriesToday();
+ boolean finished = job.isFinished();
+
+ String warningText = null;
+ if(finished) {
+ warningText = getString(R.string.connect_progress_warning_ended);
+ } else if(job.getProjectStartDate().after(new Date())) {
+ warningText = getString(R.string.connect_progress_warning_not_started);
+ } else if(job.isMultiPayment()) {
+ List warnings = new ArrayList<>();
+ Hashtable totalPaymentCounts = job.getDeliveryCountsPerPaymentUnit(false);
+ Hashtable todayPaymentCounts = job.getDeliveryCountsPerPaymentUnit(true);
+ for(int i=0; i= unit.getMaxTotal()) {
+ //Reached max total for this type
+ warnings.add(getString(R.string.connect_progress_warning_max_reached_multi, unit.getName()));
+ }
+ else {
+ int todayCount = 0;
+ if (todayPaymentCounts.containsKey(stringKey)) {
+ todayCount = todayPaymentCounts.get(stringKey);
+ }
+
+ if(todayCount >= unit.getMaxDaily()) {
+ //Reached daily max for this type
+ warnings.add(getString(R.string.connect_progress_warning_daily_max_reached_multi,
+ unit.getName()));
+ }
+ }
+ }
+
+ if(warnings.size() > 0) {
+ warningText = String.join("\n", warnings);
+ }
+ } else {
+ if(totalVisitCount >= job.getMaxVisits()) {
+ warningText = getString(R.string.connect_progress_warning_max_reached_single);
+ } else if(dailyVisitCount >= job.getMaxDailyVisits()) {
+ warningText = getString(R.string.connect_progress_warning_daily_max_reached_single);
+ }
+ }
+
+ textView = view.findViewById(R.id.connect_progress_delivery_warning_text);
+ textView.setVisibility(warningText != null ? View.VISIBLE : View.GONE);
+ if(warningText != null) {
+ textView.setText(warningText);
+ }
+
+ textView = view.findViewById(R.id.connect_progress_complete_by_text);
+ String endText = ConnectManager.formatDate(job.getProjectEndDate());
+ String text;
+ if(finished) {
+ //Project ended
+ text = getString(R.string.connect_progress_ended, endText);
+ }
+ else if(job.getProjectStartDate() != null && job.getProjectStartDate().after(new Date())) {
+ //Project hasn't started yet
+ text = getString(R.string.connect_progress_begin_date, ConnectManager.formatDate(job.getProjectStartDate()), endText);
+ } else if (job.getIsUserSuspended()) {
+ text = getString(R.string.user_suspended);
+ } else {
+ text = getString(R.string.connect_progress_complete_by, endText);
+ }
+ textView.setText(text);
+ int color = job.getIsUserSuspended() ? R.color.red : R.color.black;
+ textView.setTextColor(getResources().getColor(color));
+
+ textView = view.findViewById(R.id.connect_progress_warning_learn_text);
+ textView.setOnClickListener(v -> {
+ StandardAlertDialog dialog = new StandardAlertDialog(
+ getContext(),
+ getString(R.string.connect_progress_warning),
+ getString(R.string.connect_progress_warning_full));
+ dialog.setPositiveButton(Localization.get("dialog.ok"), (dialog1, which) -> {
+ dialog1.dismiss();
+ });
+ ((CommCareActivity>)getActivity()).showAlertDialog(dialog);
+ });
+ }
+}
diff --git a/app/src/org/commcare/fragments/connect/ConnectDeliveryProgressFragment.java b/app/src/org/commcare/fragments/connect/ConnectDeliveryProgressFragment.java
new file mode 100644
index 000000000..f988d989c
--- /dev/null
+++ b/app/src/org/commcare/fragments/connect/ConnectDeliveryProgressFragment.java
@@ -0,0 +1,258 @@
+package org.commcare.fragments.connect;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.constraintlayout.widget.ConstraintLayout;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.lifecycle.Lifecycle;
+import androidx.viewpager2.adapter.FragmentStateAdapter;
+import androidx.viewpager2.widget.ViewPager2;
+
+import com.google.android.material.tabs.TabLayout;
+
+import org.commcare.android.database.connect.models.ConnectJobPaymentRecord;
+import org.commcare.android.database.connect.models.ConnectJobRecord;
+import org.commcare.connect.ConnectManager;
+import org.commcare.connect.network.ConnectNetworkHelper;
+import org.commcare.dalvik.R;
+import org.commcare.google.services.analytics.FirebaseAnalyticsUtil;
+
+import java.util.Date;
+
+/**
+ * Fragment for showing delivery progress for a Connect job
+ *
+ * @author dviggiano
+ */
+public class ConnectDeliveryProgressFragment extends Fragment {
+ private ConnectDeliveryProgressFragment.ViewStateAdapter viewStateAdapter;
+ private TextView updateText;
+
+ private ConstraintLayout paymentAlertTile;
+ private TextView paymentAlertText;
+ private ConnectJobPaymentRecord paymentToConfirm = null;
+ private boolean showLearningLaunch = true;
+ private boolean showDeliveryLaunch = true;
+ private String tabPosition = "";
+ boolean isTabChange = false;
+
+ public ConnectDeliveryProgressFragment() {
+ // Required empty public constructor
+ }
+
+ public static ConnectDeliveryProgressFragment newInstance(boolean showLearningLaunch, boolean showDeliveryLaunch) {
+ ConnectDeliveryProgressFragment fragment = new ConnectDeliveryProgressFragment();
+ fragment.showLearningLaunch = showLearningLaunch;
+ fragment.showDeliveryLaunch = showDeliveryLaunch;
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ ConnectJobRecord job = ConnectManager.getActiveJob();
+ getActivity().setTitle(job.getTitle());
+
+ if (getArguments() != null) {
+ showLearningLaunch = getArguments().getBoolean("showLaunch", true);
+ showDeliveryLaunch = getArguments().getBoolean("showLaunch", true);
+ tabPosition = getArguments().getString("tabPosition", "0");
+ }
+
+ View view = inflater.inflate(R.layout.fragment_connect_delivery_progress, container, false);
+
+ updateText = view.findViewById(R.id.connect_delivery_last_update);
+ updateUpdatedDate(job.getLastDeliveryUpdate());
+
+ ImageView refreshButton = view.findViewById(R.id.connect_delivery_refresh);
+ refreshButton.setOnClickListener(v -> refreshData());
+
+ paymentAlertTile = view.findViewById(R.id.connect_delivery_progress_alert_tile);
+ paymentAlertText = view.findViewById(R.id.connect_payment_confirm_label);
+ TextView paymentAlertNoButton = view.findViewById(R.id.connect_payment_confirm_no_button);
+ paymentAlertNoButton.setOnClickListener(v -> {
+ updatePaymentConfirmationTile(getContext(), true);
+ FirebaseAnalyticsUtil.reportCccPaymentConfirmationInteraction(false);
+ });
+
+ TextView paymentAlertYesButton = view.findViewById(R.id.connect_payment_confirm_yes_button);
+ paymentAlertYesButton.setOnClickListener(v -> {
+ final ConnectJobPaymentRecord payment = paymentToConfirm;
+ //Dismiss the tile
+ updatePaymentConfirmationTile(getContext(), true);
+
+ if (payment != null) {
+ FirebaseAnalyticsUtil.reportCccPaymentConfirmationInteraction(true);
+
+ ConnectManager.updatePaymentConfirmed(getContext(), payment, true, success -> {
+ //Nothing to do
+ });
+ }
+ });
+
+ final ViewPager2 pager = view.findViewById(R.id.connect_delivery_progress_view_pager);
+ viewStateAdapter = new ConnectDeliveryProgressFragment.ViewStateAdapter(getChildFragmentManager(),
+ getLifecycle(), showLearningLaunch, showDeliveryLaunch);
+ pager.setAdapter(viewStateAdapter);
+
+ final TabLayout tabLayout = view.findViewById(R.id.connect_delivery_progress_tabs);
+ tabLayout.addTab(tabLayout.newTab().setText(R.string.connect_progress_delivery));
+ tabLayout.addTab(tabLayout.newTab().setText(R.string.connect_progress_delivery_verification));
+
+ if (tabPosition.equals("1")) {
+ TabLayout.Tab tab = tabLayout.getTabAt(Integer.parseInt(tabPosition));
+ if (tab != null) {
+ isTabChange = true;
+ tabLayout.selectTab(tab);
+ pager.setCurrentItem(Integer.parseInt(tabPosition), false);
+ }
+ }
+
+ pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
+ @Override
+ public void onPageSelected(int position) {
+ // This flag is used to handle cases when a tab is set programmatically,
+ // ensuring that onPageSelection does not set the default tab in such scenarios.
+ if (!isTabChange) {
+ TabLayout.Tab tab = tabLayout.getTabAt(position);
+ tabLayout.selectTab(tab);
+
+ FirebaseAnalyticsUtil.reportConnectTabChange(tab.getText().toString());
+ } else {
+ isTabChange = false;
+ }
+ }
+ });
+
+ tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
+ @Override
+ public void onTabSelected(TabLayout.Tab tab) {
+ pager.setCurrentItem(tab.getPosition());
+ }
+
+ @Override
+ public void onTabUnselected(TabLayout.Tab tab) {
+
+ }
+
+ @Override
+ public void onTabReselected(TabLayout.Tab tab) {
+
+ }
+ });
+
+ updatePaymentConfirmationTile(getContext(), false);
+
+ return view;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ if (ConnectManager.isUnlocked()) {
+ refreshData();
+ }
+ }
+
+ public void refreshData() {
+ ConnectJobRecord job = ConnectManager.getActiveJob();
+ ConnectManager.updateDeliveryProgress(getContext(), job, success -> {
+ if (success) {
+ try {
+ updateUpdatedDate(new Date());
+ updatePaymentConfirmationTile(getContext(), false);
+ viewStateAdapter.refresh();
+ } catch (Exception e) {
+ //Ignore exception, happens if we leave the page before API call finishes
+ }
+ }
+ });
+ }
+
+ private void updatePaymentConfirmationTile(Context context, boolean forceHide) {
+ ConnectJobRecord job = ConnectManager.getActiveJob();
+ paymentToConfirm = null;
+ if (!forceHide) {
+ //Look for at least one payment that needs to be confirmed
+ for (ConnectJobPaymentRecord payment : job.getPayments()) {
+ if (payment.allowConfirm()) {
+ paymentToConfirm = payment;
+ break;
+ }
+ }
+ }
+
+ //NOTE: Checking for network connectivity here
+ boolean show = paymentToConfirm != null;
+ if (show) {
+ show = ConnectNetworkHelper.isOnline(context);
+ FirebaseAnalyticsUtil.reportCccPaymentConfirmationOnlineCheck(show);
+ }
+
+ paymentAlertTile.setVisibility(show ? View.VISIBLE : View.GONE);
+ if (show) {
+ String date = ConnectManager.formatDate(paymentToConfirm.getDate());
+ paymentAlertText.setText(getString(R.string.connect_payment_confirm_text, paymentToConfirm.getAmount(), job.getCurrency(), date));
+
+ FirebaseAnalyticsUtil.reportCccPaymentConfirmationDisplayed();
+ }
+ }
+
+ private void updateUpdatedDate(Date lastUpdate) {
+ updateText.setText(getString(R.string.connect_last_update, ConnectManager.formatDateTime(lastUpdate)));
+ }
+
+ private static class ViewStateAdapter extends FragmentStateAdapter {
+ private static ConnectDeliveryProgressDeliveryFragment deliveryFragment = null;
+ private static ConnectResultsSummaryListFragment verificationFragment = null;
+ private final boolean showLearningLaunch;
+ private final boolean showDeliveryLaunch;
+
+ public ViewStateAdapter(@NonNull FragmentManager fragmentManager, @NonNull Lifecycle lifecycle, boolean showLearningLaunch, boolean showDeliveryLaunch) {
+ super(fragmentManager, lifecycle);
+ this.showLearningLaunch = showLearningLaunch;
+ this.showDeliveryLaunch = showDeliveryLaunch;
+ }
+
+ @NonNull
+ @Override
+ public Fragment createFragment(int position) {
+ if (position == 0) {
+ deliveryFragment = ConnectDeliveryProgressDeliveryFragment.newInstance(showLearningLaunch, showDeliveryLaunch);
+ return deliveryFragment;
+ }
+
+ verificationFragment = ConnectResultsSummaryListFragment.newInstance();
+ return verificationFragment;
+ }
+
+ @Override
+ public int getItemCount() {
+ return 2;
+ }
+
+ public void refresh() {
+ if (deliveryFragment != null) {
+ deliveryFragment.updateView();
+ }
+
+ if (verificationFragment != null) {
+ verificationFragment.updateView();
+ }
+ }
+ }
+}
diff --git a/app/src/org/commcare/fragments/connect/ConnectDownloadingFragment.java b/app/src/org/commcare/fragments/connect/ConnectDownloadingFragment.java
new file mode 100644
index 000000000..9ac7e41e8
--- /dev/null
+++ b/app/src/org/commcare/fragments/connect/ConnectDownloadingFragment.java
@@ -0,0 +1,218 @@
+package org.commcare.fragments.connect;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.fragment.app.Fragment;
+import androidx.navigation.NavDirections;
+import androidx.navigation.Navigation;
+
+import org.commcare.activities.connect.ConnectActivity;
+import org.commcare.connect.ConnectManager;
+import org.commcare.android.database.connect.models.ConnectAppRecord;
+import org.commcare.android.database.connect.models.ConnectJobRecord;
+import org.commcare.dalvik.R;
+import org.commcare.engine.resource.AppInstallStatus;
+import org.commcare.engine.resource.ResourceInstallUtils;
+import org.commcare.resources.model.InvalidResourceException;
+import org.commcare.resources.model.UnresolvedResourceException;
+import org.commcare.tasks.ResourceEngineListener;
+import org.commcare.views.notifications.NotificationActionButtonInfo;
+import org.javarosa.core.reference.InvalidReferenceException;
+import org.javarosa.core.services.locale.LocaleTextException;
+import org.javarosa.core.services.locale.Localization;
+
+public class ConnectDownloadingFragment extends Fragment implements ResourceEngineListener {
+
+ private ProgressBar progressBar;
+ private TextView statusText;
+ private boolean getLearnApp;
+ private boolean goToApp;
+
+ public ConnectDownloadingFragment() {
+ // Required empty public constructor
+ }
+
+ public static ConnectDownloadingFragment newInstance() {
+ return new ConnectDownloadingFragment();
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ ConnectDownloadingFragmentArgs args = ConnectDownloadingFragmentArgs.fromBundle(getArguments());
+ getLearnApp = args.getLearning();
+ goToApp = args.getGoToApp();
+
+ //Disable back button during install (done by providing empty callback)
+ setBackButtonEnabled(false);
+
+ //Disable the default wait dialog during this fragment since it displays progress on its own
+ setWaitDialogEnabled(false);
+
+ startAppDownload();
+ }
+
+ private void setBackButtonEnabled(boolean enabled) {
+ Activity activity = getActivity();
+ if(activity instanceof ConnectActivity connectActivity) {
+ connectActivity.setBackButtonEnabled(enabled);
+ }
+ }
+
+ private void setWaitDialogEnabled(boolean enabled) {
+ Activity activity = getActivity();
+ if(activity instanceof ConnectActivity connectActivity) {
+ connectActivity.setWaitDialogEnabled(enabled);
+ }
+ }
+
+ private void startAppDownload() {
+ ConnectJobRecord job = ConnectManager.getActiveJob();
+ ConnectAppRecord record = getLearnApp ? job.getLearnAppInfo() : job.getDeliveryAppInfo();
+ ConnectManager.downloadAppOrResumeUpdates(record.getInstallUrl(), this);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ ConnectJobRecord job = ConnectManager.getActiveJob();
+ ConnectDownloadingFragmentArgs args = ConnectDownloadingFragmentArgs.fromBundle(getArguments());
+ getActivity().setTitle(job.getTitle());
+
+ View view = inflater.inflate(R.layout.fragment_connect_downloading, container, false);
+
+ TextView titleTv = view.findViewById(R.id.connect_downloading_title);
+ titleTv.setText(args.getTitle());
+
+ statusText = view.findViewById(R.id.connect_downloading_status);
+ updateInstallStatus(null);
+
+ progressBar = view.findViewById(R.id.connect_downloading_progress);
+
+ return view;
+ }
+
+ @Override
+ public void reportSuccess(boolean isNewInstall) {
+ Toast.makeText(getActivity(), R.string.connect_app_installed, Toast.LENGTH_SHORT).show();
+ onSuccessfulInstall();
+ }
+
+ private void onSuccessfulInstall() {
+ setWaitDialogEnabled(true);
+ ((ConnectActivity)getActivity()).startAppValidation();
+ }
+
+ public void onSuccessfulVerification() {
+ setBackButtonEnabled(true);
+ View view = getView();
+ if (view != null) {
+ if(goToApp) {
+ Navigation.findNavController(view).popBackStack();
+
+ //Launch the learn/deliver app
+ ConnectJobRecord job = ConnectManager.getActiveJob();
+ ConnectAppRecord appToLaunch = getLearnApp ? job.getLearnAppInfo() : job.getDeliveryAppInfo();
+ ConnectManager.launchApp(getContext(), getLearnApp, appToLaunch.getAppId());
+ getActivity().finish();
+ }
+ else {
+ //Go to learn/deliver progress
+ NavDirections directions;
+ if(getLearnApp) {
+ directions = ConnectDownloadingFragmentDirections.actionConnectDownloadingFragmentToConnectJobLearningProgressFragment();
+ }
+ else {
+ directions = ConnectDownloadingFragmentDirections.actionConnectDownloadingFragmentToConnectJobDeliveryProgressFragment();
+ }
+ Navigation.findNavController(statusText).navigate(directions);
+ }
+ }
+ }
+
+ @Override
+ public void failMissingResource(UnresolvedResourceException ure, AppInstallStatus statusmissing) {
+ showInstallFailError(statusmissing);
+ }
+
+ private void showInstallFailError(AppInstallStatus statusmissing) {
+ setBackButtonEnabled(true);
+ setWaitDialogEnabled(true);
+ String installError = getString(R.string.connect_app_install_unknown_error);
+ try {
+ installError = Localization.get(statusmissing.getLocaleKeyBase() + ".title");
+ } catch (LocaleTextException e) {
+ // do nothing
+ }
+ updateInstallStatus(installError);
+ }
+
+ private void updateInstallStatus(String status) {
+ statusText.setVisibility(status != null ? View.VISIBLE : View.GONE);
+ statusText.setText(status);
+ }
+
+ @Override
+ public void failInvalidResource(InvalidResourceException e, AppInstallStatus statusmissing) {
+ showInstallFailError(statusmissing);
+ }
+
+ @Override
+ public void failInvalidReference(InvalidReferenceException e, AppInstallStatus status) {
+ showInstallFailError(status);
+ }
+
+ @Override
+ public void failBadReqs(String vReq, String vAvail, boolean majorIsProblem) {
+ ResourceInstallUtils.showApkUpdatePrompt(getActivity(), vReq, vAvail);
+ showInstallFailError(AppInstallStatus.IncompatibleReqs);
+ }
+
+ @Override
+ public void failUnknown(AppInstallStatus statusfailunknown) {
+ showInstallFailError(statusfailunknown);
+ }
+
+ @Override
+ public void updateResourceProgress(int done, int pending, int phase) {
+ progressBar.setMax(pending);
+ progressBar.setProgress(done);
+
+ // Don't change the text on the progress dialog if we are showing the generic consumer
+ // apps startup dialog
+ String installProgressText =
+ Localization.getWithDefault("profile.found",
+ new String[]{"" + done, "" + pending},
+ "Setting up app...");
+
+ updateInstallStatus(installProgressText);
+ }
+
+ @Override
+ public void failWithNotification(AppInstallStatus statusfailstate) {
+ if (statusfailstate == AppInstallStatus.DuplicateApp) {
+ onSuccessfulInstall();
+ } else {
+ showInstallFailError(statusfailstate);
+ }
+ }
+
+ @Override
+ public void failWithNotification(AppInstallStatus statusfailstate,
+ NotificationActionButtonInfo.ButtonAction buttonAction) {
+ showInstallFailError(statusfailstate);
+ }
+
+ @Override
+ public void failTargetMismatch() {
+ ResourceInstallUtils.showTargetMismatchError(getActivity());
+ showInstallFailError(AppInstallStatus.IncorrectTargetPackage);
+ }
+}
diff --git a/app/src/org/commcare/fragments/connect/ConnectJobIntroFragment.java b/app/src/org/commcare/fragments/connect/ConnectJobIntroFragment.java
new file mode 100644
index 000000000..63bfec58e
--- /dev/null
+++ b/app/src/org/commcare/fragments/connect/ConnectJobIntroFragment.java
@@ -0,0 +1,144 @@
+package org.commcare.fragments.connect;
+
+import android.os.Bundle;
+
+import androidx.fragment.app.Fragment;
+import androidx.navigation.NavDirections;
+import androidx.navigation.Navigation;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.commcare.connect.ConnectDatabaseHelper;
+import org.commcare.connect.ConnectManager;
+import org.commcare.connect.network.ConnectNetworkHelper;
+import org.commcare.android.database.connect.models.ConnectJobRecord;
+import org.commcare.android.database.connect.models.ConnectLearnModuleSummaryRecord;
+import org.commcare.connect.network.ApiConnect;
+import org.commcare.connect.network.IApiCallback;
+import org.commcare.dalvik.R;
+import org.commcare.google.services.analytics.FirebaseAnalyticsUtil;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Fragment for showing detailed info about an available job
+ *
+ * @author dviggiano
+ */
+public class ConnectJobIntroFragment extends Fragment {
+ private boolean showLaunchButton = true;
+ public ConnectJobIntroFragment() {
+ // Required empty public constructor
+ }
+
+ public static ConnectJobIntroFragment newInstance(boolean showLaunchButton) {
+ ConnectJobIntroFragment fragment = new ConnectJobIntroFragment();
+ fragment.showLaunchButton = showLaunchButton;
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ ConnectJobRecord job = ConnectManager.getActiveJob();
+
+ getActivity().setTitle(job.getTitle());
+
+ View view = inflater.inflate(R.layout.fragment_connect_job_intro, container, false);
+
+ TextView textView = view.findViewById(R.id.connect_job_intro_title);
+ textView.setText(job.getTitle());
+
+ String visitPayment = job.getMoneyString(job.getTotalBudget());
+ String fullDescription = String.format(Locale.getDefault(), getString(R.string.connect_job_full_description), job.getDescription(), visitPayment);
+
+ textView = view.findViewById(R.id.connect_job_intro_description);
+ textView.setText(fullDescription);
+
+ int totalHours = 0;
+ List lines = new ArrayList<>();
+ List modules = job.getLearnAppInfo().getLearnModules();
+ for (int i = 0; i < modules.size(); i++) {
+ lines.add(String.format(Locale.getDefault(), "%d. %s", (i + 1), modules.get(i).getName()));
+ totalHours += modules.get(i).getTimeEstimate();
+ }
+
+ String toLearn = modules.size() > 0 ? String.join("\r\n\r\n", lines) : getString(R.string.connect_job_no_learning_required);
+
+ textView = view.findViewById(R.id.connect_job_intro_learning);
+ textView.setText(toLearn);
+
+ textView = view.findViewById(R.id.connect_job_intro_learning_summary);
+ textView.setText(getString(R.string.connect_job_learn_summary, modules.size(), totalHours));
+
+ final boolean appInstalled = ConnectManager.isAppInstalled(job.getLearnAppInfo().getAppId());
+
+ Button button = view.findViewById(R.id.connect_job_intro_start_button);
+ button.setVisibility(showLaunchButton ? View.VISIBLE : View.GONE);
+ if(showLaunchButton) {
+ button.setText(getString(appInstalled ? R.string.connect_job_go_to_learn_app : R.string.connect_job_download_learn_app));
+ button.setOnClickListener(v -> {
+ //First, need to tell Connect we're starting learning so it can create a user on HQ
+ ApiConnect.startLearnApp(getContext(), job.getJobId(), new IApiCallback() {
+ @Override
+ public void processSuccess(int responseCode, InputStream responseData) {
+ reportApiCall(true);
+ //TODO: Expecting to eventually get HQ username from server here
+
+ job.setStatus(ConnectJobRecord.STATUS_LEARNING);
+ ConnectDatabaseHelper.upsertJob(getContext(), job);
+
+ NavDirections directions;
+ if (appInstalled) {
+ directions = ConnectJobIntroFragmentDirections.actionConnectJobIntroFragmentToConnectJobLearningProgressFragment();
+ } else {
+ String title = getString(R.string.connect_downloading_learn);
+ directions = ConnectJobIntroFragmentDirections.actionConnectJobIntroFragmentToConnectDownloadingFragment(title, true, false);
+ }
+
+ Navigation.findNavController(button).navigate(directions);
+ }
+
+ @Override
+ public void processFailure(int responseCode, IOException e) {
+ Toast.makeText(getContext(), "Connect: error starting learning", Toast.LENGTH_SHORT).show();
+ reportApiCall(false);
+ //TODO: Log the message from the server
+ }
+
+ @Override
+ public void processNetworkFailure() {
+ ConnectNetworkHelper.showNetworkError(getContext());
+ reportApiCall(false);
+ }
+
+ @Override
+ public void processOldApiError() {
+ ConnectNetworkHelper.showOutdatedApiError(getContext());
+ reportApiCall(false);
+ }
+ });
+ });
+ }
+
+ return view;
+ }
+
+ private void reportApiCall(boolean success) {
+ FirebaseAnalyticsUtil.reportCccApiStartLearning(success);
+ }
+}
diff --git a/app/src/org/commcare/fragments/connect/ConnectJobsAvailableListFragment.java b/app/src/org/commcare/fragments/connect/ConnectJobsAvailableListFragment.java
new file mode 100644
index 000000000..645eeb2c1
--- /dev/null
+++ b/app/src/org/commcare/fragments/connect/ConnectJobsAvailableListFragment.java
@@ -0,0 +1,61 @@
+package org.commcare.fragments.connect;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import org.commcare.connect.IConnectAppLauncher;
+import org.commcare.adapters.ConnectJobAdapter;
+import org.commcare.dalvik.R;
+
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.DividerItemDecoration;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * Fragment for showing the available jobs list
+ *
+ * @author dviggiano
+ */
+public class ConnectJobsAvailableListFragment extends Fragment {
+ private ConnectJobAdapter adapter;
+ private IConnectAppLauncher launcher;
+ public ConnectJobsAvailableListFragment() {
+ // Required empty public constructor
+ }
+
+ public static ConnectJobsAvailableListFragment newInstance(IConnectAppLauncher appLauncher) {
+ ConnectJobsAvailableListFragment fragment = new ConnectJobsAvailableListFragment();
+ fragment.launcher = appLauncher;
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_connect_available_jobs_list, container, false);
+
+ RecyclerView recyclerView = view.findViewById(R.id.available_jobs_list);
+
+ LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getContext());
+ recyclerView.setLayoutManager(linearLayoutManager);
+
+ recyclerView.addItemDecoration(new DividerItemDecoration(getContext(), linearLayoutManager.getOrientation()));
+
+ adapter = new ConnectJobAdapter(true, launcher);
+ recyclerView.setAdapter(adapter);
+
+ return view;
+ }
+
+ public void updateView() {
+ adapter.notifyDataSetChanged();
+ }
+}
diff --git a/app/src/org/commcare/fragments/connect/ConnectJobsListsFragment.java b/app/src/org/commcare/fragments/connect/ConnectJobsListsFragment.java
new file mode 100644
index 000000000..80ed6d2f5
--- /dev/null
+++ b/app/src/org/commcare/fragments/connect/ConnectJobsListsFragment.java
@@ -0,0 +1,260 @@
+package org.commcare.fragments.connect;
+
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.constraintlayout.widget.ConstraintLayout;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.lifecycle.Lifecycle;
+import androidx.viewpager2.adapter.FragmentStateAdapter;
+import androidx.viewpager2.widget.ViewPager2;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.google.android.material.tabs.TabLayout;
+
+import org.commcare.activities.CommCareActivity;
+import org.commcare.connect.ConnectDatabaseHelper;
+import org.commcare.connect.ConnectManager;
+import org.commcare.connect.IConnectAppLauncher;
+import org.commcare.connect.network.ConnectNetworkHelper;
+import org.commcare.android.database.connect.models.ConnectJobRecord;
+import org.commcare.CommCareApplication;
+import org.commcare.connect.network.ApiConnect;
+import org.commcare.connect.network.IApiCallback;
+import org.commcare.dalvik.R;
+import org.commcare.google.services.analytics.FirebaseAnalyticsUtil;
+import org.javarosa.core.io.StreamsUtil;
+import org.javarosa.core.services.Logger;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Fragment for showing the two job lists (available and mine)
+ *
+ * @author dviggiano
+ */
+public class ConnectJobsListsFragment extends Fragment {
+ private ConstraintLayout connectTile;
+ private TabLayout tabLayout;
+ private ViewPager2 viewPager;
+ private ViewStateAdapter viewStateAdapter;
+ private TextView updateText;
+ private IConnectAppLauncher launcher;
+
+ public ConnectJobsListsFragment() {
+ // Required empty public constructor
+ }
+
+ public static ConnectJobsListsFragment newInstance(IConnectAppLauncher appLauncher) {
+ ConnectJobsListsFragment fragment = new ConnectJobsListsFragment();
+ fragment.launcher = appLauncher;
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ getActivity().setTitle(R.string.connect_title);
+
+ View view = inflater.inflate(R.layout.fragment_connect_jobs_list, container, false);
+
+ connectTile = view.findViewById(R.id.connect_alert_tile);
+
+ updateText = view.findViewById(R.id.connect_jobs_last_update);
+ updateUpdatedDate(ConnectDatabaseHelper.getLastJobsUpdate(getContext()));
+
+ ImageView refreshButton = view.findViewById(R.id.connect_jobs_refresh);
+ refreshButton.setOnClickListener(v -> refreshData());
+
+ viewPager = view.findViewById(R.id.jobs_view_pager);
+ viewStateAdapter = new ViewStateAdapter(getChildFragmentManager(), getLifecycle(), (appId, isLearning) -> {
+ //Launch app and finish this activity
+ ConnectManager.launchApp(getActivity(), isLearning, appId);
+ getActivity().finish();
+ });
+ viewPager.setAdapter(viewStateAdapter);
+
+ tabLayout = view.findViewById(R.id.connect_jobs_tabs);
+ tabLayout.addTab(tabLayout.newTab().setText(R.string.connect_jobs_all));
+ tabLayout.addTab(tabLayout.newTab().setText(R.string.connect_jobs_mine));
+
+ viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
+ @Override
+ public void onPageSelected(int position) {
+ TabLayout.Tab tab = tabLayout.getTabAt(position);
+ tabLayout.selectTab(tab);
+
+ FirebaseAnalyticsUtil.reportConnectTabChange(tab.getText().toString());
+ }
+ });
+
+ tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
+ @Override
+ public void onTabSelected(TabLayout.Tab tab) {
+ viewPager.setCurrentItem(tab.getPosition());
+ }
+
+ @Override
+ public void onTabUnselected(TabLayout.Tab tab) {
+
+ }
+
+ @Override
+ public void onTabReselected(TabLayout.Tab tab) {
+
+ }
+ });
+
+ chooseTab();
+ refreshUi();
+ refreshData();
+
+ return view;
+ }
+
+ public void refreshData() {
+ ApiConnect.getConnectOpportunities(getContext(), new IApiCallback() {
+ @Override
+ public void processSuccess(int responseCode, InputStream responseData) {
+ int newJobs = 0;
+ //TODO: Sounds like we don't want a try-catch here, better to crash. Verify before changing
+ try {
+ String responseAsString = new String(StreamsUtil.inputStreamToByteArray(responseData));
+ if (responseAsString.length() > 0) {
+ //Parse the JSON
+ JSONArray json = new JSONArray(responseAsString);
+ List jobs = new ArrayList<>(json.length());
+ for (int i = 0; i < json.length(); i++) {
+ JSONObject obj = (JSONObject)json.get(i);
+ jobs.add(ConnectJobRecord.fromJson(obj));
+ }
+
+ //Store retrieved jobs
+ newJobs = ConnectDatabaseHelper.storeJobs(getContext(), jobs, true);
+ }
+ } catch (IOException | JSONException | ParseException e) {
+ Logger.exception("Parsing return from Opportunities request", e);
+ }
+
+ reportApiCall(true, newJobs);
+
+ refreshUi();
+ }
+
+ @Override
+ public void processFailure(int responseCode, IOException e) {
+ Logger.log("ERROR", String.format(Locale.getDefault(), "Opportunities call failed: %d", responseCode));
+ reportApiCall(false, 0);
+ refreshUi();
+ }
+
+ @Override
+ public void processNetworkFailure() {
+ Logger.log("ERROR", "Failed (network)");
+ reportApiCall(false, 0);
+ refreshUi();
+ }
+
+ @Override
+ public void processOldApiError() {
+ ConnectNetworkHelper.showOutdatedApiError(getContext());
+ reportApiCall(false, 0);
+ }
+ });
+ }
+
+ private void reportApiCall(boolean success, int newJobs) {
+ FirebaseAnalyticsUtil.reportCccApiJobs(success, newJobs);
+ }
+
+ private void refreshUi() {
+ try {
+ updateUpdatedDate(new Date());
+ updateSecondaryPhoneConfirmationTile();
+ viewStateAdapter.refresh();
+ chooseTab();
+ }
+ catch(Exception e) {
+ //Ignore exception, happens if we leave the page before API call finishes
+ }
+ }
+
+ private void updateSecondaryPhoneConfirmationTile() {
+ boolean show = ConnectManager.shouldShowSecondaryPhoneConfirmationTile(getContext());
+
+ ConnectManager.updateSecondaryPhoneConfirmationTile(getContext(), connectTile, show, v -> {
+ ConnectManager.verifySecondaryPhone((CommCareActivity>)getActivity(), success -> {
+ updateSecondaryPhoneConfirmationTile();
+ });
+ });
+ }
+
+ private void updateUpdatedDate(Date lastUpdate) {
+ updateText.setText(getString(R.string.connect_last_update, ConnectManager.formatDateTime(lastUpdate)));
+ }
+
+ private void chooseTab() {
+ int numAvailable = ConnectDatabaseHelper.getAvailableJobs(CommCareApplication.instance()).size();
+ int index = numAvailable > 0 ? 0 : 1;
+ viewPager.setCurrentItem(index);
+ tabLayout.setScrollPosition(index, 0f, true);
+ }
+
+ private static class ViewStateAdapter extends FragmentStateAdapter {
+ static ConnectJobsAvailableListFragment availableFragment;
+ static ConnectJobsMyListFragment myFragment;
+ final IConnectAppLauncher launcher;
+
+ public ViewStateAdapter(@NonNull FragmentManager fragmentManager, @NonNull Lifecycle lifecycle, IConnectAppLauncher appLauncher) {
+ super(fragmentManager, lifecycle);
+ launcher = appLauncher;
+ }
+
+ @NonNull
+ @Override
+ public Fragment createFragment(int position) {
+ if (position == 0) {
+ availableFragment = ConnectJobsAvailableListFragment.newInstance(launcher);
+ return availableFragment;
+ }
+
+ myFragment = ConnectJobsMyListFragment.newInstance(launcher);
+ return myFragment;
+ }
+
+ @Override
+ public int getItemCount() {
+ return 2;
+ }
+
+ public void refresh() {
+ if (availableFragment != null) {
+ availableFragment.updateView();
+ }
+
+ if (myFragment != null) {
+ myFragment.updateView();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/org/commcare/fragments/connect/ConnectJobsMyListFragment.java b/app/src/org/commcare/fragments/connect/ConnectJobsMyListFragment.java
new file mode 100644
index 000000000..3718dd772
--- /dev/null
+++ b/app/src/org/commcare/fragments/connect/ConnectJobsMyListFragment.java
@@ -0,0 +1,64 @@
+package org.commcare.fragments.connect;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import org.commcare.connect.IConnectAppLauncher;
+import org.commcare.adapters.ConnectJobAdapter;
+import org.commcare.dalvik.R;
+
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.DividerItemDecoration;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * Fragment for showing the "My Jobs" list
+ *
+ * @author dviggiano
+ */
+public class ConnectJobsMyListFragment extends Fragment {
+ private ConnectJobAdapter adapter;
+ private IConnectAppLauncher launcher;
+
+ public ConnectJobsMyListFragment() {
+ // Required empty public constructor
+ }
+
+ public static ConnectJobsMyListFragment newInstance(IConnectAppLauncher appLauncher) {
+ ConnectJobsMyListFragment fragment = new ConnectJobsMyListFragment();
+ fragment.launcher = appLauncher;
+ return fragment;
+
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ // Inflate the layout for this fragment
+ View view = inflater.inflate(R.layout.fragment_connect_my_jobs_lists, container, false);
+
+ RecyclerView recyclerView = view.findViewById(R.id.my_jobs_list);
+
+ LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getContext());
+ recyclerView.setLayoutManager(linearLayoutManager);
+
+ recyclerView.addItemDecoration(new DividerItemDecoration(getContext(), linearLayoutManager.getOrientation()));
+
+ adapter = new ConnectJobAdapter(false, launcher);
+ recyclerView.setAdapter(adapter);
+
+ return view;
+ }
+
+ public void updateView() {
+ adapter.notifyDataSetChanged();
+ }
+}
diff --git a/app/src/org/commcare/fragments/connect/ConnectLearningProgressFragment.java b/app/src/org/commcare/fragments/connect/ConnectLearningProgressFragment.java
new file mode 100644
index 000000000..3bc3fc3f4
--- /dev/null
+++ b/app/src/org/commcare/fragments/connect/ConnectLearningProgressFragment.java
@@ -0,0 +1,303 @@
+package org.commcare.fragments.connect;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import org.commcare.activities.CommCareActivity;
+import org.commcare.connect.ConnectDatabaseHelper;
+import org.commcare.connect.ConnectManager;
+import org.commcare.connect.network.ConnectNetworkHelper;
+import org.commcare.android.database.connect.models.ConnectJobAssessmentRecord;
+import org.commcare.android.database.connect.models.ConnectJobLearningRecord;
+import org.commcare.android.database.connect.models.ConnectJobRecord;
+import org.commcare.connect.network.ApiConnect;
+import org.commcare.connect.network.IApiCallback;
+import org.commcare.dalvik.R;
+import org.commcare.google.services.analytics.FirebaseAnalyticsUtil;
+import org.commcare.views.dialogs.StandardAlertDialog;
+import org.javarosa.core.io.StreamsUtil;
+import org.javarosa.core.services.Logger;
+import org.javarosa.core.services.locale.Localization;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
+import androidx.fragment.app.Fragment;
+import androidx.navigation.NavDirections;
+import androidx.navigation.Navigation;
+
+/**
+ * Fragment for showing learning progress for a Connect job
+ *
+ * @author dviggiano
+ */
+public class ConnectLearningProgressFragment extends Fragment {
+ boolean showAppLaunch = true;
+ public ConnectLearningProgressFragment() {
+ // Required empty public constructor
+ }
+
+ public static ConnectLearningProgressFragment newInstance(boolean showAppLaunch) {
+ ConnectLearningProgressFragment fragment = new ConnectLearningProgressFragment();
+ fragment.showAppLaunch = showAppLaunch;
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ ConnectJobRecord job = ConnectManager.getActiveJob();
+ getActivity().setTitle(job.getTitle());
+
+ if(getArguments() != null) {
+ showAppLaunch = getArguments().getBoolean("showLaunch", true);
+ }
+
+ View view = inflater.inflate(R.layout.fragment_connect_learning_progress, container, false);
+
+ ImageView refreshButton = view.findViewById(R.id.connect_learning_refresh);
+ refreshButton.setOnClickListener(v -> {
+ refreshData();
+ });
+
+ updateUpdatedDate(job.getLastLearnUpdate());
+ updateUi(view);
+ refreshData();
+
+ return view;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ if(ConnectManager.isUnlocked()) {
+ refreshData();
+ }
+ }
+
+ private void refreshData() {
+ ConnectJobRecord job = ConnectManager.getActiveJob();
+ ConnectManager.updateLearningProgress(getContext(), job, success -> {
+ if(success) {
+ try {
+ updateUpdatedDate(new Date());
+ updateUi(null);
+ }
+ catch(Exception e) {
+ //Ignore exception, happens if we leave the page before API call finishes
+ }
+ }
+ });
+ }
+
+ private void updateUi(View view) {
+ if(view == null) {
+ view = getView();
+ }
+
+ if(view == null) {
+ return;
+ }
+
+ ConnectJobRecord job = ConnectManager.getActiveJob();
+
+ int percent = job.getLearningPercentComplete();
+ boolean learningFinished = percent >= 100;
+ boolean assessmentAttempted = job.attemptedAssessment();
+ boolean assessmentPassed = job.passedAssessment();
+
+ boolean showReviewLearningButton = false;
+ String status;
+ String buttonText;
+ if (learningFinished) {
+ if(assessmentAttempted) {
+ showReviewLearningButton = true;
+
+ if(assessmentPassed) {
+ status = getString(R.string.connect_learn_finished, job.getAssessmentScore(), job.getLearnAppInfo().getPassingScore());
+ buttonText = getString(R.string.connect_learn_view_details);
+ }
+ else {
+ status = getString(R.string.connect_learn_failed, job.getAssessmentScore(), job.getLearnAppInfo().getPassingScore());
+ buttonText = getString(R.string.connect_learn_try_again);
+ }
+ }
+ else {
+ status = getString(R.string.connect_learn_need_assessment);
+ buttonText = getString(R.string.connect_learn_go_to_assessment);
+ }
+ } else if(percent > 0) {
+ status = getString(R.string.connect_learn_status, job.getCompletedLearningModules(),
+ job.getNumLearningModules());
+ buttonText = getString(R.string.connect_learn_continue);
+ } else {
+ status = getString(R.string.connect_learn_not_started);
+ buttonText = getString(R.string.connect_learn_start);
+ }
+
+ TextView progressText = view.findViewById(R.id.connect_learning_progress_text);
+ ProgressBar progressBar = view.findViewById(R.id.connect_learning_progress_bar);
+ LinearLayout progressBarTextContainer = view.findViewById(R.id.connect_learn_progress_bar_text_container);
+
+ progressText.setVisibility(learningFinished ? View.GONE : View.VISIBLE);
+ progressBar.setVisibility(learningFinished ? View.GONE : View.VISIBLE);
+ progressBarTextContainer.setVisibility(learningFinished ? View.GONE : View.VISIBLE);
+ if(!learningFinished) {
+ progressBar.setProgress(percent);
+ progressBar.setMax(100);
+
+ progressText.setText(String.format(Locale.getDefault(), "%d%%", percent));
+ }
+
+ LinearLayout certContainer = view.findViewById(R.id.connect_learning_certificate_container);
+ certContainer.setVisibility(learningFinished && assessmentPassed ? View.VISIBLE : View.GONE);
+
+ int titleResource;
+ if(learningFinished) {
+ if(assessmentAttempted) {
+ if(assessmentPassed) {
+ titleResource = R.string.connect_learn_complete_title;
+ }
+ else {
+ titleResource = R.string.connect_learn_failed_title;
+ }
+ }
+ else {
+ titleResource = R.string.connect_learn_need_assessment_title;
+ }
+ }
+ else {
+ titleResource = R.string.connect_learn_progress_title;
+ }
+
+ TextView textView = view.findViewById(R.id.connect_learn_progress_title);
+ textView.setText(getString(titleResource));
+
+ textView = view.findViewById(R.id.connect_learning_claim_label);
+ textView.setVisibility(learningFinished && assessmentPassed ? View.VISIBLE : View.GONE);
+
+ textView = view.findViewById(R.id.connect_learning_status_text);
+ textView.setText(status);
+
+ TextView completeByText = view.findViewById(R.id.connect_learning_complete_by_text);
+ completeByText.setVisibility(learningFinished && assessmentPassed ? View.GONE : View.VISIBLE);
+
+ boolean finished = job.isFinished();
+ textView = view.findViewById(R.id.connect_learning_ended_text);
+ textView.setVisibility(finished ? View.VISIBLE : View.GONE);
+
+ textView = view.findViewById(R.id.connect_learning_warning_learn_text);
+ textView.setOnClickListener(v -> {
+ StandardAlertDialog dialog = new StandardAlertDialog(
+ getContext(),
+ getString(R.string.connect_progress_warning),
+ getString(R.string.connect_progress_warning_full));
+ dialog.setPositiveButton(Localization.get("dialog.ok"), (dialog1, which) -> {
+ dialog1.dismiss();
+ });
+ ((CommCareActivity>)getActivity()).showAlertDialog(dialog);
+ });
+
+ if(learningFinished) {
+ textView = view.findViewById(R.id.connect_learn_cert_subject);
+ textView.setText(job.getTitle());
+
+ textView = view.findViewById(R.id.connect_learn_cert_person);
+ textView.setText(ConnectManager.getUser(getContext()).getName());
+
+ Date latestDate = null;
+ List assessments = job.getAssessments();
+ if(assessments == null || assessments.size() == 0) {
+ for (ConnectJobLearningRecord learning : job.getLearnings()) {
+ if (latestDate == null || latestDate.before(learning.getDate())) {
+ latestDate = learning.getDate();
+ }
+ }
+ } else {
+ for (ConnectJobAssessmentRecord assessment : assessments) {
+ if (latestDate == null || latestDate.before(assessment.getDate())) {
+ latestDate = assessment.getDate();
+ }
+ }
+ }
+
+ if(latestDate == null) {
+ latestDate = new Date();
+ }
+
+ textView = view.findViewById(R.id.connect_learn_cert_date);
+ textView.setText(getString(R.string.connect_learn_completed, ConnectManager.formatDate(latestDate)));
+ } else {
+ completeByText.setText(getString(R.string.connect_learn_complete_by, ConnectManager.formatDate(job.getProjectEndDate())));
+ }
+
+ final Button reviewButton = view.findViewById(R.id.connect_learning_review_button);
+ reviewButton.setVisibility(showAppLaunch && showReviewLearningButton ? View.VISIBLE : View.GONE);
+ reviewButton.setText(R.string.connect_learn_review);
+ reviewButton.setOnClickListener(v -> {
+ NavDirections directions = null;
+ if(ConnectManager.isAppInstalled(job.getLearnAppInfo().getAppId())) {
+ ConnectManager.launchApp(getContext(), true, job.getLearnAppInfo().getAppId());
+ getActivity().finish();
+ } else {
+ String title = getString(R.string.connect_downloading_learn);
+ directions = ConnectLearningProgressFragmentDirections.actionConnectJobLearningProgressFragmentToConnectDownloadingFragment(title, true, true);
+ }
+
+ if(directions != null) {
+ Navigation.findNavController(reviewButton).navigate(directions);
+ }
+ });
+
+ final Button button = view.findViewById(R.id.connect_learning_button);
+ button.setVisibility(showAppLaunch ? View.VISIBLE : View.GONE);
+ button.setText(buttonText);
+ button.setOnClickListener(v -> {
+ NavDirections directions = null;
+ if(learningFinished && assessmentPassed) {
+ directions = ConnectLearningProgressFragmentDirections.actionConnectJobLearningProgressFragmentToConnectJobDeliveryDetailsFragment();
+ } else if(ConnectManager.isAppInstalled(job.getLearnAppInfo().getAppId())) {
+ ConnectManager.launchApp(getContext(), true, job.getLearnAppInfo().getAppId());
+ getActivity().finish();
+ } else {
+ String title = getString(R.string.connect_downloading_learn);
+ directions = ConnectLearningProgressFragmentDirections.actionConnectJobLearningProgressFragmentToConnectDownloadingFragment(title, true, true);
+ }
+
+ if(directions != null) {
+ Navigation.findNavController(button).navigate(directions);
+ }
+ });
+ }
+
+ private void updateUpdatedDate(Date lastUpdate) {
+ View view = getView();
+ if(view == null) {
+ return;
+ }
+
+ TextView updateText = view.findViewById(R.id.connect_learning_last_update);
+ updateText.setText(getString(R.string.connect_last_update, ConnectManager.formatDateTime(lastUpdate)));
+ }
+}
diff --git a/app/src/org/commcare/fragments/connect/ConnectResultsListFragment.java b/app/src/org/commcare/fragments/connect/ConnectResultsListFragment.java
new file mode 100644
index 000000000..06fd9de3b
--- /dev/null
+++ b/app/src/org/commcare/fragments/connect/ConnectResultsListFragment.java
@@ -0,0 +1,171 @@
+package org.commcare.fragments.connect;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.commcare.connect.ConnectManager;
+import org.commcare.android.database.connect.models.ConnectJobDeliveryRecord;
+import org.commcare.android.database.connect.models.ConnectJobPaymentRecord;
+import org.commcare.android.database.connect.models.ConnectJobRecord;
+import org.commcare.dalvik.R;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.DividerItemDecoration;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+public class ConnectResultsListFragment extends Fragment {
+ private ResultsAdapter adapter;
+ public ConnectResultsListFragment() {
+ // Required empty public constructor
+ }
+
+ public static ConnectResultsListFragment newInstance() {
+ return new ConnectResultsListFragment();
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ ConnectResultsListFragmentArgs args = ConnectResultsListFragmentArgs.fromBundle(getArguments());
+ ConnectJobRecord job = ConnectManager.getActiveJob();
+ boolean showPayments = args.getShowPayments();
+ getActivity().setTitle(job.getTitle());
+
+ View view = inflater.inflate(R.layout.fragment_connect_results_list, container, false);
+
+ RecyclerView recyclerView = view.findViewById(R.id.results_list);
+
+ LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getContext());
+ recyclerView.setLayoutManager(linearLayoutManager);
+
+ adapter = new ResultsAdapter(showPayments);
+ recyclerView.setAdapter(adapter);
+
+ recyclerView.addItemDecoration(new DividerItemDecoration(getContext(), linearLayoutManager.getOrientation()));
+
+ return view;
+ }
+
+ public void updateView() {
+ adapter.notifyDataSetChanged();
+ }
+
+ private static class ResultsAdapter extends RecyclerView.Adapter {
+ private final boolean showPayments;
+ private Context parentContext;
+ public ResultsAdapter(boolean showPayments) {
+ this.showPayments = showPayments;
+ }
+
+ @NonNull
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ parentContext = parent.getContext();
+ if(showPayments) {
+ return new PaymentViewHolder(LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.connect_payment_item, parent, false));
+ } else {
+ return new VerificationViewHolder(LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.connect_verification_item, parent, false));
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+ ConnectJobRecord job = ConnectManager.getActiveJob();
+ if(holder instanceof VerificationViewHolder verificationHolder) {
+ ConnectJobDeliveryRecord delivery = job.getDeliveries().get(position);
+
+ verificationHolder.nameText.setText(delivery.getEntityName());
+ verificationHolder.dateText.setText(ConnectManager.formatDate(delivery.getDate()));
+ verificationHolder.statusText.setText(delivery.getStatus());
+ verificationHolder.reasonText.setText(delivery.getReason());
+ } else if(holder instanceof PaymentViewHolder paymentHolder) {
+ final ConnectJobPaymentRecord payment = job.getPayments().get(position);
+
+ String money = job.getMoneyString(Integer.parseInt(payment.getAmount()));
+ paymentHolder.nameText.setText(parentContext.getString(R.string.connect_results_payment_description, money));
+
+ paymentHolder.dateText.setText(parentContext.getString(R.string.connect_results_payment_date, ConnectManager.formatDate(payment.getDate())));
+
+ boolean enabled = paymentHolder.updateConfirmedText(parentContext, payment);
+
+ if(enabled) {
+ paymentHolder.confirmText.setOnClickListener(v -> {
+ ConnectManager.updatePaymentConfirmed(parentContext, payment, !payment.getConfirmed(), success -> {
+ paymentHolder.updateConfirmedText(parentContext, payment);
+ });
+ });
+ }
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ ConnectJobRecord job = ConnectManager.getActiveJob();
+ return showPayments ? job.getPayments().size() : job.getDeliveries().size();
+ }
+
+ public static class VerificationViewHolder extends RecyclerView.ViewHolder {
+ final TextView nameText;
+ final TextView dateText;
+ final TextView statusText;
+ final TextView reasonText;
+
+ public VerificationViewHolder(@NonNull View itemView) {
+ super(itemView);
+
+ nameText = itemView.findViewById(R.id.delivery_item_name);
+ dateText = itemView.findViewById(R.id.delivery_item_date);
+ statusText = itemView.findViewById(R.id.delivery_item_status);
+ reasonText = itemView.findViewById(R.id.delivery_item_reason);
+ }
+ }
+
+ public static class PaymentViewHolder extends RecyclerView.ViewHolder {
+ final TextView nameText;
+ final TextView dateText;
+ final TextView confirmText;
+
+ public PaymentViewHolder(@NonNull View itemView) {
+ super(itemView);
+
+ nameText = itemView.findViewById(R.id.name);
+ dateText = itemView.findViewById(R.id.date);
+ confirmText = itemView.findViewById(R.id.confirm);
+ }
+
+ public boolean updateConfirmedText(Context context, ConnectJobPaymentRecord payment) {
+ boolean enabled;
+ int confirmTextId;
+ if(payment.getConfirmed()) {
+ enabled = payment.allowConfirmUndo();
+ confirmTextId = enabled ?
+ R.string.connect_results_payment_confirm_undo :
+ R.string.connect_results_payment_confirmed;
+ } else {
+ enabled = payment.allowConfirm();
+ confirmTextId = enabled ?
+ R.string.connect_results_payment_confirm :
+ R.string.connect_results_payment_not_confirmed;
+ }
+
+ confirmText.setText(confirmTextId);
+ confirmText.setTextColor(context.getResources().getColor(enabled ? R.color.blue : R.color.dark_grey));
+
+ return enabled;
+ }
+ }
+ }
+}
diff --git a/app/src/org/commcare/fragments/connect/ConnectResultsSummaryListFragment.java b/app/src/org/commcare/fragments/connect/ConnectResultsSummaryListFragment.java
new file mode 100644
index 000000000..00406f6ac
--- /dev/null
+++ b/app/src/org/commcare/fragments/connect/ConnectResultsSummaryListFragment.java
@@ -0,0 +1,195 @@
+package org.commcare.fragments.connect;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TextView;
+
+import org.commcare.android.database.connect.models.ConnectPaymentUnitRecord;
+import org.commcare.connect.ConnectManager;
+import org.commcare.android.database.connect.models.ConnectJobDeliveryRecord;
+import org.commcare.android.database.connect.models.ConnectJobPaymentRecord;
+import org.commcare.android.database.connect.models.ConnectJobRecord;
+import org.commcare.dalvik.R;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+
+import androidx.constraintlayout.widget.ConstraintLayout;
+import androidx.fragment.app.Fragment;
+import androidx.navigation.Navigation;
+
+public class ConnectResultsSummaryListFragment extends Fragment {
+ private TextView singleDeliveryLabel;
+ private ConstraintLayout multiPaymentContainer;
+ private TextView earnedColumn;
+ private TextView approvedColumn;
+ private TextView rejectedColumn;
+ private TextView pendingColumn;
+ private TextView nameColumn;
+ private Button deliveriesButton;
+ private TextView earnedAmount;
+ private TextView transferredAmount;
+ private Button paymentsButton;
+
+ public ConnectResultsSummaryListFragment() {
+ // Required empty public constructor
+ }
+
+ public static ConnectResultsSummaryListFragment newInstance() {
+ return new ConnectResultsSummaryListFragment();
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_connect_results_summary_list, container, false);
+
+ singleDeliveryLabel = view.findViewById(R.id.single_status_label);
+ multiPaymentContainer = view.findViewById(R.id.multi_status_container);
+ earnedColumn = view.findViewById(R.id.delivery_column_earned);
+ approvedColumn = view.findViewById(R.id.delivery_column_approved);
+ rejectedColumn = view.findViewById(R.id.delivery_column_rejected);
+ pendingColumn = view.findViewById(R.id.delivery_column_pending);
+ nameColumn = view.findViewById(R.id.delivery_column_type);
+ deliveriesButton = view.findViewById(R.id.deliveries_button);
+ earnedAmount = view.findViewById(R.id.payment_earned_amount);
+ transferredAmount = view.findViewById(R.id.payment_transferred_amount);
+ paymentsButton = view.findViewById(R.id.payments_button);
+
+ deliveriesButton.setOnClickListener(v -> {
+ Navigation.findNavController(deliveriesButton).navigate(ConnectDeliveryProgressFragmentDirections
+ .actionConnectJobDeliveryProgressFragmentToConnectResultsFragment(false));
+ });
+
+ paymentsButton.setOnClickListener(v -> {
+ Navigation.findNavController(paymentsButton).navigate(ConnectDeliveryProgressFragmentDirections
+ .actionConnectJobDeliveryProgressFragmentToConnectResultsFragment(true));
+ });
+
+ updateView();
+
+ return view;
+ }
+
+ public void updateView() {
+ ConnectJobRecord job = ConnectManager.getActiveJob();
+ if (job != null) {
+ //Verification Status
+ singleDeliveryLabel.setVisibility(job.isMultiPayment() ? View.GONE : View.VISIBLE);
+ multiPaymentContainer.setVisibility(job.isMultiPayment() ? View.VISIBLE : View.GONE);
+ if (job.isMultiPayment()) {
+ //Get counts for all cells (by type and status)
+ HashMap> paymentTypeAndStatusCounts = new HashMap<>();
+ for(int i=0; i());
+ }
+ HashMap typeCounts = paymentTypeAndStatusCounts.get(delivery.getSlug());;
+
+ String status = delivery.getStatus();
+ int count = typeCounts.containsKey(status) ? typeCounts.get(status) : 0;
+ typeCounts.put(status, count + 1);
+ }
+
+ //Now populate the UI text
+ List nameLines = new ArrayList<>();
+ List pendingLines = new ArrayList<>();
+ List approvedLines = new ArrayList<>();
+ List rejectedLines = new ArrayList<>();
+ List earnedLines = new ArrayList<>();
+
+ nameLines.add("");
+ pendingLines.add(getString(R.string.connect_results_summary_pending));
+ approvedLines.add(getString(R.string.connect_results_summary_approved));
+ rejectedLines.add(getString(R.string.connect_results_summary_rejected));
+ earnedLines.add(getString(R.string.connect_results_summary_earned));
+
+ for(int i=0; i statusCounts;
+ String stringKey = Integer.toString(unit.getUnitId());
+ if(paymentTypeAndStatusCounts.containsKey(stringKey)) {
+ statusCounts = paymentTypeAndStatusCounts.get(stringKey);
+ } else {
+ statusCounts = new HashMap<>();
+ }
+
+ //Blank line
+ nameLines.add("");
+ pendingLines.add("");
+ approvedLines.add("");
+ rejectedLines.add("");
+ earnedLines.add("");
+
+ //Name line (the rest blank)
+ nameLines.add(unit.getName());
+ pendingLines.add("");
+ approvedLines.add("");
+ rejectedLines.add("");
+ earnedLines.add("");
+
+ //Counts line (name blank)
+ nameLines.add("");
+
+ String statusKey = "pending";
+ pendingLines.add(String.format(Locale.getDefault(), "%d",
+ statusCounts.containsKey(statusKey) ? statusCounts.get(statusKey) : 0));
+
+ statusKey = "approved";
+ int numApproved = statusCounts.containsKey(statusKey) ? statusCounts.get(statusKey) : 0;
+ approvedLines.add(String.format(Locale.getDefault(), "%d", numApproved));
+
+ statusKey = "rejected";
+ rejectedLines.add(String.format(Locale.getDefault(), "%d",
+ statusCounts.containsKey(statusKey) ? statusCounts.get(statusKey) : 0));
+ earnedLines.add(job.getMoneyString(numApproved * unit.getAmount()));
+ }
+
+ nameColumn.setText(String.join("\n", nameLines));
+ pendingColumn.setText(String.join("\n", pendingLines));
+ approvedColumn.setText(String.join("\n", approvedLines));
+ rejectedColumn.setText(String.join("\n", rejectedLines));
+ earnedColumn.setText(String.join("\n", earnedLines));
+ } else {
+ int numPending = 0;
+ int numFailed = 0;
+ int numApproved = 0;
+ for (ConnectJobDeliveryRecord delivery : job.getDeliveries()) {
+ if (delivery.getStatus().equals("pending")) {
+ numPending++;
+ } else if (delivery.getStatus().equals("approved")) {
+ numApproved++;
+ } else {
+ numFailed++;
+ }
+ }
+ singleDeliveryLabel.setText(getString(R.string.connect_results_summary_verifications_description, numPending, numFailed, numApproved));
+ }
+
+ //Payment Status
+ int total = 0;
+ for (ConnectJobPaymentRecord payment : job.getPayments()) {
+ try {
+ total += Integer.parseInt(payment.getAmount());
+ } catch (Exception e) {
+ //Ignore at least for now
+ }
+ }
+
+ earnedAmount.setText(job.getMoneyString(job.getPaymentAccrued()));
+ transferredAmount.setText(job.getMoneyString(total));
+ }
+ }
+}
diff --git a/app/src/org/commcare/google/services/analytics/AnalyticsParamValue.java b/app/src/org/commcare/google/services/analytics/AnalyticsParamValue.java
index a7e996799..c924677cd 100644
--- a/app/src/org/commcare/google/services/analytics/AnalyticsParamValue.java
+++ b/app/src/org/commcare/google/services/analytics/AnalyticsParamValue.java
@@ -88,6 +88,7 @@ public class AnalyticsParamValue {
public static final String SYNC_BUTTON = "sync";
public static final String SYNC_SUBTEXT = "sync_subtext";
public static final String REPORT_BUTTON = "report_an_issue";
+ public static final String CONNECT_BUTTON = "connect_info";
// Param values for form types
public static final String INCOMPLETE = "incomplete";
diff --git a/app/src/org/commcare/google/services/analytics/CCAnalyticsEvent.java b/app/src/org/commcare/google/services/analytics/CCAnalyticsEvent.java
index 83493535f..2a746b925 100644
--- a/app/src/org/commcare/google/services/analytics/CCAnalyticsEvent.java
+++ b/app/src/org/commcare/google/services/analytics/CCAnalyticsEvent.java
@@ -34,5 +34,23 @@ public class CCAnalyticsEvent {
static final String CCC_RECOVERY = "ccc_recovery";
static final String PARAM_CCC_RECOVERY_METHOD = "ccc_recovery_method";
static final String PARAM_CCC_RECOVERY_SUCCESS = "ccc_recovery_success";
+ static final String CCC_TAB_CHANGE = "ccc_tab_change";
+ static final String PARAM_CCC_TAB_CHANGE_NAME = "ccc_tab_change_name";
+ static final String CCC_LAUNCH_APP = "ccc_launch_app";
+ static final String PARAM_CCC_LAUNCH_APP_TYPE = "ccc_launch_app_type";
+ static final String CCC_AUTO_LOGIN_FAILED = "ccc_auto_login_failed";
+ static final String PARAM_CCC_APP_NAME = "ccc_app_name";
+ static final String CCC_API_JOBS = "ccc_api_jobs";
+ static final String PARAM_API_SUCCESS = "ccc_api_success";
+ static final String PARAM_API_NEW_JOBS = "ccc_api_new_jobs";
+ static final String CCC_API_START_LEARNING = "ccc_api_start_learning";
+ static final String CCC_API_LEARN_PROGRESS = "ccc_api_learn_progress";
+ static final String CCC_API_CLAIM_JOB = "ccc_api_claim_job";
+ static final String CCC_API_DELIVERY_PROGRESS = "ccc_api_delivery_progress";
+ static final String CCC_API_PAYMENT_CONFIRMATION = "ccc_api_payment_confirmation";
+ static final String CCC_PAYMENT_CONFIRMATION_CHECK = "ccc_payment_confirmation_check";
+ static final String CCC_PAYMENT_CONFIRMATION_DISPLAY = "ccc_payment_confirmation_display";
+ static final String CCC_PAYMENT_CONFIRMATION_INTERACT = "ccc_payment_confirmation_interact";
+ static final String CCC_NOTIFICATION_TYPE = "ccc_notification_type";
}
diff --git a/app/src/org/commcare/google/services/analytics/CCAnalyticsParam.java b/app/src/org/commcare/google/services/analytics/CCAnalyticsParam.java
index 6dff197b7..2e1c4e43e 100644
--- a/app/src/org/commcare/google/services/analytics/CCAnalyticsParam.java
+++ b/app/src/org/commcare/google/services/analytics/CCAnalyticsParam.java
@@ -23,5 +23,6 @@ public class CCAnalyticsParam {
static final String USERNAME = "username";
static final String USER_RETURNED = "user_returned";
+ static final String NOTIFICATION_TYPE = "notification_type";
}
diff --git a/app/src/org/commcare/google/services/analytics/FirebaseAnalyticsUtil.java b/app/src/org/commcare/google/services/analytics/FirebaseAnalyticsUtil.java
index c1d2a255e..2e6642b1b 100644
--- a/app/src/org/commcare/google/services/analytics/FirebaseAnalyticsUtil.java
+++ b/app/src/org/commcare/google/services/analytics/FirebaseAnalyticsUtil.java
@@ -1,9 +1,22 @@
package org.commcare.google.services.analytics;
+import static org.commcare.google.services.analytics.AnalyticsParamValue.CORRUPT_APP_STATE;
+import static org.commcare.google.services.analytics.AnalyticsParamValue.STAGE_UPDATE_FAILURE;
+import static org.commcare.google.services.analytics.AnalyticsParamValue.UPDATE_RESET;
+import static org.commcare.google.services.analytics.AnalyticsParamValue.VIDEO_USAGE_FULL;
+import static org.commcare.google.services.analytics.AnalyticsParamValue.VIDEO_USAGE_IMMEDIATE;
+import static org.commcare.google.services.analytics.AnalyticsParamValue.VIDEO_USAGE_LENGTH_UNKNOWN;
+import static org.commcare.google.services.analytics.AnalyticsParamValue.VIDEO_USAGE_MOST;
+import static org.commcare.google.services.analytics.AnalyticsParamValue.VIDEO_USAGE_OTHER;
+import static org.commcare.google.services.analytics.AnalyticsParamValue.VIDEO_USAGE_PARTIAL;
+
import android.os.Bundle;
import android.os.Environment;
import android.text.TextUtils;
+import androidx.navigation.NavController;
+import androidx.navigation.fragment.FragmentNavigator;
+
import com.google.firebase.analytics.FirebaseAnalytics;
import org.commcare.CommCareApplication;
@@ -16,16 +29,6 @@
import java.util.Date;
-import static org.commcare.google.services.analytics.AnalyticsParamValue.CORRUPT_APP_STATE;
-import static org.commcare.google.services.analytics.AnalyticsParamValue.STAGE_UPDATE_FAILURE;
-import static org.commcare.google.services.analytics.AnalyticsParamValue.UPDATE_RESET;
-import static org.commcare.google.services.analytics.AnalyticsParamValue.VIDEO_USAGE_FULL;
-import static org.commcare.google.services.analytics.AnalyticsParamValue.VIDEO_USAGE_IMMEDIATE;
-import static org.commcare.google.services.analytics.AnalyticsParamValue.VIDEO_USAGE_LENGTH_UNKNOWN;
-import static org.commcare.google.services.analytics.AnalyticsParamValue.VIDEO_USAGE_MOST;
-import static org.commcare.google.services.analytics.AnalyticsParamValue.VIDEO_USAGE_OTHER;
-import static org.commcare.google.services.analytics.AnalyticsParamValue.VIDEO_USAGE_PARTIAL;
-
/**
* Created by amstone326 on 10/13/17.
*/
@@ -98,7 +101,7 @@ private static void setUserProperties(FirebaseAnalytics analyticsInstance) {
private static String getFreeDiskBucket() {
long freeDiskInMb = DiskUtils.calculateFreeDiskSpaceInBytes(
- Environment.getDataDirectory().getPath())/ 1000000;
+ Environment.getDataDirectory().getPath()) / 1000000;
if (freeDiskInMb > 1000) {
return "gt_1000";
} else if (freeDiskInMb > 500) {
@@ -362,6 +365,72 @@ public static void reportCccRecovery(boolean success, String method) {
reportEvent(CCAnalyticsEvent.CCC_RECOVERY, b);
}
+ public static void reportCccAppLaunch(String type, String appId) {
+ reportEvent(CCAnalyticsEvent.CCC_LAUNCH_APP,
+ new String[]{CCAnalyticsEvent.PARAM_CCC_LAUNCH_APP_TYPE, CCAnalyticsEvent.PARAM_CCC_APP_NAME},
+ new String[]{type, appId});
+ }
+
+ public static void reportCccAppFailedAutoLogin(String app) {
+ reportEvent(CCAnalyticsEvent.CCC_AUTO_LOGIN_FAILED,
+ new String[]{CCAnalyticsEvent.PARAM_CCC_APP_NAME},
+ new String[]{app});
+ }
+
+ public static void reportCccApiJobs(boolean success, int newJobs) {
+ Bundle b = new Bundle();
+ b.putLong(CCAnalyticsEvent.PARAM_API_SUCCESS, success ? 1 : 0);
+ b.putInt(CCAnalyticsEvent.PARAM_API_NEW_JOBS, newJobs);
+ reportEvent(CCAnalyticsEvent.CCC_API_JOBS, b);
+ }
+
+ public static void reportCccApiStartLearning(boolean success) {
+ Bundle b = new Bundle();
+ b.putLong(CCAnalyticsEvent.PARAM_API_SUCCESS, success ? 1 : 0);
+ reportEvent(CCAnalyticsEvent.CCC_API_START_LEARNING, b);
+ }
+
+ public static void reportCccApiLearnProgress(boolean success) {
+ Bundle b = new Bundle();
+ b.putLong(CCAnalyticsEvent.PARAM_API_SUCCESS, success ? 1 : 0);
+ reportEvent(CCAnalyticsEvent.CCC_API_LEARN_PROGRESS, b);
+ }
+
+ public static void reportCccApiClaimJob(boolean success) {
+ Bundle b = new Bundle();
+ b.putLong(CCAnalyticsEvent.PARAM_API_SUCCESS, success ? 1 : 0);
+ reportEvent(CCAnalyticsEvent.CCC_API_CLAIM_JOB, b);
+ }
+
+ public static void reportCccApiDeliveryProgress(boolean success) {
+ Bundle b = new Bundle();
+ b.putLong(CCAnalyticsEvent.PARAM_API_SUCCESS, success ? 1 : 0);
+ reportEvent(CCAnalyticsEvent.CCC_API_DELIVERY_PROGRESS, b);
+ }
+
+ public static void reportCccApiPaymentConfirmation(boolean success) {
+ Bundle b = new Bundle();
+ b.putLong(CCAnalyticsEvent.PARAM_API_SUCCESS, success ? 1 : 0);
+ reportEvent(CCAnalyticsEvent.CCC_API_PAYMENT_CONFIRMATION, b);
+ }
+
+ public static void reportCccPaymentConfirmationOnlineCheck(boolean success) {
+ Bundle b = new Bundle();
+ b.putLong(CCAnalyticsEvent.PARAM_API_SUCCESS, success ? 1 : 0);
+ reportEvent(CCAnalyticsEvent.CCC_PAYMENT_CONFIRMATION_CHECK, b);
+ }
+
+ public static void reportCccPaymentConfirmationDisplayed() {
+ Bundle b = new Bundle();
+ reportEvent(CCAnalyticsEvent.CCC_PAYMENT_CONFIRMATION_DISPLAY, b);
+ }
+
+ public static void reportCccPaymentConfirmationInteraction(boolean positive) {
+ Bundle b = new Bundle();
+ b.putLong(CCAnalyticsEvent.PARAM_API_SUCCESS, positive ? 1 : 0);
+ reportEvent(CCAnalyticsEvent.CCC_PAYMENT_CONFIRMATION_INTERACT, b);
+ }
+
public static void reportCccSignOut() {
reportEvent(CCAnalyticsEvent.CCC_SIGN_OUT);
}
@@ -369,4 +438,26 @@ public static void reportCccSignOut() {
public static void reportLoginClicks() {
reportEvent(CCAnalyticsEvent.LOGIN_CLICK);
}
+
+ public static NavController.OnDestinationChangedListener getDestinationChangeListener() {
+ return (navController, navDestination, args) -> {
+ Bundle bundle = new Bundle();
+ var currentFragmentClassName = ((FragmentNavigator.Destination) navController.getCurrentDestination())
+ .getClassName();
+ bundle.putString(FirebaseAnalytics.Param.SCREEN_NAME, navDestination.getLabel().toString());
+ bundle.putString(FirebaseAnalytics.Param.SCREEN_CLASS, currentFragmentClassName);
+ reportEvent(FirebaseAnalytics.Event.SCREEN_VIEW, bundle);
+ };
+ }
+
+ public static void reportConnectTabChange(String tabName) {
+ reportEvent(CCAnalyticsEvent.CCC_TAB_CHANGE,
+ new String[]{CCAnalyticsEvent.PARAM_CCC_TAB_CHANGE_NAME},
+ new String[]{tabName});
+ }
+
+ public static void reportNotificationType(String notificationType) {
+ reportEvent(CCAnalyticsEvent.CCC_NOTIFICATION_TYPE,
+ CCAnalyticsParam.NOTIFICATION_TYPE, notificationType);
+ }
}
diff --git a/app/src/org/commcare/heartbeat/HeartbeatRequester.java b/app/src/org/commcare/heartbeat/HeartbeatRequester.java
index 4b87e9993..46f0e322a 100644
--- a/app/src/org/commcare/heartbeat/HeartbeatRequester.java
+++ b/app/src/org/commcare/heartbeat/HeartbeatRequester.java
@@ -24,10 +24,6 @@
import org.json.JSONException;
import org.json.JSONObject;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.TimeZone;
-
import androidx.work.WorkManager;
import com.google.common.collect.ArrayListMultimap;
diff --git a/app/src/org/commcare/interfaces/CommcareRequestEndpoints.java b/app/src/org/commcare/interfaces/CommcareRequestEndpoints.java
index 977993ccb..15b0bc6e7 100644
--- a/app/src/org/commcare/interfaces/CommcareRequestEndpoints.java
+++ b/app/src/org/commcare/interfaces/CommcareRequestEndpoints.java
@@ -1,11 +1,9 @@
-
package org.commcare.interfaces;
import com.google.common.collect.Multimap;
import java.io.IOException;
import java.util.Date;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
diff --git a/app/src/org/commcare/models/database/user/models/CommCareEntityStorageCache.java b/app/src/org/commcare/models/database/user/models/CommCareEntityStorageCache.java
old mode 100755
new mode 100644
diff --git a/app/src/org/commcare/network/CommcareRequestGenerator.java b/app/src/org/commcare/network/CommcareRequestGenerator.java
index 24717f5b6..39352019a 100755
--- a/app/src/org/commcare/network/CommcareRequestGenerator.java
+++ b/app/src/org/commcare/network/CommcareRequestGenerator.java
@@ -7,8 +7,8 @@
import com.google.common.collect.Multimap;
import org.commcare.CommCareApplication;
-import org.commcare.android.database.user.models.ACase;
import org.commcare.connect.network.ConnectSsoHelper;
+import org.commcare.android.database.user.models.ACase;
import org.commcare.core.network.AuthInfo;
import org.commcare.core.network.HTTPMethod;
import org.commcare.core.network.ModernHttpRequester;
diff --git a/app/src/org/commcare/services/CommCareFirebaseMessagingService.java b/app/src/org/commcare/services/CommCareFirebaseMessagingService.java
index f18919395..7912c1b20 100644
--- a/app/src/org/commcare/services/CommCareFirebaseMessagingService.java
+++ b/app/src/org/commcare/services/CommCareFirebaseMessagingService.java
@@ -12,7 +12,9 @@
import org.commcare.CommCareNoficationManager;
import org.commcare.activities.DispatchActivity;
+import org.commcare.activities.connect.ConnectActivity;
import org.commcare.dalvik.R;
+import org.commcare.google.services.analytics.FirebaseAnalyticsUtil;
import org.commcare.sync.FirebaseMessagingDataSyncer;
import org.commcare.util.LogTypes;
import org.commcare.utils.FirebaseMessagingUtil;
@@ -28,85 +30,100 @@
public class CommCareFirebaseMessagingService extends FirebaseMessagingService {
private final static int FCM_NOTIFICATION = R.string.fcm_notification;
- enum ActionTypes{
+
+ enum ActionTypes {
SYNC,
INVALID
}
+
private FirebaseMessagingDataSyncer dataSyncer;
+
{
dataSyncer = new FirebaseMessagingDataSyncer(this);
}
- /**
- * Upon receiving a new message from FCM, CommCare needs to:
- * 1) Trigger the notification if the message contains a Notification object. Note that the
- * presence of a Notification object causes the onMessageReceived to not be called when the
- * app is in the background, which means that the data object won't be processed from here
- * 2) Verify if the message contains a data object and trigger the necessary steps according
- * to the action it carries
- *
- */
+ /**
+ * Upon receiving a new message from FCM, CommCare needs to:
+ * 1) Trigger the notification if the message contains a Notification object. Note that the
+ * presence of a Notification object causes the onMessageReceived to not be called when the
+ * app is in the background, which means that the data object won't be processed from here
+ * 2) Verify if the message contains a data object and trigger the necessary steps according
+ * to the action it carries
+ */
@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
- Logger.log(LogTypes.TYPE_FCM, "Message received: " + remoteMessage.getMessageId());
+ Logger.log(LogTypes.TYPE_FCM, "CommCareFirebaseMessagingService Message received: " + remoteMessage.getData());
Map payloadData = remoteMessage.getData();
- RemoteMessage.Notification payloadNotification = remoteMessage.getNotification();
-
- if (payloadNotification != null) {
- showNotification(payloadNotification);
- }
// Check if the message contains a data object, there is no further action if not
- if (payloadData.size() == 0){
+ if (payloadData.isEmpty()) {
return;
}
- FCMMessageData fcmMessageData = new FCMMessageData(payloadData);
+ showNotification(payloadData);
+ FCMMessageData fcmMessageData;
+ if (!hasCccAction(payloadData)) {
+ fcmMessageData = new FCMMessageData(payloadData);
- switch(fcmMessageData.getAction()){
- case SYNC -> dataSyncer.syncData(fcmMessageData);
- default ->
- Logger.log(LogTypes.TYPE_FCM, "Invalid FCM action");
+ switch (fcmMessageData.getAction()) {
+ case SYNC -> dataSyncer.syncData(fcmMessageData);
+ default -> Logger.log(LogTypes.TYPE_FCM, "Invalid FCM action");
+ }
}
}
@Override
public void onNewToken(String token) {
// TODO: Remove the token from the log
- Logger.log(LogTypes.TYPE_FCM, "New registration token was generated: "+token);
+ Logger.log(LogTypes.TYPE_FCM, "New registration token was generated: " + token);
FirebaseMessagingUtil.updateFCMToken(token);
}
- /**
- * This method purpose is to show notifications to the user when the app is in the foreground.
- * When the app is in the background, FCM is responsible for notifying the user
- *
- */
- private void showNotification(RemoteMessage.Notification notification) {
- String notificationTitle = notification.getTitle();
- String notificationText = notification.getBody();
+ /**
+ * This method purpose is to show notifications to the user when the app is in the foreground.
+ * When the app is in the background, FCM is responsible for notifying the user
+ */
+ private void showNotification(Map payloadData) {
+ String notificationTitle = payloadData.get("title");
+ String notificationText = payloadData.get("body");
NotificationManager mNM = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
- Intent i = new Intent(this, DispatchActivity.class);
- i.setAction(Intent.ACTION_MAIN);
- i.addCategory(Intent.CATEGORY_LAUNCHER);
+ Intent intent;
+
+ if (hasCccAction(payloadData)) {
+ FirebaseAnalyticsUtil.reportNotificationType(payloadData.get("action"));
+ intent = new Intent(getApplicationContext(), ConnectActivity.class);
+ intent.putExtra("action", payloadData.get("action"));
+ intent.putExtra("opportunity_id", payloadData.get("opportunity_id"));
+ } else {
+ intent = new Intent(this, DispatchActivity.class);
+ intent.setAction(Intent.ACTION_MAIN);
+ intent.addCategory(Intent.CATEGORY_LAUNCHER);
+ }
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ int flags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
+ ? PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT
+ : PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT;
- PendingIntent contentIntent;
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
- contentIntent = PendingIntent.getActivity(this, 0, i, PendingIntent.FLAG_IMMUTABLE);
- else
- contentIntent = PendingIntent.getActivity(this, 0, i, 0);
+ PendingIntent contentIntent = PendingIntent.getActivity(this, 0, intent, flags);
NotificationCompat.Builder fcmNotification = new NotificationCompat.Builder(this,
CommCareNoficationManager.NOTIFICATION_CHANNEL_PUSH_NOTIFICATIONS_ID)
.setContentTitle(notificationTitle)
.setContentText(notificationText)
.setContentIntent(contentIntent)
+ .setAutoCancel(true)
.setSmallIcon(R.drawable.notification)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setWhen(System.currentTimeMillis());
mNM.notify(FCM_NOTIFICATION, fcmNotification.build());
}
+
+ private boolean hasCccAction(Map payloadData) {
+ String action = payloadData.get("action");
+ return action != null && action.contains("ccc_");
+ }
}
diff --git a/app/src/org/commcare/utils/CrashUtil.java b/app/src/org/commcare/utils/CrashUtil.java
index e722190d6..3b57a3b96 100644
--- a/app/src/org/commcare/utils/CrashUtil.java
+++ b/app/src/org/commcare/utils/CrashUtil.java
@@ -1,8 +1,8 @@
package org.commcare.utils;
import org.commcare.CommCareApplication;
-import org.commcare.android.logging.ReportingUtils;
import org.commcare.connect.ConnectManager;
+import org.commcare.android.logging.ReportingUtils;
import org.commcare.dalvik.BuildConfig;
import com.google.firebase.crashlytics.FirebaseCrashlytics;
diff --git a/app/src/org/commcare/utils/MediaUtil.java b/app/src/org/commcare/utils/MediaUtil.java
index ce3763c94..06a81f139 100644
--- a/app/src/org/commcare/utils/MediaUtil.java
+++ b/app/src/org/commcare/utils/MediaUtil.java
@@ -512,5 +512,4 @@ public static boolean isRecordingActive(Context context){
return ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE))
.getActiveRecordingConfigurations().size() > 0;
}
-
}
diff --git a/app/unit-tests/src/org/commcare/CommCareTestApplication.java b/app/unit-tests/src/org/commcare/CommCareTestApplication.java
index c02bd888f..f8351fa7f 100644
--- a/app/unit-tests/src/org/commcare/CommCareTestApplication.java
+++ b/app/unit-tests/src/org/commcare/CommCareTestApplication.java
@@ -6,6 +6,9 @@
import android.os.StrictMode;
import android.util.Log;
+import androidx.work.Configuration;
+
+import org.commcare.connect.ConnectDatabaseHelper;
import org.commcare.android.database.app.models.UserKeyRecord;
import org.commcare.android.mocks.ModernHttpRequesterMock;
import org.commcare.android.util.TestUtils;
@@ -17,7 +20,6 @@
import org.commcare.dalvik.BuildConfig;
import org.commcare.heartbeat.HeartbeatRequester;
import org.commcare.heartbeat.TestHeartbeatRequester;
-import org.commcare.logging.DataChangeLogger;
import org.commcare.models.AndroidPrototypeFactory;
import org.commcare.models.database.AndroidPrototypeFactorySetup;
import org.commcare.models.database.HybridFileBackedSqlStorage;
@@ -44,10 +46,11 @@
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
-import java.util.Map;
import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
+import androidx.work.testing.SynchronousExecutor;
+import androidx.work.testing.WorkManagerTestInitHelper;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
@@ -84,6 +87,15 @@ public void onCreate() {
asyncExceptions.add(ex);
Assert.fail(ex.getMessage());
});
+
+ Configuration config = new Configuration.Builder()
+ .setMinimumLoggingLevel(Log.DEBUG)
+ .setExecutor(new SynchronousExecutor())
+ .build();
+
+ // Initialize WorkManager for instrumentation tests.
+ WorkManagerTestInitHelper.initializeTestWorkManager(
+ this, config);
}
protected void attachISRGCert() {
@@ -257,6 +269,13 @@ public HeartbeatRequester getHeartbeatRequester() {
@Override
public void afterTest(Method method) {
+ CommCareApp app = getCurrentApp();
+ if(app != null) {
+ app.teardownSandbox();
+ }
+
+ ConnectDatabaseHelper.teardown();
+
if (!asyncExceptions.isEmpty()) {
for (Throwable throwable : asyncExceptions) {
throwable.printStackTrace();
diff --git a/app/unit-tests/src/org/commcare/android/tests/processing/FormStorageTest.java b/app/unit-tests/src/org/commcare/android/tests/processing/FormStorageTest.java
index 9c1b4890e..5d0464476 100644
--- a/app/unit-tests/src/org/commcare/android/tests/processing/FormStorageTest.java
+++ b/app/unit-tests/src/org/commcare/android/tests/processing/FormStorageTest.java
@@ -354,15 +354,27 @@ public class FormStorageTest {
, "org.commcare.suite.model.DetailGroup"
, "org.commcare.services.FCMMessageData"
, "org.commcare.suite.model.EndpointArgument"
- , "org.commcare.android.database.connect.models.ConnectLinkedAppRecord"
- , "org.commcare.android.database.connect.models.ConnectUserRecord"
- , "org.commcare.android.database.global.models.ConnectKeyRecord"
- , "org.commcare.suite.model.DetailGroup"
- , "org.commcare.suite.model.EndpointArgument"
, "org.commcare.suite.model.EndpointAction"
, "org.commcare.suite.model.QueryGroup"
+ , "org.commcare.android.database.connect.models.ConnectLinkedAppRecordV3"
+ , "org.commcare.android.database.connect.models.ConnectLinkedAppRecord"
+ , "org.commcare.android.database.connect.models.ConnectUserRecordV5"
+ , "org.commcare.android.database.connect.models.ConnectUserRecord"
+ , "org.commcare.android.database.connect.models.ConnectAppRecord"
+ , "org.commcare.android.database.connect.models.ConnectJobDeliveryRecordV2"
+ , "org.commcare.android.database.connect.models.ConnectJobDeliveryRecord"
+ , "org.commcare.android.database.connect.models.ConnectJobPaymentRecordV3"
+ , "org.commcare.android.database.connect.models.ConnectJobPaymentRecord"
+ , "org.commcare.android.database.connect.models.ConnectJobRecordV2"
+ , "org.commcare.android.database.connect.models.ConnectJobRecordV4"
+ , "org.commcare.android.database.connect.models.ConnectJobRecordV7"
+ , "org.commcare.android.database.connect.models.ConnectJobRecord"
+ , "org.commcare.android.database.connect.models.ConnectLearnModuleSummaryRecord"
+ , "org.commcare.android.database.connect.models.ConnectJobLearningRecord"
+ , "org.commcare.android.database.connect.models.ConnectJobAssessmentRecord"
, "org.commcare.android.database.global.models.ConnectKeyRecord"
, "org.commcare.android.database.global.models.ConnectKeyRecordV6"
+ , "org.commcare.android.database.connect.models.ConnectPaymentUnitRecord"
);
diff --git a/build.gradle b/build.gradle
index a52162cf1..021111e5b 100644
--- a/build.gradle
+++ b/build.gradle
@@ -13,6 +13,9 @@ buildscript {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'org.jacoco:org.jacoco.core:0.8.10'
classpath 'com.vanniktech:gradle-maven-publish-plugin:0.15.1'
+
+ def nav_version = "2.6.0"
+ classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
}
}
diff --git a/commcare-support-library/src/main/java/org/commcare/commcaresupportlibrary/CommCareLauncher.java b/commcare-support-library/src/main/java/org/commcare/commcaresupportlibrary/CommCareLauncher.java
index 4dbbb8575..be008902b 100644
--- a/commcare-support-library/src/main/java/org/commcare/commcaresupportlibrary/CommCareLauncher.java
+++ b/commcare-support-library/src/main/java/org/commcare/commcaresupportlibrary/CommCareLauncher.java
@@ -10,11 +10,6 @@ public class CommCareLauncher {
public static final String SESSION_ENDPOINT_APP_ID = "ccodk_session_endpoint_app_id";
private static final String CC_LAUNCH_ACTION = "org.commcare.dalvik.action.CommCareSession";
- /**
- *
- * @param context Android context to launch the CommCare with
- * @param appId Unique Id for CommCare App that CommCare should launch with
- */
public static void launchCommCareForAppId(Context context, String appId) {
Intent intent = new Intent(CC_LAUNCH_ACTION);
intent.putExtra(SESSION_ENDPOINT_APP_ID, appId);
diff --git a/gradle.properties b/gradle.properties
index 9cb51f054..5c4c18bd1 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -5,7 +5,7 @@ org.gradle.daemon=true
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx10248m -XX:MaxPermSize=256m
-org.gradle.jvmargs=-Xmx3072m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+org.gradle.jvmargs=-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit