Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ConnectID Foundation #2847

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1e72821
Created branch with foundational code for ConnectID and later Connect.
OrangeAndGreen Sep 12, 2024
56bb1fb
Added wrapper class for ConnectID API calls
OrangeAndGreen Sep 12, 2024
1351a0a
Added unit tests for encryption, and mock encryption provider to supp…
OrangeAndGreen Sep 12, 2024
fcfbb71
Added externalizables to test.
OrangeAndGreen Sep 16, 2024
b58c6eb
Merge branch 'master' of https://github.com/dimagi/commcare-android i…
OrangeAndGreen Dec 4, 2024
93314ed
Addressing PR feedback.
OrangeAndGreen Dec 4, 2024
906137d
Simplified linkHqWorker to take ConnectLinkedAppRecord from caller in…
OrangeAndGreen Dec 4, 2024
a8bcc43
Better error handling when linkHqWorker fails
OrangeAndGreen Dec 4, 2024
445ec10
Added ServerUrls.buildEndpoint helper method to build new endpoints u…
OrangeAndGreen Dec 4, 2024
b4dad76
Added SsoToken class with common code for retrieving token info from …
OrangeAndGreen Dec 4, 2024
9312dfd
Removed date-related functions from network helper class, using exist…
OrangeAndGreen Dec 4, 2024
3d9b7ea
Extracted common code for building POST data from parameters, to be u…
OrangeAndGreen Dec 4, 2024
b2aa121
Moved Connect-related classes to v2.55 section
OrangeAndGreen Dec 4, 2024
ea2db93
Added static helper class to lazy load KeyStore singleton.
OrangeAndGreen Dec 4, 2024
d412539
Lint
OrangeAndGreen Dec 5, 2024
b842ebd
Merge branch 'master' into dv/connectid_foundation
OrangeAndGreen Dec 5, 2024
fecd2bc
Merge branch 'master' of https://github.com/dimagi/commcare-android i…
OrangeAndGreen Dec 17, 2024
aa616b4
-pr review bug fixes
pm-dimagi Dec 19, 2024
918d1af
Merge branch 'master' into dv/connectid_foundation
OrangeAndGreen Dec 31, 2024
66cf541
- retrofit changes for api call
pm-dimagi Jan 7, 2025
4f42de2
-bug fix for language change
pm-dimagi Jan 7, 2025
a91d8ab
-foundation pr review changes
pm-dimagi Jan 21, 2025
f51fc58
- making clint id as constant value
pm-dimagi Jan 21, 2025
c7fd1ec
-making dynamic url builder
pm-dimagi Jan 23, 2025
b1e4eff
-coderabbit review changes
pm-dimagi Jan 23, 2025
d793b9a
-coderabbit review changes
pm-dimagi Jan 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ dependencies {
implementation 'joda-time:joda-time:2.9.4'
implementation 'net.zetetic:android-database-sqlcipher:4.5.3@aar'
implementation 'androidx.sqlite:sqlite:2.2.0'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.11.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation('org.apache.james:apache-mime4j:0.7.2') {
exclude module: 'commons-io'
}
Expand Down Expand Up @@ -168,6 +171,8 @@ ext {
HQ_API_PASSWORD = project.properties['HQ_API_PASSWORD'] ?: ''
TEST_BUILD_TYPE = project.properties['TEST_BUILD_TYPE'] ?: 'debug'
FIREBASE_DATABASE_URL = project.properties['FIREBASE_DATABASE_URL'] ?: ''
API_VERSION_CONNECT_ID = project.properties['API_VERSION_CONNECT_ID'] ?: ''
CONNECT_BASE_URL = project.properties['FIREBASE_DATABASE_URL'] ?: ''
}

afterEvaluate {
Expand Down Expand Up @@ -286,6 +291,8 @@ android {

buildConfigField "String", "FIREBASE_DATABASE_URL", "\"${project.ext.FIREBASE_DATABASE_URL}\""

buildConfigField 'String', 'CONNECT_BASE_URL', "\"https://connectid.dimagi.com\""
buildConfigField 'String', 'API_VERSION_CONNECT_ID', "\"1.0\""
buildConfigField 'String', 'CCC_HOST', "\"connect.dimagi.com\""

testInstrumentationRunner 'org.commcare.CommCareJUnitRunner'
Expand Down
7 changes: 6 additions & 1 deletion app/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
<string name="key_server">https://pact.dimagi.com/keys/getkey</string>
<string name="your_comment">Your comment</string>
<string name="support_email_address_default">[email protected]</string>

<string name="ConnectFetchDbKeyURL">https://connectid.dimagi.com/users/fetch_db_key</string>
<string name="ConnectTokenURL">https://connectid.dimagi.com/o/token/</string>
<string name="ConnectHeartbeatURL">https://connectid.dimagi.com/users/heartbeat</string>
<!-- region: All strings for multiple apps and app-agnostic properties -->

<string name="manager_activity_name">App Manager</string>
Expand Down Expand Up @@ -384,6 +386,7 @@
<string name="recovery_forms_send_error" cc:translatable="true">Error while sending forms</string>
<string name="recovery_forms_state_unavailable" cc:translatable="true">Forms are not available. Make sure your phone storage is available</string>
<string name="recovery_network_unavailable" cc:translatable="true">No network connection. Please check your internet and try again.</string>
<string name="recovery_network_outdated" cc:translatable="true">The app is outdated and can no longer communicate with the server. Please update the app on the Google Play Store.</string>
<string name="recovery_app_manager" cc:translatable="true">Go to App Manager</string>
<string name="recovery_retry" cc:translatable="true">Retry Recovery</string>

Expand Down Expand Up @@ -456,4 +459,6 @@
<string name="fcm_default_notification_channel">notification-channel-push-notifications</string>
<string name="app_with_id_not_found">Required CommCare App is not installed on device</string>
<string name="audio_recording_notification">Audio Recording Notification</string>

<string name="connect_db_corrupt">A problem occurred with the database, please recover your account.</string>
</resources>
37 changes: 24 additions & 13 deletions app/src/org/commcare/CommCareApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
import org.commcare.utils.CommCareUtil;
import org.commcare.utils.CrashUtil;
import org.commcare.utils.DeviceIdentifier;
import org.commcare.utils.EncryptionKeyProvider;
import org.commcare.utils.FileUtil;
import org.commcare.utils.FirebaseMessagingUtil;
import org.commcare.utils.GlobalConstants;
Expand Down Expand Up @@ -202,6 +203,7 @@ public class CommCareApplication extends Application implements LifecycleEventOb

private boolean invalidateCacheOnRestore;
private CommCareNoficationManager noficationManager;
private EncryptionKeyProvider encryptionKeyProvider;

private boolean backgroundSyncSafe;

Expand Down Expand Up @@ -257,6 +259,9 @@ public void onCreate() {

FirebaseMessagingUtil.verifyToken();

//Create standard provider
setEncryptionKeyProvider(new EncryptionKeyProvider());

customiseOkHttp();

setRxJavaGlobalHandler();
Expand All @@ -279,7 +284,7 @@ protected void turnOnStrictMode() {
}

@Override
public void onConfigurationChanged(Configuration newConfig) {
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
LocalePreferences.saveDeviceLocale(newConfig.locale);
shubham1g5 marked this conversation as resolved.
Show resolved Hide resolved
}
Expand All @@ -300,11 +305,11 @@ private void logFirstCommCareRun() {
}
}

public void setBackgroundSyncSafe(boolean backgroundSyncSafe){
public void setBackgroundSyncSafe(boolean backgroundSyncSafe) {
this.backgroundSyncSafe = backgroundSyncSafe;
}

public boolean isBackgroundSyncSafe(){
public boolean isBackgroundSyncSafe() {
return this.backgroundSyncSafe;
}

Expand Down Expand Up @@ -345,11 +350,11 @@ private void configureCommCareEngineConstantsAndStaticRegistrations() {
// md5 hasher. Major speed improvements.
AndroidClassHasher.registerAndroidClassHashStrategy();

ActivityManager am = (ActivityManager)getSystemService(ACTIVITY_SERVICE);
ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
int memoryClass = am.getMemoryClass();

PerformanceTuningUtil.updateMaxPrefetchCaseBlock(
PerformanceTuningUtil.guessLargestSupportedBulkCaseFetchSizeFromHeap(memoryClass * 1024 * 1024));
PerformanceTuningUtil.guessLargestSupportedBulkCaseFetchSizeFromHeap((long) memoryClass * 1024 * 1024));
}

public void startUserSession(byte[] symmetricKey, UserKeyRecord record, boolean restoreSession) {
Expand Down Expand Up @@ -425,12 +430,13 @@ synchronized public FirebaseAnalytics getAnalyticsInstance() {
analyticsInstance = FirebaseAnalytics.getInstance(this);
}
analyticsInstance.setUserId(getUserIdOrNull());

return analyticsInstance;
}

public int[] getCommCareVersion() {
String[] components = BuildConfig.VERSION_NAME.split("\\.");
int[] versions = new int[] {0, 0, 0};
int[] versions = new int[]{0, 0, 0};
for (int i = 0; i < components.length; i++) {
versions[i] = Integer.parseInt(components[i]);
}
Expand Down Expand Up @@ -475,7 +481,7 @@ public void initializeGlobalResources(CommCareApp app) {

@NonNull
public String getPhoneId() {
/**
/*
* https://source.android.com/devices/tech/config/device-identifiers
* https://issuetracker.google.com/issues/129583175#comment10
* Starting from Android 10, apps cannot access non-resettable device ids unless they have special career permission.
Expand Down Expand Up @@ -519,7 +525,7 @@ private void setRoots() {
private void initializeAnAppOnStartup() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
String lastAppId = prefs.getString(LoginActivity.KEY_LAST_APP, "");
if (!"".equals(lastAppId)) {
if (!lastAppId.isEmpty()) {
ApplicationRecord lastApp = MultipleAppsUtil.getAppById(lastAppId);
if (lastApp == null || !lastApp.isUsable()) {
AppUtils.initFirstUsableAppRecord();
Expand Down Expand Up @@ -550,7 +556,6 @@ public void initializeAppResources(CommCareApp app) {
} catch (Exception e) {
Log.i("FAILURE", "Problem with loading");
Log.i("FAILURE", "E: " + e.getMessage());
e.printStackTrace();
ForceCloseLogger.reportExceptionInBg(e);
CrashUtil.reportException(e);
resourceState = STATE_CORRUPTED;
Expand Down Expand Up @@ -736,7 +741,7 @@ public void onServiceConnected(ComponentName className, IBinder service) {
synchronized (serviceLock) {
mCurrentServiceBindTimeout = MAX_BIND_TIMEOUT;

mBoundService = ((CommCareSessionService.LocalBinder)service).getService();
mBoundService = ((CommCareSessionService.LocalBinder) service).getService();
mBoundService.showLoggedInNotification(null);

// Don't let anyone touch this until it's logged in
Expand Down Expand Up @@ -922,7 +927,6 @@ public static boolean areAutomatedActionsInvalid() {

/**
* Whether the current login is a "demo" mode login.
*
* Returns a provided default value if there is no active user login
*/
public static boolean isInDemoMode(boolean defaultValue) {
Expand Down Expand Up @@ -977,8 +981,7 @@ public CommCareSessionService getSession() {
public static boolean isSessionActive() {
try {
return CommCareApplication.instance().getSession() != null;
}
catch (SessionUnavailableException e){
} catch (SessionUnavailableException e) {
return false;
}
}
Expand Down Expand Up @@ -1158,6 +1161,14 @@ public void setInvalidateCacheFlag(boolean b) {
invalidateCacheOnRestore = b;
}

public void setEncryptionKeyProvider(EncryptionKeyProvider provider) {
encryptionKeyProvider = provider;
}

public EncryptionKeyProvider getEncryptionKeyProvider() {
return encryptionKeyProvider;
}

public PrototypeFactory getPrototypeFactory(Context c) {
return AndroidPrototypeFactorySetup.getPrototypeFactory(c);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package org.commcare.android.database.connect.models;

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.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

@Table(ConnectAppRecord.STORAGE_KEY)
public class ConnectAppRecord extends Persisted implements Serializable {
/**
* Name of table that stores app info for Connect jobs
*/
public static final String STORAGE_KEY = "connect_apps";

public static final String META_JOB_ID = "job_id";
public static final String META_DOMAIN = "cc_domain";
public static final String META_APP_ID = "cc_app_id";
public static final String META_NAME = "name";
public static final String META_DESCRIPTION = "description";
public static final String META_ORGANIZATION = "organization";
public static final String META_PASSING_SCORE = "passing_score";
public static final String META_INSTALL_URL = "install_url";
public static final String META_MODULES = "learn_modules";

@Persisting(1)
@MetaField(META_JOB_ID)
private int jobId;
@Persisting(2)
private boolean isLearning;
@Persisting(3)
@MetaField(META_DOMAIN)
private String domain;
@Persisting(4)
@MetaField(META_APP_ID)
private String appId;
@Persisting(5)
@MetaField(META_NAME)
private String name;
@Persisting(6)
@MetaField(META_DESCRIPTION)
private String description;
@Persisting(7)
@MetaField(META_ORGANIZATION)
private String organization;

@Persisting(8)
@MetaField(META_PASSING_SCORE)
private int passingScore;
@Persisting(9)
@MetaField(META_INSTALL_URL)
private String installUrl;
@Persisting(10)
private Date lastUpdate;

private List<ConnectLearnModuleSummaryRecord> learnModules;

public ConnectAppRecord() {

}

public static ConnectAppRecord fromJson(JSONObject json, int jobId, boolean isLearning) throws JSONException {
ConnectAppRecord app = new ConnectAppRecord();

app.jobId = jobId;
app.isLearning = isLearning;

app.domain = json.has(META_DOMAIN) ? json.getString(META_DOMAIN) : "";
app.appId = json.has(META_APP_ID) ? json.getString(META_APP_ID) : "";
app.name = json.has(META_NAME) ? json.getString(META_NAME) : "";
app.description = json.has(META_DESCRIPTION) ? json.getString(META_DESCRIPTION) : "";
app.organization = json.has(META_ORGANIZATION) ? json.getString(META_ORGANIZATION) : "";
app.passingScore = json.has(META_PASSING_SCORE) && !json.isNull(META_PASSING_SCORE) ? json.getInt(META_PASSING_SCORE) : -1;
app.installUrl = json.has(META_INSTALL_URL) ? json.getString(META_INSTALL_URL) : "";

JSONArray array = json.getJSONArray(META_MODULES);
app.learnModules = new ArrayList<>();
for(int i=0; i<array.length(); i++) {
JSONObject obj = (JSONObject)array.get(i);
app.learnModules.add(ConnectLearnModuleSummaryRecord.fromJson(obj, i));
}

Comment on lines +83 to +89
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Check for the existence of META_MODULES before accessing to prevent exceptions.

In the fromJson method, getJSONArray(META_MODULES) is called without verifying if META_MODULES exists in the JSON object. If META_MODULES is missing, this will throw a JSONException. Add a check to ensure it exists before attempting to retrieve it.

Apply this diff to fix the issue:

+if (json.has(META_MODULES) && !json.isNull(META_MODULES)) {
     JSONArray array = json.getJSONArray(META_MODULES);
     app.learnModules = new ArrayList<>();
     for(int i=0; i<array.length(); i++) {
         JSONObject obj = (JSONObject)array.get(i);
         app.learnModules.add(ConnectLearnModuleSummaryRecord.fromJson(obj, i));
     }
+} else {
+    app.learnModules = new ArrayList<>();
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
JSONArray array = json.getJSONArray(META_MODULES);
app.learnModules = new ArrayList<>();
for(int i=0; i<array.length(); i++) {
JSONObject obj = (JSONObject)array.get(i);
app.learnModules.add(ConnectLearnModuleSummaryRecord.fromJson(obj, i));
}
if (json.has(META_MODULES) && !json.isNull(META_MODULES)) {
JSONArray array = json.getJSONArray(META_MODULES);
app.learnModules = new ArrayList<>();
for(int i=0; i<array.length(); i++) {
JSONObject obj = (JSONObject)array.get(i);
app.learnModules.add(ConnectLearnModuleSummaryRecord.fromJson(obj, i));
}
} else {
app.learnModules = new ArrayList<>();
}

return app;
}

public boolean getIsLearning() { return isLearning; }
public int getJobId() { return jobId; }
public void setJobId(int jobId) { this.jobId = jobId; }

public String getAppId() { return appId; }
public String getDomain() { return domain; }
public int getPassingScore() { return passingScore; }

public List<ConnectLearnModuleSummaryRecord> getLearnModules() { return learnModules; }
public String getInstallUrl() { return installUrl; }
public void setLearnModules(List<ConnectLearnModuleSummaryRecord> modules) { learnModules = modules; }
public void setLastUpdate(Date lastUpdate) { this.lastUpdate = lastUpdate; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package org.commcare.android.database.connect.models;

import org.commcare.android.storage.framework.Persisted;
import org.commcare.connect.network.ConnectNetworkHelper;
import org.commcare.models.framework.Persisting;
import org.commcare.modern.database.Table;
import org.commcare.modern.models.MetaField;
import org.javarosa.core.model.utils.DateUtils;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.Serializable;
import java.text.ParseException;
import java.util.Date;

/**
* Data class for holding info related to a Connect job assessment
*
* @author dviggiano
*/
@Table(ConnectJobAssessmentRecord.STORAGE_KEY)
public class ConnectJobAssessmentRecord extends Persisted implements Serializable {
/**
* Name of database that stores Connect job assessments
*/
public static final String STORAGE_KEY = "connect_assessments";

public static final String META_JOB_ID = "id";
public static final String META_DATE = "date";
public static final String META_SCORE = "score";
public static final String META_PASSING_SCORE = "passing_score";
public static final String META_PASSED = "passed";

@Persisting(1)
@MetaField(META_JOB_ID)
private int jobId;
@Persisting(2)
@MetaField(META_DATE)
private Date date;
@Persisting(3)
@MetaField(META_SCORE)
private int score;
@Persisting(4)
@MetaField(META_PASSING_SCORE)
private int passingScore;
@Persisting(5)
@MetaField(META_PASSED)
private boolean passed;
@Persisting(6)
private Date lastUpdate;
Comment on lines +34 to +50
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add field validation constraints.

The model lacks validation for mandatory fields and value ranges. Consider:

  1. Adding @NonNull annotations for mandatory fields
  2. Adding range validation for score and passingScore (should not be negative)
  3. Adding validation for date to ensure it's not in the future


public ConnectJobAssessmentRecord() {

}

public static ConnectJobAssessmentRecord fromJson(JSONObject json, int jobId) throws JSONException, ParseException {
ConnectJobAssessmentRecord record = new ConnectJobAssessmentRecord();

record.lastUpdate = new Date();

record.jobId = jobId;
record.date = json.has(META_DATE) ? DateUtils.parseDate(json.getString(META_DATE)) : new Date();
record.score = json.has(META_SCORE) ? json.getInt(META_SCORE) : -1;
record.passingScore = json.has(META_PASSING_SCORE) ? json.getInt(META_PASSING_SCORE) : -1;
record.passed = json.has(META_PASSED) && json.getBoolean(META_PASSED);

return record;
}

public Date getDate() { return date; }
public int getScore() { return score; }
public int getPassingScore() { return passingScore; }

public void setLastUpdate(Date date) { lastUpdate = date; }
}
Loading
Loading