├── .gitignore
├── .idea
├── .gitignore
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── compiler.xml
├── deploymentTargetSelector.xml
├── gradle.xml
├── kotlinc.xml
├── migrations.xml
├── misc.xml
├── runConfigurations.xml
├── studiobot.xml
└── vcs.xml
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── eu
│ │ └── davidmayr
│ │ └── pcremote
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── ic_launcher-playstore.png
│ ├── java
│ │ └── eu
│ │ │ └── davidmayr
│ │ │ └── pcremote
│ │ │ ├── BarcodeAnalyzer.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── PermissionRequestDialog.kt
│ │ │ ├── QRCodeScannerScreen.kt
│ │ │ ├── SPenViewModel.kt
│ │ │ ├── WebSocketViewModel.kt
│ │ │ └── ui
│ │ │ └── theme
│ │ │ ├── Color.kt
│ │ │ ├── Theme.kt
│ │ │ └── Type.kt
│ └── res
│ │ ├── drawable
│ │ ├── ic_launcher_background.xml
│ │ └── ic_launcher_foreground.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── values-de
│ │ └── strings.xml
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── ic_launcher_background.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ │ └── xml
│ │ ├── backup_rules.xml
│ │ └── data_extraction_rules.xml
│ └── test
│ └── java
│ └── eu
│ └── davidmayr
│ └── pcremote
│ └── ExampleUnitTest.kt
├── build.gradle.kts
├── desktop-app
├── .run
│ └── desktop.run.xml
├── build.gradle.kts
├── gradle.properties
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
└── src
│ └── main
│ ├── kotlin
│ ├── LaserPointer.kt
│ ├── Main.kt
│ └── WebServer.kt
│ └── resources
│ ├── icon.ico
│ └── icon.svg
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle.kts
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 |
17 | desktop-app/build
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | xmlns:android
18 |
19 | ^$
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | xmlns:.*
29 |
30 | ^$
31 |
32 |
33 | BY_NAME
34 |
35 |
36 |
37 |
38 |
39 |
40 | .*:id
41 |
42 | http://schemas.android.com/apk/res/android
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | .*:name
52 |
53 | http://schemas.android.com/apk/res/android
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | name
63 |
64 | ^$
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | style
74 |
75 | ^$
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | .*
85 |
86 | ^$
87 |
88 |
89 | BY_NAME
90 |
91 |
92 |
93 |
94 |
95 |
96 | .*
97 |
98 | http://schemas.android.com/apk/res/android
99 |
100 |
101 | ANDROID_ATTRIBUTE_ORDER
102 |
103 |
104 |
105 |
106 |
107 |
108 | .*
109 |
110 | .*
111 |
112 |
113 | BY_NAME
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/deploymentTargetSelector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
30 |
31 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/.idea/studiobot.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # S Pen PC Remote
2 |
3 | Connect to your PC using your Galaxy Ultra phone and use the S Pen for presentations.
4 |
5 | S Pen button single click = Next slide
6 | S Pen button double click = Previous slide
7 | S Pen button pressed = Laser Pointer
8 |
9 | Note: This is a proof of concept and the code is quite messy. This is also my first native Android app ever.
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.application)
3 | alias(libs.plugins.kotlin.android)
4 | alias(libs.plugins.kotlin.compose)
5 | id("kotlin-kapt")
6 | }
7 |
8 | android {
9 | namespace = "eu.davidmayr.pcremote"
10 | compileSdk = 34
11 |
12 | defaultConfig {
13 | applicationId = "eu.davidmayr.pcremote"
14 | minSdk = 34
15 | targetSdk = 34
16 | versionCode = 1
17 | versionName = "1.0"
18 |
19 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
20 | }
21 |
22 | buildTypes {
23 | release {
24 | isMinifyEnabled = false
25 | proguardFiles(
26 | getDefaultProguardFile("proguard-android-optimize.txt"),
27 | "proguard-rules.pro"
28 | )
29 | }
30 | }
31 | compileOptions {
32 | sourceCompatibility = JavaVersion.VERSION_11
33 | targetCompatibility = JavaVersion.VERSION_11
34 | }
35 | kotlinOptions {
36 | jvmTarget = "11"
37 | }
38 | buildFeatures {
39 | compose = true
40 | }
41 | }
42 |
43 | dependencies {
44 | implementation(fileTree(mapOf("include" to "*.jar", "dir" to "libs")))
45 |
46 | implementation(libs.androidx.core.ktx)
47 | implementation(libs.androidx.lifecycle.runtime.ktx)
48 | implementation(libs.androidx.activity.compose)
49 | implementation(platform(libs.androidx.compose.bom))
50 | implementation(libs.androidx.ui)
51 | implementation(libs.androidx.ui.graphics)
52 | implementation(libs.androidx.ui.tooling.preview)
53 | implementation(libs.androidx.material3)
54 | testImplementation(libs.junit)
55 | androidTestImplementation(libs.androidx.junit)
56 | androidTestImplementation(libs.androidx.espresso.core)
57 | androidTestImplementation(platform(libs.androidx.compose.bom))
58 | androidTestImplementation(libs.androidx.ui.test.junit4)
59 | debugImplementation(libs.androidx.ui.tooling)
60 | debugImplementation(libs.androidx.ui.test.manifest)
61 | implementation(libs.androidx.camera.core)
62 | implementation(libs.androidx.camera.camera2)
63 | implementation(libs.androidx.camera.lifecycle)
64 | implementation(libs.androidx.camera.view)
65 | implementation(libs.barcode.scanning)
66 | implementation(libs.hilt.navigation.compose)
67 | // https://mvnrepository.com/artifact/com.google.code.gson/gson
68 | implementation("com.google.code.gson:gson:2.11.0")
69 | implementation("com.squareup.okhttp3:okhttp:4.12.0")
70 |
71 | implementation(libs.androidx.navigation.compose)
72 |
73 | }
74 |
75 | kapt {
76 | correctErrorTypes = true
77 | }
78 |
--------------------------------------------------------------------------------
/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/androidTest/java/eu/davidmayr/pcremote/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package eu.davidmayr.pcremote
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("eu.davidmayr.pcremote", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
19 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidmayr/SPen-PCRemote/d6db99a38af7cca524fa24722a13475cfa359f8b/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/java/eu/davidmayr/pcremote/BarcodeAnalyzer.kt:
--------------------------------------------------------------------------------
1 | package eu.davidmayr.pcremote
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.widget.Toast
6 | import androidx.camera.core.ImageAnalysis
7 | import androidx.camera.core.ImageProxy
8 | import com.google.gson.JsonObject
9 | import com.google.gson.JsonParser
10 | import com.google.mlkit.vision.barcode.BarcodeScannerOptions
11 | import com.google.mlkit.vision.barcode.BarcodeScanning
12 | import com.google.mlkit.vision.barcode.common.Barcode
13 | import com.google.mlkit.vision.common.InputImage
14 |
15 | class BarcodeAnalyzer(private val context: Context, val response: (JsonObject) -> Unit) : ImageAnalysis.Analyzer {
16 |
17 | private val options = BarcodeScannerOptions.Builder()
18 | .setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS)
19 | .build()
20 |
21 | private val scanner = BarcodeScanning.getClient(options)
22 |
23 | @SuppressLint("UnsafeOptInUsageError")
24 | override fun analyze(imageProxy: ImageProxy) {
25 | imageProxy.image?.let { image ->
26 | scanner.process(
27 | InputImage.fromMediaImage(
28 | image, imageProxy.imageInfo.rotationDegrees
29 | )
30 | ).addOnSuccessListener { barcode ->
31 | barcode?.takeIf { it.isNotEmpty() }
32 | ?.mapNotNull { it.rawValue }
33 | ?.joinToString(",")
34 | ?.let {
35 | try {
36 | val json = JsonParser.parseString(it).asJsonObject
37 | if(!json.has("type") || json.get("type").asString != "pcmanager/remotconfig")
38 | return@let
39 |
40 | response(json)
41 | } catch (_: Exception) {}
42 | }
43 | }.addOnCompleteListener {
44 | imageProxy.close()
45 | }
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/app/src/main/java/eu/davidmayr/pcremote/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package eu.davidmayr.pcremote
2 |
3 | import android.graphics.Paint
4 | import android.os.Bundle
5 | import android.view.WindowManager
6 | import androidx.activity.ComponentActivity
7 | import androidx.activity.compose.setContent
8 | import androidx.activity.enableEdgeToEdge
9 | import androidx.activity.viewModels
10 | import androidx.compose.foundation.Image
11 | import androidx.compose.foundation.layout.Arrangement
12 | import androidx.compose.foundation.layout.Box
13 | import androidx.compose.foundation.layout.Column
14 | import androidx.compose.foundation.layout.Row
15 | import androidx.compose.foundation.layout.fillMaxHeight
16 | import androidx.compose.foundation.layout.fillMaxSize
17 | import androidx.compose.foundation.layout.fillMaxWidth
18 | import androidx.compose.foundation.layout.padding
19 | import androidx.compose.material.icons.Icons
20 | import androidx.compose.material.icons.filled.Check
21 | import androidx.compose.material.icons.filled.CheckCircle
22 | import androidx.compose.material.icons.filled.Clear
23 | import androidx.compose.material3.Button
24 | import androidx.compose.material3.ExperimentalMaterial3Api
25 | import androidx.compose.material3.Icon
26 | import androidx.compose.material3.Scaffold
27 | import androidx.compose.material3.Switch
28 | import androidx.compose.material3.Text
29 | import androidx.compose.material3.TopAppBar
30 | import androidx.compose.material3.TopAppBarDefaults
31 | import androidx.compose.runtime.Composable
32 | import androidx.compose.ui.Alignment
33 | import androidx.compose.ui.Modifier
34 | import androidx.compose.ui.graphics.Color
35 | import androidx.compose.ui.graphics.ColorFilter
36 | import androidx.compose.ui.platform.LocalContext
37 | import androidx.compose.ui.text.font.FontWeight
38 | import androidx.compose.ui.tooling.preview.Preview
39 | import androidx.compose.ui.unit.dp
40 | import androidx.navigation.NavController
41 | import androidx.navigation.compose.NavHost
42 | import androidx.navigation.compose.composable
43 | import androidx.navigation.compose.rememberNavController
44 | import eu.davidmayr.pcremote.ui.theme.PCRemoteTheme
45 |
46 | class MainActivity : ComponentActivity() {
47 |
48 |
49 | private val webSocketViewModel by viewModels()
50 |
51 | //Needs to be stored here so we can disconnect with context
52 | private val sPenViewModel by viewModels(factoryProducer = {
53 | SPenViewModel.SPenViewModelFactory(webSocketViewModel)
54 | })
55 |
56 |
57 | override fun onCreate(savedInstanceState: Bundle?) {
58 | super.onCreate(savedInstanceState)
59 |
60 | window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
61 |
62 | sPenViewModel.connect(this)
63 | enableEdgeToEdge()
64 | setContent {
65 | PCRemoteTheme {
66 | Navigation(sPenViewModel, webSocketViewModel)
67 | }
68 | }
69 | }
70 |
71 | override fun onDestroy() {
72 | super.onDestroy()
73 | sPenViewModel.disconnect(this)
74 | }
75 |
76 | }
77 |
78 | @Composable
79 | fun Navigation(
80 | sPenViewModel: SPenViewModel,
81 | webSocketViewModel: WebSocketViewModel,
82 | modifier: Modifier = Modifier
83 | ) {
84 | val navController = rememberNavController()
85 |
86 | NavHost(navController, "home") {
87 | composable("home") {
88 | PCRemote(navController, sPenViewModel, webSocketViewModel)
89 | }
90 | composable("scan") {
91 | QRCodeScannerScreen(navController, webSocketViewModel)
92 | }
93 | }
94 | }
95 |
96 |
97 | @OptIn(ExperimentalMaterial3Api::class)
98 | @Composable
99 | fun PCRemote(
100 | navController: NavController,
101 | sPenViewModel: SPenViewModel,
102 | webSocketViewModel: WebSocketViewModel
103 | ) {
104 | val context = LocalContext.current
105 |
106 | Scaffold(
107 | topBar = {
108 | TopAppBar(
109 | colors = TopAppBarDefaults.topAppBarColors(),
110 | title = {
111 | Text(context.getString(R.string.app_name))
112 | },
113 | )
114 | },
115 | modifier = Modifier.fillMaxSize(),
116 | content = { padding ->
117 | Column(
118 | modifier = Modifier.padding(padding).fillMaxWidth().fillMaxHeight(),
119 | verticalArrangement = Arrangement.SpaceBetween,
120 | horizontalAlignment = Alignment.CenterHorizontally
121 | ) {
122 | Column(
123 | horizontalAlignment = Alignment.CenterHorizontally,
124 | modifier = Modifier.padding(start = 16.dp, end = 16.dp)
125 | ) {
126 |
127 | Row(
128 | verticalAlignment = Alignment.CenterVertically,
129 | modifier = Modifier.padding(top = 5.dp, bottom = 20.dp)
130 | ) {
131 | if (!sPenViewModel.isSpenSupported || sPenViewModel.errorState.isNotEmpty()) {
132 | Icon(Icons.Default.Clear, "Failed", modifier = Modifier.padding(end = 10.dp))
133 | Text(context.getString(R.string.spen_unsupported))
134 | } else {
135 | Icon(Icons.Default.CheckCircle, "Success", modifier = Modifier.padding(end = 10.dp))
136 | Text(context.getString(R.string.spen_supported))
137 | }
138 | }
139 |
140 |
141 |
142 | if(!webSocketViewModel.connected) {
143 | Text(context.getString(R.string.same_network))
144 | Button(onClick = {
145 | navController.navigate("scan")
146 | }, modifier = Modifier.padding(top = 10.dp)) {
147 | Text(context.getString(R.string.scan_device))
148 | }
149 | } else {
150 | Row(
151 | verticalAlignment = Alignment.CenterVertically,
152 | modifier = Modifier.padding(top = 5.dp, bottom = 20.dp)
153 | ) {
154 | Icon(Icons.Default.Check, "Connected", modifier = Modifier.padding(end = 10.dp))
155 | Text(context.getString(R.string.connected))
156 | }
157 |
158 | Button(onClick = {
159 | webSocketViewModel.closeConnection()
160 | }) {
161 | Text(context.getString(R.string.disconnect))
162 | }
163 | }
164 | }
165 |
166 | Text("Copyright 2024 - David Mayr")
167 |
168 | }
169 | }
170 | )
171 |
172 | }
173 |
174 | @Preview(showSystemUi = true)
175 | @Composable
176 | fun DefaultPreview() {
177 | val nav = rememberNavController()
178 | PCRemoteTheme {
179 | PCRemote(nav, SPenViewModel(WebSocketViewModel()), WebSocketViewModel())
180 | }
181 | }
--------------------------------------------------------------------------------
/app/src/main/java/eu/davidmayr/pcremote/PermissionRequestDialog.kt:
--------------------------------------------------------------------------------
1 | package eu.davidmayr.pcremote
2 |
3 | import android.content.Context
4 | import android.content.pm.PackageManager
5 | import androidx.activity.compose.rememberLauncherForActivityResult
6 | import androidx.activity.result.contract.ActivityResultContracts
7 | import androidx.compose.runtime.*
8 | import androidx.compose.ui.platform.LocalContext
9 | import androidx.core.content.ContextCompat
10 | import kotlinx.coroutines.delay
11 |
12 | @Composable
13 | fun PermissionRequestDialog(
14 | permission: String,
15 | onResult: (Boolean) -> Unit,
16 | ) {
17 | val context = LocalContext.current
18 |
19 | if(isPermissionGranted(context, permission)) {
20 | onResult(true)
21 | return
22 | }
23 |
24 | var launchEffectKey by remember {
25 | mutableStateOf(false)
26 | }
27 |
28 | var requestPermissionDelay by remember {
29 | mutableStateOf(0L)
30 | }
31 |
32 | val managedActivityResultLauncher = rememberLauncherForActivityResult(
33 | contract = ActivityResultContracts.RequestPermission(),
34 | onResult = { isGranted ->
35 |
36 | onResult(isGranted)
37 |
38 | if(!isGranted) {
39 | requestPermissionDelay = 1000
40 | launchEffectKey = !launchEffectKey
41 | }
42 |
43 | }
44 | )
45 |
46 | LaunchedEffect(launchEffectKey) {
47 | delay(requestPermissionDelay)
48 | managedActivityResultLauncher.launch(permission)
49 | }
50 | }
51 |
52 | fun isPermissionGranted(context: Context, permission: String) : Boolean {
53 | return ContextCompat.checkSelfPermission(
54 | context,
55 | permission
56 | ) == PackageManager.PERMISSION_GRANTED
57 | }
58 |
--------------------------------------------------------------------------------
/app/src/main/java/eu/davidmayr/pcremote/QRCodeScannerScreen.kt:
--------------------------------------------------------------------------------
1 | package eu.davidmayr.pcremote
2 |
3 | import android.Manifest
4 | import androidx.camera.core.CameraSelector
5 | import androidx.camera.core.ImageAnalysis
6 | import androidx.camera.core.Preview
7 | import androidx.camera.lifecycle.ProcessCameraProvider
8 | import androidx.compose.foundation.layout.Column
9 | import androidx.compose.foundation.layout.fillMaxSize
10 | import androidx.compose.foundation.layout.fillMaxWidth
11 | import androidx.compose.foundation.layout.padding
12 | import androidx.compose.material3.ExperimentalMaterial3Api
13 | import androidx.compose.material3.Scaffold
14 | import androidx.camera.view.PreviewView
15 | import androidx.compose.foundation.layout.Arrangement
16 | import androidx.compose.foundation.layout.fillMaxHeight
17 | import androidx.compose.foundation.layout.width
18 | import androidx.compose.material3.CircularProgressIndicator
19 | import androidx.compose.material3.MaterialTheme
20 | import androidx.compose.material3.Text
21 | import androidx.compose.material3.TopAppBar
22 | import androidx.compose.material3.TopAppBarDefaults
23 | import androidx.compose.runtime.Composable
24 | import androidx.compose.runtime.mutableStateOf
25 | import androidx.compose.runtime.*
26 | import androidx.compose.ui.Alignment
27 | import androidx.compose.ui.Modifier
28 | import androidx.compose.ui.platform.LocalContext
29 | import androidx.compose.ui.unit.dp
30 | import androidx.compose.ui.viewinterop.AndroidView
31 | import androidx.core.content.ContextCompat
32 | import androidx.lifecycle.compose.LocalLifecycleOwner
33 | import androidx.navigation.NavController
34 |
35 |
36 | @OptIn(ExperimentalMaterial3Api::class)
37 | @Composable
38 | fun QRCodeScannerScreen(navController: NavController, webSocketViewModel: WebSocketViewModel) {
39 | val context = LocalContext.current
40 | val lifeCycleOwner = LocalLifecycleOwner.current
41 |
42 | var permissionError by remember { mutableStateOf(false) }
43 | var loading by remember { mutableStateOf(false) }
44 |
45 | PermissionRequestDialog(
46 | permission = Manifest.permission.CAMERA,
47 | onResult = { isGranted ->
48 | permissionError = !isGranted
49 | },
50 | )
51 |
52 | Scaffold(
53 | topBar = {
54 | TopAppBar(
55 | colors = TopAppBarDefaults.topAppBarColors(),
56 | title = {
57 | Text(context.getString(R.string.qr_code_title))
58 | },
59 | )
60 | },
61 | modifier = Modifier.fillMaxSize(),
62 | content = { padding ->
63 | Column(
64 | modifier = Modifier.padding(padding),
65 | verticalArrangement = Arrangement.spacedBy(16.dp),
66 | ) {
67 |
68 | if (!loading) {
69 | if (permissionError) {
70 | Text(context.getString(R.string.perm_missing))
71 | } else {
72 | Text(context.getString(R.string.qr_code_desc))
73 |
74 | AndroidView(
75 | factory = { context ->
76 | val previewView = PreviewView(context)
77 | val preview = Preview.Builder().build()
78 | val cameraSelector = CameraSelector.Builder().build()
79 |
80 | preview.surfaceProvider = previewView.surfaceProvider
81 |
82 | val imageAnalysis = ImageAnalysis.Builder().build()
83 |
84 | imageAnalysis.setAnalyzer(
85 | ContextCompat.getMainExecutor(context),
86 | BarcodeAnalyzer(context, response = {
87 | if(loading) return@BarcodeAnalyzer
88 |
89 | loading = true
90 | webSocketViewModel.connect(it.get("ip").asString, it.get("port").asInt, it.get("pw").asString)
91 | navController.navigate("home")
92 | loading = false
93 | })
94 | )
95 |
96 | ProcessCameraProvider.getInstance(context).get().bindToLifecycle(
97 | lifeCycleOwner,
98 | cameraSelector,
99 | preview,
100 | imageAnalysis
101 | )
102 |
103 | previewView
104 | }
105 | )
106 | }
107 | } else {
108 | Column(
109 | horizontalAlignment = Alignment.CenterHorizontally,
110 | verticalArrangement = Arrangement.Center,
111 | modifier = Modifier.fillMaxWidth().fillMaxHeight()
112 | ) {
113 | CircularProgressIndicator(
114 | modifier = Modifier.width(64.dp),
115 | color = MaterialTheme.colorScheme.secondary,
116 | trackColor = MaterialTheme.colorScheme.surfaceVariant,
117 | )
118 | Text(context.getString(R.string.loading), modifier = Modifier.padding(top = 30.dp))
119 | }
120 |
121 | }
122 | }
123 | }
124 | )
125 | }
126 |
--------------------------------------------------------------------------------
/app/src/main/java/eu/davidmayr/pcremote/SPenViewModel.kt:
--------------------------------------------------------------------------------
1 | package eu.davidmayr.pcremote
2 |
3 | import android.content.Context
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.runtime.mutableLongStateOf
6 | import androidx.compose.runtime.mutableStateOf
7 | import androidx.compose.runtime.setValue
8 | import androidx.lifecycle.ViewModel
9 | import androidx.lifecycle.ViewModelProvider
10 | import androidx.lifecycle.viewModelScope
11 | import com.samsung.android.sdk.penremote.*
12 | import kotlinx.coroutines.delay
13 | import kotlinx.coroutines.launch
14 |
15 | const val doubleClickTime: Long = 250
16 |
17 |
18 | class SPenViewModel(
19 | val webSocketViewModel: WebSocketViewModel
20 | ): ViewModel() {
21 |
22 | private var manager: SpenUnitManager? = null
23 |
24 | var isConnecting by mutableStateOf(false)
25 | var connected by mutableStateOf(false)
26 | var errorState by mutableStateOf("")
27 |
28 | private var buttonPressedSince: Long by mutableLongStateOf(-1)
29 | private var buttonUpLastTime: Long by mutableLongStateOf(-1)
30 |
31 | var buttonPressed: Boolean by mutableStateOf(false)
32 |
33 | val isSpenSupported = SpenRemote.getInstance().isFeatureEnabled(SpenRemote.FEATURE_TYPE_BUTTON)
34 | || SpenRemote.getInstance().isFeatureEnabled(SpenRemote.FEATURE_TYPE_AIR_MOTION)
35 |
36 |
37 | private val isLaserPointerActive: Boolean
38 | get() {
39 | return buttonPressed && System.currentTimeMillis() - buttonPressedSince > doubleClickTime*2
40 | }
41 |
42 |
43 |
44 | fun connect(context: Context) {
45 | isConnecting = true
46 | try {
47 |
48 | SpenRemote.getInstance().connect(context, object : SpenRemote.ConnectionResultCallback {
49 |
50 | override fun onSuccess(spenUnitManager: SpenUnitManager?) {
51 | isConnecting = false
52 | manager = spenUnitManager
53 | connected = true
54 | errorState = ""
55 | listenEvents()
56 | }
57 |
58 | override fun onFailure(error: Int) {
59 | connected = false
60 | isConnecting = false
61 | errorState = when (error) {
62 | SpenRemote.Error.UNSUPPORTED_DEVICE -> "Unsupported"
63 | SpenRemote.Error.CONNECTION_FAILED -> "Failed"
64 | else -> "Error"
65 | }
66 | }
67 |
68 | })
69 |
70 | } catch (e: NoClassDefFoundError) {
71 | isConnecting = false
72 | errorState = "Unsupported"
73 | }
74 | }
75 |
76 | fun disconnect(context: Context) {
77 |
78 | manager?.getUnit(SpenUnit.TYPE_BUTTON)?.let { button ->
79 | manager?.unregisterSpenEventListener(button)
80 | }
81 | manager?.getUnit(SpenUnit.TYPE_AIR_MOTION)?.let { airMotion ->
82 | manager?.unregisterSpenEventListener(airMotion)
83 | }
84 |
85 | SpenRemote.getInstance().disconnect(context)
86 |
87 | connected = false
88 | errorState = ""
89 | }
90 |
91 | private fun listenEvents() {
92 |
93 | val sPen = SpenRemote.getInstance()
94 |
95 | // Button
96 | if (sPen.isFeatureEnabled(SpenRemote.FEATURE_TYPE_BUTTON)) {
97 | manager?.registerSpenEventListener({ event ->
98 |
99 | when (ButtonEvent(event).action) {
100 | ButtonEvent.ACTION_DOWN ->{
101 | buttonPressed = true
102 | buttonPressedSince = System.currentTimeMillis()
103 | }
104 | ButtonEvent.ACTION_UP -> {
105 | val time = System.currentTimeMillis()
106 |
107 | if(!isLaserPointerActive) {
108 | if(System.currentTimeMillis()-buttonUpLastTime > doubleClickTime) {
109 |
110 | viewModelScope.launch {
111 | delay(doubleClickTime)
112 | if (buttonUpLastTime != time) {
113 | webSocketViewModel.sendButtonClick(true)
114 | } else {
115 | webSocketViewModel.sendButtonClick(false)
116 | }
117 | }
118 | }
119 | } else {
120 | webSocketViewModel.sendReleased()
121 | }
122 |
123 | buttonUpLastTime = time
124 | buttonPressedSince = -1
125 | buttonPressed = false
126 | }
127 | }
128 |
129 | }, manager?.getUnit(SpenUnit.TYPE_BUTTON))
130 | }
131 |
132 | // Air motion
133 | if (sPen.isFeatureEnabled(SpenRemote.FEATURE_TYPE_AIR_MOTION)) {
134 | manager?.registerSpenEventListener({ event ->
135 |
136 | if (isLaserPointerActive) {
137 |
138 | val airMotionEvent = AirMotionEvent(event)
139 | val deltaX = airMotionEvent.deltaX
140 | val deltaY = airMotionEvent.deltaY
141 |
142 | if((deltaX < 0.0000000000001 && deltaX > -0.0000000000001) || (deltaY < 0.0000000000001 && deltaY
143 | > -0.0000000000001))
144 | return@registerSpenEventListener
145 |
146 | webSocketViewModel.sendMotion(deltaX, deltaY)
147 | }
148 |
149 | }, manager?.getUnit(SpenUnit.TYPE_AIR_MOTION))
150 | }
151 | }
152 |
153 | class SPenViewModelFactory(
154 | private val webSocketViewModel: WebSocketViewModel
155 | ) : ViewModelProvider.Factory {
156 |
157 | @Suppress("UNCHECKED_CAST")
158 | override fun create(modelClass: Class): T {
159 | if (modelClass.isAssignableFrom(SPenViewModel::class.java)) {
160 | return SPenViewModel(webSocketViewModel) as T
161 | }
162 | throw IllegalArgumentException("Unknown ViewModel class")
163 | }
164 | }
165 |
166 |
167 | }
--------------------------------------------------------------------------------
/app/src/main/java/eu/davidmayr/pcremote/WebSocketViewModel.kt:
--------------------------------------------------------------------------------
1 | package eu.davidmayr.pcremote
2 |
3 | import androidx.compose.runtime.getValue
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.compose.runtime.setValue
6 | import androidx.lifecycle.ViewModel
7 | import com.google.gson.JsonObject
8 | import okhttp3.*
9 |
10 | class WebSocketViewModel : ViewModel() {
11 |
12 | var connected by mutableStateOf(false)
13 |
14 | private var webSocket: WebSocket? = null
15 | private val client = OkHttpClient()
16 |
17 | private val listener = object : WebSocketListener() {
18 | override fun onOpen(webSocket: WebSocket, response: Response) {
19 | super.onOpen(webSocket, response)
20 | println("WebSocket Connected")
21 |
22 | if(webSocket == this@WebSocketViewModel.webSocket) {
23 | connected = true
24 | }
25 | }
26 |
27 | override fun onMessage(webSocket: WebSocket, text: String) {
28 | super.onMessage(webSocket, text)
29 | println("Message received: $text")
30 | }
31 |
32 | override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
33 | super.onFailure(webSocket, t, response)
34 | println("Error: ${t.message}")
35 |
36 | if(webSocket == this@WebSocketViewModel.webSocket) {
37 | this@WebSocketViewModel.webSocket = null
38 | connected = false
39 | }
40 | }
41 |
42 | override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
43 | super.onClosing(webSocket, code, reason)
44 | println("Closing WebSocket: $reason")
45 | }
46 |
47 | override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
48 | super.onClosed(webSocket, code, reason)
49 | println("WebSocket Closed: $reason")
50 | if(webSocket == this@WebSocketViewModel.webSocket) {
51 | this@WebSocketViewModel.webSocket = null
52 | connected = false
53 | }
54 |
55 | }
56 | }
57 |
58 | fun connect(ip: String, port: Int, pw: String) {
59 | closeConnection()
60 |
61 | val request = Request.Builder().url("ws://$ip:$port").build()
62 | webSocket = client.newWebSocket(request, listener)
63 | webSocket?.send(pw)
64 | println("connected to ws://$ip:$port")
65 | }
66 |
67 | fun sendButtonClick(double: Boolean) {
68 | webSocket?.send(JsonObject().also {
69 | it.addProperty("type", "b")
70 | it.addProperty("d", double)
71 | }.toString())
72 | }
73 |
74 | fun sendMotion(deltaX: Float, deltaY: Float) {
75 | webSocket?.send(JsonObject().also {
76 | it.addProperty("type", "m")
77 | it.addProperty("x", deltaX)
78 | it.addProperty("y", deltaY)
79 | }.toString())
80 | }
81 |
82 | fun closeConnection() {
83 | webSocket?.close(1000, "Closing connection")
84 | connected = false
85 | }
86 |
87 | override fun onCleared() {
88 | super.onCleared()
89 | closeConnection()
90 | }
91 |
92 | fun sendReleased() {
93 | webSocket?.send(JsonObject().also {
94 | it.addProperty("type", "mr")
95 | }.toString())
96 | }
97 | }
--------------------------------------------------------------------------------
/app/src/main/java/eu/davidmayr/pcremote/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package eu.davidmayr.pcremote.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
--------------------------------------------------------------------------------
/app/src/main/java/eu/davidmayr/pcremote/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package eu.davidmayr.pcremote.ui.theme
2 |
3 | import android.app.Activity
4 | import android.os.Build
5 | import androidx.compose.foundation.isSystemInDarkTheme
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.darkColorScheme
8 | import androidx.compose.material3.dynamicDarkColorScheme
9 | import androidx.compose.material3.dynamicLightColorScheme
10 | import androidx.compose.material3.lightColorScheme
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.platform.LocalContext
13 |
14 | private val DarkColorScheme = darkColorScheme(
15 | primary = Purple80,
16 | secondary = PurpleGrey80,
17 | tertiary = Pink80
18 | )
19 |
20 | private val LightColorScheme = lightColorScheme(
21 | primary = Purple40,
22 | secondary = PurpleGrey40,
23 | tertiary = Pink40
24 |
25 | /* Other default colors to override
26 | background = Color(0xFFFFFBFE),
27 | surface = Color(0xFFFFFBFE),
28 | onPrimary = Color.White,
29 | onSecondary = Color.White,
30 | onTertiary = Color.White,
31 | onBackground = Color(0xFF1C1B1F),
32 | onSurface = Color(0xFF1C1B1F),
33 | */
34 | )
35 |
36 | @Composable
37 | fun PCRemoteTheme(
38 | darkTheme: Boolean = isSystemInDarkTheme(),
39 | // Dynamic color is available on Android 12+
40 | dynamicColor: Boolean = true,
41 | content: @Composable () -> Unit
42 | ) {
43 | val colorScheme = when {
44 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
45 | val context = LocalContext.current
46 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
47 | }
48 |
49 | darkTheme -> DarkColorScheme
50 | else -> LightColorScheme
51 | }
52 |
53 | MaterialTheme(
54 | colorScheme = colorScheme,
55 | typography = Typography,
56 | content = content
57 | )
58 | }
--------------------------------------------------------------------------------
/app/src/main/java/eu/davidmayr/pcremote/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package eu.davidmayr.pcremote.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
13 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidmayr/SPen-PCRemote/d6db99a38af7cca524fa24722a13475cfa359f8b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidmayr/SPen-PCRemote/d6db99a38af7cca524fa24722a13475cfa359f8b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidmayr/SPen-PCRemote/d6db99a38af7cca524fa24722a13475cfa359f8b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidmayr/SPen-PCRemote/d6db99a38af7cca524fa24722a13475cfa359f8b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidmayr/SPen-PCRemote/d6db99a38af7cca524fa24722a13475cfa359f8b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidmayr/SPen-PCRemote/d6db99a38af7cca524fa24722a13475cfa359f8b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidmayr/SPen-PCRemote/d6db99a38af7cca524fa24722a13475cfa359f8b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidmayr/SPen-PCRemote/d6db99a38af7cca524fa24722a13475cfa359f8b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidmayr/SPen-PCRemote/d6db99a38af7cca524fa24722a13475cfa359f8b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidmayr/SPen-PCRemote/d6db99a38af7cca524fa24722a13475cfa359f8b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values-de/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | S Pen PCRemote
4 | Samsung S Pen wird auf diesem Gerät unterstützt.
5 | Samsung S Pen wird auf diesem Gerät nicht unterstützt oder ist fehlgeschlagen.
6 | Geräte QR Code scannen
7 | Trennen
8 | Du bist verbunden!
9 | Bitte verbinde dich mit dem selben Netzwerk wie der Computer und starte den Verbindungsprozess.
10 | QR Code scannen
11 | Scanne den QR Code auf der PCRemote desktop app und stelle sicher, dass du im selben Netzerk bist.
12 | Versuche zu verbinden...
13 | Konnte keine Kamera Berechtigung erhalten.
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #034B9D
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | S Pen PCRemote
3 | Samsung S Pen is supported on this device.
4 | Samsung S Pen is not supported on this device or failed.
5 | Scan Device QR Code
6 | Disconnect
7 | You are connected!
8 |
9 | Please connect to the same network as the computer and start the paring process.
10 | Scan QR Code
11 | Scan the QR Code of the PCRemote desktop application and make sure to you are in the same network!
12 | Trying to connect…
13 | Could not get camera permission!
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/test/java/eu/davidmayr/pcremote/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package eu.davidmayr.pcremote
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | alias(libs.plugins.android.application) apply false
4 | alias(libs.plugins.kotlin.android) apply false
5 | alias(libs.plugins.kotlin.compose) apply false
6 | }
--------------------------------------------------------------------------------
/desktop-app/.run/desktop.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | true
19 |
20 |
21 |
--------------------------------------------------------------------------------
/desktop-app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat
2 |
3 | plugins {
4 | kotlin("jvm")
5 | id("org.jetbrains.compose")
6 | id("org.jetbrains.kotlin.plugin.compose")
7 | }
8 |
9 | group = "com.example"
10 | version = "1.0-SNAPSHOT"
11 |
12 | repositories {
13 | mavenCentral()
14 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
15 | google()
16 | }
17 |
18 | dependencies {
19 | // Note, if you develop a library, you should use compose.desktop.common.
20 | // compose.desktop.currentOs should be used in launcher-sourceSet
21 | // (in a separate module for demo project and in testMain).
22 | // With compose.desktop.common you will also lose @Preview functionality
23 | implementation(compose.desktop.currentOs)
24 | implementation("io.javalin:javalin-bundle:6.3.0")
25 | implementation("io.nayuki:qrcodegen:1.8.0")
26 | implementation("com.google.code.gson:gson:2.11.0")
27 | implementation("org.apache.logging.log4j:log4j-core:2.23.1")
28 | implementation("org.apache.logging.log4j:log4j-slf4j2-impl:2.23.1")
29 |
30 | }
31 |
32 | compose.desktop {
33 | application {
34 | mainClass = "MainKt"
35 |
36 | nativeDistributions {
37 | windows {
38 | shortcut = true
39 | menu = true
40 | iconFile = file("src/main/resources/icon.ico")
41 | }
42 |
43 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
44 | packageName = "S Pen PC Remote"
45 | packageVersion = "1.0.0"
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/desktop-app/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
2 | kotlin.code.style=official
3 | kotlin.version=2.0.0
4 | compose.version=1.6.11
5 |
--------------------------------------------------------------------------------
/desktop-app/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidmayr/SPen-PCRemote/d6db99a38af7cca524fa24722a13475cfa359f8b/desktop-app/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/desktop-app/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/desktop-app/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
84 |
85 | APP_NAME="Gradle"
86 | APP_BASE_NAME=${0##*/}
87 |
88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | MAX_FD=$( ulimit -H -n ) ||
147 | warn "Could not query maximum file descriptor limit"
148 | esac
149 | case $MAX_FD in #(
150 | '' | soft) :;; #(
151 | *)
152 | ulimit -n "$MAX_FD" ||
153 | warn "Could not set maximum file descriptor limit to $MAX_FD"
154 | esac
155 | fi
156 |
157 | # Collect all arguments for the java command, stacking in reverse order:
158 | # * args from the command line
159 | # * the main class name
160 | # * -classpath
161 | # * -D...appname settings
162 | # * --module-path (only if needed)
163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
164 |
165 | # For Cygwin or MSYS, switch paths to Windows format before running java
166 | if "$cygwin" || "$msys" ; then
167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
169 |
170 | JAVACMD=$( cygpath --unix "$JAVACMD" )
171 |
172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
173 | for arg do
174 | if
175 | case $arg in #(
176 | -*) false ;; # don't mess with options #(
177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
178 | [ -e "$t" ] ;; #(
179 | *) false ;;
180 | esac
181 | then
182 | arg=$( cygpath --path --ignore --mixed "$arg" )
183 | fi
184 | # Roll the args list around exactly as many times as the number of
185 | # args, so each arg winds up back in the position where it started, but
186 | # possibly modified.
187 | #
188 | # NB: a `for` loop captures its iteration list before it begins, so
189 | # changing the positional parameters here affects neither the number of
190 | # iterations, nor the values presented in `arg`.
191 | shift # remove old arg
192 | set -- "$@" "$arg" # push replacement arg
193 | done
194 | fi
195 |
196 | # Collect all arguments for the java command;
197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
198 | # shell script including quotes and variable substitutions, so put them in
199 | # double quotes to make sure that they get re-expanded; and
200 | # * put everything else in single quotes, so that it's not re-expanded.
201 |
202 | set -- \
203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
204 | -classpath "$CLASSPATH" \
205 | org.gradle.wrapper.GradleWrapperMain \
206 | "$@"
207 |
208 | # Use "xargs" to parse quoted args.
209 | #
210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
211 | #
212 | # In Bash we could simply go:
213 | #
214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
215 | # set -- "${ARGS[@]}" "$@"
216 | #
217 | # but POSIX shell has neither arrays nor command substitution, so instead we
218 | # post-process each arg (as a line of input to sed) to backslash-escape any
219 | # character that might be a shell metacharacter, then use eval to reverse
220 | # that process (while maintaining the separation between arguments), and wrap
221 | # the whole thing up as a single "set" statement.
222 | #
223 | # This will of course break if any of these variables contains a newline or
224 | # an unmatched quote.
225 | #
226 |
227 | eval "set -- $(
228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
229 | xargs -n1 |
230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
231 | tr '\n' ' '
232 | )" '"$@"'
233 |
234 | exec "$JAVACMD" "$@"
235 |
--------------------------------------------------------------------------------
/desktop-app/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/desktop-app/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
4 | google()
5 | gradlePluginPortal()
6 | mavenCentral()
7 | }
8 |
9 | plugins {
10 | kotlin("jvm").version(extra["kotlin.version"] as String)
11 | id("org.jetbrains.compose").version(extra["compose.version"] as String)
12 | id("org.jetbrains.kotlin.plugin.compose").version(extra["kotlin.version"] as String)
13 | }
14 | }
15 |
16 | rootProject.name = "desktop-app"
17 |
--------------------------------------------------------------------------------
/desktop-app/src/main/kotlin/LaserPointer.kt:
--------------------------------------------------------------------------------
1 | import java.awt.Color
2 | import java.awt.Graphics
3 | import java.awt.GraphicsEnvironment
4 | import java.awt.Rectangle
5 | import java.awt.Window
6 | import javax.swing.JComponent
7 | import javax.swing.JFrame
8 |
9 |
10 | object LaserPointer {
11 |
12 | val laserFrame = JFrame()
13 |
14 | init {
15 |
16 | laserFrame.isUndecorated = true
17 | laserFrame.opacity = 0.5f
18 | laserFrame.background = Color(0, 0, 0, 0)
19 | laserFrame.type = Window.Type.UTILITY
20 | laserFrame.focusableWindowState = false
21 | laserFrame.isAlwaysOnTop = true
22 |
23 | laserFrame.setSize(20, 20)
24 | laserFrame.add(object : JComponent() {
25 | override fun paintComponent(g: Graphics) {
26 | super.paintComponent(g)
27 | g.color = Color.RED // Laser color
28 | g.fillOval(0, 0, 20, 20) // Draw a small circle as laser
29 | }
30 | })
31 |
32 | setVisibility(false)
33 | }
34 |
35 | private fun resetPos() {
36 | val ge = GraphicsEnvironment.getLocalGraphicsEnvironment()
37 | val screens = ge.screenDevices
38 |
39 |
40 | // Check if a secondary screen is available
41 | if (screens.size > 1) {
42 | val secondaryScreen = screens[1] // Get the secondary screen
43 | val bounds: Rectangle = secondaryScreen.defaultConfiguration.bounds
44 |
45 | // Calculate center position
46 | val x: Int = bounds.x + (bounds.width - laserFrame.width) / 2
47 | val y: Int = bounds.y + (bounds.height - laserFrame.height) / 2
48 |
49 | // Set location to center on secondary screen
50 | laserFrame.setLocation(x, y)
51 | } else {
52 | // Default to centering on primary screen
53 | laserFrame.setLocationRelativeTo(null)
54 | }
55 |
56 | }
57 |
58 | fun setVisibility(visible: Boolean) {
59 | if(laserFrame.isVisible != visible) {
60 | resetPos()
61 | }
62 | laserFrame.isVisible = visible
63 | }
64 |
65 | fun move(x: Float, y: Float) {
66 | val point = laserFrame.location
67 | laserFrame.setLocation(
68 | point.x + (x * 1000).toInt(),
69 | point.y + (-y * 1000).toInt()
70 | )
71 | }
72 |
73 | }
74 |
--------------------------------------------------------------------------------
/desktop-app/src/main/kotlin/Main.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.desktop.ui.tooling.preview.Preview
2 | import androidx.compose.foundation.Image
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.material.Button
7 | import androidx.compose.material.MaterialTheme
8 | import androidx.compose.material.Text
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.runtime.getValue
11 | import androidx.compose.runtime.mutableStateOf
12 | import androidx.compose.runtime.remember
13 | import androidx.compose.runtime.setValue
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.draw.scale
17 | import androidx.compose.ui.graphics.toComposeImageBitmap
18 | import androidx.compose.ui.res.loadSvgPainter
19 | import androidx.compose.ui.unit.Density
20 | import androidx.compose.ui.window.Window
21 | import androidx.compose.ui.window.application
22 |
23 | @Composable
24 | @Preview
25 | fun App() {
26 | MaterialTheme {
27 |
28 | Column(
29 | horizontalAlignment = Alignment.CenterHorizontally,
30 | verticalArrangement = Arrangement.Center,
31 | modifier = Modifier.fillMaxSize()
32 | ) {
33 | Text("Scan this code with your S Pen capable Smartphone")
34 | Image(WebServer.generateQRCodeForConnection().toComposeImageBitmap(),
35 | "QR Code")
36 | }
37 |
38 | }
39 | }
40 |
41 | fun main() = application {
42 | WebServer //init
43 |
44 | Window(
45 | title = "S Pen PC Remote",
46 | onCloseRequest = ::exitApplication,
47 | icon = loadSvgPainter(WebServer.javaClass.getResourceAsStream("/icon.svg")!!, density = Density(1.0F, 1.0F))
48 | ) {
49 | App()
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/desktop-app/src/main/kotlin/WebServer.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.runtime.getValue
2 | import androidx.compose.runtime.mutableStateOf
3 | import com.google.gson.JsonObject
4 | import com.google.gson.JsonParser
5 | import io.javalin.Javalin
6 | import io.javalin.websocket.WsContext
7 | import io.nayuki.qrcodegen.QrCode
8 | import java.awt.Robot
9 | import java.awt.event.KeyEvent
10 | import java.awt.image.BufferedImage
11 | import java.net.InetAddress
12 | import java.net.UnknownHostException
13 | import java.security.SecureRandom
14 | import java.util.*
15 |
16 | val random = SecureRandom()
17 |
18 | fun generateAuthToken(length: Int = 16): String {
19 | val charPool = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
20 | return (1..length)
21 | .map { charPool[random.nextInt(charPool.length)] }
22 | .joinToString("")
23 | }
24 |
25 | object WebServer {
26 |
27 | //TODO: Some sort of encryption maybe?
28 |
29 | val robot = Robot()
30 |
31 | private var localIp: String = ""
32 | //Generate random secure token
33 | val password = generateAuthToken(24)
34 |
35 | private val authenticatedSessions = mutableListOf()
36 |
37 | val webServer = Javalin.create { config ->
38 | config.router.mount { router ->
39 | router.ws("/") { ws ->
40 | ws.onError {
41 | println("Err")
42 | }
43 | ws.onConnect { ctx ->
44 | println("User joined")
45 | }
46 | ws.onClose { ctx ->
47 | println("User left")
48 | authenticatedSessions.remove(ctx)
49 | }
50 | ws.onMessage { ctx ->
51 | if(!authenticatedSessions.contains(ctx)) {
52 | if(ctx.message() == password) {
53 | authenticatedSessions.add(ctx)
54 | ctx.send("Authenticated")
55 | println("New user authenticated")
56 | } else {
57 | ctx.send("Invalid password")
58 | println("User failed to authenticate")
59 | }
60 | } else {
61 |
62 | val jsonObject = JsonParser.parseString(ctx.message()).asJsonObject
63 |
64 | if(jsonObject.get("type").asString == "m") {
65 | LaserPointer.setVisibility(true)
66 | LaserPointer.move(
67 | jsonObject.get("x").asFloat,
68 | jsonObject.get("y").asFloat
69 | )
70 | } else if(jsonObject.get("type").asString == "mr") {
71 | LaserPointer.setVisibility(false)
72 | } else {
73 | val doubleClick = jsonObject.get("d").asBoolean
74 | if(doubleClick) {
75 |
76 | robot.keyPress(KeyEvent.VK_LEFT)
77 | robot.keyRelease(KeyEvent.VK_LEFT)
78 | } else {
79 | robot.keyPress(KeyEvent.VK_RIGHT)
80 | robot.keyRelease(KeyEvent.VK_RIGHT)
81 | }
82 | }
83 | }
84 | }
85 | }
86 | }
87 | }.start(54321)
88 |
89 | init {
90 | println("Web Server port: " +webServer.port())
91 |
92 | try {
93 | // Get the local host's InetAddress instance
94 | val localHost = InetAddress.getLocalHost()
95 |
96 | // Retrieve and print the IP address
97 | val ipAddress = localHost.hostAddress
98 | this.localIp = ipAddress
99 | println("Local IP Address: $ipAddress")
100 | } catch (e: UnknownHostException) {
101 | System.err.println("Unable to retrieve IP address: " + e.message)
102 | }
103 | }
104 |
105 |
106 | fun generateQRCodeForConnection(): BufferedImage {
107 |
108 |
109 | val json = JsonObject()
110 | json.addProperty("type", "pcmanager/remotconfig")
111 | json.addProperty("pw", password)
112 | json.addProperty("ip", localIp)
113 | json.addProperty("port", webServer.port())
114 |
115 | val qrCode = QrCode.encodeText(json.toString(), QrCode.Ecc.MEDIUM)
116 | val img: BufferedImage = toImage(qrCode, 10, 10)
117 |
118 | return img
119 | }
120 |
121 | private fun toImage(qr: QrCode, scale: Int, border: Int): BufferedImage {
122 | return toImage(qr, scale, border, 0xFFFFFF, 0x000000)
123 | }
124 |
125 | private fun toImage(qr: QrCode, scale: Int, border: Int, lightColor: Int, darkColor: Int): BufferedImage {
126 | require(!(scale <= 0 || border < 0)) { "Value out of range" }
127 | require(!(border > Int.MAX_VALUE / 2 || qr.size + border * 2L > Int.MAX_VALUE / scale)) { "Scale or border too large" }
128 |
129 | val result = BufferedImage(
130 | (qr.size + border * 2) * scale,
131 | (qr.size + border * 2) * scale,
132 | BufferedImage.TYPE_INT_RGB
133 | )
134 | for (y in 0..
2 |
48 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. For more details, visit
12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | agp = "8.7.2"
3 | barcodeScanning = "17.3.0"
4 | hiltAndroidCompiler = "2.51.1"
5 | hiltNavigationCompose = "1.2.0"
6 | kotlin = "2.0.0"
7 | coreKtx = "1.10.1"
8 | junit = "4.13.2"
9 | junitVersion = "1.2.1"
10 | espressoCore = "3.6.1"
11 | lifecycleRuntimeKtx = "2.8.7"
12 | activityCompose = "1.9.3"
13 | composeBom = "2024.11.00"
14 | navigationCompose = "2.8.4"#
15 | camera = "1.4.0"
16 |
17 | [libraries]
18 | androidx-camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref="camera" }
19 | androidx-camera-core = { group = "androidx.camera", name = "camera-core", version.ref="camera" }
20 | androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref="camera" }
21 | androidx-camera-view = { group = "androidx.camera", name = "camera-view", version.ref="camera" }
22 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
23 | androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
24 | barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version.ref = "barcodeScanning" }
25 | hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroidCompiler" }
26 | hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
27 | junit = { group = "junit", name = "junit", version.ref = "junit" }
28 | androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
29 | androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
30 | androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
31 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
32 | androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
33 | androidx-ui = { group = "androidx.compose.ui", name = "ui" }
34 | androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
35 | androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
36 | androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
37 | androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
38 | androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
39 | androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
40 |
41 | [plugins]
42 | android-application = { id = "com.android.application", version.ref = "agp" }
43 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
44 | kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
45 |
46 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidmayr/SPen-PCRemote/d6db99a38af7cca524fa24722a13475cfa359f8b/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Nov 30 14:25:28 CET 2024
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google {
4 | content {
5 | includeGroupByRegex("com\\.android.*")
6 | includeGroupByRegex("com\\.google.*")
7 | includeGroupByRegex("androidx.*")
8 | }
9 | }
10 | mavenCentral()
11 | gradlePluginPortal()
12 | }
13 | }
14 | dependencyResolutionManagement {
15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
16 | repositories {
17 | google()
18 | mavenCentral()
19 | }
20 | }
21 |
22 | rootProject.name = "PCRemote"
23 | include(":app")
24 |
--------------------------------------------------------------------------------