diff --git a/deps.gradle b/deps.gradle index 89e7b447..5d218f1f 100644 --- a/deps.gradle +++ b/deps.gradle @@ -12,7 +12,7 @@ versions.junit5_api = '5.7.1' versions.junit5_plugin = '1.7.1.1' versions.ktlint = '0.41.0' versions.mockk = '1.11.0' -versions.robolectric = '4.4' +versions.robolectric = '4.8' versions.timber = '4.7.1' versions.room = '2.3.0' versions.spotless = '5.11.0' diff --git a/p2p-lib/build.gradle b/p2p-lib/build.gradle index 53088015..9cdb1855 100644 --- a/p2p-lib/build.gradle +++ b/p2p-lib/build.gradle @@ -5,7 +5,9 @@ plugins { id 'jacoco' id 'com.github.kt3k.coveralls' id 'com.diffplug.spotless' + id 'maven-publish' } +apply plugin: 'kotlin-kapt' jacoco { toolVersion = '0.8.7' @@ -21,6 +23,13 @@ android { versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + + javaCompileOptions { + annotationProcessorOptions { + arguments = ["room.schemaLocation": "$projectDir/schemas".toString()] + } + } } buildTypes { @@ -60,19 +69,23 @@ android { dependencies { configuration -> - implementation 'androidx.core:core-ktx:1.3.0' + implementation 'androidx.core:core-ktx:1.6.0' implementation 'androidx.work:work-runtime-ktx:2.6.0' - implementation 'androidx.appcompat:appcompat:1.2.0' - implementation 'com.google.android.material:material:1.3.0' + implementation 'androidx.appcompat:appcompat:1.3.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5' implementation 'androidx.navigation:navigation-ui-ktx:2.3.5' - implementation'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2' implementation 'com.jakewharton.timber:timber:5.0.1' + implementation 'com.google.android.material:material:1.3.0' + implementation 'com.google.code.gson:gson:2.9.0' + implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2' + implementation 'com.google.code.gson:gson:2.8.7' + implementation 'androidx.activity:activity:1.3.1' roomDependencies(configuration) + locationDependencies(configuration) - testImplementation 'junit:junit:4.+' + testImplementation 'junit:junit:4.13.2' testImplementation deps.junit5_api testRuntimeOnly deps.junit5_engine @@ -91,12 +104,18 @@ dependencies { configuration -> def roomDependencies(configuration) { configuration.implementation(deps.room.ktx) + configuration.kapt(deps.room.compiler) + + // sql cipher deps + configuration.implementation "net.zetetic:android-database-sqlcipher:4.5.0" + configuration.implementation "androidx.sqlite:sqlite:2.0.1" // Room Test helpers - configuration.testImplementation(deps.room.testing) + configuration.testImplementation "androidx.room:room-testing:2.3.0" +} - // Encrypted SQLite help - configuration.implementation "com.commonsware.cwac:saferoom:1.0.2" +def locationDependencies(configuration) { + configuration.implementation 'com.google.android.gms:play-services-location:16.0.0' } task jacocoTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest'/*, 'createDebugCoverageReport'*/]) { @@ -129,7 +148,9 @@ task jacocoTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest'/*, 'cr '**/*$Result.*', '**/*$Result$*.*', // Data Binding - '**/databinding/*' + '**/databinding/*', + // Generated room DAO implementation classes + '**/*_Impl*.*' ] def javaDebugTree = fileTree(dir: "$project.buildDir/intermediates/javac/debug/classes/", excludes: fileFilter) @@ -167,4 +188,41 @@ spotless { ktfmt().googleStyle() licenseHeaderFile "${project.rootProject.projectDir}/license-header.txt" } -} \ No newline at end of file +} + +afterEvaluate { + publishing { + publications { + snapshot(MavenPublication) { + from(components["release"]) + artifactId = "p2p-lib" + groupId = "org.smartregister" + version = "0.3.0-SNAPSHOT" + pom { + name.set("Peer to Peer Library") + } + } + } + repositories { + maven { + name = 'sonatype' + url = uri("https://oss.sonatype.org/content/repositories/snapshots/") + credentials { + username = getRepositoryUsername() + password = getRepositoryPassword() + } + } + + } + } +} + + + +def getRepositoryPassword() { + return hasProperty('sonatypePassword') ? sonatypePassword : "" +} + +def getRepositoryUsername() { + return hasProperty('sonatypeUsername') ? sonatypeUsername : "" +} diff --git a/p2p-lib/schemas/org.smartregister.p2p.model.AppDatabase/1.json b/p2p-lib/schemas/org.smartregister.p2p.model.AppDatabase/1.json new file mode 100644 index 00000000..0cfbbf32 --- /dev/null +++ b/p2p-lib/schemas/org.smartregister.p2p.model.AppDatabase/1.json @@ -0,0 +1,47 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "d355d8a44926f53645f849d9d8200228", + "entities": [ + { + "tableName": "p2p_received_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`app_lifetime_key` TEXT NOT NULL, `entity_type` TEXT NOT NULL, `last_updated_at` INTEGER NOT NULL, PRIMARY KEY(`entity_type`, `app_lifetime_key`))", + "fields": [ + { + "fieldPath": "appLifetimeKey", + "columnName": "app_lifetime_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "entityType", + "columnName": "entity_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastUpdatedAt", + "columnName": "last_updated_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "entity_type", + "app_lifetime_key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd355d8a44926f53645f849d9d8200228')" + ] + } +} \ No newline at end of file diff --git a/p2p-lib/src/main/java/org/smartregister/p2p/P2PLibrary.kt b/p2p-lib/src/main/java/org/smartregister/p2p/P2PLibrary.kt index b0e7ba93..149a718f 100644 --- a/p2p-lib/src/main/java/org/smartregister/p2p/P2PLibrary.kt +++ b/p2p-lib/src/main/java/org/smartregister/p2p/P2PLibrary.kt @@ -17,11 +17,15 @@ package org.smartregister.p2p import android.content.Context import androidx.annotation.NonNull -import androidx.annotation.Nullable import java.util.UUID +import org.smartregister.p2p.dao.ReceiverTransferDao +import org.smartregister.p2p.dao.SenderTransferDao +import org.smartregister.p2p.data_sharing.DataSharingStrategy +import org.smartregister.p2p.data_sharing.WifiDirectDataSharingStrategy import org.smartregister.p2p.model.AppDatabase import org.smartregister.p2p.utils.Constants import org.smartregister.p2p.utils.Settings +import org.smartregister.p2p.utils.isAppDebuggable import timber.log.Timber /** Created by Ephraim Kigamba - nek.eam@gmail.com on 14-03-2022. */ @@ -30,44 +34,44 @@ class P2PLibrary private constructor() { private lateinit var options: Options private var hashKey: String? = null private var deviceUniqueIdentifier: String? = null + var dataSharingStrategy: DataSharingStrategy = WifiDirectDataSharingStrategy() companion object { private var instance: P2PLibrary? = null - @NonNull - fun getInstance(): P2PLibrary? { + fun getInstance(): P2PLibrary { checkNotNull(instance) { - ("Instance does not exist!!! Call P2PLibrary.init method" + + ("Instance does not exist!!! Call P2PLibrary.init(P2PLibrary.Options) method " + "in the onCreate method of " + "your Application class ") } - return instance + return instance!! } - } - fun init(@NonNull options: Options) { - instance = P2PLibrary(options) + fun init(options: Options): P2PLibrary { + instance = P2PLibrary(options) + return instance!! + } } - private constructor(@NonNull options: Options) : this() { + private constructor(options: Options) : this() { this.options = options // We should not override the host applications Timber trees - if (Timber.treeCount == 0) { + if (Timber.treeCount == 0 && isAppDebuggable(options.context)) { Timber.plant(Timber.DebugTree()) } + hashKey = getHashKey() // Start the DB AppDatabase.getInstance(getContext(), options.dbPassphrase) } - @NonNull - fun getDb(): AppDatabase? { + fun getDb(): AppDatabase { return AppDatabase.getInstance(getContext(), options.dbPassphrase) } - @NonNull fun getHashKey(): String? { if (hashKey == null) { val settings = Settings(getContext()) @@ -88,17 +92,14 @@ class P2PLibrary private constructor() { this.deviceUniqueIdentifier = deviceUniqueIdentifier } - @Nullable fun getDeviceUniqueIdentifier(): String? { return deviceUniqueIdentifier } - @NonNull fun getUsername(): String { return options.username } - @NonNull fun getContext(): Context { return options.context } @@ -107,11 +108,21 @@ class P2PLibrary private constructor() { return options.batchSize } + fun getSenderTransferDao(): SenderTransferDao { + return options.senderTransferDao + } + + fun getReceiverTransferDao(): ReceiverTransferDao { + return options.receiverTransferDao + } + /** [P2PLibrary] configurability options an */ class Options( val context: Context, val dbPassphrase: String, val username: String, + val senderTransferDao: SenderTransferDao, + val receiverTransferDao: ReceiverTransferDao ) { var batchSize: Int = Constants.DEFAULT_SHARE_BATCH_SIZE } diff --git a/p2p-lib/src/main/java/org/smartregister/p2p/WifiP2pBroadcastReceiver.kt b/p2p-lib/src/main/java/org/smartregister/p2p/WifiP2pBroadcastReceiver.kt index e62782ee..e4bf8e28 100644 --- a/p2p-lib/src/main/java/org/smartregister/p2p/WifiP2pBroadcastReceiver.kt +++ b/p2p-lib/src/main/java/org/smartregister/p2p/WifiP2pBroadcastReceiver.kt @@ -16,11 +16,12 @@ package org.smartregister.p2p import android.Manifest -import android.app.Activity import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.pm.PackageManager +import android.net.wifi.p2p.WifiP2pGroup +import android.net.wifi.p2p.WifiP2pInfo import android.net.wifi.p2p.WifiP2pManager import android.os.Build import androidx.core.app.ActivityCompat @@ -30,12 +31,17 @@ class WifiP2pBroadcastReceiver( private val manager: WifiP2pManager, private val channel: WifiP2pManager.Channel, private val listener: P2PManagerListener, - private val context: Activity + private val context: Context ) : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { when (intent.action) { - WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> handleConnectionChanged() + WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> { + val p2pGroupInfo = + intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP) + val wifiP2pInfo = intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_INFO) + listener.onConnectionInfoAvailable(wifiP2pInfo!!, p2pGroupInfo) + } WifiP2pManager.WIFI_P2P_DISCOVERY_CHANGED_ACTION -> handleDiscoveryChanged(intent.getIntExtra(WifiP2pManager.EXTRA_DISCOVERY_STATE, -1)) WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION -> handlePeersChanged() diff --git a/p2p-lib/src/main/java/org/smartregister/p2p/dao/P2pReceivedHistoryDao.kt b/p2p-lib/src/main/java/org/smartregister/p2p/dao/P2pReceivedHistoryDao.kt index 7eca885c..49926513 100644 --- a/p2p-lib/src/main/java/org/smartregister/p2p/dao/P2pReceivedHistoryDao.kt +++ b/p2p-lib/src/main/java/org/smartregister/p2p/dao/P2pReceivedHistoryDao.kt @@ -15,8 +15,6 @@ */ package org.smartregister.p2p.dao -import androidx.annotation.NonNull -import androidx.annotation.Nullable import androidx.room.Dao import androidx.room.Insert import androidx.room.Query @@ -27,22 +25,18 @@ import org.smartregister.p2p.model.P2PReceivedHistory @Dao interface P2pReceivedHistoryDao { - @Insert fun addReceivedHistory(@NonNull receivedP2PReceivedHistory: P2PReceivedHistory?) + @Insert fun addReceivedHistory(receivedP2PReceivedHistory: P2PReceivedHistory?) - @Update fun updateReceivedHistory(@NonNull receivedP2PReceivedHistory: P2PReceivedHistory?) + @Update fun updateReceivedHistory(receivedP2PReceivedHistory: P2PReceivedHistory?) @Query("DELETE FROM p2p_received_history WHERE app_lifetime_key = :appLifetimeKey") - fun clearDeviceRecords(@NonNull appLifetimeKey: String?): Int + fun clearDeviceRecords(appLifetimeKey: String?): Int @Query("SELECT * FROM p2p_received_history WHERE app_lifetime_key = :appLifetimeKey") - fun getDeviceReceivedHistory(@NonNull appLifetimeKey: String?): List? + fun getDeviceReceivedHistory(appLifetimeKey: String?): List? - @Nullable @Query( "SELECT * FROM p2p_received_history WHERE app_lifetime_key = :appLifetimeKey AND entity_type = :entityType LIMIT 1" ) - fun getHistory( - @NonNull appLifetimeKey: String?, - @NonNull entityType: String? - ): P2PReceivedHistory? + fun getHistory(appLifetimeKey: String?, entityType: String?): P2PReceivedHistory? } diff --git a/p2p-lib/src/main/java/org/smartregister/p2p/dao/ReceiverTransferDao.kt b/p2p-lib/src/main/java/org/smartregister/p2p/dao/ReceiverTransferDao.kt new file mode 100644 index 00000000..7cd44b43 --- /dev/null +++ b/p2p-lib/src/main/java/org/smartregister/p2p/dao/ReceiverTransferDao.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2022 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.smartregister.p2p.dao + +import java.util.TreeSet +import org.json.JSONArray +import org.smartregister.p2p.sync.DataType + +interface ReceiverTransferDao { + + fun getP2PDataTypes(): TreeSet + + fun receiveJson(type: DataType, jsonArray: JSONArray): Long +} diff --git a/p2p-lib/src/main/java/org/smartregister/p2p/dao/SenderTransferDao.kt b/p2p-lib/src/main/java/org/smartregister/p2p/dao/SenderTransferDao.kt new file mode 100644 index 00000000..cba4e944 --- /dev/null +++ b/p2p-lib/src/main/java/org/smartregister/p2p/dao/SenderTransferDao.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2022 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.smartregister.p2p.dao + +import java.util.TreeSet +import org.smartregister.p2p.search.data.JsonData +import org.smartregister.p2p.sync.DataType + +interface SenderTransferDao { + fun getP2PDataTypes(): TreeSet + + fun getJsonData(dataType: DataType, lastRecordId: Long, batchSize: Int): JsonData? +} diff --git a/p2p-lib/src/main/java/org/smartregister/p2p/data_sharing/DataSharingStrategy.kt b/p2p-lib/src/main/java/org/smartregister/p2p/data_sharing/DataSharingStrategy.kt new file mode 100644 index 00000000..2532cf13 --- /dev/null +++ b/p2p-lib/src/main/java/org/smartregister/p2p/data_sharing/DataSharingStrategy.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2022 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.smartregister.p2p.data_sharing + +import android.app.Activity +import kotlinx.coroutines.CoroutineScope +import org.smartregister.p2p.payload.PayloadContract +import org.smartregister.p2p.utils.DispatcherProvider + +/** Created by Ephraim Kigamba - nek.eam@gmail.com on 21-03-2022. */ +interface DataSharingStrategy { + + fun setDispatcherProvider(dispatcherProvider: DispatcherProvider) + + fun setActivity(context: Activity) + + fun setCoroutineScope(coroutineScope: CoroutineScope) + + fun searchDevices(onDeviceFound: OnDeviceFound, onConnected: PairingListener) + + fun stopSearchingDevices(operationListener: OperationListener?) + + fun connect(device: DeviceInfo, operationListener: OperationListener) + + fun disconnect(device: DeviceInfo, operationListener: OperationListener) + + fun send( + device: DeviceInfo?, + syncPayload: PayloadContract, + operationListener: OperationListener + ) + + fun sendManifest(device: DeviceInfo?, manifest: Manifest, operationListener: OperationListener) + + fun receive( + device: DeviceInfo?, + payloadReceiptListener: PayloadReceiptListener, + operationListener: OperationListener + ) + + fun receiveManifest(device: DeviceInfo, operationListener: OperationListener): Manifest? + + fun onErrorOccurred(ex: Exception) + + fun onConnectionFailed(device: DeviceInfo, ex: Exception) + + fun onConnectionSucceeded(device: DeviceInfo) + + fun onDisconnectFailed(device: DeviceInfo, ex: Exception) + + fun onDisconnectSucceeded(device: DeviceInfo) + + fun onPairingFailed(ex: Exception) + + fun onSendingFailed(ex: Exception) + + fun onSearchingFailed(ex: Exception) + + fun getCurrentDevice(): DeviceInfo? + + interface OperationListener { + + fun onSuccess(device: DeviceInfo?) + + fun onFailure(device: DeviceInfo?, ex: Exception) + } + + interface PairingListener { + + fun onSuccess(device: DeviceInfo?) + + fun onFailure(device: DeviceInfo?, ex: Exception) + + fun onDisconnected() + } + + interface PayloadReceiptListener { + + fun onPayloadReceived(payload: PayloadContract?) + } + + fun onResume(isScanning: Boolean = false) + + fun onPause() +} diff --git a/p2p-lib/src/main/java/org/smartregister/p2p/data_sharing/DeviceInfo.kt b/p2p-lib/src/main/java/org/smartregister/p2p/data_sharing/DeviceInfo.kt new file mode 100644 index 00000000..b0792efa --- /dev/null +++ b/p2p-lib/src/main/java/org/smartregister/p2p/data_sharing/DeviceInfo.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2022 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.smartregister.p2p.data_sharing + +/** Created by Ephraim Kigamba - nek.eam@gmail.com on 21-03-2022. */ +interface DeviceInfo { + + var strategySpecificDevice: Any + + fun getDisplayName(): String + + fun name(): String + + fun address(): String +} diff --git a/p2p-lib/src/main/java/org/smartregister/p2p/data_sharing/Manifest.kt b/p2p-lib/src/main/java/org/smartregister/p2p/data_sharing/Manifest.kt new file mode 100644 index 00000000..ec0bd4e0 --- /dev/null +++ b/p2p-lib/src/main/java/org/smartregister/p2p/data_sharing/Manifest.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2022 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.smartregister.p2p.data_sharing + +import org.smartregister.p2p.sync.DataType + +/** Created by Ephraim Kigamba - nek.eam@gmail.com on 21-03-2022. */ +data class Manifest(val dataType: DataType, val recordsSize: Int, val payloadSize: Int) diff --git a/p2p-lib/src/main/java/org/smartregister/p2p/data_sharing/OnDeviceFound.kt b/p2p-lib/src/main/java/org/smartregister/p2p/data_sharing/OnDeviceFound.kt new file mode 100644 index 00000000..43e6f2da --- /dev/null +++ b/p2p-lib/src/main/java/org/smartregister/p2p/data_sharing/OnDeviceFound.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2022 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.smartregister.p2p.data_sharing + +import java.lang.Exception + +/** Created by Ephraim Kigamba - nek.eam@gmail.com on 21-03-2022. */ +interface OnDeviceFound { + + fun deviceFound(devices: List) + + fun failed(ex: Exception) +} diff --git a/p2p-lib/src/main/java/org/smartregister/p2p/data_sharing/SyncReceiverHandler.kt b/p2p-lib/src/main/java/org/smartregister/p2p/data_sharing/SyncReceiverHandler.kt new file mode 100644 index 00000000..ae08f96b --- /dev/null +++ b/p2p-lib/src/main/java/org/smartregister/p2p/data_sharing/SyncReceiverHandler.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2022 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.smartregister.p2p.data_sharing + +import kotlinx.coroutines.withContext +import org.json.JSONArray +import org.smartregister.p2p.P2PLibrary +import org.smartregister.p2p.R +import org.smartregister.p2p.dao.P2pReceivedHistoryDao +import org.smartregister.p2p.model.P2PReceivedHistory +import org.smartregister.p2p.search.ui.P2PReceiverViewModel +import org.smartregister.p2p.utils.Constants +import org.smartregister.p2p.utils.DispatcherProvider +import timber.log.Timber + +class SyncReceiverHandler +constructor( + val p2PReceiverViewModel: P2PReceiverViewModel, + private val dispatcherProvider: DispatcherProvider +) { + + private lateinit var currentManifest: Manifest + + fun processManifest(manifest: Manifest) { + currentManifest = manifest + // update UI with number of records to expect + p2PReceiverViewModel.updateProgress(R.string.transferring_x_records, manifest.recordsSize) + if (manifest.dataType.name == Constants.SYNC_COMPLETE) { + p2PReceiverViewModel.handleDataTransferCompleteManifest() + } else { + p2PReceiverViewModel.processChunkData() + } + } + + suspend fun processData(data: JSONArray) { + Timber.i("Processing chunk data") + + var lastUpdatedAt = + P2PLibrary.getInstance().getReceiverTransferDao().receiveJson(currentManifest.dataType, data) + + addOrUpdateLastRecord(currentManifest.dataType.name, lastUpdatedAt = lastUpdatedAt) + + p2PReceiverViewModel.processIncomingManifest() + } + + suspend fun addOrUpdateLastRecord(entityType: String, lastUpdatedAt: Long) { + // Retrieve sending device details + val sendingDeviceAppLifetimeKey = p2PReceiverViewModel.getSendingDeviceAppLifetimeKey() + + withContext(dispatcherProvider.io()) { + if (sendingDeviceAppLifetimeKey.isNotBlank()) { + val p2pReceivedHistoryDao = getP2pReceivedHistoryDao() + + var receivedHistory: P2PReceivedHistory? = + p2pReceivedHistoryDao.getHistory(sendingDeviceAppLifetimeKey, entityType) + + if (receivedHistory == null) { + receivedHistory = P2PReceivedHistory() + receivedHistory.lastUpdatedAt = lastUpdatedAt + receivedHistory.entityType = entityType + receivedHistory.appLifetimeKey = sendingDeviceAppLifetimeKey + p2pReceivedHistoryDao?.addReceivedHistory(receivedHistory) + } else { + receivedHistory.lastUpdatedAt = lastUpdatedAt + p2pReceivedHistoryDao?.updateReceivedHistory(receivedHistory) + } + } + } + } + + private fun getP2pReceivedHistoryDao(): P2pReceivedHistoryDao { + return P2PLibrary.getInstance().getDb().p2pReceivedHistoryDao() + } +} diff --git a/p2p-lib/src/main/java/org/smartregister/p2p/data_sharing/SyncSenderHandler.kt b/p2p-lib/src/main/java/org/smartregister/p2p/data_sharing/SyncSenderHandler.kt new file mode 100644 index 00000000..7788774a --- /dev/null +++ b/p2p-lib/src/main/java/org/smartregister/p2p/data_sharing/SyncSenderHandler.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2022 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.smartregister.p2p.data_sharing + +import java.util.TreeSet +import kotlinx.coroutines.withContext +import org.smartregister.p2p.P2PLibrary +import org.smartregister.p2p.model.P2PReceivedHistory +import org.smartregister.p2p.payload.BytePayload +import org.smartregister.p2p.payload.PayloadContract +import org.smartregister.p2p.search.ui.P2PSenderViewModel +import org.smartregister.p2p.sync.DataType +import org.smartregister.p2p.utils.Constants +import org.smartregister.p2p.utils.DispatcherProvider +import timber.log.Timber + +class SyncSenderHandler +constructor( + val p2PSenderViewModel: P2PSenderViewModel, + val dataSyncOrder: TreeSet, + val receivedHistory: List, + private val dispatcherProvider: DispatcherProvider +) { + private val remainingLastRecordIds = HashMap() + private val batchSize = 25 + private var awaitingDataTypeRecordsBatchSize = 0 + + private lateinit var awaitingPayload: PayloadContract + private var sendingSyncCompleteManifest = false + + suspend fun startSyncProcess() { + Timber.i("Start sync process") + generateRecordsToSend() + sendNextManifest() + } + + fun generateRecordsToSend() { + for (dataType in dataSyncOrder) { + remainingLastRecordIds[dataType.name] = 0L + } + + if (receivedHistory.isNotEmpty()) { + for (dataTypeHistory in receivedHistory) { + if (dataTypeHistory.lastUpdatedAt == 0L) { + continue + } + remainingLastRecordIds[dataTypeHistory.entityType!!] = dataTypeHistory.lastUpdatedAt + } + } + } + + suspend fun sendNextManifest() { + Timber.i("in send next manifest") + if (!dataSyncOrder.isEmpty()) { + sendJsonDataManifest(dataSyncOrder.first()) + } else { + val manifest = + Manifest( + dataType = DataType(Constants.SYNC_COMPLETE, DataType.Filetype.JSON, 0), + recordsSize = 0, + payloadSize = 0 + ) + + sendingSyncCompleteManifest = true + p2PSenderViewModel.sendManifest(manifest = manifest) + p2PSenderViewModel.updateSenderSyncComplete(senderSyncComplete = true) + } + } + + suspend fun sendJsonDataManifest(dataType: DataType) { + Timber.i("Sending json manifest") + val nullableRecordId = remainingLastRecordIds[dataType.name] + val lastRecordId = nullableRecordId ?: 0L + + withContext(dispatcherProvider.io()) { + val jsonData = + P2PLibrary.getInstance() + .getSenderTransferDao() + .getJsonData(dataType, lastRecordId, batchSize)!! + + // send actual manifest + + if (jsonData != null && (jsonData.getJsonArray()?.length()!! > 0)) { + Timber.i("Json data is has content") + val recordsArray = jsonData.getJsonArray() + + remainingLastRecordIds[dataType.name] = jsonData.getHighestRecordId() + Timber.i("remaining records last updated is ${remainingLastRecordIds[dataType.name]}") + + val recordsJsonString = recordsArray.toString() + awaitingDataTypeRecordsBatchSize = recordsArray!!.length() + awaitingPayload = + BytePayload( + recordsArray.toString().toByteArray(), + ) + + if (recordsJsonString.isNotBlank()) { + + val manifest = + Manifest( + dataType = dataType, + recordsSize = awaitingDataTypeRecordsBatchSize, + payloadSize = recordsJsonString.length + ) + + p2PSenderViewModel.sendManifest(manifest = manifest) + } + } else { + // signifies all data has been sent + Timber.i("Json data is null") + dataSyncOrder.remove(dataType) + sendNextManifest() + } + } + } + + fun processManifestSent() { + if (sendingSyncCompleteManifest) { + sendingSyncCompleteManifest = false + } else { + p2PSenderViewModel.sendChunkData(awaitingPayload) + } + } +} diff --git a/p2p-lib/src/main/java/org/smartregister/p2p/data_sharing/WifiDirectDataSharingStrategy.kt b/p2p-lib/src/main/java/org/smartregister/p2p/data_sharing/WifiDirectDataSharingStrategy.kt new file mode 100644 index 00000000..b4b6fbcc --- /dev/null +++ b/p2p-lib/src/main/java/org/smartregister/p2p/data_sharing/WifiDirectDataSharingStrategy.kt @@ -0,0 +1,795 @@ +/* + * Copyright 2022 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.smartregister.p2p.data_sharing + +import android.app.Activity +import android.content.BroadcastReceiver +import android.content.Context +import android.content.IntentFilter +import android.content.pm.PackageManager +import android.net.wifi.p2p.WifiP2pConfig +import android.net.wifi.p2p.WifiP2pDevice +import android.net.wifi.p2p.WifiP2pDeviceList +import android.net.wifi.p2p.WifiP2pGroup +import android.net.wifi.p2p.WifiP2pInfo +import android.net.wifi.p2p.WifiP2pManager +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.core.app.ActivityCompat +import com.google.gson.Gson +import java.io.DataInputStream +import java.io.DataOutputStream +import java.io.IOException +import java.net.InetSocketAddress +import java.net.ServerSocket +import java.net.Socket +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.smartregister.p2p.WifiP2pBroadcastReceiver +import org.smartregister.p2p.payload.BytePayload +import org.smartregister.p2p.payload.PayloadContract +import org.smartregister.p2p.payload.StringPayload +import org.smartregister.p2p.payload.SyncPayloadType +import org.smartregister.p2p.search.contract.P2PManagerListener +import org.smartregister.p2p.utils.DefaultDispatcherProvider +import org.smartregister.p2p.utils.DispatcherProvider +import timber.log.Timber + +/** Created by Ephraim Kigamba - nek.eam@gmail.com on 21-03-2022. */ +class WifiDirectDataSharingStrategy : DataSharingStrategy, P2PManagerListener { + + lateinit var context: Activity + private val wifiP2pManager: WifiP2pManager by lazy(LazyThreadSafetyMode.NONE) { + context.getSystemService(Context.WIFI_P2P_SERVICE) as WifiP2pManager + } + private val accessFineLocationPermissionRequestInt: Int = 12345 + private var wifiP2pChannel: WifiP2pManager.Channel? = null + private var wifiP2pReceiver: BroadcastReceiver? = null + + private var wifiP2pInfo: WifiP2pInfo? = null + private var onConnectionInfo: (() -> Unit)? = null + private var wifiP2pGroup: WifiP2pGroup? = null + private var currentDevice: WifiP2pDevice? = null + + val PORT = 8988 + val SOCKET_TIMEOUT = 5_000 + + private var socket: Socket? = null + private var dataInputStream: DataInputStream? = null + private var dataOutputStream: DataOutputStream? = null + + private var requestedDisconnection = false + private var isSearchingDevices = false + private var paired = false + private var dispatcherProvider: DispatcherProvider = DefaultDispatcherProvider() + + private lateinit var coroutineScope: CoroutineScope + + private val MANIFEST = "MANIFEST" + + override fun setDispatcherProvider(dispatcherProvider: DispatcherProvider) { + this.dispatcherProvider = dispatcherProvider + } + + override fun setActivity(context: Activity) { + this.context = context + } + + override fun setCoroutineScope(coroutineScope: CoroutineScope) { + this.coroutineScope = coroutineScope + } + + override fun searchDevices( + onDeviceFound: OnDeviceFound, + onConnected: DataSharingStrategy.PairingListener + ) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + requestAccessFineLocationIfNotGranted() + } + + wifiP2pChannel = wifiP2pManager.initialize(context, context.mainLooper, null) + wifiP2pChannel?.also { channel -> + wifiP2pReceiver = + WifiP2pBroadcastReceiver( + wifiP2pManager, + channel, + object : P2PManagerListener { + override fun handleWifiP2pDisabled() { + this@WifiDirectDataSharingStrategy.handleWifiP2pDisabled() + } + + override fun handleWifiP2pEnabled() { + this@WifiDirectDataSharingStrategy.handleWifiP2pEnabled() + } + + override fun handleUnexpectedWifiP2pState(wifiState: Int) { + this@WifiDirectDataSharingStrategy.handleUnexpectedWifiP2pState(wifiState) + } + + override fun handleWifiP2pDevice(device: WifiP2pDevice) { + onDeviceFound.deviceFound(listOf(WifiDirectDevice(device))) + + this@WifiDirectDataSharingStrategy.handleWifiP2pDevice(device) + } + + override fun handleP2pDiscoveryStarted() { + this@WifiDirectDataSharingStrategy.handleP2pDiscoveryStarted() + } + + override fun handleP2pDiscoveryStopped() { + this@WifiDirectDataSharingStrategy.handleP2pDiscoveryStopped() + } + + override fun handleUnexpectedWifiP2pDiscoveryState(discoveryState: Int) { + this@WifiDirectDataSharingStrategy.handleUnexpectedWifiP2pDiscoveryState( + discoveryState + ) + } + + override fun handleP2pPeersChanged(peerDeviceList: WifiP2pDeviceList) { + val devicesList = peerDeviceList.deviceList.map { WifiDirectDevice(it) } + onDeviceFound.deviceFound(devicesList) + + this@WifiDirectDataSharingStrategy.handleP2pPeersChanged(peerDeviceList) + } + + override fun handleAccessFineLocationNotGranted() { + this@WifiDirectDataSharingStrategy.handleAccessFineLocationNotGranted() + } + + override fun handleMinimumSDKVersionNotMet(minimumSdkVersion: Int) { + this@WifiDirectDataSharingStrategy.handleMinimumSDKVersionNotMet(minimumSdkVersion) + } + + override fun onConnectionInfoAvailable(info: WifiP2pInfo, wifiP2pGroup: WifiP2pGroup?) { + this@WifiDirectDataSharingStrategy.onConnectionInfoAvailable(info, wifiP2pGroup) + + if (info.groupFormed) { + paired = true + onConnected.onSuccess(null) + } else { + + if (paired) { + closeSocketAndStreams() + if (!requestedDisconnection) { + onConnected.onDisconnected() + } + + paired = false + } + requestedDisconnection = false + } + } + + override fun onConnectionInfoAvailable(info: WifiP2pInfo) { + this@WifiDirectDataSharingStrategy.onConnectionInfoAvailable(info, null) + } + }, + context + ) + } + + listenForWifiP2pIntents() + initiatePeerDiscovery(onDeviceFound) + } + + private fun requestConnectionInfo() { + wifiP2pManager.requestConnectionInfo(wifiP2pChannel) { onConnectionInfoAvailable(it, null) } + } + + private fun listenForWifiP2pIntents() { + wifiP2pReceiver?.also { + context.registerReceiver( + it, + IntentFilter().apply { + addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION) + addAction(WifiP2pManager.WIFI_P2P_DISCOVERY_CHANGED_ACTION) + addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION) + addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION) + addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION) + } + ) + } + } + + private fun initiatePeerDiscoveryOnceAccessFineLocationGranted() { + if (ActivityCompat.checkSelfPermission( + context, + android.Manifest.permission.ACCESS_FINE_LOCATION + ) != PackageManager.PERMISSION_GRANTED + ) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + requestAccessFineLocationIfNotGranted() + } else { + handleMinimumSDKVersionNotMet(Build.VERSION_CODES.M) + } + } else { + initiatePeerDiscovery(null) + } + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun requestAccessFineLocationIfNotGranted() { + when (ActivityCompat.checkSelfPermission( + context, + android.Manifest.permission.ACCESS_FINE_LOCATION + ) + ) { + PackageManager.PERMISSION_GRANTED -> logDebug("Wifi P2P: Access fine location granted") + else -> { + logDebug("Wifi P2P: Requesting access fine location permission") + return context.requestPermissions( + arrayOf(android.Manifest.permission.ACCESS_FINE_LOCATION), + accessFineLocationPermissionRequestInt + ) + } + } + } + + private fun initiatePeerDiscovery(onDeviceFound: OnDeviceFound?) { + if (ActivityCompat.checkSelfPermission( + context, + android.Manifest.permission.ACCESS_FINE_LOCATION + ) != PackageManager.PERMISSION_GRANTED + ) { + return handleAccessFineLocationNotGranted() + } + + isSearchingDevices = true + wifiP2pManager.discoverPeers( + wifiP2pChannel, + object : WifiP2pManager.ActionListener { + override fun onSuccess() { + logDebug("Discovering peers successful") + } + + override fun onFailure(reason: Int) { + val exception = Exception("$reason: ${getWifiP2pReason(reason)}") + onDeviceFound?.failed(exception) + onSearchingFailed(exception) + } + } + ) + Timber.d("Peer discovery initiated") + } + private fun requestDeviceInfo() { + wifiP2pChannel?.also { wifiP2pChannel -> + if (ActivityCompat.checkSelfPermission( + context, + android.Manifest.permission.ACCESS_FINE_LOCATION + ) != PackageManager.PERMISSION_GRANTED + ) { + return handleAccessFineLocationNotGranted() + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + wifiP2pManager.requestDeviceInfo(wifiP2pChannel) { + if (it != null) { + handleWifiP2pDevice(it) + } + } + } else { + // TODO: Handle fetching device details + } + } + } + override fun connect( + device: DeviceInfo, + operationListener: DataSharingStrategy.OperationListener + ) { + val wifiDirectDevice = device.strategySpecificDevice as WifiP2pDevice + + Timber.d("Wifi P2P: Initiating connection to device: ${wifiDirectDevice.deviceName}") + val wifiP2pConfig = WifiP2pConfig().apply { deviceAddress = wifiDirectDevice.deviceAddress } + wifiP2pChannel?.also { wifiP2pChannel -> + if (ActivityCompat.checkSelfPermission( + context, + android.Manifest.permission.ACCESS_FINE_LOCATION + ) != PackageManager.PERMISSION_GRANTED + ) { + return handleAccessFineLocationNotGranted() + } + wifiP2pManager.connect( + wifiP2pChannel, + wifiP2pConfig, + object : WifiP2pManager.ActionListener { + override fun onSuccess() { + currentDevice = wifiDirectDevice + paired = true + + onConnectionSucceeded(device) + operationListener.onSuccess(device) + } + + override fun onFailure(reason: Int) { + val exception = Exception("Error #$reason: ${getWifiP2pReason(reason)}") + onConnectionFailed(device, exception) + operationListener.onFailure(device, exception) + } + } + ) + } + } + + override fun disconnect( + device: DeviceInfo, + operationListener: DataSharingStrategy.OperationListener + ) { + requestedDisconnection = true + wifiP2pManager.removeGroup( + wifiP2pChannel, + object : WifiP2pManager.ActionListener { + override fun onSuccess() { + paired = false + onDisconnectSucceeded(device) + operationListener.onSuccess(device) + } + + override fun onFailure(reason: Int) { + val exception = Exception("Error #$reason: ${getWifiP2pReason(reason)}") + onDisconnectFailed(device, exception) + operationListener.onFailure(device, exception) + } + } + ) + } + + override fun send( + device: DeviceInfo?, + syncPayload: PayloadContract, + operationListener: DataSharingStrategy.OperationListener + ) { + // Check if the socket is setup for sending + // Check if this is the sender/receiver + if (wifiP2pInfo == null) { + val p2pDevice = (device?.strategySpecificDevice as WifiP2pDevice) + val errorMsg = "WifiP2PInfo is not available" + logError(errorMsg) + operationListener.onFailure( + device, + Exception("Error sending to ${p2pDevice.deviceName}(${p2pDevice.deviceAddress}): $errorMsg") + ) + return + } + + coroutineScope.launch(dispatcherProvider.io()) { + makeSocketConnections(getGroupOwnerAddress()) { socket -> + if (socket != null) { + + when (syncPayload.getDataType()) { + SyncPayloadType.STRING -> { + + if (dataOutputStream != null) { + dataOutputStream?.apply { + writeUTF(SyncPayloadType.STRING.name) + flush() + + writeUTF(syncPayload.getData() as String) + flush() + + operationListener.onSuccess(device) + } + } else { + operationListener.onFailure(device, Exception("DataOutputStream is null")) + } + } + SyncPayloadType.BYTES -> { + + dataOutputStream?.apply { + val byteArray = syncPayload.getData() as ByteArray + + writeUTF(SyncPayloadType.BYTES.name) + writeLong(byteArray.size.toLong()) + + val len = byteArray.size + var offset = 0 + var chunkSize = 1024 + + while (offset < len) { + if (chunkSize > len) { + chunkSize = len + } + write(byteArray, offset, chunkSize) + + offset += chunkSize + if ((len - offset) < chunkSize) { + chunkSize = len - offset + } + } + + operationListener.onSuccess(device) + } + ?: run { + operationListener.onFailure(device, Exception("DataOutputStream is null")) + } + } + } + } else { + onConnectionInfo = + fun() { + send(device, syncPayload, operationListener) + } + } + } + } + } + + private fun getGroupOwnerAddress(): String { + return wifiP2pInfo!!.groupOwnerAddress.hostAddress + } + + suspend fun makeSocketConnections( + groupOwnerAddress: String, + onSocketConnectionMade: (socket: Socket?) -> Unit + ) { + val socketResult: Socket? + if (socket != null) { + socketResult = socket + } else if (wifiP2pInfo == null) { + // Request connections + requestConnectionInfo() + + socketResult = null + socket = socketResult + } else if (wifiP2pInfo?.isGroupOwner == true) { + // Start a server to accept connections. + socketResult = acceptConnectionsToServerSocket() + socket = socketResult + } else { + // Connect to the server running on the group owner device. + socketResult = connectToServerSocket(groupOwnerAddress) + socket = socketResult + } + + onSocketConnectionMade.invoke(socketResult) + } + + private suspend fun acceptConnectionsToServerSocket(): Socket? = + withContext(dispatcherProvider.io()) { + try { + val serverSocket = ServerSocket(PORT) + serverSocket.accept().apply { constructStreamsFromSocket(this) } + } catch (e: Exception) { + Timber.e(e) + null + } + } + + private fun constructStreamsFromSocket(socket: Socket) { + dataInputStream = DataInputStream(socket.getInputStream()) + dataOutputStream = DataOutputStream(socket.getOutputStream()) + } + + private suspend fun connectToServerSocket(groupOwnerAddress: String): Socket? = + withContext(dispatcherProvider.io()) { + try { + Socket().apply { + bind(null) + connect(InetSocketAddress(groupOwnerAddress, PORT), SOCKET_TIMEOUT) + constructStreamsFromSocket(this) + } + } catch (e: Exception) { + Timber.e(e) + null + } + } + + override fun sendManifest( + device: DeviceInfo?, + manifest: Manifest, + operationListener: DataSharingStrategy.OperationListener + ) { + // Check if the socket is setup for sending + // Check if this is the sender/receiver + + dataOutputStream?.apply { + val manifestString = Gson().toJson(manifest) + writeUTF(MANIFEST) + writeUTF(manifestString) + flush() + operationListener.onSuccess(device = device) + } + } + + override fun receive( + device: DeviceInfo?, + payloadReceiptListener: DataSharingStrategy.PayloadReceiptListener, + operationListener: DataSharingStrategy.OperationListener + ) { + // Check if the socket is setup for listening + // Check if this is the receiver/sender + + if (wifiP2pInfo == null) { + val p2pDevice = (device?.strategySpecificDevice as WifiP2pDevice) + val errorMsg = "WifiP2PInfo is not available" + logError(errorMsg) + operationListener.onFailure( + device, + Exception( + "Error receiving from ${p2pDevice.deviceName}(${p2pDevice.deviceAddress}): $errorMsg" + ) + ) + } + + coroutineScope.launch(dispatcherProvider.io()) { + makeSocketConnections(getGroupOwnerAddress()) { socket -> + if (socket != null) { + + dataInputStream?.run { + val dataType = readUTF() + + if (dataType == SyncPayloadType.STRING.name) { + val stringPayload = readUTF() + payloadReceiptListener.onPayloadReceived(StringPayload(stringPayload)) + } else if (dataType == SyncPayloadType.BYTES.name) { + var payloadLen = readLong() + val payloadByteArray = ByteArray(payloadLen.toInt()) + var currentBufferPos = 0 + var n = 0 + + while (payloadLen > 0 && + read(payloadByteArray, currentBufferPos, Math.min(1024, payloadLen).toInt()).also { + n = it + } != -1) { + + currentBufferPos += n + payloadLen -= n.toLong() + logDebug("file size $payloadLen") + } + payloadReceiptListener.onPayloadReceived(BytePayload(payloadByteArray)) + } else { + operationListener.onFailure( + getCurrentDevice(), + Exception("Unknown datatype: $dataType") + ) + } + } + } else { + operationListener.onFailure(getCurrentDevice(), Exception("Socket is null")) + } + } + } + } + + override fun receiveManifest( + device: DeviceInfo, + operationListener: DataSharingStrategy.OperationListener + ): Manifest? { + // Check if the socket is setup for listening + // Check if this is the receiver/sender + + return dataInputStream?.run { + val dataType = readUTF() + + if (dataType == MANIFEST) { + + val manifestString = readUTF() + Gson().fromJson(manifestString, Manifest::class.java) + } else { + null + } + } + } + + override fun onErrorOccurred(ex: Exception) { + // TODO: Show random error occurred + closeSocketAndStreams() + } + + override fun onConnectionFailed(device: DeviceInfo, ex: Exception) { + // TODO: Return this to the device + closeSocketAndStreams() + } + + override fun onConnectionSucceeded(device: DeviceInfo) { + // TODO: Return this to the device + } + + override fun onDisconnectFailed(device: DeviceInfo, ex: Exception) { + // TODO: Return this to the device + } + + override fun onDisconnectSucceeded(device: DeviceInfo) { + // TODO: Return this to the device + + closeSocketAndStreams() + } + + override fun onPairingFailed(ex: Exception) { + // TODO: Return this to the device + } + + override fun onSendingFailed(ex: Exception) { + // TODO: Return this to the device + // Also show an error on the UI + } + + override fun onSearchingFailed(ex: Exception) { + // TODO: Return this to the device + } + + override fun getCurrentDevice(): DeviceInfo? { + if (currentDevice != null) { + return WifiDirectDevice(currentDevice!!) + } else { + return null + } + } + + override fun onResume(isScanning: Boolean) { + if (isScanning) { + listenForWifiP2pIntents() + initiatePeerDiscoveryOnceAccessFineLocationGranted() + requestDeviceInfo() + requestConnectionInfo() + } + } + + override fun onPause() { + wifiP2pReceiver?.also { context.unregisterReceiver(it) } + } + + private fun logDebug(message: String) { + Timber.d(message) + } + + private fun logError(message: String) { + Timber.e(message) + } + + override fun handleWifiP2pDisabled() { + // TODO: Handle the issue here + } + + override fun handleWifiP2pEnabled() { + // TODO: Handle the issue here + } + + override fun handleUnexpectedWifiP2pState(wifiState: Int) { + // TODO: Handle the issue here + // Also show an error on the UI + } + + override fun handleWifiP2pDevice(device: WifiP2pDevice) { + // TODO: Handle the issue here + // This is a new p2p device + } + + override fun handleP2pDiscoveryStarted() { + // TODO: Handle the issue here + } + + override fun handleP2pDiscoveryStopped() { + // TODO: Handle the issue here + } + + override fun handleUnexpectedWifiP2pDiscoveryState(discoveryState: Int) { + // TODO: Handle the issue here + } + + override fun handleP2pPeersChanged(peerDeviceList: WifiP2pDeviceList) { + // TODO: Handle the issue here + } + + override fun handleAccessFineLocationNotGranted() { + // TODO: Handle the issue here + } + + override fun handleMinimumSDKVersionNotMet(minimumSdkVersion: Int) { + // TODO: Handle the issue here + } + + override fun onConnectionInfoAvailable(info: WifiP2pInfo, wifiP2pGroup: WifiP2pGroup?) { + if (info == null) { + logError("Connection info provided is NULL") + return + } + + val message = + "Connection info available: groupFormed = ${info.groupFormed}, isGroupOwner = ${info.isGroupOwner}" + logDebug(message) + wifiP2pInfo = info + + if (info.groupFormed && wifiP2pGroup != null) { + this.wifiP2pGroup = wifiP2pGroup + + if (info.isGroupOwner) { + val isGroupOwner = info.isGroupOwner + currentDevice = wifiP2pGroup.clientList.firstOrNull { it.isGroupOwner != isGroupOwner } + } + } + + if (onConnectionInfo != null) { + onConnectionInfo?.invoke() + onConnectionInfo = null + } + } + + override fun onConnectionInfoAvailable(info: WifiP2pInfo) { + this.onConnectionInfoAvailable(info, null) + } + + override fun stopSearchingDevices(operationListener: DataSharingStrategy.OperationListener?) { + if (isSearchingDevices) { + wifiP2pManager.stopPeerDiscovery( + wifiP2pChannel, + object : WifiP2pManager.ActionListener { + override fun onSuccess() { + logDebug("Successfully stopped peer discovery") + operationListener?.onSuccess(null) + } + + override fun onFailure(reason: Int) { + val ex = + Exception("Error occurred trying to stop peer discovery ${getWifiP2pReason(reason)}") + Timber.e(ex) + operationListener?.onFailure(null, ex) + } + } + ) + isSearchingDevices = false + } + } + + fun closeSocketAndStreams() { + stopSearchingDevices(null) + + dataInputStream?.run { close() } + + dataOutputStream?.run { + flush() + close() + } + + if (socket != null) { + try { + socket!!.close() + } catch (e: IOException) { + Timber.e(e) + } + socket = null + } + } + + fun getWifiP2pReason(reasonInt: Int): String = + when (reasonInt) { + 0 -> "Error" + 1 -> "Unsupported" + 2 -> "Busy" + else -> "Unknown" + } + + class WifiDirectDevice(var wifiP2pDevice: WifiP2pDevice) : DeviceInfo { + + override var strategySpecificDevice: Any + get() = wifiP2pDevice + set(value) { + wifiP2pDevice = value as WifiP2pDevice + } + + override fun getDisplayName(): String = + "${wifiP2pDevice.deviceName} (${wifiP2pDevice.deviceAddress})" + + override fun name(): String { + return wifiP2pDevice.deviceName + } + + override fun address(): String { + return wifiP2pDevice.deviceAddress + } + } +} diff --git a/p2p-lib/src/main/java/org/smartregister/p2p/model/AppDatabase.java b/p2p-lib/src/main/java/org/smartregister/p2p/model/AppDatabase.java new file mode 100644 index 00000000..6eb05368 --- /dev/null +++ b/p2p-lib/src/main/java/org/smartregister/p2p/model/AppDatabase.java @@ -0,0 +1,39 @@ +package org.smartregister.p2p.model; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.room.Database; +import androidx.room.Room; +import androidx.room.RoomDatabase; + +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SupportFactory; + +import org.smartregister.p2p.dao.P2pReceivedHistoryDao; + +/** + * Created by Ephraim Kigamba - nek.eam@gmail.com on 11-05-2022. + */ +@Database(entities = {P2PReceivedHistory.class}, version = 1) +public abstract class AppDatabase extends RoomDatabase { + + private static AppDatabase instance; + public static final String DB_NAME = "p2p"; + + @NonNull + public static AppDatabase getInstance(@NonNull Context context, @NonNull String passphrase) { + if (instance == null) { + SupportFactory safeHelperFactory = new SupportFactory(SQLiteDatabase.getBytes(passphrase.toCharArray())); + instance = + Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DB_NAME) + .openHelperFactory(safeHelperFactory) + .build(); + } + + return instance; + } + + @NonNull + public abstract P2pReceivedHistoryDao p2pReceivedHistoryDao(); +} diff --git a/p2p-lib/src/main/java/org/smartregister/p2p/model/AppDatabase.kt b/p2p-lib/src/main/java/org/smartregister/p2p/model/AppDatabase.kt deleted file mode 100644 index d95cc2ec..00000000 --- a/p2p-lib/src/main/java/org/smartregister/p2p/model/AppDatabase.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2022 Ona Systems, Inc - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.smartregister.p2p.model - -import android.content.Context -import android.text.SpannableStringBuilder -import androidx.annotation.NonNull -import androidx.room.Database -import androidx.room.Room -import androidx.room.RoomDatabase -import com.commonsware.cwac.saferoom.SafeHelperFactory -import org.smartregister.p2p.dao.P2pReceivedHistoryDao - -/** - * Provides [AppDatabase.getInstance] to access database instance. The instance gives access to the - * [P2pReceivedHistoryDao] - */ -@Database(entities = [P2PReceivedHistory::class], version = 1) -abstract class AppDatabase : RoomDatabase() { - abstract fun p2pReceivedHistoryDao(): P2pReceivedHistoryDao? - - companion object { - private var instance: AppDatabase? = null - var dbName = "p2p" - fun getInstance(@NonNull context: Context, @NonNull passphrase: String?): AppDatabase? { - if (instance == null) { - val safeHelperFactory: SafeHelperFactory = - SafeHelperFactory.fromUser(SpannableStringBuilder(passphrase)) - instance = - Room.databaseBuilder(context.getApplicationContext(), AppDatabase::class.java, dbName) - .openHelperFactory(safeHelperFactory) - .build() - } - return instance - } - } -} diff --git a/p2p-lib/src/main/java/org/smartregister/p2p/model/P2PReceivedHistory.kt b/p2p-lib/src/main/java/org/smartregister/p2p/model/P2PReceivedHistory.kt index bb3ff356..8e8284f9 100644 --- a/p2p-lib/src/main/java/org/smartregister/p2p/model/P2PReceivedHistory.kt +++ b/p2p-lib/src/main/java/org/smartregister/p2p/model/P2PReceivedHistory.kt @@ -25,9 +25,10 @@ import androidx.room.Entity */ @Entity(tableName = "p2p_received_history", primaryKeys = ["entity_type", "app_lifetime_key"]) class P2PReceivedHistory { - @NonNull @ColumnInfo(name = "app_lifetime_key") var appLifetimeKey: String? = null - @NonNull @ColumnInfo(name = "entity_type") var entityType: String? = null + @NonNull @ColumnInfo(name = "app_lifetime_key") lateinit var appLifetimeKey: String + + @NonNull @ColumnInfo(name = "entity_type") lateinit var entityType: String @ColumnInfo(name = "last_updated_at") var lastUpdatedAt: Long = 0 } diff --git a/p2p-lib/src/main/java/org/smartregister/p2p/payload/BytePayload.kt b/p2p-lib/src/main/java/org/smartregister/p2p/payload/BytePayload.kt new file mode 100644 index 00000000..2dcbb889 --- /dev/null +++ b/p2p-lib/src/main/java/org/smartregister/p2p/payload/BytePayload.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2022 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.smartregister.p2p.payload + +/** Created by Ephraim Kigamba - nek.eam@gmail.com on 04-04-2022. */ +class BytePayload(val payload: ByteArray) : PayloadContract { + + override fun getDataType(): SyncPayloadType = SyncPayloadType.BYTES + + override fun getData(): ByteArray = payload +} diff --git a/p2p-lib/src/main/java/org/smartregister/p2p/payload/PayloadContract.kt b/p2p-lib/src/main/java/org/smartregister/p2p/payload/PayloadContract.kt new file mode 100644 index 00000000..34e4a211 --- /dev/null +++ b/p2p-lib/src/main/java/org/smartregister/p2p/payload/PayloadContract.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2022 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.smartregister.p2p.payload + +/** Created by Ephraim Kigamba - nek.eam@gmail.com on 04-04-2022. */ +interface PayloadContract { + + fun getDataType(): SyncPayloadType + + fun getData(): T +} diff --git a/p2p-lib/src/main/java/org/smartregister/p2p/payload/StringPayload.kt b/p2p-lib/src/main/java/org/smartregister/p2p/payload/StringPayload.kt new file mode 100644 index 00000000..52361853 --- /dev/null +++ b/p2p-lib/src/main/java/org/smartregister/p2p/payload/StringPayload.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2022 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.smartregister.p2p.payload + +class StringPayload(val string: String) : PayloadContract { + + override fun getDataType(): SyncPayloadType = SyncPayloadType.STRING + + override fun getData(): String = string +} diff --git a/p2p-lib/src/main/java/org/smartregister/p2p/SyncPayload.kt b/p2p-lib/src/main/java/org/smartregister/p2p/payload/SyncPayloadType.kt similarity index 76% rename from p2p-lib/src/main/java/org/smartregister/p2p/SyncPayload.kt rename to p2p-lib/src/main/java/org/smartregister/p2p/payload/SyncPayloadType.kt index 624a5f77..f7f2edee 100644 --- a/p2p-lib/src/main/java/org/smartregister/p2p/SyncPayload.kt +++ b/p2p-lib/src/main/java/org/smartregister/p2p/payload/SyncPayloadType.kt @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.smartregister.p2p +package org.smartregister.p2p.payload -import kotlinx.serialization.Serializable - -/** Simple class containing message to sync using p2p */ -@Serializable data class SyncPayload(val message: String) +/** Created by Ephraim Kigamba - nek.eam@gmail.com on 04-04-2022. */ +enum class SyncPayloadType { + STRING, + BYTES +} diff --git a/p2p-lib/src/main/java/org/smartregister/p2p/search/adapter/DeviceListAdapter.kt b/p2p-lib/src/main/java/org/smartregister/p2p/search/adapter/DeviceListAdapter.kt index 960a4ab1..0dd309d4 100644 --- a/p2p-lib/src/main/java/org/smartregister/p2p/search/adapter/DeviceListAdapter.kt +++ b/p2p-lib/src/main/java/org/smartregister/p2p/search/adapter/DeviceListAdapter.kt @@ -15,7 +15,6 @@ */ package org.smartregister.p2p.search.adapter -import android.net.wifi.p2p.WifiP2pDevice import android.view.LayoutInflater import android.view.MotionEvent import android.view.View @@ -26,12 +25,13 @@ import android.widget.TextView import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView import org.smartregister.p2p.R +import org.smartregister.p2p.data_sharing.DeviceInfo import timber.log.Timber /** Recycler view adapter used to list discovered devices on the device list bottom sheet */ class DeviceListAdapter( - private val peerDeviceList: List, - private val onDeviceClick: (deviceAddress: WifiP2pDevice) -> Unit + private val peerDeviceList: List, + private val onDeviceClick: (deviceAddress: DeviceInfo) -> Unit ) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeviceListAdapter.ViewHolder { @@ -43,25 +43,25 @@ class DeviceListAdapter( override fun onBindViewHolder(holder: DeviceListAdapter.ViewHolder, position: Int) { val peerDevice = peerDeviceList[position] - holder.deviceName.text = peerDevice.deviceName - holder.deviceAddress.text = peerDevice.deviceAddress + holder.deviceName.text = peerDevice.name() + holder.deviceAddress.text = peerDevice.address() holder.currentDevice = peerDevice } override fun getItemCount(): Int = peerDeviceList.size /** View holder used by [DeviceListAdapter] */ - inner class ViewHolder(itemView: View, onDeviceClick: (deviceAddress: WifiP2pDevice) -> Unit) : + inner class ViewHolder(itemView: View, onDeviceClick: (device: DeviceInfo) -> Unit) : RecyclerView.ViewHolder(itemView) { val deviceName = itemView.findViewById(R.id.device_item_title) val deviceAddress = itemView.findViewById(R.id.device_item_subtitle) - var currentDevice: WifiP2pDevice? = null + var currentDevice: DeviceInfo? = null init { itemView.setOnClickListener { currentDevice?.let { - Timber.e("Item ${it.deviceName} has been clicked") + Timber.e("Item ${it.getDisplayName()} has been clicked") deviceAddress.setText(R.string.pairing) itemView.findViewById(R.id.device_item_pairing_icon).visibility = View.VISIBLE diff --git a/p2p-lib/src/main/java/org/smartregister/p2p/search/contract/P2PManagerListener.kt b/p2p-lib/src/main/java/org/smartregister/p2p/search/contract/P2PManagerListener.kt index 43932ad0..3d5d40db 100644 --- a/p2p-lib/src/main/java/org/smartregister/p2p/search/contract/P2PManagerListener.kt +++ b/p2p-lib/src/main/java/org/smartregister/p2p/search/contract/P2PManagerListener.kt @@ -17,6 +17,8 @@ package org.smartregister.p2p.search.contract import android.net.wifi.p2p.WifiP2pDevice import android.net.wifi.p2p.WifiP2pDeviceList +import android.net.wifi.p2p.WifiP2pGroup +import android.net.wifi.p2p.WifiP2pInfo import android.net.wifi.p2p.WifiP2pManager /** Created by Ephraim Kigamba - nek.eam@gmail.com on 25-02-2022. */ @@ -41,4 +43,6 @@ interface P2PManagerListener : WifiP2pManager.ConnectionInfoListener { fun handleAccessFineLocationNotGranted() fun handleMinimumSDKVersionNotMet(minimumSdkVersion: Int) + + fun onConnectionInfoAvailable(info: WifiP2pInfo, wifiP2pGroup: WifiP2pGroup?) } diff --git a/p2p-lib/src/main/java/org/smartregister/p2p/search/contract/P2pModeSelectContract.kt b/p2p-lib/src/main/java/org/smartregister/p2p/search/contract/P2pModeSelectContract.kt index dfd0057d..7e46a041 100644 --- a/p2p-lib/src/main/java/org/smartregister/p2p/search/contract/P2pModeSelectContract.kt +++ b/p2p-lib/src/main/java/org/smartregister/p2p/search/contract/P2pModeSelectContract.kt @@ -16,11 +16,49 @@ package org.smartregister.p2p.search.contract import org.smartregister.p2p.authentication.model.DeviceRole +import org.smartregister.p2p.data_sharing.DeviceInfo +import org.smartregister.p2p.data_sharing.Manifest +import org.smartregister.p2p.model.P2PReceivedHistory +import org.smartregister.p2p.payload.PayloadContract +import org.smartregister.p2p.payload.StringPayload /** Interface for functions used to make changes to the data transfer page UI */ interface P2pModeSelectContract { - fun showP2PSelectPage(deviceRole: DeviceRole, deviceName: String) + interface View { - fun getDeviceRole(): DeviceRole + fun showP2PSelectPage(deviceRole: DeviceRole, deviceName: String) + + fun getDeviceRole(): DeviceRole + + fun showTransferCompleteDialog() + + fun getCurrentConnectedDevice(): DeviceInfo? + + fun senderSyncComplete(complete: Boolean) + } + + interface SenderViewModel { + + fun sendManifest(manifest: Manifest) + + fun getCurrentConnectedDevice(): DeviceInfo? + + fun processReceivedHistory(syncPayload: StringPayload) + + fun requestSyncParams(deviceInfo: DeviceInfo?) + + fun sendSyncComplete() + + fun sendChunkData(awaitingPayload: PayloadContract) + } + + interface ReceiverViewModel { + + fun getSendingDeviceAppLifetimeKey(): String + + fun updateProgress(resStringMsg: Int, recordSize: Int) + + fun sendLastReceivedRecords(receivedHistory: List?) + } } diff --git a/p2p-lib/src/main/java/org/smartregister/p2p/SocketReceiverSession.kt b/p2p-lib/src/main/java/org/smartregister/p2p/search/data/JsonData.kt similarity index 53% rename from p2p-lib/src/main/java/org/smartregister/p2p/SocketReceiverSession.kt rename to p2p-lib/src/main/java/org/smartregister/p2p/search/data/JsonData.kt index cf72bf1b..2cfe2f70 100644 --- a/p2p-lib/src/main/java/org/smartregister/p2p/SocketReceiverSession.kt +++ b/p2p-lib/src/main/java/org/smartregister/p2p/search/data/JsonData.kt @@ -13,23 +13,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.smartregister.p2p +package org.smartregister.p2p.search.data -import android.util.Log -import java.net.Socket -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.json.Json +import org.json.JSONArray -class SocketReceiverSession(private val socket: Socket) : ReceiverSession { - override fun receive() { - val reader = socket.getInputStream().bufferedReader() - reader.forEachLine { - val decoded = Json.decodeFromString(it) - Log.d(this::class.simpleName, """Message received: $decoded""") - } +class JsonData { + private var jsonArray: JSONArray? = null + private var highestRecordId: Long = 0 + + constructor(jsonArray: JSONArray?, highestRecordId: Long) { + this.jsonArray = jsonArray + this.highestRecordId = highestRecordId } -} -interface ReceiverSession { - fun receive() + constructor() + + fun getJsonArray(): JSONArray? { + return jsonArray + } + + fun getHighestRecordId(): Long { + return highestRecordId + } } diff --git a/p2p-lib/src/main/java/org/smartregister/p2p/search/ui/P2PDeviceSearchActivity.kt b/p2p-lib/src/main/java/org/smartregister/p2p/search/ui/P2PDeviceSearchActivity.kt index eaabd4c3..0a547df0 100644 --- a/p2p-lib/src/main/java/org/smartregister/p2p/search/ui/P2PDeviceSearchActivity.kt +++ b/p2p-lib/src/main/java/org/smartregister/p2p/search/ui/P2PDeviceSearchActivity.kt @@ -16,44 +16,47 @@ package org.smartregister.p2p.search.ui import android.Manifest -import android.content.BroadcastReceiver -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context import android.content.Intent -import android.content.IntentFilter +import android.content.IntentSender import android.content.pm.PackageManager -import android.net.wifi.p2p.WifiP2pConfig -import android.net.wifi.p2p.WifiP2pDevice -import android.net.wifi.p2p.WifiP2pDeviceList -import android.net.wifi.p2p.WifiP2pInfo -import android.net.wifi.p2p.WifiP2pManager import android.os.Build import android.os.Bundle -import android.provider.Settings import android.view.Menu import android.view.MenuItem import android.view.View +import android.view.WindowManager import android.widget.Button import android.widget.ImageButton import android.widget.ImageView import android.widget.TextView import android.widget.Toast +import androidx.activity.viewModels import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.app.ActivityCompat import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import com.google.android.gms.common.api.ResolvableApiException +import com.google.android.gms.location.LocationRequest +import com.google.android.gms.location.LocationServices +import com.google.android.gms.location.LocationSettingsRequest +import com.google.android.gms.location.LocationSettingsResponse +import com.google.android.gms.tasks.OnFailureListener +import com.google.android.gms.tasks.OnSuccessListener import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.snackbar.Snackbar +import org.smartregister.p2p.P2PLibrary import org.smartregister.p2p.R -import org.smartregister.p2p.WifiP2pBroadcastReceiver import org.smartregister.p2p.authentication.model.DeviceRole +import org.smartregister.p2p.data_sharing.DataSharingStrategy +import org.smartregister.p2p.data_sharing.DeviceInfo +import org.smartregister.p2p.data_sharing.OnDeviceFound import org.smartregister.p2p.search.adapter.DeviceListAdapter -import org.smartregister.p2p.search.contract.P2PManagerListener import org.smartregister.p2p.search.contract.P2pModeSelectContract +import org.smartregister.p2p.utils.DefaultDispatcherProvider import org.smartregister.p2p.utils.getDeviceName +import org.smartregister.p2p.utils.isAppDebuggable import org.smartregister.p2p.utils.startP2PScreen import timber.log.Timber @@ -61,56 +64,191 @@ import timber.log.Timber * This is the exposed activity that provides access to all P2P operations and steps. It can be * called from other apps via [startP2PScreen] function. */ -class P2PDeviceSearchActivity : AppCompatActivity(), P2PManagerListener, P2pModeSelectContract { +class P2PDeviceSearchActivity : AppCompatActivity(), P2pModeSelectContract.View { - private val wifiP2pManager: WifiP2pManager by lazy(LazyThreadSafetyMode.NONE) { - getSystemService(Context.WIFI_P2P_SERVICE) as WifiP2pManager - } - private var wifiP2pChannel: WifiP2pManager.Channel? = null - private var wifiP2pReceiver: BroadcastReceiver? = null private val accessFineLocationPermissionRequestInt: Int = 12345 + private val p2PReceiverViewModel by viewModels { + P2PReceiverViewModel.Factory( + context = this, + dataSharingStrategy = dataSharingStrategy, + DefaultDispatcherProvider() + ) + } + private val p2PSenderViewModel by viewModels { + P2PSenderViewModel.Factory( + context = this, + dataSharingStrategy = dataSharingStrategy, + DefaultDispatcherProvider() + ) + } private var isSender = false private var scanning = false - private lateinit var interactiveDialog: BottomSheetDialog + private var isSenderSyncComplete = false + internal lateinit var interactiveDialog: BottomSheetDialog + private var currentConnectedDevice: DeviceInfo? = null + + private lateinit var dataSharingStrategy: DataSharingStrategy + + private var keepScreenOnCounter = 0 private val rootView: View by lazy { findViewById(R.id.device_search_root_layout) } + val REQUEST_CHECK_LOCATION_ENABLED = 2398 + var requestDisconnection = false + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_p2_pdevice_search) - if (Timber.treeCount == 0) { + if (Timber.treeCount == 0 && isAppDebuggable(this)) { Timber.plant(Timber.DebugTree()) } title = getString(R.string.device_to_device_sync) supportActionBar?.setHomeAsUpIndicator(android.R.drawable.ic_menu_close_clear_cancel) + // Remaining setup for the DataSharingStrategy class + dataSharingStrategy = P2PLibrary.getInstance().dataSharingStrategy + dataSharingStrategy.setActivity(this) + findViewById