From 18b9e9a825dafdb957d35bc64512d79d9a4ec6cc Mon Sep 17 00:00:00 2001 From: wafa lakhal <4533152+wafalakhal@users.noreply.github.com> Date: Tue, 25 Mar 2025 19:49:23 +0100 Subject: [PATCH 001/153] peer to peer navigation (#372) * add nearby share option * add send and receive file view * wip on add nearbysharing navigation and start fragment * fix pr comments * fix pr comments * fix PR comments --- mobile/src/main/AndroidManifest.xml | 5 +- .../tella/mobile/domain/entity/ServerType.kt | 2 +- .../peertopeer/StartNearBySharingFragment.kt | 49 ++++++++++ .../peertopeer/activity/PeerToPeerActivity.kt | 17 ++++ .../adapters/connections/ServerViewHolder.kt | 14 +++ .../fragment/vault/home/HomeVaultFragment.kt | 5 + mobile/src/main/res/drawable/ic_share.xml | 10 ++ mobile/src/main/res/drawable/ic_share_big.xml | 10 ++ .../main/res/layout/activity_peer_to_peer.xml | 43 +++++++++ .../res/layout/item_home_vault_server.xml | 56 +++++++----- .../layout/start_near_by_sharing_fragment.xml | 91 +++++++++++++++++++ mobile/src/main/res/navigation/home.xml | 11 +++ .../res/navigation/peer_to_peer_graph.xml | 13 +++ mobile/src/main/res/values/strings.xml | 11 +++ 14 files changed, 312 insertions(+), 25 deletions(-) create mode 100644 mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/StartNearBySharingFragment.kt create mode 100644 mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/activity/PeerToPeerActivity.kt create mode 100644 mobile/src/main/res/drawable/ic_share.xml create mode 100644 mobile/src/main/res/drawable/ic_share_big.xml create mode 100644 mobile/src/main/res/layout/activity_peer_to_peer.xml create mode 100644 mobile/src/main/res/layout/start_near_by_sharing_fragment.xml create mode 100644 mobile/src/main/res/navigation/peer_to_peer_graph.xml diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index cc549d1b0..6cc4674bf 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -662,7 +662,10 @@ android:name=".views.dialog.nextcloud.NextCloudLoginFlowActivity" android:configChanges="keyboardHidden|orientation|screenSize" android:theme="@style/AppTheme.NoActionBar" /> - + diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/domain/entity/ServerType.kt b/mobile/src/main/java/org/horizontal/tella/mobile/domain/entity/ServerType.kt index 31466c64b..5946e5bea 100644 --- a/mobile/src/main/java/org/horizontal/tella/mobile/domain/entity/ServerType.kt +++ b/mobile/src/main/java/org/horizontal/tella/mobile/domain/entity/ServerType.kt @@ -1,5 +1,5 @@ package org.horizontal.tella.mobile.domain.entity enum class ServerType { - UNKNOWN, ODK_COLLECT, TELLA_UPLOAD, TELLA_RESORCES, UWAZI, GOOGLE_DRIVE, DROP_BOX, NEXTCLOUD, + UNKNOWN, ODK_COLLECT, TELLA_UPLOAD, TELLA_RESORCES, UWAZI, GOOGLE_DRIVE, DROP_BOX, NEXTCLOUD,PEERTOPEER } \ No newline at end of file diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/StartNearBySharingFragment.kt b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/StartNearBySharingFragment.kt new file mode 100644 index 000000000..3ebe4c6e7 --- /dev/null +++ b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/StartNearBySharingFragment.kt @@ -0,0 +1,49 @@ +package org.horizontal.tella.mobile.views.fragment.peertopeer + +import android.os.Bundle +import android.view.View +import androidx.core.view.isVisible +import org.horizontal.tella.mobile.R +import org.horizontal.tella.mobile.databinding.StartNearBySharingFragmentBinding +import org.horizontal.tella.mobile.util.Util +import org.horizontal.tella.mobile.util.hide +import org.horizontal.tella.mobile.views.base_ui.BaseBindingFragment + +class StartNearBySharingFragment : BaseBindingFragment( + StartNearBySharingFragmentBinding::inflate +) { + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initViews() + } + + private fun initViews() { + binding?.apply { + nextBtn.hide() + + learnMoreTextView.setOnClickListener { + baseActivity.maybeChangeTemporaryTimeout() + Util.startBrowserIntent(context, getString(R.string.peerToPeer_documentation_url)) + } + + sendFilesBtn.setOnClickListener { selectOption(true) } + receiveFilesBtn.setOnClickListener { selectOption(false) } + nextBtn.setOnClickListener { onNextClicked() } + } + } + private fun selectOption(isSend: Boolean) { + binding?.apply { + sendFilesBtn.isChecked = isSend + receiveFilesBtn.isChecked = !isSend + nextBtn.isVisible = true + } + } + + private fun onNextClicked() { + // TODO: Implement navigation on "Next" + } + +} + + diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/activity/PeerToPeerActivity.kt b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/activity/PeerToPeerActivity.kt new file mode 100644 index 000000000..81f8f350c --- /dev/null +++ b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/activity/PeerToPeerActivity.kt @@ -0,0 +1,17 @@ +package org.horizontal.tella.mobile.views.fragment.peertopeer.activity + +import android.os.Bundle +import org.horizontal.tella.mobile.databinding.ActivityPeerToPeerBinding +import org.horizontal.tella.mobile.views.base_ui.BaseLockActivity + +class PeerToPeerActivity : BaseLockActivity() { + private lateinit var binding: ActivityPeerToPeerBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityPeerToPeerBinding.inflate(layoutInflater) + setContentView(binding.getRoot()) + binding.toolbar.backClickListener = { this.onBackPressed() } + + } +} \ No newline at end of file diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/vault/adapters/connections/ServerViewHolder.kt b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/vault/adapters/connections/ServerViewHolder.kt index 7dcb67d74..36b117ea0 100644 --- a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/vault/adapters/connections/ServerViewHolder.kt +++ b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/vault/adapters/connections/ServerViewHolder.kt @@ -1,5 +1,6 @@ package org.horizontal.tella.mobile.views.fragment.vault.adapters.connections +import android.text.TextUtils import android.view.View import android.view.ViewGroup import android.widget.ImageView @@ -14,9 +15,12 @@ import org.horizontal.tella.mobile.views.fragment.vault.adapters.viewholders.bas class ServerViewHolder(val view: View) : BaseViewHolder(view) { private lateinit var reportTypeTextView: TextView private lateinit var reportTypeImg: ImageView + private lateinit var twoLinesReportTypeTextView: TextView + override fun bind(item: ServerDataItem, vaultClickListener: VaultClickListener) { reportTypeTextView = view.findViewById(R.id.server_name_textView) + twoLinesReportTypeTextView = view.findViewById(R.id.two_line_server_name_textView) reportTypeImg = view.findViewById(R.id.server_img) // Set the default padding @@ -94,6 +98,16 @@ class ServerViewHolder(val view: View) : BaseViewHolder(view) { ) ) } + ServerType.PEERTOPEER -> { + twoLinesReportTypeTextView.text = view.context.getText(R.string.NearBySharing) + reportTypeImg.setImageDrawable( + ResourcesCompat.getDrawable( + view.resources, + R.drawable.ic_share, + null + ) + ) + } else -> { // todo create default server type } } diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/vault/home/HomeVaultFragment.kt b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/vault/home/HomeVaultFragment.kt index 4b1ba9d2a..4d6f5fd22 100644 --- a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/vault/home/HomeVaultFragment.kt +++ b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/vault/home/HomeVaultFragment.kt @@ -463,6 +463,10 @@ class HomeVaultFragment : BaseFragment(), VaultClickListener { nav().navigate(R.id.action_homeScreen_to_next_cloud_screen) } + ServerType.PEERTOPEER -> { + nav().navigate(R.id.action_homeScreen_to_peerToPeer_screen) + } + else -> {} } } @@ -745,6 +749,7 @@ class HomeVaultFragment : BaseFragment(), VaultClickListener { * This function show connections when all the server types are counted. **/ private fun maybeShowConnections() { + serversList?.add(ServerDataItem(ArrayList(), ServerType.PEERTOPEER)) // If the serversList is not empty, check if it has changed if (serversList?.isEmpty() == false) { // Use the vaultAdapter to check existing connections diff --git a/mobile/src/main/res/drawable/ic_share.xml b/mobile/src/main/res/drawable/ic_share.xml new file mode 100644 index 000000000..1938144ea --- /dev/null +++ b/mobile/src/main/res/drawable/ic_share.xml @@ -0,0 +1,10 @@ + + + diff --git a/mobile/src/main/res/drawable/ic_share_big.xml b/mobile/src/main/res/drawable/ic_share_big.xml new file mode 100644 index 000000000..fd3222f58 --- /dev/null +++ b/mobile/src/main/res/drawable/ic_share_big.xml @@ -0,0 +1,10 @@ + + + diff --git a/mobile/src/main/res/layout/activity_peer_to_peer.xml b/mobile/src/main/res/layout/activity_peer_to_peer.xml new file mode 100644 index 000000000..098bc4fc4 --- /dev/null +++ b/mobile/src/main/res/layout/activity_peer_to_peer.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + diff --git a/mobile/src/main/res/layout/item_home_vault_server.xml b/mobile/src/main/res/layout/item_home_vault_server.xml index f216f9e01..a2fb5e027 100644 --- a/mobile/src/main/res/layout/item_home_vault_server.xml +++ b/mobile/src/main/res/layout/item_home_vault_server.xml @@ -1,5 +1,6 @@ - - + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toTopOf="@id/server_name_textView" + tools:src="@drawable/ic_google_drive_white" /> - + - - + - \ No newline at end of file + diff --git a/mobile/src/main/res/layout/start_near_by_sharing_fragment.xml b/mobile/src/main/res/layout/start_near_by_sharing_fragment.xml new file mode 100644 index 000000000..9de4a3bf7 --- /dev/null +++ b/mobile/src/main/res/layout/start_near_by_sharing_fragment.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mobile/src/main/res/navigation/home.xml b/mobile/src/main/res/navigation/home.xml index 451360d73..1e1ca86f9 100644 --- a/mobile/src/main/res/navigation/home.xml +++ b/mobile/src/main/res/navigation/home.xml @@ -65,6 +65,11 @@ app:destination="@id/next_cloud_graph" app:enterAnim="@anim/slide_in_start" app:exitAnim="@anim/fade_out" /> + + + diff --git a/mobile/src/main/res/navigation/peer_to_peer_graph.xml b/mobile/src/main/res/navigation/peer_to_peer_graph.xml new file mode 100644 index 000000000..df4dd7c20 --- /dev/null +++ b/mobile/src/main/res/navigation/peer_to_peer_graph.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml index ab332ec15..8132cdc06 100644 --- a/mobile/src/main/res/values/strings.xml +++ b/mobile/src/main/res/values/strings.xml @@ -1003,4 +1003,15 @@ Would you like to share analytics with the Tella team? Delete report? Your report have successfully been submitted. Attach file + Nearby sharing + + Share files with nearby devices without an Internet connection\n 1. Both devices must be connected to the same Wifi network\n 2. Make sure you trust the person you are sharing files with + + Send files + Receive files + Learn more about nearby sharing + Nearby sharing + How it works + https://tella-app.org/nearby-sharing/ + From 0e0b53a79303b49ac181360e6e48242655cde2e3 Mon Sep 17 00:00:00 2001 From: wafa lakhal <4533152+wafalakhal@users.noreply.github.com> Date: Wed, 21 May 2025 11:44:21 +0100 Subject: [PATCH 002/153] peerToPeer register and prepare upload implementation (#375) * WIP qr code design * WIP qr code generation * add dual text checkbox custom view * WIP add show device info design * WIP device info ligic * WIP verification info design * show device wifi hotspot * wip complete hotspot logic/design * add navigation to connect screen * fix design * fix design * Fix hotspot name * WIP generate certificate * Fix certificate generation * WIP generate code and start server * Finish QrCode generation * Finish scan and start qr code scanning * add register endpoint logic in the server side * change endpoint naming * wip register from the sender side * wip register api * handle back button * refactor code * WIP code refactor * wip implement prepare upload * WIP refactor * handle register response * Fix serialization problem * wip fix server issue * Fix ktor parsin * add file * wip prepare upload handle happy path api --------- Co-authored-by: Ahlem --- build.gradle | 1 + mobile/build.gradle | 16 +- mobile/proguard-rules.pro | 5 +- mobile/src/main/AndroidManifest.xml | 7 +- mobile/src/main/assets/testDemo.txt | 1 + .../tella/mobile/MyApplication.java | 18 +- .../certificate/CertificateGenerator.kt | 68 ++++++ .../mobile/certificate/CertificateUtils.kt | 85 +++++++ .../tella/mobile/data/peertopeer/FileItem.kt | 12 + .../data/peertopeer/PrepareUploadRequest.kt | 10 + .../data/peertopeer/TellaPeerToPeerClient.kt | 183 +++++++++++++++ .../data/peertopeer/TellaPeerToPeerServer.kt | 138 +++++++++++ .../entity/peertopeer/DeviceMetadata.kt | 12 + .../domain/entity/peertopeer/QRCodeInfos.kt | 15 ++ .../domain/peertopeer/KeyStoreConfig.kt | 26 +++ .../peertopeer/PeerConnectionPayload.kt | 18 ++ .../peertopeer/PeerPrepareUploadResponse.kt | 8 + .../domain/peertopeer/PeerRegisterPayload.kt | 10 + .../mobile/domain/peertopeer/PeerResponse.kt | 8 + .../mobile/domain/peertopeer/TellaServer.kt | 7 + .../tella/mobile/util/NavigationManager.kt | 19 +- .../mobile/views/fragment/CameraFragment.kt | 42 ---- .../mobile/views/fragment/ReportsFragment.kt | 58 ----- .../peertopeer/PeerToPeerViewModel.kt | 220 ++++++++++++++++++ .../peertopeer/StartNearBySharingFragment.kt | 26 ++- .../peertopeer/activity/PeerToPeerActivity.kt | 2 - .../peertopeer/data/ConnectionType.kt | 5 + .../fragment/peertopeer/data/NetWorkInfo.kt | 8 + .../fragment/peertopeer/di/PeerModule.kt | 19 ++ .../receipentflow/ConnectHotspotFragment.kt | 83 +++++++ .../receipentflow/QRCodeFragment.kt | 89 +++++++ .../receipentflow/ShowDeviceInfoFragment.kt | 18 ++ .../receipentflow/VerificationInfoFragment.kt | 4 + .../senderflow/ScanQrCodeFragment.kt | 111 +++++++++ .../mobile/views/settings/LanguageSettings.kt | 2 +- mobile/src/main/res/drawable/icon_wifi.xml | 13 ++ .../src/main/res/drawable/qr_scan_border.xml | 10 + .../main/res/layout/activity_peer_to_peer.xml | 22 +- .../res/layout/connect_hotspot_layout.xml | 134 +++++++++++ .../src/main/res/layout/fragment_qr_code.xml | 91 ++++++++ mobile/src/main/res/layout/fragment_vault.xml | 1 + .../main/res/layout/peer_to_peer_layout.xml | 8 + .../main/res/layout/scan_qrcode_fragment.xml | 88 +++++++ .../res/layout/show_device_info_layout.xml | 113 +++++++++ .../layout/start_near_by_sharing_fragment.xml | 36 ++- .../res/layout/verification_info_fragment.xml | 116 +++++++++ .../res/navigation/peer_to_peer_graph.xml | 38 ++- mobile/src/main/res/values/strings.xml | 23 ++ .../shared_ui/buttons/DualTextCheckView.kt | 90 +++++++ .../hzontal/shared_ui/buttons/RoundButton.kt | 29 +-- .../main/res/drawable/bg_dual_text_check.xml | 16 ++ shared-ui/src/main/res/drawable/device.xml | 24 ++ .../res/layout-hdpi/information_button.xml | 4 +- .../res/layout-xhdpi/information_button.xml | 4 +- .../res/layout/dual_text_check_layout.xml | 53 +++++ shared-ui/src/main/res/values/attrs.xml | 7 + shared-ui/src/main/res/values/styles.xml | 4 + tella-keys/build.gradle | 5 + .../tella/keys/key/LifecycleMainKey.java | 4 - 59 files changed, 2114 insertions(+), 173 deletions(-) create mode 100644 mobile/src/main/assets/testDemo.txt create mode 100644 mobile/src/main/java/org/horizontal/tella/mobile/certificate/CertificateGenerator.kt create mode 100644 mobile/src/main/java/org/horizontal/tella/mobile/certificate/CertificateUtils.kt create mode 100644 mobile/src/main/java/org/horizontal/tella/mobile/data/peertopeer/FileItem.kt create mode 100644 mobile/src/main/java/org/horizontal/tella/mobile/data/peertopeer/PrepareUploadRequest.kt create mode 100644 mobile/src/main/java/org/horizontal/tella/mobile/data/peertopeer/TellaPeerToPeerClient.kt create mode 100644 mobile/src/main/java/org/horizontal/tella/mobile/data/peertopeer/TellaPeerToPeerServer.kt create mode 100644 mobile/src/main/java/org/horizontal/tella/mobile/domain/entity/peertopeer/DeviceMetadata.kt create mode 100644 mobile/src/main/java/org/horizontal/tella/mobile/domain/entity/peertopeer/QRCodeInfos.kt create mode 100644 mobile/src/main/java/org/horizontal/tella/mobile/domain/peertopeer/KeyStoreConfig.kt create mode 100644 mobile/src/main/java/org/horizontal/tella/mobile/domain/peertopeer/PeerConnectionPayload.kt create mode 100644 mobile/src/main/java/org/horizontal/tella/mobile/domain/peertopeer/PeerPrepareUploadResponse.kt create mode 100644 mobile/src/main/java/org/horizontal/tella/mobile/domain/peertopeer/PeerRegisterPayload.kt create mode 100644 mobile/src/main/java/org/horizontal/tella/mobile/domain/peertopeer/PeerResponse.kt create mode 100644 mobile/src/main/java/org/horizontal/tella/mobile/domain/peertopeer/TellaServer.kt delete mode 100644 mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/CameraFragment.kt delete mode 100644 mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/ReportsFragment.kt create mode 100644 mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/PeerToPeerViewModel.kt create mode 100644 mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/data/ConnectionType.kt create mode 100644 mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/data/NetWorkInfo.kt create mode 100644 mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/di/PeerModule.kt create mode 100644 mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/receipentflow/ConnectHotspotFragment.kt create mode 100644 mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/receipentflow/QRCodeFragment.kt create mode 100644 mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/receipentflow/ShowDeviceInfoFragment.kt create mode 100644 mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/receipentflow/VerificationInfoFragment.kt create mode 100644 mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/senderflow/ScanQrCodeFragment.kt create mode 100644 mobile/src/main/res/drawable/icon_wifi.xml create mode 100644 mobile/src/main/res/drawable/qr_scan_border.xml create mode 100644 mobile/src/main/res/layout/connect_hotspot_layout.xml create mode 100644 mobile/src/main/res/layout/fragment_qr_code.xml create mode 100644 mobile/src/main/res/layout/peer_to_peer_layout.xml create mode 100644 mobile/src/main/res/layout/scan_qrcode_fragment.xml create mode 100644 mobile/src/main/res/layout/show_device_info_layout.xml create mode 100644 mobile/src/main/res/layout/verification_info_fragment.xml create mode 100644 shared-ui/src/main/java/org/hzontal/shared_ui/buttons/DualTextCheckView.kt create mode 100644 shared-ui/src/main/res/drawable/bg_dual_text_check.xml create mode 100644 shared-ui/src/main/res/drawable/device.xml create mode 100644 shared-ui/src/main/res/layout/dual_text_check_layout.xml diff --git a/build.gradle b/build.gradle index fcee3f3e7..2bacd942a 100644 --- a/build.gradle +++ b/build.gradle @@ -102,6 +102,7 @@ buildscript { classpath "com.google.firebase:firebase-crashlytics-gradle:$versions.crashlyticsGradle" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin" classpath 'com.google.dagger:hilt-android-gradle-plugin:2.50' + classpath "org.jetbrains.kotlin:kotlin-serialization:$versions.kotlin" } } diff --git a/mobile/build.gradle b/mobile/build.gradle index e452de71a..29bb8a07c 100644 --- a/mobile/build.gradle +++ b/mobile/build.gradle @@ -5,6 +5,7 @@ plugins { id 'dagger.hilt.android.plugin' id 'com.google.firebase.crashlytics' id 'com.google.gms.google-services' + id 'org.jetbrains.kotlin.plugin.serialization' } android { @@ -88,7 +89,7 @@ android { } packagingOptions { exclude 'META-INF/DEPENDENCIES' - + exclude 'META-INF/versions/9/OSGI-INF/MANIFEST.MF' } namespace 'org.horizontal.tella.mobile' } @@ -233,6 +234,19 @@ dependencies { implementation "com.github.nextcloud-deps.hwsecurity:hwsecurity-fido:$versions.fidoVersion" implementation "com.github.nextcloud-deps.hwsecurity:hwsecurity-fido2:$versions.fidoVersion" + //QR CODE SCAN/GENERATE LIBRARY + implementation 'com.journeyapps:zxing-android-embedded:4.3.0' + + //ktor imp + implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.9") + implementation("io.ktor:ktor-server-core:2.3.9") + implementation("io.ktor:ktor-server-netty:2.3.9") + implementation("io.ktor:ktor-server-content-negotiation:2.3.9") + implementation("io.ktor:ktor-server-cio:2.3.9") + + implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3' // Use the latest version + + } def getLocalProperties() { diff --git a/mobile/proguard-rules.pro b/mobile/proguard-rules.pro index d114ab925..acb937b3a 100644 --- a/mobile/proguard-rules.pro +++ b/mobile/proguard-rules.pro @@ -246,4 +246,7 @@ -if class androidx.credentials.CredentialManager -keep class androidx.credentials.playservices.** { *; -} \ No newline at end of file +} + +-keep class org.bouncycastle.** { *; } +-dontwarn org.bouncycastle.** \ No newline at end of file diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index 6cc4674bf..428d36e0d 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -49,9 +49,9 @@ android:icon="@mipmap/tella_icon" android:label="@string/app_name" android:largeHeap="true" + android:localeConfig="@xml/locales_config" android:networkSecurityConfig="@xml/configure_localhost_media_file_http_server" android:roundIcon="@mipmap/tella_icon_round" - android:localeConfig="@xml/locales_config" android:supportsRtl="true" android:theme="@style/AppTheme" android:usesCleartextTraffic="true" @@ -701,6 +701,11 @@ + + \ No newline at end of file diff --git a/mobile/src/main/assets/testDemo.txt b/mobile/src/main/assets/testDemo.txt new file mode 100644 index 000000000..f6983e619 --- /dev/null +++ b/mobile/src/main/assets/testDemo.txt @@ -0,0 +1 @@ + This is a test yo \ No newline at end of file diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/MyApplication.java b/mobile/src/main/java/org/horizontal/tella/mobile/MyApplication.java index 509ff254b..b7a6fc3d9 100644 --- a/mobile/src/main/java/org/horizontal/tella/mobile/MyApplication.java +++ b/mobile/src/main/java/org/horizontal/tella/mobile/MyApplication.java @@ -11,6 +11,7 @@ import android.os.Build; import android.os.StrictMode; import android.os.Bundle; +import android.util.Log; import android.widget.Toast; import androidx.annotation.NonNull; @@ -36,6 +37,7 @@ import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.resources.status.OwnCloudVersion; +import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.hzontal.shared_ui.data.CommonPrefs; import org.conscrypt.Conscrypt; @@ -53,6 +55,7 @@ import java.io.File; import java.lang.ref.WeakReference; import java.security.NoSuchAlgorithmException; +import java.security.Provider; import java.security.Security; import java.util.Arrays; @@ -63,6 +66,7 @@ import dagger.hilt.android.HiltAndroidApp; import io.reactivex.functions.Consumer; import io.reactivex.plugins.RxJavaPlugins; + import org.horizontal.tella.mobile.bus.TellaBus; import org.horizontal.tella.mobile.data.database.KeyDataSource; import org.horizontal.tella.mobile.data.nextcloud.TempFileManager; @@ -99,8 +103,6 @@ public class MyApplication extends MultiDexApplication implements IUnlockRegistr Vault.Config vaultConfig; private static final String TAG = MyApplication.class.getSimpleName(); public static final String DOT = "."; - public static final OwnCloudVersion MINIMUM_SUPPORTED_SERVER_VERSION = OwnCloudVersion.nextcloud_17; - private static WeakReference appContext; private long startTime; private long totalTimeSpent = 0; // Store total time spent in the app private int activityReferences = 0; @@ -235,6 +237,7 @@ public void accept(Throwable throwable) { TellaKeysUI.initialize(mainKeyStore, mainKeyHolder, unlockRegistry, this, Preferences.getFailedUnlockOption(), Preferences.getUnlockRemainingAttempts(), Preferences.isShowUnlockRemainingAttempts()); insertConscrypt(); enableStrictMode(); + setupBouncyCastleProvider(); } private void configureCrashlytics() { @@ -437,4 +440,15 @@ private void enableStrictMode() { } + private void setupBouncyCastleProvider() { + try { + if (Security.getProvider("BC") == null) { + Security.addProvider(new BouncyCastleProvider()); + Timber.i("BouncyCastle provider registered"); + } + } catch (Exception e) { + Timber.e(e, "Failed to register BouncyCastle provider"); + } + } + } diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/certificate/CertificateGenerator.kt b/mobile/src/main/java/org/horizontal/tella/mobile/certificate/CertificateGenerator.kt new file mode 100644 index 000000000..766bcd477 --- /dev/null +++ b/mobile/src/main/java/org/horizontal/tella/mobile/certificate/CertificateGenerator.kt @@ -0,0 +1,68 @@ +package org.horizontal.tella.mobile.certificate + +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder +import java.security.KeyPair +import java.security.KeyPairGenerator +import java.security.Security +import java.security.cert.X509Certificate +import java.util.Date + +object CertificateGenerator { + + init { + if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { + Security.addProvider(BouncyCastleProvider()) + } + } + + fun generateCertificate( + commonName: String = "Tella Android", + organization: String = "Tella", + validityDays: Int = 365, + ipAddress: String + ): Pair { + val keyGen = KeyPairGenerator.getInstance("RSA") + keyGen.initialize(2048) + val keyPair = keyGen.generateKeyPair() + + val now = Date() + val validTo = Date(now.time + validityDays * 86400000L) + + val issuer = X500Name("CN=$commonName,O=$organization") + val subject = issuer + + val serialNumber = CertificateUtils.generateSerialNumber() + + val certBuilder = JcaX509v3CertificateBuilder( + issuer, serialNumber, now, validTo, subject, keyPair.public + ) + + if (ipAddress.isNotEmpty()) { + val san = org.bouncycastle.asn1.x509.GeneralNames( + org.bouncycastle.asn1.x509.GeneralName( + org.bouncycastle.asn1.x509.GeneralName.iPAddress, ipAddress + ) + ) + certBuilder.addExtension( + org.bouncycastle.asn1.x509.Extension.subjectAlternativeName, + false, + san + ) + } + + val signer = JcaContentSignerBuilder("SHA256withRSA") + .build(keyPair.private) + + val holder = certBuilder.build(signer) + + val certificate = JcaX509CertificateConverter() + .getCertificate(holder) + + return Pair(keyPair, certificate) + } + +} \ No newline at end of file diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/certificate/CertificateUtils.kt b/mobile/src/main/java/org/horizontal/tella/mobile/certificate/CertificateUtils.kt new file mode 100644 index 000000000..53cb67a5e --- /dev/null +++ b/mobile/src/main/java/org/horizontal/tella/mobile/certificate/CertificateUtils.kt @@ -0,0 +1,85 @@ +package org.horizontal.tella.mobile.certificate + +import android.util.Base64 +import java.math.BigInteger +import java.security.KeyPair +import java.security.SecureRandom +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import java.security.KeyStore +import java.util.UUID +import javax.net.ssl.KeyManagerFactory +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManagerFactory +import javax.net.ssl.X509TrustManager + +object CertificateUtils { + + fun generateSerialNumber(): BigInteger { + return BigInteger(128, SecureRandom()) + } + + fun certificateToPem(certificate: X509Certificate): String { + val encoded = Base64.encodeToString(certificate.encoded, Base64.NO_WRAP) + return "-----BEGIN CERTIFICATE-----\n$encoded\n-----END CERTIFICATE-----" + } + + fun pemToCertificate(pemString: String): X509Certificate { + val cleanPem = pemString + .replace("-----BEGIN CERTIFICATE-----", "") + .replace("-----END CERTIFICATE-----", "") + .replace("\\s".toRegex(), "") + val decoded = Base64.decode(cleanPem, Base64.DEFAULT) + val certFactory = CertificateFactory.getInstance("X.509") + return certFactory.generateCertificate(decoded.inputStream()) as X509Certificate + } + + fun encodeBase64(data: ByteArray): String = + Base64.encodeToString(data, Base64.NO_WRAP) + + fun decodeBase64(encodedString: String): ByteArray = + Base64.decode(encodedString, Base64.NO_WRAP) + + fun getPublicKeyHash(certificate: X509Certificate): String { + val digest = java.security.MessageDigest.getInstance("SHA-256") + val hash = digest.digest(certificate.publicKey.encoded) + return hash.joinToString("") { "%02x".format(it) } + } + + fun createSSLContext( + keyPair: KeyPair, + certificate: X509Certificate + ): SSLContext { + val keyStore = KeyStore.getInstance("PKCS12") + val keyPassword = UUID.randomUUID().toString() + keyStore.load(null, null) + keyStore.setKeyEntry( + "alias", + keyPair.private, + keyPassword.toCharArray(), + arrayOf(certificate) + ) + + val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) + kmf.init(keyStore, keyPassword.toCharArray()) + + val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + tmf.init(keyStore) + + val sslContext = SSLContext.getInstance("TLS") + sslContext.init(kmf.keyManagers, tmf.trustManagers, SecureRandom()) + + return sslContext + } + + fun getTrustManager(certificate: X509Certificate): X509TrustManager { + val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()) + keyStore.load(null, null) + keyStore.setCertificateEntry("alias", certificate) + + val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + tmf.init(keyStore) + + return tmf.trustManagers[0] as X509TrustManager + } +} \ No newline at end of file diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/data/peertopeer/FileItem.kt b/mobile/src/main/java/org/horizontal/tella/mobile/data/peertopeer/FileItem.kt new file mode 100644 index 000000000..1c0d3c2dd --- /dev/null +++ b/mobile/src/main/java/org/horizontal/tella/mobile/data/peertopeer/FileItem.kt @@ -0,0 +1,12 @@ +package org.horizontal.tella.mobile.data.peertopeer + +import kotlinx.serialization.Serializable + +@Serializable +data class FileItem( + val id: String, + val fileName: String, + val size: Long, + val fileType: String, + val sha256: String +) \ No newline at end of file diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/data/peertopeer/PrepareUploadRequest.kt b/mobile/src/main/java/org/horizontal/tella/mobile/data/peertopeer/PrepareUploadRequest.kt new file mode 100644 index 000000000..e6e8b9aa8 --- /dev/null +++ b/mobile/src/main/java/org/horizontal/tella/mobile/data/peertopeer/PrepareUploadRequest.kt @@ -0,0 +1,10 @@ +package org.horizontal.tella.mobile.data.peertopeer + +import kotlinx.serialization.Serializable + +@Serializable +data class PrepareUploadRequest( + val title: String, + val sessionId: String, + val files: List +) \ No newline at end of file diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/data/peertopeer/TellaPeerToPeerClient.kt b/mobile/src/main/java/org/horizontal/tella/mobile/data/peertopeer/TellaPeerToPeerClient.kt new file mode 100644 index 000000000..6345097b1 --- /dev/null +++ b/mobile/src/main/java/org/horizontal/tella/mobile/data/peertopeer/TellaPeerToPeerClient.kt @@ -0,0 +1,183 @@ +package org.horizontal.tella.mobile.data.peertopeer + +import android.util.Log +import kotlinx.serialization.encodeToString +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import org.horizontal.tella.mobile.certificate.CertificateUtils +import org.horizontal.tella.mobile.domain.peertopeer.PeerRegisterPayload +import org.json.JSONObject +import java.io.File +import java.security.SecureRandom +import java.security.cert.CertificateException +import java.security.cert.X509Certificate +import java.util.UUID +import java.util.concurrent.TimeUnit +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManager +import javax.net.ssl.X509TrustManager + +class TellaPeerToPeerClient { + + private fun getClientWithFingerprintValidation(expectedFingerprint: String): OkHttpClient { + val trustManager = object : X509TrustManager { + override fun getAcceptedIssuers(): Array = arrayOf() + + override fun checkClientTrusted(chain: Array, authType: String?) {} + + override fun checkServerTrusted(chain: Array, authType: String?) { + val serverCert = chain[0] + val actualFingerprint = CertificateUtils.getPublicKeyHash(serverCert) + + if (!actualFingerprint.equals(expectedFingerprint, ignoreCase = true)) { + throw CertificateException( + "Server certificate fingerprint mismatch.\nExpected: $expectedFingerprint\nGot: $actualFingerprint" + ) + } + } + } + + val sslContext = SSLContext.getInstance("TLS").apply { + init(null, arrayOf(trustManager), SecureRandom()) + } + + return OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .sslSocketFactory(sslContext.socketFactory, trustManager) + .build() + } + + suspend fun registerPeerDevice( + ip: String, + port: String, + expectedFingerprint: String, + pin: String + ): Result = withContext(Dispatchers.IO) { + val url = "https://$ip:$port/api/v1/register" + Log.d("PeerClient", "Connecting to: $url") + + try { + val payload = PeerRegisterPayload( + pin = pin, + nonce = UUID.randomUUID().toString() + ) + + val jsonPayload = Json.encodeToString(payload) + val requestBody = jsonPayload.toRequestBody("application/json".toMediaType()) + Log.d("PeerClient", "Request payload: $requestBody") + + val client = getClientWithFingerprintValidation(expectedFingerprint) + val request = Request.Builder() + .url(url) + .post(requestBody) + .addHeader("Content-Type", "application/json") + .build() + + client.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + val errorBody = response.body.string() + Log.e( + "PeerClient", """ + HTTP ${response.code} Error + URL: $url + Headers: ${response.headers} + Body: $errorBody + """.trimIndent() + ) + return@use Result.failure(Exception("HTTP ${response.code}: $errorBody")) + } + + val contentType = response.header("Content-Type") ?: "" + if (!contentType.contains("application/json")) { + Log.w("PeerClient", "Unexpected Content-Type: $contentType") + } + + val body = response.body?.string() ?: "" + return@use try { + val json = JSONObject(body) + Result.success(json.getString("sessionId")) + } catch (e: Exception) { + Result.failure(Exception("Invalid JSON response: ${e.message}")) + } + } + } catch (e: Exception) { + Log.e("PeerClient", "Request failed", e) + Result.failure(e) + } + } + + suspend fun prepareUpload( + ip: String, + port: String, + expectedFingerprint: String, + title: String = "Title of the report", + file: File, + fileId: String, + sha256: String, + sessionId: String + ): Result { + return withContext(Dispatchers.IO) { + val url = "https://$ip:$port/api/v1/prepare-upload" + + val fileItem = FileItem( + id = fileId, + fileName = file.name, + size = file.length(), + fileType = "application/octet-stream", + sha256 = sha256 + ) + + val requestPayload = PrepareUploadRequest( + title = title, + sessionId = sessionId, + files = listOf(fileItem) + ) + + val jsonPayload = Json.encodeToString(requestPayload) + val requestBody = jsonPayload.toRequestBody("application/json".toMediaType()) + Log.d("PeerClient", "Request payload: $requestBody") + val client = getClientWithFingerprintValidation(expectedFingerprint) + + try { + val request = Request.Builder() + .url(url) + .post(requestBody) + .addHeader("Content-Type", "application/json") + .build() + + client.newCall(request).execute().use { response -> + if (response.isSuccessful) { + val responseBody = response.body?.string() + responseBody?.let { + val jsonObject = JSONObject(it) + val transmissionId = jsonObject.getString("transmissionId") + Log.d("PrepareUpload", "Transmission ID: $transmissionId") + return@use Result.success(transmissionId) + } + } else { + Log.e("PrepareUpload", "Error ${response.code}: ${response.message}") + when (response.code) { + 409 -> { + Log.e("PrepareUpload", "Conflict: Try canceling active sessions.") + } + + else -> {} + } + } + } + Result.failure(Exception("Unsuccessful response")) + } catch (e: Exception) { + Log.e("PrepareUpload", "Exception: ${e.message}", e) + Result.failure(e) + } + } + } + +} \ No newline at end of file diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/data/peertopeer/TellaPeerToPeerServer.kt b/mobile/src/main/java/org/horizontal/tella/mobile/data/peertopeer/TellaPeerToPeerServer.kt new file mode 100644 index 000000000..3a56cb036 --- /dev/null +++ b/mobile/src/main/java/org/horizontal/tella/mobile/data/peertopeer/TellaPeerToPeerServer.kt @@ -0,0 +1,138 @@ +package org.horizontal.tella.mobile.data.peertopeer + +import android.util.Log +import com.google.gson.Gson +import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.serialization.kotlinx.json.json +import io.ktor.server.application.call +import io.ktor.server.application.install +import io.ktor.server.engine.ApplicationEngine +import io.ktor.server.engine.applicationEngineEnvironment +import io.ktor.server.engine.embeddedServer +import io.ktor.server.engine.sslConnector +import io.ktor.server.netty.Netty +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation +import io.ktor.server.request.receive +import io.ktor.server.response.respond +import io.ktor.server.response.respondText +import io.ktor.server.routing.get +import io.ktor.server.routing.post +import io.ktor.server.routing.routing +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import org.horizontal.tella.mobile.certificate.CertificateUtils +import org.horizontal.tella.mobile.domain.peertopeer.KeyStoreConfig +import org.horizontal.tella.mobile.domain.peertopeer.PeerPrepareUploadResponse +import org.horizontal.tella.mobile.domain.peertopeer.PeerRegisterPayload +import org.horizontal.tella.mobile.domain.peertopeer.PeerResponse +import org.horizontal.tella.mobile.domain.peertopeer.TellaServer +import java.security.KeyPair +import java.security.KeyStore +import java.security.cert.X509Certificate +import java.util.UUID +import kotlin.reflect.jvm.internal.impl.load.kotlin.JvmType + +const val port = 53317 + +class TellaPeerToPeerServer( + private val ip: String, + private val serverPort: Int = port, + private val keyPair: KeyPair, + private val certificate: X509Certificate, + private val keyStoreConfig: KeyStoreConfig +) : TellaServer { + + private var engine: ApplicationEngine? = null + + override val certificatePem: String + get() = CertificateUtils.certificateToPem(certificate) + + override fun start() { + val keyStore = KeyStore.getInstance("PKCS12").apply { + load(null, null) + setKeyEntry( + keyStoreConfig.alias, + keyPair.private, + keyStoreConfig.password, + arrayOf(certificate) + ) + } + + engine = embeddedServer(Netty, environment = applicationEngineEnvironment { + sslConnector( + keyStore = keyStore, + keyAlias = keyStoreConfig.alias, + keyStorePassword = { keyStoreConfig.password }, + privateKeyPassword = { keyStoreConfig.password } + ) { + this.host = ip + this.port = serverPort + } + + module { + install(ContentNegotiation) { + json() + } + routing { + // Root route to confirm the server is running + get("/") { + Log.i("Test", "Server started") + call.respondText("The server is running securely over HTTPS.") + } + + post("/api/v1/register") { + val request = try { + call.receive() + } catch (e: Exception) { + call.respondText( + """{"error": "Invalid request body"}""", + ContentType.Application.Json, + HttpStatusCode.BadRequest + ) + return@post + } + + val sessionId = UUID.randomUUID().toString() + call.respondText( + Gson().toJson(PeerResponse(sessionId)), + ContentType.Application.Json, + HttpStatusCode.OK + ) + } + + + post("/api/v1/prepare-upload") { + val request = try { + call.receive() + } catch (e: Exception) { + call.respondText("Invalid body", status = HttpStatusCode.BadRequest) + return@post + } + + if (request.title.isBlank() || request.sessionId.isBlank() || request.files.isEmpty()) { + call.respondText( + "Missing required fields", + status = HttpStatusCode.BadRequest + ) + return@post + } + + // we can process the files or store metadata here + val transmissionId = UUID.randomUUID().toString() + call.respondText( + Gson().toJson(PeerPrepareUploadResponse(transmissionId)), + ContentType.Application.Json, + HttpStatusCode.OK + ) + } + } + + } + }).start(wait = false) + } + + override fun stop() { + engine?.stop(1000, 5000) + } +} diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/domain/entity/peertopeer/DeviceMetadata.kt b/mobile/src/main/java/org/horizontal/tella/mobile/domain/entity/peertopeer/DeviceMetadata.kt new file mode 100644 index 000000000..90662f51a --- /dev/null +++ b/mobile/src/main/java/org/horizontal/tella/mobile/domain/entity/peertopeer/DeviceMetadata.kt @@ -0,0 +1,12 @@ +package org.horizontal.tella.mobile.domain.entity.peertopeer + +data class DeviceMetadata( + val deviceType: String = "mobile", + val version: String = "2.0", + val fingerprint: String, + val serverPort: Int = 53317, + val protocol: String = "https", + val download: Boolean = true, + val deviceModel: String = android.os.Build.MODEL ?: "Android", + val alias: String = "AndroidDevice" +) diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/domain/entity/peertopeer/QRCodeInfos.kt b/mobile/src/main/java/org/horizontal/tella/mobile/domain/entity/peertopeer/QRCodeInfos.kt new file mode 100644 index 000000000..2a85a6eb7 --- /dev/null +++ b/mobile/src/main/java/org/horizontal/tella/mobile/domain/entity/peertopeer/QRCodeInfos.kt @@ -0,0 +1,15 @@ +package org.horizontal.tella.mobile.domain.entity.peertopeer + +data class QRCodeInfos( + val ipAddress: String, + val pin: String, + val hash: String +) { + override fun equals(other: Any?): Boolean { + return (other as? QRCodeInfos)?.ipAddress == ipAddress + } + + override fun hashCode(): Int { + return ipAddress.hashCode() + } +} diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/domain/peertopeer/KeyStoreConfig.kt b/mobile/src/main/java/org/horizontal/tella/mobile/domain/peertopeer/KeyStoreConfig.kt new file mode 100644 index 000000000..8d067c115 --- /dev/null +++ b/mobile/src/main/java/org/horizontal/tella/mobile/domain/peertopeer/KeyStoreConfig.kt @@ -0,0 +1,26 @@ +package org.horizontal.tella.mobile.domain.peertopeer + +import java.util.UUID + +data class KeyStoreConfig( + val alias: String = "tella-alias", + val password: CharArray = UUID.randomUUID().toString().toCharArray() +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as KeyStoreConfig + + if (alias != other.alias) return false + if (!password.contentEquals(other.password)) return false + + return true + } + + override fun hashCode(): Int { + var result = alias.hashCode() + result = 31 * result + password.contentHashCode() + return result + } +} diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/domain/peertopeer/PeerConnectionPayload.kt b/mobile/src/main/java/org/horizontal/tella/mobile/domain/peertopeer/PeerConnectionPayload.kt new file mode 100644 index 000000000..7a28595c5 --- /dev/null +++ b/mobile/src/main/java/org/horizontal/tella/mobile/domain/peertopeer/PeerConnectionPayload.kt @@ -0,0 +1,18 @@ +package org.horizontal.tella.mobile.domain.peertopeer + +import com.google.gson.annotations.SerializedName + +data class PeerConnectionPayload( + @SerializedName("connect_code") + val connectCode: String, + + @SerializedName("port") + val port: Int, + + @SerializedName("certificate_hash") + val certificateHash: String, + + @SerializedName("pin") + val pin: String +) + diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/domain/peertopeer/PeerPrepareUploadResponse.kt b/mobile/src/main/java/org/horizontal/tella/mobile/domain/peertopeer/PeerPrepareUploadResponse.kt new file mode 100644 index 000000000..acfc25782 --- /dev/null +++ b/mobile/src/main/java/org/horizontal/tella/mobile/domain/peertopeer/PeerPrepareUploadResponse.kt @@ -0,0 +1,8 @@ +package org.horizontal.tella.mobile.domain.peertopeer + +import kotlinx.serialization.Serializable + +@Serializable +data class PeerPrepareUploadResponse( + val transmissionId: String +) \ No newline at end of file diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/domain/peertopeer/PeerRegisterPayload.kt b/mobile/src/main/java/org/horizontal/tella/mobile/domain/peertopeer/PeerRegisterPayload.kt new file mode 100644 index 000000000..d9c399056 --- /dev/null +++ b/mobile/src/main/java/org/horizontal/tella/mobile/domain/peertopeer/PeerRegisterPayload.kt @@ -0,0 +1,10 @@ +package org.horizontal.tella.mobile.domain.peertopeer + +import kotlinx.serialization.Serializable +import java.util.UUID + +@Serializable +data class PeerRegisterPayload( + val pin: String, + val nonce: String = UUID.randomUUID().toString() +) \ No newline at end of file diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/domain/peertopeer/PeerResponse.kt b/mobile/src/main/java/org/horizontal/tella/mobile/domain/peertopeer/PeerResponse.kt new file mode 100644 index 000000000..9891126b7 --- /dev/null +++ b/mobile/src/main/java/org/horizontal/tella/mobile/domain/peertopeer/PeerResponse.kt @@ -0,0 +1,8 @@ +package org.horizontal.tella.mobile.domain.peertopeer + +import kotlinx.serialization.Serializable + +@Serializable +data class PeerResponse( + val sessionId: String +) \ No newline at end of file diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/domain/peertopeer/TellaServer.kt b/mobile/src/main/java/org/horizontal/tella/mobile/domain/peertopeer/TellaServer.kt new file mode 100644 index 000000000..655edf426 --- /dev/null +++ b/mobile/src/main/java/org/horizontal/tella/mobile/domain/peertopeer/TellaServer.kt @@ -0,0 +1,7 @@ +package org.horizontal.tella.mobile.domain.peertopeer + +interface TellaServer { + fun start() + fun stop() + val certificatePem: String +} \ No newline at end of file diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/util/NavigationManager.kt b/mobile/src/main/java/org/horizontal/tella/mobile/util/NavigationManager.kt index f6ffc4a05..a69026b15 100644 --- a/mobile/src/main/java/org/horizontal/tella/mobile/util/NavigationManager.kt +++ b/mobile/src/main/java/org/horizontal/tella/mobile/util/NavigationManager.kt @@ -55,9 +55,11 @@ class NavigationManager( fun navigateFromGoogleDriveEntryScreenToGoogleDriveSendScreen() { navigateToWithBundle(R.id.action_newGoogleDriveScreen_to_googleDriveSendScreen) } + fun navigateFromNextCloudEntryScreenToNextCloudSendScreen() { navigateToWithBundle(R.id.action_newNextCloudScreen_to_nextCloudSendScreen) } + fun navigateFromGoogleDriveMainScreenToGoogleDriveSendScreen() { navigateToWithBundle(R.id.action_googleDriveScreen_to_googleDriveSendScreen) } @@ -65,6 +67,7 @@ class NavigationManager( fun navigateFromNextCloudMainScreenToNextCloudSendScreen() { navigateToWithBundle(R.id.action_nextCloudScreen_to_nextCloudSendScreen) } + fun navigateFromGoogleDriveScreenToGoogleDriveSubmittedScreen() { navigateToWithBundle(R.id.action_googleDriveScreen_to_googleDriveSubmittedScreen) } @@ -72,6 +75,7 @@ class NavigationManager( fun navigateFromNextCloudScreenToNextCloudSubmittedScreen() { navigateToWithBundle(R.id.action_nextCloudScreen_to_nextCloudSubmittedScreen) } + fun navigateFromDropBoxScreenToDropBoxSubmittedScreen() { navigateToWithBundle(R.id.action_dropBoxScreen_to_dropBoxSubmittedScreen) } @@ -136,7 +140,8 @@ class NavigationManager( fun navigateToNextCloudCreateFolderScreen() { navigateToWithBundle(R.id.action_loginNextCloudScreen_to_nextCloudNewFolderScreen) } - fun actionNextCloudNewFolderScreenToSuccessfulScreen(){ + + fun actionNextCloudNewFolderScreenToSuccessfulScreen() { navigateToWithBundle(R.id.action_nextCloudNewFolderScreen_to_successfulSetServerFragment) } @@ -160,4 +165,16 @@ class NavigationManager( navigateToWithBundle(R.id.action_selectSharedDriveFragment_to_googleDriveConnectedServerFragment) } + fun navigateFromStartNearBySharingFragmentToConnectHotspotFragment() { + navigateToWithBundle(R.id.action_startNearBySharingFragment_to_connectHotspotFragment) + } + + fun navigateFromActionConnectHotspotScreenToQrCodeScreen() { + navigateToWithBundle(R.id.action_connectHotspotScreen_to_qrCodeScreen) + } + + fun navigateFromActionConnectHotspotScreenToScanQrCodeScreen() { + navigateToWithBundle(R.id.action_startNearBySharingFragment_to_scanQrCodeScreen) + } + } \ No newline at end of file diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/CameraFragment.kt b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/CameraFragment.kt deleted file mode 100644 index 7aa74f02c..000000000 --- a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/CameraFragment.kt +++ /dev/null @@ -1,42 +0,0 @@ -package org.horizontal.tella.mobile.views.fragment - -import android.os.Bundle -import androidx.fragment.app.Fragment -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import org.horizontal.tella.mobile.R - -// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER -private const val ARG_PARAM1 = "param1" -private const val ARG_PARAM2 = "param2" - -class CameraFragment : Fragment() { - private var param1: String? = null - private var param2: String? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - arguments?.let { - param1 = it.getString(ARG_PARAM1) - param2 = it.getString(ARG_PARAM2) - } - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View? { - // Inflate the layout for this fragment - return inflater.inflate(R.layout.fragment_camera, container, false) - } - - companion object { - @JvmStatic - fun newInstance(param1: String, param2: String) = - CameraFragment().apply { - arguments = Bundle().apply { - putString(ARG_PARAM1, param1) - putString(ARG_PARAM2, param2) - } - } - } -} \ No newline at end of file diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/ReportsFragment.kt b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/ReportsFragment.kt deleted file mode 100644 index b604058d5..000000000 --- a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/ReportsFragment.kt +++ /dev/null @@ -1,58 +0,0 @@ -package org.horizontal.tella.mobile.views.fragment - -import android.os.Bundle -import androidx.fragment.app.Fragment -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import org.horizontal.tella.mobile.R - -// TODO: Rename parameter arguments, choose names that match -// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER -private const val ARG_PARAM1 = "param1" -private const val ARG_PARAM2 = "param2" - -/** - * A simple [Fragment] subclass. - * Use the [ReportsFragment.newInstance] factory method to - * create an instance of this fragment. - */ -class ReportsFragment : Fragment() { - // TODO: Rename and change types of parameters - private var param1: String? = null - private var param2: String? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - arguments?.let { - param1 = it.getString(ARG_PARAM1) - param2 = it.getString(ARG_PARAM2) - } - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View? { - // Inflate the layout for this fragment - return inflater.inflate(R.layout.fragment_reports, container, false) - } - - companion object { - /** - * Use this factory method to create a new instance of - * this fragment using the provided parameters. - * - * @param param1 Parameter 1. - * @param param2 Parameter 2. - * @return A new instance of fragment ReportsFragment. - */ - // TODO: Rename and change types and number of parameters - @JvmStatic - fun newInstance(param1: String, param2: String) = - ReportsFragment().apply { - arguments = Bundle().apply { - putString(ARG_PARAM1, param1) - putString(ARG_PARAM2, param2) - } - } - } -} \ No newline at end of file diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/PeerToPeerViewModel.kt b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/PeerToPeerViewModel.kt new file mode 100644 index 000000000..9faa3cb48 --- /dev/null +++ b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/PeerToPeerViewModel.kt @@ -0,0 +1,220 @@ +package org.horizontal.tella.mobile.views.fragment.peertopeer + +import android.annotation.SuppressLint +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.net.wifi.WifiInfo +import android.net.wifi.WifiManager +import android.os.Build +import android.text.format.Formatter +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.content.ContextCompat +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import org.horizontal.tella.mobile.certificate.CertificateUtils +import org.horizontal.tella.mobile.data.peertopeer.TellaPeerToPeerClient +import org.horizontal.tella.mobile.views.fragment.peertopeer.data.ConnectionType +import org.horizontal.tella.mobile.views.fragment.peertopeer.data.NetworkInfo +import org.json.JSONArray +import org.json.JSONObject +import java.io.File +import java.io.FileOutputStream +import java.net.Inet4Address +import java.net.NetworkInterface +import java.security.MessageDigest +import java.security.SecureRandom +import java.security.cert.CertificateException +import java.security.cert.X509Certificate +import java.util.UUID +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManager +import javax.net.ssl.X509TrustManager + +@HiltViewModel +class PeerToPeerViewModel @Inject constructor( + @ApplicationContext private val context: Context, + private val peerClient: TellaPeerToPeerClient +) : + ViewModel() { + + private val _networkInfo = MutableLiveData() + val networkInfo: LiveData = _networkInfo + var currentNetworkInfo: NetworkInfo? = null + private set + + @RequiresApi(Build.VERSION_CODES.M) + @SuppressLint("MissingPermission", "DiscouragedPrivateApi") + fun fetchCurrentNetworkInfo() { + val connectivityManager = + ContextCompat.getSystemService(context, ConnectivityManager::class.java) + val network = connectivityManager?.activeNetwork + val capabilities = connectivityManager?.getNetworkCapabilities(network) + + when { + capabilities == null -> { + _networkInfo.value = NetworkInfo(ConnectionType.NONE, null, null) + } + + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> { + val wifiManager = + context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + val wifiInfo = wifiManager.connectionInfo + + val ssid = + if (!wifiInfo.ssid.isNullOrEmpty() && wifiInfo.ssid != WifiManager.UNKNOWN_SSID) { + wifiInfo.ssid.trim('"') + } else { + getWifiSsidFromCapabilities(capabilities) ?: "Hotspot" + } + + val ipAddress = Formatter.formatIpAddress(wifiInfo.ipAddress) + _networkInfo.value = NetworkInfo(ConnectionType.WIFI, ssid, ipAddress) + } + + isDeviceHotspotEnabled(context) -> { + val hotspotSSID = getDeviceHotspotSSID(context) ?: "Hotspot" + val hotspotIpAddress = getDeviceHotspotIpAddress() + _networkInfo.value = + NetworkInfo(ConnectionType.HOTSPOT, hotspotSSID, hotspotIpAddress) + } + + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> { + _networkInfo.value = NetworkInfo(ConnectionType.CELLULAR, null, null) + } + + else -> { + _networkInfo.value = NetworkInfo(ConnectionType.NONE, null, null) + } + } + + + currentNetworkInfo = _networkInfo.value + } + + @SuppressLint("DiscouragedPrivateApi") + private fun getDeviceHotspotSSID(context: Context): String? { + return try { + val wifiManager = + context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + val method = wifiManager.javaClass.getDeclaredMethod("getWifiApConfiguration") + method.isAccessible = true + val wifiConfig = method.invoke(wifiManager) + wifiConfig?.let { + val ssidField = it.javaClass.getDeclaredField("SSID") + ssidField.isAccessible = true + (ssidField.get(it) as? String)?.trim('"') + } + } catch (e: Exception) { + null + } + } + + @SuppressLint("DiscouragedPrivateApi") + private fun isDeviceHotspotEnabled(context: Context): Boolean { + return try { + val wifiManager = + context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + val method = wifiManager.javaClass.getDeclaredMethod("isWifiApEnabled") + method.isAccessible = true + method.invoke(wifiManager) as Boolean + } catch (e: Exception) { + false + } + } + + private fun getWifiSsidFromCapabilities(capabilities: NetworkCapabilities): String? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + (capabilities.transportInfo as? WifiInfo)?.ssid?.trim('"') + } else { + null + } + } + + @SuppressLint("DiscouragedPrivateApi") + private fun getDeviceHotspotIpAddress(): String? { + return try { + val networkInterfaces = NetworkInterface.getNetworkInterfaces() + networkInterfaces.iterator().forEach { networkInterface -> + networkInterface.inetAddresses.iterator().forEach { inetAddress -> + if (!inetAddress.isLoopbackAddress && inetAddress is Inet4Address) { + if (networkInterface.displayName.contains("wlan") || networkInterface.displayName.contains( + "ap" + ) + ) { + return inetAddress.hostAddress + } + } + } + } + null + } catch (e: Exception) { + null + } + } + + fun onQrCodeParsed(ip: String, port: String, hash: String, pin: String) { + viewModelScope.launch { + val result = peerClient.registerPeerDevice(ip, port, hash, pin) + result.onSuccess { + Log.d("QRCode", "Registered successfully: $it") + val file = createFileFromAsset(context, "testDemo.txt") + peerClient.prepareUpload(ip,port,hash,"test report",file,UUID.randomUUID().toString(),calculateSha256(file),UUID.randomUUID().toString()) + // update UI state + }.onFailure { + Log.e("QRCode", "Registration failed: ${it.message}") + // handle error (maybe post a value to LiveData) + } + } + } + + + private fun createFileFromAsset(context: Context, assetFileName: String): File { + // Create a temporary file in the cache directory + val tempFile = File(context.cacheDir, assetFileName) + + if (!tempFile.exists()) { + Log.e("PrepareUpload", "File does not exist: ${tempFile.absolutePath}") + } + + // Open the asset file + context.assets.open(assetFileName).use { inputStream -> + // Write the content of the asset to the temporary file + FileOutputStream(tempFile).use { outputStream -> + inputStream.copyTo(outputStream) + } + } + + return tempFile + } + + + + private fun calculateSha256(file: File): String { + val digest = MessageDigest.getInstance("SHA-256") + file.inputStream().use { input -> + val buffer = ByteArray(8192) + var bytesRead: Int + while (input.read(buffer).also { bytesRead = it } != -1) { + digest.update(buffer, 0, bytesRead) + } + } + return digest.digest().joinToString("") { "%02x".format(it) } + } + + + +} \ No newline at end of file diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/StartNearBySharingFragment.kt b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/StartNearBySharingFragment.kt index 3ebe4c6e7..558712df8 100644 --- a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/StartNearBySharingFragment.kt +++ b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/StartNearBySharingFragment.kt @@ -2,11 +2,10 @@ package org.horizontal.tella.mobile.views.fragment.peertopeer import android.os.Bundle import android.view.View -import androidx.core.view.isVisible +import com.hzontal.tella_locking_ui.ui.pin.pinview.ResourceUtils.getColor import org.horizontal.tella.mobile.R import org.horizontal.tella.mobile.databinding.StartNearBySharingFragmentBinding import org.horizontal.tella.mobile.util.Util -import org.horizontal.tella.mobile.util.hide import org.horizontal.tella.mobile.views.base_ui.BaseBindingFragment class StartNearBySharingFragment : BaseBindingFragment( @@ -19,9 +18,9 @@ class StartNearBySharingFragment : BaseBindingFragment navManager().navigateFromActionConnectHotspotScreenToScanQrCodeScreen() + receiveFilesBtn.isChecked -> navManager().navigateFromStartNearBySharingFragmentToConnectHotspotFragment() + else -> {} + } + } } - } diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/activity/PeerToPeerActivity.kt b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/activity/PeerToPeerActivity.kt index 81f8f350c..c44c043b7 100644 --- a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/activity/PeerToPeerActivity.kt +++ b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/activity/PeerToPeerActivity.kt @@ -11,7 +11,5 @@ class PeerToPeerActivity : BaseLockActivity() { super.onCreate(savedInstanceState) binding = ActivityPeerToPeerBinding.inflate(layoutInflater) setContentView(binding.getRoot()) - binding.toolbar.backClickListener = { this.onBackPressed() } - } } \ No newline at end of file diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/data/ConnectionType.kt b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/data/ConnectionType.kt new file mode 100644 index 000000000..4cca5a3b9 --- /dev/null +++ b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/data/ConnectionType.kt @@ -0,0 +1,5 @@ +package org.horizontal.tella.mobile.views.fragment.peertopeer.data + +enum class ConnectionType { + WIFI, CELLULAR, HOTSPOT, NONE +} \ No newline at end of file diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/data/NetWorkInfo.kt b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/data/NetWorkInfo.kt new file mode 100644 index 000000000..ffaf84136 --- /dev/null +++ b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/data/NetWorkInfo.kt @@ -0,0 +1,8 @@ +package org.horizontal.tella.mobile.views.fragment.peertopeer.data + +data class NetworkInfo( + val type: ConnectionType, + val networkName: String?, + val ipAddress: String?, + var port: String = "53317" +) diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/di/PeerModule.kt b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/di/PeerModule.kt new file mode 100644 index 000000000..fefe59992 --- /dev/null +++ b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/di/PeerModule.kt @@ -0,0 +1,19 @@ +package org.horizontal.tella.mobile.views.fragment.peertopeer.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.horizontal.tella.mobile.data.peertopeer.TellaPeerToPeerClient +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object PeerModule { + + @Provides + @Singleton + fun providePeerClient(): TellaPeerToPeerClient { + return TellaPeerToPeerClient() + } +} \ No newline at end of file diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/receipentflow/ConnectHotspotFragment.kt b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/receipentflow/ConnectHotspotFragment.kt new file mode 100644 index 000000000..ecd84c81c --- /dev/null +++ b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/receipentflow/ConnectHotspotFragment.kt @@ -0,0 +1,83 @@ +package org.horizontal.tella.mobile.views.fragment.peertopeer.receipentflow + +import android.os.Build +import android.os.Bundle +import android.view.View +import androidx.annotation.RequiresApi +import androidx.fragment.app.activityViewModels +import com.hzontal.tella_locking_ui.ui.pin.pinview.ResourceUtils.getColor +import dagger.hilt.android.AndroidEntryPoint +import org.horizontal.tella.mobile.R +import org.horizontal.tella.mobile.databinding.ConnectHotspotLayoutBinding +import org.horizontal.tella.mobile.views.base_ui.BaseBindingFragment +import org.horizontal.tella.mobile.views.fragment.peertopeer.PeerToPeerViewModel +import org.horizontal.tella.mobile.views.fragment.peertopeer.data.ConnectionType + +@AndroidEntryPoint +class ConnectHotspotFragment : + BaseBindingFragment(ConnectHotspotLayoutBinding::inflate) { + + private val viewModel: PeerToPeerViewModel by activityViewModels() + private var isCheckboxChecked = false + + + @RequiresApi(Build.VERSION_CODES.M) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initObservers() + initListeners() + viewModel.fetchCurrentNetworkInfo() + } + + private fun initObservers() { + + viewModel.networkInfo.observe(viewLifecycleOwner) { info -> + when (info.type) { + ConnectionType.HOTSPOT, ConnectionType.WIFI, ConnectionType.CELLULAR -> { + binding.currentWifiText.setRightText(info.networkName) + updateNextButtonState(ConnectionType.HOTSPOT) + } + + ConnectionType.NONE -> { + binding.currentWifiText.setRightText("No network connected") + updateNextButtonState(ConnectionType.NONE) + } + } + } + } + + private fun initListeners() { + + binding.currentWifi.setOnCheckedChangeListener { isChecked -> + isCheckboxChecked = isChecked + updateNextButtonState(viewModel.currentNetworkInfo?.type) + } + + binding.toolbar.backClickListener = { baseActivity.onBackPressed() } + + binding.nextBtn.setOnClickListener { } + + binding.backBtn.setOnClickListener { baseActivity.onBackPressed() } + } + + private fun updateNextButtonState(connectionType: ConnectionType?) { + val isEligibleConnection = + connectionType != ConnectionType.NONE && connectionType != ConnectionType.CELLULAR + val shouldEnable = isEligibleConnection && isCheckboxChecked + + binding.nextBtn.setOnClickListener(if (shouldEnable) { + { onNextClicked() } + } else { + { } + }) + binding.nextBtn.setTextColor( + getColor(baseActivity, if (shouldEnable) R.color.wa_white else R.color.wa_white_40) + ) + binding.currentWifi.setCheckboxEnabled(isEligibleConnection) + } + + private fun onNextClicked() { + navManager().navigateFromActionConnectHotspotScreenToQrCodeScreen() + } + +} \ No newline at end of file diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/receipentflow/QRCodeFragment.kt b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/receipentflow/QRCodeFragment.kt new file mode 100644 index 000000000..229783d68 --- /dev/null +++ b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/receipentflow/QRCodeFragment.kt @@ -0,0 +1,89 @@ +package org.horizontal.tella.mobile.views.fragment.peertopeer.receipentflow + +import android.graphics.Bitmap +import android.os.Bundle +import android.view.View +import androidx.fragment.app.activityViewModels +import com.google.gson.Gson +import com.google.zxing.BarcodeFormat +import com.google.zxing.WriterException +import com.journeyapps.barcodescanner.BarcodeEncoder +import org.horizontal.tella.mobile.certificate.CertificateGenerator +import org.horizontal.tella.mobile.certificate.CertificateUtils +import org.horizontal.tella.mobile.domain.peertopeer.PeerConnectionPayload +import org.horizontal.tella.mobile.data.peertopeer.TellaPeerToPeerServer +import org.horizontal.tella.mobile.data.peertopeer.port +import org.horizontal.tella.mobile.databinding.FragmentQrCodeBinding +import org.horizontal.tella.mobile.domain.peertopeer.KeyStoreConfig +import org.horizontal.tella.mobile.domain.peertopeer.TellaServer +import org.horizontal.tella.mobile.views.base_ui.BaseBindingFragment +import org.horizontal.tella.mobile.views.fragment.peertopeer.PeerToPeerViewModel + +class QRCodeFragment : BaseBindingFragment(FragmentQrCodeBinding::inflate) { + + private val viewModel: PeerToPeerViewModel by activityViewModels() + private var server: TellaServer? = null + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val ip = viewModel.currentNetworkInfo?.ipAddress + if (!ip.isNullOrEmpty()) { + setupServerAndQr(ip) + } + handleBack() + } + + private fun setupServerAndQr(ip: String) { + val (keyPair, certificate) = CertificateGenerator.generateCertificate(ipAddress = ip) + val config = KeyStoreConfig() + + server = TellaPeerToPeerServer( + ip = ip, + keyPair = keyPair, + certificate = certificate, + keyStoreConfig = config + ) + server?.start() + + val certHash = CertificateUtils.getPublicKeyHash(certificate) + val pin = "111111" + val port = port + + val payload = PeerConnectionPayload( + connectCode = ip, + port = port, + certificateHash = certHash, + pin = pin + ) + + val qrPayload = Gson().toJson(payload) + generateQrCode(qrPayload) + } + + + private fun generateQrCode(content: String) { + try { + val barcodeEncoder = BarcodeEncoder() + val bitmap: Bitmap = barcodeEncoder.encodeBitmap( + content, + BarcodeFormat.QR_CODE, + 600, + 600 + ) + binding.qrCodeImageView.setImageBitmap(bitmap) + } catch (e: WriterException) { + e.printStackTrace() + } + } + + private fun handleBack() { + binding.toolbar.backClickListener = { nav().popBackStack() } + binding.backBtn.setOnClickListener { nav().popBackStack() } + } + + // TODO NEXT STEPS + //TODO WORK ON THE SENDER RESPONER + // TODO PREAPRE REGISTER RESPONE + +} \ No newline at end of file diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/receipentflow/ShowDeviceInfoFragment.kt b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/receipentflow/ShowDeviceInfoFragment.kt new file mode 100644 index 000000000..698da492d --- /dev/null +++ b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/receipentflow/ShowDeviceInfoFragment.kt @@ -0,0 +1,18 @@ +package org.horizontal.tella.mobile.views.fragment.peertopeer.receipentflow + +import android.os.Bundle +import android.view.View +import org.horizontal.tella.mobile.databinding.ShowDeviceInfoLayoutBinding +import org.horizontal.tella.mobile.views.base_ui.BaseBindingFragment + +class ShowDeviceInfoFragment : BaseBindingFragment(ShowDeviceInfoLayoutBinding::inflate) { + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initListeners() + } + + private fun initListeners() { + + } +} \ No newline at end of file diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/receipentflow/VerificationInfoFragment.kt b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/receipentflow/VerificationInfoFragment.kt new file mode 100644 index 000000000..5af7e3916 --- /dev/null +++ b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/receipentflow/VerificationInfoFragment.kt @@ -0,0 +1,4 @@ +package org.horizontal.tella.mobile.views.fragment.peertopeer.receipentflow + +class VerificationInfoFragment { +} \ No newline at end of file diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/senderflow/ScanQrCodeFragment.kt b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/senderflow/ScanQrCodeFragment.kt new file mode 100644 index 000000000..64a36f774 --- /dev/null +++ b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/senderflow/ScanQrCodeFragment.kt @@ -0,0 +1,111 @@ +package org.horizontal.tella.mobile.views.fragment.peertopeer.senderflow + +import android.Manifest +import android.content.pm.PackageManager +import android.os.Build +import android.os.Bundle +import android.view.View +import androidx.annotation.RequiresApi +import androidx.core.content.ContextCompat +import androidx.fragment.app.activityViewModels +import com.google.gson.Gson +import com.journeyapps.barcodescanner.BarcodeCallback +import com.journeyapps.barcodescanner.BarcodeResult +import com.journeyapps.barcodescanner.CompoundBarcodeView +import org.horizontal.tella.mobile.domain.peertopeer.PeerConnectionPayload +import org.horizontal.tella.mobile.databinding.ScanQrcodeFragmentBinding +import org.horizontal.tella.mobile.views.base_ui.BaseBindingFragment +import org.horizontal.tella.mobile.views.fragment.peertopeer.PeerToPeerViewModel + +class ScanQrCodeFragment : + BaseBindingFragment(ScanQrcodeFragmentBinding::inflate) { + + private val viewModel: PeerToPeerViewModel by activityViewModels() + private lateinit var barcodeView: CompoundBarcodeView + + companion object { + private const val CAMERA_REQUEST_CODE = 1001 + } + + @RequiresApi(Build.VERSION_CODES.M) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + barcodeView = CompoundBarcodeView(requireContext()) + barcodeView = binding.qrCodeScanView + + if (ContextCompat.checkSelfPermission( + requireContext(), + Manifest.permission.CAMERA + ) == PackageManager.PERMISSION_GRANTED + ) { + startScanning() + } else { + baseActivity.maybeChangeTemporaryTimeout { + requestPermissions(arrayOf(Manifest.permission.CAMERA), CAMERA_REQUEST_CODE) + } + } + handleBack() + } + + private fun startScanning() { + barcodeView.decodeContinuous(object : BarcodeCallback { + + override fun barcodeResult(result: BarcodeResult?) { + result?.text?.let { qrContent -> + barcodeView.pause() + + try { + val payload = Gson().fromJson(qrContent, PeerConnectionPayload::class.java) + + viewModel.onQrCodeParsed( + ip = payload.connectCode, + port = payload.port.toString(), + hash = payload.certificateHash, + pin = payload.pin + ) + + } catch (e: Exception) { + e.printStackTrace() + // Show a message: Invalid QR Code + } + } + } + + + override fun possibleResultPoints(resultPoints: MutableList?) { + } + }) + + barcodeView.resume() + } + + override fun onPause() { + super.onPause() + barcodeView.pause() + } + + override fun onResume() { + super.onResume() + barcodeView.resume() + } + + override fun onDestroyView() { + barcodeView.pauseAndWait() + super.onDestroyView() + } + + override fun onRequestPermissionsResult( + requestCode: Int, permissions: Array, grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode == CAMERA_REQUEST_CODE && grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) { + startScanning() + } + } + + private fun handleBack() { + binding.toolbar.backClickListener = { nav().popBackStack() } + binding.backBtn.setOnClickListener { nav().popBackStack() } + } +} diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/views/settings/LanguageSettings.kt b/mobile/src/main/java/org/horizontal/tella/mobile/views/settings/LanguageSettings.kt index 015e0d738..e166126d6 100644 --- a/mobile/src/main/java/org/horizontal/tella/mobile/views/settings/LanguageSettings.kt +++ b/mobile/src/main/java/org/horizontal/tella/mobile/views/settings/LanguageSettings.kt @@ -49,7 +49,7 @@ class LanguageSettings : BaseFragment(), View.OnClickListener { private fun createLangViews() { if (languages.isEmpty()) { languages = - ArrayList(Arrays.asList(*resources.getStringArray(R.array.ra_lang_codes))) + ArrayList(listOf(*resources.getStringArray(R.array.ra_lang_codes))) languages.add(0, null) val prefferedLang = LocaleManager.getInstance().languageSetting diff --git a/mobile/src/main/res/drawable/icon_wifi.xml b/mobile/src/main/res/drawable/icon_wifi.xml new file mode 100644 index 000000000..79f46f896 --- /dev/null +++ b/mobile/src/main/res/drawable/icon_wifi.xml @@ -0,0 +1,13 @@ + + + diff --git a/mobile/src/main/res/drawable/qr_scan_border.xml b/mobile/src/main/res/drawable/qr_scan_border.xml new file mode 100644 index 000000000..2ef2efe84 --- /dev/null +++ b/mobile/src/main/res/drawable/qr_scan_border.xml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/mobile/src/main/res/layout/activity_peer_to_peer.xml b/mobile/src/main/res/layout/activity_peer_to_peer.xml index 098bc4fc4..06985463c 100644 --- a/mobile/src/main/res/layout/activity_peer_to_peer.xml +++ b/mobile/src/main/res/layout/activity_peer_to_peer.xml @@ -9,35 +9,15 @@ android:textDirection="locale" tools:context="org.horizontal.tella.mobile.views.fragment.peertopeer.activity.PeerToPeerActivity"> - - - - - - + diff --git a/mobile/src/main/res/layout/connect_hotspot_layout.xml b/mobile/src/main/res/layout/connect_hotspot_layout.xml new file mode 100644 index 000000000..171c2009e --- /dev/null +++ b/mobile/src/main/res/layout/connect_hotspot_layout.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mobile/src/main/res/layout/fragment_qr_code.xml b/mobile/src/main/res/layout/fragment_qr_code.xml new file mode 100644 index 000000000..26a86698e --- /dev/null +++ b/mobile/src/main/res/layout/fragment_qr_code.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/src/main/res/layout/fragment_vault.xml b/mobile/src/main/res/layout/fragment_vault.xml index e8e67a2d3..327e0c814 100644 --- a/mobile/src/main/res/layout/fragment_vault.xml +++ b/mobile/src/main/res/layout/fragment_vault.xml @@ -5,6 +5,7 @@ android:layout_height="match_parent" android:background="@color/space_cadet" xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/root" > + + + \ No newline at end of file diff --git a/mobile/src/main/res/layout/scan_qrcode_fragment.xml b/mobile/src/main/res/layout/scan_qrcode_fragment.xml new file mode 100644 index 000000000..8e7336bcf --- /dev/null +++ b/mobile/src/main/res/layout/scan_qrcode_fragment.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/src/main/res/layout/show_device_info_layout.xml b/mobile/src/main/res/layout/show_device_info_layout.xml new file mode 100644 index 000000000..f850f652d --- /dev/null +++ b/mobile/src/main/res/layout/show_device_info_layout.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mobile/src/main/res/layout/start_near_by_sharing_fragment.xml b/mobile/src/main/res/layout/start_near_by_sharing_fragment.xml index 9de4a3bf7..24a6169f6 100644 --- a/mobile/src/main/res/layout/start_near_by_sharing_fragment.xml +++ b/mobile/src/main/res/layout/start_near_by_sharing_fragment.xml @@ -1,10 +1,28 @@ + + + + + @@ -63,8 +82,10 @@ android:layout_marginTop="12dp" android:gravity="center" app:check_state="false" + app:layout_constraintHorizontal_bias="0.5" app:layout_constraintTop_toBottomOf="@+id/sendFilesBtn" - app:text="@string/receive_files" /> + app:text="@string/receive_files" + tools:layout_editor_absoluteX="24dp" /> diff --git a/mobile/src/main/res/layout/verification_info_fragment.xml b/mobile/src/main/res/layout/verification_info_fragment.xml new file mode 100644 index 000000000..a8c269056 --- /dev/null +++ b/mobile/src/main/res/layout/verification_info_fragment.xml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mobile/src/main/res/navigation/peer_to_peer_graph.xml b/mobile/src/main/res/navigation/peer_to_peer_graph.xml index df4dd7c20..ed48fc285 100644 --- a/mobile/src/main/res/navigation/peer_to_peer_graph.xml +++ b/mobile/src/main/res/navigation/peer_to_peer_graph.xml @@ -8,6 +8,40 @@ + android:label="StartNearBySharingFragment" + tools:layout="@layout/start_near_by_sharing_fragment"> + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml index 8132cdc06..fbc7fb6db 100644 --- a/mobile/src/main/res/values/strings.xml +++ b/mobile/src/main/res/values/strings.xml @@ -1003,6 +1003,29 @@ Would you like to share analytics with the Tella team? Delete report? Your report have successfully been submitted. Attach file + Connect to device + Show this QR code for the sender to scan + Scan the recipient’s QR code + Having trouble with the QR code? + Connect manually + Show your device information + The sender needs to input the following to\n connect to your device. + Connect code + PIN + Port + Verification + Make sure that this sequence matches what is shown on the sender’s device. + If the sequence on your device does not match the sequence on the sender’s device, the connection may not be secure and should be discarded. + Confirm and connect + Discard and start over + Wi-Fi + Get connected + Make sure both devices are connected to the same Wi-Fi network + Tips to connect\n1. The Wi-Fi network does not need to be \nconnected to the internet.\n2. +You can create your own Wi-Fi network\n using your phone’s hotspot feature.\n +3. Disable any VPN connections. + Current Wifi network + Yes, we are on the same Wi-Fi network Nearby sharing Share files with nearby devices without an Internet connection\n 1. Both devices must be connected to the same Wifi network\n 2. Make sure you trust the person you are sharing files with diff --git a/shared-ui/src/main/java/org/hzontal/shared_ui/buttons/DualTextCheckView.kt b/shared-ui/src/main/java/org/hzontal/shared_ui/buttons/DualTextCheckView.kt new file mode 100644 index 000000000..9dfd63788 --- /dev/null +++ b/shared-ui/src/main/java/org/hzontal/shared_ui/buttons/DualTextCheckView.kt @@ -0,0 +1,90 @@ +package org.hzontal.shared_ui.buttons + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import androidx.annotation.StringRes +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isVisible +import org.hzontal.shared_ui.R +import org.hzontal.shared_ui.databinding.DualTextCheckLayoutBinding + +class DualTextCheckView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr) { + + @StringRes + private var rightTextRes: Int = -1 + + @StringRes + private var leftTextRes: Int = -1 + + private var isCheckboxVisible: Boolean = false + + + private val binding: DualTextCheckLayoutBinding = + DualTextCheckLayoutBinding.inflate(LayoutInflater.from(context), this, true) + + private var onCheckedChangeListener: ((Boolean) -> Unit)? = null + + fun setOnCheckedChangeListener(listener: (Boolean) -> Unit) { + this.onCheckedChangeListener = listener + } + + + init { + initListeners() + extractAttributes(attrs, defStyleAttr) + } + + private fun extractAttributes(attrs: AttributeSet?, defStyleAttr: Int) { + attrs?.let { + val typedArray = context.obtainStyledAttributes( + attrs, R.styleable.DualTextCheckView, defStyleAttr, defStyleAttr + ) + + try { + rightTextRes = typedArray.getResourceId(R.styleable.DualTextCheckView_rightText, -1) + leftTextRes = typedArray.getResourceId(R.styleable.DualTextCheckView_leftText, -1) + isCheckboxVisible = + typedArray.getBoolean(R.styleable.DualTextCheckView_checkboxVisible, false) + + } finally { + typedArray.recycle() + } + } + bindView() + } + + private fun bindView() { + if (rightTextRes != -1) { + binding.rightTextView.setText(rightTextRes) + } + if (leftTextRes != -1) { + binding.leftTextView.setText(leftTextRes) + } + binding.checkBox.isVisible = isCheckboxVisible + } + + + private fun initListeners() { + binding.checkBox.setOnCheckedChangeListener { _, isChecked -> + onCheckedChangeListener?.invoke(isChecked) + } + } + + fun setRightText(text : String?){ + if (text != null){ + binding.rightTextView.text = text + } + } + + // New method: Enable/Disable Checkbox + fun setCheckboxEnabled(isEnabled: Boolean) { + binding.checkBox.isEnabled = isEnabled + binding.rightTextView.isEnabled = isEnabled + binding.leftTextView.isEnabled = isEnabled + alpha = if (isEnabled) 1f else 0.5f // visually indicate disabled state + } + +} \ No newline at end of file diff --git a/shared-ui/src/main/java/org/hzontal/shared_ui/buttons/RoundButton.kt b/shared-ui/src/main/java/org/hzontal/shared_ui/buttons/RoundButton.kt index 2669a16e9..075bc0200 100644 --- a/shared-ui/src/main/java/org/hzontal/shared_ui/buttons/RoundButton.kt +++ b/shared-ui/src/main/java/org/hzontal/shared_ui/buttons/RoundButton.kt @@ -15,15 +15,14 @@ import org.hzontal.shared_ui.R import org.hzontal.shared_ui.databinding.LayoutRoundButtonBinding class RoundButton @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : ConstraintLayout(context, attrs, defStyleAttr), Checkable { @StringRes private var text: Int = -1 private var tintColor: Int = -1 private var textColor: Int = -1 + private var isTextAllCaps = false private val binding: LayoutRoundButtonBinding = LayoutRoundButtonBinding.inflate(LayoutInflater.from(context), this, true) private var mChecked = false @@ -42,12 +41,8 @@ class RoundButton @JvmOverloads constructor( private fun extractAttributes(attrs: AttributeSet?, defStyleAttr: Int) { attrs?.let { - val typedArray = context - .obtainStyledAttributes( - attrs, - R.styleable.RoundButton, - defStyleAttr, - defStyleAttr + val typedArray = context.obtainStyledAttributes( + attrs, R.styleable.RoundButton, defStyleAttr, defStyleAttr ) try { @@ -55,6 +50,7 @@ class RoundButton @JvmOverloads constructor( mChecked = typedArray.getBoolean(R.styleable.RoundButton_check_state, false) tintColor = typedArray.getColor(R.styleable.RoundButton_tint_color, -1) textColor = typedArray.getColor(R.styleable.RoundButton_text_color, -1) + isTextAllCaps = typedArray.getBoolean(R.styleable.RoundButton_text_all_caps, false) isChecked = mChecked } finally { @@ -77,6 +73,7 @@ class RoundButton @JvmOverloads constructor( private fun bindView() { setTextAndVisibility(text, binding.sheetTextView) setBackgroundTintColor(tintColor) + setTextCaps(isTextAllCaps) } private fun setTextAndVisibility(text: Int, textView: TextView) { @@ -86,7 +83,7 @@ class RoundButton @JvmOverloads constructor( } } - fun setBackgroundTintColor(tintColor: Int) { + private fun setBackgroundTintColor(tintColor: Int) { if (tintColor != -1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { binding.sheetTextView.backgroundTintList = ColorStateList.valueOf(tintColor) @@ -124,11 +121,15 @@ class RoundButton @JvmOverloads constructor( private fun refreshLayoutState() { super.refreshDrawableState() - binding.sheetTextView.background = if (mChecked) - ContextCompat.getDrawable(context, R.drawable.bg_information_button_selected) - else - ContextCompat.getDrawable(context, R.drawable.bg_information_button) + binding.sheetTextView.background = if (mChecked) ContextCompat.getDrawable( + context, + R.drawable.bg_information_button_selected + ) + else ContextCompat.getDrawable(context, R.drawable.bg_information_button) } + private fun setTextCaps(isTextAllCaps: Boolean) { + binding.sheetTextView.isAllCaps = isTextAllCaps + } } \ No newline at end of file diff --git a/shared-ui/src/main/res/drawable/bg_dual_text_check.xml b/shared-ui/src/main/res/drawable/bg_dual_text_check.xml new file mode 100644 index 000000000..9cca8653f --- /dev/null +++ b/shared-ui/src/main/res/drawable/bg_dual_text_check.xml @@ -0,0 +1,16 @@ + + + + + + + + + \ No newline at end of file diff --git a/shared-ui/src/main/res/drawable/device.xml b/shared-ui/src/main/res/drawable/device.xml new file mode 100644 index 000000000..236bd69e3 --- /dev/null +++ b/shared-ui/src/main/res/drawable/device.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/shared-ui/src/main/res/layout-hdpi/information_button.xml b/shared-ui/src/main/res/layout-hdpi/information_button.xml index b4b9ef84f..f06c44ca4 100644 --- a/shared-ui/src/main/res/layout-hdpi/information_button.xml +++ b/shared-ui/src/main/res/layout-hdpi/information_button.xml @@ -1,11 +1,9 @@ + android:layout_height="70dp"> + android:layout_height="80dp"> + + + + + + + + + + + diff --git a/shared-ui/src/main/res/values/attrs.xml b/shared-ui/src/main/res/values/attrs.xml index 7eec7c766..6cbaa3a85 100644 --- a/shared-ui/src/main/res/values/attrs.xml +++ b/shared-ui/src/main/res/values/attrs.xml @@ -14,11 +14,18 @@ + + + + + + + diff --git a/shared-ui/src/main/res/values/styles.xml b/shared-ui/src/main/res/values/styles.xml index f024c9024..71302242b 100644 --- a/shared-ui/src/main/res/values/styles.xml +++ b/shared-ui/src/main/res/values/styles.xml @@ -67,6 +67,10 @@ 14sp + + diff --git a/tella-keys/build.gradle b/tella-keys/build.gradle index 0e52237c9..61c935303 100644 --- a/tella-keys/build.gradle +++ b/tella-keys/build.gradle @@ -37,6 +37,11 @@ dependencies { api "androidx.core:core-ktx:$versions.ktx" api "androidx.biometric:biometric:$versions.biometric" + //bouncycastle + api 'org.bouncycastle:bcprov-jdk18on:1.78' + api 'org.bouncycastle:bcpkix-jdk18on:1.78' + + testImplementation "junit:junit:$versions.junit" androidTestImplementation "androidx.test.ext:junit:$versions.testJunit" androidTestImplementation "androidx.test.espresso:espresso-core:$versions.espressoCore" diff --git a/tella-keys/src/main/java/org/hzontal/tella/keys/key/LifecycleMainKey.java b/tella-keys/src/main/java/org/hzontal/tella/keys/key/LifecycleMainKey.java index 8bc8db318..c771aae36 100644 --- a/tella-keys/src/main/java/org/hzontal/tella/keys/key/LifecycleMainKey.java +++ b/tella-keys/src/main/java/org/hzontal/tella/keys/key/LifecycleMainKey.java @@ -1,7 +1,5 @@ package org.hzontal.tella.keys.key; -import android.content.Context; - import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; @@ -11,8 +9,6 @@ import androidx.lifecycle.LifecycleObserver; import androidx.lifecycle.OnLifecycleEvent; -import org.hzontal.tella.keys.util.Preferences; - import timber.log.Timber; /** From 3302904259be775c133f9231810f6459b0233660 Mon Sep 17 00:00:00 2001 From: Ahlem Date: Thu, 22 May 2025 22:04:08 +0100 Subject: [PATCH 003/153] show connect manual informations --- .../tella/mobile/util/NavigationManager.kt | 5 ++++- .../receipentflow/QRCodeFragment.kt | 20 +++++++++++++++++-- .../receipentflow/ShowDeviceInfoFragment.kt | 18 ++++++++++++++++- .../fragment/vault/home/HomeVaultFragment.kt | 1 - .../res/layout/show_device_info_layout.xml | 4 ++-- .../res/navigation/peer_to_peer_graph.xml | 16 +++++++++++---- 6 files changed, 53 insertions(+), 11 deletions(-) diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/util/NavigationManager.kt b/mobile/src/main/java/org/horizontal/tella/mobile/util/NavigationManager.kt index 0c987252e..b3349874b 100644 --- a/mobile/src/main/java/org/horizontal/tella/mobile/util/NavigationManager.kt +++ b/mobile/src/main/java/org/horizontal/tella/mobile/util/NavigationManager.kt @@ -2,7 +2,6 @@ package org.horizontal.tella.mobile.util import android.os.Bundle import org.horizontal.tella.mobile.R -import org.horizontal.tella.mobile.domain.entity.uwazi.UwaziTemplate import org.horizontal.tella.mobile.views.fragment.reports.di.NavControllerProvider class NavigationManager( @@ -182,4 +181,8 @@ class NavigationManager( navigateToWithBundle(R.id.action_startNearBySharingFragment_to_scanQrCodeScreen) } + fun navigateFromScanQrCodeToDeviceInfo() { + navigateToWithBundle(R.id.action_qrCodeScreen_to_deviceInfoScreen) + } + } \ No newline at end of file diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/receipentflow/QRCodeFragment.kt b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/receipentflow/QRCodeFragment.kt index 229783d68..a0efe6c9a 100644 --- a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/receipentflow/QRCodeFragment.kt +++ b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/receipentflow/QRCodeFragment.kt @@ -23,6 +23,8 @@ class QRCodeFragment : BaseBindingFragment(FragmentQrCode private val viewModel: PeerToPeerViewModel by activityViewModels() private var server: TellaServer? = null + private var payload: PeerConnectionPayload? = null + private lateinit var qrPayload: String override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -32,6 +34,7 @@ class QRCodeFragment : BaseBindingFragment(FragmentQrCode setupServerAndQr(ip) } handleBack() + handleConnectManually() } private fun setupServerAndQr(ip: String) { @@ -50,14 +53,14 @@ class QRCodeFragment : BaseBindingFragment(FragmentQrCode val pin = "111111" val port = port - val payload = PeerConnectionPayload( + payload = PeerConnectionPayload( connectCode = ip, port = port, certificateHash = certHash, pin = pin ) - val qrPayload = Gson().toJson(payload) + qrPayload = Gson().toJson(payload) generateQrCode(qrPayload) } @@ -82,6 +85,19 @@ class QRCodeFragment : BaseBindingFragment(FragmentQrCode binding.backBtn.setOnClickListener { nav().popBackStack() } } + private fun handleConnectManually(){ + binding.connectManuallyButton.setOnClickListener { + connectManually() + } + } + + private fun connectManually() { + payload?.let { + bundle.putString("payload", qrPayload) + navManager().navigateFromScanQrCodeToDeviceInfo() + } + } + // TODO NEXT STEPS //TODO WORK ON THE SENDER RESPONER // TODO PREAPRE REGISTER RESPONE diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/receipentflow/ShowDeviceInfoFragment.kt b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/receipentflow/ShowDeviceInfoFragment.kt index 698da492d..8f92adae6 100644 --- a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/receipentflow/ShowDeviceInfoFragment.kt +++ b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/receipentflow/ShowDeviceInfoFragment.kt @@ -2,14 +2,30 @@ package org.horizontal.tella.mobile.views.fragment.peertopeer.receipentflow import android.os.Bundle import android.view.View +import com.google.gson.Gson import org.horizontal.tella.mobile.databinding.ShowDeviceInfoLayoutBinding +import org.horizontal.tella.mobile.domain.peertopeer.PeerConnectionPayload import org.horizontal.tella.mobile.views.base_ui.BaseBindingFragment -class ShowDeviceInfoFragment : BaseBindingFragment(ShowDeviceInfoLayoutBinding::inflate) { +class ShowDeviceInfoFragment : + BaseBindingFragment(ShowDeviceInfoLayoutBinding::inflate) { + + + private var payload: PeerConnectionPayload? = null override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + arguments?.getString("payload")?.let { payloadJson -> + payload = Gson().fromJson(payloadJson, PeerConnectionPayload::class.java) + } initListeners() + initView() + } + + private fun initView() { + binding.connectCode.setRightText(payload?.connectCode) + binding.pin.setRightText(payload?.pin) + binding.port.setRightText(payload?.port.toString()) } private fun initListeners() { diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/vault/home/HomeVaultFragment.kt b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/vault/home/HomeVaultFragment.kt index 7e6d9e0e8..d7c26aac6 100644 --- a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/vault/home/HomeVaultFragment.kt +++ b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/vault/home/HomeVaultFragment.kt @@ -98,7 +98,6 @@ class HomeVaultFragment : BaseFragment(), VaultClickListener { private var googleDriveServers: ArrayList? = null private var dropBoxServers: ArrayList? = null private var nextCloudServers: ArrayList? = null - private var favoriteForms: ArrayList? = null private lateinit var disposables: EventCompositeDisposable private var reportServersCounted = false private var collectServersCounted = false diff --git a/mobile/src/main/res/layout/show_device_info_layout.xml b/mobile/src/main/res/layout/show_device_info_layout.xml index f850f652d..dd1f8829b 100644 --- a/mobile/src/main/res/layout/show_device_info_layout.xml +++ b/mobile/src/main/res/layout/show_device_info_layout.xml @@ -87,7 +87,7 @@ app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/connectCode" - app:leftText="@string/connect_code" /> + app:leftText="@string/pin" /> + app:leftText="@string/port" /> + tools:layout="@layout/connect_hotspot_layout"> + tools:layout="@layout/scan_qrcode_fragment"/> - + tools:layout="@layout/fragment_qr_code"> + + + \ No newline at end of file From bd15467a2934b590d17c9ef79a125eff13a3d592 Mon Sep 17 00:00:00 2001 From: Ahlem Date: Fri, 23 May 2025 14:48:33 +0100 Subject: [PATCH 004/153] Finish sender manual connection design --- mobile/src/main/AndroidManifest.xml | 8 +- .../tella/mobile/util/NavigationManager.kt | 3 + .../views/base_ui/BaseBindingFragment.kt | 1 - .../receipentflow/ShowDeviceInfoFragment.kt | 1 - .../senderflow/ScanQrCodeFragment.kt | 7 + .../SenderManualConnectionFragment.kt | 70 ++++++++ .../res/layout/sender_manual_connection.xml | 163 ++++++++++++++++++ .../res/layout/show_device_info_layout.xml | 4 +- .../res/navigation/peer_to_peer_graph.xml | 12 +- mobile/src/main/res/values/strings.xml | 1 + 10 files changed, 260 insertions(+), 10 deletions(-) create mode 100644 mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/senderflow/SenderManualConnectionFragment.kt create mode 100644 mobile/src/main/res/layout/sender_manual_connection.xml diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index 428d36e0d..37ad47641 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -662,10 +662,7 @@ android:name=".views.dialog.nextcloud.NextCloudLoginFlowActivity" android:configChanges="keyboardHidden|orientation|screenSize" android:theme="@style/AppTheme.NoActionBar" /> - + @@ -704,7 +701,8 @@ + android:theme="@style/AppTheme.NoActionBar" + android:windowSoftInputMode="adjustResize" /> diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/util/NavigationManager.kt b/mobile/src/main/java/org/horizontal/tella/mobile/util/NavigationManager.kt index b3349874b..698219654 100644 --- a/mobile/src/main/java/org/horizontal/tella/mobile/util/NavigationManager.kt +++ b/mobile/src/main/java/org/horizontal/tella/mobile/util/NavigationManager.kt @@ -185,4 +185,7 @@ class NavigationManager( navigateToWithBundle(R.id.action_qrCodeScreen_to_deviceInfoScreen) } + fun navigateFromScanQrCodeToSenderManualConnectionScreen() { + navigateToWithBundle(R.id.action_scanQrCodeScreen_to_senderManualConnectionScreen) + } } \ No newline at end of file diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/views/base_ui/BaseBindingFragment.kt b/mobile/src/main/java/org/horizontal/tella/mobile/views/base_ui/BaseBindingFragment.kt index f5f0647bc..a8c39c889 100644 --- a/mobile/src/main/java/org/horizontal/tella/mobile/views/base_ui/BaseBindingFragment.kt +++ b/mobile/src/main/java/org/horizontal/tella/mobile/views/base_ui/BaseBindingFragment.kt @@ -96,5 +96,4 @@ abstract class BaseBindingFragment( _binding = null isViewInitialized = false } - } diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/receipentflow/ShowDeviceInfoFragment.kt b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/receipentflow/ShowDeviceInfoFragment.kt index 8f92adae6..354b39ac7 100644 --- a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/receipentflow/ShowDeviceInfoFragment.kt +++ b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/receipentflow/ShowDeviceInfoFragment.kt @@ -10,7 +10,6 @@ import org.horizontal.tella.mobile.views.base_ui.BaseBindingFragment class ShowDeviceInfoFragment : BaseBindingFragment(ShowDeviceInfoLayoutBinding::inflate) { - private var payload: PeerConnectionPayload? = null override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/senderflow/ScanQrCodeFragment.kt b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/senderflow/ScanQrCodeFragment.kt index 64a36f774..82d6d66cb 100644 --- a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/senderflow/ScanQrCodeFragment.kt +++ b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/senderflow/ScanQrCodeFragment.kt @@ -46,6 +46,7 @@ class ScanQrCodeFragment : } } handleBack() + initListeners() } private fun startScanning() { @@ -108,4 +109,10 @@ class ScanQrCodeFragment : binding.toolbar.backClickListener = { nav().popBackStack() } binding.backBtn.setOnClickListener { nav().popBackStack() } } + + private fun initListeners() { + binding.connectManuallyButton.setOnClickListener { + navManager().navigateFromScanQrCodeToSenderManualConnectionScreen() + } + } } diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/senderflow/SenderManualConnectionFragment.kt b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/senderflow/SenderManualConnectionFragment.kt new file mode 100644 index 000000000..9a166d6ef --- /dev/null +++ b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/senderflow/SenderManualConnectionFragment.kt @@ -0,0 +1,70 @@ +package org.horizontal.tella.mobile.views.fragment.peertopeer.senderflow + +import android.os.Bundle +import android.view.View +import androidx.core.content.ContextCompat +import androidx.fragment.app.activityViewModels +import com.hzontal.tella_locking_ui.common.extensions.onChange +import org.horizontal.tella.mobile.databinding.SenderManualConnectionBinding +import org.horizontal.tella.mobile.views.base_ui.BaseBindingFragment +import org.horizontal.tella.mobile.views.fragment.peertopeer.PeerToPeerViewModel +import org.hzontal.shared_ui.bottomsheet.KeyboardUtil + +class SenderManualConnectionFragment : + BaseBindingFragment(SenderManualConnectionBinding::inflate) { + + private val viewModel: PeerToPeerViewModel by activityViewModels() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + initListeners() + initView() + } + + private fun initView() { + binding.connectCode.onChange { + updateNextButtonState() + } + binding.port.onChange { + updateNextButtonState() + } + + binding.port.onChange { + updateNextButtonState() + } + + updateNextButtonState() + + KeyboardUtil(binding.root) + + } + + private fun initListeners() { + binding.backBtn.setOnClickListener { + nav().popBackStack() + } + + binding.nextBtn.setOnClickListener { + + } + } + + private fun isInputValid(): Boolean { + return binding.connectCode.text?.isNotBlank() == true && + binding.pin.text?.isNotBlank() == true && + binding.port.text?.isNotBlank() == true + } + + private fun updateNextButtonState() { + val enabled = isInputValid() + binding.nextBtn.isEnabled = enabled + binding.nextBtn.setTextColor( + ContextCompat.getColor( + baseActivity, + if (enabled) android.R.color.white else android.R.color.darker_gray + ) + ) + } + +} \ No newline at end of file diff --git a/mobile/src/main/res/layout/sender_manual_connection.xml b/mobile/src/main/res/layout/sender_manual_connection.xml new file mode 100644 index 000000000..2999a1ba4 --- /dev/null +++ b/mobile/src/main/res/layout/sender_manual_connection.xml @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mobile/src/main/res/layout/show_device_info_layout.xml b/mobile/src/main/res/layout/show_device_info_layout.xml index dd1f8829b..f850f652d 100644 --- a/mobile/src/main/res/layout/show_device_info_layout.xml +++ b/mobile/src/main/res/layout/show_device_info_layout.xml @@ -87,7 +87,7 @@ app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/connectCode" - app:leftText="@string/pin" /> + app:leftText="@string/connect_code" /> + app:leftText="@string/connect_code" /> + tools:layout="@layout/scan_qrcode_fragment"> + + + + \ No newline at end of file diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml index fea695848..9265c460c 100644 --- a/mobile/src/main/res/values/strings.xml +++ b/mobile/src/main/res/values/strings.xml @@ -1037,5 +1037,6 @@ You can create your own Wi-Fi network\n using your phone’s hotspot feature.\n Nearby sharing How it works https://tella-app.org/nearby-sharing/ + Enter the recipient’s device\n information to find it on the network From 2abd9ffbf36d86978a706398ebf9cfbb82ce4f2e Mon Sep 17 00:00:00 2001 From: wafa Date: Fri, 23 May 2025 16:52:57 +0100 Subject: [PATCH 005/153] implement prepare upload view --- .../tella/mobile/util/NavigationManager.kt | 3 + .../peertopeer/PeerToPeerViewModel.kt | 10 +- .../fragment/peertopeer/SenderViewModel.kt | 87 +++++++++ .../senderflow/PrepareUploadFragment.kt | 177 ++++++++++++++++++ .../senderflow/ScanQrCodeFragment.kt | 8 +- .../res/layout/fragment_prepare_upload.xml | 118 ++++++++++++ .../res/navigation/peer_to_peer_graph.xml | 8 + 7 files changed, 406 insertions(+), 5 deletions(-) create mode 100644 mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/SenderViewModel.kt create mode 100644 mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/senderflow/PrepareUploadFragment.kt create mode 100644 mobile/src/main/res/layout/fragment_prepare_upload.xml diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/util/NavigationManager.kt b/mobile/src/main/java/org/horizontal/tella/mobile/util/NavigationManager.kt index 698219654..528ebe74e 100644 --- a/mobile/src/main/java/org/horizontal/tella/mobile/util/NavigationManager.kt +++ b/mobile/src/main/java/org/horizontal/tella/mobile/util/NavigationManager.kt @@ -184,6 +184,9 @@ class NavigationManager( fun navigateFromScanQrCodeToDeviceInfo() { navigateToWithBundle(R.id.action_qrCodeScreen_to_deviceInfoScreen) } + fun navigateFromScanQrCodeTo() { + navigateToWithBundle(R.id.action_scanQrCodeScreen_to_prepareUploadFragment) + } fun navigateFromScanQrCodeToSenderManualConnectionScreen() { navigateToWithBundle(R.id.action_scanQrCodeScreen_to_senderManualConnectionScreen) diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/PeerToPeerViewModel.kt b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/PeerToPeerViewModel.kt index 9faa3cb48..cfd8fa991 100644 --- a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/PeerToPeerViewModel.kt +++ b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/PeerToPeerViewModel.kt @@ -54,7 +54,8 @@ class PeerToPeerViewModel @Inject constructor( private val _networkInfo = MutableLiveData() val networkInfo: LiveData = _networkInfo var currentNetworkInfo: NetworkInfo? = null - private set + private val _registrationSuccess = MutableLiveData() + val registrationSuccess: LiveData get() = _registrationSuccess @RequiresApi(Build.VERSION_CODES.M) @SuppressLint("MissingPermission", "DiscouragedPrivateApi") @@ -172,11 +173,12 @@ class PeerToPeerViewModel @Inject constructor( result.onSuccess { Log.d("QRCode", "Registered successfully: $it") val file = createFileFromAsset(context, "testDemo.txt") - peerClient.prepareUpload(ip,port,hash,"test report",file,UUID.randomUUID().toString(),calculateSha256(file),UUID.randomUUID().toString()) - // update UI state + _registrationSuccess.postValue(true) // Notify success }.onFailure { + // peerClient.prepareUpload(ip,port,hash,"test report",file,UUID.randomUUID().toString(),calculateSha256(file),UUID.randomUUID().toString()) + // update UI state Log.e("QRCode", "Registration failed: ${it.message}") - // handle error (maybe post a value to LiveData) + _registrationSuccess.postValue(false) // Optional: Notify failure } } } diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/SenderViewModel.kt b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/SenderViewModel.kt new file mode 100644 index 000000000..2ab1cf6be --- /dev/null +++ b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/SenderViewModel.kt @@ -0,0 +1,87 @@ +package org.horizontal.tella.mobile.views.fragment.peertopeer + +import dagger.hilt.android.lifecycle.HiltViewModel +import org.horizontal.tella.mobile.domain.entity.EntityStatus +import org.horizontal.tella.mobile.domain.entity.Server +import org.horizontal.tella.mobile.domain.entity.collect.FormMediaFile +import org.horizontal.tella.mobile.domain.entity.reports.ReportInstance +import org.horizontal.tella.mobile.views.fragment.main_connexions.base.BaseReportsViewModel +import javax.inject.Inject + +@HiltViewModel +class SenderViewModel @Inject constructor( + +) : BaseReportsViewModel() { + override fun clearDisposable() { + TODO("Not yet implemented") + } + + override fun deleteReport(instance: ReportInstance) { + TODO("Not yet implemented") + } + + override fun getReportBundle(instance: ReportInstance) { + TODO("Not yet implemented") + } + + override fun getFormInstance( + title: String, + description: String, + files: List?, + server: Server, + id: Long?, + reportApiId: String, + status: EntityStatus + ): ReportInstance { + TODO("Not yet implemented") + } + + override fun getDraftFormInstance( + title: String, + description: String, + files: List?, + server: Server, + id: Long? + ): ReportInstance { + TODO("Not yet implemented") + } + + override fun listSubmitted() { + TODO("Not yet implemented") + } + + override fun listOutbox() { + TODO("Not yet implemented") + } + + override fun listDraftsOutboxAndSubmitted() { + TODO("Not yet implemented") + } + + override fun listDrafts() { + TODO("Not yet implemented") + } + + override fun saveSubmitted(reportInstance: ReportInstance) { + TODO("Not yet implemented") + } + + override fun saveOutbox(reportInstance: ReportInstance) { + TODO("Not yet implemented") + } + + override fun saveDraft(reportInstance: ReportInstance, exitAfterSave: Boolean) { + TODO("Not yet implemented") + } + + override fun listServers() { + TODO("Not yet implemented") + } + + override fun submitReport(instance: ReportInstance, backButtonPressed: Boolean) { + TODO("Not yet implemented") + } + +} + + diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/senderflow/PrepareUploadFragment.kt b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/senderflow/PrepareUploadFragment.kt new file mode 100644 index 000000000..f995159fe --- /dev/null +++ b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/senderflow/PrepareUploadFragment.kt @@ -0,0 +1,177 @@ +package org.horizontal.tella.mobile.views.fragment.peertopeer.senderflow + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.fragment.app.setFragmentResultListener +import androidx.recyclerview.widget.GridLayoutManager +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.hzontal.tella_vault.VaultFile +import com.hzontal.tella_vault.filter.FilterType +import dagger.hilt.android.AndroidEntryPoint +import org.horizontal.tella.mobile.R +import org.horizontal.tella.mobile.databinding.FragmentPrepareUploadBinding +import org.horizontal.tella.mobile.media.MediaFileHandler +import org.horizontal.tella.mobile.util.C +import org.horizontal.tella.mobile.views.activity.camera.CameraActivity +import org.horizontal.tella.mobile.views.activity.camera.CameraActivity.Companion.CAPTURE_WITH_AUTO_UPLOAD +import org.horizontal.tella.mobile.views.adapters.reports.ReportsFilesRecyclerViewAdapter +import org.horizontal.tella.mobile.views.base_ui.BaseBindingFragment +import org.horizontal.tella.mobile.views.fragment.main_connexions.base.BUNDLE_REPORT_AUDIO +import org.horizontal.tella.mobile.views.fragment.main_connexions.base.BUNDLE_REPORT_VAULT_FILE +import org.horizontal.tella.mobile.views.fragment.main_connexions.base.OnNavBckListener +import org.horizontal.tella.mobile.views.fragment.recorder.REPORT_ENTRY +import org.horizontal.tella.mobile.views.fragment.uwazi.attachments.AttachmentsActivitySelector +import org.horizontal.tella.mobile.views.fragment.uwazi.attachments.VAULT_FILES_FILTER +import org.horizontal.tella.mobile.views.fragment.uwazi.attachments.VAULT_FILE_KEY +import org.horizontal.tella.mobile.views.fragment.uwazi.attachments.VAULT_PICKER_SINGLE +import org.horizontal.tella.mobile.views.interfaces.IReportAttachmentsHandler +import org.hzontal.shared_ui.bottomsheet.VaultSheetUtils.IVaultFilesSelector +import org.hzontal.shared_ui.bottomsheet.VaultSheetUtils.showVaultSelectFilesSheet + +@AndroidEntryPoint +class PrepareUploadFragment : + BaseBindingFragment(FragmentPrepareUploadBinding::inflate), + IReportAttachmentsHandler, OnNavBckListener { + private lateinit var gridLayoutManager: GridLayoutManager + private val filesRecyclerViewAdapter: ReportsFilesRecyclerViewAdapter by lazy { + ReportsFilesRecyclerViewAdapter(this) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setFragmentResultListener(BUNDLE_REPORT_AUDIO) { _, bundle -> + val file = bundle.get(BUNDLE_REPORT_VAULT_FILE) as VaultFile + bundle.remove(BUNDLE_REPORT_VAULT_FILE) + //putFiles(listOf(file)) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + initView() + initData() + } + + private fun initView() { + gridLayoutManager = GridLayoutManager(context, 3) + binding.filesRecyclerView.apply { + layoutManager = gridLayoutManager + adapter = filesRecyclerViewAdapter + } + binding.toolbar.backClickListener = { + exitOrSave() + } + + } + + private fun exitOrSave() { + + } + + @SuppressLint("StringFormatInvalid") + private fun initData() { + } + + + private fun showSelectFilesSheet() { + showVaultSelectFilesSheet(baseActivity.supportFragmentManager, + baseActivity.getString(R.string.Uwazi_WidgetMedia_Take_Photo), + baseActivity.getString(R.string.Vault_RecordAudio_SheetAction), + baseActivity.getString(R.string.Uwazi_WidgetMedia_Select_From_Device), + baseActivity.getString(R.string.Uwazi_WidgetMedia_Select_From_Tella), + null, + baseActivity.getString(R.string.Uwazi_MiltiFileWidget_SelectFiles), + object : IVaultFilesSelector { + override fun importFromVault() { + showAttachmentsActivity() + } + + override fun goToRecorder() { + showAudioRecorderActivity() + } + + override fun goToCamera() { + showCameraActivity() + } + + override fun importFromDevice() { + importMedia() + } + }) + } + + + private fun showAttachmentsActivity() { + try { + baseActivity.startActivityForResult( + Intent(activity, AttachmentsActivitySelector::class.java) + // .putExtra(VAULT_FILE_KEY, Gson().toJson(ids)) + .putExtra( + VAULT_FILES_FILTER, FilterType.ALL_WITHOUT_DIRECTORY + ).putExtra(VAULT_PICKER_SINGLE, false), C.MEDIA_FILE_ID + ) + } catch (e: Exception) { + FirebaseCrashlytics.getInstance().recordException(e) + } + } + + private fun showCameraActivity() { + try { + val intent = Intent(context, CameraActivity::class.java) + intent.apply { + putExtra(CameraActivity.INTENT_MODE, CameraActivity.IntentMode.COLLECT.name) + putExtra(CAPTURE_WITH_AUTO_UPLOAD, false) + } + + baseActivity.startActivityForResult(intent, C.MEDIA_FILE_ID) + } catch (e: java.lang.Exception) { + FirebaseCrashlytics.getInstance().recordException(e) + } + } + + private fun importMedia() { + baseActivity.maybeChangeTemporaryTimeout { + MediaFileHandler.startSelectMediaActivity( + activity, "image/* video/* audio/*", + arrayOf("image/*", "video/*", "audio/*"), C.IMPORT_FILE + ) + } + } + + private fun showAudioRecorderActivity() { + try { + bundle.putBoolean(REPORT_ENTRY, true) + this.navManager().navigateToMicro() + } catch (e: java.lang.Exception) { + FirebaseCrashlytics.getInstance().recordException(e) + } + } + + @Deprecated("Deprecated in Java") + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == C.MEDIA_FILE_ID && resultCode == Activity.RESULT_OK) { + val vaultFile = data?.getStringExtra(VAULT_FILE_KEY) ?: "" + // putFiles(viewModel.putVaultFilesInForm(vaultFile).blockingGet()) + } + } + + + override fun playMedia(mediaFile: VaultFile?) { + + } + + override fun addFiles() { + showSelectFilesSheet() + } + + override fun removeFiles() { + } + + override fun onBackPressed(): Boolean { + exitOrSave() + return true + } + +} \ No newline at end of file diff --git a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/senderflow/ScanQrCodeFragment.kt b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/senderflow/ScanQrCodeFragment.kt index 82d6d66cb..21d62795d 100644 --- a/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/senderflow/ScanQrCodeFragment.kt +++ b/mobile/src/main/java/org/horizontal/tella/mobile/views/fragment/peertopeer/senderflow/ScanQrCodeFragment.kt @@ -33,7 +33,13 @@ class ScanQrCodeFragment : barcodeView = CompoundBarcodeView(requireContext()) barcodeView = binding.qrCodeScanView - + viewModel.registrationSuccess.observe(viewLifecycleOwner) { success -> + if (success) { + navManager().navigateFromScanQrCodeTo() + } else { + // handle error UI + } + } if (ContextCompat.checkSelfPermission( requireContext(), Manifest.permission.CAMERA diff --git a/mobile/src/main/res/layout/fragment_prepare_upload.xml b/mobile/src/main/res/layout/fragment_prepare_upload.xml new file mode 100644 index 000000000..954845043 --- /dev/null +++ b/mobile/src/main/res/layout/fragment_prepare_upload.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + +