Android library for peer-to-peer record synchronization in the OpenSRP ecosystem, built on Google Nearby Connections so devices can exchange data even without conventional network access.
- Toolchain: Gradle 8.7, Android Gradle Plugin 8.5.2, JDK 17; Kotlin is not required by the library.
- CI: Gradle release build executed through
jitpack.ymlkeeps publishing tasks green. - Default branch:
master; latest tag:v0.5.0.
- Offline-first synchronization channel that transfers JSON and multimedia payloads over Nearby Connections.
- Pluggable
SenderTransferDaoandReceiverTransferDaocontracts that adapt to any local data store. - Built-in activities and fragments that guide users through send/receive flows with authorization hooks.
- Optional callbacks for progress reporting and post-transfer handling.
- Sample app demonstrating end-to-end setup with mock data providers.
- JDK 17
- Gradle 8.7 (managed through the included wrapper)
- Android Gradle Plugin 8.5.2
- AndroidX / Jetifier enabled
minSdk28,targetSdk34,compileSdk34
Add Maven Central and depend on the artifact that matches your release. Replace <version> with the version listed on the Releases page.
dependencyResolutionManagement {
repositories {
mavenCentral()
}
}
dependencies {
implementation 'io.github.bluecodesystems:android-p2p-sync:<version>'
}repositories {
mavenCentral()
}
dependencies {
implementation("io.github.bluecodesystems:android-p2p-sync:<version>")
}Call the library once from your Application class to provide context, credentials, and data access objects.
public final class MyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
P2PLibrary.Options options = new P2PLibrary.Options(
this,
"encrypted_db_passphrase",
"John Doe",
new MyAuthorizationService(),
new MyReceiverDao(),
new MySenderDao()
);
options.setBatchSize(200);
options.setSyncFinishedCallback(new SyncFinishedCallback() {
@Override
public void onSuccess(@NonNull HashMap<String, Integer> transferRecords) {
// Update UI or analytics when records arrive.
}
@Override
public void onFailure(@NonNull Exception exception,
@Nullable HashMap<String, Integer> transferRecords) {
// Handle retry strategy or user messaging.
}
});
P2PLibrary.init(options);
}
}Implement ReceiverTransferDao to accept incoming payloads and acknowledge the last processed record. Multimedia files arrive on a worker thread, so you can persist them immediately.
public final class MyReceiverDao implements ReceiverTransferDao {
private final Map<String, Long> lastReceived = new HashMap<>();
@Override
public TreeSet<DataType> getDataTypes() {
TreeSet<DataType> dataTypes = new TreeSet<>();
dataTypes.add(new DataType("names", DataType.Type.NON_MEDIA, 0));
dataTypes.add(new DataType("profile_photos", DataType.Type.MEDIA, 1));
return dataTypes;
}
@Override
public long receiveJson(@NonNull DataType type, @NonNull JSONArray payload) {
long lastId = lastReceived.getOrDefault(type.getName(), 0L) + payload.length();
lastReceived.put(type.getName(), lastId);
return lastId;
}
@Override
public long receiveMultimedia(@NonNull DataType dataType, @NonNull File file,
@Nullable HashMap<String, Object> details, long recordId) {
// Persist file and return the associated record id when successful.
return recordId;
}
}Pair it with SenderTransferDao to expose batches of outbound records. The library requests JSON first and then multimedia when available.
public final class MySenderDao implements SenderTransferDao {
@Override
public TreeSet<DataType> getDataTypes() {
TreeSet<DataType> dataTypes = new TreeSet<>();
dataTypes.add(new DataType("names", DataType.Type.NON_MEDIA, 0));
return dataTypes;
}
@Override
public JsonData getJsonData(@NonNull DataType type, long lastRecordId, int batchSize) {
JSONArray records = fetchNamesAfter(lastRecordId, batchSize);
long highestId = records.length() > 0 ? lastRecordId + records.length() : lastRecordId;
return new JsonData(records, highestId);
}
@Override
public MultiMediaData getMultiMediaData(@NonNull DataType type, long lastRecordId) {
return null; // Provide when you have files to share.
}
}Helper methods such as fetchNamesAfter illustrate app-specific data access.
Supply a P2PAuthorizationService to guard who may connect and exchange data.
public final class MyAuthorizationService implements P2PAuthorizationService {
@Override
public void getAuthorizationDetails(@NonNull OnAuthorizationDetailsProvidedCallback callback) {
Map<String, Object> details = new HashMap<>();
details.put(Constants.AuthorizationKeys.PEER_STATUS, Constants.PeerStatus.SENDER);
callback.onAuthorizationDetailsProvided(details);
}
@Override
public void authorizeConnection(@NonNull Map<String, Object> details,
@NonNull AuthorizationCallback callback) {
if (userHasSyncRole()) {
callback.onConnectionAuthorized();
} else {
callback.onConnectionAuthorizationRejected("User lacks sync permissions.");
}
}
}Start P2pModeSelectActivity when you want the user to choose between sending and receiving. Override the processing_disclaimer string resource in your app if you need custom guidance during long-running imports.
startActivity(new Intent(currentActivity, P2pModeSelectActivity.class));A runnable sample module lives under sample/. Install it on a connected device or emulator with:
./gradlew :sample:installDebugYou can also open the project in Android Studio and run the sample configuration directly.
./gradlew clean assemble
./gradlew testSee the GitHub Releases for published versions, change notes, and migration guidance.
Issues and pull requests are welcome. Please build and test with JDK 17, Gradle 8.7, and AGP 8.5.2 to match the current toolchain. If you introduce new examples or guidance, keep the README in sync.
Licensed under the Apache License 2.0.