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