Skip to content

Commit

Permalink
use an activity to allow clipboard read on vanilla android 10+ devices
Browse files Browse the repository at this point in the history
  • Loading branch information
C10udburst committed Nov 6, 2023
1 parent bfe1198 commit 1da28b1
Show file tree
Hide file tree
Showing 16 changed files with 135 additions and 54 deletions.
2 changes: 1 addition & 1 deletion .idea/compiler.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/kotlinc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ Emulate a USB keyboard to paste text from the clipboard from your phone to your
## Requirements
- Rooted Android device
- [USB Gadget Tool](https://github.com/tejado/android-usb-gadget) switched into hid keyboard mode
- On android 10+ you also need clipboard access for background apps enabled via [Riru-ClipboardWhitelist](https://github.com/Kr328/Riru-ClipboardWhitelist) or [xposed-clipboard-whitelist](https://github.com/GamerGirlandCo/xposed-clipboard-whitelist)
<!-- - On android 10+ you also need clipboard access for background apps enabled via [Riru-ClipboardWhitelist](https://github.com/Kr328/Riru-ClipboardWhitelist) or [xposed-clipboard-whitelist](https://github.com/GamerGirlandCo/xposed-clipboard-whitelist) -->

## Usage
Just copy some text to the clipboard and click the Quick Settings tile to paste it on your computer while connected via USB.


## Roadmap
- [ ] Add support for other keyboard layouts
- [ ] Handle caps lock (?)
- [ ] Improve UI
2 changes: 2 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-android-extensions'
}

android {
Expand Down Expand Up @@ -37,6 +38,7 @@ dependencies {
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.10.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
Expand Down
9 changes: 7 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
<uses-permission
android:name="android.permission.READ_CLIPBOARD_IN_BACKGROUND"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.INTERNAL_SYSTEM_WINDOW"
<uses-permission
android:name="android.permission.INTERNAL_SYSTEM_WINDOW"
tools:ignore="ProtectedPermissions" />

<application
Expand All @@ -16,8 +17,12 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.Light"
android:theme="@style/Theme.AppCompat.DayNight.Dialog"
tools:targetApi="31">
<activity
android:name=".WorkingActivity"
android:exported="false" />

<service
android:name=".UsbQSService"
android:enabled="true"
Expand Down
25 changes: 25 additions & 0 deletions app/src/main/java/io/github/cloudburst/cliptype/UsbKbdMap.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package io.github.cloudburst.cliptype
class UsbKbdMap {
private val kbdVal = mapOf(
null to byteArrayOf(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00),

'a' to byteArrayOf(0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00),
'b' to byteArrayOf(0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00),
'c' to byteArrayOf(0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00),
Expand Down Expand Up @@ -104,6 +105,30 @@ class UsbKbdMap {
'<' to byteArrayOf(0x02, 0x00, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00),
'>' to byteArrayOf(0x02, 0x00, 0x37, 0x00, 0x00, 0x00, 0x00, 0x00),
'?' to byteArrayOf(0x02, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00),

'\n' to byteArrayOf(0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00),

// if there are polish characters in clipboard, we implicitly assume that keyboard layout is polish-programmers
'ą' to byteArrayOf(0x40, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00),
'ć' to byteArrayOf(0x40, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00),
'ę' to byteArrayOf(0x40, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00),
'ł' to byteArrayOf(0x40, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00),
'ń' to byteArrayOf(0x40, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00),
'ó' to byteArrayOf(0x40, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00),
'ś' to byteArrayOf(0x40, 0x00, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00),
'ż' to byteArrayOf(0x40, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x00, 0x00),
'ź' to byteArrayOf(0x40, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x00, 0x00),

'Ą' to byteArrayOf(0x42, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00),
'Ć' to byteArrayOf(0x42, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00),
'Ę' to byteArrayOf(0x42, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00),
'Ł' to byteArrayOf(0x42, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00),
'Ń' to byteArrayOf(0x42, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00),
'Ó' to byteArrayOf(0x42, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00),
'Ś' to byteArrayOf(0x42, 0x00, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00),
'Ż' to byteArrayOf(0x42, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x00, 0x00),
'Ź' to byteArrayOf(0x42, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x00, 0x00),

)

fun getScancode(key: Char?): ByteArray =
Expand Down
54 changes: 9 additions & 45 deletions app/src/main/java/io/github/cloudburst/cliptype/UsbQSService.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.cloudburst.cliptype

import android.app.PendingIntent
import android.content.ClipboardManager
import android.content.Intent
import android.os.Build.VERSION
Expand All @@ -8,56 +9,19 @@ import com.topjohnwu.superuser.ipc.RootService

class UsbQSService : TileService() {

private var connection: UsbRootService.Connection? = null

override fun onClick() {
super.onClick()

updateDesc(getString(R.string.usb_qs_desc_default))

val clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
val content = clipboard.primaryClip?.getItemAt(0)?.text ?: return

val intent = Intent(this, UsbRootService::class.java)
intent.putExtra("text", content)

createConnection()

for (i in 1..5) {
if (connection?.binder == null) {
Thread.sleep(100)
} else {
break
}
}

if (connection?.binder?.hidCapable() == false) {
updateDesc(getString(R.string.usb_qs_desc_hid_unavailable))
// start Working Activity
if (VERSION.SDK_INT >= 34) {
val intent = PendingIntent.getActivity(this, 0, Intent(this, WorkingActivity::class.java),
PendingIntent.FLAG_IMMUTABLE)
startActivityAndCollapse(intent)
} else {
updateDesc(getString(R.string.usb_qs_desc_default))
connection?.binder?.typeUsb(content.toString())
val intent = Intent(this, WorkingActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivityAndCollapse(intent)
}
}

override fun onDestroy() {
super.onDestroy()
connection?.let {
RootService.unbind(it)
connection = null
}
}

private fun createConnection() {
if (connection != null) return
connection = UsbRootService.Connection()
val intent = Intent(this, UsbRootService::class.java)
RootService.bind(intent, connection!!)
}

private fun updateDesc(desc: String) {
qsTile.contentDescription = desc
if (VERSION.SDK_INT >= 29)
qsTile.subtitle = desc
qsTile.updateTile()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class UsbRootService: RootService() {
val hidDev = FileOutputStream(hidFile, true)
for (c in text) {
hidDev.write(kbdMap.getScancode(c))
Thread.sleep(10)
hidDev.write(kbdMap.getScancode(null))
hidDev.flush()

Expand All @@ -43,11 +44,14 @@ class UsbRootService: RootService() {

class Connection: ServiceConnection {

var onConnected: ((Connection) -> Unit)? = null

var binder: IUsbRootService? = null
private set

override fun onServiceConnected(p0: ComponentName?, p1: IBinder?) {
binder = IUsbRootService.Stub.asInterface(p1)
onConnected?.invoke(this)
}

override fun onServiceDisconnected(p0: ComponentName?) {
Expand Down
45 changes: 45 additions & 0 deletions app/src/main/java/io/github/cloudburst/cliptype/WorkingActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package io.github.cloudburst.cliptype

import android.content.ClipboardManager
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.topjohnwu.superuser.ipc.RootService

import kotlinx.android.synthetic.main.activity_working.*

class WorkingActivity : AppCompatActivity() {

private var connection: UsbRootService.Connection? = null

private var clipboardContents: CharSequence = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_working)

val clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
clipboardContents = clipboard.primaryClip?.getItemAt(0)?.text ?: return finish()

connection = UsbRootService.Connection()
connection!!.onConnected = ::onConnected
val intent = Intent(this, UsbRootService::class.java)
RootService.bind(intent, connection!!)
}

private fun onConnected(connection: UsbRootService.Connection) {
val binder = connection.binder ?: return finish()
if (!binder.hidCapable()) {
statusTextView.text = getString(R.string.usb_qs_desc_hid_unavailable)
} else {
binder.typeUsb(clipboardContents.toString())
finish()
}
}

override fun onDestroy() {
super.onDestroy()
connection?.let {
RootService.unbind(it)
}
}
}
18 changes: 18 additions & 0 deletions app/src/main/res/layout/activity_working.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
tools:context=".WorkingActivity">

<TextView
android:id="@+id/statusTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/usb_qs_desc_default"
android:textAlignment="center" />

</LinearLayout>
1 change: 0 additions & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,5 @@
<string name="app_name">ClipType</string>
<string name="usb_qs_label">Type via USB</string>
<string name="usb_qs_desc_default">Send clipboard contents</string>
<string name="usb_qs_desc_service_dead">Starting root service…</string>
<string name="usb_qs_desc_hid_unavailable">HID emulation unavailable</string>
</resources>
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '7.3.1' apply false
id 'com.android.library' version '7.3.1' apply false
id 'com.android.application' version '7.4.2' apply false
id 'com.android.library' version '7.4.2' apply false
id 'org.jetbrains.kotlin.android' version '1.7.20' apply false
}
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#Fri Oct 13 09:02:28 CEST 2023
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

0 comments on commit 1da28b1

Please sign in to comment.