├── .gitattributes ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── build-test.yml │ ├── check-style.yml │ ├── release.yml │ └── resources │ └── release_notes.md ├── .gitignore ├── .yamllint ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── .gitignore │ ├── androidTest │ └── java │ │ └── com │ │ └── tw │ │ └── clipshare │ │ ├── UseAppContextTest.java │ │ ├── UtilsTest.java │ │ ├── netConnection │ │ └── MockConnection.java │ │ ├── platformUtils │ │ ├── AndroidUtilsTest.java │ │ ├── StatusNotifierTest.java │ │ └── TimeContainerTest.java │ │ └── proto │ │ ├── BAOStreamBuilder.java │ │ ├── ProtoV1Test.java │ │ ├── ProtoV2Test.java │ │ ├── ProtoV3Test.java │ │ └── ProtocolSelectorTest.java │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ └── com │ │ └── tw │ │ └── clipshare │ │ ├── CertUtils.java │ │ ├── ClipShareActivity.java │ │ ├── FileService.java │ │ ├── PendingFile.java │ │ ├── PendingTask.java │ │ ├── ServerFinder.java │ │ ├── Settings.java │ │ ├── SettingsActivity.java │ │ ├── SubnetScanner.java │ │ ├── Utils.java │ │ ├── netConnection │ │ ├── PlainConnection.java │ │ ├── SecureConnection.java │ │ ├── ServerConnection.java │ │ ├── TunnelConnection.java │ │ └── TunnelManager.java │ │ ├── platformUtils │ │ ├── AndroidUtils.java │ │ ├── DataContainer.java │ │ ├── FSUtils.java │ │ ├── StatusNotifier.java │ │ └── directoryTree │ │ │ ├── Directory.java │ │ │ ├── DirectoryTreeNode.java │ │ │ └── RegularFile.java │ │ └── protocol │ │ ├── Proto.java │ │ ├── ProtoMethods.java │ │ ├── ProtoV1.java │ │ ├── ProtoV2.java │ │ ├── ProtoV3.java │ │ └── ProtocolSelector.java │ └── res │ ├── drawable-hdpi │ ├── ic_insecure.png │ └── ic_secure.png │ ├── drawable-mdpi │ ├── ic_insecure.png │ └── ic_secure.png │ ├── drawable-xhdpi │ ├── ic_insecure.png │ └── ic_secure.png │ ├── drawable-xxhdpi │ ├── ic_insecure.png │ └── ic_secure.png │ ├── drawable-xxxhdpi │ ├── ic_insecure.png │ └── ic_secure.png │ ├── drawable │ ├── clip_share_icon_mono.xml │ ├── ic_download_icon.xml │ ├── ic_upload_icon.xml │ ├── open_icon.xml │ ├── open_icon_resized.xml │ ├── share_icon.xml │ └── share_icon_resized.xml │ ├── layout-v26 │ ├── activity_main.xml │ ├── list_element.xml │ └── popup_elem.xml │ ├── layout │ ├── activity_main.xml │ ├── list_element.xml │ ├── popup_display.xml │ ├── popup_elem.xml │ ├── popup_servers.xml │ ├── settings_activity.xml │ └── tunnel_switch.xml │ ├── menu │ └── action_bar.xml │ ├── mipmap-anydpi │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ └── ic_launcher_foreground.png │ ├── mipmap-mdpi │ └── ic_launcher_foreground.png │ ├── mipmap-xhdpi │ └── ic_launcher_foreground.png │ ├── mipmap-xxhdpi │ └── ic_launcher_foreground.png │ ├── mipmap-xxxhdpi │ └── ic_launcher_foreground.png │ ├── values-night │ ├── colors.xml │ └── themes.xml │ ├── values │ ├── colors.xml │ ├── ic_launcher_background.xml │ ├── strings.xml │ └── themes.xml │ └── xml │ └── provider_paths.xml ├── build.gradle ├── fastlane └── metadata │ └── android │ └── en-US │ ├── full_description.txt │ ├── images │ ├── featureGraphic.png │ ├── icon.png │ └── phoneScreenshots │ │ ├── 1.jpg │ │ ├── 2.jpg │ │ ├── 3.jpg │ │ └── 4.jpg │ ├── short_description.txt │ └── title.txt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | *.jar binary 4 | *.jpg binary 5 | *.png binary 6 | 7 | *.cmd text eol=crlf 8 | *.bat text eol=crlf 9 | 10 | *.sh text eol=lf 11 | /gradlew text eol=lf 12 | *.html text eol=lf 13 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @thevindu-w 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | groups: 8 | github-actions: 9 | patterns: 10 | - "*" 11 | 12 | - package-ecosystem: "gradle" 13 | directory: "app/" 14 | schedule: 15 | interval: "weekly" 16 | groups: 17 | gradle: 18 | patterns: 19 | - "*" 20 | -------------------------------------------------------------------------------- /.github/workflows/build-test.yml: -------------------------------------------------------------------------------- 1 | name: Build-and-Test 2 | run-name: Build and Test on ${{ github.sha }} 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | paths: 9 | - 'app/**' 10 | - 'settings.gradle' 11 | - 'build.gradle' 12 | - '.github/workflows/build-test.yml' 13 | pull_request: 14 | branches: 15 | - master 16 | workflow_call: null 17 | 18 | jobs: 19 | Style-Check: 20 | uses: ./.github/workflows/check-style.yml 21 | with: 22 | trigger: "${{ github.event_name }}" 23 | permissions: 24 | contents: write 25 | pull-requests: write 26 | 27 | Build-and-Test: 28 | runs-on: ubuntu-latest 29 | timeout-minutes: 15 30 | needs: Style-Check 31 | 32 | steps: 33 | - name: Check out repository code 34 | uses: actions/checkout@v4 35 | with: 36 | ref: ${{ github.ref }} 37 | 38 | - name: Set up JDK 21 39 | uses: actions/setup-java@v4 40 | with: 41 | java-version: '21' 42 | distribution: 'temurin' 43 | 44 | - name: Create KeyStore and gradle.properties 45 | run: | 46 | storePass=dummyPassword 47 | keyAlias=test 48 | keyPass=dummyPassword 49 | keytool -genkey -keystore keyStore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias "$keyAlias" \ 50 | -dname "cn=Unknown, ou=Unknown, o=Unknown, c=Unknown" -storepass "$storePass" -keypass "$keyPass" 51 | mkdir -p ~/.gradle 52 | echo "RELEASE_STORE_FILE=../keyStore.jks" >~/.gradle/gradle.properties 53 | echo "RELEASE_STORE_PASSWORD=$storePass" >>~/.gradle/gradle.properties 54 | echo "RELEASE_KEY_ALIAS=$keyAlias" >>~/.gradle/gradle.properties 55 | echo "RELEASE_KEY_PASSWORD=$keyPass" >>~/.gradle/gradle.properties 56 | 57 | - name: Build 58 | run: | 59 | chmod +x gradlew 60 | ./gradlew assembleLatest 61 | 62 | - name: Enable KVM 63 | run: | 64 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | \ 65 | sudo tee /etc/udev/rules.d/99-kvm4all.rules 66 | sudo udevadm control --reload-rules 67 | sudo udevadm trigger --name-match=kvm 68 | 69 | - name: Test 70 | uses: reactivecircus/android-emulator-runner@v2 71 | with: 72 | api-level: 34 73 | arch: x86_64 74 | script: ./gradlew connectedLatestDebugAndroidTest 75 | -------------------------------------------------------------------------------- /.github/workflows/check-style.yml: -------------------------------------------------------------------------------- 1 | name: Style-Check 2 | run-name: Check Style on ${{ github.sha }} 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | paths: 9 | - '.github/**' 10 | - '!.github/workflows/resources/*' 11 | - '!.github/workflows/build-test.yml' 12 | - '.yamllint' 13 | workflow_call: 14 | inputs: 15 | trigger: 16 | type: string 17 | required: false 18 | 19 | jobs: 20 | Style-Check: 21 | runs-on: ubuntu-latest 22 | timeout-minutes: 5 23 | 24 | permissions: 25 | contents: write 26 | pull-requests: write 27 | 28 | steps: 29 | - name: Check out repository code 30 | uses: actions/checkout@v4 31 | with: 32 | ref: ${{ github.ref }} 33 | 34 | - name: Install tools 35 | run: | 36 | echo "set man-db/auto-update false" | sudo debconf-communicate && sudo dpkg-reconfigure man-db -f noninteractive 37 | sudo apt-get update && sudo apt-get install -y --no-install-recommends yamllint 38 | 39 | - name: Set up JDK 21 40 | uses: actions/setup-java@v4 41 | with: 42 | java-version: '21' 43 | distribution: 'temurin' 44 | 45 | - name: Set environment 46 | run: echo EVENT="${{ inputs.trigger || github.event_name }}" >> $GITHUB_ENV 47 | 48 | - name: Create gradle.properties 49 | run: | 50 | mkdir -p ~/.gradle 51 | echo "RELEASE_STORE_FILE=notExisting.jks" >~/.gradle/gradle.properties 52 | echo "RELEASE_STORE_PASSWORD=unused" >>~/.gradle/gradle.properties 53 | echo "RELEASE_KEY_ALIAS=unused" >>~/.gradle/gradle.properties 54 | echo "RELEASE_KEY_PASSWORD=unused" >>~/.gradle/gradle.properties 55 | 56 | - name: Grant execute permission for gradlew 57 | run: chmod +x gradlew 58 | 59 | - name: Verify Google Java format 60 | run: ./gradlew verifyGoogleJavaFormat 61 | 62 | - name: Check yaml style 63 | run: yamllint . 64 | 65 | - name: Format Java code 66 | if: ${{ (env.EVENT == 'push') && (success() || failure()) }} 67 | run: ./gradlew googleJavaFormat 68 | 69 | - name: Create pull request 70 | if: ${{ (env.EVENT == 'push') && (success() || failure()) }} 71 | env: 72 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 73 | run: | 74 | [[ -z "$(git status -s)" ]] && echo No changes && exit 0 75 | curr_branch="${{ github.ref_name }}" 76 | new_branch="auto-format-$(git rev-parse HEAD | head -c 8)" 77 | author_name="$(git log -1 --pretty=format:'%an')" 78 | author_email="$(git log -1 --pretty=format:'%ae')" 79 | git checkout -b "$new_branch" && git merge "$curr_branch" 80 | git config user.name "$author_name" 81 | git config user.email "$author_email" 82 | git remote set-url origin "https://github.com/${{ github.repository }}" 83 | git commit -am 'Apply code formatting automatically from GitHub actions' 84 | git push origin "$new_branch" 85 | gh pr create -B "$curr_branch" -H "$new_branch" --title "Merge \`$new_branch\` into \`$curr_branch\`" \ 86 | --body 'Apply code formatting [generated automatically]' 87 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create-Release 2 | run-name: Create release ${{github.ref_name}} 3 | 4 | on: 5 | push: 6 | tags: 7 | - 'v[0-9]+.[0-9]+.[0-9]+' 8 | 9 | jobs: 10 | Build-and-Test: 11 | uses: ./.github/workflows/build-test.yml 12 | permissions: 13 | contents: write 14 | pull-requests: write 15 | security-events: write 16 | 17 | Release: 18 | runs-on: ubuntu-latest 19 | timeout-minutes: 20 20 | permissions: 21 | contents: write 22 | id-token: write 23 | attestations: write 24 | needs: 25 | - Build-and-Test 26 | 27 | steps: 28 | - name: Check out repository code 29 | uses: actions/checkout@v4 30 | 31 | - name: Set variables 32 | run: | 33 | version="$(echo '${{github.ref_name}}' | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')" 34 | echo VERSION="$version" >> $GITHUB_ENV 35 | echo TAG="v${version}" >> $GITHUB_ENV 36 | echo TITLE="Version ${version}" >> $GITHUB_ENV 37 | 38 | - name: Check version 39 | run: | 40 | version="${{env.VERSION}}" 41 | echo 'Checking versionName ...' 42 | [ ! -z "$(grep -E 'versionName\s?=' app/build.gradle | grep -F "$version")" ] 43 | echo 'versionName is correct.' 44 | major="$(cut -d'.' -f1 <<<"$version")" 45 | minor="$(cut -d'.' -f2 <<<"$version")" 46 | patch="$(cut -d'.' -f3 <<<"$version")" 47 | versionCode="$(printf '%i%02i%02i' "$major" "$minor" "$patch")" 48 | echo 'Checking versionCode ...' 49 | [ ! -z "$(grep -E 'versionCode\s?=' app/build.gradle | grep -F "$versionCode")" ] 50 | echo 'versionCode is correct.' 51 | 52 | - name: Check release_notes.md version 53 | run: | 54 | for v in $(grep -oE '[0-9]+\.[0-9]+\.[0-9]+' .github/workflows/resources/release_notes.md); do \ 55 | grep -qF "${{env.VERSION}}" <<<"$v"; \ 56 | done 57 | 58 | - name: Set up JDK 21 59 | uses: actions/setup-java@v4 60 | with: 61 | java-version: '21' 62 | distribution: 'temurin' 63 | 64 | - name: Set gradle.properties 65 | env: 66 | PROPERTIES_BASE64: ${{ secrets.PROPERTIES_BASE64 }} 67 | run: | 68 | mkdir -p ~/.gradle 69 | echo "$PROPERTIES_BASE64" | base64 --decode >~/.gradle/gradle.properties 70 | 71 | - name: Set KeyStore 72 | env: 73 | KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }} 74 | run: echo "$KEYSTORE_BASE64" | base64 --decode >keyStore.jks 75 | 76 | - name: Build APK 77 | run: | 78 | chmod +x gradlew 79 | ./gradlew assembleRelease 80 | mkdir release 81 | mv app/build/outputs/apk/latest/release/*.apk "release/clip_share_client.apk" 82 | mv app/build/outputs/apk/legacy/release/*.apk "release/clip_share_client-legacy.apk" 83 | cd release/ 84 | sha256sum -b * >SHA2-256SUM 85 | 86 | - name: Generate artifact attestation 87 | uses: actions/attest-build-provenance@v2 88 | with: 89 | subject-path: "release/SHA2-256SUM" 90 | 91 | - name: Create release 92 | env: 93 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 94 | run: | 95 | sed -i "s//${{env.VERSION}}/g" .github/workflows/resources/release_notes.md 96 | cd release 97 | gh release create "${{env.TAG}}" --latest --verify-tag \ 98 | --notes-file ../.github/workflows/resources/release_notes.md \ 99 | --title "${{env.TITLE}}" * 100 | -------------------------------------------------------------------------------- /.github/workflows/resources/release_notes.md: -------------------------------------------------------------------------------- 1 | This is version of the Android client of ClipShare, which supports protocol versions 1, 2, and 3. 2 | You will need the ClipShare server running on a Windows, macOS, or Linux machine to connect. You can download the server at [github.com/thevindu-w/clip_share_server/releases](https://github.com/thevindu-w/clip_share_server/releases). 3 | 4 | There are 2 APK files included in the release assets for compatibility. 5 | - `clip_share_client.apk` - Recommended for Android 9 (API level 28) and above. 6 | - `clip_share_client-legacy.apk` - Supports Android 7 (API level 24) and above. 7 |
8 | Refer to the [README](https://github.com/thevindu-w/clip_share_client/#how-to-use) for usage information. 9 |
10 | **Changes:** 11 | - Reduce file transfer estimated time fluctuations. 12 | - UI improvements. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.apk 3 | *.class 4 | *.jks 5 | .gradle 6 | local.properties 7 | .idea/ 8 | .DS_Store 9 | build/ 10 | captures/ 11 | .externalNativeBuild 12 | .cxx 13 | TODO 14 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | yaml-files: 2 | - '*.yaml' 3 | - '*.yml' 4 | - '.yamllint' 5 | 6 | rules: 7 | braces: enable 8 | brackets: enable 9 | colons: enable 10 | commas: enable 11 | comments: enable 12 | comments-indentation: enable 13 | document-start: false 14 | empty-lines: enable 15 | empty-values: enable 16 | hyphens: enable 17 | indentation: 18 | spaces: 2 19 | indent-sequences: false 20 | key-duplicates: enable 21 | line-length: 22 | max: 120 23 | level: warning 24 | new-line-at-end-of-file: enable 25 | new-lines: enable 26 | octal-values: enable 27 | trailing-spaces: enable 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-2025 H. Thevindu J. Wijesekera 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ClipShare Android Client 2 | 3 | ### Client application of ClipShare for Android devices. 4 | 5 | ![Build and Test](https://github.com/thevindu-w/clip_share_client/actions/workflows/build-test.yml/badge.svg?branch=master) 6 | ![Last commit](https://img.shields.io/github/last-commit/thevindu-w/clip_share_client.svg?color=yellow) 7 | ![License](https://img.shields.io/github/license/thevindu-w/clip_share_client.svg?color=blue) 8 | 9 | [![Latest release](https://img.shields.io/github/v/release/thevindu-w/clip_share_client?color=purple)](https://github.com/thevindu-w/clip_share_client/releases) 10 | [![Stars](https://img.shields.io/github/stars/thevindu-w/clip_share_client)](https://github.com/thevindu-w/clip_share_client/stargazers) 11 | 12 | Share the clipboard between your phone and desktop. Share files and screenshots securely. 13 |
14 | ClipShare is a lightweight, cross-platform app for sharing copied text, files, and screenshots between an Android mobile 15 | and a desktop. 16 | 17 | ## Download 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 29 | 35 | 36 |
ServerClient
26 | Get it on GitHub
27 | Download the server from Releases. 28 |
30 | Get it on IzzyOnDroid
31 | Download the client app 32 | from apt.izzysoft.de/fdroid/index/apk/com.tw.clipshare.
33 | or from GitHub Releases. 34 |
37 | 38 |
39 | 40 | This is the Android client of ClipShare. You will need the server on your desktop to connect with it. 41 | ClipShare is lightweight and easy to use. Run the server on your Windows, macOS, or Linux machine to use the ClipShare 42 | app. You can find more information on running the server on Windows, macOS, or Linux at 43 | [github.com/thevindu-w/clip_share_server](https://github.com/thevindu-w/clip_share_server#how-to-use). 44 | 45 | ## How to use 46 | 47 | ### Main screen 48 | 49 |

50 | help image 51 |      52 | dark theme 53 |      54 |

55 | 56 | - **Get text**: To get copied text from the server (ex: laptop) to the phone. 57 | 58 | _Steps_: 59 | - Copy any text on the laptop. 60 | - Press the green colored _GET_ button. 61 | - Now, the copied text is received and copied to the phone. Paste it anywhere on the phone (possibly in a different 62 | app). 63 | 64 | 65 | - **Send text**: To send copied text from the phone to the server (ex: laptop). 66 | 67 | _Steps_: 68 | - Copy any text on the phone (possibly in a different app). 69 | - Press the red colored _SEND_ button. 70 | - Now, the copied text is sent and copied to the laptop. Paste it anywhere on the laptop. 71 | 72 | 73 | - **Get files**: To get copied files from the server (ex: laptop) to the phone. 74 | 75 | _Steps_: 76 | - Copy any file(s) and/or folder(s) on the laptop. 77 | - Press the green colored _FILE_ button. 78 | - The copied files and folders are now received and saved on the phone. 79 | 80 | 81 | - **Send files**: To send files from the phone to the server (ex: laptop). 82 | 83 | _Method 1 Steps_: 84 | - Press the red colored _FILE_ button. 85 | - Select the file(s) to send. 86 | - The files are now sent to the laptop. 87 | 88 | _Method 2 Steps_: 89 | - Share any file(s) with ClipShare from any other app. 90 | - Press the red colored _FILE_ button. 91 | - The files are now sent to the laptop. 92 | 93 | 94 | - **Send folder**: To send a folder from the phone to the server (ex: laptop). 95 | 96 | _Steps_: 97 | - Press the red colored _FOLDER_ button. 98 | - Select the folder to send. 99 | - The folder is now sent to the laptop. 100 | 101 |   Note: Sending folders requires a server version 2.x or later. 102 | 103 | - **Get image/screenshot**: To get a copied image or screenshot from the server (ex: laptop) to the phone. 104 | 105 | _Steps_: 106 | - Optional: Copy an image (not an image file) to the clipboard on the laptop. 107 | - Press the green colored _IMAGE_ button. 108 | - If there is an image copied on the laptop, it will be received and saved on the phone. 109 | Otherwise, a screenshot of the laptop will be received and saved on the phone. 110 |

111 | Long pressing the _IMAGE_ button gives more options. 112 | - Get only a copied image without a screenshot. 113 | - Get only a screenshot, even when there is an image, copied to the clipboard of the laptop. 114 | - Select the display to get the screenshot. 115 | 116 |   Note: These options require a server version 3.x or later to work. 117 | 118 | - **Scan**: To scan the network to find available servers in the network. 119 | 120 | If there is any server in the network, scanning will find that. If the scan finds only one server, its address will be 121 | placed in the _Server_ address input area. If the scan finds many servers, a popup will appear to select any server 122 | out of them, and the selected address will be placed in the _Server_ address input area. 123 | 124 | ### Settings 125 | 126 | #### Auto send 127 | 128 | - **Auto send text:** When this setting is enabled, ClipShare will automatically send the text shared with it from other 129 | apps (ex: when sharing a link from the web browser) without requiring to tap the _Send_ button. 130 | - **Auto send files:** When this setting is enabled, ClipShare will automatically send the files shared with it 131 | (ex: sharing documents or photos from the file manager or gallery) without requiring to tap the _Send File_ button. 132 | - **Auto send to:** This is the list of trusted servers to auto-send. Add the IP address of each server using the `+` 133 | button. Setting the address to `*` will allow auto-sending to any server. Tap on the address to edit it, and tap on 134 | the `X` button to remove the entry from the list. 135 | 136 | #### Saved addresses 137 | 138 | - **Save addresses:** When this setting is enabled, ClipShare will save the server addresses used by the app. 139 | - **Saved servers:** This is the list of automatically saved server addresses. You can manually add an IP address to 140 | the list using the `+` button. Tap on any address to edit it, and tap on the `X` button to remove it from the list. 141 | 142 | #### Secure mode 143 | 144 | - **CA Certificate:** This is the self-signed TLS certificate of the certification authority that signed the client and 145 | server's TLS certificates. Select the certificate file using the _Browse_ button. 146 | - **Client Certificate:** This is the TLS key and certificate _p12_ or _pfx_ file of the client. Before selecting the 147 | file using the _Browse_ button, you must enter the password for the _pfx_ file. 148 | The password should have less than 256 characters. 149 | - **Trusted servers:** This is the list of trusted servers to which the client is allowed to connect. Add the _Common 150 | Name_ of each server using the `+` button. Tap on the name to edit it, and tap on the `X` button to remove the entry 151 | from the list. The client app will refuse to connect to servers not having TLS certificates with their _Common Name_ 152 | listed under this list when secure mode is enabled. 153 | - **Secure mode:** When this setting is enabled, the connections with the server (ex: your laptop) are secured with TLS 154 | encryption. Enabling this setting prevents others on the same network from spying on or modifying the data you share 155 | with your laptop. To enable this setting, you need to select the CA certificate and client TLS certificate and add at 156 | least one trusted server. Additionally, you need to configure the server to create and use a server certificate. 157 | Refer to the 158 | [TLS certificates](https://github.com/thevindu-w/clip_share_server#create-ssltls-certificate-and-key-files) and 159 | [Configuration](https://github.com/thevindu-w/clip_share_server#configuration) sections of the 160 | [ClipShare server](https://github.com/thevindu-w/clip_share_server) for more information. 161 | 162 | #### Other settings 163 | 164 | - **Close app if idle:** When this setting is enabled, the ClipShare app will automatically close if it is kept idle 165 | without interacting with it for some time. This time duration can be changed from the _Auto-close delay_ setting 166 | described below. 167 | - **Auto-close delay:** This is the time duration, in seconds, for which the app is kept idle before automatically 168 | closing. This setting is visible only when the _Close app if idle_ setting is enabled. 169 | - **Vibration alerts:** When this setting is enabled, the phone will give a short vibration pulse after each successful 170 | operation (ex: _Get Files_) as feedback to the user. 171 | 172 | #### Ports 173 | 174 | - **Port:** This is the port on which the server on your laptop listens for plaintext TCP connections. The default value 175 | for this port is `4337`. If a different port is assigned for the server according to the 176 | [server configuration](https://github.com/thevindu-w/clip_share_server#configuration), enter the same port here. 177 | - **Secure Port:** This is the port on which the server on your laptop listens for TLS-encrypted connections. The 178 | default value for this port is `4338`. If a different port is assigned for the server according to the 179 | [server configuration](https://github.com/thevindu-w/clip_share_server#configuration), enter the same port here. 180 | - **UDP Port:** This is the port on which the server on your laptop listens for UDP scanning requests. The default value 181 | for this port is `4337`. If a different port is assigned for the server according to the 182 | [server configuration](https://github.com/thevindu-w/clip_share_server#configuration), enter the same port here. 183 | 184 | #### Import/Export settings 185 | 186 | - **Import settings:** Use this to import settings from a _JSON_ file exported before. Note that the current settings 187 | will be discarded when importing settings from a file. 188 | - **Export settings:** Use this to export settings to a _JSON_ file that can be imported later. Settings can be exported 189 | to preserve settings after reinstalling the app or moving app settings to another device. -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /release 3 | /latest 4 | /legacy 5 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'com.github.sherter.google-java-format' version '0.9' 4 | } 5 | 6 | android { 7 | compileSdkVersion = 34 8 | buildToolsVersion = "34.0.0" 9 | namespace "com.tw.clipshare" 10 | flavorDimensions = ["default"] 11 | 12 | defaultConfig { 13 | applicationId = "com.tw.clipshare" 14 | minSdkVersion 24 15 | targetSdkVersion 34 16 | versionCode = 31401 17 | versionName = "3.14.1" 18 | resConfigs "en" 19 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 20 | } 21 | 22 | packagingOptions { 23 | dex { 24 | useLegacyPackaging true 25 | } 26 | } 27 | 28 | signingConfigs { 29 | release { 30 | storeFile file(RELEASE_STORE_FILE) 31 | storePassword RELEASE_STORE_PASSWORD 32 | keyAlias RELEASE_KEY_ALIAS 33 | keyPassword RELEASE_KEY_PASSWORD 34 | 35 | v2SigningEnabled true 36 | } 37 | } 38 | 39 | productFlavors { 40 | latest { 41 | minSdkVersion 28 42 | } 43 | legacy { 44 | minSdkVersion 24 45 | } 46 | } 47 | 48 | buildTypes { 49 | release { 50 | minifyEnabled = true 51 | shrinkResources = true 52 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 53 | signingConfig signingConfigs.release 54 | testCoverageEnabled = false 55 | } 56 | debug { 57 | minifyEnabled = false 58 | shrinkResources = false 59 | testCoverageEnabled = true 60 | } 61 | } 62 | 63 | compileOptions { 64 | sourceCompatibility JavaVersion.VERSION_17 65 | targetCompatibility JavaVersion.VERSION_17 66 | } 67 | } 68 | 69 | dependencies { 70 | implementation 'androidx.appcompat:appcompat:1.7.0' 71 | implementation 'com.google.android.material:material:1.12.0' 72 | implementation 'org.json:json:20250517' 73 | androidTestImplementation 'androidx.test.ext:junit:1.2.1' 74 | androidTestImplementation 'androidx.test:core:1.6.1' 75 | androidTestImplementation 'androidx.test:runner:1.6.2' 76 | androidTestImplementation 'androidx.test:rules:1.6.1' 77 | } 78 | 79 | googleJavaFormat { 80 | toolVersion = "1.25.2" 81 | exclude 'src/test' 82 | } 83 | -------------------------------------------------------------------------------- /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/.gitignore: -------------------------------------------------------------------------------- 1 | /test -------------------------------------------------------------------------------- /app/src/androidTest/java/com/tw/clipshare/UseAppContextTest.java: -------------------------------------------------------------------------------- 1 | package com.tw.clipshare; 2 | 3 | import android.content.Context; 4 | import androidx.test.ext.junit.runners.AndroidJUnit4; 5 | import androidx.test.platform.app.InstrumentationRegistry; 6 | import junit.framework.TestCase; 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | /** 11 | * Instrumented test, which will execute on an Android device. 12 | * 13 | * @see Testing documentation 14 | */ 15 | @RunWith(AndroidJUnit4.class) 16 | public class UseAppContextTest extends TestCase { 17 | @Test 18 | public void testUseAppContext() { 19 | // Context of the app under test. 20 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/tw/clipshare/UtilsTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2022-2025 H. Thevindu J. Wijesekera 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.tw.clipshare; 26 | 27 | import static org.junit.Assert.*; 28 | 29 | import androidx.test.ext.junit.runners.AndroidJUnit4; 30 | import org.junit.Test; 31 | import org.junit.runner.RunWith; 32 | 33 | @RunWith(AndroidJUnit4.class) 34 | public class UtilsTest { 35 | @Test 36 | public void isValidIPTest() { 37 | String[] validIPv4s = {"192.168.1.1", "0.0.0.0"}; 38 | for (String ip : validIPv4s) { 39 | assertTrue(Utils.isValidIP(ip)); 40 | } 41 | String[] validIPv6s = {"::", "fc00:abcd::123", "fe80::1", "::1"}; 42 | for (String ip : validIPv6s) { 43 | assertTrue(Utils.isValidIP(ip)); 44 | } 45 | String[] invalidIPv4s = {"192.168.1.1.1", "127.0.0.256"}; 46 | for (String ip : invalidIPv4s) { 47 | assertFalse(Utils.isValidIP(ip)); 48 | } 49 | String[] invalidIPv6s = {":::", "fc00", "fe80:", ":1", "fe80:abcg::1", " fe80::1"}; 50 | for (String ip : invalidIPv6s) { 51 | assertFalse(Utils.isValidIP(ip)); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/tw/clipshare/netConnection/MockConnection.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2022-2025 H. Thevindu J. Wijesekera 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.tw.clipshare.netConnection; 26 | 27 | import java.io.ByteArrayOutputStream; 28 | import java.io.InputStream; 29 | import java.io.OutputStream; 30 | 31 | public class MockConnection extends ServerConnection { 32 | public MockConnection(InputStream inputStream, OutputStream outputStream) { 33 | super(); 34 | this.inStream = inputStream; 35 | this.outStream = outputStream; 36 | } 37 | 38 | public MockConnection(InputStream inputStream) { 39 | this(inputStream, new ByteArrayOutputStream()); 40 | } 41 | 42 | public byte[] getOutputBytes() { 43 | ByteArrayOutputStream ostream = (ByteArrayOutputStream) this.outStream; 44 | return ostream.toByteArray(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/tw/clipshare/platformUtils/AndroidUtilsTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2022-2025 H. Thevindu J. Wijesekera 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.tw.clipshare.platformUtils; 26 | 27 | import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; 28 | import static org.junit.Assert.*; 29 | 30 | import android.content.Context; 31 | import androidx.test.core.app.ActivityScenario; 32 | import androidx.test.ext.junit.runners.AndroidJUnit4; 33 | import com.tw.clipshare.ClipShareActivity; 34 | import org.junit.Test; 35 | import org.junit.runner.RunWith; 36 | 37 | @RunWith(AndroidJUnit4.class) 38 | public class AndroidUtilsTest { 39 | @Test 40 | public void testClipboardMethods() { 41 | String text = "Sample text for clipboard test testClipboardMethods"; 42 | try { 43 | ActivityScenario.launch(ClipShareActivity.class) 44 | .onActivity( 45 | activity -> { 46 | Context appContext = activity.getApplicationContext(); 47 | AndroidUtils androidUtils = new AndroidUtils(appContext, activity); 48 | 49 | androidUtils.setClipboardText(text); 50 | 51 | String received = androidUtils.getClipboardText(); 52 | assertEquals(text, received); 53 | }) 54 | .close(); 55 | } catch (Exception ignored) { 56 | } 57 | } 58 | 59 | @Test 60 | public void testClipboardMethodsNoActivity() { 61 | try { 62 | Context appContext = getInstrumentation().getTargetContext(); 63 | AndroidUtils androidUtils = new AndroidUtils(appContext, null); 64 | 65 | String received = androidUtils.getClipboardText(); 66 | assertNull(received); 67 | } catch (Exception ignored) { 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/tw/clipshare/platformUtils/StatusNotifierTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2022-2025 H. Thevindu J. Wijesekera 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.tw.clipshare.platformUtils; 26 | 27 | import static org.junit.Assert.assertEquals; 28 | import static org.junit.Assert.assertNotNull; 29 | 30 | import android.app.NotificationManager; 31 | import android.content.Context; 32 | import androidx.core.app.NotificationCompat; 33 | import androidx.test.ext.junit.runners.AndroidJUnit4; 34 | import androidx.test.platform.app.InstrumentationRegistry; 35 | import com.tw.clipshare.FileService; 36 | import java.util.Random; 37 | import org.junit.Before; 38 | import org.junit.BeforeClass; 39 | import org.junit.Test; 40 | import org.junit.runner.RunWith; 41 | 42 | @RunWith(AndroidJUnit4.class) 43 | public class StatusNotifierTest { 44 | private static Context context; 45 | private static long[] curSizes; 46 | private static long[] curTimes; 47 | private static long[] speeds; 48 | private static final String SEC = TimeContainer.SECOND; 49 | private static final String MIN = TimeContainer.MINUTE; 50 | private static final String HOUR = TimeContainer.HOUR; 51 | private static final String DAY = TimeContainer.DAY; 52 | private StatusNotifier statusNotifier; 53 | 54 | @BeforeClass 55 | public static void initializeClass() { 56 | context = InstrumentationRegistry.getInstrumentation().getTargetContext(); 57 | assertNotNull(context); 58 | 59 | curSizes = 60 | new long[] { 61 | 0, 62 | 100000, 63 | 200000, 64 | 300000, 65 | 500000, 66 | 1000000, 67 | 1500000, 68 | 1700000, 69 | 2000000, 70 | 2100000, 71 | 2500000, 72 | 2600000, 73 | 3100000, 74 | 3300000, 75 | 4000000, 76 | 4500000, 77 | 4600000, 78 | 4700000, 79 | 4800000, 80 | 150000000000L, 81 | 268470500000L, 82 | 273411700000L, 83 | 279670500000L, 84 | 279885620210L, 85 | 279983500000L, 86 | 279996800000L, 87 | 279998500000L, 88 | 280000000000L 89 | }; 90 | curTimes = 91 | new long[] { 92 | 1000, 1120, 1280, 1440, 1600, 1880, 2040, 2200, 2240, 2320, 2520, 2600, 2880, 3000, 3440, 93 | 3800, 3920, 4160, 4200, 100000000, 163000000, 166000000, 169800000, 169930000, 169990000, 94 | 169998000, 169999000, 170000000 95 | }; 96 | speeds = 97 | new long[] { 98 | -1, -1, -1, 681818, 681818, 909090, 909090, 909090, 909090, 1306817, 1306817, 1306817, 99 | 1426541, 1426541, 1471691, 1471691, 1416268, 1416268, 1416268, 1437204, 1548024, 1572784, 100 | 1591351, 1607205, 1613236, 1625552, 1644164, 1608123 101 | }; 102 | } 103 | 104 | @Before 105 | public void initializeEach() { 106 | NotificationManager notificationManager = 107 | (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 108 | assertNotNull(notificationManager); 109 | NotificationCompat.Builder builder = 110 | new NotificationCompat.Builder(context, FileService.CHANNEL_ID); 111 | assertNotNull(builder); 112 | Random rnd = new Random(); 113 | int notificationId = Math.abs(rnd.nextInt(Integer.MAX_VALUE - 1)) + 1; 114 | this.statusNotifier = new StatusNotifier(notificationManager, builder, notificationId); 115 | statusNotifier.setFileSize(curSizes[curSizes.length - 1]); 116 | } 117 | 118 | @Test 119 | public void getSpeedTest() { 120 | for (int i = 0; i < speeds.length; i++) { 121 | long speed = statusNotifier.getSpeed(curSizes[i], curTimes[i]); 122 | assertEquals(speeds[i], speed); 123 | } 124 | } 125 | 126 | @Test 127 | public void getRemainingTimeTest() { 128 | int[] times = { 129 | -1, -1, -1, 5, 5, 4, 4, 4, 4, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 1, 3, 1, 10, 1, 0, 0 130 | }; 131 | String[] units = { 132 | SEC, SEC, SEC, DAY, DAY, DAY, DAY, DAY, DAY, DAY, DAY, DAY, DAY, DAY, DAY, DAY, DAY, DAY, DAY, 133 | DAY, HOUR, HOUR, MIN, MIN, SEC, SEC, SEC, SEC 134 | }; 135 | for (int i = 0; i < speeds.length; i++) { 136 | long speed = statusNotifier.getSpeed(curSizes[i], curTimes[i]); 137 | TimeContainer time = statusNotifier.getRemainingTime(curSizes[i], speed); 138 | assertEquals(times[i], time.time); 139 | assertEquals(units[i], time.unit); 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/tw/clipshare/platformUtils/TimeContainerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2022-2025 H. Thevindu J. Wijesekera 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.tw.clipshare.platformUtils; 26 | 27 | import static org.junit.Assert.*; 28 | 29 | import androidx.test.ext.junit.runners.AndroidJUnit4; 30 | import org.junit.Test; 31 | import org.junit.runner.RunWith; 32 | 33 | @RunWith(AndroidJUnit4.class) 34 | public class TimeContainerTest { 35 | @Test 36 | public void toStringTest() { 37 | TimeContainer time; 38 | time = TimeContainer.initBySeconds(200000); 39 | assertEquals("2 days", time.toString()); 40 | time = TimeContainer.initBySeconds(100000); 41 | assertEquals("1 day", time.toString()); 42 | time = TimeContainer.initBySeconds(20000); 43 | assertEquals("6 hours", time.toString()); 44 | time = TimeContainer.initBySeconds(4000); 45 | assertEquals("1 hour", time.toString()); 46 | time = TimeContainer.initBySeconds(400); 47 | assertEquals("7 mins", time.toString()); 48 | time = TimeContainer.initBySeconds(80); 49 | assertEquals("1 min", time.toString()); 50 | time = TimeContainer.initBySeconds(10); 51 | assertEquals("10 secs", time.toString()); 52 | time = TimeContainer.initBySeconds(1); 53 | assertEquals("1 sec", time.toString()); 54 | time = TimeContainer.initBySeconds(0); 55 | assertEquals("0 secs", time.toString()); 56 | } 57 | 58 | @Test 59 | public void equalsTest() { 60 | assertEquals(TimeContainer.initBySeconds(-1), TimeContainer.initBySeconds(-1)); 61 | assertEquals(TimeContainer.initBySeconds(9999999999999L), TimeContainer.initBySeconds(-1)); 62 | assertNotEquals(TimeContainer.initBySeconds(0), TimeContainer.initBySeconds(-1)); 63 | assertEquals(TimeContainer.initBySeconds(0), TimeContainer.initBySeconds(0)); 64 | assertNotEquals(TimeContainer.initBySeconds(0), TimeContainer.initBySeconds(1)); 65 | assertEquals(TimeContainer.initBySeconds(1), TimeContainer.initBySeconds(1)); 66 | assertEquals(TimeContainer.initBySeconds(2), TimeContainer.initBySeconds(2)); 67 | assertNotEquals(TimeContainer.initBySeconds(60), TimeContainer.initBySeconds(59)); 68 | assertEquals(TimeContainer.initBySeconds(60), TimeContainer.initBySeconds(60)); 69 | assertEquals(TimeContainer.initBySeconds(60), TimeContainer.initBySeconds(61)); 70 | assertEquals(TimeContainer.initBySeconds(60), TimeContainer.initBySeconds(89)); 71 | assertNotEquals(TimeContainer.initBySeconds(60), TimeContainer.initBySeconds(90)); 72 | assertEquals(TimeContainer.initBySeconds(120), TimeContainer.initBySeconds(90)); 73 | assertEquals(TimeContainer.initBySeconds(120), TimeContainer.initBySeconds(149)); 74 | assertEquals(TimeContainer.initBySeconds(3570), TimeContainer.initBySeconds(3599)); 75 | assertNotEquals(TimeContainer.initBySeconds(3600), TimeContainer.initBySeconds(3599)); 76 | assertEquals(TimeContainer.initBySeconds(3600), TimeContainer.initBySeconds(3600)); 77 | assertEquals(TimeContainer.initBySeconds(3600), TimeContainer.initBySeconds(4000)); 78 | assertNotEquals(TimeContainer.initBySeconds(3600), TimeContainer.initBySeconds(6000)); 79 | assertNotEquals(TimeContainer.initBySeconds(86400), TimeContainer.initBySeconds(86399)); 80 | assertEquals(TimeContainer.initBySeconds(86400), TimeContainer.initBySeconds(86400)); 81 | assertEquals(TimeContainer.initBySeconds(86400), TimeContainer.initBySeconds(100000)); 82 | assertNotEquals(TimeContainer.initBySeconds(86400), TimeContainer.initBySeconds(200000)); 83 | assertEquals(TimeContainer.initBySeconds(172800), TimeContainer.initBySeconds(200000)); 84 | assertNotEquals(TimeContainer.initBySeconds(-1), null); 85 | assertNotEquals(TimeContainer.initBySeconds(1), TimeContainer.initBySeconds(60)); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/tw/clipshare/proto/BAOStreamBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2022-2025 H. Thevindu J. Wijesekera 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.tw.clipshare.proto; 26 | 27 | import java.io.ByteArrayInputStream; 28 | import java.io.ByteArrayOutputStream; 29 | import java.io.IOException; 30 | import java.nio.charset.StandardCharsets; 31 | 32 | public class BAOStreamBuilder { 33 | private final ByteArrayOutputStream baoStream; 34 | 35 | BAOStreamBuilder() { 36 | this.baoStream = new ByteArrayOutputStream(); 37 | } 38 | 39 | public ByteArrayInputStream getStream() { 40 | return new ByteArrayInputStream(this.baoStream.toByteArray()); 41 | } 42 | 43 | public byte[] getArray() { 44 | return this.baoStream.toByteArray(); 45 | } 46 | 47 | public void addByte(int b) { 48 | this.baoStream.write(b); 49 | } 50 | 51 | public void addBytes(byte[] array) throws IOException { 52 | this.baoStream.write(array); 53 | } 54 | 55 | public void addSize(long size) throws IOException { 56 | byte[] data = new byte[8]; 57 | for (int i = data.length - 1; i >= 0; i--) { 58 | data[i] = (byte) (size & 0xFF); 59 | size >>= 8; 60 | } 61 | this.baoStream.write(data); 62 | } 63 | 64 | public void addString(String str) throws IOException { 65 | byte[] bytes = str.getBytes(StandardCharsets.UTF_8); 66 | addData(bytes); 67 | } 68 | 69 | public void addData(byte[] data) throws IOException { 70 | addSize(data.length); 71 | this.baoStream.write(data); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/tw/clipshare/proto/ProtocolSelectorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2022-2025 H. Thevindu J. Wijesekera 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.tw.clipshare.proto; 26 | 27 | import static com.tw.clipshare.Utils.PROTOCOL_OBSOLETE; 28 | import static com.tw.clipshare.Utils.PROTOCOL_SUPPORTED; 29 | import static com.tw.clipshare.Utils.PROTOCOL_UNKNOWN; 30 | import static org.junit.Assert.*; 31 | 32 | import androidx.test.ext.junit.runners.AndroidJUnit4; 33 | import com.tw.clipshare.netConnection.MockConnection; 34 | import com.tw.clipshare.protocol.*; 35 | import com.tw.clipshare.protocol.ProtocolSelector; 36 | import java.io.ByteArrayInputStream; 37 | import java.io.IOException; 38 | import java.net.ProtocolException; 39 | import org.junit.Test; 40 | import org.junit.runner.RunWith; 41 | 42 | @RunWith(AndroidJUnit4.class) 43 | public class ProtocolSelectorTest { 44 | static final byte PROTOCOL_REJECT = 0; 45 | static final byte MAX_PROTO = ProtocolSelector.PROTO_MAX; 46 | 47 | @SuppressWarnings("ConstantConditions") 48 | @Test 49 | public void testNullConnection() throws IOException { 50 | Proto proto = ProtocolSelector.getProto(null, null, null); 51 | assertNull(proto); 52 | } 53 | 54 | @SuppressWarnings("ConstantConditions") 55 | @Test 56 | public void testProtoOk() throws IOException { 57 | BAOStreamBuilder builder = new BAOStreamBuilder(); 58 | builder.addByte(PROTOCOL_SUPPORTED); 59 | ByteArrayInputStream istream = builder.getStream(); 60 | MockConnection connection = new MockConnection(istream); 61 | Proto proto = ProtocolSelector.getProto(connection, null, null); 62 | Class protoClass; 63 | switch (MAX_PROTO) { 64 | case 1: 65 | { 66 | protoClass = ProtoV1.class; 67 | break; 68 | } 69 | case 2: 70 | { 71 | protoClass = ProtoV2.class; 72 | break; 73 | } 74 | case 3: 75 | { 76 | protoClass = ProtoV3.class; 77 | break; 78 | } 79 | default: 80 | { 81 | throw new ProtocolException("Unknown protocol version"); 82 | } 83 | } 84 | assertTrue(protoClass.isInstance(proto)); 85 | byte[] received = connection.getOutputBytes(); 86 | assertArrayEquals(new byte[] {MAX_PROTO}, received); 87 | proto.close(); 88 | } 89 | 90 | @Test 91 | public void testProtoObsolete() { 92 | BAOStreamBuilder builder = new BAOStreamBuilder(); 93 | builder.addByte(PROTOCOL_OBSOLETE); 94 | ByteArrayInputStream istream = builder.getStream(); 95 | MockConnection connection = new MockConnection(istream); 96 | assertThrows(ProtocolException.class, () -> ProtocolSelector.getProto(connection, null, null)); 97 | byte[] received = connection.getOutputBytes(); 98 | assertArrayEquals(new byte[] {MAX_PROTO}, received); 99 | } 100 | 101 | @SuppressWarnings("ConstantConditions") 102 | @Test 103 | public void testProtoNegotiateV1() throws ProtocolException { 104 | if (MAX_PROTO <= 1) return; 105 | BAOStreamBuilder builder = new BAOStreamBuilder(); 106 | builder.addByte(PROTOCOL_UNKNOWN); 107 | builder.addByte(1); 108 | ByteArrayInputStream istream = builder.getStream(); 109 | MockConnection connection = new MockConnection(istream); 110 | Proto proto = ProtocolSelector.getProto(connection, null, null); 111 | byte[] received = connection.getOutputBytes(); 112 | assertArrayEquals(new byte[] {MAX_PROTO, 1}, received); 113 | proto.close(); 114 | } 115 | 116 | @SuppressWarnings("ConstantConditions") 117 | @Test 118 | public void testProtoNegotiateV2() throws ProtocolException { 119 | if (MAX_PROTO <= 2) return; 120 | BAOStreamBuilder builder = new BAOStreamBuilder(); 121 | builder.addByte(PROTOCOL_UNKNOWN); 122 | builder.addByte(2); 123 | ByteArrayInputStream istream = builder.getStream(); 124 | MockConnection connection = new MockConnection(istream); 125 | Proto proto = ProtocolSelector.getProto(connection, null, null); 126 | byte[] received = connection.getOutputBytes(); 127 | assertArrayEquals(new byte[] {MAX_PROTO, 2}, received); 128 | proto.close(); 129 | } 130 | 131 | @Test 132 | public void testProtoNegotiateFail() { 133 | BAOStreamBuilder builder = new BAOStreamBuilder(); 134 | builder.addByte(PROTOCOL_UNKNOWN); 135 | builder.addByte(PROTOCOL_REJECT); 136 | ByteArrayInputStream istream = builder.getStream(); 137 | MockConnection connection = new MockConnection(istream); 138 | assertThrows(ProtocolException.class, () -> ProtocolSelector.getProto(connection, null, null)); 139 | byte[] received = connection.getOutputBytes(); 140 | assertArrayEquals(new byte[] {MAX_PROTO, 0}, received); 141 | } 142 | 143 | @Test 144 | public void testInvalidStatus() throws ProtocolException { 145 | BAOStreamBuilder builder = new BAOStreamBuilder(); 146 | builder.addByte(4); // 4 is invalid 147 | ByteArrayInputStream istream = builder.getStream(); 148 | MockConnection connection = new MockConnection(istream); 149 | Proto proto = ProtocolSelector.getProto(connection, null, null); 150 | assertNull(proto); 151 | } 152 | 153 | @Test 154 | public void testReceiveFail1() throws ProtocolException { 155 | BAOStreamBuilder builder = new BAOStreamBuilder(); 156 | ByteArrayInputStream istream = builder.getStream(); 157 | MockConnection connection = new MockConnection(istream); 158 | Proto proto = ProtocolSelector.getProto(connection, null, null); 159 | assertNull(proto); 160 | } 161 | 162 | @Test 163 | public void testReceiveFail2() throws ProtocolException { 164 | BAOStreamBuilder builder = new BAOStreamBuilder(); 165 | builder.addByte(PROTOCOL_UNKNOWN); 166 | ByteArrayInputStream istream = builder.getStream(); 167 | MockConnection connection = new MockConnection(istream); 168 | Proto proto = ProtocolSelector.getProto(connection, null, null); 169 | assertNull(proto); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 13 | 14 | 15 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 45 | 46 | 50 | 51 | 56 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thevindu-w/clip_share_client/7c1e4a8dd112c295d5bb98eb2c4aaf7c0ab58e4a/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/tw/clipshare/CertUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2022-2025 H. Thevindu J. Wijesekera 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.tw.clipshare; 26 | 27 | import java.io.InputStream; 28 | import java.security.KeyStore; 29 | import java.security.cert.CertificateFactory; 30 | import java.security.cert.X509Certificate; 31 | import java.util.Enumeration; 32 | import javax.net.ssl.KeyManagerFactory; 33 | 34 | public class CertUtils { 35 | public static String getCertCN(X509Certificate cert) { 36 | try { 37 | String name = cert.getSubjectX500Principal().getName("RFC1779"); 38 | String[] attributes = name.split(","); 39 | String cn = null; 40 | for (String attribute : attributes) { 41 | if (!attribute.startsWith("CN=")) { 42 | continue; 43 | } 44 | String[] cnSep = attribute.split("=", 2); 45 | cn = cnSep[1]; 46 | break; 47 | } 48 | return cn; 49 | } catch (Exception ignored) { 50 | return null; 51 | } 52 | } 53 | 54 | public static String getCertCN(char[] passwd, InputStream certIn) { 55 | try { 56 | KeyStore keyStore = KeyStore.getInstance("PKCS12"); 57 | keyStore.load(certIn, passwd); 58 | KeyManagerFactory kmf = 59 | KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); 60 | kmf.init(keyStore, passwd); 61 | Enumeration enm = keyStore.aliases(); 62 | if (enm.hasMoreElements()) { 63 | String alias = enm.nextElement(); 64 | X509Certificate cert = (X509Certificate) keyStore.getCertificate(alias); 65 | return getCertCN(cert); 66 | } 67 | return null; 68 | } catch (Exception ignored) { 69 | return null; 70 | } 71 | } 72 | 73 | public static X509Certificate getX509fromInputStream(InputStream caCertIn) { 74 | try { 75 | CertificateFactory cf = CertificateFactory.getInstance("X.509"); 76 | return (X509Certificate) cf.generateCertificate(caCertIn); 77 | } catch (Exception ignored) { 78 | return null; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/src/main/java/com/tw/clipshare/FileService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2022-2025 H. Thevindu J. Wijesekera 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.tw.clipshare; 26 | 27 | import android.app.*; 28 | import android.content.BroadcastReceiver; 29 | import android.content.Context; 30 | import android.content.Intent; 31 | import android.os.IBinder; 32 | import android.widget.Toast; 33 | import androidx.annotation.Nullable; 34 | import androidx.core.app.NotificationCompat; 35 | import com.tw.clipshare.platformUtils.AndroidUtils; 36 | import com.tw.clipshare.platformUtils.DataContainer; 37 | import com.tw.clipshare.platformUtils.StatusNotifier; 38 | import com.tw.clipshare.protocol.Proto; 39 | import java.util.HashMap; 40 | import java.util.LinkedList; 41 | import java.util.Random; 42 | import java.util.concurrent.ExecutorService; 43 | import java.util.concurrent.Executors; 44 | import java.util.concurrent.TimeUnit; 45 | 46 | public class FileService extends Service { 47 | public static final String CHANNEL_ID = "notification_channel"; 48 | private static LinkedList pendingTasks = null; 49 | private ExecutorService executorService; 50 | private StatusNotifier statusNotifier; 51 | private static final Object LOCK = new Object(); 52 | private static DataContainer data; 53 | 54 | static DataContainer getNextMessage() throws InterruptedException { 55 | DataContainer current; 56 | try { 57 | synchronized (LOCK) { 58 | LOCK.wait(2000); 59 | current = data; 60 | } 61 | } finally { 62 | synchronized (LOCK) { 63 | data = null; 64 | } 65 | } 66 | return current; 67 | } 68 | 69 | static void setMessage(DataContainer dataContainer, String msg) { 70 | synchronized (LOCK) { 71 | data = dataContainer != null ? dataContainer : new DataContainer(); 72 | data.setMessage(msg); 73 | LOCK.notifyAll(); 74 | } 75 | } 76 | 77 | @Nullable 78 | @Override 79 | public IBinder onBind(Intent intent) { 80 | return null; 81 | } 82 | 83 | private static final class RunningTasksHolder { 84 | static final HashMap runningTasks = new HashMap<>(1); 85 | } 86 | 87 | public static boolean isStopped() { 88 | return RunningTasksHolder.runningTasks.isEmpty(); 89 | } 90 | 91 | @Override 92 | public int onStartCommand(Intent intent, int flags, int startId) { 93 | if (FileService.pendingTasks == null || FileService.pendingTasks.isEmpty()) { 94 | endService(); 95 | return START_NOT_STICKY; 96 | } 97 | int id = createStatusNotifier(); 98 | try { 99 | startForeground(statusNotifier.getId(), statusNotifier.getNotification()); 100 | } catch (Exception ignored) { 101 | } 102 | 103 | LinkedList pendingTasksInstance; 104 | // noinspection SynchronizeOnNonFinalField 105 | synchronized (FileService.pendingTasks) { 106 | pendingTasksInstance = new LinkedList<>(pendingTasks); 107 | FileService.pendingTasks.clear(); 108 | } 109 | FileShareRunnable runnable = new FileShareRunnable(pendingTasksInstance, id); 110 | synchronized (RunningTasksHolder.runningTasks) { 111 | RunningTasksHolder.runningTasks.put(id, runnable); 112 | } 113 | executorService = Executors.newSingleThreadExecutor(); 114 | executorService.submit(runnable); 115 | 116 | // Stop service when executorService completes 117 | (new Thread( 118 | () -> { 119 | try { 120 | executorService.shutdown(); 121 | while (true) { 122 | try { 123 | if (executorService.awaitTermination(1, TimeUnit.HOURS)) break; 124 | } catch (Exception ignored) { 125 | } 126 | } 127 | endService(); 128 | } catch (Exception ignored) { 129 | } 130 | })) 131 | .start(); 132 | 133 | return START_REDELIVER_INTENT; 134 | } 135 | 136 | public static void addPendingTask(PendingTask pendingTask) { 137 | synchronized (FileService.class) { 138 | if (FileService.pendingTasks == null) FileService.pendingTasks = new LinkedList<>(); 139 | } 140 | //noinspection SynchronizeOnNonFinalField 141 | synchronized (FileService.pendingTasks) { 142 | FileService.pendingTasks.add(pendingTask); 143 | } 144 | } 145 | 146 | private void endService() { 147 | try { 148 | if (executorService != null) { 149 | executorService.shutdownNow(); 150 | executorService = null; 151 | } 152 | if (FileService.pendingTasks != null) { 153 | // noinspection SynchronizeOnNonFinalField 154 | synchronized (FileService.pendingTasks) { 155 | FileService.pendingTasks.clear(); 156 | } 157 | } 158 | if (this.statusNotifier != null) this.statusNotifier.finish(); 159 | } catch (Exception ignored) { 160 | } 161 | stopForeground(STOP_FOREGROUND_REMOVE); 162 | stopSelf(); 163 | } 164 | 165 | private int createStatusNotifier() { 166 | Intent notificationIntent = new Intent(this, FileService.class); 167 | PendingIntent pendingIntent = 168 | PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE); 169 | Context context = getApplicationContext(); 170 | NotificationManager notificationManager = 171 | (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE); 172 | 173 | Random rnd = new Random(); 174 | int notificationId = Math.abs(rnd.nextInt(Integer.MAX_VALUE - 1)) + 1; 175 | if (RunningTasksHolder.runningTasks.containsKey(notificationId)) 176 | notificationId = Math.abs(rnd.nextInt(Integer.MAX_VALUE - 1)) + 1; 177 | Intent intent = new Intent(context, StopEventReceiver.class); 178 | intent.putExtra("TaskID", notificationId); 179 | PendingIntent pendingIntentStop = 180 | PendingIntent.getBroadcast(context, notificationId, intent, PendingIntent.FLAG_IMMUTABLE); 181 | 182 | NotificationCompat.Builder builder = 183 | new NotificationCompat.Builder(context, FileService.CHANNEL_ID) 184 | .setContentIntent(pendingIntent) 185 | .addAction(0, "Stop", pendingIntentStop); 186 | this.statusNotifier = new StatusNotifier(notificationManager, builder, notificationId); 187 | return notificationId; 188 | } 189 | 190 | private class FileShareRunnable implements Runnable { 191 | private final LinkedList pendingTasks; 192 | private final int id; 193 | private Proto proto; 194 | 195 | FileShareRunnable(LinkedList pendingTasks, int id) { 196 | this.pendingTasks = pendingTasks; 197 | this.proto = null; 198 | this.id = id; 199 | } 200 | 201 | @Override 202 | public void run() { 203 | try { 204 | PendingTask pendingTask; 205 | while (!this.pendingTasks.isEmpty()) { 206 | pendingTask = this.pendingTasks.pop(); 207 | 208 | proto = pendingTask.proto(); 209 | AndroidUtils utils = pendingTask.utils(); 210 | try { 211 | proto.setStatusNotifier(statusNotifier); 212 | statusNotifier.reset(); 213 | boolean success = false; 214 | switch (pendingTask.task()) { 215 | case PendingTask.GET_FILES: 216 | { 217 | statusNotifier.setTitle("Getting file"); 218 | statusNotifier.setIcon(R.drawable.ic_download_icon); 219 | if (proto.getFile()) success = true; 220 | else if (!proto.isStopped()) utils.showToast("Failed getting files"); 221 | break; 222 | } 223 | case PendingTask.SEND_FILES: 224 | { 225 | statusNotifier.setTitle("Sending file"); 226 | statusNotifier.setIcon(R.drawable.ic_upload_icon); 227 | if (proto.sendFile()) { 228 | success = true; 229 | utils.showToast("Sent all files"); 230 | } else if (!proto.isStopped()) utils.showToast("Failed sending files"); 231 | break; 232 | } 233 | } 234 | if (proto.isStopped()) { 235 | setMessage( 236 | null, 237 | (pendingTask.task() == PendingTask.GET_FILES ? "Getting" : "Sending") 238 | + " files stopped"); 239 | break; 240 | } 241 | utils.vibrate(); 242 | setMessage( 243 | proto.dataContainer, 244 | (pendingTask.task() == PendingTask.GET_FILES ? "Getting" : "Sending") 245 | + " files " 246 | + (success ? "completed" : "failed")); 247 | } catch (Exception ignored) { 248 | } finally { 249 | proto.close(); 250 | } 251 | } 252 | } catch (Exception ignored) { 253 | } finally { 254 | synchronized (RunningTasksHolder.runningTasks) { 255 | RunningTasksHolder.runningTasks.remove(this.id); 256 | } 257 | } 258 | } 259 | 260 | void requestStop() { 261 | proto.requestStop(); 262 | } 263 | } 264 | 265 | public static class StopEventReceiver extends BroadcastReceiver { 266 | @Override 267 | public void onReceive(Context context, Intent intent) { 268 | try { 269 | int id = intent.getIntExtra("TaskID", -1); 270 | if (id == -1) return; 271 | FileShareRunnable runnable; 272 | synchronized (RunningTasksHolder.runningTasks) { 273 | runnable = RunningTasksHolder.runningTasks.get(id); 274 | } 275 | if (runnable == null) return; 276 | runnable.requestStop(); 277 | Toast.makeText(context, "Cancelled", Toast.LENGTH_SHORT).show(); 278 | } catch (Exception ignored) { 279 | } 280 | } 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /app/src/main/java/com/tw/clipshare/PendingFile.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2022-2025 H. Thevindu J. Wijesekera 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.tw.clipshare; 26 | 27 | import android.net.Uri; 28 | 29 | public record PendingFile(Uri uri, String name, long size) {} 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/tw/clipshare/PendingTask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2022-2025 H. Thevindu J. Wijesekera 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.tw.clipshare; 26 | 27 | import com.tw.clipshare.platformUtils.AndroidUtils; 28 | import com.tw.clipshare.protocol.Proto; 29 | 30 | public record PendingTask(Proto proto, AndroidUtils utils, int task) { 31 | public static final int GET_FILES = 3; 32 | public static final int SEND_FILES = 4; 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/tw/clipshare/ServerFinder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2022-2025 H. Thevindu J. Wijesekera 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.tw.clipshare; 26 | 27 | import java.io.IOException; 28 | import java.net.*; 29 | import java.nio.charset.StandardCharsets; 30 | import java.util.*; 31 | import java.util.concurrent.ExecutorService; 32 | import java.util.concurrent.Executors; 33 | import java.util.concurrent.TimeUnit; 34 | 35 | class ServerFinder implements Runnable { 36 | 37 | private static final byte[] SCAN_MSG = "in".getBytes(StandardCharsets.UTF_8); 38 | private static final HashMap serverAddresses = new HashMap<>(2); 39 | private static final Set myAddresses = new HashSet<>(2); 40 | private static ExecutorService executorStatic; 41 | private static InetAddress multicastGroup; 42 | private final NetworkInterface netIF; 43 | private final Thread parent; 44 | private final int port; 45 | private final int portUDP; 46 | 47 | private ServerFinder(NetworkInterface netIF, int port, int portUDP, Thread parent) { 48 | this.netIF = netIF; 49 | this.parent = parent; 50 | this.port = port; 51 | this.portUDP = portUDP; 52 | } 53 | 54 | public static List find(int port, int portUDP) { 55 | try { 56 | synchronized (serverAddresses) { 57 | serverAddresses.clear(); 58 | myAddresses.clear(); 59 | } 60 | if (executorStatic != null) executorStatic.shutdownNow(); 61 | if (multicastGroup == null) multicastGroup = Inet6Address.getByName("ff05::4567"); 62 | Enumeration netIFEnum = NetworkInterface.getNetworkInterfaces(); 63 | Object[] netIFList = Collections.list(netIFEnum).toArray(); 64 | executorStatic = Executors.newFixedThreadPool(netIFList.length); 65 | ExecutorService executor = executorStatic; 66 | Thread curThread = Thread.currentThread(); 67 | for (Object netIFList1 : netIFList) { 68 | NetworkInterface ni = (NetworkInterface) netIFList1; 69 | Runnable task = new ServerFinder(ni, port, portUDP, curThread); 70 | executor.submit(task); 71 | } 72 | while (!executor.isTerminated()) { 73 | if (!serverAddresses.isEmpty()) { 74 | executor.shutdownNow(); 75 | break; 76 | } 77 | try { 78 | //noinspection ResultOfMethodCallIgnored 79 | executor.awaitTermination(600, TimeUnit.MILLISECONDS); 80 | } catch (InterruptedException ignored) { 81 | break; 82 | } 83 | executor.shutdown(); 84 | } 85 | executor.shutdownNow(); 86 | } catch (IOException | RuntimeException ignored) { 87 | if (executorStatic != null) executorStatic.shutdownNow(); 88 | } 89 | List addresses; 90 | synchronized (serverAddresses) { 91 | addresses = new ArrayList<>(serverAddresses.size()); 92 | for (InetAddress address : serverAddresses.values()) { 93 | if (address instanceof Inet4Address) { 94 | addresses.add(address); 95 | continue; 96 | } 97 | boolean isOther = true; 98 | for (InetAddress myAddress : myAddresses) { 99 | if (Arrays.equals(myAddress.getAddress(), address.getAddress())) { 100 | isOther = false; 101 | break; 102 | } 103 | } 104 | if (isOther) addresses.add(address); 105 | } 106 | serverAddresses.clear(); 107 | myAddresses.clear(); 108 | } 109 | //noinspection ResultOfMethodCallIgnored 110 | Thread.interrupted(); 111 | return addresses; 112 | } 113 | 114 | private void scanBroadcast(Inet4Address broadcastAddress, Inet4Address myAddress) { 115 | new Thread( 116 | () -> { 117 | try { 118 | DatagramSocket socket = new DatagramSocket(); 119 | DatagramPacket pkt = 120 | new DatagramPacket(SCAN_MSG, SCAN_MSG.length, broadcastAddress, portUDP); 121 | socket.send(pkt); 122 | byte[] buf = new byte[256]; 123 | pkt = new DatagramPacket(buf, buf.length); 124 | int timeout = 1000; 125 | while (true) { 126 | socket.setSoTimeout(timeout); 127 | timeout = 250; 128 | try { 129 | socket.receive(pkt); 130 | } catch (SocketTimeoutException ignored) { 131 | break; 132 | } 133 | InetAddress serverAddress = pkt.getAddress(); 134 | if (myAddress.equals(serverAddress)) continue; 135 | String received = new String(pkt.getData()).replace("\0", ""); 136 | if ("clip_share".equals(received)) { 137 | String addressStr = serverAddress.getHostAddress(); 138 | if (addressStr != null) { 139 | addressStr = addressStr.intern(); 140 | synchronized (serverAddresses) { 141 | serverAddresses.put(addressStr, serverAddress); 142 | } 143 | } 144 | } 145 | } 146 | if (!serverAddresses.isEmpty()) parent.interrupt(); 147 | socket.close(); 148 | } catch (IOException | RuntimeException ignored) { 149 | } 150 | }) 151 | .start(); 152 | } 153 | 154 | private void scanMulticast(Inet6Address ifAddress) { 155 | new Thread( 156 | () -> { 157 | try { 158 | MulticastSocket socket = new MulticastSocket(); 159 | socket.setInterface(ifAddress); 160 | socket.setTimeToLive(4); 161 | DatagramPacket pkt = 162 | new DatagramPacket(SCAN_MSG, SCAN_MSG.length, multicastGroup, portUDP); 163 | socket.send(pkt); 164 | byte[] buf = new byte[256]; 165 | pkt = new DatagramPacket(buf, buf.length); 166 | int timeout = 1000; 167 | while (true) { 168 | socket.setSoTimeout(timeout); 169 | timeout = 250; 170 | try { 171 | socket.receive(pkt); 172 | } catch (SocketTimeoutException ignored) { 173 | break; 174 | } 175 | InetAddress serverAddress = pkt.getAddress(); 176 | if (ifAddress.equals(serverAddress)) continue; 177 | String received = new String(pkt.getData()).replace("\0", ""); 178 | if ("clip_share".equals(received)) { 179 | String addressStr = serverAddress.getHostAddress(); 180 | if (addressStr != null) { 181 | addressStr = addressStr.intern(); 182 | synchronized (serverAddresses) { 183 | serverAddresses.put(addressStr, serverAddress); 184 | } 185 | } 186 | } 187 | } 188 | if (!serverAddresses.isEmpty()) parent.interrupt(); 189 | socket.close(); 190 | } catch (IOException | RuntimeException ignored) { 191 | } 192 | }) 193 | .start(); 194 | } 195 | 196 | @Override 197 | public void run() { 198 | try { 199 | if (netIF == null || netIF.isLoopback() || !netIF.isUp() || netIF.isVirtual()) { 200 | return; 201 | } 202 | List addresses = netIF.getInterfaceAddresses(); 203 | for (InterfaceAddress intAddress : addresses) { 204 | InetAddress address = intAddress.getAddress(); 205 | if (address instanceof Inet6Address && Settings.getInstance().getScanIPv6()) { 206 | myAddresses.add(address); 207 | } 208 | } 209 | for (InterfaceAddress intAddress : addresses) { 210 | try { 211 | InetAddress address = intAddress.getAddress(); 212 | if (address instanceof Inet4Address) { 213 | InetAddress broadcastAddress = intAddress.getBroadcast(); 214 | if (broadcastAddress instanceof Inet4Address) { 215 | scanBroadcast((Inet4Address) broadcastAddress, (Inet4Address) address); 216 | } 217 | short subLen = intAddress.getNetworkPrefixLength(); 218 | if (subLen <= 22) subLen = 23; 219 | SubnetScanner subnetScanner = new SubnetScanner(address, port, subLen); 220 | InetAddress server = subnetScanner.scan(subLen >= 24 ? 32 : 64); 221 | if (server != null) { 222 | String addressStr = server.getHostAddress(); 223 | if (addressStr != null) { 224 | addressStr = addressStr.intern(); 225 | synchronized (serverAddresses) { 226 | serverAddresses.put(addressStr, server); 227 | } 228 | } 229 | break; 230 | } 231 | } else if (address instanceof Inet6Address && Settings.getInstance().getScanIPv6()) { 232 | scanMulticast((Inet6Address) address); 233 | } 234 | } catch (RuntimeException ignored) { 235 | } 236 | } 237 | } catch (Exception ignored) { 238 | } 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /app/src/main/java/com/tw/clipshare/SubnetScanner.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2022-2025 H. Thevindu J. Wijesekera 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.tw.clipshare; 26 | 27 | import com.tw.clipshare.netConnection.PlainConnection; 28 | import com.tw.clipshare.netConnection.ServerConnection; 29 | import com.tw.clipshare.protocol.Proto; 30 | import com.tw.clipshare.protocol.ProtocolSelector; 31 | import java.io.IOException; 32 | import java.net.InetAddress; 33 | import java.net.UnknownHostException; 34 | import java.util.concurrent.ExecutorService; 35 | import java.util.concurrent.Executors; 36 | 37 | class SubnetScanner { 38 | 39 | private final byte[] addressBytes; 40 | private final InetAddress myAddress; 41 | private final int hostCnt; 42 | private final Object lock; 43 | private final int port; 44 | private volatile InetAddress serverAddress; 45 | 46 | public SubnetScanner(InetAddress address, int port, short subLen) { 47 | this.lock = new Object(); 48 | this.myAddress = address; 49 | this.port = port; 50 | this.addressBytes = address.getAddress(); 51 | this.hostCnt = (1 << (32 - subLen)) - 2; 52 | short hostLen = (short) (32 - subLen); 53 | for (int i = 3; i >= 0 && hostLen > 0; i--) { 54 | this.addressBytes[i] &= (byte) -(1 << hostLen); 55 | hostLen -= 8; 56 | } 57 | } 58 | 59 | private static InetAddress convertAddress(int addressInt) throws UnknownHostException { 60 | byte[] addressBytes = new byte[4]; 61 | for (int i = 3; i >= 0; i--) { 62 | addressBytes[i] = (byte) (addressInt & 0xff); 63 | addressInt >>= 8; 64 | } 65 | return InetAddress.getByAddress(addressBytes); 66 | } 67 | 68 | public InetAddress scan(int threadCnt) { 69 | ExecutorService executor = Executors.newFixedThreadPool(threadCnt); 70 | int addressInt = 0; 71 | for (byte addressByte : addressBytes) { 72 | addressInt = (addressInt << 8) | (addressByte & 0xff); 73 | } 74 | addressInt++; 75 | int endAddress = addressInt + hostCnt; 76 | for (int i = 0; i < threadCnt; i++) { 77 | executor.submit(new IPScanner(addressInt++, endAddress, port, threadCnt)); 78 | } 79 | while (this.serverAddress == null && !executor.isTerminated() && !Thread.interrupted()) { 80 | synchronized (this.lock) { 81 | try { 82 | lock.wait(500); 83 | } catch (InterruptedException ex) { 84 | break; 85 | } 86 | } 87 | executor.shutdown(); 88 | } 89 | executor.shutdownNow(); 90 | return this.serverAddress; 91 | } 92 | 93 | private class IPScanner implements Runnable { 94 | 95 | private final int addressEnd; 96 | private final int step; 97 | private int addressInt; 98 | private final int port; 99 | 100 | IPScanner(int startAddress, int endAddress, int port, int step) { 101 | this.step = step; 102 | this.addressInt = startAddress; 103 | this.addressEnd = endAddress; 104 | this.port = port; 105 | } 106 | 107 | @Override 108 | public void run() { 109 | while (!Thread.interrupted() && this.addressInt < this.addressEnd && serverAddress == null) { 110 | try { 111 | InetAddress address = convertAddress(addressInt); 112 | if (!address.equals(myAddress)) { 113 | ServerConnection con = new PlainConnection(address, port); 114 | Proto pr = ProtocolSelector.getProto(con, null, null); 115 | if (pr != null) { 116 | String serverName = pr.checkInfo(); 117 | if ("clip_share".equals(serverName)) { 118 | synchronized (lock) { 119 | serverAddress = address; 120 | lock.notifyAll(); 121 | } 122 | } 123 | } 124 | } 125 | } catch (IOException ex) { // Do not catch Interrupted exception in loop 126 | } finally { 127 | addressInt += step; 128 | } 129 | } 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /app/src/main/java/com/tw/clipshare/Utils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2022-2025 H. Thevindu J. Wijesekera 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.tw.clipshare; 26 | 27 | import java.net.Inet6Address; 28 | 29 | public class Utils { 30 | public static final byte PROTOCOL_SUPPORTED = 1; 31 | public static final byte PROTOCOL_OBSOLETE = 2; 32 | public static final byte PROTOCOL_UNKNOWN = 3; 33 | 34 | public static boolean isValidIP(String str) { 35 | try { 36 | if (str == null) return false; 37 | if (str.matches("^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)(\\.(?!$)|$)){4}$")) return true; 38 | if (!str.contains(":")) return false; 39 | //noinspection ResultOfMethodCallIgnored 40 | Inet6Address.getByName(str); 41 | return true; 42 | } catch (Exception ignored) { 43 | } 44 | return false; 45 | } 46 | 47 | private Utils() {} 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/tw/clipshare/netConnection/PlainConnection.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2022-2025 H. Thevindu J. Wijesekera 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.tw.clipshare.netConnection; 26 | 27 | import java.io.IOException; 28 | import java.net.InetAddress; 29 | import java.net.InetSocketAddress; 30 | import java.net.Socket; 31 | 32 | public class PlainConnection extends ServerConnection { 33 | 34 | /** 35 | * Unencrypted TCP connection to the server. 36 | * 37 | * @param serverAddress address of the server 38 | * @param port port on which the server is listening 39 | * @throws IOException on socket connection error 40 | */ 41 | public PlainConnection(InetAddress serverAddress, int port) throws IOException { 42 | super(new Socket()); 43 | this.socket.connect(new InetSocketAddress(serverAddress, port), 500); 44 | this.inStream = this.socket.getInputStream(); 45 | this.outStream = this.socket.getOutputStream(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/tw/clipshare/netConnection/SecureConnection.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2022-2025 H. Thevindu J. Wijesekera 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.tw.clipshare.netConnection; 26 | 27 | import com.tw.clipshare.CertUtils; 28 | import java.io.IOException; 29 | import java.io.InputStream; 30 | import java.net.InetAddress; 31 | import java.security.GeneralSecurityException; 32 | import java.security.KeyStore; 33 | import java.security.cert.X509Certificate; 34 | import java.util.concurrent.ExecutorService; 35 | import java.util.concurrent.Executors; 36 | import javax.net.ssl.*; 37 | 38 | public class SecureConnection extends ServerConnection { 39 | 40 | private static final Object CTX_LOCK = new Object(); 41 | private static SSLContext ctxInstance = null; 42 | 43 | /** 44 | * TLS encrypted connection to the server. 45 | * 46 | * @param serverAddress address of the server 47 | * @param port port on which the server is listening 48 | * @param caCertInput input stream to get the CA's certificate 49 | * @param clientCertStoreInput input stream to get the client key certificate store 50 | * @param certStorePassword input stream to get the client key certificate store password 51 | * @param acceptedCNs array of accepted servers (common names) 52 | * @throws IOException on connection error 53 | * @throws GeneralSecurityException on security related errors 54 | */ 55 | public SecureConnection( 56 | InetAddress serverAddress, 57 | int port, 58 | InputStream caCertInput, 59 | InputStream clientCertStoreInput, 60 | char[] certStorePassword, 61 | String[] acceptedCNs) 62 | throws IOException, GeneralSecurityException { 63 | SSLContext ctx; 64 | synchronized (SecureConnection.CTX_LOCK) { 65 | if (SecureConnection.ctxInstance == null) { 66 | X509Certificate caCert = CertUtils.getX509fromInputStream(caCertInput); 67 | TrustManagerFactory tmf = 68 | TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 69 | KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); 70 | ks.load(null); 71 | ks.setCertificateEntry("caCert", caCert); 72 | tmf.init(ks); 73 | 74 | KeyStore keyStore = KeyStore.getInstance("PKCS12"); 75 | keyStore.load(clientCertStoreInput, certStorePassword); 76 | KeyManagerFactory kmf = 77 | KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); 78 | kmf.init(keyStore, certStorePassword); 79 | 80 | ctx = SSLContext.getInstance("TLS"); 81 | ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); 82 | SecureConnection.ctxInstance = ctx; 83 | } else { 84 | ctx = SecureConnection.ctxInstance; 85 | } 86 | } 87 | SSLSocketFactory sslsocketfactory = ctx.getSocketFactory(); 88 | SSLSocket sslsocket = (SSLSocket) sslsocketfactory.createSocket(serverAddress, port); 89 | SSLSession sslSession = sslsocket.getSession(); 90 | X509Certificate serverCertificate = (X509Certificate) sslSession.getPeerCertificates()[0]; 91 | boolean accepted = false; 92 | try { 93 | String cn = CertUtils.getCertCN(serverCertificate); 94 | if (cn != null) { 95 | for (String acceptedCN : acceptedCNs) { 96 | if (acceptedCN.equals(cn)) { 97 | accepted = true; 98 | break; 99 | } 100 | } 101 | } 102 | } catch (Exception ignored) { 103 | } 104 | if (!accepted) { 105 | throw new SecurityException("Untrusted Server"); 106 | } 107 | this.socket = sslsocket; 108 | this.inStream = this.socket.getInputStream(); 109 | this.outStream = this.socket.getOutputStream(); 110 | } 111 | 112 | /** Reset the SSLContext instance to null */ 113 | public static void resetSSLContext() { 114 | ExecutorService executorService = Executors.newSingleThreadExecutor(); 115 | Runnable resetCtx = 116 | () -> { 117 | try { 118 | synchronized (SecureConnection.CTX_LOCK) { 119 | SecureConnection.ctxInstance = null; 120 | } 121 | } catch (Exception ignored) { 122 | } 123 | }; 124 | executorService.submit(resetCtx); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /app/src/main/java/com/tw/clipshare/netConnection/ServerConnection.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2022-2025 H. Thevindu J. Wijesekera 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.tw.clipshare.netConnection; 26 | 27 | import java.io.IOException; 28 | import java.io.InputStream; 29 | import java.io.OutputStream; 30 | import java.net.Socket; 31 | import java.net.SocketException; 32 | 33 | public abstract class ServerConnection { 34 | 35 | protected OutputStream outStream; 36 | protected InputStream inStream; 37 | protected Socket socket; 38 | private boolean closed; 39 | private boolean lastOperationSend; 40 | 41 | protected ServerConnection() { 42 | this(null); 43 | } 44 | 45 | protected ServerConnection(Socket socket) { 46 | this.socket = socket; 47 | this.closed = false; 48 | this.lastOperationSend = false; 49 | try { 50 | if (this.socket != null) this.socket.setSoTimeout(10000); 51 | } catch (RuntimeException | SocketException ignored) { 52 | } 53 | } 54 | 55 | /** 56 | * Sends length bytes of data from buffer starting at offset to server. 57 | * 58 | * @param buffer buffer containing data, which should be at least offset+length in size 59 | * @param offset index of starting byte of buffer 60 | * @param length number of bytes to send 61 | * @return false on success or true on failure 62 | */ 63 | public boolean send(byte[] buffer, int offset, int length) { 64 | this.lastOperationSend = true; 65 | try { 66 | outStream.write(buffer, offset, length); 67 | return false; 68 | } catch (RuntimeException | IOException ex) { 69 | return true; 70 | } 71 | } 72 | 73 | /** 74 | * Receives length bytes of data from server and stores it in buffer starting at offset 75 | * 76 | * @param buffer buffer to store data, which should be at least offset+length in size 77 | * @param offset index of starting byte in buffer 78 | * @param length number of bytes to read 79 | * @return false on success or true on failure 80 | */ 81 | public boolean receive(byte[] buffer, int offset, int length) { 82 | this.lastOperationSend = false; 83 | int remaining = length; 84 | try { 85 | while (remaining > 0) { 86 | int read = inStream.read(buffer, offset, remaining); 87 | if (read > 0) { 88 | offset += read; 89 | remaining -= read; 90 | } else if (read < 0) { 91 | return true; 92 | } 93 | } 94 | return false; 95 | } catch (RuntimeException | IOException ex) { 96 | return true; 97 | } 98 | } 99 | 100 | /** 101 | * Sends all data in buffer to server. 102 | * 103 | * @param buffer buffer containing data 104 | * @return false on success or true on failure 105 | */ 106 | public boolean send(byte[] buffer) { 107 | return this.send(buffer, 0, buffer.length); 108 | } 109 | 110 | /** 111 | * Receives data into buffer from server until buffer is full. 112 | * 113 | * @param buffer buffer to store data 114 | * @return false on success or true on failure 115 | */ 116 | public boolean receive(byte[] buffer) { 117 | return this.receive(buffer, 0, buffer.length); 118 | } 119 | 120 | public void close() { 121 | synchronized (this) { 122 | if (this.closed) return; 123 | this.closed = true; 124 | } 125 | if (this.lastOperationSend) { 126 | try { 127 | this.socket.setSoTimeout(1000); 128 | int ignored = this.inStream.read(); // wait for peer to receive all data 129 | } catch (RuntimeException | IOException ignored) { 130 | } 131 | } 132 | try { 133 | this.socket.close(); 134 | } catch (RuntimeException | IOException ignored) { 135 | } 136 | } 137 | 138 | @Override 139 | protected void finalize() throws Throwable { 140 | this.close(); 141 | super.finalize(); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /app/src/main/java/com/tw/clipshare/netConnection/TunnelConnection.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2022-2023 H. Thevindu J. Wijesekera 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.tw.clipshare.netConnection; 26 | 27 | import java.io.IOException; 28 | import java.net.Socket; 29 | import java.net.SocketException; 30 | 31 | /** 32 | * @noinspection unused 33 | */ 34 | public class TunnelConnection extends ServerConnection { 35 | public TunnelConnection(String address) throws IOException { 36 | super(); 37 | Socket tunnel = TunnelManager.getConnection(address); 38 | if (tunnel == null) { 39 | throw new SocketException("No tunnel available for " + address); 40 | } 41 | this.socket = tunnel; 42 | this.inStream = this.socket.getInputStream(); 43 | this.outStream = this.socket.getOutputStream(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/com/tw/clipshare/netConnection/TunnelManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2022-2023 H. Thevindu J. Wijesekera 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.tw.clipshare.netConnection; 26 | 27 | import java.io.IOException; 28 | import java.io.InputStream; 29 | import java.io.OutputStream; 30 | import java.net.ServerSocket; 31 | import java.net.Socket; 32 | import java.net.SocketException; 33 | import java.util.HashMap; 34 | import java.util.concurrent.ExecutorService; 35 | import java.util.concurrent.Executors; 36 | 37 | /** 38 | * @noinspection unused 39 | */ 40 | public class TunnelManager { 41 | 42 | private static final HashMap tunnels = new HashMap<>(1); 43 | private static ExecutorService connectionExecutor = null; 44 | private static ExecutorService listenerExecutor = null; 45 | private static ServerSocket serverSocket = null; 46 | 47 | public static synchronized Socket getConnection(String address) { 48 | if (!tunnels.containsKey(address)) { 49 | return null; 50 | } 51 | Tunnel tunnel = tunnels.remove(address); 52 | if (tunnel == null) { 53 | return null; 54 | } 55 | Socket socket = null; 56 | try { 57 | socket = tunnel.releaseSocket(); 58 | } catch (Exception ignored) { 59 | } 60 | return socket; 61 | } 62 | 63 | private static synchronized void putConnection(Socket connection) { 64 | String address = connection.getInetAddress().getHostAddress(); 65 | if (tunnels.containsKey(address)) { 66 | Tunnel old = tunnels.get(address); 67 | if (old != null) { 68 | try { 69 | old.close(); 70 | } catch (Exception ignored) { 71 | } 72 | } 73 | } 74 | try { 75 | Tunnel tunnel = new Tunnel(connection); 76 | tunnels.put(address, tunnel); 77 | connectionExecutor.submit(tunnel); 78 | } catch (Exception ignored) { 79 | } 80 | } 81 | 82 | private static synchronized void removeConnection(Socket connection) { 83 | String address = connection.getInetAddress().getHostAddress(); 84 | if (tunnels.containsKey(address)) { 85 | Tunnel tunnel = tunnels.get(address); 86 | if (tunnel != null) { 87 | try { 88 | tunnel.close(); 89 | } catch (Exception ignored) { 90 | } 91 | } 92 | } 93 | } 94 | 95 | public static void start() { 96 | try { 97 | if (serverSocket != null) { 98 | serverSocket.close(); 99 | } 100 | } catch (Exception ignored) { 101 | } 102 | try { 103 | serverSocket = new ServerSocket(4367); 104 | if (listenerExecutor != null) { 105 | listenerExecutor.shutdownNow(); 106 | } 107 | listenerExecutor = Executors.newSingleThreadExecutor(); 108 | connectionExecutor = Executors.newCachedThreadPool(); 109 | Runnable listenerRunnable = 110 | () -> { 111 | try { 112 | while (!Thread.interrupted()) { 113 | Socket socket = serverSocket.accept(); 114 | putConnection(socket); 115 | } 116 | serverSocket.close(); 117 | } catch (Exception ignored) { 118 | } 119 | }; 120 | listenerExecutor.submit(listenerRunnable); 121 | } catch (Exception ignored) { 122 | } 123 | } 124 | 125 | public static void stop() { 126 | try { 127 | serverSocket.close(); 128 | } catch (IOException ignored) { 129 | } 130 | try { 131 | if (listenerExecutor != null) listenerExecutor.shutdownNow(); 132 | if (connectionExecutor != null) connectionExecutor.shutdown(); 133 | tunnels.forEach( 134 | (ip, tunnel) -> { 135 | try { 136 | tunnel.close(); 137 | } catch (Exception ignored) { 138 | } 139 | }); 140 | tunnels.clear(); 141 | } catch (Exception ignored) { 142 | } 143 | } 144 | 145 | private static class Tunnel extends Thread { 146 | 147 | private final Socket socket; 148 | private final InputStream inputStream; 149 | private final OutputStream outputStream; 150 | private boolean released; 151 | 152 | Tunnel(Socket socket) throws IOException { 153 | this.socket = socket; 154 | this.inputStream = socket.getInputStream(); 155 | this.outputStream = socket.getOutputStream(); 156 | released = false; 157 | } 158 | 159 | Socket releaseSocket() throws IOException { 160 | try { 161 | this.socket.setSoTimeout(5000); 162 | } catch (Exception ignored) { 163 | } 164 | synchronized (this) { 165 | outputStream.write(3); 166 | int read = inputStream.read(); 167 | if (read != 4) { 168 | socket.close(); 169 | throw new SocketException("Invalid client response"); 170 | } 171 | if (this.released) { 172 | throw new SocketException("Socket is already released"); 173 | } 174 | this.released = true; 175 | this.interrupt(); 176 | } 177 | return this.socket; 178 | } 179 | 180 | void close() throws IOException { 181 | synchronized (this) { 182 | this.released = true; 183 | this.interrupt(); 184 | this.socket.close(); 185 | } 186 | } 187 | 188 | @Override 189 | public void run() { 190 | try { 191 | socket.setSoTimeout(1000); 192 | while (!Thread.interrupted()) { 193 | synchronized (this) { 194 | if (this.released) { 195 | break; 196 | } 197 | } 198 | outputStream.write(1); 199 | int read = inputStream.read(); 200 | if (read != 2) { 201 | removeConnection(socket); 202 | break; 203 | } 204 | if (Thread.interrupted()) break; 205 | //noinspection BusyWait 206 | Thread.sleep(2000); 207 | } 208 | } catch (Exception ignored) { 209 | } 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /app/src/main/java/com/tw/clipshare/platformUtils/AndroidUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2022-2025 H. Thevindu J. Wijesekera 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.tw.clipshare.platformUtils; 26 | 27 | import static android.content.ClipDescription.MIMETYPE_TEXT_HTML; 28 | import static android.content.ClipDescription.MIMETYPE_TEXT_PLAIN; 29 | 30 | import android.app.Activity; 31 | import android.content.ClipData; 32 | import android.content.ClipboardManager; 33 | import android.content.Context; 34 | import android.os.*; 35 | import android.widget.Toast; 36 | import com.tw.clipshare.Settings; 37 | 38 | public class AndroidUtils { 39 | private static long lastToastTime = 0; 40 | 41 | protected final Context context; 42 | protected final Activity activity; 43 | 44 | public AndroidUtils(Context context, Activity activity) { 45 | this.context = context; 46 | this.activity = activity; 47 | } 48 | 49 | private ClipboardManager getClipboardManager() { 50 | try { 51 | Object lock = new Object(); 52 | ClipboardManager[] clipboardManagers = new ClipboardManager[1]; 53 | this.activity.runOnUiThread( 54 | () -> { 55 | clipboardManagers[0] = 56 | (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); 57 | synchronized (lock) { 58 | lock.notifyAll(); 59 | } 60 | }); 61 | while (clipboardManagers[0] == null) { 62 | try { 63 | synchronized (lock) { 64 | if (clipboardManagers[0] == null) { 65 | lock.wait(100); 66 | } 67 | } 68 | } catch (Exception ignored) { 69 | } 70 | } 71 | return clipboardManagers[0]; 72 | } catch (Exception ignored) { 73 | return null; 74 | } 75 | } 76 | 77 | /** 78 | * Get the text copied to the clipboard. 79 | * 80 | * @return text copied to the clipboard as a String or null on error. 81 | */ 82 | public String getClipboardText() { 83 | try { 84 | ClipboardManager clipboard = this.getClipboardManager(); 85 | if (clipboard == null 86 | || !(clipboard.hasPrimaryClip()) 87 | || !(clipboard.getPrimaryClipDescription().hasMimeType(MIMETYPE_TEXT_PLAIN) 88 | || !clipboard.getPrimaryClipDescription().hasMimeType(MIMETYPE_TEXT_HTML))) { 89 | return null; 90 | } 91 | ClipData.Item item = clipboard.getPrimaryClip().getItemAt(0); 92 | CharSequence clipDataSequence = item.getText(); 93 | if (clipDataSequence == null) { 94 | return null; 95 | } 96 | return clipDataSequence.toString(); 97 | } catch (Exception ignored) { 98 | return null; 99 | } 100 | } 101 | 102 | /** 103 | * Copy the text to the clipboard. 104 | * 105 | * @param text to be copied to the clipboard 106 | */ 107 | public void setClipboardText(String text) { 108 | try { 109 | ClipboardManager clipboard = this.getClipboardManager(); 110 | ClipData clip = ClipData.newPlainText("clip_share", text); 111 | if (clipboard != null) clipboard.setPrimaryClip(clip); 112 | } catch (Exception ignored) { 113 | } 114 | } 115 | 116 | public void showToast(String message) { 117 | if (this.context == null) return; 118 | try { 119 | long currTime = System.currentTimeMillis(); 120 | if (currTime - lastToastTime < 2000) return; 121 | lastToastTime = currTime; 122 | Handler handler = new Handler(Looper.getMainLooper()); 123 | handler.post(() -> Toast.makeText(context, message, Toast.LENGTH_SHORT).show()); 124 | } catch (Exception ignored) { 125 | } 126 | } 127 | 128 | @SuppressWarnings("deprecation") 129 | public void vibrate() { 130 | try { 131 | if (context == null || !Settings.getInstance().getVibrate()) return; 132 | Vibrator vibrator; 133 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 134 | VibratorManager vibratorManager = 135 | (VibratorManager) context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE); 136 | vibrator = vibratorManager.getDefaultVibrator(); 137 | } else { 138 | vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); 139 | } 140 | 141 | final int duration = 100; 142 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 143 | vibrator.vibrate( 144 | VibrationEffect.createOneShot(duration, VibrationEffect.DEFAULT_AMPLITUDE)); 145 | } else { 146 | vibrator.vibrate(duration); 147 | } 148 | } catch (Exception ignored) { 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /app/src/main/java/com/tw/clipshare/platformUtils/DataContainer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2022-2025 H. Thevindu J. Wijesekera 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.tw.clipshare.platformUtils; 26 | 27 | import androidx.annotation.Nullable; 28 | import java.io.File; 29 | import java.util.List; 30 | 31 | public class DataContainer { 32 | private Object data; 33 | private String message; 34 | 35 | public void setData(Object data) { 36 | this.data = data; 37 | } 38 | 39 | @Nullable 40 | public String getString() { 41 | if (data instanceof String) { 42 | return (String) data; 43 | } 44 | return null; 45 | } 46 | 47 | @Nullable 48 | public List getFiles() { 49 | if (data instanceof File file) { 50 | return List.of(file); 51 | } 52 | if (data instanceof List) { 53 | for (Object obj : (List) data) { 54 | if (!(obj instanceof File)) return null; 55 | } 56 | //noinspection unchecked 57 | return (List) data; 58 | } 59 | return null; 60 | } 61 | 62 | @Nullable 63 | public String getMessage() { 64 | return this.message; 65 | } 66 | 67 | public void setMessage(String msg) { 68 | this.message = msg; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/java/com/tw/clipshare/platformUtils/FSUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2022-2025 H. Thevindu J. Wijesekera 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.tw.clipshare.platformUtils; 26 | 27 | import android.app.Activity; 28 | import android.content.Context; 29 | import android.media.MediaScannerConnection; 30 | import android.net.Uri; 31 | import android.os.Build; 32 | import android.os.Environment; 33 | import com.tw.clipshare.PendingFile; 34 | import com.tw.clipshare.platformUtils.directoryTree.DirectoryTreeNode; 35 | import java.io.*; 36 | import java.util.ArrayList; 37 | import java.util.LinkedList; 38 | import java.util.Random; 39 | 40 | /** Utility to access files */ 41 | public class FSUtils extends AndroidUtils { 42 | private long fileSize; 43 | private String inFileName; 44 | private InputStream inStream; 45 | private final String id; 46 | private String outFilePath; 47 | private final LinkedList pendingFiles; 48 | private final DirectoryTreeNode directoryTree; 49 | private DataContainer dataContainer; 50 | 51 | private FSUtils( 52 | Context context, 53 | Activity activity, 54 | LinkedList pendingFiles, 55 | DirectoryTreeNode directoryTree) { 56 | super(context, activity); 57 | this.pendingFiles = pendingFiles; 58 | this.directoryTree = directoryTree; 59 | Random rnd = new Random(); 60 | long idNum = Math.abs(rnd.nextLong()); 61 | String id; 62 | File file; 63 | String dirName = getDocumentDir(); 64 | do { 65 | id = Long.toString(idNum, 36); 66 | String tmpDirName = dirName + '/' + id; 67 | file = new File(tmpDirName); 68 | idNum++; 69 | } while (file.exists()); 70 | this.id = id; 71 | } 72 | 73 | public FSUtils(Context context, Activity activity, LinkedList pendingFiles) { 74 | this(context, activity, pendingFiles, null); 75 | } 76 | 77 | public FSUtils(Context context, Activity activity, DirectoryTreeNode directoryTree) { 78 | this(context, activity, null, directoryTree); 79 | } 80 | 81 | public FSUtils(Context context, Activity activity) { 82 | this(context, activity, null, null); 83 | } 84 | 85 | private String getDocumentDir() { 86 | String baseDirName; 87 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 88 | baseDirName = 89 | Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) 90 | .getAbsolutePath(); 91 | } else { 92 | baseDirName = Environment.getExternalStorageDirectory().getAbsolutePath(); 93 | } 94 | return baseDirName + "/ClipShareDocuments"; 95 | } 96 | 97 | private String getDataDirPath(String path) { 98 | if (!path.isEmpty() && path.charAt(path.length() - 1) != '/') { 99 | path += '/'; 100 | } 101 | final String dirName = getDocumentDir(); 102 | File dir = new File(dirName); 103 | if (!dir.exists() && !dir.mkdirs()) { 104 | return null; 105 | } 106 | 107 | String dataDirName = dirName + "/" + this.id; 108 | File dataDir = new File(dataDirName); 109 | if (!dataDir.exists() && !dataDir.mkdirs()) { 110 | return null; 111 | } 112 | return dataDirName + "/" + path; 113 | } 114 | 115 | public OutputStream getFileOutStream(String fileName) { 116 | int base_ind = fileName.lastIndexOf('/') + 1; 117 | String baseName = fileName.substring(base_ind); 118 | String path = fileName.substring(0, base_ind); 119 | if (path.startsWith("../") || path.endsWith("/..") || path.contains("/../")) return null; 120 | path = getDataDirPath(path); 121 | if (path == null) return null; 122 | File fp = new File(path); 123 | if (!fp.exists() && !fp.mkdirs()) return null; 124 | String filename = path + baseName; 125 | File f = new File(filename); 126 | this.outFilePath = filename; 127 | try { 128 | return new FileOutputStream(f); 129 | } catch (Exception ignored) { 130 | return null; 131 | } 132 | } 133 | 134 | public boolean createDirectory(String dirPath) { 135 | dirPath = getDataDirPath(dirPath); 136 | if (dirPath == null) return false; 137 | File fp = new File(dirPath); 138 | if (fp.isDirectory()) return true; 139 | if (fp.exists()) return false; 140 | return fp.mkdirs(); 141 | } 142 | 143 | public boolean finish() { 144 | final String dir = getDocumentDir() + "/"; 145 | final String dataDirName = dir + this.id; 146 | File dataDir = new File(dataDirName); 147 | if (!dataDir.exists()) return true; 148 | String[] content = dataDir.list(); 149 | if (content == null) return true; 150 | boolean status = true; 151 | ArrayList files = new ArrayList<>(content.length); 152 | for (String fileName : content) { 153 | File newFile = new File(dir + fileName); 154 | int pref = 1; 155 | while (newFile.exists()) { 156 | String newName = pref++ + "_" + fileName; 157 | newFile = new File(dir + newName); 158 | } 159 | File file = new File(dataDirName + "/" + fileName); 160 | status &= file.renameTo(newFile); 161 | scanMediaFile(newFile.getAbsolutePath()); 162 | if (newFile.isFile()) files.add(newFile); 163 | } 164 | if (status) dataContainer.setData(files); 165 | status &= dataDir.delete(); 166 | return status; 167 | } 168 | 169 | public OutputStream getImageOutStream() { 170 | String baseDirName; 171 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 172 | baseDirName = 173 | String.valueOf( 174 | Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)); 175 | } else { 176 | baseDirName = String.valueOf(Environment.getExternalStorageDirectory()); 177 | } 178 | final String dirName = baseDirName + "/ClipShareImages"; 179 | File dir = new File(dirName); 180 | if (!dir.exists()) { 181 | if (!dir.mkdirs()) { 182 | return null; 183 | } 184 | } 185 | String fileName = Long.toString(System.currentTimeMillis(), 32) + ".png"; 186 | String fileNameTmp = dirName + "/" + fileName; 187 | File file = new File(fileNameTmp); 188 | if (file.exists()) { 189 | int i = 1; 190 | while (file.exists()) { 191 | fileNameTmp = dirName + "/" + i + "_" + fileName; 192 | file = new File(fileNameTmp); 193 | i++; 194 | } 195 | } 196 | this.outFilePath = fileNameTmp; 197 | try { 198 | return new FileOutputStream(file); 199 | } catch (FileNotFoundException ignored) { 200 | return null; 201 | } 202 | } 203 | 204 | public void getFileDone(String type) { 205 | String path; 206 | if ("image".equals(type)) { 207 | path = outFilePath; 208 | dataContainer.setData(new File(path)); 209 | } else { 210 | path = getDocumentDir(); 211 | } 212 | path = path.replaceFirst("^/storage/emulated/0", "Internal Storage"); 213 | showToast("Saved " + type + " to " + path); 214 | } 215 | 216 | public void scanMediaFile(String filePath) { 217 | if (this.activity == null) return; 218 | int dotIndex = filePath.lastIndexOf('.'); 219 | if (dotIndex <= 0) return; 220 | String extension = filePath.substring(dotIndex + 1); 221 | String[] mediaExtensions = { 222 | "png", "jpg", "jpeg", "gif", "bmp", "webp", "heic", "tif", "tiff", "mp4", "mkv", "mov", 223 | "webm", "wmv", "flv", "avi" 224 | }; 225 | for (String mediaExtension : mediaExtensions) { 226 | if (mediaExtension.equalsIgnoreCase(extension)) { 227 | MediaScannerConnection.scanFile( 228 | this.activity.getApplicationContext(), new String[] {filePath}, null, null); 229 | break; 230 | } 231 | } 232 | } 233 | 234 | public void scanMediaFile() { 235 | scanMediaFile(outFilePath); 236 | } 237 | 238 | public String getFileName() { 239 | return this.inFileName; 240 | } 241 | 242 | public long getFileSize() { 243 | return this.fileSize; 244 | } 245 | 246 | public InputStream getFileInStream() { 247 | return this.inStream; 248 | } 249 | 250 | public int getRemainingFileCount(boolean includeLeafDirs) { 251 | if (this.pendingFiles != null) return this.pendingFiles.size(); 252 | if (this.directoryTree != null) return this.directoryTree.getLeafCount(includeLeafDirs); 253 | return -1; 254 | } 255 | 256 | public int getRemainingFileCount() { 257 | return this.getRemainingFileCount(false); 258 | } 259 | 260 | public boolean prepareNextFile(boolean allowDirs) { 261 | try { 262 | if (this.directoryTree != null) { 263 | DirectoryTreeNode node = this.directoryTree.pop(allowDirs); 264 | this.inFileName = node.getFullName(); 265 | this.fileSize = node.getFileSize(); 266 | Uri uri = node.getUri(); 267 | if (uri != null) this.inStream = activity.getContentResolver().openInputStream(uri); 268 | else this.inStream = null; 269 | return true; 270 | } 271 | if (this.pendingFiles != null) { 272 | PendingFile pendingFile = this.pendingFiles.pop(); 273 | this.inFileName = pendingFile.name(); 274 | this.fileSize = pendingFile.size(); 275 | if (pendingFile.uri() != null) 276 | this.inStream = activity.getContentResolver().openInputStream(pendingFile.uri()); 277 | else this.inStream = null; 278 | return true; 279 | } 280 | } catch (Exception ignored) { 281 | } 282 | return false; 283 | } 284 | 285 | public boolean prepareNextFile() { 286 | return this.prepareNextFile(false); 287 | } 288 | 289 | public void setDataContainer(DataContainer dataContainer) { 290 | this.dataContainer = dataContainer; 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /app/src/main/java/com/tw/clipshare/platformUtils/StatusNotifier.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2022-2025 H. Thevindu J. Wijesekera 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.tw.clipshare.platformUtils; 26 | 27 | import android.annotation.SuppressLint; 28 | import android.app.Notification; 29 | import android.app.NotificationManager; 30 | import androidx.annotation.NonNull; 31 | import androidx.core.app.NotificationCompat; 32 | import java.util.Locale; 33 | 34 | public final class StatusNotifier { 35 | 36 | private static final int PROGRESS_MAX = 100; 37 | private final NotificationManager notificationManager; 38 | private final NotificationCompat.Builder builder; 39 | private final int notificationId; 40 | private long fileSize; 41 | private String fileSizeStr; 42 | private long prevNotifyTime; 43 | private DataSize prevProgress; 44 | private long prevSize; 45 | private long prevSpeed; 46 | private TimeContainer prevTimeRemaining; 47 | private long prevTime; 48 | private boolean finished; 49 | 50 | public StatusNotifier( 51 | NotificationManager notificationManager, 52 | NotificationCompat.Builder builder, 53 | int notificationId) { 54 | this.notificationManager = notificationManager; 55 | this.builder = 56 | builder 57 | .setContentText("0%") 58 | .setPriority(NotificationCompat.PRIORITY_DEFAULT) 59 | .setOnlyAlertOnce(true) 60 | .setVibrate(new long[] {0L}) 61 | .setSilent(true); 62 | this.notificationId = notificationId; 63 | this.fileSize = -1; 64 | this.fileSizeStr = ""; 65 | this.prevNotifyTime = 0; 66 | this.prevProgress = null; 67 | this.prevTime = 0; 68 | this.prevSize = -1; 69 | this.prevSpeed = -1; 70 | this.prevTimeRemaining = null; 71 | this.finished = false; 72 | } 73 | 74 | public void setTitle(String title) { 75 | if (this.builder == null) return; 76 | try { 77 | int len = title.length(); 78 | if (len > 32) { 79 | title = title.substring(0, 20) + "..." + title.substring(len - 9); 80 | } 81 | this.builder.setContentTitle(title); 82 | } catch (Exception ignored) { 83 | } 84 | } 85 | 86 | public void setIcon(int icon) { 87 | if (this.builder == null) return; 88 | try { 89 | this.builder.setSmallIcon(icon); 90 | } catch (Exception ignored) { 91 | } 92 | } 93 | 94 | /** 95 | * Get the data transfer speed in Bytes per seconds. 96 | * 97 | * @param curSize current transfer amount in Bytes 98 | * @param curTime current time in milliseconds since a fixed time (ex: Unix epoch) 99 | * @return time averaged data transfer speed in Bytes/sec 100 | */ 101 | long getSpeed(long curSize, long curTime) { 102 | if (prevSize < 0) { 103 | prevSize = curSize; 104 | prevTime = curTime; 105 | return -1; 106 | } 107 | long dur = curTime - prevTime; 108 | if (dur >= 400) { // smaller durations cause less precision and high fluctuations 109 | long speed = ((curSize - prevSize) * 1000) / dur; // Bytes per second 110 | if (prevSpeed > 0) 111 | speed = (speed + 3 * prevSpeed) / 4; // prevent too large fluctuations in speed value 112 | prevSpeed = speed; 113 | prevSize = curSize; 114 | prevTime = curTime; 115 | } 116 | return prevSpeed; 117 | } 118 | 119 | /** 120 | * Get estimated time remaining to complete the data transfer. 121 | * 122 | * @param curSize current transfer amount in Bytes 123 | * @param speed data transfer speed in Bytes/sec 124 | * @return estimated remaining time 125 | */ 126 | TimeContainer getRemainingTime(long curSize, long speed) { 127 | long remSize = fileSize - curSize; 128 | long remSeconds; 129 | if (speed >= 500) { // smaller values cause less precision 130 | remSeconds = remSize / speed; 131 | } else { 132 | remSeconds = -1; 133 | } 134 | return TimeContainer.initBySeconds(remSeconds); 135 | } 136 | 137 | @SuppressLint("MissingPermission") 138 | public void setProgress(long current) { 139 | try { 140 | long curTime = System.currentTimeMillis(); 141 | if (curTime < this.prevNotifyTime + 800 || curTime % 1000 > 200) return; 142 | long speed = getSpeed(current, curTime); 143 | DataSize progress = new DataSize(current); 144 | TimeContainer timeRemaining = getRemainingTime(current, speed); 145 | if (progress.equals(prevProgress) && timeRemaining.equals(prevTimeRemaining)) return; 146 | this.prevProgress = progress; 147 | this.prevTimeRemaining = timeRemaining; 148 | this.prevNotifyTime = curTime; 149 | int percent = (int) ((current * 100) / fileSize); 150 | builder 151 | .setProgress(PROGRESS_MAX, percent, false) 152 | .setContentText(progress + "/" + fileSizeStr); 153 | if (timeRemaining.time >= 0) builder.setSubText(timeRemaining + " left"); 154 | notificationManager.notify(notificationId, builder.build()); 155 | } catch (Exception ignored) { 156 | } 157 | } 158 | 159 | public void setFileSize(long fileSize) { 160 | this.fileSize = fileSize; 161 | this.fileSizeStr = (new DataSize(fileSize)).toString(); 162 | } 163 | 164 | public void reset() { 165 | this.prevNotifyTime = 0; 166 | this.prevProgress = null; 167 | this.prevTime = 0; 168 | this.prevSize = -1; 169 | this.prevSpeed = -1; 170 | this.prevTimeRemaining = null; 171 | } 172 | 173 | public Notification getNotification() { 174 | return builder.build(); 175 | } 176 | 177 | public int getId() { 178 | return this.notificationId; 179 | } 180 | 181 | public void finish() { 182 | synchronized (this) { 183 | if (this.finished) return; 184 | this.finished = true; 185 | } 186 | try { 187 | if (this.notificationManager != null) { 188 | this.notificationManager.cancel(this.notificationId); 189 | } 190 | } catch (Exception ignored) { 191 | } 192 | } 193 | 194 | @Override 195 | protected void finalize() throws Throwable { 196 | this.finish(); 197 | super.finalize(); 198 | } 199 | } 200 | 201 | enum DataUnit { 202 | B, 203 | KB, 204 | MB, 205 | GB, 206 | TB 207 | } 208 | 209 | class DataSize { 210 | final DataUnit unit; 211 | final float value; 212 | 213 | DataSize(long size) { 214 | int p1000; 215 | long size1 = size; 216 | for (p1000 = 0; size1 >= 1000; size1 /= 1000) { 217 | p1000++; 218 | size = size1; 219 | } 220 | if (size < 1000) this.value = (float) size; 221 | else this.value = size / 1000.f; 222 | switch (p1000) { 223 | case 0: 224 | { 225 | this.unit = DataUnit.B; 226 | break; 227 | } 228 | case 1: 229 | { 230 | this.unit = DataUnit.KB; 231 | break; 232 | } 233 | case 2: 234 | { 235 | this.unit = DataUnit.MB; 236 | break; 237 | } 238 | case 3: 239 | { 240 | this.unit = DataUnit.GB; 241 | break; 242 | } 243 | default: 244 | { 245 | this.unit = DataUnit.TB; 246 | } 247 | } 248 | } 249 | 250 | @Override 251 | public boolean equals(Object other) { 252 | if (!(other instanceof DataSize otherSize)) return false; 253 | if (this.unit != otherSize.unit) return false; 254 | return Math.round(this.value * 100) == Math.round(otherSize.value * 100); 255 | } 256 | 257 | @Override 258 | @NonNull 259 | public String toString() { 260 | return String.format(Locale.ENGLISH, "%.3G %s", this.value, this.unit.name()); 261 | } 262 | } 263 | 264 | class TimeContainer { 265 | static final String SECOND = "sec"; 266 | static final String MINUTE = "min"; 267 | static final String HOUR = "hour"; 268 | static final String DAY = "day"; 269 | final short time; 270 | final String unit; 271 | 272 | private TimeContainer(short time, String unit) { 273 | this.time = time; 274 | this.unit = unit; 275 | } 276 | 277 | static TimeContainer initBySeconds(long seconds) { 278 | if (seconds < 0) { // Undefined time 279 | return new TimeContainer((short) -1, TimeContainer.SECOND); 280 | } 281 | if (seconds < 60) { 282 | return new TimeContainer((short) seconds, TimeContainer.SECOND); 283 | } 284 | if (seconds < 3600) { 285 | return new TimeContainer((short) ((seconds + 30) / 60), TimeContainer.MINUTE); 286 | } 287 | if (seconds < 86400) { 288 | return new TimeContainer((short) ((seconds + 1800) / 3600), TimeContainer.HOUR); 289 | } 290 | if (seconds < 5184000) { 291 | return new TimeContainer((short) ((seconds + 43200) / 86400), TimeContainer.DAY); 292 | } 293 | return new TimeContainer((short) -1, TimeContainer.SECOND); 294 | } 295 | 296 | @Override 297 | public boolean equals(Object other) { 298 | if (!(other instanceof TimeContainer otherContainer)) return false; 299 | return (this.time == otherContainer.time 300 | && (this.time < 0 || this.unit.equals(otherContainer.unit))); 301 | } 302 | 303 | @Override 304 | @NonNull 305 | public String toString() { 306 | if (this.time == 1) { 307 | return this.time + " " + this.unit; 308 | } 309 | return this.time + " " + this.unit + 's'; 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /app/src/main/java/com/tw/clipshare/platformUtils/directoryTree/Directory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2022-2025 H. Thevindu J. Wijesekera 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.tw.clipshare.platformUtils.directoryTree; 26 | 27 | import android.net.Uri; 28 | import java.util.ArrayList; 29 | 30 | public class Directory extends DirectoryTreeNode { 31 | public final ArrayList children; 32 | 33 | public Directory(String name, int size, Directory parent) { 34 | super(name, parent); 35 | this.children = new ArrayList<>(size); 36 | } 37 | 38 | @Override 39 | public int getLeafCount(boolean includeLeafDirs) { 40 | int leaves = 0; 41 | for (DirectoryTreeNode child : children) { 42 | leaves += child.getLeafCount(includeLeafDirs); 43 | } 44 | if (leaves == 0 && includeLeafDirs) leaves = 1; 45 | return leaves; 46 | } 47 | 48 | @Override 49 | public long getFileSize() { 50 | return -1; 51 | } 52 | 53 | @Override 54 | public Uri getUri() { 55 | return null; 56 | } 57 | 58 | @Override 59 | public DirectoryTreeNode pop(boolean includeDirs) { 60 | if (this.children.isEmpty() && includeDirs) return this; 61 | for (DirectoryTreeNode child : this.children) { 62 | if (child instanceof RegularFile) { 63 | this.children.remove(child); 64 | return child; 65 | } 66 | Directory childDir = (Directory) child; 67 | DirectoryTreeNode node = childDir.pop(includeDirs); 68 | if (childDir.children.isEmpty()) this.children.remove(child); 69 | if (node != null) return node; 70 | } 71 | return null; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/com/tw/clipshare/platformUtils/directoryTree/DirectoryTreeNode.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2022-2025 H. Thevindu J. Wijesekera 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.tw.clipshare.platformUtils.directoryTree; 26 | 27 | import android.net.Uri; 28 | import java.util.LinkedList; 29 | 30 | public abstract class DirectoryTreeNode { 31 | public final String name; 32 | private final Directory parent; 33 | 34 | DirectoryTreeNode(String name, Directory parent) { 35 | this.name = name; 36 | this.parent = parent; 37 | } 38 | 39 | public abstract int getLeafCount(boolean includeLeafDirs); 40 | 41 | public abstract long getFileSize(); 42 | 43 | public abstract Uri getUri(); 44 | 45 | public abstract DirectoryTreeNode pop(boolean includeDirs); 46 | 47 | public String getFullName() { 48 | LinkedList stack = new LinkedList<>(); 49 | DirectoryTreeNode node = this; 50 | do { 51 | stack.push(node); 52 | node = node.parent; 53 | } while (node != null); 54 | StringBuilder builder = new StringBuilder(); 55 | boolean first = true; 56 | while (!stack.isEmpty()) { 57 | if (!first) builder.append('/'); 58 | first = false; 59 | builder.append(stack.pop().name); 60 | } 61 | return builder.toString(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/com/tw/clipshare/platformUtils/directoryTree/RegularFile.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2022-2025 H. Thevindu J. Wijesekera 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.tw.clipshare.platformUtils.directoryTree; 26 | 27 | import android.net.Uri; 28 | 29 | public class RegularFile extends DirectoryTreeNode { 30 | 31 | public final Uri uri; 32 | public final long size; 33 | 34 | public RegularFile(String name, long size, Uri uri, Directory parent) { 35 | super(name, parent); 36 | this.size = size; 37 | this.uri = uri; 38 | } 39 | 40 | @Override 41 | public int getLeafCount(boolean includeLeafDirs) { 42 | return 1; 43 | } 44 | 45 | @Override 46 | public long getFileSize() { 47 | return this.size; 48 | } 49 | 50 | @Override 51 | public Uri getUri() { 52 | return this.uri; 53 | } 54 | 55 | @Override 56 | public DirectoryTreeNode pop(boolean includeDirs) { 57 | return this; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/com/tw/clipshare/protocol/Proto.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2022-2025 H. Thevindu J. Wijesekera 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.tw.clipshare.protocol; 26 | 27 | import com.tw.clipshare.netConnection.ServerConnection; 28 | import com.tw.clipshare.platformUtils.AndroidUtils; 29 | import com.tw.clipshare.platformUtils.DataContainer; 30 | import com.tw.clipshare.platformUtils.StatusNotifier; 31 | 32 | public abstract class Proto { 33 | protected final ProtoMethods protoMethods; 34 | public final DataContainer dataContainer; 35 | 36 | protected Proto(ServerConnection serverConnection, AndroidUtils utils, StatusNotifier notifier) { 37 | this.dataContainer = new DataContainer(); 38 | this.protoMethods = new ProtoMethods(serverConnection, utils, notifier, dataContainer); 39 | } 40 | 41 | public void setStatusNotifier(StatusNotifier notifier) { 42 | this.protoMethods.setStatusNotifier(notifier); 43 | } 44 | 45 | /** Close the connection used for communicating with the server */ 46 | public void close() { 47 | this.protoMethods.close(); 48 | } 49 | 50 | public abstract boolean getText(); 51 | 52 | public abstract boolean sendText(String text); 53 | 54 | public abstract boolean getFile(); 55 | 56 | public abstract boolean sendFile(); 57 | 58 | public abstract boolean getImage(); 59 | 60 | public abstract String checkInfo(); 61 | 62 | public void requestStop() { 63 | this.protoMethods.requestStop(); 64 | } 65 | 66 | public boolean isStopped() { 67 | return this.protoMethods.isStopped(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/com/tw/clipshare/protocol/ProtoV1.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2022-2025 H. Thevindu J. Wijesekera 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.tw.clipshare.protocol; 26 | 27 | import com.tw.clipshare.netConnection.ServerConnection; 28 | import com.tw.clipshare.platformUtils.AndroidUtils; 29 | import com.tw.clipshare.platformUtils.StatusNotifier; 30 | 31 | public class ProtoV1 extends Proto { 32 | 33 | ProtoV1(ServerConnection serverConnection, AndroidUtils utils, StatusNotifier notifier) { 34 | super(serverConnection, utils, notifier); 35 | } 36 | 37 | @Override 38 | public boolean getText() { 39 | return this.protoMethods.v1_getText(); 40 | } 41 | 42 | @Override 43 | public boolean sendText(String text) { 44 | return this.protoMethods.v1_sendText(text); 45 | } 46 | 47 | @Override 48 | public boolean getFile() { 49 | return this.protoMethods.v1_getFiles(); 50 | } 51 | 52 | @Override 53 | public boolean sendFile() { 54 | return this.protoMethods.v1_sendFile(); 55 | } 56 | 57 | @Override 58 | public boolean getImage() { 59 | return this.protoMethods.v1_getImage(); 60 | } 61 | 62 | @Override 63 | public String checkInfo() { 64 | return this.protoMethods.v1_checkInfo(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/com/tw/clipshare/protocol/ProtoV2.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2022-2025 H. Thevindu J. Wijesekera 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.tw.clipshare.protocol; 26 | 27 | import com.tw.clipshare.netConnection.ServerConnection; 28 | import com.tw.clipshare.platformUtils.AndroidUtils; 29 | import com.tw.clipshare.platformUtils.StatusNotifier; 30 | 31 | public class ProtoV2 extends Proto { 32 | 33 | ProtoV2(ServerConnection serverConnection, AndroidUtils utils, StatusNotifier notifier) { 34 | super(serverConnection, utils, notifier); 35 | } 36 | 37 | @Override 38 | public boolean getText() { 39 | return this.protoMethods.v1_getText(); 40 | } 41 | 42 | @Override 43 | public boolean sendText(String text) { 44 | return this.protoMethods.v1_sendText(text); 45 | } 46 | 47 | @Override 48 | public boolean getFile() { 49 | return this.protoMethods.v2_getFiles(); 50 | } 51 | 52 | @Override 53 | public boolean sendFile() { 54 | return this.protoMethods.v2_sendFiles(); 55 | } 56 | 57 | @Override 58 | public boolean getImage() { 59 | return this.protoMethods.v1_getImage(); 60 | } 61 | 62 | @Override 63 | public String checkInfo() { 64 | return this.protoMethods.v1_checkInfo(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/com/tw/clipshare/protocol/ProtoV3.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2022-2025 H. Thevindu J. Wijesekera 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.tw.clipshare.protocol; 26 | 27 | import com.tw.clipshare.netConnection.ServerConnection; 28 | import com.tw.clipshare.platformUtils.AndroidUtils; 29 | import com.tw.clipshare.platformUtils.StatusNotifier; 30 | 31 | public class ProtoV3 extends Proto { 32 | 33 | ProtoV3(ServerConnection serverConnection, AndroidUtils utils, StatusNotifier notifier) { 34 | super(serverConnection, utils, notifier); 35 | } 36 | 37 | @Override 38 | public boolean getText() { 39 | return this.protoMethods.v1_getText(); 40 | } 41 | 42 | @Override 43 | public boolean sendText(String text) { 44 | return this.protoMethods.v1_sendText(text); 45 | } 46 | 47 | @Override 48 | public boolean getFile() { 49 | return this.protoMethods.v3_getFiles(); 50 | } 51 | 52 | @Override 53 | public boolean sendFile() { 54 | return this.protoMethods.v3_sendFiles(); 55 | } 56 | 57 | @Override 58 | public boolean getImage() { 59 | return this.protoMethods.v1_getImage(); 60 | } 61 | 62 | public boolean getCopiedImage() { 63 | return this.protoMethods.v3_getCopiedImage(); 64 | } 65 | 66 | public boolean getScreenshot(int display) { 67 | return this.protoMethods.v3_getScreenshot(display); 68 | } 69 | 70 | @Override 71 | public String checkInfo() { 72 | return this.protoMethods.v1_checkInfo(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/src/main/java/com/tw/clipshare/protocol/ProtocolSelector.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2022-2025 H. Thevindu J. Wijesekera 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.tw.clipshare.protocol; 26 | 27 | import com.tw.clipshare.Utils; 28 | import com.tw.clipshare.netConnection.ServerConnection; 29 | import com.tw.clipshare.platformUtils.AndroidUtils; 30 | import com.tw.clipshare.platformUtils.StatusNotifier; 31 | import java.net.ProtocolException; 32 | 33 | public class ProtocolSelector { 34 | private static final byte PROTO_MIN = 1; 35 | public static final byte PROTO_MAX = 3; 36 | 37 | private ProtocolSelector() {} 38 | 39 | public static Proto getProto( 40 | ServerConnection connection, AndroidUtils utils, StatusNotifier notifier) 41 | throws ProtocolException { 42 | if (connection == null) { 43 | return null; 44 | } 45 | byte[] proto_v = {PROTO_MAX}; 46 | if (connection.send(proto_v)) { 47 | return null; 48 | } 49 | if (connection.receive(proto_v)) { 50 | return null; 51 | } 52 | int selectedProto = PROTO_MAX; 53 | if (proto_v[0] == Utils.PROTOCOL_OBSOLETE) { 54 | throw new ProtocolException("Obsolete client"); 55 | } else if (proto_v[0] == Utils.PROTOCOL_UNKNOWN) { 56 | byte[] serverProto = new byte[1]; 57 | if (connection.receive(serverProto)) { 58 | return null; 59 | } 60 | byte serverMaxProto = serverProto[0]; 61 | if (serverMaxProto < PROTO_MIN) { 62 | serverProto[0] = 0; 63 | connection.send(serverProto); 64 | throw new ProtocolException("Obsolete server"); 65 | } 66 | if (acceptProto(connection, serverMaxProto)) { 67 | return null; 68 | } 69 | selectedProto = serverMaxProto; 70 | } else if (proto_v[0] != Utils.PROTOCOL_SUPPORTED) { 71 | return null; 72 | } 73 | return switch (selectedProto) { 74 | case 1 -> new ProtoV1(connection, utils, notifier); 75 | case 2 -> new ProtoV2(connection, utils, notifier); 76 | case 3 -> new ProtoV3(connection, utils, notifier); 77 | default -> throw new ProtocolException("Unknown protocol"); 78 | }; 79 | } 80 | 81 | /** 82 | * Accept the protocol and acknowledge the server 83 | * 84 | * @param connection Server connection 85 | * @param proto protocol version 86 | * @return false on success or true on error 87 | */ 88 | private static boolean acceptProto(ServerConnection connection, byte proto) { 89 | byte[] proto_v = new byte[1]; 90 | proto_v[0] = proto; 91 | return connection.send(proto_v); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_insecure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thevindu-w/clip_share_client/7c1e4a8dd112c295d5bb98eb2c4aaf7c0ab58e4a/app/src/main/res/drawable-hdpi/ic_insecure.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_secure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thevindu-w/clip_share_client/7c1e4a8dd112c295d5bb98eb2c4aaf7c0ab58e4a/app/src/main/res/drawable-hdpi/ic_secure.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_insecure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thevindu-w/clip_share_client/7c1e4a8dd112c295d5bb98eb2c4aaf7c0ab58e4a/app/src/main/res/drawable-mdpi/ic_insecure.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_secure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thevindu-w/clip_share_client/7c1e4a8dd112c295d5bb98eb2c4aaf7c0ab58e4a/app/src/main/res/drawable-mdpi/ic_secure.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_insecure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thevindu-w/clip_share_client/7c1e4a8dd112c295d5bb98eb2c4aaf7c0ab58e4a/app/src/main/res/drawable-xhdpi/ic_insecure.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_secure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thevindu-w/clip_share_client/7c1e4a8dd112c295d5bb98eb2c4aaf7c0ab58e4a/app/src/main/res/drawable-xhdpi/ic_secure.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_insecure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thevindu-w/clip_share_client/7c1e4a8dd112c295d5bb98eb2c4aaf7c0ab58e4a/app/src/main/res/drawable-xxhdpi/ic_insecure.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_secure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thevindu-w/clip_share_client/7c1e4a8dd112c295d5bb98eb2c4aaf7c0ab58e4a/app/src/main/res/drawable-xxhdpi/ic_secure.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_insecure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thevindu-w/clip_share_client/7c1e4a8dd112c295d5bb98eb2c4aaf7c0ab58e4a/app/src/main/res/drawable-xxxhdpi/ic_insecure.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_secure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thevindu-w/clip_share_client/7c1e4a8dd112c295d5bb98eb2c4aaf7c0ab58e4a/app/src/main/res/drawable-xxxhdpi/ic_secure.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/clip_share_icon_mono.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 37 | 44 | 47 | 54 | 61 | 65 | 68 | 72 | 75 | 76 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_download_icon.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_upload_icon.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/open_icon.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 16 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/open_icon_resized.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/share_icon.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 16 | 21 | 26 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/share_icon_resized.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/layout-v26/list_element.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 20 | 21 | 35 | 36 | 45 | -------------------------------------------------------------------------------- /app/src/main/res/layout-v26/popup_elem.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_element.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 21 | 22 | 36 | 37 | 46 | -------------------------------------------------------------------------------- /app/src/main/res/layout/popup_display.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 17 | 18 | 23 | 24 | 31 | 32 | 41 | 42 | 43 | 48 | 49 | 54 | 55 |