├── app
├── src
│ ├── .gitignore
│ ├── main
│ │ ├── res
│ │ │ ├── mipmap-anydpi
│ │ │ ├── drawable-hdpi
│ │ │ │ ├── ic_secure.png
│ │ │ │ └── ic_insecure.png
│ │ │ ├── drawable-mdpi
│ │ │ │ ├── ic_secure.png
│ │ │ │ └── ic_insecure.png
│ │ │ ├── drawable-xhdpi
│ │ │ │ ├── ic_insecure.png
│ │ │ │ └── ic_secure.png
│ │ │ ├── drawable-xxhdpi
│ │ │ │ ├── ic_secure.png
│ │ │ │ └── ic_insecure.png
│ │ │ ├── drawable-xxxhdpi
│ │ │ │ ├── ic_secure.png
│ │ │ │ └── ic_insecure.png
│ │ │ ├── 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
│ │ │ │ ├── ic_launcher_background.xml
│ │ │ │ ├── themes.xml
│ │ │ │ ├── colors.xml
│ │ │ │ └── strings.xml
│ │ │ ├── xml
│ │ │ │ └── provider_paths.xml
│ │ │ ├── drawable
│ │ │ │ ├── open_icon_resized.xml
│ │ │ │ ├── share_icon_resized.xml
│ │ │ │ ├── history_icon_resized.xml
│ │ │ │ ├── ic_upload_icon.xml
│ │ │ │ ├── ic_download_icon.xml
│ │ │ │ ├── history_icon.xml
│ │ │ │ ├── open_icon.xml
│ │ │ │ ├── share_icon.xml
│ │ │ │ └── clip_share_icon_mono.xml
│ │ │ ├── layout
│ │ │ │ ├── activity_invisible.xml
│ │ │ │ ├── tunnel_switch.xml
│ │ │ │ ├── popup_elem.xml
│ │ │ │ ├── popup_servers.xml
│ │ │ │ ├── list_element.xml
│ │ │ │ ├── popup_reset.xml
│ │ │ │ └── popup_display.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── menu
│ │ │ │ ├── settings_action_bar.xml
│ │ │ │ └── action_bar.xml
│ │ │ ├── values-night
│ │ │ │ ├── themes.xml
│ │ │ │ └── colors.xml
│ │ │ └── layout-v26
│ │ │ │ ├── popup_elem.xml
│ │ │ │ └── list_element.xml
│ │ ├── ic_launcher-playstore.png
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── tw
│ │ │ │ └── clipshare
│ │ │ │ ├── PendingTask.java
│ │ │ │ ├── netConnection
│ │ │ │ ├── TunnelConnection.java
│ │ │ │ ├── PlainConnection.java
│ │ │ │ ├── ServerConnection.java
│ │ │ │ ├── SecureConnection.java
│ │ │ │ └── TunnelManager.java
│ │ │ │ ├── platformUtils
│ │ │ │ ├── directoryTree
│ │ │ │ │ ├── RegularFile.java
│ │ │ │ │ ├── Directory.java
│ │ │ │ │ └── DirectoryTreeNode.java
│ │ │ │ ├── DataContainer.java
│ │ │ │ ├── AndroidUtils.java
│ │ │ │ └── StatusNotifier.java
│ │ │ │ ├── InvisibleActivity.java
│ │ │ │ ├── protocol
│ │ │ │ ├── ProtoV1.java
│ │ │ │ ├── ProtoV2.java
│ │ │ │ ├── ProtoV3.java
│ │ │ │ ├── Proto.java
│ │ │ │ └── ProtocolSelector.java
│ │ │ │ ├── CertUtils.java
│ │ │ │ ├── Utils.java
│ │ │ │ ├── TCPScanner.java
│ │ │ │ ├── BackgroundService.java
│ │ │ │ ├── ServerFinder.java
│ │ │ │ └── FileService.java
│ │ └── AndroidManifest.xml
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── tw
│ │ └── clipshare
│ │ ├── UseAppContextTest.java
│ │ ├── netConnection
│ │ └── MockConnection.java
│ │ ├── UtilsTest.java
│ │ ├── proto
│ │ ├── BAOStreamBuilder.java
│ │ └── ProtocolSelectorTest.java
│ │ └── platformUtils
│ │ ├── AndroidUtilsTest.java
│ │ ├── TimeContainerTest.java
│ │ └── StatusNotifierTest.java
├── .gitignore
├── proguard-rules.pro
└── build.gradle
├── .github
├── CODEOWNERS
├── dependabot.yml
└── workflows
│ ├── resources
│ └── release_notes.md
│ ├── build-test.yml
│ ├── check-style.yml
│ └── release.yml
├── fastlane
└── metadata
│ └── android
│ └── en-US
│ ├── title.txt
│ ├── short_description.txt
│ ├── images
│ ├── icon.png
│ ├── featureGraphic.png
│ └── phoneScreenshots
│ │ ├── 1.jpg
│ │ ├── 2.jpg
│ │ ├── 3.jpg
│ │ ├── 4.jpg
│ │ └── 5.jpg
│ └── full_description.txt
├── settings.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── .gitattributes
├── .yamllint
├── LICENSE
├── gradle.properties
├── gradlew.bat
└── gradlew
/app/src/.gitignore:
--------------------------------------------------------------------------------
1 | /test
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @thevindu-w
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/title.txt:
--------------------------------------------------------------------------------
1 | ClipShare
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /release
3 | /latest
4 | /legacy
5 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = "ClipShare"
2 | include ':app'
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/short_description.txt:
--------------------------------------------------------------------------------
1 | Share clipboard between devices. Securely share files, text, and screenshots.
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevindu-w/clip_share_client/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevindu-w/clip_share_client/HEAD/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_secure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevindu-w/clip_share_client/HEAD/app/src/main/res/drawable-hdpi/ic_secure.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_secure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevindu-w/clip_share_client/HEAD/app/src/main/res/drawable-mdpi/ic_secure.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_insecure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevindu-w/clip_share_client/HEAD/app/src/main/res/drawable-hdpi/ic_insecure.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_insecure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevindu-w/clip_share_client/HEAD/app/src/main/res/drawable-mdpi/ic_insecure.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_insecure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevindu-w/clip_share_client/HEAD/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/HEAD/app/src/main/res/drawable-xhdpi/ic_secure.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_secure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevindu-w/clip_share_client/HEAD/app/src/main/res/drawable-xxhdpi/ic_secure.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_secure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevindu-w/clip_share_client/HEAD/app/src/main/res/drawable-xxxhdpi/ic_secure.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevindu-w/clip_share_client/HEAD/fastlane/metadata/android/en-US/images/icon.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_insecure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevindu-w/clip_share_client/HEAD/app/src/main/res/drawable-xxhdpi/ic_insecure.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_insecure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevindu-w/clip_share_client/HEAD/app/src/main/res/drawable-xxxhdpi/ic_insecure.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevindu-w/clip_share_client/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevindu-w/clip_share_client/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevindu-w/clip_share_client/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevindu-w/clip_share_client/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/featureGraphic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevindu-w/clip_share_client/HEAD/fastlane/metadata/android/en-US/images/featureGraphic.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevindu-w/clip_share_client/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #E8E8E8
4 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/provider_paths.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevindu-w/clip_share_client/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevindu-w/clip_share_client/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevindu-w/clip_share_client/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/3.jpg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevindu-w/clip_share_client/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/4.jpg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevindu-w/clip_share_client/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/5.jpg
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/open_icon_resized.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/share_icon_resized.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/history_icon_resized.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_invisible.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/settings_action_bar.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_upload_icon.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFFF
4 | #5000
5 | #FFFF
6 | #FDDD
7 | #FCCF
8 | #FDDE
9 | #F0C0
10 | #FF00
11 | #F444
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #F000
4 | #5FFF
5 | #F000
6 | #F333
7 | #F115
8 | #F223
9 | #F0D0
10 | #FF11
11 | #FBBB
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_download_icon.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/history_icon.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/tunnel_switch.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
17 |
--------------------------------------------------------------------------------
/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/main/res/drawable/open_icon.xml:
--------------------------------------------------------------------------------
1 |
6 |
11 |
16 |
21 |
22 |
--------------------------------------------------------------------------------
/.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 | **Changes:**
10 | - Add a new settings option to reset settings.
11 | - Minor bug fixes.
12 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/main/res/menu/action_bar.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
12 |
13 |
18 |
19 |
24 |
--------------------------------------------------------------------------------
/app/src/main/res/layout-v26/popup_elem.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
21 |
22 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/popup_elem.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
22 |
23 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/share_icon.xml:
--------------------------------------------------------------------------------
1 |
6 |
11 |
16 |
21 |
26 |
31 |
32 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/popup_servers.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
17 |
18 |
24 |
25 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 \
10 | --add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
11 | --add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED \
12 | --add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
13 | --add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
14 | --add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
15 | --add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
16 | # When configured, Gradle will run in incubating parallel mode.
17 | # This option should only be used with decoupled projects. More details, visit
18 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
19 | # org.gradle.parallel=true
20 | # AndroidX package structure to make it clearer which packages are bundled with the
21 | # Android operating system, and which are packaged with your app's APK
22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
23 | android.useAndroidX=true
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/full_description.txt:
--------------------------------------------------------------------------------
1 | Share the clipboard between your phone and desktop. Securely share text, files, and screenshots.
2 | ClipShare is a simple, lightweight, and cross-platform app for sharing copied text, files, and screenshots between an Android phone and a desktop.
3 |
4 | Features
5 |
6 | Share copied text
7 | Share files
8 | Share copied images
9 | Get a screenshot of the desktop to the mobile
10 | Open the received links, files, and images from the app
11 | Re-share received files and images with other apps
12 | Scan the local network to find server devices to connect with
13 | Highly configurable
14 |
15 | Configuration options (of the Android app)
16 |
17 | Enable secure mode and add trusted devices
18 | Option to auto-send files and text
19 | Limit auto-sending only to trusted devices
20 | Option to save previously connected devices' addresses
21 | Automatically close the app if it is kept idle (Idle time duration is adjustable)
22 | Enable IPv6 network scanning
23 | Import and export the above settings to move them to another device or to backup settings
24 |
25 |
26 | This is the Android client of ClipShare. You need the server program running on your desktop to connect with it. The server is available for Windows, macOS, and Linux. You can find the server here on GitHub .
27 | A desktop client for ClipShare is also available on GitHub .
28 |
--------------------------------------------------------------------------------
/app/src/main/res/layout-v26/list_element.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
20 |
21 |
35 |
36 |
45 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/list_element.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
21 |
22 |
36 |
37 |
46 |
--------------------------------------------------------------------------------
/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/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 long size;
33 |
34 | public RegularFile(Uri uri, Directory parent) {
35 | super(null, parent);
36 | this.uri = uri;
37 | }
38 |
39 | public RegularFile(Uri uri) {
40 | this(uri, null);
41 | }
42 |
43 | @Override
44 | public int getLeafCount(boolean includeLeafDirs) {
45 | return 1;
46 | }
47 |
48 | @Override
49 | public long getFileSize() {
50 | return this.size;
51 | }
52 |
53 | @Override
54 | public Uri getUri() {
55 | return this.uri;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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/main/java/com/tw/clipshare/InvisibleActivity.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.Activity;
28 | import android.os.Bundle;
29 | import android.view.WindowManager;
30 | import com.tw.clipshare.platformUtils.AndroidUtils;
31 |
32 | public class InvisibleActivity extends Activity {
33 | @Override
34 | protected void onCreate(Bundle savedInstanceState) {
35 | super.onCreate(savedInstanceState);
36 | try {
37 | setContentView(R.layout.activity_invisible);
38 | WindowManager.LayoutParams params = getWindow().getAttributes();
39 | params.dimAmount = 0;
40 | params.flags =
41 | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
42 | | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
43 | | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
44 | getWindow().setAttributes(params);
45 | AndroidUtils utils = new AndroidUtils(getApplicationContext(), this);
46 | BackgroundService.doUIOperation(utils);
47 | this.finish();
48 | } catch (Exception ignored) {
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/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/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 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 String getFullName() {
46 | LinkedList stack = new LinkedList<>();
47 | DirectoryTreeNode node = this;
48 | do {
49 | stack.push(node);
50 | node = node.parent;
51 | } while (node != null);
52 | StringBuilder builder = new StringBuilder();
53 | boolean first = true;
54 | while (!stack.isEmpty()) {
55 | if (!first) builder.append('/');
56 | first = false;
57 | builder.append(stack.pop().name);
58 | }
59 | return builder.toString();
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/.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@v6
35 | with:
36 | ref: ${{ github.ref }}
37 |
38 | - name: Set up JDK 21
39 | uses: actions/setup-java@v5
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 |
--------------------------------------------------------------------------------
/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/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 = 31900
17 | versionName = "3.19.0"
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.1'
71 | implementation 'com.google.android.material:material:1.13.0'
72 | implementation "androidx.documentfile:documentfile:1.1.0"
73 | implementation 'org.json:json:20250517'
74 | androidTestImplementation 'androidx.test.ext:junit:1.3.0'
75 | androidTestImplementation 'androidx.test:core:1.7.0'
76 | androidTestImplementation 'androidx.test:runner:1.7.0'
77 | androidTestImplementation 'androidx.test:rules:1.7.0'
78 | }
79 |
80 | googleJavaFormat {
81 | toolVersion = "1.25.2"
82 | exclude 'src/test'
83 | }
84 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/.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@v6
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@v5
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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@v6
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: Set up JDK 21
53 | uses: actions/setup-java@v5
54 | with:
55 | java-version: '21'
56 | distribution: 'temurin'
57 |
58 | - name: Set gradle.properties
59 | env:
60 | PROPERTIES_BASE64: ${{ secrets.PROPERTIES_BASE64 }}
61 | run: |
62 | mkdir -p ~/.gradle
63 | echo "$PROPERTIES_BASE64" | base64 --decode >~/.gradle/gradle.properties
64 |
65 | - name: Set KeyStore
66 | env:
67 | KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
68 | run: echo "$KEYSTORE_BASE64" | base64 --decode >keyStore.jks
69 |
70 | - name: Build APK
71 | run: |
72 | chmod +x gradlew
73 | ./gradlew assembleRelease
74 | mkdir release
75 | mv app/build/outputs/apk/latest/release/*.apk "release/clip_share_client.apk"
76 | mv app/build/outputs/apk/legacy/release/*.apk "release/clip_share_client-legacy.apk"
77 | cd release/
78 | sha256sum -b * >SHA2-256SUM
79 |
80 | - name: Generate artifact attestation
81 | uses: actions/attest-build-provenance@v3
82 | with:
83 | subject-path: "release/SHA2-256SUM"
84 |
85 | - name: Create release
86 | env:
87 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
88 | run: |
89 | sed -i "s//${{env.VERSION}}/g" .github/workflows/resources/release_notes.md
90 | cd release
91 | gh release create "${{env.TAG}}" --latest --verify-tag \
92 | --notes-file ../.github/workflows/resources/release_notes.md \
93 | --title "${{env.TITLE}}" *
94 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo. 1>&2
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
48 | echo. 1>&2
49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
50 | echo location of your Java installation. 1>&2
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo. 1>&2
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
62 | echo. 1>&2
63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
64 | echo location of your Java installation. 1>&2
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/popup_reset.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
17 |
18 |
23 |
24 |
28 |
29 |
36 |
37 |
41 |
42 |
43 |
48 |
49 |
53 |
54 |
63 |
64 |
68 |
69 |
78 |
79 |
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
9 |
10 |
13 |
14 |
15 |
22 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
46 |
51 |
52 |
56 |
57 |
61 |
62 |
67 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/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/layout/popup_display.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
18 |
19 |
24 |
25 |
32 |
33 |
42 |
43 |
44 |
49 |
50 |
55 |
56 |
65 |
66 |
71 |
72 |
81 |
82 |
87 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/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/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 androidx.annotation.NonNull;
28 | import com.tw.clipshare.netConnection.PlainConnection;
29 | import com.tw.clipshare.netConnection.SecureConnection;
30 | import com.tw.clipshare.netConnection.ServerConnection;
31 | import com.tw.clipshare.platformUtils.AndroidUtils;
32 | import com.tw.clipshare.protocol.Proto;
33 | import com.tw.clipshare.protocol.ProtocolSelector;
34 | import java.io.InputStream;
35 | import java.net.Inet6Address;
36 | import java.net.InetAddress;
37 | import java.net.ProtocolException;
38 |
39 | public class Utils {
40 | public static final byte PROTOCOL_SUPPORTED = 1;
41 | public static final byte PROTOCOL_OBSOLETE = 2;
42 | public static final byte PROTOCOL_UNKNOWN = 3;
43 |
44 | public static boolean isValidIP(String str) {
45 | try {
46 | if (str == null) return false;
47 | if (str.matches("^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)(\\.(?!$)|$)){4}$")) return true;
48 | if (!str.contains(":")) return false;
49 | //noinspection ResultOfMethodCallIgnored
50 | Inet6Address.getByName(str);
51 | return true;
52 | } catch (Exception ignored) {
53 | }
54 | return false;
55 | }
56 |
57 | /** Opens a ServerConnection. Returns null on error. */
58 | private static ServerConnection getServerConnection(@NonNull String addressStr) {
59 | int retries = 2;
60 | do {
61 | try {
62 | Settings settings = Settings.getInstance();
63 | if (settings.getSecure()) {
64 | InputStream caCertIn = settings.getCACertInputStream();
65 | InputStream clientCertKeyIn = settings.getCertInputStream();
66 | char[] clientPass = settings.getPasswd();
67 | if (clientCertKeyIn == null || clientPass == null) {
68 | return null;
69 | }
70 | String[] acceptedServers = settings.getTrustedList().toArray(new String[0]);
71 | return new SecureConnection(
72 | InetAddress.getByName(addressStr),
73 | settings.getPortSecure(),
74 | caCertIn,
75 | clientCertKeyIn,
76 | clientPass,
77 | acceptedServers);
78 | } else {
79 | return new PlainConnection(InetAddress.getByName(addressStr), settings.getPort());
80 | }
81 | } catch (Exception ignored) {
82 | }
83 | } while (retries-- > 0);
84 | return null;
85 | }
86 |
87 | public static Proto getProtoWrapper(@NonNull String address, AndroidUtils utils)
88 | throws ProtocolException {
89 | int retries = 1;
90 | do {
91 | try {
92 | ServerConnection connection = getServerConnection(address);
93 | if (connection == null) continue;
94 | Proto proto = ProtocolSelector.getProto(connection, utils, null);
95 | if (proto != null) return proto;
96 | connection.close();
97 | } catch (ProtocolException ex) {
98 | throw ex;
99 | } catch (Exception ignored) {
100 | }
101 | } while (retries-- > 0);
102 | return null;
103 | }
104 |
105 | private Utils() {}
106 | }
107 |
--------------------------------------------------------------------------------
/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/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/TCPScanner.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 TCPScanner {
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 TCPScanner(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/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | ClipShare
3 | Image
4 | Text
5 | Server:
6 | 192.168.1.2
7 | File
8 | Folder
9 | Scan
10 | Open link
11 | Share
12 | File selected. Press send file button.
13 | File transfer progress
14 | File upload/download progress
15 |
16 | - %d file selected. Press send file button.
17 | - %d files selected. Press send file button.
18 |
19 | No files selected
20 | Settings
21 | Secure mode
22 | Vibration alerts
23 | IPv6 Scan
24 | TCP Scan
25 | Security
26 | Delete
27 | Add
28 | Trusted servers
29 | Saved servers
30 | Auto-send to
31 | Enter server name
32 | Password :
33 | Client Certificate
34 | Name :
35 | Browse
36 | CA Certificate
37 | Auto-send
38 | Auto-send text
39 | Auto-send files
40 | Sending files\n
41 | Tunnel
42 | Text is selected. Press the Send button.\nEnable auto-send text in
43 | Settings to automatically send.
44 |
45 | Ports
46 | Port
47 | Secure Port
48 | UDP Port
49 | 4337
50 | 4338
51 | Display :
52 | Copied Image
53 | Screenshot
54 | Export Settings
55 | Import Settings
56 | Reset Settings
57 | Close app if idle
58 | 120
59 | Auto-close delay (seconds)
60 | Other settings
61 | Expand
62 | Saved addresses
63 | Save addresses
64 | Send
65 | Get
66 | Open File
67 | Auto-scan at start
68 | Address history
69 | Scan network
70 | Send copied text
71 | Send folder
72 | Get copied text
73 | Get copied files
74 | Run in background
75 | Cancel
76 | Reset
77 | Are you sure you need to reset settings to their defaults?
78 | Info
79 |
80 |
--------------------------------------------------------------------------------
/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/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/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 | private 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 | ClipData.Item item = clipboard.getPrimaryClip().getItemAt(0);
91 | CharSequence clipDataSequence = item.getText();
92 | if (clipDataSequence == null) {
93 | return null;
94 | }
95 | return clipDataSequence.toString();
96 | } catch (Exception ignored) {
97 | return null;
98 | }
99 | }
100 |
101 | /**
102 | * Copy the text to the clipboard.
103 | *
104 | * @param text to be copied to the clipboard
105 | */
106 | public void setClipboardText(String text) {
107 | try {
108 | ClipboardManager clipboard = this.getClipboardManager();
109 | ClipData clip = ClipData.newPlainText("clip_share", text);
110 | if (clipboard != null) clipboard.setPrimaryClip(clip);
111 | } catch (Exception ignored) {
112 | }
113 | }
114 |
115 | public void showToast(String message) {
116 | if (this.context == null) return;
117 | try {
118 | long currTime = System.currentTimeMillis();
119 | if (currTime - lastToastTime < 2000) return;
120 | lastToastTime = currTime;
121 | Handler handler = new Handler(Looper.getMainLooper());
122 | handler.post(() -> Toast.makeText(context, message, Toast.LENGTH_SHORT).show());
123 | } catch (Exception ignored) {
124 | }
125 | }
126 |
127 | @SuppressWarnings("deprecation")
128 | public void vibrate() {
129 | try {
130 | if (context == null || !Settings.getInstance().getVibrate()) return;
131 | Vibrator vibrator;
132 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
133 | VibratorManager vibratorManager =
134 | (VibratorManager) context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE);
135 | vibrator = vibratorManager.getDefaultVibrator();
136 | } else {
137 | vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
138 | }
139 |
140 | final int duration = 100;
141 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
142 | vibrator.vibrate(
143 | VibrationEffect.createOneShot(duration, VibrationEffect.DEFAULT_AMPLITUDE));
144 | } else {
145 | vibrator.vibrate(duration);
146 | }
147 | } catch (Exception ignored) {
148 | }
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/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/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/java/com/tw/clipshare/BackgroundService.java:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 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 androidx.annotation.Nullable;
33 | import androidx.core.app.NotificationCompat;
34 | import com.tw.clipshare.platformUtils.AndroidUtils;
35 | import com.tw.clipshare.protocol.Proto;
36 | import java.util.Random;
37 | import java.util.concurrent.ExecutorService;
38 | import java.util.concurrent.Executors;
39 |
40 | public class BackgroundService extends Service {
41 | private static final int GET_TEXT = 1;
42 | private static final int SEND_TEXT = 2;
43 | private static volatile boolean running = false;
44 | private static volatile int command;
45 | private static final Object LOCK = new Object();
46 | private static volatile String copiedText = null;
47 | private static final Object TEXT_LOCK = new Object();
48 |
49 | @Nullable
50 | @Override
51 | public IBinder onBind(Intent intent) {
52 | return null;
53 | }
54 |
55 | @Override
56 | public int onStartCommand(Intent intent, int flags, int startId) {
57 | try {
58 | Intent notificationIntent = new Intent(this, BackgroundService.class);
59 | PendingIntent pendingIntent =
60 | PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE);
61 | Context ctx = getApplicationContext();
62 | Random rnd = new Random();
63 | int id = Math.abs(rnd.nextInt(Integer.MAX_VALUE - 1)) + 1;
64 | Intent intentStop = new Intent(ctx, EventReceiver.class);
65 | intentStop.putExtra("stop", true);
66 | PendingIntent pendingIntentStop =
67 | PendingIntent.getBroadcast(ctx, 0, intentStop, PendingIntent.FLAG_IMMUTABLE);
68 | Intent intentGet = new Intent(ctx, EventReceiver.class);
69 | intentGet.putExtra("command", GET_TEXT);
70 | PendingIntent pendingIntentGet =
71 | PendingIntent.getBroadcast(ctx, GET_TEXT, intentGet, PendingIntent.FLAG_IMMUTABLE);
72 | Intent intentSend = new Intent(ctx, EventReceiver.class);
73 | intentSend.putExtra("command", SEND_TEXT);
74 | PendingIntent pendingIntentSend =
75 | PendingIntent.getBroadcast(ctx, SEND_TEXT, intentSend, PendingIntent.FLAG_IMMUTABLE);
76 | NotificationCompat.Builder builder =
77 | new NotificationCompat.Builder(ctx, FileService.CHANNEL_ID)
78 | .setContentIntent(pendingIntent)
79 | .setContentTitle(getApplicationContext().getString(R.string.app_name))
80 | .setSmallIcon(R.drawable.clip_share_icon_mono)
81 | .addAction(0, "Get", pendingIntentGet)
82 | .addAction(0, "Send", pendingIntentSend)
83 | .addAction(0, "Stop", pendingIntentStop);
84 | running = true;
85 | command = 0;
86 | startForeground(id, builder.build());
87 | (new Thread(
88 | () -> {
89 | try {
90 | processCommands();
91 | } catch (Exception ignored) {
92 | }
93 | }))
94 | .start();
95 | } catch (Exception ignored) {
96 | }
97 | return START_REDELIVER_INTENT;
98 | }
99 |
100 | private void processCommands() throws InterruptedException {
101 | while (running) {
102 | synchronized (LOCK) {
103 | LOCK.wait();
104 | }
105 | if (command == GET_TEXT) getText();
106 | else if (command == SEND_TEXT) sendText();
107 | }
108 | stopForeground(STOP_FOREGROUND_REMOVE);
109 | }
110 |
111 | private void getText() {
112 | if (ClipShareActivity.lastAddress == null) return;
113 | ExecutorService executorService = Executors.newSingleThreadExecutor();
114 | Runnable runnable =
115 | () -> {
116 | try {
117 | AndroidUtils utils = new AndroidUtils(getApplicationContext(), null);
118 | Proto proto = Utils.getProtoWrapper(ClipShareActivity.lastAddress, utils);
119 | if (proto == null) {
120 | utils.showToast("Couldn't connect");
121 | return;
122 | }
123 | boolean status = proto.getText();
124 | proto.close();
125 | String text = null;
126 | if (status) text = proto.dataContainer.getString();
127 | if (text == null) {
128 | utils.showToast("Couldn't get text");
129 | return;
130 | }
131 | synchronized (TEXT_LOCK) {
132 | copiedText = text;
133 | command = GET_TEXT;
134 | }
135 | Intent intent = new Intent(this, InvisibleActivity.class);
136 | intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
137 | startActivity(intent);
138 | utils.vibrate();
139 | } catch (Exception ignored) {
140 | }
141 | };
142 | executorService.submit(runnable);
143 | }
144 |
145 | private void sendText() {
146 | if (ClipShareActivity.lastAddress == null) return;
147 | ExecutorService executorService = Executors.newSingleThreadExecutor();
148 | Runnable sendClip =
149 | () -> {
150 | try {
151 | String text;
152 | synchronized (TEXT_LOCK) {
153 | if (copiedText == null) TEXT_LOCK.wait(5000);
154 | text = copiedText;
155 | copiedText = null;
156 | command = 0;
157 | }
158 | if (text == null) return;
159 | AndroidUtils utils = new AndroidUtils(getApplicationContext(), null);
160 | Proto proto = Utils.getProtoWrapper(ClipShareActivity.lastAddress, utils);
161 | if (proto == null) {
162 | utils.showToast("Couldn't connect");
163 | return;
164 | }
165 | proto.sendText(text);
166 | proto.close();
167 | utils.vibrate();
168 | } catch (Exception ignored) {
169 | }
170 | };
171 | executorService.submit(sendClip);
172 | try {
173 | Intent intent = new Intent(this, InvisibleActivity.class);
174 | intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
175 | startActivity(intent);
176 | } catch (Exception ignored) {
177 | }
178 | }
179 |
180 | public static void doUIOperation(AndroidUtils utils) {
181 | if (command == GET_TEXT) {
182 | String text;
183 | synchronized (TEXT_LOCK) {
184 | text = copiedText;
185 | command = 0;
186 | copiedText = null;
187 | }
188 | utils.setClipboardText(text);
189 | } else if (command == SEND_TEXT) {
190 | String text = utils.getClipboardText();
191 | (new Thread(
192 | () -> {
193 | synchronized (TEXT_LOCK) {
194 | copiedText = text;
195 | TEXT_LOCK.notifyAll();
196 | }
197 | }))
198 | .start();
199 | }
200 | }
201 |
202 | public static class EventReceiver extends BroadcastReceiver {
203 | @Override
204 | public void onReceive(Context ctx, Intent receivedIntent) {
205 | try {
206 | int cmd = 0;
207 | if (receivedIntent.getBooleanExtra("stop", false)) running = false;
208 | else cmd = receivedIntent.getIntExtra("command", 0);
209 | synchronized (LOCK) {
210 | command = cmd;
211 | LOCK.notifyAll();
212 | }
213 | } catch (Exception ignored) {
214 | }
215 | }
216 | }
217 | }
218 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | # This is normally unused
84 | # shellcheck disable=SC2034
85 | APP_BASE_NAME=${0##*/}
86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
88 |
89 | # Use the maximum available, or set MAX_FD != -1 to use that value.
90 | MAX_FD=maximum
91 |
92 | warn () {
93 | echo "$*"
94 | } >&2
95 |
96 | die () {
97 | echo
98 | echo "$*"
99 | echo
100 | exit 1
101 | } >&2
102 |
103 | # OS specific support (must be 'true' or 'false').
104 | cygwin=false
105 | msys=false
106 | darwin=false
107 | nonstop=false
108 | case "$( uname )" in #(
109 | CYGWIN* ) cygwin=true ;; #(
110 | Darwin* ) darwin=true ;; #(
111 | MSYS* | MINGW* ) msys=true ;; #(
112 | NONSTOP* ) nonstop=true ;;
113 | esac
114 |
115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
116 |
117 |
118 | # Determine the Java command to use to start the JVM.
119 | if [ -n "$JAVA_HOME" ] ; then
120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
121 | # IBM's JDK on AIX uses strange locations for the executables
122 | JAVACMD=$JAVA_HOME/jre/sh/java
123 | else
124 | JAVACMD=$JAVA_HOME/bin/java
125 | fi
126 | if [ ! -x "$JAVACMD" ] ; then
127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
128 |
129 | Please set the JAVA_HOME variable in your environment to match the
130 | location of your Java installation."
131 | fi
132 | else
133 | JAVACMD=java
134 | if ! command -v java >/dev/null 2>&1
135 | then
136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 | fi
142 |
143 | # Increase the maximum file descriptors if we can.
144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
145 | case $MAX_FD in #(
146 | max*)
147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
148 | # shellcheck disable=SC2039,SC3045
149 | MAX_FD=$( ulimit -H -n ) ||
150 | warn "Could not query maximum file descriptor limit"
151 | esac
152 | case $MAX_FD in #(
153 | '' | soft) :;; #(
154 | *)
155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
156 | # shellcheck disable=SC2039,SC3045
157 | ulimit -n "$MAX_FD" ||
158 | warn "Could not set maximum file descriptor limit to $MAX_FD"
159 | esac
160 | fi
161 |
162 | # Collect all arguments for the java command, stacking in reverse order:
163 | # * args from the command line
164 | # * the main class name
165 | # * -classpath
166 | # * -D...appname settings
167 | # * --module-path (only if needed)
168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
169 |
170 | # For Cygwin or MSYS, switch paths to Windows format before running java
171 | if "$cygwin" || "$msys" ; then
172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
174 |
175 | JAVACMD=$( cygpath --unix "$JAVACMD" )
176 |
177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
178 | for arg do
179 | if
180 | case $arg in #(
181 | -*) false ;; # don't mess with options #(
182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
183 | [ -e "$t" ] ;; #(
184 | *) false ;;
185 | esac
186 | then
187 | arg=$( cygpath --path --ignore --mixed "$arg" )
188 | fi
189 | # Roll the args list around exactly as many times as the number of
190 | # args, so each arg winds up back in the position where it started, but
191 | # possibly modified.
192 | #
193 | # NB: a `for` loop captures its iteration list before it begins, so
194 | # changing the positional parameters here affects neither the number of
195 | # iterations, nor the values presented in `arg`.
196 | shift # remove old arg
197 | set -- "$@" "$arg" # push replacement arg
198 | done
199 | fi
200 |
201 |
202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
204 |
205 | # Collect all arguments for the java command:
206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
207 | # and any embedded shellness will be escaped.
208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
209 | # treated as '${Hostname}' itself on the command line.
210 |
211 | set -- \
212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
213 | -classpath "$CLASSPATH" \
214 | org.gradle.wrapper.GradleWrapperMain \
215 | "$@"
216 |
217 | # Stop when "xargs" is not available.
218 | if ! command -v xargs >/dev/null 2>&1
219 | then
220 | die "xargs is not available"
221 | fi
222 |
223 | # Use "xargs" to parse quoted args.
224 | #
225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
226 | #
227 | # In Bash we could simply go:
228 | #
229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
230 | # set -- "${ARGS[@]}" "$@"
231 | #
232 | # but POSIX shell has neither arrays nor command substitution, so instead we
233 | # post-process each arg (as a line of input to sed) to backslash-escape any
234 | # character that might be a shell metacharacter, then use eval to reverse
235 | # that process (while maintaining the separation between arguments), and wrap
236 | # the whole thing up as a single "set" statement.
237 | #
238 | # This will of course break if any of these variables contains a newline or
239 | # an unmatched quote.
240 | #
241 |
242 | eval "set -- $(
243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
244 | xargs -n1 |
245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
246 | tr '\n' ' '
247 | )" '"$@"'
248 |
249 | exec "$JAVACMD" "$@"
250 |
--------------------------------------------------------------------------------
/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/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 | socket.close();
147 | if (!serverAddresses.isEmpty()) parent.interrupt();
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 | socket.close();
189 | if (!serverAddresses.isEmpty()) parent.interrupt();
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 | Settings settings = Settings.getInstance();
204 | for (InterfaceAddress intAddress : addresses) {
205 | InetAddress address = intAddress.getAddress();
206 | if (address instanceof Inet6Address && settings.getScanIPv6()) {
207 | myAddresses.add(address);
208 | }
209 | }
210 | for (InterfaceAddress intAddress : addresses) {
211 | try {
212 | InetAddress address = intAddress.getAddress();
213 | if (address instanceof Inet4Address) {
214 | InetAddress broadcastAddress = intAddress.getBroadcast();
215 | if (broadcastAddress instanceof Inet4Address) {
216 | scanBroadcast((Inet4Address) broadcastAddress, (Inet4Address) address);
217 | }
218 | if (!settings.getScanTCP()) continue;
219 | short subLen = intAddress.getNetworkPrefixLength();
220 | if (subLen <= 22) subLen = 23;
221 | TCPScanner TCPScanner = new TCPScanner(address, port, subLen);
222 | InetAddress server = TCPScanner.scan(subLen >= 24 ? 32 : 64);
223 | if (server != null) {
224 | String addressStr = server.getHostAddress();
225 | if (addressStr != null) {
226 | addressStr = addressStr.intern();
227 | synchronized (serverAddresses) {
228 | serverAddresses.put(addressStr, server);
229 | }
230 | }
231 | break;
232 | }
233 | } else if (address instanceof Inet6Address && settings.getScanIPv6()) {
234 | scanMulticast((Inet6Address) address);
235 | }
236 | } catch (RuntimeException ignored) {
237 | }
238 | }
239 | if (serverAddresses.isEmpty()) Thread.sleep(3000);
240 | } catch (Exception ignored) {
241 | }
242 | }
243 | }
244 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------