├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── assets
│ │ │ ├── certificate.bks
│ │ │ ├── certificate.p12
│ │ │ ├── csr.pem
│ │ │ ├── certificate.pem
│ │ │ └── key.pem
│ │ ├── res
│ │ │ ├── drawable
│ │ │ │ ├── folder.png
│ │ │ │ ├── file_icon.png
│ │ │ │ ├── seen_icon.png
│ │ │ │ ├── send_icon.png
│ │ │ │ ├── arrow_icon.png
│ │ │ │ ├── attach_icon.png
│ │ │ │ ├── close_icon.png
│ │ │ │ ├── power_icon.png
│ │ │ │ ├── attach_file_icon.png
│ │ │ │ ├── connected_wifi_icon.png
│ │ │ │ ├── connected_ethernet_icon.png
│ │ │ │ ├── disconnected_wifi_icon.png
│ │ │ │ ├── disconnected_ethernet_icon.png
│ │ │ │ ├── bg_border_item_white.xml
│ │ │ │ ├── bg_border_item_black.xml
│ │ │ │ ├── ic_launcher_foreground.xml
│ │ │ │ └── ic_launcher_background.xml
│ │ │ ├── font
│ │ │ │ ├── iranyekan.ttf
│ │ │ │ ├── vazir_bold.ttf
│ │ │ │ ├── vazir_thin.ttf
│ │ │ │ ├── vazir_black.ttf
│ │ │ │ ├── vazir_light.ttf
│ │ │ │ ├── vazir_medium.ttf
│ │ │ │ ├── vazir_regular.ttf
│ │ │ │ ├── iranyekan_black.ttf
│ │ │ │ ├── iranyekan_bold.ttf
│ │ │ │ ├── iranyekan_light.ttf
│ │ │ │ ├── iranyekan_thin.ttf
│ │ │ │ ├── iranyekan_medium.ttf
│ │ │ │ ├── iranyekan_extrabold.ttf
│ │ │ │ └── ranyekan_extrablack.ttf
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ ├── android_socket_icon.webp
│ │ │ │ ├── ic_launcher_round.webp
│ │ │ │ ├── connected_ethernet_icon.webp
│ │ │ │ ├── android_socket_icon_round.webp
│ │ │ │ ├── connected_ethernet_icon_round.webp
│ │ │ │ ├── android_socket_icon_foreground.webp
│ │ │ │ └── connected_ethernet_icon_foreground.webp
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── android_socket_icon.webp
│ │ │ │ ├── android_socket_icon_round.webp
│ │ │ │ └── android_socket_icon_foreground.webp
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── android_socket_icon.webp
│ │ │ │ ├── android_socket_icon_round.webp
│ │ │ │ └── android_socket_icon_foreground.webp
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── android_socket_icon.webp
│ │ │ │ ├── android_socket_icon_round.webp
│ │ │ │ └── android_socket_icon_foreground.webp
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── android_socket_icon.webp
│ │ │ │ ├── android_socket_icon_round.webp
│ │ │ │ └── android_socket_icon_foreground.webp
│ │ │ ├── values
│ │ │ │ ├── android_socket_icon_background.xml
│ │ │ │ ├── connected_ethernet_icon_background.xml
│ │ │ │ ├── themes.xml
│ │ │ │ ├── colors.xml
│ │ │ │ └── strings.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── android_socket_icon.xml
│ │ │ │ ├── android_socket_icon_round.xml
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── Shape.kt
│ │ │ └── xml
│ │ │ │ ├── backup_rules.xml
│ │ │ │ └── data_extraction_rules.xml
│ │ ├── android_socket_icon-playstore.png
│ │ ├── connected_ethernet_icon-playstore.png
│ │ ├── java
│ │ │ └── ir
│ │ │ │ └── example
│ │ │ │ └── androidsocket
│ │ │ │ ├── ui
│ │ │ │ ├── base
│ │ │ │ │ ├── BaseUiEvent.kt
│ │ │ │ │ ├── AppIcon.kt
│ │ │ │ │ ├── KeyboardListener.kt
│ │ │ │ │ ├── BaseViewModel.kt
│ │ │ │ │ ├── AppButton.kt
│ │ │ │ │ ├── ProtocolTypeMenu.kt
│ │ │ │ │ ├── AppText.kt
│ │ │ │ │ ├── PermisionDialog.kt
│ │ │ │ │ ├── AppButtonsRow.kt
│ │ │ │ │ ├── CaptureBitmap.kt
│ │ │ │ │ └── AppOutlinedTextField.kt
│ │ │ │ └── theme
│ │ │ │ │ ├── AppLayoutDirection.kt
│ │ │ │ │ ├── Spacing.kt
│ │ │ │ │ ├── Type.kt
│ │ │ │ │ ├── Color.kt
│ │ │ │ │ └── Theme.kt
│ │ │ │ ├── utils
│ │ │ │ ├── LogUtils.kt
│ │ │ │ ├── ValidationUtils.kt
│ │ │ │ ├── NotificationMessageBroadcastReceiver.kt
│ │ │ │ ├── ByteUtils.kt
│ │ │ │ ├── CaptureBitmap.kt
│ │ │ │ ├── ConnectionTypeManager.kt
│ │ │ │ ├── IpAddressManager.kt
│ │ │ │ └── NotificationHandler.kt
│ │ │ │ ├── SocketConnectionListener.kt
│ │ │ │ ├── MainApplication.kt
│ │ │ │ └── Constants.kt
│ │ ├── aidl
│ │ │ └── ir
│ │ │ │ └── sep
│ │ │ │ └── android
│ │ │ │ └── Service
│ │ │ │ └── IProxy.aidl
│ │ └── AndroidManifest.xml
│ ├── server
│ │ ├── java
│ │ │ └── ir
│ │ │ │ └── example
│ │ │ │ └── androidsocket
│ │ │ │ ├── socket
│ │ │ │ ├── SocketServer.kt
│ │ │ │ ├── ServerManager.kt
│ │ │ │ ├── WebsocketServerManger.kt
│ │ │ │ └── SocketServerForegroundService.kt
│ │ │ │ └── ui
│ │ │ │ ├── ServerEvent.kt
│ │ │ │ ├── ServerViewModel.kt
│ │ │ │ └── ServerActivity.kt
│ │ ├── res
│ │ │ └── values
│ │ │ │ └── strings.xml
│ │ └── AndroidManifest.xml
│ └── client
│ │ ├── java
│ │ └── ir
│ │ │ └── example
│ │ │ └── androidsocket
│ │ │ ├── client
│ │ │ ├── SocketClient.kt
│ │ │ ├── TcpSocketClient.kt
│ │ │ ├── WebsocketClientManager.kt
│ │ │ ├── SocketClientForegroundService.kt
│ │ │ └── TcpClientManager.kt
│ │ │ └── ui
│ │ │ ├── ClientEvent.kt
│ │ │ ├── ClientActivity.kt
│ │ │ └── ClientViewModel.kt
│ │ ├── res
│ │ └── values
│ │ │ └── strings.xml
│ │ └── AndroidManifest.xml
├── libs
│ └── NeptuneLiteApi_V2.03.00_20180208.jar
├── proguard-rules.pro
└── build.gradle.kts
├── .gradle
├── 8.2
│ ├── gc.properties
│ ├── fileChanges
│ │ └── last-build.bin
│ ├── dependencies-accessors
│ │ ├── gc.properties
│ │ └── dependencies-accessors.lock
│ ├── checksums
│ │ ├── checksums.lock
│ │ ├── md5-checksums.bin
│ │ └── sha1-checksums.bin
│ ├── fileHashes
│ │ ├── fileHashes.bin
│ │ ├── fileHashes.lock
│ │ └── resourceHashesCache.bin
│ └── executionHistory
│ │ ├── executionHistory.bin
│ │ └── executionHistory.lock
├── vcs-1
│ └── gc.properties
├── buildOutputCleanup
│ ├── cache.properties
│ ├── outputFiles.bin
│ └── buildOutputCleanup.lock
├── file-system.probe
├── checksums
│ ├── checksums.lock
│ ├── md5-checksums.bin
│ └── sha1-checksums.bin
└── config.properties
├── .idea
├── .name
├── .gitignore
├── vcs.xml
├── compiler.xml
├── kotlinc.xml
├── deploymentTargetDropDown.xml
├── migrations.xml
├── secondClone.iml
├── androidSocket - Copy.iml
├── misc.xml
├── deploymentTargetSelector.xml
├── gradle.xml
├── inspectionProfiles
│ └── Project_Default.xml
└── other.xml
├── android_socket.gif
├── my_android_socket_demo.gif
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── local.properties
├── settings.gradle.kts
├── gradle.properties
├── README.md
├── gradlew.bat
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/.gradle/8.2/gc.properties:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gradle/vcs-1/gc.properties:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.idea/.name:
--------------------------------------------------------------------------------
1 | Android Socket
--------------------------------------------------------------------------------
/.gradle/8.2/fileChanges/last-build.bin:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gradle/8.2/dependencies-accessors/gc.properties:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/.gradle/buildOutputCleanup/cache.properties:
--------------------------------------------------------------------------------
1 | #Sun May 05 09:44:51 IRST 2024
2 | gradle.version=8.2
3 |
--------------------------------------------------------------------------------
/android_socket.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/android_socket.gif
--------------------------------------------------------------------------------
/.gradle/file-system.probe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/.gradle/file-system.probe
--------------------------------------------------------------------------------
/my_android_socket_demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/my_android_socket_demo.gif
--------------------------------------------------------------------------------
/.gradle/checksums/checksums.lock:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/.gradle/checksums/checksums.lock
--------------------------------------------------------------------------------
/.gradle/config.properties:
--------------------------------------------------------------------------------
1 | #Fri Jul 26 15:06:31 IRST 2024
2 | java.home=/Applications/Android Studio.app/Contents/jbr/Contents/Home
3 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/.gradle/8.2/checksums/checksums.lock:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/.gradle/8.2/checksums/checksums.lock
--------------------------------------------------------------------------------
/.gradle/checksums/md5-checksums.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/.gradle/checksums/md5-checksums.bin
--------------------------------------------------------------------------------
/.gradle/checksums/sha1-checksums.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/.gradle/checksums/sha1-checksums.bin
--------------------------------------------------------------------------------
/app/src/main/assets/certificate.bks:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/assets/certificate.bks
--------------------------------------------------------------------------------
/app/src/main/assets/certificate.p12:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/assets/certificate.p12
--------------------------------------------------------------------------------
/app/src/main/res/drawable/folder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/drawable/folder.png
--------------------------------------------------------------------------------
/app/src/main/res/font/iranyekan.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/font/iranyekan.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/vazir_bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/font/vazir_bold.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/vazir_thin.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/font/vazir_thin.ttf
--------------------------------------------------------------------------------
/.gradle/8.2/checksums/md5-checksums.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/.gradle/8.2/checksums/md5-checksums.bin
--------------------------------------------------------------------------------
/.gradle/8.2/fileHashes/fileHashes.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/.gradle/8.2/fileHashes/fileHashes.bin
--------------------------------------------------------------------------------
/.gradle/8.2/fileHashes/fileHashes.lock:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/.gradle/8.2/fileHashes/fileHashes.lock
--------------------------------------------------------------------------------
/app/src/main/res/drawable/file_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/drawable/file_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/seen_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/drawable/seen_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/send_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/drawable/send_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/font/vazir_black.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/font/vazir_black.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/vazir_light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/font/vazir_light.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/vazir_medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/font/vazir_medium.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/vazir_regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/font/vazir_regular.ttf
--------------------------------------------------------------------------------
/.gradle/8.2/checksums/sha1-checksums.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/.gradle/8.2/checksums/sha1-checksums.bin
--------------------------------------------------------------------------------
/app/src/main/res/drawable/arrow_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/drawable/arrow_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/attach_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/drawable/attach_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/close_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/drawable/close_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/power_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/drawable/power_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/font/iranyekan_black.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/font/iranyekan_black.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/iranyekan_bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/font/iranyekan_bold.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/iranyekan_light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/font/iranyekan_light.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/iranyekan_thin.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/font/iranyekan_thin.ttf
--------------------------------------------------------------------------------
/.gradle/buildOutputCleanup/outputFiles.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/.gradle/buildOutputCleanup/outputFiles.bin
--------------------------------------------------------------------------------
/app/src/main/res/font/iranyekan_medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/font/iranyekan_medium.ttf
--------------------------------------------------------------------------------
/.gradle/8.2/fileHashes/resourceHashesCache.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/.gradle/8.2/fileHashes/resourceHashesCache.bin
--------------------------------------------------------------------------------
/app/libs/NeptuneLiteApi_V2.03.00_20180208.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/libs/NeptuneLiteApi_V2.03.00_20180208.jar
--------------------------------------------------------------------------------
/app/src/main/android_socket_icon-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/android_socket_icon-playstore.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/attach_file_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/drawable/attach_file_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/font/iranyekan_extrabold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/font/iranyekan_extrabold.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/ranyekan_extrablack.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/font/ranyekan_extrablack.ttf
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/.gradle/8.2/executionHistory/executionHistory.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/.gradle/8.2/executionHistory/executionHistory.bin
--------------------------------------------------------------------------------
/app/src/main/res/drawable/connected_wifi_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/drawable/connected_wifi_icon.png
--------------------------------------------------------------------------------
/.gradle/8.2/executionHistory/executionHistory.lock:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/.gradle/8.2/executionHistory/executionHistory.lock
--------------------------------------------------------------------------------
/.gradle/buildOutputCleanup/buildOutputCleanup.lock:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/.gradle/buildOutputCleanup/buildOutputCleanup.lock
--------------------------------------------------------------------------------
/app/src/main/connected_ethernet_icon-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/connected_ethernet_icon-playstore.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/connected_ethernet_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/drawable/connected_ethernet_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/disconnected_wifi_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/drawable/disconnected_wifi_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/android_socket_icon.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/mipmap-hdpi/android_socket_icon.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/android_socket_icon.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/mipmap-mdpi/android_socket_icon.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/android_socket_icon.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/mipmap-xhdpi/android_socket_icon.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/drawable/disconnected_ethernet_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/drawable/disconnected_ethernet_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/android_socket_icon.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/mipmap-xxhdpi/android_socket_icon.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/android_socket_icon.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/mipmap-xxxhdpi/android_socket_icon.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/android_socket_icon_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/mipmap-hdpi/android_socket_icon_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/android_socket_icon_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/mipmap-mdpi/android_socket_icon_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/connected_ethernet_icon.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/mipmap-xhdpi/connected_ethernet_icon.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/android_socket_icon_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/mipmap-xhdpi/android_socket_icon_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/android_socket_icon_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/mipmap-xxhdpi/android_socket_icon_round.webp
--------------------------------------------------------------------------------
/.gradle/8.2/dependencies-accessors/dependencies-accessors.lock:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/.gradle/8.2/dependencies-accessors/dependencies-accessors.lock
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/android_socket_icon_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/mipmap-hdpi/android_socket_icon_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/android_socket_icon_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/mipmap-mdpi/android_socket_icon_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/connected_ethernet_icon_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/mipmap-xhdpi/connected_ethernet_icon_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/android_socket_icon_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/mipmap-xxxhdpi/android_socket_icon_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values/android_socket_icon_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/android_socket_icon_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/mipmap-xhdpi/android_socket_icon_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/android_socket_icon_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/mipmap-xxhdpi/android_socket_icon_foreground.webp
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/connected_ethernet_icon_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/mipmap-xhdpi/connected_ethernet_icon_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/android_socket_icon_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SepidehAkbarinezhad/android_socket/HEAD/app/src/main/res/mipmap-xxxhdpi/android_socket_icon_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/values/connected_ethernet_icon_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFFFFF
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/deploymentTargetDropDown.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Feb 17 09:23:07 IRST 2024
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/ir/example/androidsocket/ui/base/BaseUiEvent.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.ui.base
2 |
3 | import androidx.annotation.StringRes
4 |
5 | sealed class BaseUiEvent {
6 | object None : BaseUiEvent()
7 | data class ShowToast(@StringRes val messageId: Int?, val parameters: Array) : BaseUiEvent()
8 | }
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/android_socket_icon.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/java/ir/example/androidsocket/utils/LogUtils.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.utils
2 |
3 | import android.util.Log
4 |
5 | fun serverLog(message : String , tag : String = "serverTag"){
6 | Log.d(tag , message)
7 | }
8 |
9 | fun clientLog(message : String , tag : String = "clientTag"){
10 | Log.d(tag , message)
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/android_socket_icon_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/secondClone.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/Shape.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.ui.theme
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.material.Shapes
5 | import androidx.compose.ui.unit.dp
6 |
7 | val MyShapes = Shapes(
8 | small = RoundedCornerShape(4.dp),
9 | medium = RoundedCornerShape(4.dp),
10 | large = RoundedCornerShape(0.dp)
11 | )
--------------------------------------------------------------------------------
/.idea/androidSocket - Copy.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/local.properties:
--------------------------------------------------------------------------------
1 | ## This file must *NOT* be checked into Version Control Systems,
2 | # as it contains information specific to your local configuration.
3 | #
4 | # Location of the SDK. This is only used by Gradle.
5 | # For customization when using a Version Control System, please read the
6 | # header note.
7 | #Wed May 01 13:51:58 IRST 2024
8 | sdk.dir=/Users/sepideh/Library/Android/sdk
9 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_border_item_white.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
8 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 |
16 | rootProject.name = "Android Socket"
17 | include(":app")
18 |
--------------------------------------------------------------------------------
/app/src/main/java/ir/example/androidsocket/utils/ValidationUtils.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.utils
2 |
3 | import androidx.core.text.isDigitsOnly
4 |
5 |
6 | fun isIpValid(ip: String): Boolean {
7 | for (i in ip) {
8 | if ((i != '.') && (!i.isDigit()))
9 | return false
10 | }
11 | return true
12 | }
13 |
14 | fun isPortValid(port: String): Boolean {
15 | return port.isDigitsOnly()
16 | }
17 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_border_item_black.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
8 |
9 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/java/ir/example/androidsocket/SocketConnectionListener.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket
2 |
3 | interface SocketConnectionListener {
4 | fun onStart()
5 | fun onConnected()
6 | fun onMessage(messageContentType : Int?, message: String?)
7 | fun onProgressUpdate(progress : Int)
8 | fun onDisconnected(code: Int?, reason: String?)
9 | fun onError(exception: Exception?)
10 | fun onException(exception: Exception?)
11 | }
--------------------------------------------------------------------------------
/app/src/main/aidl/ir/sep/android/Service/IProxy.aidl:
--------------------------------------------------------------------------------
1 | // IProxy.aidl
2 | package ir.sep.android.Service;
3 | import android.graphics.Bitmap;
4 |
5 | interface IProxy {
6 |
7 |
8 | int VerifyTransaction(in int appId, in String refNum,String resNum);
9 |
10 | int ReverseTransaction(in int appId,in String refNum,String resNum);
11 |
12 | int PrintByRefNum(String refNum);
13 |
14 | int PrintByBitmap(in Bitmap bitmap);
15 |
16 | int PrintByString( String string);
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/java/ir/example/androidsocket/ui/theme/AppLayoutDirection.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.ui.theme
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.CompositionLocalProvider
5 | import androidx.compose.runtime.ProvidedValue
6 | import androidx.compose.ui.unit.LayoutDirection
7 |
8 | @Composable
9 | fun AppLayoutDirection(direction: ProvidedValue, content: @Composable () -> Unit) {
10 | CompositionLocalProvider(direction, content = content)
11 | }
--------------------------------------------------------------------------------
/app/src/server/java/ir/example/androidsocket/socket/SocketServer.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.socket
2 |
3 | import ir.example.androidsocket.SocketConnectionListener
4 | import java.io.File
5 |
6 | interface SocketServer {
7 | val serverPort: Int
8 | val path: File?
9 | val socketListener: List
10 |
11 | fun startServer()
12 | fun stopServer()
13 | fun isPortAvailable(): Boolean
14 | suspend fun sendMessageWithTimeout(message : String,timeoutMillis : Long)
15 | }
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/client/java/ir/example/androidsocket/client/SocketClient.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.client
2 |
3 | import android.net.Uri
4 | import ir.example.androidsocket.SocketConnectionListener
5 |
6 | interface SocketClient {
7 |
8 | var ip : String
9 | var port : String
10 | val socketListener: List
11 |
12 | suspend fun connectWithTimeout(timeoutMillis: Long = 90000)
13 | fun sendMessage(message: String,timeoutMillis : Long)
14 | fun sendFile(uri: Uri)
15 | fun onMessage(message: String?)
16 | fun disconnect()
17 | }
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/.idea/deploymentTargetSelector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | permission is required
4 | cancel
5 | disconnected : %s
6 | error : %s
7 | connection status:
8 | ip
9 | port
10 | "protocol type : "
11 | "Connecting"
12 | "there is no wifi or ethernet available"
13 |
14 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/main/java/ir/example/androidsocket/ui/theme/Spacing.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.ui.theme
2 |
3 | import androidx.compose.material3.MaterialTheme
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.ReadOnlyComposable
6 | import androidx.compose.runtime.staticCompositionLocalOf
7 | import androidx.compose.ui.unit.Dp
8 | import androidx.compose.ui.unit.dp
9 |
10 |
11 | data class Spacing(
12 | val extraSmall: Dp = 4.dp,
13 | val small: Dp = 8.dp,
14 | val medium: Dp = 16.dp,
15 | val extraMedium: Dp = 24.dp,
16 | val large: Dp = 32.dp,
17 | val extraLarge: Dp = 64.dp,
18 | )
19 |
20 |
21 | val LocalSpacing = staticCompositionLocalOf { Spacing() }
22 |
23 | val MaterialTheme.spacing: Spacing
24 | @Composable
25 | @ReadOnlyComposable
26 | get() = LocalSpacing.current
--------------------------------------------------------------------------------
/app/src/client/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Android Client
3 |
4 | %s Client
5 | connect to server
6 | disconnect
7 | send message
8 | send file
9 | server message :
10 | message
11 | message is empty
12 | waiting for server confirmation ...
13 | notification permission is mandatory to inform the user about the service which is running
14 |
15 |
--------------------------------------------------------------------------------
/app/src/server/java/ir/example/androidsocket/ui/ServerEvent.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.ui
2 |
3 | import android.content.Context
4 | import ir.example.androidsocket.Constants
5 |
6 | sealed class ServerEvent {
7 | data class SetLoading(val value: Boolean) : ServerEvent()
8 | data class SetProtocolType(val type : String ,val connectionType : Constants.ConnectionType) : ServerEvent()
9 | data class SetIsConnecting(val isConnecting : Boolean) : ServerEvent()
10 | data class SetSocketConnectionStatus(val status: Constants.SocketStatus) : ServerEvent()
11 | data class SetClientMessage(val message: String) : ServerEvent()
12 | data class SetFileIsSaved(val saved : Boolean) : ServerEvent()
13 | data class GetWifiIpAddress(val context: Context) : ServerEvent()
14 | data class GetLanIpAddress(val context: Context) : ServerEvent()
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/ir/example/androidsocket/MainApplication.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket
2 |
3 | import android.app.Application
4 | import androidx.appcompat.app.AppCompatDelegate
5 | import dagger.hilt.android.HiltAndroidApp
6 |
7 | @HiltAndroidApp
8 | class MainApplication : Application() {
9 |
10 |
11 | companion object {
12 | var isAppInForeground = false
13 | private set
14 | }
15 |
16 |
17 | override fun onCreate() {
18 | super.onCreate()
19 | AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
20 | }
21 |
22 | fun notifyAppForeground() {
23 | if (!isAppInForeground) {
24 | isAppInForeground = true
25 | }
26 | }
27 |
28 | fun notifyAppBackground() {
29 | if (isAppInForeground) {
30 | isAppInForeground = false
31 | }
32 | }
33 |
34 | }
--------------------------------------------------------------------------------
/app/src/client/java/ir/example/androidsocket/client/TcpSocketClient.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.client
2 |
3 | import ir.example.androidsocket.Constants
4 | import ir.example.androidsocket.utils.clientLog
5 | import kotlinx.coroutines.flow.MutableStateFlow
6 | import kotlinx.coroutines.flow.StateFlow
7 | import kotlinx.coroutines.flow.asStateFlow
8 | import java.net.Socket
9 |
10 | object TcpSocketClient {
11 | var socket : Socket ? = null
12 | const val BUFFER_SIZE = 1024
13 |
14 | private val _socketStatus = MutableStateFlow(Constants.SocketStatus.DISCONNECTED)
15 | val socketStatus: StateFlow get() = _socketStatus.asStateFlow()
16 |
17 | fun updateSocketStatus(connectionStatus: Constants.SocketStatus) {
18 | clientLog("updateSocketStatus $connectionStatus")
19 | _socketStatus.value = connectionStatus
20 | }
21 |
22 |
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/ir/example/androidsocket/utils/NotificationMessageBroadcastReceiver.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.utils
2 |
3 | import android.content.BroadcastReceiver
4 | import android.content.Context
5 | import android.content.Intent
6 | import ir.example.androidsocket.Constants
7 |
8 | class NotificationMessageBroadcastReceiver(
9 | private val onMessageReceivedAction: () -> Unit = {},
10 | ) : BroadcastReceiver() {
11 |
12 | override fun onReceive(context: Context?, intent: Intent?) {
13 | serverLog(message = "NotificationBroadcastReceiver onReceive")
14 |
15 | val packageName = context?.packageName
16 | packageName?.let {
17 | when (intent?.action) {
18 | Constants.ActionCode.NotificationMessage.title -> {
19 | onMessageReceivedAction()
20 | }
21 |
22 | else -> {}
23 | }
24 | }
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/app/src/main/java/ir/example/androidsocket/Constants.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket
2 |
3 | object Constants {
4 |
5 | const val CLIENT_MESSAGE_NOTIFICATION_ID = 10
6 |
7 | enum class SocketStatus(
8 | val title: String,
9 | ) {
10 | CONNECTED("Connected"), DISCONNECTED("Disconnected")
11 | }
12 |
13 | enum class ConnectionType(
14 | val title: String
15 | ) {
16 | NONE("none"), WIFI("wifi"), ETHERNET("ethernet")
17 | }
18 |
19 | enum class ActionCode(val title: String) {
20 | NotificationMessage(
21 | title = "client message"
22 | ),
23 | }
24 |
25 | enum class ProtocolType(val title: String) {
26 | WEBSOCKET("Websocket"), TCP("Tcp")
27 | }
28 |
29 | val PROTOCOLS = listOf(ProtocolType.WEBSOCKET.title, ProtocolType.TCP.title)
30 |
31 | object MessageConstantType {
32 | const val MESSAGE_TYPE_TEXT_CONTENT = 0x01
33 | const val MESSAGE_TYPE_FILE_CONTENT = 0x02
34 | }
35 |
36 |
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/ir/example/androidsocket/utils/ByteUtils.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.utils
2 |
3 | object BytesUtils {
4 | private const val GBK = "GBK"
5 | const val UTF_8 = "utf-8"
6 | private val ASCII = "0123456789ABCDEF".toCharArray()
7 | private val HEX_VOCABLE = charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F')
8 |
9 | fun hexToString(hex: String): String {
10 | val output = StringBuilder()
11 | for (i in hex.indices step 2) {
12 | val str = hex.substring(i, i + 2)
13 | output.append(str.toInt(16).toChar())
14 | }
15 | return output.toString()
16 | }
17 |
18 | fun bytesToHex(bs: ByteArray): String {
19 | val sb = StringBuilder()
20 |
21 | for (b in bs) {
22 | val high = (b.toInt() shr 4) and 15
23 | val low = b.toInt() and 15
24 | sb.append(HEX_VOCABLE[high])
25 | sb.append(HEX_VOCABLE[low])
26 | }
27 |
28 | return sb.toString()
29 | }
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/ir/example/androidsocket/ui/base/AppIcon.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.ui.base
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.layout.size
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.graphics.Color
9 | import androidx.compose.ui.graphics.ColorFilter
10 | import androidx.compose.ui.res.painterResource
11 | import androidx.compose.ui.unit.dp
12 |
13 |
14 | @Composable
15 | fun AppIcon(
16 | modifier: Modifier = Modifier,
17 | contentDescription: String,
18 | enable: Boolean,
19 | enableSource: Int,
20 | disableSource: Int,
21 | ) {
22 | Image(
23 | modifier = modifier.size(80.dp),
24 | painter = if (enable) painterResource(id = enableSource) else painterResource(
25 | id = disableSource
26 | ),
27 | colorFilter = ColorFilter.tint(if (enable) MaterialTheme.colorScheme.onSecondary else Color.LightGray),
28 | contentDescription = contentDescription
29 | )
30 | }
--------------------------------------------------------------------------------
/app/src/main/assets/csr.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE REQUEST-----
2 | MIIC1zCCAb8CAQAwXTELMAkGA1UEBhMCaXIxDTALBgNVBAgMBGlyYW4xDzANBgNV
3 | BAcMBnRlaHJhbjEOMAwGA1UECgwFZW5pYWMxDjAMBgNVBAsMBWVuaWFjMQ4wDAYD
4 | VQQDDAVlbmlhYzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALZVL7t9
5 | o6EWy+XxY+k3Aoy1VTE23eT/zscrPDuZbdtpBeeVj4/hboleySQsQfHki4MvYck9
6 | hZ2Is3VRyNy5f7ekEKMhSEqWExOsdRFvMC4vlf28RYwvOwxDzk5NC0S6ZFZhZYC9
7 | 9VG3rll85JmT4yHlLXWbaxX1aAxkdGGSb8JMiN4flQFfjSD1x1wtKsR5FsLP4Hk+
8 | mPlb6KI9u8ZAWA6ifFjH1/n3YIs8b666jN+T7sPKx0Y1y8lL2xXf0goS+E4sDBS0
9 | hbKtVAPJuQiQgthOIwjpzlCvunMUxoGj6udqaMreO0+IG27mQjAX8OU3gWrhaqpS
10 | jYZC+wp/wy8qHy8CAwEAAaA1MBQGCSqGSIb3DQEJAjEHDAVlbmlhYzAdBgkqhkiG
11 | 9w0BCQcxEAwOc29yZW5hc29ja2V0MDEwDQYJKoZIhvcNAQELBQADggEBAAYPkHQh
12 | 47FuS0kHItS7zw6vsF7hCbVgqADUtwja6GyUSe5WPCngIVJW4gLvXOhF8UzLF940
13 | 4w584H/oqkN5Ypm5MHEKr02p0vtLRG5mk4pUR+KTl7XUaUZnGEEmjQZjR1ePK9+X
14 | vmYaHe7UtdJPgVNliAnyVdRVECtYyuPG5PfnUVcdYcMRqjTSyd4SJ9lPRj126L+Q
15 | yrCCdH6nAx4QZxwf4J/kuJioZilBYOE4Si3govSmX/LOd2yPe1jIj1MCIVaOci54
16 | +va8xb95eXQUEZNyNAMmSTOJOF8S+TfQSjJPow2PM25ypsuN+LjWfKPPMFvlRN5D
17 | bl/mB9/7prxcaFU=
18 | -----END CERTIFICATE REQUEST-----
19 |
--------------------------------------------------------------------------------
/app/src/client/java/ir/example/androidsocket/ui/ClientEvent.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.ui
2 |
3 | import android.content.Context
4 | import android.net.Uri
5 | import ir.example.androidsocket.Constants
6 |
7 | sealed class ClientEvent {
8 | data class StartClientService(val context: Context) : ClientEvent()
9 | data class SetLoading(val value: Boolean) : ClientEvent()
10 | data class SetInConnectionProcess(val value: Boolean) : ClientEvent()
11 | data class SetProtocolType(val type : String) : ClientEvent()
12 | data class SetServerIp(val ip: String) : ClientEvent()
13 | data class SetServerPort(val port: String) : ClientEvent()
14 | data class SetSocketConnectionStatus(val status: Constants.SocketStatus) : ClientEvent()
15 | data class SetClientMessage(val message: String) : ClientEvent()
16 | data class SetFileUrl(val uri: Uri?) : ClientEvent()
17 | data class SetServerMessage(val message: String) : ClientEvent()
18 | data class SendMessageToServer(val message: String) : ClientEvent()
19 | data object OnConnectionButtonClicked : ClientEvent()
20 | data object ResetClientMessage : ClientEvent()
21 | }
--------------------------------------------------------------------------------
/app/src/main/java/ir/example/androidsocket/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/app/src/server/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Android Server
3 |
4 | %s Server
5 | connection type
6 | wifi
7 | ethernet
8 | client message
9 | ip address :
10 | port :
11 | wifi connection
12 | ethernet connection
13 | notification permission is mandatory to inform the user about the service which is running and about the received message from client when server is in background
14 | write external storage permission is mandatory to save received file by server in external storage
15 | a file is received by server
16 | the file is saved in download folder
17 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/assets/certificate.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDQTCCAikCFC6MD73MjuzCd1dI8TJ57dMDwM60MA0GCSqGSIb3DQEBCwUAMF0x
3 | CzAJBgNVBAYTAmlyMQ0wCwYDVQQIDARpcmFuMQ8wDQYDVQQHDAZ0ZWhyYW4xDjAM
4 | BgNVBAoMBWVuaWFjMQ4wDAYDVQQLDAVlbmlhYzEOMAwGA1UEAwwFZW5pYWMwHhcN
5 | MjQwNTEyMTMyNDQ5WhcNMzQwNTEwMTMyNDQ5WjBdMQswCQYDVQQGEwJpcjENMAsG
6 | A1UECAwEaXJhbjEPMA0GA1UEBwwGdGVocmFuMQ4wDAYDVQQKDAVlbmlhYzEOMAwG
7 | A1UECwwFZW5pYWMxDjAMBgNVBAMMBWVuaWFjMIIBIjANBgkqhkiG9w0BAQEFAAOC
8 | AQ8AMIIBCgKCAQEAtlUvu32joRbL5fFj6TcCjLVVMTbd5P/Oxys8O5lt22kF55WP
9 | j+FuiV7JJCxB8eSLgy9hyT2FnYizdVHI3Ll/t6QQoyFISpYTE6x1EW8wLi+V/bxF
10 | jC87DEPOTk0LRLpkVmFlgL31UbeuWXzkmZPjIeUtdZtrFfVoDGR0YZJvwkyI3h+V
11 | AV+NIPXHXC0qxHkWws/geT6Y+Vvooj27xkBYDqJ8WMfX+fdgizxvrrqM35Puw8rH
12 | RjXLyUvbFd/SChL4TiwMFLSFsq1UA8m5CJCC2E4jCOnOUK+6cxTGgaPq52poyt47
13 | T4gbbuZCMBfw5TeBauFqqlKNhkL7Cn/DLyofLwIDAQABMA0GCSqGSIb3DQEBCwUA
14 | A4IBAQCCBoes5W8K43w+Pviw+ek5YA6zjmkYG1uazHGm2mY0etVhu6/xIXBpbRdr
15 | UQLkA92oCvuidyBmOSqudY2NnQzBjDaJ2gwLhR/XC2UOR+/SEpjuwr9aBQKKW9Tm
16 | 4sFve6zSumZ5gpquGf7Xj6uqT84K/vj08kYjoFwJbf70/iI/kBQr41qvWBs+A1YG
17 | 70tDjGAr+wSvNb/G7E0pBhYbKr1zeCHBCPTW+Nh+8vRckDCaDwBttBM/qR8IOCCN
18 | g8VWXslkte+ExJxXNahhOGeOk9glvONr0ZvW5PwZ1bnUqaWhXNIy66x/LkcI4OIw
19 | 73xyyLSC8qOyTR9aUetswlzOulfR
20 | -----END CERTIFICATE-----
21 |
--------------------------------------------------------------------------------
/app/src/main/java/ir/example/androidsocket/utils/CaptureBitmap.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.utils
2 |
3 | import android.graphics.Bitmap
4 | import android.view.View
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.runtime.remember
7 | import androidx.compose.ui.platform.ComposeView
8 | import androidx.compose.ui.platform.LocalContext
9 | import androidx.compose.ui.viewinterop.AndroidView
10 | import androidx.core.view.drawToBitmap
11 |
12 | @Composable
13 | fun captureBitmap(
14 | content: @Composable () -> Unit,
15 | ): () -> Bitmap {
16 |
17 | val context = LocalContext.current
18 |
19 | /**
20 | * ComposeView that would take composable as its content
21 | * Kept in remember so recomposition doesn't re-initialize it
22 | **/
23 | val composeView = remember { ComposeView(context) }
24 |
25 | /**
26 | * Callback function which could get latest image bitmap
27 | **/
28 | fun captureBitmap(): Bitmap {
29 | return composeView.drawToBitmap()
30 | }
31 |
32 |
33 | AndroidView(
34 | factory = {
35 | composeView.apply {
36 | setContent {
37 | content.invoke()
38 | }
39 | }
40 | }
41 | )
42 |
43 | composeView.visibility = View.GONE
44 |
45 | return ::captureBitmap
46 |
47 | }
--------------------------------------------------------------------------------
/app/src/main/java/ir/example/androidsocket/ui/base/KeyboardListener.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.ui.base
2 |
3 | import androidx.compose.ui.Modifier
4 | import androidx.compose.ui.composed
5 | import androidx.compose.ui.input.key.onKeyEvent
6 |
7 | fun Modifier.customKeyboard(
8 | onEnterPressed: () -> Unit,
9 | onDeletePressed: () -> Unit,
10 | ): Modifier = composed {
11 |
12 | this.onKeyEvent {
13 |
14 | when (it.nativeKeyEvent.keyCode) {
15 | KeyType.KEY_ENTER.value -> {
16 |
17 | onEnterPressed()
18 | true
19 | }
20 | KeyType.KEY_DELETE.value -> {
21 | onDeletePressed()
22 | true
23 | }
24 | else -> {
25 | false
26 | }
27 | }
28 | }
29 |
30 | }
31 |
32 | enum class KeyType(val value: Int) {
33 | KEY_ENTER(value = 66),
34 | KEY_DELETE(value = 67),
35 | KEY_BACK(value = 4),
36 | KEY_UP(value = 19),
37 | KEY_DOWN(value = 20),
38 | KEY_NUMBER_0(value = 7),
39 | KEY_NUMBER_1(value = 8),
40 | KEY_NUMBER_2(value = 9),
41 | KEY_NUMBER_3(value = 10),
42 | KEY_NUMBER_4(value = 11),
43 | KEY_NUMBER_5(value = 12),
44 | KEY_NUMBER_6(value = 13),
45 | KEY_NUMBER_7(value = 14),
46 | KEY_NUMBER_8(value = 15),
47 | KEY_NUMBER_9(value = 16);
48 | }
49 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/app/src/main/java/ir/example/androidsocket/ui/base/BaseViewModel.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.ui.base
2 |
3 | import android.os.Build
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.lifecycle.ViewModel
6 | import ir.example.androidsocket.Constants
7 | import ir.example.androidsocket.utils.serverLog
8 | import kotlinx.coroutines.flow.MutableStateFlow
9 |
10 | internal abstract class BaseViewModel() : ViewModel() {
11 |
12 | var uiEvent = MutableStateFlow(BaseUiEvent.None)
13 | private set
14 |
15 | val loading = mutableStateOf(false)
16 |
17 | var openNotificationPermissionDialog = MutableStateFlow(false)
18 |
19 | var notificationPermissionGranted = MutableStateFlow(Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU)
20 |
21 | var selectedProtocol = MutableStateFlow(Constants.ProtocolType.WEBSOCKET)
22 |
23 | private fun sendUiEvent(event: BaseUiEvent) {
24 | uiEvent.value = event
25 | }
26 |
27 | fun emitMessageValue(messageId: Int?, vararg parameters: String? = emptyArray()) {
28 | serverLog("emitMessageValue : $messageId","progressCheck")
29 | sendUiEvent(BaseUiEvent.ShowToast(messageId, parameters))
30 | }
31 |
32 | fun setOpenNotificationPermissionDialog(value : Boolean){
33 | openNotificationPermissionDialog.value= value
34 | }
35 |
36 | fun setNotificationGranted(value : Boolean){
37 | notificationPermissionGranted.value= value
38 | }
39 |
40 | }
--------------------------------------------------------------------------------
/app/src/client/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
15 |
20 |
21 |
22 |
23 |
24 |
25 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/assets/key.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC2VS+7faOhFsvl
3 | 8WPpNwKMtVUxNt3k/87HKzw7mW3baQXnlY+P4W6JXskkLEHx5IuDL2HJPYWdiLN1
4 | UcjcuX+3pBCjIUhKlhMTrHURbzAuL5X9vEWMLzsMQ85OTQtEumRWYWWAvfVRt65Z
5 | fOSZk+Mh5S11m2sV9WgMZHRhkm/CTIjeH5UBX40g9cdcLSrEeRbCz+B5Ppj5W+ii
6 | PbvGQFgOonxYx9f592CLPG+uuozfk+7DysdGNcvJS9sV39IKEvhOLAwUtIWyrVQD
7 | ybkIkILYTiMI6c5Qr7pzFMaBo+rnamjK3jtPiBtu5kIwF/DlN4Fq4WqqUo2GQvsK
8 | f8MvKh8vAgMBAAECggEAALFYKhUzHvSQN8JwB73Ow6wYs8QqmakjJvySEQPnN/Mx
9 | M40M65YGrnzutLV6VqnwocH3VCVosY2IaQSzh6sowhDcg1IoDSh+0A9SrotGmpZR
10 | spIZjYvMZKKJUSDXzx4KLY3XLeMK4JGbFvnzgGXgcguF3bGGeaHiEFH9YzWEOS/B
11 | nOEAEr826sdTTyYqSldt53+AiANBz4Lt/QP9XSiGScfp7Aw7vmDXT8Cg2Acd4hC2
12 | BvUKyFp8HWKREmHUOOCqjPtcHWce0KAsSJmMdijNoXvCur6bdY247gJGFP5gKdwF
13 | w9PIMGKX/bR7nAtPyXzcHp1mSjYR1F3KEfdLVwgzGQKBgQDe8E1/yhPfhsmuw/mW
14 | +gfsH6rbrpPTXPjSmlfK9COs1JHUSg/hDYQoS0nkCHaniFFsm0vDrF2gMoSKh4Dv
15 | cvryEnS9YZbFt3cHkHdQ4AmGcmeS3+AF0z6dFmyHKsmlcHqcaBAXh0qjtcn1ftfK
16 | gyOr8tKb7xyMrjdWmdSOR4nE+wKBgQDRX0+P0j6od/7E1F9VlUS0eqFj4Hmaqww1
17 | 9WotU8TR+yR++PfsA37ZDaG9Wjm01RtYWL8GR1IIxFfPEQr8mDyYlkgr/OYCCqVt
18 | lWwL7acSuMZ5Bbx3Go5u2+8X/v4X/GZq3qT/oF37A6zwpgW4lNFpB3W3fCBe+2Sj
19 | qd5MN0OwXQKBgQDVx/+5JBGMjpdGJJ1pvpfsQK25/Z252HSul9zKPPUc3bZ9qQuz
20 | Fx/8972Cku8dViYmjIDJwbcCUI8yvB/S/iJQEYyqsjM/o9/bfBg+kKfxqjFR8abE
21 | Tz7Cqmcl8szs/YBGDbAor87OUBu7c4gApWKGl9KIP3HUIZflTpw6V7VwEwKBgHrO
22 | 5hMdJkWZBOYdD8JaaM9X8txrqchwNhxcVCg8L4FfRzv2+y0Dq97S4SD5EEoiigSb
23 | IYkkQlkEGeFKROB+x2RVGgY7NArUhc4uTA7/GfWgTkJke/R8rjkWZjr0BcS59rUO
24 | 3UdXoGiA8mrBZy+qkt6BUqoKc85itNhO5iZccCa9AoGBAKSum7EE2z8aY4TnA4yJ
25 | 3WwoL1ZbYGQX0eUEYQB3CFfXC+oj1o46PjzE2M48Qh5zmYG74SkQQLotDOgw4ovj
26 | feWMhULmPCbNQ2tB2Txx2YFCaTsRIqIlDpAViXpoVx6PGDefXPp/pLf64vm7UpFZ
27 | m0u+pN3NpKFjeYMk/KNTK1D1
28 | -----END PRIVATE KEY-----
29 |
--------------------------------------------------------------------------------
/app/src/server/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
16 |
21 |
22 |
23 |
24 |
25 |
26 |
30 |
31 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/java/ir/example/androidsocket/ui/base/AppButton.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.ui.base
2 |
3 | import androidx.compose.foundation.BorderStroke
4 | import androidx.compose.foundation.shape.RoundedCornerShape
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.material3.Button
7 | import androidx.compose.material3.ButtonColors
8 | import androidx.compose.material3.ButtonDefaults
9 | import androidx.compose.material.Text
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.graphics.Color
13 | import androidx.compose.ui.text.style.TextAlign
14 | import androidx.compose.ui.tooling.preview.Preview
15 | import ir.example.androidsocket.ui.theme.spacing
16 |
17 | @Composable
18 | fun AppButton(
19 | modifier: Modifier = Modifier,
20 | border: BorderStroke? = null,
21 | text: String,
22 | enabled: Boolean = true,
23 | textColor: Color = Color.White,
24 | colors: ButtonColors = ButtonDefaults.buttonColors(
25 | disabledContainerColor = Color.LightGray,
26 | contentColor = MaterialTheme.colorScheme.primary,
27 |
28 | ),
29 | onClick: () -> Unit
30 | ) {
31 | Button(
32 | modifier = modifier, onClick = { onClick() },
33 | colors = colors,
34 | enabled = enabled,
35 | shape = RoundedCornerShape( MaterialTheme.spacing.medium),
36 | border = border
37 | ) {
38 | Text(
39 | text = text,
40 | style = MaterialTheme.typography.labelLarge,
41 | color = textColor,
42 | textAlign = TextAlign.Center
43 | )
44 | }
45 | }
46 |
47 |
48 | @Preview(showBackground = true)
49 | @Composable
50 | fun AppButtonPreview() {
51 | AppButton(
52 | modifier = Modifier,
53 | text = "click",
54 | enabled = true,
55 | onClick = {},
56 | )
57 | }
--------------------------------------------------------------------------------
/app/src/main/java/ir/example/androidsocket/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Green900 = Color(0xFF33691E)
6 | val Green400 = Color(0xFF9CCC65)
7 |
8 | sealed class AppColors(
9 | val primary: Color,
10 | val onPrimary: Color,
11 | val primaryContainer: Color,
12 | val onPrimaryContainer: Color,
13 | val secondary: Color,
14 | val onSecondary: Color,
15 | val tertiary: Color,
16 | val onTertiary: Color,
17 | val surface: Color,
18 | val onSurface: Color,
19 | val background: Color,
20 | val error: Color,
21 | val onError: Color,
22 |
23 | ) {
24 |
25 | data object Dark : AppColors(
26 | primary = Color(0xFF3462CD),
27 | onPrimary = Color(0xFFFFFFFF),
28 | primaryContainer = Color(0xFFdbe1ff),
29 | onPrimaryContainer = Color(0xFF001849),
30 | secondary = Color(0xFF33691E),
31 | onSecondary = Color(0xFF9CCC65),
32 | tertiary = Color(0xFFCDDC39),
33 | onTertiary = Color(0xFFCDDC39),
34 | surface = Color(0xFFfaf8ff),
35 | onSurface = Color(0xFFdad9e0),
36 | background = Color(0xFFdad9e0),
37 | error = Color(0xFFA8ADBD),
38 | onError = Color(0xFFA8ADBD)
39 | )
40 |
41 | data object Light : AppColors(
42 | primary = Color(0xFF3462CD),
43 | onPrimary = Color(0xFFffffff),
44 | primaryContainer = Color(0xFFd6e3ff),
45 | onPrimaryContainer = Color(0xFF001849),
46 | secondary = Color(0xFF33691E),
47 | onSecondary = Color(0xFF9CCC65),
48 | tertiary = Color(0xFFFDD835),
49 | onTertiary = Color(0xFFffffff),
50 | surface = Color(0xFFEBEBF0),
51 | onSurface = Color(0xFF44483d),
52 | background = Color(0xFFFFFFFF),
53 | error = Color(0xFFba1a1a),
54 | onError = Color(0xFFffffff)
55 | )
56 | }
57 |
58 |
--------------------------------------------------------------------------------
/app/src/main/java/ir/example/androidsocket/ui/base/ProtocolTypeMenu.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.ui.base
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.material.DropdownMenuItem
6 | import androidx.compose.material3.DropdownMenu
7 | import androidx.compose.material3.MaterialTheme
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.geometry.Offset
11 | import androidx.compose.ui.graphics.Color
12 | import androidx.compose.ui.graphics.Color.Companion.DarkGray
13 | import androidx.compose.ui.unit.DpOffset
14 | import androidx.compose.ui.unit.dp
15 | import ir.example.androidsocket.Constants
16 |
17 | @Composable
18 | fun ProtocolTypeMenu(
19 | expanded: Boolean,
20 | protocols: List,
21 | selectedProtocol: Constants.ProtocolType,
22 | onProtocolSelected: (String) -> Unit,
23 | onDismissClicked: () -> Unit,
24 | ) {
25 |
26 | DropdownMenu(
27 | modifier = Modifier.background(MaterialTheme.colorScheme.surface),
28 | offset = DpOffset((-90).dp,0.dp),
29 | expanded = expanded,
30 | onDismissRequest = { onDismissClicked() }) {
31 | protocols.forEachIndexed { index, s ->
32 | DropdownMenuItem(
33 | modifier = Modifier.background(if (selectedProtocol.title == s) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface),
34 | onClick = {
35 | onProtocolSelected(s)
36 | }) {
37 | AppText(
38 | text = s,
39 | textColor = if (selectedProtocol.title == s) Color.White else DarkGray
40 | )
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/main/java/ir/example/androidsocket/utils/ConnectionTypeManager.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.utils
2 |
3 | import android.content.Context
4 | import android.net.ConnectivityManager
5 | import android.net.NetworkCapabilities
6 | import ir.example.androidsocket.Constants
7 | import kotlinx.coroutines.flow.MutableStateFlow
8 |
9 |
10 | class ConnectionTypeManager(context: Context) {
11 |
12 | private val connectivityManager =
13 | context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
14 |
15 | var connectionType: MutableStateFlow =
16 | MutableStateFlow(Constants.ConnectionType.NONE)
17 | private set
18 |
19 | var isEthernetConnected: MutableStateFlow = MutableStateFlow(null)
20 | private set
21 |
22 | var isWifiConnected: MutableStateFlow = MutableStateFlow(null)
23 | private set
24 |
25 | init {
26 | checkConnectionStatus()
27 | }
28 |
29 | fun checkConnectionStatus() {
30 | val networkCapability =
31 | connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
32 | if (networkCapability?.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) == true) {
33 | serverLog("checkEthernetStatus->ETHERNET")
34 | connectionType.value = Constants.ConnectionType.ETHERNET
35 | isEthernetConnected.value = true
36 | } else if (networkCapability?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true) {
37 | serverLog("checkEthernetStatus->WIFI")
38 | connectionType.value = Constants.ConnectionType.WIFI
39 | isWifiConnected.value = true
40 | } else {
41 | serverLog("checkEthernetStatus->NONE")
42 | connectionType.value = Constants.ConnectionType.NONE
43 | isWifiConnected.value = false
44 | isEthernetConnected.value = false
45 | }
46 | }
47 | }
--------------------------------------------------------------------------------
/app/src/server/java/ir/example/androidsocket/socket/ServerManager.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.socket
2 |
3 | import ir.example.androidsocket.Constants
4 | import ir.example.androidsocket.utils.serverLog
5 | import kotlinx.coroutines.CoroutineScope
6 | import kotlinx.coroutines.Dispatchers
7 | import kotlinx.coroutines.delay
8 | import kotlinx.coroutines.launch
9 | import okio.Timeout
10 |
11 | class ServerManager(
12 | private val socketProtocol: Constants.ProtocolType,
13 | private val websocketServerManger: WebsocketServerManger,
14 | private val tcpSocketManager: TcpServerManager,
15 | ) {
16 |
17 | private lateinit var serverManager: SocketServer
18 |
19 | fun startServer() {
20 | serverManager = when (socketProtocol) {
21 | Constants.ProtocolType.WEBSOCKET -> websocketServerManger
22 | Constants.ProtocolType.TCP -> tcpSocketManager
23 | }
24 | // check to prevent starting a server on a port that is already in use, which would cause a conflict and result in an error.
25 | if (!serverManager.isPortAvailable()) {
26 | stopServer()
27 | }
28 |
29 | CoroutineScope(Dispatchers.IO).launch {
30 | try {
31 | //some times it gets time to release the port
32 | delay(5000)
33 | serverManager.startServer()
34 | } catch (e: Exception) {
35 | serverLog("ServerManager startServer exception: ${e.message}")
36 | }
37 | }
38 | }
39 |
40 | /**
41 | * when client message is received by server , server send a confirmation message to client
42 | * **/
43 | suspend fun sendMessageWithTimeout(message: String, timeoutMillis: Long = 20000) {
44 | serverManager.sendMessageWithTimeout(message,timeoutMillis)
45 | }
46 |
47 | fun stopServer() {
48 | serverLog("ServerManager stopServer")
49 | serverManager.stopServer()
50 | }
51 |
52 |
53 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### Introduction
2 | The Android Socket Project is a robust Android application that enables real-time, full-duplex communication between devices using both WebSocket and TCP protocols. The project leverages Android flavors to separate server-side and client-side implementations, providing flexibility and modularity in code. Utilizing Android foreground services, it maintains persistent connections and supports file transfer, enabling the exchange of both text messages and files between devices. Whenever a message is received from a client by the server, the application displays it as a notification, ensuring users are informed of real-time interactions. Additionally, a Wi-Fi and Ethernet connection observer monitors network connectivity, ensuring stable communication across network interfaces.
3 |
4 | ### Features
5 | - Foreground Service: Ensures persistent connections by running in the foreground, enhancing reliability for long-running communication tasks.
6 | - Server and Client Implementations with Flavors: Separates server and client code using Android flavors, providing both functionalities for WebSocket and TCP protocols to enable real-time, full-duplex data exchange.
7 | - Network Connectivity Monitoring: Actively monitors Wi-Fi and Ethernet connections, ensuring stable communication across various network interfaces.
8 | - Dual Protocol Support: Establishes reliable WebSocket and TCP connections, allowing flexible communication options between devices.
9 | - File Transfer Capability: Supports file exchange by allowing clients to send files to the server, which automatically saves them in the download folder for easy access.
10 | - Real-Time Notifications: Displays incoming messages as notifications whenever the server receives messages from clients, keeping users informed of real-time interactions.
11 | - Modern Android Stack: Utilizes Jetpack Compose for UI development and is fully implemented in Kotlin.
12 |
13 | ### demo
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/java/ir/example/androidsocket/ui/base/AppText.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.ui.base
2 |
3 | import androidx.compose.material.MaterialTheme
4 | import androidx.compose.material.Text
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Modifier
7 | import androidx.compose.ui.graphics.Color
8 | import androidx.compose.ui.text.TextStyle
9 | import androidx.compose.ui.text.font.FontWeight
10 | import androidx.compose.ui.text.style.TextAlign
11 | import androidx.compose.ui.text.style.TextDecoration
12 | import androidx.compose.ui.text.style.TextOverflow
13 |
14 | enum class TextType {
15 | HEADER,
16 | TEXT,
17 | TEXT2,
18 | TITLE,
19 | SUBTITLE,
20 | BUTTON,
21 | }
22 |
23 | @Composable
24 | fun AppText(
25 | modifier: Modifier = Modifier,
26 | textType: TextType = TextType.TEXT,
27 | text: String,
28 | textColor: Color = Color.DarkGray,
29 | textAlign: TextAlign = TextAlign.Center,
30 | maxLine: Int = 1,
31 | fontWeight: FontWeight = FontWeight.Normal,
32 | textDecoration: TextDecoration? = null,
33 | style: TextStyle = TextStyle()
34 | ) {
35 | Text(
36 | modifier = modifier,
37 | text = text,
38 | color = textColor,
39 | style = style,
40 | textAlign = textAlign,
41 | maxLines = maxLine,
42 | overflow = TextOverflow.Ellipsis,
43 | fontWeight = fontWeight,
44 | )
45 | }
46 |
47 | @Composable
48 | fun styleText(textType: TextType, textDecoration: TextDecoration? = null): TextStyle {
49 | val baseStyle = when (textType) {
50 | TextType.HEADER -> MaterialTheme.typography.h5
51 | TextType.TITLE -> MaterialTheme.typography.h6
52 | TextType.SUBTITLE -> MaterialTheme.typography.subtitle1
53 | TextType.TEXT -> MaterialTheme.typography.body1
54 | TextType.TEXT2 -> MaterialTheme.typography.body2
55 | TextType.BUTTON -> MaterialTheme.typography.button
56 | }
57 |
58 | return baseStyle.copy(
59 | textDecoration = textDecoration // Add the text decoration to the style
60 | )
61 | }
62 |
63 |
--------------------------------------------------------------------------------
/app/src/main/java/ir/example/androidsocket/ui/base/PermisionDialog.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.ui.base
2 |
3 | import androidx.compose.material3.MaterialTheme
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.height
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Alignment
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.graphics.Color
14 | import androidx.compose.ui.res.stringResource
15 | import androidx.compose.ui.unit.dp
16 | import androidx.compose.ui.window.Dialog
17 | import androidx.compose.ui.window.DialogProperties
18 | import com.example.androidSocket.R
19 | import ir.example.androidsocket.ui.theme.spacing
20 |
21 | @Composable
22 | fun PermissionDialog(
23 | modifier: Modifier = Modifier,
24 | permissionReason : Int,
25 | onDismissRequest: () -> Unit,
26 | onGrantClicked: () -> Unit,
27 | ) {
28 | Dialog(
29 | onDismissRequest = onDismissRequest,
30 | properties = DialogProperties(
31 | dismissOnBackPress = true, dismissOnClickOutside = true
32 | )
33 | ) {
34 | Column(
35 | modifier
36 | .fillMaxWidth()
37 | .background(Color.White)
38 | .padding(MaterialTheme.spacing.small),
39 | horizontalAlignment = Alignment.CenterHorizontally
40 | ) {
41 | AppText(
42 | text = stringResource(id = R.string.permission_dialog_title),
43 | textColor = Color.Red,
44 | textType = TextType.TITLE
45 | )
46 | Box(modifier = Modifier
47 | .padding(vertical = MaterialTheme.spacing.small)
48 | .fillMaxWidth()
49 | .height(1.dp)
50 | .background(Color.Red))
51 | AppText(
52 | text = stringResource(id = permissionReason),
53 | maxLine = 5
54 | )
55 | AppButtonsRow(
56 | firstButtonTitle = "grant",
57 | onFirstClicked = { onGrantClicked() },
58 | secondButtonTitle = "cancel"
59 | ) {
60 | onDismissRequest()
61 | }
62 | }
63 |
64 | }
65 |
66 | }
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/app/src/main/java/ir/example/androidsocket/utils/IpAddressManager.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.utils
2 |
3 | import android.content.Context
4 | import android.net.ConnectivityManager
5 | import android.net.NetworkCapabilities
6 | import android.net.wifi.WifiInfo
7 | import android.net.wifi.WifiManager
8 | import java.net.Inet4Address
9 | import java.net.NetworkInterface
10 |
11 |
12 | object IpAddressManager {
13 |
14 | fun getLocalIpAddress(context: Context): Pair {
15 | var wifiIpAddress: String? = null
16 | var ethernetIpAddress: String? = null
17 |
18 | val connectivityManager =
19 | context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
20 |
21 | // WiFi
22 | val wifiNetwork =
23 | connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
24 | if (wifiNetwork?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true) {
25 | wifiIpAddress = getWifiIpAddress(context)
26 | }
27 |
28 | // Ethernet
29 | val ethernetNetwork =
30 | connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
31 | if (ethernetNetwork?.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) == true) {
32 | ethernetIpAddress = getEthernetIpAddress()
33 | }
34 |
35 | return Pair(wifiIpAddress, ethernetIpAddress)
36 | }
37 |
38 | private fun getWifiIpAddress(context: Context): String? {
39 | try {
40 | val wifiManager =
41 | context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
42 | val wifiInfo: WifiInfo = wifiManager.connectionInfo
43 | val ipAddress = wifiInfo.ipAddress
44 |
45 | return formatIpAddress(ipAddress)
46 | } catch (ex: Exception) {
47 | }
48 | return null
49 | }
50 |
51 | private fun getEthernetIpAddress(): String? {
52 | try {
53 | val networkInterfaces = NetworkInterface.getNetworkInterfaces()
54 |
55 | while (networkInterfaces.hasMoreElements()) {
56 | val networkInterface = networkInterfaces.nextElement()
57 |
58 | // Check if the interface is an Ethernet interface
59 | if (networkInterface.name.startsWith("eth") || networkInterface.name.startsWith("eth0")) {
60 | val inetAddresses = networkInterface.inetAddresses
61 |
62 | while (inetAddresses.hasMoreElements()) {
63 | val inetAddress = inetAddresses.nextElement()
64 |
65 | // Check if it's an IPv4 address
66 | if (inetAddress is Inet4Address) {
67 | return inetAddress.hostAddress
68 | }
69 | }
70 | }
71 | }
72 | } catch (ex: Exception) {
73 | }
74 | return null
75 | }
76 |
77 | private fun formatIpAddress(ipAddress: Int): String {
78 | return "${ipAddress and 0xFF}.${(ipAddress shr 8) and 0xFF}.${(ipAddress shr 16) and 0xFF}.${(ipAddress shr 24) and 0xFF}"
79 | }
80 | }
--------------------------------------------------------------------------------
/app/src/main/java/ir/example/androidsocket/ui/base/AppButtonsRow.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.ui.base
2 |
3 | import androidx.compose.material3.MaterialTheme
4 | import androidx.compose.foundation.BorderStroke
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.IntrinsicSize
7 | import androidx.compose.foundation.layout.Row
8 | import androidx.compose.foundation.layout.fillMaxHeight
9 | import androidx.compose.foundation.layout.height
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.material3.ButtonColors
12 | import androidx.compose.material3.ButtonDefaults
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.graphics.Color
17 | import androidx.compose.ui.tooling.preview.Preview
18 | import ir.example.androidsocket.ui.theme.spacing
19 |
20 | @Composable
21 | fun AppButtonsRow(
22 | modifier: Modifier = Modifier,
23 | firstButtonTitle: String,
24 | firstTitleButtonColor: Color = Color.White,
25 | firstButtonColor: ButtonColors = ButtonDefaults.buttonColors(),
26 | onFirstClicked: () -> Unit,
27 | firstButtonBorder: BorderStroke? = null,
28 | firstEnable: Boolean = true,
29 | secondEnable: Boolean = true,
30 | secondButtonTitle: String,
31 | secondTitleButtonColor: Color = Color.White,
32 | secondButtonColor: ButtonColors = ButtonDefaults.buttonColors(),
33 | secondButtonBorder: BorderStroke? = null,
34 | onSecondClick: () -> Unit
35 | ) {
36 |
37 | Row(
38 | modifier.height(IntrinsicSize.Max),
39 | verticalAlignment = Alignment.CenterVertically,
40 | horizontalArrangement = Arrangement.SpaceBetween,
41 | ) {
42 | AppButton(
43 | modifier = Modifier.fillMaxHeight()
44 | .weight(1f).padding(MaterialTheme.spacing.small),
45 | text = firstButtonTitle,
46 | textColor = firstTitleButtonColor,
47 | border = firstButtonBorder,
48 | colors = firstButtonColor,
49 | enabled = firstEnable,
50 | ) {
51 | onFirstClicked()
52 | }
53 | AppButton(
54 | modifier = Modifier.fillMaxHeight().weight(1f).padding(MaterialTheme.spacing.small)
55 | .weight(1f),
56 | border = secondButtonBorder,
57 | text = secondButtonTitle,
58 | textColor = secondTitleButtonColor,
59 | colors = secondButtonColor,
60 | enabled = secondEnable
61 | ) {
62 | onSecondClick()
63 | }
64 | }
65 | }
66 |
67 | @Preview(showBackground = true)
68 | @Composable
69 | fun AppButtonRowPreview() {
70 |
71 | AppButtonsRow(
72 | modifier = Modifier,
73 | firstButtonTitle = "confirm",
74 | onFirstClicked = { },
75 | firstButtonColor = ButtonDefaults.buttonColors(
76 | disabledContainerColor = Color.LightGray,
77 | contentColor = MaterialTheme.colorScheme.primary,
78 | ),
79 | secondButtonTitle = "cancel",
80 | secondEnable = false,
81 | secondButtonColor = ButtonDefaults.buttonColors(
82 | disabledContainerColor = Color.LightGray,
83 | contentColor = MaterialTheme.colorScheme.primary,
84 | )
85 | ) {
86 | }
87 | }
--------------------------------------------------------------------------------
/app/src/main/java/ir/example/androidsocket/utils/NotificationHandler.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.utils
2 |
3 | import android.Manifest
4 | import android.app.Notification
5 | import android.app.NotificationChannel
6 | import android.app.NotificationManager
7 | import android.app.PendingIntent
8 | import android.content.Context
9 | import android.content.pm.PackageManager
10 | import android.media.RingtoneManager
11 | import android.os.Build
12 | import androidx.core.app.ActivityCompat
13 | import androidx.core.app.NotificationManagerCompat
14 |
15 | class NotificationHandler(val context: Context, private val channelId: String) {
16 |
17 | fun createNotificationChannel() {
18 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
19 | val channel = NotificationChannel(
20 | channelId,
21 | "SocketChannel",
22 | NotificationManager.IMPORTANCE_HIGH
23 | )
24 | val manager = context.getSystemService(NotificationManager::class.java)
25 | manager.createNotificationChannel(channel)
26 | }
27 | }
28 |
29 | fun createNotification(
30 | message: String,
31 | title: String = "android socket",
32 | onContentIntent: (Context) -> PendingIntent?
33 | ): Notification {
34 | serverLog("createNotification")
35 | return setNotification(
36 | builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
37 | Notification.Builder(context, channelId)
38 | else Notification.Builder(context),
39 | message = message,
40 | title = title,
41 | onContentIntent = onContentIntent
42 | )
43 | }
44 |
45 | private fun setNotification(
46 | builder: Notification.Builder,
47 | message: String,
48 | title: String,
49 | onContentIntent: (Context) -> PendingIntent?
50 | ): Notification {
51 | serverLog("setNotification")
52 | val soundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
53 |
54 | val notificationBuilder = builder.setContentTitle(title)
55 | .setContentText(message)
56 | .setSmallIcon(android.R.drawable.ic_dialog_info)
57 | .setSound(soundUri)
58 |
59 | notificationBuilder.setContentIntent(onContentIntent(context)).setAutoCancel(true)
60 |
61 | return notificationBuilder.build()
62 | }
63 |
64 |
65 | fun displayNotification(
66 | context: Context,
67 | notificationId: Int,
68 | title: String,
69 | message: String,
70 | onContentIntent: (Context) -> PendingIntent?
71 | ) {
72 | val notification =
73 | createNotification(
74 | title = title,
75 | message = message,
76 | onContentIntent = onContentIntent
77 | )
78 |
79 | val notificationManager = NotificationManagerCompat.from(context)
80 |
81 | //checks whether the app has the necessary permission to post notifications
82 | if (ActivityCompat.checkSelfPermission(
83 | context,
84 | Manifest.permission.POST_NOTIFICATIONS
85 | ) != PackageManager.PERMISSION_GRANTED
86 | ) {
87 | return
88 | }
89 | notificationManager.notify(notificationId, notification)
90 | }
91 |
92 |
93 | }
--------------------------------------------------------------------------------
/app/src/client/java/ir/example/androidsocket/client/WebsocketClientManager.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.client
2 |
3 | import android.net.Uri
4 | import ir.example.androidsocket.SocketConnectionListener
5 | import ir.example.androidsocket.utils.clientLog
6 | import kotlinx.coroutines.CoroutineScope
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.TimeoutCancellationException
9 | import kotlinx.coroutines.launch
10 | import kotlinx.coroutines.withContext
11 | import kotlinx.coroutines.withTimeout
12 | import org.java_websocket.client.WebSocketClient
13 | import org.java_websocket.handshake.ServerHandshake
14 | import java.net.URI
15 |
16 | class WebsocketClientManager(
17 | override var ip: String,
18 | override var port: String,
19 | override val socketListener: List
20 | ) : SocketClient, WebSocketClient(URI("ws://$ip:$port")) {
21 |
22 | override suspend fun connectWithTimeout(timeoutMillis: Long) {
23 | withContext(Dispatchers.IO) {
24 | return@withContext try {
25 | withTimeout(timeoutMillis) {
26 | socketListener.forEach { it.onStart() }
27 | connect()
28 | }
29 | } catch (e: TimeoutCancellationException) {
30 | clientLog("connectWithTimeout TimeoutCancellationException: ${e.message}")
31 | socketListener.forEach { it.onException(e) }
32 | } catch (e: Exception) {
33 | clientLog("connectWithTimeout Exception: ${e.message}")
34 | socketListener.forEach { it.onException(e) }
35 | }
36 | }
37 | }
38 |
39 | override fun disconnect() {
40 | this.close()
41 | }
42 |
43 | override fun sendMessage(message: String, timeoutMillis: Long) {
44 | clientLog("sendMessageWithTimeout")
45 | CoroutineScope(Dispatchers.IO).launch {
46 | try {
47 | if (connection != null && connection!!.isOpen) {
48 | connection!!.send(message)
49 | }
50 | } catch (e: TimeoutCancellationException) {
51 | clientLog("sendMessageWithTimeout TimeoutCancellationException : ${e.message}")
52 | socketListener.forEach { it.onException(e) }
53 | } catch (e: Exception) {
54 | clientLog("sendMessageWithTimeout Exception: ${e.message}")
55 | socketListener.forEach { it.onException(e) }
56 | }
57 | }
58 |
59 | }
60 |
61 | override fun sendFile(uri: Uri) {
62 | TODO("Not yet implemented")
63 | }
64 |
65 |
66 | override fun onOpen(handshakedata: ServerHandshake?) {
67 | clientLog("SocketClientManager onOpen")
68 | socketListener.forEach { it.onConnected() }
69 | }
70 |
71 | override fun onClose(code: Int, reason: String?, remote: Boolean) {
72 | clientLog("SocketClientManager onClose")
73 | socketListener.forEach { it.onDisconnected(code, reason) }
74 | }
75 |
76 | override fun onMessage(message: String?) {
77 | clientLog("SocketClientManager onMessage $message")
78 | socketListener.forEach { it.onMessage(null,message = message) }
79 | }
80 |
81 | override fun onError(ex: Exception?) {
82 | clientLog("SocketClientManager onError ${ex?.message}")
83 | socketListener.forEach { it.onError(ex) }
84 | ex?.printStackTrace()
85 | }
86 |
87 |
88 | }
--------------------------------------------------------------------------------
/app/src/main/java/ir/example/androidsocket/ui/base/CaptureBitmap.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.ui.base
2 |
3 | import android.graphics.Bitmap
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.remember
6 | import androidx.compose.ui.platform.ComposeView
7 | import androidx.compose.ui.platform.LocalContext
8 | import androidx.compose.ui.viewinterop.AndroidView
9 | import androidx.core.view.drawToBitmap
10 |
11 | @Composable
12 | fun captureBitmap(
13 | content: @Composable () -> Unit,
14 | ): () -> Bitmap {
15 |
16 | val context = LocalContext.current
17 |
18 | /**
19 | * ComposeView that would take composable as its content
20 | * Kept in remember so recomposition doesn't re-initialize it
21 | **/
22 | val composeView = remember { ComposeView(context) }
23 |
24 | /**
25 | * Callback function which could get latest image bitmap
26 | **/
27 | fun captureBitmap(): Bitmap {
28 | return composeView.drawToBitmap()
29 | }
30 |
31 |
32 | AndroidView(
33 | factory = {
34 | composeView.apply {
35 | setContent {
36 | content.invoke()
37 | }
38 | }
39 | }
40 | )
41 |
42 |
43 | return ::captureBitmap
44 |
45 | }
46 |
47 |
48 | /*
49 | @Composable
50 | fun captureBitmap(
51 | content: @Composable () -> Unit,
52 | onBitmapCaptured: (Bitmap) -> Unit
53 | ) {
54 | val context = LocalContext.current
55 | val composeView = remember { ComposeView(context) }
56 |
57 | var isViewPositioned by remember { mutableStateOf(false) }
58 |
59 | DisposableEffect(composeView) {
60 | val viewTreeObserver = composeView.viewTreeObserver
61 |
62 | val listener = ViewTreeObserver.OnPreDrawListener {
63 | if (!isViewPositioned) {
64 | isViewPositioned = true
65 | serverLog(
66 | "listener",
67 | "printTag"
68 | )
69 | onBitmapCaptured(composeView.drawToBitmap())
70 | }
71 | true
72 | }
73 |
74 | viewTreeObserver.addOnPreDrawListener(listener)
75 |
76 | onDispose {
77 | // Remove the listener when the ComposeView is disposed
78 | viewTreeObserver.removeOnPreDrawListener(listener)
79 | }
80 | }
81 |
82 | AndroidView(factory = {
83 | composeView.apply {
84 | setContent {
85 | content.invoke()
86 | }
87 | }
88 | })
89 | }
90 | */
91 |
92 |
93 | /*
94 | @Composable
95 | fun captureBitmap(
96 | content: @Composable () -> Unit,
97 | onBitmapCaptured: (Bitmap) -> Unit
98 | ) {
99 | val context = LocalContext.current
100 | val composeView = remember { ComposeView(context) }
101 |
102 | var isViewPositioned by remember { mutableStateOf(false) }
103 |
104 | Box(
105 | modifier = Modifier
106 | .size(1.dp, 1.dp)
107 | .onGloballyPositioned {
108 | serverLog("onGloballyPositioned", "printTag")
109 | if (!isViewPositioned) {
110 | isViewPositioned = true
111 | onBitmapCaptured(composeView.drawToBitmap())
112 | }
113 | }
114 | ) {
115 | // Embed the ComposeView inside the Box
116 | AndroidView(factory = {
117 | composeView.apply {
118 | setContent {
119 | content.invoke()
120 | }
121 | }
122 | })
123 | }
124 | }*/
--------------------------------------------------------------------------------
/app/src/client/java/ir/example/androidsocket/client/SocketClientForegroundService.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.client
2 |
3 | import android.app.Service
4 | import android.content.Intent
5 | import android.net.Uri
6 | import android.os.Binder
7 | import android.os.IBinder
8 | import ir.example.androidsocket.Constants
9 | import ir.example.androidsocket.SocketConnectionListener
10 | import ir.example.androidsocket.utils.NotificationHandler
11 | import ir.example.androidsocket.utils.clientLog
12 | import kotlinx.coroutines.CoroutineScope
13 | import kotlinx.coroutines.Dispatchers
14 | import kotlinx.coroutines.delay
15 | import kotlinx.coroutines.launch
16 |
17 | class SocketClientForegroundService : Service() {
18 |
19 | companion object {
20 | private const val CHANNEL_ID = "SocketChannel"
21 | private const val NOTIFICATION_ID = 1
22 | }
23 |
24 | private lateinit var clientManager: SocketClient
25 | private val binder = LocalBinder()
26 | private val connectionListeners = mutableListOf()
27 | private val notificationHandler =
28 | NotificationHandler(this, CHANNEL_ID)
29 |
30 |
31 | inner class LocalBinder : Binder() {
32 | fun getService(): SocketClientForegroundService = this@SocketClientForegroundService
33 | }
34 |
35 | private var serverAddress = ""
36 |
37 |
38 | override fun onCreate() {
39 | clientLog("SocketClientForegroundService onCreate")
40 | super.onCreate()
41 | notificationHandler.createNotificationChannel()
42 | }
43 |
44 | override fun onBind(intent: Intent?): IBinder? {
45 | return binder
46 | }
47 |
48 | override fun onUnbind(intent: Intent?): Boolean {
49 | clientLog("SocketClientForegroundService onUnbind")
50 | closeClientSocket()
51 | return super.onUnbind(intent)
52 | }
53 |
54 | override fun onDestroy() {
55 | clientLog("SocketClientForegroundService onDestroy")
56 | closeClientSocket()
57 | super.onDestroy()
58 | }
59 |
60 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
61 | clientLog("SocketClientForegroundService onStartCommand")
62 |
63 | // Start the foreground service and display a notification when service is started
64 | startForeground(
65 | NOTIFICATION_ID,
66 | notificationHandler.createNotification(
67 | message = "socket client",
68 | onContentIntent = { null })
69 | )
70 |
71 | return START_STICKY
72 | }
73 |
74 | fun registerConnectionListener(listener: SocketConnectionListener) {
75 | connectionListeners.add(listener)
76 | }
77 |
78 | fun unregisterConnectionListener(listener: SocketConnectionListener) {
79 | connectionListeners.remove(listener)
80 | }
81 |
82 |
83 | fun connectWebSocket(protocolType: Constants.ProtocolType, ip: String, port: String) {
84 | clientLog("connectWebSocket : ${protocolType.title}")
85 | clientManager = when (protocolType) {
86 | Constants.ProtocolType.WEBSOCKET -> WebsocketClientManager(ip,port,connectionListeners)
87 | Constants.ProtocolType.TCP -> TcpClientManager(ip, port , connectionListeners,this.contentResolver)
88 | }
89 | CoroutineScope(Dispatchers.IO).launch {
90 | delay(1000)
91 | clientManager.connectWithTimeout()
92 | }
93 | }
94 |
95 | fun closeClientSocket() {
96 | try {
97 | clientLog("SocketClientForegroundService closeClientSocket")
98 | clientManager.disconnect()
99 | } catch (e: Exception) {
100 | clientLog("closeClientSocket catch exception : ${e.message}")
101 | }
102 | }
103 |
104 | fun sendMessageWithTimeout(message: String) {
105 | CoroutineScope(Dispatchers.IO).launch {
106 | clientLog("SocketClientForegroundService sendMessageWithTimeout")
107 | clientManager.sendMessage(message = message, timeoutMillis = 50000)
108 | }
109 | }
110 |
111 | fun sendFile(uri : Uri){
112 | clientLog("SocketClientForegroundService sendFile")
113 | clientManager.sendFile(uri)
114 | }
115 |
116 |
117 | }
--------------------------------------------------------------------------------
/app/src/main/java/ir/example/androidsocket/ui/base/AppOutlinedTextField.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.ui.base
2 |
3 | import androidx.compose.material3.MaterialTheme
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.foundation.shape.RoundedCornerShape
8 | import androidx.compose.foundation.text.KeyboardActions
9 | import androidx.compose.foundation.text.KeyboardOptions
10 | import androidx.compose.material.OutlinedTextField
11 | import androidx.compose.material.TextFieldColors
12 | import androidx.compose.material.TextFieldDefaults
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.runtime.LaunchedEffect
15 | import androidx.compose.runtime.remember
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.focus.FocusRequester
18 | import androidx.compose.ui.graphics.Color
19 | import androidx.compose.ui.text.TextStyle
20 | import androidx.compose.ui.text.font.FontWeight
21 | import androidx.compose.ui.text.input.VisualTransformation
22 | import androidx.compose.ui.tooling.preview.Preview
23 | import androidx.compose.ui.unit.dp
24 | import ir.example.androidsocket.ui.theme.spacing
25 |
26 |
27 | @Composable
28 | fun AppOutlinedTextField(
29 | modifier: Modifier = Modifier,
30 | value: String,
31 | onValueChange: (String) -> Unit,
32 | label: String = "",
33 | hint: String = "",
34 | singleLine: Boolean = true,
35 | enabled: Boolean = true,
36 | visualTransformation: VisualTransformation = VisualTransformation.None,
37 | keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
38 | leadingIcon: @Composable (() -> Unit)? = {},
39 | trailingIcon : @Composable (() -> Unit)? = {},
40 | colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors(
41 | textColor = MaterialTheme.colorScheme.onSurface,
42 | focusedBorderColor = MaterialTheme.colorScheme.primary,
43 | unfocusedBorderColor = MaterialTheme.colorScheme.primaryContainer,
44 | disabledBorderColor = MaterialTheme.colorScheme.primaryContainer,
45 | disabledTextColor = Color.LightGray,
46 | cursorColor = MaterialTheme.colorScheme.primary,
47 | backgroundColor = MaterialTheme.colorScheme.surface,
48 | ),
49 | keyboardActions: KeyboardActions = KeyboardActions.Default,
50 | textStyle: TextStyle = styleText(TextType.TEXT),
51 | isFocused: Boolean = false,
52 | hasError: Boolean = false,
53 | shape: RoundedCornerShape = RoundedCornerShape(MaterialTheme.spacing.extraMedium)
54 | ) {
55 |
56 | val focusRequester = remember { FocusRequester() }
57 |
58 | LaunchedEffect(key1 = isFocused) {
59 | if (isFocused)
60 | focusRequester.requestFocus()
61 | }
62 |
63 | OutlinedTextField(
64 | modifier = modifier,
65 | value = value,
66 | onValueChange = { onValueChange(it) },
67 | label = {
68 | AppText(
69 | text = if (hasError) {
70 | "*$label"
71 | } else {
72 | label
73 | } ?: "",
74 | textColor = if (hasError) MaterialTheme.colorScheme.error else if (enabled) MaterialTheme.colorScheme.primary else Color.LightGray,
75 | fontWeight = FontWeight.Bold,
76 | style = MaterialTheme.typography.titleLarge
77 | )
78 | },
79 | singleLine = singleLine,
80 | enabled = enabled,
81 | textStyle = textStyle,
82 | colors = colors,
83 | placeholder = {
84 | if (hint.isNotEmpty())
85 | AppText(
86 | text = hint,
87 | textColor = Color.LightGray
88 | )
89 | },
90 | visualTransformation = visualTransformation,
91 | keyboardOptions = keyboardOptions,
92 | leadingIcon = leadingIcon,
93 | trailingIcon = trailingIcon,
94 | keyboardActions = keyboardActions,
95 | shape = shape
96 | )
97 | }
98 |
99 |
100 | @Preview(showBackground = true)
101 | @Composable
102 | fun AppOutlinedTextFieldPreview() {
103 | AppOutlinedTextField(
104 | modifier = Modifier
105 | .background(Color.White)
106 | .padding(24.dp),
107 | value = "موبایل",
108 | onValueChange = {},
109 | hint = "mobileHint",
110 | singleLine = false,
111 | enabled = true,
112 | label = "label"
113 |
114 | )
115 | }
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.application")
3 | id("org.jetbrains.kotlin.android")
4 | id("kotlin-kapt")
5 | id("dagger.hilt.android.plugin")
6 | }
7 |
8 | android {
9 | namespace = "com.example.androidSocket"
10 | compileSdk = 35
11 |
12 | defaultConfig {
13 | applicationId = "com.example.androidSocket"
14 | minSdk = 23
15 | targetSdk = 35
16 | versionCode = 1
17 | versionName = "1.0"
18 |
19 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
20 | vectorDrawables {
21 | useSupportLibrary = true
22 | }
23 | }
24 |
25 | buildTypes {
26 | release {
27 | isMinifyEnabled = false
28 | proguardFiles(
29 | getDefaultProguardFile("proguard-android-optimize.txt"),
30 | "proguard-rules.pro"
31 | )
32 | }
33 | }
34 | compileOptions {
35 | sourceCompatibility = JavaVersion.VERSION_1_8
36 | targetCompatibility = JavaVersion.VERSION_1_8
37 | }
38 | kotlinOptions {
39 | jvmTarget = "1.8"
40 | }
41 | buildFeatures {
42 | compose = true
43 | }
44 | composeOptions {
45 | kotlinCompilerExtensionVersion = "1.5.1"
46 | }
47 | packaging {
48 | resources {
49 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
50 | }
51 | }
52 | buildFeatures {
53 | aidl = true
54 | }
55 |
56 | flavorDimensions += "version"
57 | productFlavors {
58 | create("server") {
59 | dimension = "version"
60 | }
61 | create("client") {
62 | dimension = "version"
63 | }
64 | }
65 |
66 | }
67 |
68 | dependencies {
69 |
70 | implementation (files("libs/NeptuneLiteApi_V2.03.00_20180208.jar"))
71 |
72 | implementation("androidx.core:core-ktx:1.12.0")
73 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
74 | implementation("androidx.activity:activity-compose:1.8.2")
75 | implementation(platform("androidx.compose:compose-bom:2023.08.00"))
76 | implementation("androidx.compose.ui:ui")
77 | implementation("androidx.compose.ui:ui-graphics")
78 | implementation("androidx.compose.ui:ui-tooling-preview")
79 | implementation("androidx.compose.material3:material3")
80 | testImplementation("junit:junit:4.13.2")
81 | androidTestImplementation("androidx.test.ext:junit:1.1.5")
82 | androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
83 | androidTestImplementation(platform("androidx.compose:compose-bom:2023.08.00"))
84 | androidTestImplementation("androidx.compose.ui:ui-test-junit4")
85 | debugImplementation("androidx.compose.ui:ui-tooling")
86 | debugImplementation("androidx.compose.ui:ui-test-manifest")
87 | implementation ("androidx.compose.material:material:1.7.4")
88 | implementation ("com.google.android.material:material:1.12.0")
89 |
90 |
91 |
92 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
93 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1")
94 | testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1")
95 |
96 | implementation("org.java-websocket:Java-WebSocket:1.5.6")
97 |
98 |
99 | implementation("androidx.navigation:navigation-fragment-ktx:2.7.7")
100 | implementation("androidx.navigation:navigation-ui-ktx:2.7.7")
101 | implementation("androidx.navigation:navigation-compose:2.7.7")
102 |
103 | implementation("com.google.dagger:hilt-android:2.48")
104 | kapt("com.google.dagger:hilt-compiler:2.48")
105 | implementation("androidx.hilt:hilt-navigation-compose:1.1.0")
106 |
107 | implementation("com.google.code.gson:gson:2.10.1")
108 |
109 | implementation("com.cedarsoftware:json-io:4.10.1")
110 |
111 | implementation("com.squareup.retrofit2:retrofit:2.9.0")
112 | implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.3")
113 | implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.3")
114 | implementation("com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2")
115 | implementation("com.jakewharton.retrofit:retrofit2-kotlin-coroutines-experimental-adapter:1.0.0")
116 | implementation("com.squareup.retrofit2:converter-gson:2.5.0")
117 | implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0")
118 | implementation("org.bouncycastle:bcprov-jdk15on:1.68")
119 | implementation ("androidx.constraintlayout:constraintlayout:2.2.0-alpha13")
120 | implementation ("androidx.constraintlayout:constraintlayout-compose:1.1.0-alpha13")
121 | implementation ("com.google.accompanist:accompanist-systemuicontroller:0.31.1-alpha")
122 |
123 | }
124 |
125 |
--------------------------------------------------------------------------------
/app/src/server/java/ir/example/androidsocket/socket/WebsocketServerManger.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.socket
2 |
3 | import ir.example.androidsocket.SocketConnectionListener
4 | import ir.example.androidsocket.utils.serverLog
5 | import kotlinx.coroutines.CoroutineScope
6 | import kotlinx.coroutines.Dispatchers
7 | import kotlinx.coroutines.TimeoutCancellationException
8 | import kotlinx.coroutines.async
9 | import kotlinx.coroutines.delay
10 | import kotlinx.coroutines.withContext
11 | import kotlinx.coroutines.withTimeout
12 | import org.java_websocket.WebSocket
13 | import org.java_websocket.handshake.ClientHandshake
14 | import org.java_websocket.server.WebSocketServer
15 | import java.io.File
16 | import java.io.IOException
17 | import java.net.InetSocketAddress
18 | import java.net.ServerSocket
19 |
20 | class WebsocketServerManger(
21 | override var serverPort: Int,
22 | override var path: File,
23 | override val socketListener: List,
24 | ) : SocketServer, WebSocketServer(InetSocketAddress(serverPort)) {
25 |
26 | private var connection: WebSocket? = null
27 |
28 |
29 | /**
30 | * called when the WebSocket server has successfully started and is ready to accept client connections
31 | * */
32 | override fun onStart() {
33 | serverLog("SocketServerManger onStart")
34 | }
35 |
36 | /**
37 | * Called when a new client connection is opened
38 | * */
39 | override fun onOpen(conn: WebSocket?, handshake: ClientHandshake?) {
40 | serverLog("SocketServerManger onOpen ${conn?.remoteSocketAddress}")
41 | connection = conn
42 | socketListener.forEach { it.onConnected() }
43 | }
44 |
45 | /**
46 | * called when a individual client connection is closed , not when the server itself is stopped
47 | * This method is called when a client disconnects from the server or when the server forcefully closes a client connection
48 | * */
49 | override fun onClose(conn: WebSocket?, code: Int, reason: String?, remote: Boolean) {
50 | serverLog(
51 | "SocketServerManger onClose connection: ${conn?.remoteSocketAddress} - Code: $code, Reason: $reason"
52 | )
53 | socketListener.forEach { it.onDisconnected(code, reason) }
54 | }
55 |
56 | /**
57 | * on receiving message by server
58 | **/
59 | override fun onMessage(conn: WebSocket?, message: String?) {
60 | serverLog(
61 | "SocketServerManger onMessage $message"
62 | )
63 | socketListener.forEach { it.onMessage(null,message) }
64 | }
65 |
66 | override fun onError(conn: WebSocket?, ex: Exception?) {
67 | serverLog("SocketServerManger onError : ${ex?.message}")
68 | socketListener.forEach { it.onError(ex) }
69 | ex?.printStackTrace()
70 | }
71 |
72 | override suspend fun sendMessageWithTimeout(message: String, timeoutMillis: Long) {
73 | return withContext(Dispatchers.IO) {
74 | return@withContext try {
75 | withTimeout(timeoutMillis) {
76 | CoroutineScope(Dispatchers.IO).async {
77 | try {
78 | if (connection != null && connection!!.isOpen) {
79 | connection!!.send(message)
80 |
81 | }
82 | } catch (e: org.java_websocket.exceptions.WebsocketNotConnectedException) {
83 | serverLog("sendMessageWithTimeout catch ${e.message}", "timeOutTag")
84 | socketListener.forEach { it.onException(e) }
85 | }
86 | }.await()
87 |
88 | }
89 | } catch (e: TimeoutCancellationException) {
90 | serverLog(
91 | "sendMessageWithTimeout TimeoutCancellationException: ${e.message}",
92 | "timeOutTag"
93 | )
94 | socketListener.forEach { it.onException(e) }
95 | } catch (e: Exception) {
96 | serverLog("sendMessageWithTimeout e: ${e.message}", "timeOutTag")
97 | socketListener.forEach { it.onException(e) }
98 | }
99 | }
100 | }
101 |
102 |
103 | override fun isPortAvailable(): Boolean {
104 | return try {
105 | ServerSocket(serverPort).close()
106 | true
107 | } catch (e: IOException) {
108 | false
109 | }
110 | }
111 |
112 | override fun startServer() {
113 | serverLog("WebsocketServerManger startServer")
114 | this.start()
115 | }
116 |
117 | override fun stopServer() {
118 | serverLog("WebsocketServerManger stopServer")
119 | this.stop()
120 | }
121 |
122 | }
123 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
11 |
13 |
15 |
17 |
19 |
21 |
23 |
25 |
27 |
29 |
31 |
33 |
35 |
37 |
39 |
41 |
43 |
45 |
47 |
49 |
51 |
53 |
55 |
57 |
59 |
61 |
63 |
65 |
67 |
69 |
71 |
73 |
75 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/app/src/server/java/ir/example/androidsocket/socket/SocketServerForegroundService.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.socket
2 |
3 | import android.app.PendingIntent
4 | import android.app.Service
5 | import android.content.Context
6 | import android.content.Intent
7 | import android.os.Binder
8 | import android.os.IBinder
9 | import ir.example.androidsocket.Constants
10 | import ir.example.androidsocket.SocketConnectionListener
11 | import ir.example.androidsocket.utils.NotificationHandler
12 | import ir.example.androidsocket.utils.serverLog
13 | import kotlinx.coroutines.CoroutineScope
14 | import kotlinx.coroutines.Dispatchers
15 | import kotlinx.coroutines.delay
16 | import kotlinx.coroutines.launch
17 | import kotlin.random.Random
18 |
19 | class SocketServerForegroundService() : Service() {
20 |
21 | companion object {
22 | val PORT = Random.nextInt(1024, 49151)
23 | private const val CHANNEL_ID = "SocketChannel"
24 | private const val NOTIFICATION_ID = 1
25 | }
26 |
27 | private lateinit var serverManager: ServerManager
28 | private val binder = LocalBinder()
29 | private val connectionListeners = mutableListOf()
30 | private val notificationHandler = NotificationHandler(this, CHANNEL_ID)
31 |
32 | inner class LocalBinder : Binder() {
33 | fun getService(): SocketServerForegroundService = this@SocketServerForegroundService
34 | }
35 |
36 |
37 | /**
38 | * Called when the service is first created
39 | * is called only once during the lifetime of the service
40 | **/
41 | override fun onCreate() {
42 | serverLog("SocketServerForegroundService onCreate")
43 | super.onCreate()
44 | notificationHandler.createNotificationChannel()
45 | }
46 |
47 |
48 | /**
49 | * Called every time the service is started with startService()
50 | * handles the logic for what the service should do based on the provided Intent
51 | **/
52 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
53 | serverLog("SocketServerForegroundService onStartCommand")
54 |
55 | // Start the foreground service and display a notification when service is started
56 | startForeground(
57 | NOTIFICATION_ID,
58 | notificationHandler.createNotification(
59 | message = "socket server",
60 | onContentIntent = { null })
61 | )
62 |
63 | startSocketServer()
64 |
65 | // Return START_STICKY to indicate that if the system kills the service after it's started, it should try to recreate the service
66 | // is useful for services that are performing background tasks, like maintaining a socket connection
67 | return START_STICKY
68 | }
69 |
70 | /**
71 | * is invoked when a client calls bindService() to bind to the service
72 | * allows components (like activities) to interact with the service, send requests, and receive results
73 | **/
74 | override fun onBind(intent: Intent?): IBinder? {
75 | serverLog("SocketServerForegroundService onBind")
76 | return binder
77 | }
78 |
79 | override fun onUnbind(intent: Intent?): Boolean {
80 | serverLog("SocketServerForegroundService onUnbind")
81 | closeServerSocket()
82 | return super.onUnbind(intent)
83 | }
84 |
85 | override fun onDestroy() {
86 | serverLog("SocketServerForegroundService onDestroy")
87 | closeServerSocket()
88 | super.onDestroy()
89 | }
90 |
91 | override fun onRebind(intent: Intent?) {
92 | serverLog("SocketServerForegroundService onRebind")
93 | super.onRebind(intent)
94 | }
95 |
96 |
97 | fun registerConnectionListener(listener: SocketConnectionListener) {
98 | serverLog("SocketServerForegroundService registerConnectionListener")
99 | connectionListeners.add(listener)
100 | }
101 |
102 |
103 | fun startSocketServer(protocolType: Constants.ProtocolType = Constants.ProtocolType.WEBSOCKET) {
104 | serverLog("SocketServerForegroundService startSocketServer")
105 | setServerManager(protocolType)
106 | serverManager.startServer()
107 |
108 | }
109 |
110 | private fun setServerManager(protocolType: Constants.ProtocolType = Constants.ProtocolType.WEBSOCKET) {
111 | serverManager = ServerManager(
112 | protocolType,
113 | WebsocketServerManger(PORT, this.filesDir, connectionListeners),
114 | TcpServerManager(PORT, this.getExternalFilesDir(null), connectionListeners,this.contentResolver)
115 | )
116 | }
117 |
118 | fun displayNotification(
119 | notificationId: Int,
120 | title: String,
121 | message: String,
122 | onContentIntent: (Context) -> PendingIntent?
123 | ) {
124 | notificationHandler.displayNotification(
125 | context = this,
126 | notificationId = notificationId,
127 | title = title,
128 | message = message,
129 | onContentIntent = onContentIntent
130 | )
131 | }
132 |
133 | fun sendMessageWithTimeout(message: String, timeoutMillis: Long = 20000) {
134 | serverLog("SocketForegroundService sendMessageWithTimeout")
135 | CoroutineScope(Dispatchers.IO).launch {
136 | serverManager.sendMessageWithTimeout(timeoutMillis = timeoutMillis, message = message)
137 | }
138 | }
139 |
140 | fun closeServerSocket() {
141 | try {
142 | serverLog("SocketServerForegroundService closeServerSocket")
143 | serverManager.stopServer()
144 | } catch (e: Exception) {
145 | serverLog("SocketServerForegroundService catch exception : ${e.message}")
146 | }
147 | }
148 |
149 |
150 | }
--------------------------------------------------------------------------------
/app/src/client/java/ir/example/androidsocket/ui/ClientActivity.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.ui
2 |
3 | import android.Manifest
4 | import android.annotation.SuppressLint
5 | import android.content.pm.PackageManager
6 | import android.os.Build
7 | import android.os.Bundle
8 | import androidx.activity.ComponentActivity
9 | import androidx.activity.compose.setContent
10 | import androidx.activity.enableEdgeToEdge
11 | import androidx.activity.result.ActivityResultLauncher
12 | import androidx.activity.result.contract.ActivityResultContracts
13 | import androidx.activity.viewModels
14 | import androidx.compose.foundation.layout.Box
15 | import androidx.compose.foundation.layout.fillMaxSize
16 | import androidx.compose.runtime.LaunchedEffect
17 | import androidx.compose.runtime.getValue
18 | import androidx.compose.ui.Alignment
19 | import androidx.compose.ui.Modifier
20 | import androidx.core.app.ActivityCompat
21 | import androidx.core.content.ContextCompat
22 | import androidx.lifecycle.compose.collectAsStateWithLifecycle
23 | import com.example.androidSocket.R
24 | import dagger.hilt.android.AndroidEntryPoint
25 | import ir.example.androidsocket.ui.base.PermissionDialog
26 | import ir.example.androidsocket.utils.clientLog
27 |
28 |
29 | @AndroidEntryPoint
30 | class ClientActivity : ComponentActivity() {
31 |
32 | private val viewModel: ClientViewModel by viewModels()
33 |
34 | @SuppressLint("SuspiciousIndentation")
35 | override fun onCreate(savedInstanceState: Bundle?) {
36 | clientLog("ClientActivity onCreate()")
37 | enableEdgeToEdge()
38 | super.onCreate(savedInstanceState)
39 | val activity = this@ClientActivity
40 | var requestPermissionLauncher: ActivityResultLauncher =
41 | registerForActivityResult(
42 | ActivityResultContracts.RequestPermission()
43 | ) { isGranted: Boolean ->
44 | if (isGranted) {
45 | // Permission is granted. Continue the action or workflow in your app
46 | clientLog("ActivityResult permission isGranted")
47 | viewModel.setOpenNotificationPermissionDialog(false)
48 | viewModel.onEvent(ClientEvent.StartClientService(activity))
49 |
50 | } else {
51 | clientLog("ActivityResult permission isNotGranted")
52 | viewModel.setOpenNotificationPermissionDialog(true)
53 | }
54 | }
55 |
56 |
57 | setContent {
58 |
59 | val openPermissionDialog by viewModel.openNotificationPermissionDialog.collectAsStateWithLifecycle(
60 | initialValue = false
61 | )
62 |
63 | LaunchedEffect(Unit) {
64 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
65 | when {
66 | ContextCompat.checkSelfPermission(
67 | activity,
68 | Manifest.permission.POST_NOTIFICATIONS
69 | ) == PackageManager.PERMISSION_GRANTED -> {
70 | viewModel.onEvent(ClientEvent.StartClientService(activity))
71 | }
72 |
73 | ActivityCompat.shouldShowRequestPermissionRationale(
74 | activity, Manifest.permission.POST_NOTIFICATIONS
75 | ) -> {
76 | /*
77 | * true if the user has previously denied the permission request .
78 | * explain to the user why your app requires this permission for a specific feature to behave as expected
79 | * and what features are disabled if it's declined.
80 | * */
81 | viewModel.setOpenNotificationPermissionDialog(true)
82 |
83 | }
84 |
85 | else -> {
86 | // The registered ActivityResultCallback gets the result of this request.
87 | viewModel.setOpenNotificationPermissionDialog(false)
88 | requestPermissionLauncher.launch(
89 | Manifest.permission.POST_NOTIFICATIONS
90 | )
91 | }
92 | }
93 | } else {
94 | viewModel.onEvent(ClientEvent.StartClientService(activity))
95 | }
96 |
97 | }
98 |
99 | Box(Modifier.fillMaxSize()) {
100 | ClientCompose(
101 | viewModel = viewModel,
102 | ) { event -> viewModel.onEvent(event) }
103 |
104 | if (openPermissionDialog) {
105 | clientLog("showPermissionDialog")
106 | PermissionDialog(
107 | modifier = Modifier.align(Alignment.Center),
108 | permissionReason = R.string.notification_permission_reason,
109 | onDismissRequest = {
110 | viewModel.setOpenNotificationPermissionDialog(false)
111 | finish()
112 | }
113 | ) {
114 | requestPermissionLauncher.launch(
115 | Manifest.permission.POST_NOTIFICATIONS
116 | )
117 | }
118 | }
119 | }
120 | }
121 | }
122 |
123 | override fun onStart() {
124 | clientLog("ClientActivity onStart()")
125 | super.onStart()
126 | }
127 |
128 | override fun onStop() {
129 | clientLog("ClientActivity onStop()")
130 | super.onStop()
131 | }
132 |
133 |
134 | override fun onDestroy() {
135 | clientLog("ClientActivity onDestroy()")
136 | viewModel.performCleanup()
137 | super.onDestroy()
138 | }
139 |
140 | }
141 |
142 |
--------------------------------------------------------------------------------
/app/src/main/java/ir/example/androidsocket/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.ui.theme
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.isSystemInDarkTheme
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.WindowInsets
7 | import androidx.compose.foundation.layout.asPaddingValues
8 | import androidx.compose.foundation.layout.fillMaxSize
9 | import androidx.compose.foundation.layout.navigationBars
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.material3.CircularProgressIndicator
12 | import androidx.compose.material3.MaterialTheme
13 | import androidx.compose.material3.Scaffold
14 | import androidx.compose.material3.Snackbar
15 | import androidx.compose.material3.SnackbarHost
16 | import androidx.compose.material3.SnackbarHostState
17 | import androidx.compose.material3.Typography
18 | import androidx.compose.material3.darkColorScheme
19 | import androidx.compose.material3.lightColorScheme
20 | import androidx.compose.runtime.Composable
21 | import androidx.compose.runtime.CompositionLocalProvider
22 | import androidx.compose.runtime.LaunchedEffect
23 | import androidx.compose.runtime.ProvidedValue
24 | import androidx.compose.ui.Alignment
25 | import androidx.compose.ui.Modifier
26 | import androidx.compose.ui.graphics.Color
27 | import androidx.compose.ui.platform.LocalContext
28 | import androidx.compose.ui.platform.LocalLayoutDirection
29 | import androidx.compose.ui.unit.LayoutDirection
30 | import androidx.compose.ui.unit.dp
31 | import ir.example.androidsocket.ui.base.BaseUiEvent
32 |
33 | private val DarkColorScheme = darkColorScheme(
34 | primary = AppColors.Dark.primary,
35 | onPrimary = AppColors.Dark.onPrimary,
36 | primaryContainer = AppColors.Dark.primaryContainer,
37 | onPrimaryContainer = AppColors.Dark.onPrimaryContainer,
38 | secondary = AppColors.Dark.secondary,
39 | onSecondary = AppColors.Dark.onSecondary,
40 | tertiary = AppColors.Dark.tertiary,
41 | onTertiary = AppColors.Dark.onTertiary,
42 | surface = AppColors.Dark.surface,
43 | onSurface = AppColors.Dark.onSurface,
44 | background = AppColors.Dark.background,
45 | error = AppColors.Dark.error,
46 | onError = AppColors.Dark.onError,
47 |
48 | )
49 |
50 | private val LightColorScheme = lightColorScheme(
51 | primary = AppColors.Light.primary,
52 | onPrimary = AppColors.Light.onPrimary,
53 | primaryContainer = AppColors.Light.primaryContainer,
54 | onPrimaryContainer = AppColors.Light.onPrimaryContainer,
55 | secondary = AppColors.Light.secondary,
56 | onSecondary = AppColors.Light.onSecondary,
57 | tertiary = AppColors.Light.tertiary,
58 | onTertiary = AppColors.Light.onTertiary,
59 | surface = AppColors.Light.surface,
60 | onSurface = AppColors.Light.onSurface,
61 | background = AppColors.Light.background,
62 | error = AppColors.Light.error,
63 | onError = AppColors.Light.onError,
64 | )
65 |
66 | @Composable
67 | fun AndroidSocketTheme(
68 | direction: ProvidedValue = LocalLayoutDirection provides LayoutDirection.Ltr,
69 | darkTheme: Boolean = isSystemInDarkTheme(),
70 | scaffoldState: SnackbarHostState = SnackbarHostState(),
71 | uiEvent: BaseUiEvent,
72 | onResetScreenMessage: () -> Unit = {},
73 | displayProgressBar: Boolean? = null,
74 | content: @Composable () -> Unit
75 | ) {
76 | val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
77 |
78 |
79 | CompositionLocalProvider(LocalSpacing provides Spacing()) {
80 | androidx.compose.material3.MaterialTheme(
81 | colorScheme = colorScheme,
82 | typography = Typography(),
83 | ) {
84 | val context = LocalContext.current
85 |
86 | LaunchedEffect(key1 = uiEvent) {
87 | when (uiEvent) {
88 | is BaseUiEvent.ShowToast -> {
89 | uiEvent.messageId?.let { messageId ->
90 | val messageValue =
91 | if (uiEvent.parameters.isNotEmpty() && uiEvent.parameters[0] != null) {
92 | context.getString(messageId, *uiEvent.parameters)
93 | } else {
94 | context.getString(messageId)
95 | }
96 | scaffoldState.showSnackbar(message = messageValue)
97 | onResetScreenMessage()
98 | }
99 | }
100 |
101 | else -> {}
102 |
103 | }
104 | }
105 |
106 | Scaffold(
107 | snackbarHost = {
108 |
109 | SnackbarHost(
110 | modifier = Modifier.padding(WindowInsets.navigationBars.asPaddingValues()),
111 | hostState = scaffoldState
112 | ) { data ->
113 | CompositionLocalProvider(
114 | direction
115 | ) {
116 | Snackbar(
117 | snackbarData = data,
118 | containerColor = MaterialTheme.colorScheme.tertiary,
119 | contentColor = Color.White
120 | )
121 | }
122 |
123 | }
124 | }
125 | ) {
126 | Box(
127 | modifier = Modifier
128 | .fillMaxSize()
129 | .padding(it)
130 | .background(color = androidx.compose.material.MaterialTheme.colors.background)
131 | ) {
132 |
133 | AppLayoutDirection(direction, content)
134 | if (displayProgressBar == true)
135 | CircularProgressIndicator(
136 | Modifier
137 | .align(Alignment.Center)
138 | .padding(24.dp),
139 | )
140 |
141 | }
142 | }
143 |
144 | }
145 | }
146 |
147 | }
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/app/src/client/java/ir/example/androidsocket/ui/ClientViewModel.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.ui
2 |
3 | import android.content.ComponentName
4 | import android.content.Intent
5 | import android.content.ServiceConnection
6 | import android.net.Uri
7 | import android.os.Build
8 | import android.os.IBinder
9 | import androidx.activity.ComponentActivity
10 | import androidx.lifecycle.viewModelScope
11 | import com.example.androidSocket.R
12 | import dagger.hilt.android.lifecycle.HiltViewModel
13 | import ir.example.androidsocket.Constants
14 | import ir.example.androidsocket.client.SocketClientForegroundService
15 | import ir.example.androidsocket.SocketConnectionListener
16 | import ir.example.androidsocket.ui.base.BaseViewModel
17 | import ir.example.androidsocket.utils.clientLog
18 | import kotlinx.coroutines.delay
19 | import kotlinx.coroutines.flow.MutableStateFlow
20 | import kotlinx.coroutines.launch
21 | import javax.inject.Inject
22 |
23 |
24 | @HiltViewModel
25 | internal class ClientViewModel @Inject constructor() : BaseViewModel() {
26 |
27 |
28 | var clientMessage = MutableStateFlow("")
29 | private set
30 |
31 | var fileUrl: MutableStateFlow = MutableStateFlow(null)
32 | private set
33 |
34 | var fileProgress = MutableStateFlow(null)
35 | private set
36 |
37 | var serverMessage = MutableStateFlow("")
38 | private set
39 |
40 | var waitingForServerConfirmation = MutableStateFlow(null)
41 | private set
42 |
43 | var serverIp = MutableStateFlow("")
44 | private set
45 |
46 | var serverIpError = MutableStateFlow(false)
47 | private set
48 |
49 | var serverPort = MutableStateFlow("")
50 | private set
51 |
52 | var serverPortError = MutableStateFlow(false)
53 | private set
54 |
55 | var socketStatus = MutableStateFlow(Constants.SocketStatus.DISCONNECTED)
56 | private set
57 |
58 | var inConnectionProcess = MutableStateFlow(false)
59 | private set
60 |
61 | var isServiceBound = MutableStateFlow(false)
62 | private set
63 |
64 | private var clientForegroundService: SocketClientForegroundService? = null
65 |
66 | val clientConnectionListener = object : SocketConnectionListener {
67 |
68 | override fun onStart() {
69 | clientLog(
70 | "clientConnectionListener onStart()"
71 | )
72 | }
73 |
74 | override fun onConnected() {
75 | clientLog("clientConnectionListener onConnected","connectionnnn")
76 | onEvent(ClientEvent.SetLoading(false))
77 | onEvent(ClientEvent.SetInConnectionProcess(false))
78 | onEvent(ClientEvent.SetSocketConnectionStatus(Constants.SocketStatus.CONNECTED))
79 | }
80 |
81 | override fun onMessage(messageContentType: Int?, message: String?) {
82 | clientLog("clientConnectionListener onMessage : $message", "progressCheck")
83 | onEvent(ClientEvent.SetLoading(false))
84 | setWaitingForServer(false)
85 | onEvent(ClientEvent.SetServerMessage(message ?: ""))
86 | }
87 |
88 | override fun onProgressUpdate(progress: Int) {
89 | clientLog("clientConnectionListener onProgressUpdate : $progress", "progressCheck")
90 | setFileProgress(progress)
91 | if (fileProgress.value == 100)
92 | setFileProgress(null)
93 | }
94 |
95 | override fun onDisconnected(code: Int?, reason: String?) {
96 | clientLog(
97 | "clientConnectionListener onDisconnected : $code $reason","connectionnnn"
98 | )
99 | emitMessageValue(R.string.disconnected_error_message, reason)
100 | onEvent(ClientEvent.SetSocketConnectionStatus(Constants.SocketStatus.DISCONNECTED))
101 | //onEvent(ClientEvent.SetLoading(false))
102 | onEvent(ClientEvent.SetInConnectionProcess(false))
103 | }
104 |
105 | override fun onError(exception: Exception?) {
106 | clientLog(
107 | "clientConnectionListener ClientActivity onError ${exception?.message}","connectionnnn"
108 | )
109 | emitMessageValue(R.string.error_message, exception?.message ?: "")
110 | //onEvent(ClientEvent.SetLoading(false))
111 | onEvent(ClientEvent.SetInConnectionProcess(false))
112 | onEvent(ClientEvent.SetSocketConnectionStatus(Constants.SocketStatus.DISCONNECTED))
113 | }
114 |
115 | override fun onException(exception: Exception?) {
116 | clientLog(
117 | "clientConnectionListener onException() ${exception?.message}"
118 | )
119 | // onEvent(ClientEvent.SetLoading(false))
120 | onEvent(ClientEvent.SetInConnectionProcess(false))
121 | emitMessageValue(R.string.error_message, exception?.message ?: "")
122 | }
123 | }
124 | private var serviceConnection: ServiceConnection? = object : ServiceConnection {
125 | override fun onServiceConnected(className: ComponentName, service: IBinder) {
126 | val binder = service as SocketClientForegroundService.LocalBinder
127 | clientLog("serviceConnection onServiceConnected")
128 | clientForegroundService = binder.getService()
129 | clientForegroundService?.registerConnectionListener(clientConnectionListener)
130 | isServiceBound.value = true
131 |
132 | }
133 |
134 | override fun onServiceDisconnected(arg0: ComponentName) {
135 | clientForegroundService?.unregisterConnectionListener(clientConnectionListener)
136 | isServiceBound.value = false
137 | }
138 | }
139 |
140 | fun onEvent(event: ClientEvent) {
141 | when (event) {
142 | is ClientEvent.StartClientService -> {
143 | if (!isServiceBound.value) {
144 | try {
145 | serviceConnection?.let { connection ->
146 | val serviceIntent =
147 | Intent(event.context, SocketClientForegroundService::class.java)
148 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
149 | event.context.startForegroundService(serviceIntent)
150 | } else {
151 | event.context.startService(serviceIntent)
152 | }
153 | event.context.bindService(
154 | serviceIntent,
155 | connection,
156 | ComponentActivity.BIND_AUTO_CREATE
157 | )
158 | isServiceBound.value = true
159 |
160 | } ?: throw Exception()
161 |
162 | } catch (e: Exception) {
163 | clientLog("startSocketService catch : ${e.message}")
164 |
165 | }
166 | }
167 |
168 | }
169 |
170 | is ClientEvent.SetLoading -> {
171 | loading.value = event.value
172 | }
173 |
174 | is ClientEvent.SetInConnectionProcess -> {
175 | clientLog("SetInConnectionProcess-> ${event.value}","connectionnnn")
176 |
177 | inConnectionProcess.value = event.value
178 | }
179 |
180 | is ClientEvent.SetServerIp -> {
181 | serverIp.value = event.ip
182 | }
183 |
184 | is ClientEvent.SetServerPort -> serverPort.value = event.port
185 | is ClientEvent.SetSocketConnectionStatus -> {
186 | clientLog("ClientEvent.SetSocketConnectionStatus -> ${event.status}","connectionnnn")
187 | socketStatus.value = event.status}
188 | is ClientEvent.SetClientMessage -> {
189 | clientMessage.value = event.message
190 | if (clientMessage.value.isEmpty()) {
191 | setWaitingForServer(null)
192 | }
193 | }
194 |
195 | is ClientEvent.SetFileUrl -> {
196 | fileUrl.value = event.uri
197 | }
198 |
199 | is ClientEvent.SetServerMessage -> {
200 | clientLog("ClientEvent.SetServerMessage ${waitingForServerConfirmation.value}")
201 | serverMessage.value = event.message
202 | }
203 | ClientEvent.OnConnectionButtonClicked->{
204 | when (socketStatus.value == Constants.SocketStatus.CONNECTED) {
205 | true -> {
206 | clientLog("onDisconnectFromServer","connection")
207 | onDisconnectFromServer()
208 | }
209 | false -> {
210 | clientLog("onConnectToServer","connection")
211 | onConnectToServer()
212 | }
213 | }
214 | }
215 | is ClientEvent.SendMessageToServer -> {
216 | clientLog("SocketClientForegroundService SendMessageToServer")
217 |
218 | setWaitingForServer(true)
219 | viewModelScope.launch {
220 | delay(1000)
221 | fileUrl.value?.let {
222 | clientLog("SocketClientForegroundService SendMessageToServer 1")
223 | clientForegroundService?.sendFile(it)
224 | } ?: clientForegroundService?.sendMessageWithTimeout(message = event.message)
225 | }
226 |
227 | }
228 |
229 | is ClientEvent.SetProtocolType -> selectedProtocol.value = when (event.type) {
230 | Constants.ProtocolType.TCP.title -> Constants.ProtocolType.TCP
231 | else -> Constants.ProtocolType.WEBSOCKET
232 | }
233 |
234 | ClientEvent.ResetClientMessage -> {
235 | onEvent(ClientEvent.SetClientMessage(""))
236 | onEvent(ClientEvent.SetFileUrl(null))
237 | setFileProgress(null)
238 | }
239 | }
240 | }
241 |
242 | private fun onConnectToServer(){
243 | serverIpError.value = serverIp.value.isEmpty()
244 | serverPortError.value = serverPort.value.isEmpty()
245 |
246 | if (!serverIpError.value && !serverPortError.value && isServiceBound.value) {
247 | clientForegroundService?.let { service ->
248 | service.connectWebSocket(
249 | selectedProtocol.value,
250 | ip = serverIp.value,
251 | port = serverPort.value
252 | )
253 | }
254 | }
255 | }
256 | private fun onDisconnectFromServer(){
257 | clientLog("onDisconnectFromServer()","connectionnnn")
258 | clientForegroundService?.closeClientSocket()
259 | }
260 | private fun setFileProgress(progress: Int?) {
261 | fileProgress.value = progress
262 | }
263 |
264 | private fun setWaitingForServer(waiting: Boolean?) {
265 | waitingForServerConfirmation.value = waiting
266 | }
267 |
268 | fun performCleanup() {
269 | clientLog("performCleanup()")
270 | try {
271 | clientForegroundService?.let { foregroundService ->
272 | serviceConnection?.let { serviceConnection ->
273 | //stopping the service makes it automatically unbinds all clients that are bound to it
274 | foregroundService.stopSelf()
275 | isServiceBound.value = false
276 | }
277 | }
278 |
279 | } catch (e: Exception) {
280 | // Handle exceptions
281 | clientLog("performCleanup catch: ${e.message}")
282 | }
283 |
284 | }
285 |
286 | }
--------------------------------------------------------------------------------
/app/src/server/java/ir/example/androidsocket/ui/ServerViewModel.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.ui
2 |
3 | import android.app.PendingIntent
4 | import android.content.ComponentName
5 | import android.content.Context
6 | import android.content.Intent
7 | import android.content.ServiceConnection
8 | import android.os.Build
9 | import android.os.IBinder
10 | import androidx.activity.ComponentActivity
11 | import androidx.lifecycle.viewModelScope
12 | import com.example.androidSocket.R
13 | import dagger.hilt.android.lifecycle.HiltViewModel
14 | import ir.example.androidsocket.Constants
15 | import ir.example.androidsocket.Constants.CLIENT_MESSAGE_NOTIFICATION_ID
16 | import ir.example.androidsocket.Constants.MessageConstantType.MESSAGE_TYPE_FILE_CONTENT
17 | import ir.example.androidsocket.Constants.MessageConstantType.MESSAGE_TYPE_TEXT_CONTENT
18 | import ir.example.androidsocket.MainApplication
19 | import ir.example.androidsocket.SocketConnectionListener
20 | import ir.example.androidsocket.socket.SocketServerForegroundService
21 | import ir.example.androidsocket.ui.base.BaseViewModel
22 | import ir.example.androidsocket.utils.ConnectionTypeManager
23 | import ir.example.androidsocket.utils.IpAddressManager
24 | import ir.example.androidsocket.utils.serverLog
25 | import kotlinx.coroutines.delay
26 | import kotlinx.coroutines.flow.MutableStateFlow
27 | import kotlinx.coroutines.launch
28 | import javax.inject.Inject
29 |
30 |
31 | @HiltViewModel
32 | internal class ServerViewModel @Inject constructor() : BaseViewModel() {
33 |
34 | var openStoragePermissionDialog = MutableStateFlow(false)
35 |
36 | var clientMessage = MutableStateFlow("")
37 | private set
38 |
39 | var fileProgress = MutableStateFlow(null)
40 | private set
41 |
42 | var fileIsSaved = MutableStateFlow(false)
43 | private set
44 |
45 | var wifiServerIp = MutableStateFlow("")
46 | private set
47 |
48 | var ethernetServerIp = MutableStateFlow("")
49 | private set
50 |
51 | var socketStatus = MutableStateFlow(Constants.SocketStatus.DISCONNECTED)
52 | private set
53 |
54 |
55 | var isServiceBound = MutableStateFlow(false)
56 | private set
57 |
58 | var isConnecting = MutableStateFlow(false)
59 | private set
60 |
61 |
62 | private var serverForgroundService: SocketServerForegroundService? = null
63 |
64 | val socketConnectionListener = object : SocketConnectionListener {
65 | override fun onStart() {
66 | /*
67 | * server starts successfully and is ready to accept connections
68 | * */
69 | serverLog("SocketConnectionListener onStart")
70 | onEvent(ServerEvent.SetLoading(false))
71 | onEvent(ServerEvent.SetIsConnecting(false))
72 | }
73 |
74 | override fun onConnected() {
75 | serverLog("SocketConnectionListener onConnected")
76 | socketStatus.value = Constants.SocketStatus.CONNECTED
77 | onEvent(ServerEvent.SetLoading(false))
78 |
79 | }
80 |
81 | override fun onMessage(messageContentType: Int?, message: String?) {
82 | serverLog("SocketConnectionListener onMessage: $message", "progressCheck")
83 | when (messageContentType) {
84 | MESSAGE_TYPE_TEXT_CONTENT -> {
85 | onEvent(ServerEvent.SetClientMessage(message ?: ""))
86 | createNotificationFromClientMessage(message = message)
87 | serverForgroundService?.sendMessageWithTimeout("message is received by server")
88 | }
89 |
90 | MESSAGE_TYPE_FILE_CONTENT -> {
91 | serverLog("MESSAGE_TYPE_FILE_CONTENT", "progressCheck")
92 | onEvent(ServerEvent.SetFileIsSaved(true))
93 | emitMessageValue(R.string.file_message_saved)
94 | }
95 | }
96 | }
97 |
98 | override fun onProgressUpdate(progress: Int) {
99 | serverLog("SocketConnectionListener onProgressUpdate: $progress", "progressCheck")
100 | fileProgress.value = progress
101 | if (progress == 100) {
102 | emitMessageValue(R.string.file_message_received)
103 | createNotificationFromClientMessage(message = "a file message is received")
104 | serverForgroundService?.sendMessageWithTimeout("message is received by server")
105 | fileProgress.value = null
106 | }
107 |
108 | }
109 |
110 | override fun onDisconnected(code: Int?, reason: String?) {
111 | serverLog("SocketConnectionListener onDisconnected: $reason")
112 | emitMessageValue(R.string.disconnected_error_message, reason)
113 | socketStatus.value = Constants.SocketStatus.DISCONNECTED
114 |
115 | }
116 |
117 | override fun onError(exception: Exception?) {
118 | serverLog("SocketConnectionListener onError: ${exception?.message}")
119 |
120 | /*socketStatus should not changed to disconnected because in some cases such as when creating notification from client
121 | *message got error on >=31 ,the socket was connected but onError was called.
122 | */
123 | emitMessageValue(R.string.error_message, exception?.message)
124 | onEvent(ServerEvent.SetIsConnecting(false))
125 |
126 | }
127 |
128 | override fun onException(exception: Exception?) {
129 | serverLog("SocketConnectionListener onException: ${exception?.message}")
130 | emitMessageValue(R.string.error_message, exception?.message)
131 | onEvent(ServerEvent.SetLoading(false))
132 | onEvent(ServerEvent.SetIsConnecting(false))
133 | }
134 |
135 | }
136 |
137 | private var serviceConnection: ServiceConnection? = object : ServiceConnection {
138 | override fun onServiceConnected(className: ComponentName, service: IBinder) {
139 | serverLog("serverConnectionListener onServiceConnected")
140 | val binder = service as SocketServerForegroundService.LocalBinder
141 | serverForgroundService = binder.getService()
142 | serverForgroundService?.registerConnectionListener(socketConnectionListener)
143 | isServiceBound.value = true
144 | }
145 |
146 | override fun onServiceDisconnected(arg0: ComponentName) {
147 | serverLog("serverConnectionListener onServiceDisconnected")
148 |
149 | /*This method is not guaranteed to be called when you call unbindService.
150 | It is more commonly used in scenarios where the service process is unexpectedly
151 | terminated or crashes.*/
152 |
153 | }
154 | }
155 |
156 | fun setOpenStoragePermissionDialog(value: Boolean) {
157 | openStoragePermissionDialog.value = value
158 | }
159 |
160 | fun startServerService(context: Context) {
161 | serverLog("startServerService() ${isServiceBound.value}")
162 | if (!isServiceBound.value) {
163 | try {
164 | serverLog("startServerService() try")
165 | serviceConnection?.let { connection ->
166 | val serviceIntent = Intent(context, SocketServerForegroundService::class.java)
167 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
168 | serverLog("startServerService() Build.VERSION_CODES.O")
169 | context.startForegroundService(serviceIntent)
170 | } else {
171 | serverLog("startServerService() Build.VERSION_CODES.O else")
172 | context.startService(serviceIntent)
173 | }
174 | context.bindService(
175 | serviceIntent,
176 | connection,
177 | ComponentActivity.BIND_AUTO_CREATE
178 | )
179 | isServiceBound.value = true
180 | } ?: throw Exception()
181 |
182 | } catch (e: Exception) {
183 | serverLog("startSocketService catch: ${e.message}")
184 | }
185 | }
186 | }
187 |
188 | fun onEvent(event: ServerEvent) {
189 | when (event) {
190 | is ServerEvent.SetLoading -> loading.value = event.value
191 | is ServerEvent.SetSocketConnectionStatus -> socketStatus.value = event.status
192 | is ServerEvent.SetClientMessage -> clientMessage.value = event.message
193 | is ServerEvent.GetWifiIpAddress -> wifiServerIp.value =
194 | IpAddressManager.getLocalIpAddress(event.context).first ?: ""
195 |
196 | is ServerEvent.GetLanIpAddress -> ethernetServerIp.value =
197 | IpAddressManager.getLocalIpAddress(event.context).second ?: ""
198 |
199 | is ServerEvent.SetProtocolType -> {
200 | serverLog("SetProtocolType ${event.type}")
201 | //if the selected protocol doesn't differ from previous one, return
202 | if (selectedProtocol.value.title == event.type)
203 | return
204 | if (event.connectionType == Constants.ConnectionType.NONE) {
205 | emitMessageValue(R.string.set_protocol_error)
206 | return
207 | }
208 | // before running socket on new selected protocol type , close the previous
209 | serverForgroundService?.closeServerSocket()
210 | selectedProtocol.value = when (event.type) {
211 | Constants.ProtocolType.TCP.title -> Constants.ProtocolType.TCP
212 | else -> Constants.ProtocolType.WEBSOCKET
213 | }
214 | serverForgroundService?.startSocketServer(selectedProtocol.value)
215 | }
216 |
217 | is ServerEvent.SetIsConnecting -> {
218 | serverLog("SetIsConnecting ${event.isConnecting}")
219 | isConnecting.value = event.isConnecting
220 | }
221 |
222 | is ServerEvent.SetFileIsSaved -> fileIsSaved.value = event.saved
223 |
224 | }
225 | }
226 |
227 | private fun createNotificationFromClientMessage(message: String?) {
228 | if (!message.isNullOrEmpty())
229 | serverForgroundService?.displayNotification(
230 | notificationId = CLIENT_MESSAGE_NOTIFICATION_ID,
231 | title = Constants.ActionCode.NotificationMessage.title,
232 | message = message,
233 | onContentIntent = { context ->
234 | //will be triggered when the user taps on the notification
235 | if (!MainApplication.isAppInForeground) {
236 | val intent = Intent(context, ServerActivity::class.java)
237 | intent.action = Constants.ActionCode.NotificationMessage.title
238 | PendingIntent.getActivity(
239 | context,
240 | 0,
241 | intent,
242 | PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
243 | )
244 | } else null
245 | }
246 | )
247 | }
248 |
249 | fun performCleanup() {
250 | serverLog("performCleanup()")
251 | try {
252 | serverForgroundService?.let { foregroundService ->
253 | serviceConnection?.let {
254 | //stopping the service makes it automatically unbinds all clients that are bound to it
255 | foregroundService.stopSelf()
256 | isServiceBound.value = false
257 | }
258 | }
259 | } catch (e: Exception) {
260 | serverLog("performCleanup catch: ${e.message}")
261 | }
262 | }
263 |
264 | }
--------------------------------------------------------------------------------
/app/src/server/java/ir/example/androidsocket/ui/ServerActivity.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.ui
2 |
3 | import android.Manifest
4 | import android.content.BroadcastReceiver
5 | import android.content.Context
6 | import android.content.Intent
7 | import android.content.IntentFilter
8 | import android.content.pm.PackageManager
9 | import android.net.ConnectivityManager
10 | import android.os.Build
11 | import android.os.Bundle
12 | import androidx.activity.ComponentActivity
13 | import androidx.activity.compose.setContent
14 | import androidx.activity.enableEdgeToEdge
15 | import androidx.activity.result.ActivityResultLauncher
16 | import androidx.activity.result.contract.ActivityResultContracts
17 | import androidx.activity.viewModels
18 | import androidx.annotation.RequiresApi
19 | import androidx.compose.foundation.layout.Box
20 | import androidx.compose.foundation.layout.fillMaxSize
21 | import androidx.compose.foundation.layout.safeDrawingPadding
22 | import androidx.compose.runtime.LaunchedEffect
23 | import androidx.compose.runtime.getValue
24 | import androidx.compose.ui.Alignment
25 | import androidx.compose.ui.Modifier
26 | import androidx.core.app.ActivityCompat
27 | import androidx.core.content.ContextCompat
28 | import androidx.core.view.WindowCompat
29 | import androidx.lifecycle.compose.collectAsStateWithLifecycle
30 | import com.example.androidSocket.R
31 | import dagger.hilt.android.AndroidEntryPoint
32 | import ir.example.androidsocket.Constants
33 | import ir.example.androidsocket.MainApplication
34 | import ir.example.androidsocket.ui.base.PermissionDialog
35 | import ir.example.androidsocket.utils.ConnectionTypeManager
36 | import ir.example.androidsocket.utils.NotificationMessageBroadcastReceiver
37 | import ir.example.androidsocket.utils.serverLog
38 |
39 |
40 | @AndroidEntryPoint
41 | class ServerActivity : ComponentActivity() {
42 |
43 | private val viewModel: ServerViewModel by viewModels()
44 | private lateinit var connectivityBroadcastReceiver: BroadcastReceiver
45 | private lateinit var notificationMessageReceiver: NotificationMessageBroadcastReceiver
46 |
47 |
48 | @RequiresApi(Build.VERSION_CODES.O)
49 | override fun onCreate(savedInstanceState: Bundle?) {
50 | enableEdgeToEdge()
51 | super.onCreate(savedInstanceState)
52 | val connectionTypeManager = ConnectionTypeManager(this)
53 |
54 | val activity = this@ServerActivity
55 | var requestNotificationPermissionLauncher: ActivityResultLauncher =
56 | registerForActivityResult(
57 | ActivityResultContracts.RequestPermission()
58 | ) { isGranted: Boolean ->
59 | if (isGranted) {
60 | viewModel.setOpenNotificationPermissionDialog(false)
61 | viewModel.setNotificationGranted(true)
62 | } else {
63 | viewModel.setOpenNotificationPermissionDialog(true)
64 | }
65 | }
66 |
67 | var requestStoragePermissionLauncher: ActivityResultLauncher =
68 | registerForActivityResult(
69 | ActivityResultContracts.RequestPermission()
70 | ) { isGranted ->
71 | if (isGranted) {
72 | viewModel.setOpenStoragePermissionDialog(false)
73 | viewModel.startServerService(activity)
74 | } else {
75 | viewModel.setOpenStoragePermissionDialog(true)
76 | }
77 | }
78 | setClientMessageBroadcastReceiver()
79 | setConnectivityBroadcastReceiver(connectionTypeManager)
80 | setContent {
81 |
82 | val openNotificationPermissionDialog by viewModel.openNotificationPermissionDialog.collectAsStateWithLifecycle(
83 | initialValue = false
84 | )
85 |
86 | val notificationPermissionGranted by viewModel.notificationPermissionGranted.collectAsStateWithLifecycle(
87 | initialValue = false
88 | )
89 |
90 | val openStoragePermissionDialog by viewModel.openStoragePermissionDialog.collectAsStateWithLifecycle(
91 | initialValue = false
92 | )
93 |
94 | LaunchedEffect(Unit) {
95 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
96 | /**
97 | * handles the logic to determine whether the app already has the permission, whether it needs to show a rationale, or whether it should request the permission.
98 | * **/
99 | when {
100 | ContextCompat.checkSelfPermission(
101 | activity,
102 | Manifest.permission.POST_NOTIFICATIONS
103 | ) == PackageManager.PERMISSION_GRANTED -> {
104 | viewModel.setNotificationGranted(true)
105 | }
106 |
107 | ActivityCompat.shouldShowRequestPermissionRationale(
108 | activity, Manifest.permission.POST_NOTIFICATIONS
109 | ) -> {
110 | /**
111 | * true if the user has previously denied the permission request .
112 | * explain to the user why your app requires this permission for a specific feature to behave as expected
113 | * and what features are disabled if it's declined.
114 | * */
115 | viewModel.setOpenNotificationPermissionDialog(true)
116 |
117 | }
118 |
119 | else -> {
120 | // The registered ActivityResultCallback gets the result of this request.
121 | viewModel.setOpenNotificationPermissionDialog(false)
122 | requestNotificationPermissionLauncher.launch(
123 | Manifest.permission.POST_NOTIFICATIONS
124 | )
125 | }
126 | }
127 | }
128 |
129 | }
130 | LaunchedEffect(notificationPermissionGranted) {
131 | if(notificationPermissionGranted){
132 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
133 | when {
134 | ContextCompat.checkSelfPermission(
135 | activity,
136 | Manifest.permission.WRITE_EXTERNAL_STORAGE
137 | ) == PackageManager.PERMISSION_GRANTED -> {
138 | viewModel.startServerService(activity)
139 | }
140 |
141 | ActivityCompat.shouldShowRequestPermissionRationale(
142 | activity, Manifest.permission.WRITE_EXTERNAL_STORAGE
143 | ) -> {
144 | viewModel.setOpenStoragePermissionDialog(true)
145 | }
146 | else -> {
147 | // The registered ActivityResultCallback gets the result of this request.
148 | viewModel.setOpenStoragePermissionDialog(false)
149 | requestStoragePermissionLauncher.launch(
150 | Manifest.permission.WRITE_EXTERNAL_STORAGE
151 | )
152 | }
153 | }
154 | } else {
155 | viewModel.startServerService(activity)
156 | }
157 | }
158 | }
159 |
160 | Box(Modifier.fillMaxSize()) {
161 | ServerComposable(
162 | viewModel = viewModel,
163 | connectionTypeManager = connectionTypeManager,
164 | onEvent = { viewModel.onEvent(it) }
165 | )
166 | if (openNotificationPermissionDialog) {
167 | PermissionDialog(
168 | modifier = Modifier.align(Alignment.Center),
169 | permissionReason = R.string.notification_permission_reason,
170 | onDismissRequest = {
171 | viewModel.setOpenNotificationPermissionDialog(false)
172 | finish()
173 | }
174 | ) {
175 | viewModel.setOpenNotificationPermissionDialog(false)
176 | requestNotificationPermissionLauncher.launch(
177 | Manifest.permission.POST_NOTIFICATIONS
178 | )
179 | }
180 | }
181 | if(openStoragePermissionDialog){
182 | PermissionDialog(
183 | modifier = Modifier.align(Alignment.Center),
184 | permissionReason = R.string.storage_permission_reason,
185 | onDismissRequest = {
186 | viewModel.setOpenStoragePermissionDialog(false)
187 | finish()
188 | }
189 | ) {
190 | viewModel.setOpenStoragePermissionDialog(false)
191 | requestStoragePermissionLauncher.launch(
192 | Manifest.permission.WRITE_EXTERNAL_STORAGE
193 | )
194 | }
195 | }
196 | }
197 | }
198 | }
199 |
200 | override fun onDestroy() {
201 | viewModel.performCleanup()
202 | unregisterReceiver(connectivityBroadcastReceiver)
203 | unregisterReceiver(notificationMessageReceiver)
204 | super.onDestroy()
205 | }
206 |
207 |
208 | override fun onResume() {
209 | (application as? MainApplication)?.notifyAppForeground()
210 | super.onResume()
211 | }
212 |
213 | override fun onPause() {
214 | (application as? MainApplication)?.notifyAppBackground()
215 | super.onPause()
216 | }
217 |
218 | /**
219 | * set a broadcastReceiver to react to the action which is sent by the notification containing client's message
220 | * */
221 | @RequiresApi(Build.VERSION_CODES.O)
222 | private fun setClientMessageBroadcastReceiver() {
223 | notificationMessageReceiver = NotificationMessageBroadcastReceiver(
224 | onMessageReceivedAction = {
225 | serverLog(message = "notificationReceiver onMessageReceivedAction ")
226 | //handle the tasks you want to be done in case of message is sent by client
227 | }
228 | )
229 |
230 | // Register the receiver with specific actions
231 | val intentFilter = IntentFilter().apply {
232 | addAction(Constants.ActionCode.NotificationMessage.title)
233 | }
234 |
235 |
236 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
237 | // For Android Oreo (API level 26) and above
238 | registerReceiver(notificationMessageReceiver, intentFilter, RECEIVER_NOT_EXPORTED)
239 | } else {
240 | // For older Android versions (before Oreo), use Context.registerReceiver without flags
241 | registerReceiver(notificationMessageReceiver, intentFilter)
242 | }
243 | }
244 |
245 |
246 | /**
247 | * set a broadcastReceiver to react to the connection type changes
248 | * */
249 | private fun setConnectivityBroadcastReceiver(connectionTypeManager: ConnectionTypeManager) {
250 | connectivityBroadcastReceiver = object : BroadcastReceiver() {
251 | override fun onReceive(context: Context?, intent: Intent?) {
252 | connectionTypeManager.checkConnectionStatus()
253 | }
254 | }
255 |
256 | val intentFilter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
257 | registerReceiver(connectivityBroadcastReceiver, intentFilter)
258 | }
259 | }
260 |
261 |
--------------------------------------------------------------------------------
/app/src/client/java/ir/example/androidsocket/client/TcpClientManager.kt:
--------------------------------------------------------------------------------
1 | package ir.example.androidsocket.client
2 |
3 |
4 | import android.content.ContentResolver
5 | import android.net.Uri
6 | import android.os.ParcelFileDescriptor
7 | import ir.example.androidsocket.SocketConnectionListener
8 | import ir.example.androidsocket.utils.BytesUtils
9 | import ir.example.androidsocket.utils.clientLog
10 | import kotlinx.coroutines.CoroutineScope
11 | import kotlinx.coroutines.Dispatchers
12 | import kotlinx.coroutines.delay
13 | import kotlinx.coroutines.launch
14 | import kotlinx.coroutines.withContext
15 | import java.io.IOException
16 | import java.io.InputStream
17 | import java.io.OutputStream
18 | import java.net.InetAddress
19 | import java.net.InetSocketAddress
20 | import java.net.Socket
21 | import java.nio.ByteBuffer
22 |
23 | class TcpClientManager(
24 | override var ip: String,
25 | override var port: String,
26 | override val socketListener: List,
27 | private val contentResolver: ContentResolver
28 | ) : SocketClient {
29 |
30 | var socket: Socket? = null
31 | val serverAddress: InetAddress = InetAddress.getByName(ip)
32 | private var outputStream: OutputStream? = null
33 | private var inputStream: InputStream? = null
34 | val BUFFER_SIZE = 1024
35 |
36 | override suspend fun connectWithTimeout(timeoutMillis: Long): Unit =
37 | withContext(Dispatchers.IO) {
38 | //to show message properly
39 | delay(1000)
40 | try {
41 | clientLog("TcpClientManager connectWithTimeout $timeoutMillis $serverAddress $port")
42 | socket = Socket()
43 | socket?.let { socket ->
44 |
45 | socket.connect(
46 | InetSocketAddress(serverAddress, port.toInt()),
47 | timeoutMillis.toInt()
48 | )
49 |
50 | clientLog("TcpClientManager connectWithTimeout isConnected ${socket.isConnected}")
51 | if (socket.isConnected) {
52 | socketListener.forEach { it.onConnected() }
53 | inputStream = socket.getInputStream()
54 | outputStream = socket.getOutputStream()
55 |
56 | listenToServer(socket)
57 | }
58 | }
59 |
60 |
61 | } catch (e: IOException) {
62 | clientLog("connectWithTimeout IOException--> ${e.message}")
63 | socketListener.forEach { it.onException(e) }
64 | } catch (e: Exception) {
65 | clientLog("connectWithTimeout Exception--> $serverAddress")
66 | socketListener.forEach { it.onException(e) }
67 | }
68 | }
69 |
70 | override fun sendMessage(message: String, timeoutMillis: Long) {
71 | clientLog("sendMessage()")
72 | CoroutineScope(Dispatchers.IO).launch {
73 | try {
74 | clientLog("send-->try")
75 | val outputStream = socket?.getOutputStream()
76 |
77 | // Convert message to byte array
78 | val messageBytes = message.toByteArray()
79 |
80 | // Convert message size to 4-byte array (integer)
81 | val messageSizeBytes = ByteBuffer.allocate(4).putInt(messageBytes.size).array()
82 |
83 | // Message type as a single byte (for example, 0x01 for text message)
84 | val messageType = 0x01.toByte()
85 |
86 | // Combine everything into one array
87 | val dataToSend = ByteArray(1 + 4 + messageBytes.size)
88 | dataToSend[0] = messageType // First byte is message type
89 | System.arraycopy(messageSizeBytes, 0, dataToSend, 1, 4) // Next 4 bytes are the message size
90 | System.arraycopy(messageBytes, 0, dataToSend, 5, messageBytes.size) // Rest is the message
91 |
92 | // Write the combined array to the output stream
93 | outputStream?.write(dataToSend)
94 | outputStream?.flush()
95 |
96 | } catch (e: Exception) {
97 | clientLog("send--> catch ${e.message}")
98 | }
99 | }
100 | }
101 |
102 | override fun sendFile(uri: Uri) {
103 | CoroutineScope(Dispatchers.IO).launch {
104 | try {
105 | clientLog("sendFile--> try $uri")
106 | val outputStream = socket?.getOutputStream()
107 |
108 | val mimeType = contentResolver.getType(uri)
109 | clientLog("Detected MIME type: $mimeType")
110 |
111 | // Extract the specific type after the "/"
112 | val fileType = mimeType?.substringAfter("/") ?: "unknown"
113 | clientLog("Extracted file type: $fileType")
114 | val fileTypeBytes = fileType.toByteArray(Charsets.UTF_8)
115 | val fileTypeLength = fileTypeBytes.size
116 | clientLog("sendFile--> Sending file fileTypeLength: $fileTypeLength")
117 | clientLog("sendFile--> Sending file fileTypeBytes bytesToHex: ${BytesUtils.bytesToHex(fileTypeBytes)}")
118 | clientLog("sendFile--> Sending file hexToString: ${BytesUtils.hexToString(BytesUtils.bytesToHex(fileTypeBytes))}")
119 |
120 |
121 | /**
122 | * Get the file size using ContentResolver
123 | * ParcelFileDescriptor : allows you to perform file operations like reading or writing on the file associated with the URI.
124 | * **/
125 | val fileDescriptor: ParcelFileDescriptor? = contentResolver.openFileDescriptor(uri, "r")
126 | if (fileDescriptor != null) {
127 | val fileSize : Long =
128 | fileDescriptor.statSize // Get the file size directly from the file descriptor
129 |
130 | val messageType = 0x02.toByte()
131 |
132 | clientLog("sendFile--> write1")
133 |
134 | // Send the file size to the server first
135 | clientLog("sendFile--> Sending file size: $fileSize bytes")
136 | val messageSizeBytes = ByteBuffer.allocate(4).putInt(fileSize.toInt()).array()
137 |
138 |
139 | // 1 byte for message type + 4 bytes for file size + 1 byte for file type length + file type bytes
140 | val headerSize = 1 + 4 + 1 + fileTypeLength
141 | val header = ByteArray(headerSize)
142 |
143 | header[0] = messageType // Set the message type
144 | System.arraycopy(messageSizeBytes, 0, header, 1, 4)// Copy the file size bytes to the header
145 | header[5] = fileTypeLength.toByte()// Set the file type length (1 byte)
146 | System.arraycopy(fileTypeBytes, 0, header, 6, fileTypeLength) // Copy the file type bytes after the length byte
147 |
148 |
149 | // Write the header (message type + file size)
150 | outputStream?.write(header)
151 | outputStream?.flush()
152 |
153 | // Prepare to read the file
154 | val fileInputStream = contentResolver.openInputStream(uri)
155 | if (fileInputStream == null) {
156 | clientLog("sendFile--> InputStream is null for Uri: $uri")
157 | return@launch
158 | }
159 |
160 | // Send the file in chunks
161 | val buffer = ByteArray(BUFFER_SIZE)
162 |
163 | var bytesRead: Int
164 | var totalBytesRead = 0
165 |
166 | while (fileInputStream.read(buffer).also { bytesRead = it } != -1) {
167 | clientLog("sendFile-->sendFile while $bytesRead")
168 | totalBytesRead += bytesRead
169 | val progress = (totalBytesRead * 100) / fileSize.toInt()
170 | delay(50)
171 | socketListener.forEach { it.onProgressUpdate(progress) }
172 | outputStream?.write(buffer, 0, bytesRead)
173 | outputStream?.flush()
174 | }
175 | fileInputStream.close()
176 | clientLog("sendFile--> File transfer complete")
177 | } else {
178 | clientLog("sendFile--> Could not open file descriptor for Uri: $uri")
179 | }
180 |
181 | } catch (e: Exception) {
182 | clientLog("sendFile--> catch ${e.message}")
183 | socketListener.forEach { it.onError(e) }
184 | }
185 | }
186 | }
187 |
188 | /**
189 | * listen to stream came from server
190 | * **/
191 | private suspend fun listenToServer(clientSocket: Socket) {
192 | clientLog("listenToServer")
193 |
194 | withContext(Dispatchers.IO) {
195 |
196 | try {
197 | clientLog("listenToServer try")
198 | val buffer = ByteArray(BUFFER_SIZE)
199 | clientLog("listenToServer try ${clientSocket.isConnected}")
200 | var bytesRead: Int = 0
201 | // Read data into the buffer and assign the number of bytes read
202 | while (clientSocket.isConnected && inputStream?.read(buffer)
203 | .also { bytesRead = it ?: 0 } != -1
204 | ) {
205 | clientLog("listenToServer while: ${clientSocket.isConnected}")
206 | if (bytesRead > 0) {
207 | clientLog("handleClient try bytesRead > 0")
208 | val hexMessage = BytesUtils.bytesToHex(buffer.copyOf(bytesRead))
209 | val stringMessage = BytesUtils.hexToString(hexMessage)
210 | socketListener.forEach { it.onMessage(null,stringMessage) }
211 | }
212 | }
213 |
214 | } catch (e: Exception) {
215 | clientLog("listenToServer catch: ${e.message}")
216 | socketListener.forEach { it.onError(e) }
217 | } finally {
218 | /**
219 | * this block is executed whenever the while become false (client becomes disconnected or inputStream?.read(buffer)==-1 which means server is disconnected) or
220 | * the block throws an exception
221 | * **/
222 | clientLog("listenToServer finally: ${clientSocket.isConnected}")
223 | closeClient()
224 | }
225 | }
226 | }
227 |
228 | /**
229 | * close the client whenever it become disconnected
230 | * **/
231 | private fun closeClient() {
232 | clientLog("closeClient")
233 | socket?.let { client ->
234 | socketListener.forEach {
235 | it.onDisconnected(
236 | code = null,
237 | reason = "client : ${client.inetAddress?.hostAddress} is disconnected"
238 | )
239 | }
240 | inputStream?.close()
241 | outputStream?.close()
242 | client.close()
243 | }
244 |
245 | }
246 |
247 | override fun disconnect() {
248 | closeClient()
249 | }
250 |
251 |
252 | override fun onMessage(message: String?) {
253 | CoroutineScope(Dispatchers.IO).launch {
254 | try {
255 | clientLog("send-->try")
256 | inputStream = socket?.getInputStream()
257 | val buffer = ByteArray(BUFFER_SIZE)
258 | val bytesRead = inputStream?.read(buffer)
259 | if (bytesRead != null && bytesRead > 0) {
260 | val hexMessage = BytesUtils.bytesToHex(buffer)
261 | val stringMessage = BytesUtils.hexToString(hexMessage)
262 | socketListener.forEach { it.onMessage(null,message = stringMessage) }
263 | }
264 | } catch (e: Exception) {
265 | clientLog("send--> catch ${e.message}")
266 | socketListener.forEach { it.onError(e) }
267 | }
268 | }
269 | }
270 |
271 |
272 | }
--------------------------------------------------------------------------------
/.idea/other.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
--------------------------------------------------------------------------------