Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,11 @@ dependencies {
implementation 'androidx.fragment:fragment:1.8.8'
implementation "androidx.datastore:datastore-preferences:1.1.7"
implementation "androidx.datastore:datastore-preferences-rxjava3:1.1.7"
// P2P WiFi Hotspot Sync dependencies
implementation 'org.nanohttpd:nanohttpd:2.3.1'
implementation 'com.google.zxing:core:3.5.3'
implementation 'com.journeyapps:zxing-android-embedded:4.3.0'

compileOnly 'com.github.spotbugs:spotbugs-annotations:4.9.3'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
testImplementation 'junit:junit:4.13.2'
Expand Down
29 changes: 29 additions & 0 deletions src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<uses-feature android:name="android.hardware.location.network" android:required="false"/>

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/>
Expand All @@ -26,13 +27,24 @@
<uses-permission android:name="android.permission.SEND_SMS"/>
-->

<!-- P2P WiFi Hotspot Sync permissions -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" android:minSdkVersion="33"/>
<uses-feature android:name="android.hardware.wifi" android:required="false"/>
<uses-feature android:name="android.hardware.camera" android:required="false"/>

<application android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:allowBackup="false"
android:allowCrossUidActivitySwitchFromBelow="false"
android:fullBackupContent="@xml/backup_rules_sdk_30_and_lower"
android:dataExtractionRules="@xml/backup_rules"
android:largeHeap="true"
android:usesCleartextTraffic="true"
tools:ignore="LockedOrientationActivity,UnusedAttribute">
<activity android:name="StartupActivity" android:exported="true">
<intent-filter>
Expand Down Expand Up @@ -98,6 +110,23 @@
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="true"
tools:targetApi="23" />

<!-- P2P WiFi Hotspot Sync service + activity -->
<service android:name=".p2p.P2pForegroundService"
android:foregroundServiceType="connectedDevice"
android:exported="false"/>
<activity android:name="RequestP2pPermissionsActivity"
android:screenOrientation="portrait"
tools:ignore="DiscouragedApi"/>
<activity android:name=".p2p.QrScannerActivity"
android:screenOrientation="portrait"
tools:ignore="DiscouragedApi"/>
<activity android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="portrait"
android:stateNotNeeded="true"
android:theme="@style/zxing_CaptureTheme"
tools:replace="screenOrientation"
tools:ignore="DiscouragedApi"/>
</application>

<queries>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,12 @@ protected void onActivityResult(int requestCd, int resultCode, Intent intent) {
case ACCESS_SEND_SMS_PERMISSION:
this.smsSender.resumeProcess(resultCode);
return;
case ACCESS_P2P_PERMISSION:
p2pPermissionResolved(resultCode);
return;
case P2P_QR_SCAN:
processP2pQrScanResult(resultCode, intent);
return;
default:
trace(this, "onActivityResult() :: no handling for requestCode=%s", requestCode.name());
}
Expand Down Expand Up @@ -320,6 +326,37 @@ public boolean getLocationPermissions() {
}

//> PRIVATE HELPERS
public boolean getP2pPermissions() {
String[] perms = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
? new String[]{ "android.permission.CAMERA", ACCESS_FINE_LOCATION, "android.permission.NEARBY_WIFI_DEVICES" }
: new String[]{ "android.permission.CAMERA", ACCESS_FINE_LOCATION };

boolean allGranted = true;
for (String perm : perms) {
if (ContextCompat.checkSelfPermission(this, perm) != PERMISSION_GRANTED) {
allGranted = false;
break;
}
}

if (allGranted) {
trace(this, "getP2pPermissions() :: All P2P permissions already granted");
return true;
}

trace(this, "getP2pPermissions() :: P2P permissions not granted, requesting access...");
startActivityForResult(
new Intent(this, RequestP2pPermissionsActivity.class),
RequestCode.ACCESS_P2P_PERMISSION.getCode()
);
return false;
}

private void p2pPermissionResolved(int resultCode) {
String granted = resultCode == RESULT_OK ? "true" : "false";
evaluateJavascript("window.CHTCore.AndroidApi.v1.p2pPermissionRequestResolved(" + granted + ");");
}

private void locationRequestResolved() {
evaluateJavascript("window.CHTCore.AndroidApi.v1.locationPermissionRequestResolved();");
}
Expand Down Expand Up @@ -406,6 +443,18 @@ private void enableJavascript(WebView container) {

maj.setConnectivityManager((ConnectivityManager) this.getSystemService(Context.CONNECTIVITY_SERVICE));

// Initialize P2P bridge (singleton — survives activity recreation)
org.medicmobile.webapp.mobile.p2p.P2pManager p2pManager =
org.medicmobile.webapp.mobile.p2p.P2pManager.getInstance(this);
org.medicmobile.webapp.mobile.p2p.TransitDocManager transitDocManager =
new org.medicmobile.webapp.mobile.p2p.TransitDocManager();
org.medicmobile.webapp.mobile.p2p.P2pTracker tracker =
new org.medicmobile.webapp.mobile.p2p.P2pTracker();
org.medicmobile.webapp.mobile.p2p.P2pBridgeMethods p2pBridge =
new org.medicmobile.webapp.mobile.p2p.P2pBridgeMethods(p2pManager, transitDocManager, tracker);
maj.setP2pBridge(p2pBridge);
p2pBridge.setWebView(container);

container.addJavascriptInterface(maj, "medicmobile_android");
}

Expand Down Expand Up @@ -444,14 +493,53 @@ private void registerRetryConnectionBroadcastReceiver() {
);
}

/**
* Launch ZXing QR scanner for P2P peer connection.
* The scan result is returned via JavaScript callback.
*/
public void scanP2pQrCode() {
trace(this, "scanP2pQrCode() :: launching ZXing scanner");
com.journeyapps.barcodescanner.ScanOptions options =
new com.journeyapps.barcodescanner.ScanOptions();
options.setDesiredBarcodeFormats(com.journeyapps.barcodescanner.ScanOptions.QR_CODE);
options.setPrompt("Scan the Supervisor's QR code");
options.setCameraId(0);
options.setBeepEnabled(true);
options.setOrientationLocked(true);
com.journeyapps.barcodescanner.ScanContract scanContract =
new com.journeyapps.barcodescanner.ScanContract();
Intent scanIntent = scanContract.createIntent(this, options);
startActivityForResult(scanIntent, RequestCode.P2P_QR_SCAN.getCode());
}

private void processP2pQrScanResult(int resultCode, Intent intent) {
if (resultCode != RESULT_OK || intent == null) {
trace(this, "processP2pQrScanResult() :: scan cancelled or no result");
evaluateJavascript("window.CHTCore.P2p.onQrScanResult(null);");
return;
}
com.journeyapps.barcodescanner.ScanIntentResult result =
com.journeyapps.barcodescanner.ScanIntentResult.parseActivityResult(resultCode, intent);
String contents = result.getContents();
if (contents == null || contents.isEmpty()) {
evaluateJavascript("window.CHTCore.P2p.onQrScanResult(null);");
return;
}
// Escape for JavaScript string
String escaped = contents.replace("\\", "\\\\").replace("'", "\\'").replace("\n", "\\n");
evaluateJavascript("window.CHTCore.P2p.onQrScanResult('" + escaped + "');");
}

//> ENUMS
public enum RequestCode {
ACCESS_LOCATION_PERMISSION(100),
ACCESS_STORAGE_PERMISSION(101),
ACCESS_SEND_SMS_PERMISSION(102),
CHT_EXTERNAL_APP_ACTIVITY(103),
GRAB_MRDT_PHOTO_ACTIVITY(104),
FILE_PICKER_ACTIVITY(105);
FILE_PICKER_ACTIVITY(105),
ACCESS_P2P_PERMISSION(106),
P2P_QR_SCAN(107);

private final int requestCode;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ public boolean getLocationPermissions() {
return this.parent.getLocationPermissions();
}

@android.webkit.JavascriptInterface
public boolean getP2pPermissions() {
return this.parent.getP2pPermissions();
}

@android.webkit.JavascriptInterface
public void datePicker(final String targetElement) {
try {
Expand Down Expand Up @@ -312,6 +317,127 @@ public String getDeviceInfo() {
}
}

//> P2P SYNC BRIDGE METHODS
private static final String P2P_NOT_INITIALIZED = "P2P not initialized";
private org.medicmobile.webapp.mobile.p2p.P2pBridgeMethods p2pBridge;

public void setP2pBridge(org.medicmobile.webapp.mobile.p2p.P2pBridgeMethods bridge) {
this.p2pBridge = bridge;
}

@android.webkit.JavascriptInterface
public String p2pStartHostMode() {
if (p2pBridge == null) return jsonError(P2P_NOT_INITIALIZED);
return p2pBridge.p2pStartHostMode();
}

@android.webkit.JavascriptInterface
public String p2pStartClientMode(String qrPayloadJson) {
if (p2pBridge == null) return jsonError(P2P_NOT_INITIALIZED);
return p2pBridge.p2pStartClientMode(qrPayloadJson);
}

@android.webkit.JavascriptInterface
public void p2pStop() {
if (p2pBridge != null) p2pBridge.p2pStop();
}

@android.webkit.JavascriptInterface
public String p2pGetStatus() {
if (p2pBridge == null) return jsonError(P2P_NOT_INITIALIZED);
return p2pBridge.p2pGetStatus();
}

@android.webkit.JavascriptInterface
public String p2pCheckConnection() {
if (p2pBridge == null) return "{\"connected\":false}";
return p2pBridge.p2pCheckConnection();
}

@android.webkit.JavascriptInterface
public String p2pGetTransitDocIds() {
if (p2pBridge == null) return "[]";
return p2pBridge.p2pGetTransitDocIds();
}

@android.webkit.JavascriptInterface
public boolean p2pIsTransitDoc(String docId) {
return p2pBridge != null && p2pBridge.p2pIsTransitDoc(docId);
}

@android.webkit.JavascriptInterface
public String p2pPurgeTransitDocs() {
if (p2pBridge == null) return jsonError(P2P_NOT_INITIALIZED);
return p2pBridge.p2pPurgeTransitDocs();
}

@android.webkit.JavascriptInterface
public void p2pConfirmBatchPurged(String batchId) {
if (p2pBridge != null) p2pBridge.p2pConfirmBatchPurged(batchId);
}

@android.webkit.JavascriptInterface
public String p2pGetSyncHistory() {
if (p2pBridge == null) return "{}";
return p2pBridge.p2pGetSyncHistory();
}

@android.webkit.JavascriptInterface
public boolean p2pHasStaleTransitDocs() {
return p2pBridge != null && p2pBridge.p2pHasStaleTransitDocs();
}

@android.webkit.JavascriptInterface
public String p2pGetCapability() {
if (p2pBridge == null) return jsonError(P2P_NOT_INITIALIZED);
return p2pBridge.p2pGetCapability();
}

@android.webkit.JavascriptInterface
public String p2pGetBatteryGuidance() {
if (p2pBridge == null) return "";
return p2pBridge.p2pGetBatteryGuidance();
}

@android.webkit.JavascriptInterface
public boolean p2pNeedsBatteryGuidance() {
return p2pBridge != null && p2pBridge.p2pNeedsBatteryGuidance();
}

@android.webkit.JavascriptInterface
public String p2pInitialize(String configJson) {
if (p2pBridge == null) return jsonError(P2P_NOT_INITIALIZED);
return p2pBridge.p2pInitialize(configJson);
}

@android.webkit.JavascriptInterface
public void p2pScanQrCode() {
this.parent.scanP2pQrCode();
}

@android.webkit.JavascriptInterface
public String p2pRetrySync() {
if (p2pBridge == null) return jsonError(P2P_NOT_INITIALIZED);
return p2pBridge.p2pRetrySync();
}

@android.webkit.JavascriptInterface
public String p2pProceedSync() {
if (p2pBridge == null) return jsonError(P2P_NOT_INITIALIZED);
return p2pBridge.p2pProceedSync();
}

@android.webkit.JavascriptInterface
public String p2pIsActive() {
if (p2pBridge == null) return jsonError(P2P_NOT_INITIALIZED);
return p2pBridge.p2pIsActive();
}

@android.webkit.JavascriptInterface
public void p2pAsyncCallback(String callbackId, String result) {
if (p2pBridge != null) p2pBridge.p2pAsyncCallback(callbackId, result);
}

//> PRIVATE HELPER METHODS
private void datePicker(String targetElement, Calendar initialDate) {
// Remove single-quotes from the `targetElement` CSS selecter, as
Expand Down
Loading
Loading