mirror of
https://github.com/KDE/kdeconnect-android
synced 2025-10-19 14:26:49 +00:00
Compare commits
15 Commits
albertvaka
...
v1.33.9
Author | SHA1 | Date | |
---|---|---|---|
|
d6fa07a614 | ||
|
21d58b1ca2 | ||
|
5c614d9094 | ||
|
f32bc29700 | ||
|
9d1687c995 | ||
|
d1a565737a | ||
|
ee139314e1 | ||
|
937f77ced7 | ||
|
c8d2f4d97c | ||
|
83c0617145 | ||
|
3b40e008ee | ||
|
193f1e4bac | ||
|
a24b50ae3e | ||
|
a9216ee636 | ||
|
673352d12b |
@@ -40,8 +40,8 @@ android {
|
||||
applicationId = "org.kde.kdeconnect_tp"
|
||||
minSdk = 22
|
||||
targetSdk = 35
|
||||
versionCode = 13308
|
||||
versionName = "1.33.8"
|
||||
versionCode = 13309
|
||||
versionName = "1.33.9"
|
||||
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
|
||||
}
|
||||
buildFeatures {
|
||||
@@ -327,7 +327,8 @@ dependencies {
|
||||
|
||||
// Testing
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.mockito.core)
|
||||
testImplementation(libs.mockk)
|
||||
testImplementation(libs.slf4j.simple) // do not try to use the Android logger backend in tests
|
||||
testImplementation(libs.jsonassert)
|
||||
|
||||
// For device controls
|
||||
|
16
fastlane/metadata/android/en-US/changelogs/13309.txt
Normal file
16
fastlane/metadata/android/en-US/changelogs/13309.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
1.33.9
|
||||
* Bug fixes and translation improvements.
|
||||
|
||||
1.33.4
|
||||
* Extend offline URL sharing behavior to direct share targets.
|
||||
* Improve paring screen.
|
||||
|
||||
1.33.3
|
||||
* Fix connection issues. Pairing again might be needed in some cases.
|
||||
* Add a setting to export the application logs.
|
||||
|
||||
1.33.0
|
||||
* Add support for PeerTube links.
|
||||
* Allow filtering notifications from work profile.
|
||||
* Fix bug where devices would unpair without user interaction.
|
||||
* Verification key now changes every second (if both devices support it).
|
@@ -28,12 +28,13 @@ composeMaterialIcons = "1.7.8"
|
||||
composeMaterial3 = "1.4.0"
|
||||
media = "1.7.1"
|
||||
minaCore = "2.2.4"
|
||||
mockitoCore = "5.20.0"
|
||||
mockk = "1.14.6"
|
||||
preferenceKtx = "1.2.1"
|
||||
reactiveStreams = "1.0.4"
|
||||
recyclerview = "1.4.0"
|
||||
rxjava = "2.2.21"
|
||||
sl4j = "2.0.13"
|
||||
slf4j-handroid = "2.0.13"
|
||||
slf4j-simple = "2.0.17"
|
||||
sshdCore = "2.16.0"
|
||||
swiperefreshlayout = "1.1.0"
|
||||
uiToolingPreview = "1.9.2"
|
||||
@@ -80,11 +81,12 @@ apache-sshd-core = { module = "org.apache.sshd:sshd-core", version.ref = "sshdCo
|
||||
apache-sshd-sftp = { module = "org.apache.sshd:sshd-sftp", version.ref = "sshdCore" }
|
||||
apache-sshd-scp = { module = "org.apache.sshd:sshd-scp", version.ref = "sshdCore" }
|
||||
apache-sshd-mina = { module = "org.apache.sshd:sshd-mina", version.ref = "sshdCore" }
|
||||
mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockitoCore" }
|
||||
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
|
||||
reactive-streams = { module = "org.reactivestreams:reactive-streams", version.ref = "reactiveStreams" }
|
||||
rxjava = { module = "io.reactivex.rxjava2:rxjava", version.ref = "rxjava" }
|
||||
univocity-parsers = { module = "com.univocity:univocity-parsers", version.ref = "univocityParsers" }
|
||||
slf4j-handroid = { group = "com.gitlab.mvysny.slf4j", name = "slf4j-handroid", version.ref = "sl4j" }
|
||||
slf4j-handroid = { module = "com.gitlab.mvysny.slf4j:slf4j-handroid", version.ref = "slf4j-handroid" }
|
||||
slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j-simple" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
|
||||
|
@@ -65,6 +65,8 @@
|
||||
<string name="mousepad_scroll_sensitivity_title">Sensibilitat del desplaçament</string>
|
||||
<string name="gyro_mouse_enabled_title">Activa el ratolí giroscòpic</string>
|
||||
<string name="gyro_mouse_sensitivity_title">Sensibilitat del giroscopi</string>
|
||||
<string name="bigscreen_show_home_title">Mostra el botó inici</string>
|
||||
<string name="bigscreen_show_back_title">Mostra el botó enrere</string>
|
||||
<string-array name="mousepad_tap_entries">
|
||||
<item>Clic esquerre</item>
|
||||
<item>Clic dret</item>
|
||||
@@ -347,6 +349,7 @@
|
||||
<string name="bigscreen_select">Selecció</string>
|
||||
<string name="bigscreen_right">Dreta</string>
|
||||
<string name="bigscreen_down">Avall</string>
|
||||
<string name="bigscreen_back">Enrere</string>
|
||||
<string name="bigscreen_mic">Micròfon</string>
|
||||
<string name="pref_plugin_bigscreen">Bigscreen remota</string>
|
||||
<string name="pref_plugin_bigscreen_desc">Useu el dispositiu com a remot per a la Bigscreen del Plasma</string>
|
||||
|
@@ -65,6 +65,8 @@
|
||||
<string name="mousepad_scroll_sensitivity_title">Sensibilidade da rolagem</string>
|
||||
<string name="gyro_mouse_enabled_title">Ativar mouse giroscópio</string>
|
||||
<string name="gyro_mouse_sensitivity_title">Sensibilidade do giroscópio</string>
|
||||
<string name="bigscreen_show_home_title">Mostrar botão de início</string>
|
||||
<string name="bigscreen_show_back_title">Mostrar botão de voltar</string>
|
||||
<string-array name="mousepad_tap_entries">
|
||||
<item>Clique esquerdo</item>
|
||||
<item>Clique direito</item>
|
||||
@@ -347,6 +349,7 @@
|
||||
<string name="bigscreen_select">Selecionar</string>
|
||||
<string name="bigscreen_right">Direita</string>
|
||||
<string name="bigscreen_down">Para baixo</string>
|
||||
<string name="bigscreen_back">Voltar</string>
|
||||
<string name="bigscreen_mic">Mic</string>
|
||||
<string name="pref_plugin_bigscreen">Controle remoto do Bigscreen</string>
|
||||
<string name="pref_plugin_bigscreen_desc">Use seu dispositivo como um controle remoto para o Plasma Bigscreen</string>
|
||||
|
@@ -65,6 +65,8 @@
|
||||
<string name="mousepad_scroll_sensitivity_title">Občutljivost drsenja</string>
|
||||
<string name="gyro_mouse_enabled_title">Omogoči žiroskopsko miško</string>
|
||||
<string name="gyro_mouse_sensitivity_title">Občutljivost žiroskopa</string>
|
||||
<string name="bigscreen_show_home_title">Prikaži gumb Domov</string>
|
||||
<string name="bigscreen_show_back_title">Prikaži gumb Nazaj</string>
|
||||
<string-array name="mousepad_tap_entries">
|
||||
<item>Levi klik</item>
|
||||
<item>Desni klik</item>
|
||||
@@ -132,10 +134,10 @@
|
||||
<item quantity="other">Prejemanje %1$d datotek od %2$s</item>
|
||||
</plurals>
|
||||
<plurals name="incoming_files_text">
|
||||
<item quantity="one">(Datoteka %2$d od %3$d) : %1$s</item>
|
||||
<item quantity="one">(Datoteke %2$d od %3$d) : %1$s</item>
|
||||
<item quantity="two">(Datoteka %2$d od %3$d) : %1$s</item>
|
||||
<item quantity="few">(Datoteka %2$d od %3$d) : %1$s</item>
|
||||
<item quantity="other">(Datoteka %2$d od %3$d) : %1$s</item>
|
||||
<item quantity="few">(Datoteki %2$d od %3$d) : %1$s</item>
|
||||
<item quantity="other">(Datoteke %2$d od %3$d) : %1$s</item>
|
||||
</plurals>
|
||||
<plurals name="outgoing_file_title">
|
||||
<item quantity="one">Pošiljanje %1$d datotek na %2$s</item>
|
||||
@@ -363,6 +365,7 @@
|
||||
<string name="bigscreen_select">Izberi</string>
|
||||
<string name="bigscreen_right">Desno</string>
|
||||
<string name="bigscreen_down">Dol</string>
|
||||
<string name="bigscreen_back">Nazaj</string>
|
||||
<string name="bigscreen_mic">Mikrofon</string>
|
||||
<string name="pref_plugin_bigscreen">Oddaljeni veliki zaslon</string>
|
||||
<string name="pref_plugin_bigscreen_desc">Uporabite svojo napravo kot daljinec za Plasmin veliki zaslon</string>
|
||||
|
@@ -65,6 +65,8 @@
|
||||
<string name="mousepad_scroll_sensitivity_title">Rullningskänslighet</string>
|
||||
<string name="gyro_mouse_enabled_title">Aktivera gyroskopisk mus</string>
|
||||
<string name="gyro_mouse_sensitivity_title">Gyroskop-känslighet</string>
|
||||
<string name="bigscreen_show_home_title">Visa hemknapp</string>
|
||||
<string name="bigscreen_show_back_title">Visa bakåtknapp</string>
|
||||
<string-array name="mousepad_tap_entries">
|
||||
<item>Vänsterklick</item>
|
||||
<item>Högerklick</item>
|
||||
@@ -347,6 +349,7 @@
|
||||
<string name="bigscreen_select">Markera</string>
|
||||
<string name="bigscreen_right">Höger</string>
|
||||
<string name="bigscreen_down">Neråt</string>
|
||||
<string name="bigscreen_back">Bakåt</string>
|
||||
<string name="bigscreen_mic">Mikrofon</string>
|
||||
<string name="pref_plugin_bigscreen">Fjärrkontroll för storskärm</string>
|
||||
<string name="pref_plugin_bigscreen_desc">Använd apparaten som en fjärrkontroll för Plasma storskärm</string>
|
||||
|
@@ -65,6 +65,8 @@
|
||||
<string name="mousepad_scroll_sensitivity_title">Sarma duyarlılığı</string>
|
||||
<string name="gyro_mouse_enabled_title">Jiroskop fareyi etkinleştir</string>
|
||||
<string name="gyro_mouse_sensitivity_title">Jiroskop duyarlılığı</string>
|
||||
<string name="bigscreen_show_home_title">Ana Ekran düğmesini göster</string>
|
||||
<string name="bigscreen_show_back_title">Geri düğmesini göster</string>
|
||||
<string-array name="mousepad_tap_entries">
|
||||
<item>Sol tık</item>
|
||||
<item>Sağ tık</item>
|
||||
@@ -347,6 +349,7 @@
|
||||
<string name="bigscreen_select">Seç</string>
|
||||
<string name="bigscreen_right">Sağ</string>
|
||||
<string name="bigscreen_down">Aşağı</string>
|
||||
<string name="bigscreen_back">Geri</string>
|
||||
<string name="bigscreen_mic">Mikrofon</string>
|
||||
<string name="pref_plugin_bigscreen">Büyük Ekran uzaktan kumandası</string>
|
||||
<string name="pref_plugin_bigscreen_desc">Aygıtınızı Plasma Büyük Ekran için uzaktan kumanda olarak kullanın</string>
|
||||
|
@@ -65,6 +65,8 @@
|
||||
<string name="mousepad_scroll_sensitivity_title">Чутливість до гортання</string>
|
||||
<string name="gyro_mouse_enabled_title">Увімкнути гіроскопічну мишу</string>
|
||||
<string name="gyro_mouse_sensitivity_title">Чутливість гіроскопа</string>
|
||||
<string name="bigscreen_show_home_title">Показувати кнопку «Домівка»</string>
|
||||
<string name="bigscreen_show_back_title">Показувати кнопку «Назад»</string>
|
||||
<string-array name="mousepad_tap_entries">
|
||||
<item>Клацання лівою</item>
|
||||
<item>Клацання правою</item>
|
||||
@@ -363,6 +365,7 @@
|
||||
<string name="bigscreen_select">Вибрати</string>
|
||||
<string name="bigscreen_right">Праворуч</string>
|
||||
<string name="bigscreen_down">Вниз</string>
|
||||
<string name="bigscreen_back">Назад</string>
|
||||
<string name="bigscreen_mic">Мікрофон</string>
|
||||
<string name="pref_plugin_bigscreen">Керування великим екраном</string>
|
||||
<string name="pref_plugin_bigscreen_desc">Скористайтеся вашим пристроєм як дистанційним керуванням для великого екрана Плазми</string>
|
||||
|
@@ -284,7 +284,7 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
|
||||
<string name="custom_devices_settings_summary">%d devices added manually</string>
|
||||
<string name="custom_device_list">Add devices by IP</string>
|
||||
<string name="custom_device_deleted">Custom device deleted</string>
|
||||
<string name="custom_device_list_help">If your device is not automatically detected you can add its IP address or hostname by clicking on the Floating Action Button</string>
|
||||
<string name="custom_device_list_help">If your device is not automatically detected, you can add its IP address or hostname by clicking on the button below</string>
|
||||
<string name="custom_device_fab_hint">Add a device</string>
|
||||
<string name="undo">Undo</string>
|
||||
<string name="share_notification_preference">Noisy notifications</string>
|
||||
|
@@ -25,15 +25,14 @@ import java.util.UUID
|
||||
import kotlin.text.Charsets.UTF_8
|
||||
|
||||
class BluetoothLink(
|
||||
context: Context?,
|
||||
connection: ConnectionMultiplexer,
|
||||
context: Context,
|
||||
private val connection: ConnectionMultiplexer,
|
||||
val input: InputStream,
|
||||
val output: OutputStream,
|
||||
val remoteAddress: BluetoothDevice,
|
||||
val theDeviceInfo: DeviceInfo,
|
||||
val linkProvider: BluetoothLinkProvider
|
||||
) : BaseLink(context!!, linkProvider) {
|
||||
private val connection: ConnectionMultiplexer? = connection
|
||||
) : BaseLink(context, linkProvider) {
|
||||
private var continueAccepting = true
|
||||
private val receivingThread = Thread(object : Runnable {
|
||||
override fun run() {
|
||||
@@ -99,13 +98,10 @@ class BluetoothLink(
|
||||
}
|
||||
|
||||
override fun disconnect() {
|
||||
if (connection == null) {
|
||||
return
|
||||
}
|
||||
continueAccepting = false
|
||||
try {
|
||||
connection.close()
|
||||
} catch (ignored: IOException) {
|
||||
} catch (_: IOException) {
|
||||
}
|
||||
linkProvider.disconnectedLink(this, remoteAddress)
|
||||
}
|
||||
@@ -124,7 +120,7 @@ class BluetoothLink(
|
||||
return try {
|
||||
var transferUuid: UUID? = null
|
||||
if (np.hasPayload()) {
|
||||
transferUuid = connection!!.newChannel()
|
||||
transferUuid = connection.newChannel()
|
||||
val payloadTransferInfo = JSONObject()
|
||||
payloadTransferInfo.put("uuid", transferUuid.toString())
|
||||
np.payloadTransferInfo = payloadTransferInfo
|
||||
@@ -132,7 +128,7 @@ class BluetoothLink(
|
||||
sendMessage(np)
|
||||
if (transferUuid != null) {
|
||||
try {
|
||||
connection!!.getChannelOutputStream(transferUuid).use { payloadStream ->
|
||||
connection.getChannelOutputStream(transferUuid).use { payloadStream ->
|
||||
val BUFFER_LENGTH = 1024
|
||||
val buffer = ByteArray(BUFFER_LENGTH)
|
||||
var bytesRead: Int
|
||||
|
@@ -133,6 +133,7 @@ class ConnectionMultiplexer(socket: BluetoothSocket) : Closeable {
|
||||
override fun close() {
|
||||
flush()
|
||||
lock.withLock {
|
||||
if (!open) return
|
||||
open = false
|
||||
readBuffer.clear()
|
||||
lockCondition.signalAll()
|
||||
@@ -158,7 +159,7 @@ class ConnectionMultiplexer(socket: BluetoothSocket) : Closeable {
|
||||
if (freeWriteAmount == 0) {
|
||||
try {
|
||||
lockCondition.await()
|
||||
} catch (ignored: Exception) {
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
} else {
|
||||
break
|
||||
@@ -177,9 +178,9 @@ class ConnectionMultiplexer(socket: BluetoothSocket) : Closeable {
|
||||
}
|
||||
}
|
||||
|
||||
private var socket: BluetoothSocket?
|
||||
private val socket: BluetoothSocket
|
||||
private val channels: MutableMap<UUID, Channel> = HashMap()
|
||||
private val lock = ReentrantLock()
|
||||
private val channelsLock = ReentrantLock()
|
||||
private var open = true
|
||||
private var receivedProtocolVersion = false
|
||||
|
||||
@@ -199,27 +200,27 @@ class ConnectionMultiplexer(socket: BluetoothSocket) : Closeable {
|
||||
message.position(19)
|
||||
message.putShort(1.toShort())
|
||||
message.putShort(1.toShort())
|
||||
socket!!.outputStream.write(data)
|
||||
socket.outputStream.write(data)
|
||||
}
|
||||
|
||||
private fun handleException(@Suppress("UNUSED_PARAMETER") ignored: Exception) {
|
||||
lock.withLock {
|
||||
channelsLock.withLock {
|
||||
open = false
|
||||
for (channel in channels.values) {
|
||||
channel.doClose()
|
||||
}
|
||||
channels.clear()
|
||||
if (socket != null && socket!!.isConnected) {
|
||||
if (socket.isConnected) {
|
||||
try {
|
||||
socket!!.close()
|
||||
} catch (ignored: IOException) {
|
||||
socket.close()
|
||||
} catch (_: IOException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun closeChannel(id: UUID) {
|
||||
lock.withLock {
|
||||
channelsLock.withLock {
|
||||
if (channels.containsKey(id)) {
|
||||
channels.remove(id)
|
||||
val data = ByteArray(19)
|
||||
@@ -230,7 +231,7 @@ class ConnectionMultiplexer(socket: BluetoothSocket) : Closeable {
|
||||
message.putLong(id.mostSignificantBits)
|
||||
message.putLong(id.leastSignificantBits)
|
||||
try {
|
||||
socket!!.outputStream.write(data)
|
||||
socket.outputStream.write(data)
|
||||
} catch (e: IOException) {
|
||||
handleException(e)
|
||||
}
|
||||
@@ -239,7 +240,7 @@ class ConnectionMultiplexer(socket: BluetoothSocket) : Closeable {
|
||||
}
|
||||
|
||||
private fun readRequest(id: UUID) {
|
||||
lock.withLock {
|
||||
channelsLock.withLock {
|
||||
val channel = channels[id] ?: return
|
||||
val data = ByteArray(21)
|
||||
channel.lock.withLock {
|
||||
@@ -254,7 +255,7 @@ class ConnectionMultiplexer(socket: BluetoothSocket) : Closeable {
|
||||
message.putShort(amount.toShort())
|
||||
channel.requestedReadAmount += amount
|
||||
try {
|
||||
socket!!.outputStream.write(data)
|
||||
socket.outputStream.write(data)
|
||||
} catch (e: IOException) {
|
||||
handleException(e)
|
||||
} catch (e: NullPointerException) {
|
||||
@@ -267,7 +268,7 @@ class ConnectionMultiplexer(socket: BluetoothSocket) : Closeable {
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun writeRequest(id: UUID, writeData: ByteArray, off: Int, writeLen: Int): Int {
|
||||
lock.withLock {
|
||||
channelsLock.withLock {
|
||||
val channel = channels[id] ?: return 0
|
||||
val data = ByteArray(19 + BUFFER_SIZE)
|
||||
var length: Int
|
||||
@@ -296,7 +297,7 @@ class ConnectionMultiplexer(socket: BluetoothSocket) : Closeable {
|
||||
channel.lockCondition.signalAll()
|
||||
}
|
||||
try {
|
||||
socket!!.outputStream.write(data, 0, 19 + length)
|
||||
socket.outputStream.write(data, 0, 19 + length)
|
||||
} catch (e: IOException) {
|
||||
handleException(e)
|
||||
}
|
||||
@@ -306,29 +307,27 @@ class ConnectionMultiplexer(socket: BluetoothSocket) : Closeable {
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun flush() {
|
||||
lock.withLock {
|
||||
channelsLock.withLock {
|
||||
if (!open) return
|
||||
socket!!.outputStream.flush()
|
||||
socket.outputStream.flush()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun close() {
|
||||
if (socket == null) {
|
||||
return
|
||||
channelsLock.withLock {
|
||||
socket.close()
|
||||
for (channel in channels.values) {
|
||||
channel.doClose()
|
||||
}
|
||||
channels.clear()
|
||||
}
|
||||
socket!!.close()
|
||||
socket = null
|
||||
for (channel in channels.values) {
|
||||
channel.doClose()
|
||||
}
|
||||
channels.clear()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun newChannel(): UUID {
|
||||
val id = UUID.randomUUID()
|
||||
lock.withLock {
|
||||
channelsLock.withLock {
|
||||
val data = ByteArray(19)
|
||||
val message = ByteBuffer.wrap(data)
|
||||
message.order(ByteOrder.BIG_ENDIAN)
|
||||
@@ -337,7 +336,7 @@ class ConnectionMultiplexer(socket: BluetoothSocket) : Closeable {
|
||||
message.putLong(id.mostSignificantBits)
|
||||
message.putLong(id.leastSignificantBits)
|
||||
try {
|
||||
socket!!.outputStream.write(data)
|
||||
socket.outputStream.write(data)
|
||||
} catch (e: IOException) {
|
||||
handleException(e)
|
||||
throw e
|
||||
@@ -357,7 +356,7 @@ class ConnectionMultiplexer(socket: BluetoothSocket) : Closeable {
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun getChannelInputStream(id: UUID): InputStream {
|
||||
lock.withLock {
|
||||
channelsLock.withLock {
|
||||
val channel = channels[id] ?: throw IOException("Invalid channel!")
|
||||
return ChannelInputStream(channel)
|
||||
}
|
||||
@@ -365,7 +364,7 @@ class ConnectionMultiplexer(socket: BluetoothSocket) : Closeable {
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun getChannelOutputStream(id: UUID): OutputStream {
|
||||
lock.withLock {
|
||||
channelsLock.withLock {
|
||||
val channel = channels[id] ?: throw IOException("Invalid channel!")
|
||||
return ChannelOutputStream(channel)
|
||||
}
|
||||
@@ -416,12 +415,12 @@ class ConnectionMultiplexer(socket: BluetoothSocket) : Closeable {
|
||||
}
|
||||
when (type) {
|
||||
MESSAGE_OPEN_CHANNEL -> {
|
||||
lock.withLock {
|
||||
channelsLock.withLock {
|
||||
channels.put(channelId, Channel(this@ConnectionMultiplexer, channelId))
|
||||
}
|
||||
}
|
||||
MESSAGE_CLOSE_CHANNEL -> {
|
||||
lock.withLock {
|
||||
channelsLock.withLock {
|
||||
val channel = channels[channelId] ?: return
|
||||
channels.remove(channelId)
|
||||
channel.doClose()
|
||||
@@ -435,7 +434,7 @@ class ConnectionMultiplexer(socket: BluetoothSocket) : Closeable {
|
||||
var amount = ByteBuffer.wrap(data, 0, 2).order(ByteOrder.BIG_ENDIAN).short.toInt()
|
||||
//signed short -> unsigned short (as int) conversion
|
||||
if (amount < 0) amount += 0x10000
|
||||
lock.withLock {
|
||||
channelsLock.withLock {
|
||||
val channel = channels[channelId] ?: return
|
||||
channel.lock.withLock {
|
||||
channel.freeWriteAmount += amount
|
||||
@@ -448,7 +447,7 @@ class ConnectionMultiplexer(socket: BluetoothSocket) : Closeable {
|
||||
throw IOException("Message length is bigger than read size!")
|
||||
}
|
||||
readBuffer(data, length)
|
||||
lock.withLock {
|
||||
channelsLock.withLock {
|
||||
val channel = channels[channelId] ?: return
|
||||
channel.lock.withLock {
|
||||
if (channel.requestedReadAmount < length) {
|
||||
@@ -495,7 +494,7 @@ class ConnectionMultiplexer(socket: BluetoothSocket) : Closeable {
|
||||
|
||||
override fun run() {
|
||||
while (true) {
|
||||
lock.withLock {
|
||||
channelsLock.withLock {
|
||||
if (!open) {
|
||||
Log.w("ConnectionMultiplexer", "connection not open, returning")
|
||||
return
|
||||
|
@@ -47,7 +47,7 @@ object DeviceHelper {
|
||||
((config.screenLayout and Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_LARGE)
|
||||
}
|
||||
|
||||
private val isTv: Boolean by lazy {
|
||||
val isTv: Boolean by lazy {
|
||||
val uiMode = Resources.getSystem().configuration.uiMode
|
||||
(uiMode and Configuration.UI_MODE_TYPE_MASK) == Configuration.UI_MODE_TYPE_TELEVISION
|
||||
}
|
||||
|
@@ -17,55 +17,74 @@ object VideoUrlsHelper {
|
||||
private val peerTubePathPattern = Regex("^/w/[1-9a-km-zA-HJ-NP-Z]{22}(\\?.+)?$")
|
||||
|
||||
@Throws(MalformedURLException::class)
|
||||
fun formatUriWithSeek(address: String, position: Long): URL {
|
||||
val positionSeconds = position / 1000 // milliseconds to seconds
|
||||
val url = URL(address)
|
||||
fun formatUriWithSeek(address: String, positionMillis: Long): String {
|
||||
val positionSeconds = positionMillis / 1000
|
||||
if (positionSeconds <= 0) {
|
||||
return url // nothing to do
|
||||
return address // do not change the url if time is zero
|
||||
}
|
||||
val host = url.host.lowercase()
|
||||
|
||||
val (host, path) = URL(address).let { Pair(it.host.lowercase(), it.path) }
|
||||
return when {
|
||||
listOf("youtube.com", "youtu.be", "pornhub.com").any { site -> site in host } -> {
|
||||
url.editParameter("t", Regex("\\d+")) { "$positionSeconds" }
|
||||
editOrAddParameter(address, "t", Regex("\\d+"), "$positionSeconds")
|
||||
}
|
||||
host.contains("vimeo.com") -> {
|
||||
url.editParameter("t", Regex("\\d+s")) { "${positionSeconds}s" }
|
||||
editOrAddParameter(address, "t", Regex("\\d+s"), "${positionSeconds}s")
|
||||
}
|
||||
host.contains("dailymotion.com") -> {
|
||||
url.editParameter("start", Regex("\\d+")) { "$positionSeconds" }
|
||||
editOrAddParameter(address, "start", Regex("\\d+"), "$positionSeconds")
|
||||
}
|
||||
host.contains("twitch.tv") -> {
|
||||
url.editParameter("t", Regex("(\\d+[hH])?(\\d+[mM])?\\d+[sS]")) { formatTimestampHMS(positionSeconds) }
|
||||
editOrAddParameter(address, "t", Regex("(\\d+[hH])?(\\d+[mM])?\\d+[sS]"), formatTimestampHMS(positionSeconds))
|
||||
}
|
||||
url.path.matches(peerTubePathPattern) -> {
|
||||
url.editParameter("start", Regex("(\\d+[hH])?(\\d+[mM])?\\d+[sS]")) { formatTimestampHMS(positionSeconds) }
|
||||
path.matches(peerTubePathPattern) -> {
|
||||
editOrAddParameter(address, "start", Regex("(\\d+[hH])?(\\d+[mM])?\\d+[sS]"), formatTimestampHMS(positionSeconds))
|
||||
}
|
||||
else -> url
|
||||
else -> address
|
||||
}
|
||||
}
|
||||
|
||||
private fun URL.editParameter(parameter: CharSequence, valuePattern: Regex?, parameterValueModifier: (String) -> String): URL {
|
||||
// "https://www.youtube.com/watch?v=ovX5G0O5ZvA&t=13" -> ["https://www.youtube.com/watch", "v=ovX5G0O5ZvA&t=13"]
|
||||
val urlSplit = this.toString().split("?")
|
||||
if (urlSplit.size != 2) {
|
||||
return this
|
||||
}
|
||||
val (urlBase, urlQuery) = urlSplit
|
||||
val modifiedUrlQuery = urlQuery
|
||||
.split("&") // "v=ovX5G0O5ZvA&t=13" -> ["v=ovX5G0O5ZvA", "t=13"]
|
||||
.map { it.split("=", limit = 2) } // […, "t=13"] -> […, ["t", "13"]]
|
||||
.map { Pair(it.first(), it.lastOrNull() ?: return this) }
|
||||
.map { paramAndValue ->
|
||||
// Modify matching parameter and optionally matches the old value with the provided pattern
|
||||
if (paramAndValue.first == parameter && valuePattern?.matches(paramAndValue.second) != false) {
|
||||
Pair(paramAndValue.first, parameterValueModifier(paramAndValue.second)) // ["t", "13"] -> ["t", result]
|
||||
} else {
|
||||
paramAndValue
|
||||
fun editOrAddParameter(
|
||||
url: String,
|
||||
parameter: String,
|
||||
valuePattern: Regex,
|
||||
newValue: String
|
||||
): String {
|
||||
val (urlWithoutFragment, fragment) = url.split("#", limit = 2).let { Pair(it[0], it.getOrNull(1)) }
|
||||
val (baseUrl, query) = urlWithoutFragment.split("?", limit = 2).let { Pair(it[0], it.getOrElse(1) { "" }) }
|
||||
|
||||
val params = query
|
||||
.split("&")
|
||||
.filter { it.isNotEmpty() }
|
||||
.associate {
|
||||
val (key, value) = it.split("=", limit = 2).let { parts ->
|
||||
parts[0] to parts.getOrElse(1) { "" }
|
||||
}
|
||||
key to value
|
||||
}.toMutableMap()
|
||||
|
||||
val currentValue = params[parameter]
|
||||
if (currentValue != null && !currentValue.matches(valuePattern)) {
|
||||
// The argument exists but it doesn't match the format we expect, did we match the wrong url?
|
||||
return url
|
||||
}
|
||||
params[parameter] = newValue
|
||||
|
||||
val newQuery = params.entries.joinToString("&") { "${it.key}=${it.value}" }
|
||||
val newUrlWithoutFragment = if (newQuery.isNotEmpty()) "$baseUrl?$newQuery" else baseUrl
|
||||
return if (fragment != null) "$newUrlWithoutFragment#$fragment" else newUrlWithoutFragment
|
||||
}
|
||||
|
||||
fun convertToAndFromYoutubeTvLinks(url : String): String {
|
||||
if (url.contains("youtube.com/watch") || url.contains("youtube.com/tv")) {
|
||||
val wantTvLinks = DeviceHelper.isTv
|
||||
val isTvLink = url.contains("youtube\\.com/tv.*#/watch".toRegex())
|
||||
if (wantTvLinks && !isTvLink) {
|
||||
return url.replace("youtube.com/watch", "youtube.com/tv#/watch")
|
||||
} else if (!wantTvLinks && isTvLink) {
|
||||
return url.replace("youtube\\.com/tv.*#/watch".toRegex(), "youtube.com/watch")
|
||||
}
|
||||
.joinToString("&") { "${it.first}=${it.second}" } // [["v", "ovX5G0O5ZvA"], ["t", "14"]] -> "v=ovX5G0O5ZvA&t=14"
|
||||
return URL("${urlBase}?${modifiedUrlQuery}") // -> "https://www.youtube.com/watch?v=ovX5G0O5ZvA&t=14"
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -77,7 +77,11 @@ class ConnectivityListener(context: Context) {
|
||||
val tm = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
|
||||
for (subID in removedSubs) {
|
||||
Log.i(TAG, "Removed subscription ID $subID")
|
||||
tm.listen(connectivityListeners.get(subID), PhoneStateListener.LISTEN_NONE)
|
||||
try {
|
||||
tm.listen(connectivityListeners[subID], PhoneStateListener.LISTEN_NONE)
|
||||
} catch (_: Exception) {
|
||||
// It seems like the subscription ID is no longer valid by this point, so this might trigger
|
||||
}
|
||||
connectivityListeners.remove(subID)
|
||||
states.remove(subID)
|
||||
statesChanged()
|
||||
@@ -152,7 +156,7 @@ class ConnectivityListener(context: Context) {
|
||||
}
|
||||
for (subID in connectivityListeners.keys) {
|
||||
Log.i(TAG, "Removed subscription ID $subID")
|
||||
tm.listen(connectivityListeners.get(subID), PhoneStateListener.LISTEN_NONE)
|
||||
tm.listen(connectivityListeners[subID], PhoneStateListener.LISTEN_NONE)
|
||||
}
|
||||
connectivityListeners.clear()
|
||||
states.clear()
|
||||
|
@@ -1,158 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2014 Ahmed I. Khalil <ahmedibrahimkhali@gmail.com>
|
||||
* SPDX-FileCopyrightText: 2020 Sylvia van Os <sylvia@hackerchick.me>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
package org.kde.kdeconnect.Plugins.MousePadPlugin;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.speech.RecognizerIntent;
|
||||
import android.speech.SpeechRecognizer;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.kde.kdeconnect.KdeConnect;
|
||||
import org.kde.kdeconnect.UserInterface.MainActivity;
|
||||
import org.kde.kdeconnect.UserInterface.PermissionsAlertDialogFragment;
|
||||
import org.kde.kdeconnect.base.BaseActivity;
|
||||
import org.kde.kdeconnect_tp.R;
|
||||
import org.kde.kdeconnect_tp.databinding.ActivityBigscreenBinding;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Objects;
|
||||
|
||||
import kotlin.Lazy;
|
||||
import kotlin.LazyKt;
|
||||
|
||||
public class BigscreenActivity extends BaseActivity<ActivityBigscreenBinding> {
|
||||
|
||||
private static final int REQUEST_SPEECH = 100;
|
||||
|
||||
private final Lazy<ActivityBigscreenBinding> lazyBinding = LazyKt.lazy(() -> ActivityBigscreenBinding.inflate(getLayoutInflater()));
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected ActivityBigscreenBinding getBinding() {
|
||||
return lazyBinding.getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setSupportActionBar(getBinding().toolbarLayout.toolbar);
|
||||
Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||
|
||||
final String deviceId = getIntent().getStringExtra("deviceId");
|
||||
|
||||
if (!SpeechRecognizer.isRecognitionAvailable(this)) {
|
||||
getBinding().micButton.setEnabled(false);
|
||||
getBinding().micButton.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
|
||||
if (plugin == null) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
getBinding().leftButton.setOnClickListener(v -> plugin.sendLeft());
|
||||
getBinding().rightButton.setOnClickListener(v -> plugin.sendRight());
|
||||
getBinding().upButton.setOnClickListener(v -> plugin.sendUp());
|
||||
getBinding().downButton.setOnClickListener(v -> plugin.sendDown());
|
||||
getBinding().selectButton.setOnClickListener(v -> plugin.sendSelect());
|
||||
getBinding().homeButton.setOnClickListener(v -> plugin.sendHome());
|
||||
getBinding().backButton.setOnClickListener(v -> plugin.sendBack());
|
||||
getBinding().micButton.setOnClickListener(v -> {
|
||||
if (plugin.hasMicPermission()) {
|
||||
activateSTT();
|
||||
} else {
|
||||
new PermissionsAlertDialogFragment.Builder()
|
||||
.setTitle(plugin.getDisplayName())
|
||||
.setMessage(R.string.bigscreen_optional_permission_explanation)
|
||||
.setPermissions(new String[]{Manifest.permission.RECORD_AUDIO})
|
||||
.setRequestCode(MainActivity.RESULT_NEEDS_RELOAD)
|
||||
.create().show(getSupportFragmentManager(), null);
|
||||
}
|
||||
});
|
||||
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
if (prefs.getBoolean(getString(R.string.pref_bigscreen_show_back), true)) {
|
||||
getBinding().backButton.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
getBinding().backButton.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
if (prefs.getBoolean(getString(R.string.pref_bigscreen_show_home), false)) {
|
||||
getBinding().homeButton.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
getBinding().homeButton.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
public void activateSTT() {
|
||||
Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
|
||||
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
|
||||
intent.putExtra(RecognizerIntent.EXTRA_PROMPT, R.string.bigscreen_speech_extra_prompt);
|
||||
startActivityForResult(intent, REQUEST_SPEECH);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.menu_bigscreen, menu);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
int id = item.getItemId();
|
||||
if (id == R.id.menu_use_mouse_and_keyboard) {
|
||||
Intent intent = new Intent(this, MousePadActivity.class);
|
||||
intent.putExtra("deviceId", getIntent().getStringExtra("deviceId"));
|
||||
startActivity(intent);
|
||||
return true;
|
||||
} else {
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (requestCode == REQUEST_SPEECH) {
|
||||
if (resultCode == Activity.RESULT_OK && data != null) {
|
||||
ArrayList<String> result = data
|
||||
.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
|
||||
if (result.get(0) != null) {
|
||||
final String deviceId = getIntent().getStringExtra("deviceId");
|
||||
MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
|
||||
if (plugin == null) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
plugin.sendText(result.get(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSupportNavigateUp() {
|
||||
super.onBackPressed();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2014 Ahmed I. Khalil <ahmedibrahimkhali@gmail.com>
|
||||
* SPDX-FileCopyrightText: 2020 Sylvia van Os <sylvia@hackerchick.me>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
package org.kde.kdeconnect.Plugins.MousePadPlugin
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.speech.RecognizerIntent
|
||||
import android.speech.SpeechRecognizer
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.ActionBar
|
||||
import androidx.preference.PreferenceManager
|
||||
import org.kde.kdeconnect.KdeConnect.Companion.getInstance
|
||||
import org.kde.kdeconnect.UserInterface.MainActivity
|
||||
import org.kde.kdeconnect.UserInterface.PermissionsAlertDialogFragment
|
||||
import org.kde.kdeconnect.base.BaseActivity
|
||||
import org.kde.kdeconnect.extensions.viewBinding
|
||||
import org.kde.kdeconnect_tp.R
|
||||
import org.kde.kdeconnect_tp.databinding.ActivityBigscreenBinding
|
||||
import java.util.Objects
|
||||
|
||||
class BigscreenActivity : BaseActivity<ActivityBigscreenBinding>() {
|
||||
|
||||
override val binding : ActivityBigscreenBinding by viewBinding(ActivityBigscreenBinding::inflate)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setSupportActionBar(binding.toolbarLayout.toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
|
||||
val deviceId = intent.getStringExtra("deviceId")
|
||||
|
||||
if (!SpeechRecognizer.isRecognitionAvailable(this)) {
|
||||
binding.micButton.isEnabled = false
|
||||
binding.micButton.visibility = View.INVISIBLE
|
||||
}
|
||||
|
||||
val plugin = getInstance().getDevicePlugin(deviceId, MousePadPlugin::class.java)
|
||||
if (plugin == null) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
binding.leftButton.setOnClickListener { v: View? -> plugin.sendLeft() }
|
||||
binding.rightButton.setOnClickListener { v: View? -> plugin.sendRight() }
|
||||
binding.upButton.setOnClickListener { v: View? -> plugin.sendUp() }
|
||||
binding.downButton.setOnClickListener { v: View? -> plugin.sendDown() }
|
||||
binding.selectButton.setOnClickListener { v: View? -> plugin.sendSelect() }
|
||||
binding.homeButton.setOnClickListener { v: View? -> plugin.sendHome() }
|
||||
binding.backButton.setOnClickListener { v: View? -> plugin.sendBack() }
|
||||
binding.micButton.setOnClickListener { v: View? ->
|
||||
if (plugin.hasMicPermission()) {
|
||||
activateSTT()
|
||||
} else {
|
||||
PermissionsAlertDialogFragment.Builder()
|
||||
.setTitle(plugin.displayName)
|
||||
.setMessage(R.string.bigscreen_optional_permission_explanation)
|
||||
.setPermissions(arrayOf(Manifest.permission.RECORD_AUDIO))
|
||||
.setRequestCode(MainActivity.RESULT_NEEDS_RELOAD)
|
||||
.create().show(supportFragmentManager, null)
|
||||
}
|
||||
}
|
||||
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
if (prefs.getBoolean(getString(R.string.pref_bigscreen_show_back), true)) {
|
||||
binding.backButton.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.backButton.visibility = View.INVISIBLE
|
||||
}
|
||||
if (prefs.getBoolean(getString(R.string.pref_bigscreen_show_home), false)) {
|
||||
binding.homeButton.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.homeButton.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
fun activateSTT() {
|
||||
val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)
|
||||
intent.putExtra(
|
||||
RecognizerIntent.EXTRA_LANGUAGE_MODEL,
|
||||
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM
|
||||
)
|
||||
intent.putExtra(RecognizerIntent.EXTRA_PROMPT, R.string.bigscreen_speech_extra_prompt)
|
||||
startActivityForResult(intent, REQUEST_SPEECH)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_bigscreen, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
val id = item.itemId
|
||||
if (id == R.id.menu_use_mouse_and_keyboard) {
|
||||
val intent = Intent(this, MousePadActivity::class.java)
|
||||
intent.putExtra("deviceId", getIntent().getStringExtra("deviceId"))
|
||||
startActivity(intent)
|
||||
return true
|
||||
} else {
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (requestCode == REQUEST_SPEECH && resultCode == RESULT_OK) {
|
||||
// The results are ordered by confidence, use the first one
|
||||
val firstResult = data?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)?.first()
|
||||
if (firstResult != null) {
|
||||
val deviceId = intent.getStringExtra("deviceId")
|
||||
val plugin = getInstance().getDevicePlugin(deviceId,MousePadPlugin::class.java)
|
||||
if (plugin == null) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
plugin.sendText(firstResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
super.onBackPressed()
|
||||
return true
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val REQUEST_SPEECH = 100
|
||||
}
|
||||
}
|
||||
|
@@ -1,217 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2014 Ahmed I. Khalil <ahmedibrahimkhali@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
package org.kde.kdeconnect.Plugins.MousePadPlugin;
|
||||
|
||||
import static org.kde.kdeconnect.Plugins.MousePadPlugin.KeyListenerView.SpecialKeysMap;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.view.KeyEvent;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.kde.kdeconnect.DeviceType;
|
||||
import org.kde.kdeconnect.NetworkPacket;
|
||||
import org.kde.kdeconnect.Plugins.Plugin;
|
||||
import org.kde.kdeconnect.Plugins.PluginFactory;
|
||||
import org.kde.kdeconnect.UserInterface.PluginSettingsFragment;
|
||||
import org.kde.kdeconnect_tp.R;
|
||||
|
||||
@PluginFactory.LoadablePlugin
|
||||
public class MousePadPlugin extends Plugin {
|
||||
|
||||
public final static String PACKET_TYPE_MOUSEPAD_REQUEST = "kdeconnect.mousepad.request";
|
||||
private final static String PACKET_TYPE_MOUSEPAD_KEYBOARDSTATE = "kdeconnect.mousepad.keyboardstate";
|
||||
|
||||
private boolean keyboardEnabled = true;
|
||||
|
||||
@Override
|
||||
public boolean onPacketReceived(@NonNull NetworkPacket np) {
|
||||
|
||||
keyboardEnabled = np.getBoolean("state", true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getDisplayName() {
|
||||
return context.getString(R.string.pref_plugin_mousepad);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getDescription() {
|
||||
return context.getString(R.string.pref_plugin_mousepad_desc_nontv);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @DrawableRes int getIcon() {
|
||||
return R.drawable.touchpad_plugin_action_24dp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSettings() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PluginSettingsFragment getSettingsFragment(Activity activity) {
|
||||
if (device.getDeviceType() == DeviceType.TV) {
|
||||
return PluginSettingsFragment.newInstance(getPluginKey(), R.xml.mousepadplugin_preferences, R.xml.mousepadplugin_preferences_tv);
|
||||
} else {
|
||||
return PluginSettingsFragment.newInstance(getPluginKey(), R.xml.mousepadplugin_preferences);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean displayAsButton(Context context) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startMainActivity(Activity parentActivity) {
|
||||
if (device.getDeviceType() == DeviceType.TV) {
|
||||
Intent intent = new Intent(parentActivity, BigscreenActivity.class);
|
||||
intent.putExtra("deviceId", device.getDeviceId());
|
||||
parentActivity.startActivity(intent);
|
||||
} else {
|
||||
Intent intent = new Intent(parentActivity, MousePadActivity.class);
|
||||
intent.putExtra("deviceId", device.getDeviceId());
|
||||
parentActivity.startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String[] getSupportedPacketTypes() {
|
||||
return new String[]{PACKET_TYPE_MOUSEPAD_KEYBOARDSTATE};
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String[] getOutgoingPacketTypes() {
|
||||
return new String[]{PACKET_TYPE_MOUSEPAD_REQUEST};
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getActionName() {
|
||||
return context.getString(R.string.open_mousepad);
|
||||
}
|
||||
|
||||
public void sendMouseDelta(float dx, float dy) {
|
||||
NetworkPacket np = new NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST);
|
||||
np.set("dx", dx);
|
||||
np.set("dy", dy);
|
||||
sendPacket(np);
|
||||
}
|
||||
|
||||
public Boolean hasMicPermission() {
|
||||
return isPermissionGranted(Manifest.permission.RECORD_AUDIO);
|
||||
}
|
||||
|
||||
public void sendLeftClick() {
|
||||
NetworkPacket np = new NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST);
|
||||
np.set("singleclick", true);
|
||||
sendPacket(np);
|
||||
}
|
||||
|
||||
public void sendDoubleClick() {
|
||||
NetworkPacket np = new NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST);
|
||||
np.set("doubleclick", true);
|
||||
sendPacket(np);
|
||||
}
|
||||
|
||||
public void sendMiddleClick() {
|
||||
NetworkPacket np = new NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST);
|
||||
np.set("middleclick", true);
|
||||
sendPacket(np);
|
||||
}
|
||||
|
||||
public void sendRightClick() {
|
||||
NetworkPacket np = new NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST);
|
||||
np.set("rightclick", true);
|
||||
sendPacket(np);
|
||||
}
|
||||
|
||||
public void sendSingleHold() {
|
||||
NetworkPacket np = new NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST);
|
||||
np.set("singlehold", true);
|
||||
sendPacket(np);
|
||||
}
|
||||
|
||||
public void sendSingleRelease() {
|
||||
NetworkPacket np = new NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST);
|
||||
np.set("singlerelease", true);
|
||||
sendPacket(np);
|
||||
}
|
||||
|
||||
public void sendScroll(float dx, float dy) {
|
||||
NetworkPacket np = new NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST);
|
||||
np.set("scroll", true);
|
||||
np.set("dx", dx);
|
||||
np.set("dy", dy);
|
||||
sendPacket(np);
|
||||
}
|
||||
|
||||
public void sendLeft() {
|
||||
NetworkPacket np = new NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST);
|
||||
np.set("specialKey", SpecialKeysMap.get(KeyEvent.KEYCODE_DPAD_LEFT));
|
||||
sendPacket(np);
|
||||
}
|
||||
|
||||
public void sendRight() {
|
||||
NetworkPacket np = new NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST);
|
||||
np.set("specialKey", SpecialKeysMap.get(KeyEvent.KEYCODE_DPAD_RIGHT));
|
||||
sendPacket(np);
|
||||
}
|
||||
|
||||
public void sendUp() {
|
||||
NetworkPacket np = new NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST);
|
||||
np.set("specialKey", SpecialKeysMap.get(KeyEvent.KEYCODE_DPAD_UP));
|
||||
sendPacket(np);
|
||||
}
|
||||
|
||||
public void sendDown() {
|
||||
NetworkPacket np = new NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST);
|
||||
np.set("specialKey", SpecialKeysMap.get(KeyEvent.KEYCODE_DPAD_DOWN));
|
||||
sendPacket(np);
|
||||
}
|
||||
|
||||
public void sendSelect() {
|
||||
NetworkPacket np = new NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST);
|
||||
np.set("specialKey", SpecialKeysMap.get(KeyEvent.KEYCODE_ENTER));
|
||||
sendPacket(np);
|
||||
}
|
||||
|
||||
public void sendHome() {
|
||||
NetworkPacket np = new NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST);
|
||||
np.set("alt", true);
|
||||
np.set("specialKey", SpecialKeysMap.get(KeyEvent.KEYCODE_F4));
|
||||
getDevice().sendPacket(np);
|
||||
}
|
||||
|
||||
public void sendBack() {
|
||||
NetworkPacket np = new NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST);
|
||||
np.set("specialKey", SpecialKeysMap.get(KeyEvent.KEYCODE_ESCAPE));
|
||||
getDevice().sendPacket(np);
|
||||
}
|
||||
|
||||
public void sendText(String content) {
|
||||
NetworkPacket np = new NetworkPacket(MousePadPlugin.PACKET_TYPE_MOUSEPAD_REQUEST);
|
||||
np.set("key", content);
|
||||
sendPacket(np);
|
||||
}
|
||||
|
||||
void sendPacket(NetworkPacket np) {
|
||||
device.sendPacket(np);
|
||||
}
|
||||
|
||||
boolean isKeyboardEnabled() {
|
||||
return keyboardEnabled;
|
||||
}
|
||||
|
||||
}
|
181
src/org/kde/kdeconnect/Plugins/MousePadPlugin/MousePadPlugin.kt
Normal file
181
src/org/kde/kdeconnect/Plugins/MousePadPlugin/MousePadPlugin.kt
Normal file
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2014 Ahmed I. Khalil <ahmedibrahimkhali@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
package org.kde.kdeconnect.Plugins.MousePadPlugin
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.view.KeyEvent
|
||||
import androidx.annotation.DrawableRes
|
||||
import org.kde.kdeconnect.DeviceType
|
||||
import org.kde.kdeconnect.NetworkPacket
|
||||
import org.kde.kdeconnect.Plugins.Plugin
|
||||
import org.kde.kdeconnect.Plugins.PluginFactory.LoadablePlugin
|
||||
import org.kde.kdeconnect.UserInterface.PluginSettingsFragment
|
||||
import org.kde.kdeconnect.UserInterface.PluginSettingsFragment.Companion.newInstance
|
||||
import org.kde.kdeconnect_tp.R
|
||||
|
||||
@LoadablePlugin
|
||||
class MousePadPlugin : Plugin() {
|
||||
var isKeyboardEnabled: Boolean = true
|
||||
private set
|
||||
|
||||
override fun onPacketReceived(np: NetworkPacket): Boolean {
|
||||
this.isKeyboardEnabled = np.getBoolean("state", true)
|
||||
return true
|
||||
}
|
||||
|
||||
override val displayName: String
|
||||
get() = context.getString(R.string.pref_plugin_mousepad)
|
||||
|
||||
override val actionName: String
|
||||
get() = context.getString(R.string.open_mousepad)
|
||||
|
||||
override val description: String
|
||||
get() = context.getString(R.string.pref_plugin_mousepad_desc_nontv)
|
||||
|
||||
@get:DrawableRes
|
||||
override val icon: Int = R.drawable.touchpad_plugin_action_24dp
|
||||
|
||||
override fun displayAsButton(context: Context): Boolean = true
|
||||
|
||||
override fun hasSettings(): Boolean = true
|
||||
|
||||
override fun getSettingsFragment(activity: Activity): PluginSettingsFragment? {
|
||||
return if (device.deviceType == DeviceType.TV) {
|
||||
newInstance(pluginKey, R.xml.mousepadplugin_preferences, R.xml.mousepadplugin_preferences_tv)
|
||||
} else {
|
||||
newInstance(pluginKey, R.xml.mousepadplugin_preferences)
|
||||
}
|
||||
}
|
||||
|
||||
override fun startMainActivity(parentActivity: Activity) {
|
||||
val intent = if (device.deviceType == DeviceType.TV) {
|
||||
Intent(parentActivity, BigscreenActivity::class.java)
|
||||
} else {
|
||||
Intent(parentActivity, MousePadActivity::class.java)
|
||||
}
|
||||
intent.putExtra("deviceId", device.deviceId)
|
||||
parentActivity.startActivity(intent)
|
||||
}
|
||||
|
||||
fun sendMouseDelta(dx: Float, dy: Float) {
|
||||
val np = NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST)
|
||||
np["dx"] = dx.toDouble()
|
||||
np["dy"] = dy.toDouble()
|
||||
sendPacket(np)
|
||||
}
|
||||
|
||||
fun hasMicPermission(): Boolean {
|
||||
return isPermissionGranted(Manifest.permission.RECORD_AUDIO)
|
||||
}
|
||||
|
||||
fun sendLeftClick() {
|
||||
val np = NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST)
|
||||
np["singleclick"] = true
|
||||
sendPacket(np)
|
||||
}
|
||||
|
||||
fun sendDoubleClick() {
|
||||
val np = NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST)
|
||||
np["doubleclick"] = true
|
||||
sendPacket(np)
|
||||
}
|
||||
|
||||
fun sendMiddleClick() {
|
||||
val np = NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST)
|
||||
np["middleclick"] = true
|
||||
sendPacket(np)
|
||||
}
|
||||
|
||||
fun sendRightClick() {
|
||||
val np = NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST)
|
||||
np["rightclick"] = true
|
||||
sendPacket(np)
|
||||
}
|
||||
|
||||
fun sendSingleHold() {
|
||||
val np = NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST)
|
||||
np["singlehold"] = true
|
||||
sendPacket(np)
|
||||
}
|
||||
|
||||
fun sendSingleRelease() {
|
||||
val np = NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST)
|
||||
np["singlerelease"] = true
|
||||
sendPacket(np)
|
||||
}
|
||||
|
||||
fun sendScroll(dx: Float, dy: Float) {
|
||||
val np = NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST)
|
||||
np["scroll"] = true
|
||||
np["dx"] = dx.toDouble()
|
||||
np["dy"] = dy.toDouble()
|
||||
sendPacket(np)
|
||||
}
|
||||
|
||||
fun sendLeft() {
|
||||
val np = NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST)
|
||||
np["specialKey"] = KeyListenerView.SpecialKeysMap.get(KeyEvent.KEYCODE_DPAD_LEFT)
|
||||
sendPacket(np)
|
||||
}
|
||||
|
||||
fun sendRight() {
|
||||
val np = NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST)
|
||||
np["specialKey"] = KeyListenerView.SpecialKeysMap.get(KeyEvent.KEYCODE_DPAD_RIGHT)
|
||||
sendPacket(np)
|
||||
}
|
||||
|
||||
fun sendUp() {
|
||||
val np = NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST)
|
||||
np["specialKey"] = KeyListenerView.SpecialKeysMap.get(KeyEvent.KEYCODE_DPAD_UP)
|
||||
sendPacket(np)
|
||||
}
|
||||
|
||||
fun sendDown() {
|
||||
val np = NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST)
|
||||
np["specialKey"] = KeyListenerView.SpecialKeysMap.get(KeyEvent.KEYCODE_DPAD_DOWN)
|
||||
sendPacket(np)
|
||||
}
|
||||
|
||||
fun sendSelect() {
|
||||
val np = NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST)
|
||||
np["specialKey"] = KeyListenerView.SpecialKeysMap.get(KeyEvent.KEYCODE_ENTER)
|
||||
sendPacket(np)
|
||||
}
|
||||
|
||||
fun sendHome() {
|
||||
val np = NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST)
|
||||
np["alt"] = true
|
||||
np["specialKey"] = KeyListenerView.SpecialKeysMap.get(KeyEvent.KEYCODE_F4)
|
||||
device.sendPacket(np)
|
||||
}
|
||||
|
||||
fun sendBack() {
|
||||
val np = NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST)
|
||||
np["specialKey"] = KeyListenerView.SpecialKeysMap.get(KeyEvent.KEYCODE_ESCAPE)
|
||||
device.sendPacket(np)
|
||||
}
|
||||
|
||||
fun sendText(content: String?) {
|
||||
val np = NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST)
|
||||
np["key"] = content
|
||||
sendPacket(np)
|
||||
}
|
||||
|
||||
fun sendPacket(np: NetworkPacket) {
|
||||
device.sendPacket(np)
|
||||
}
|
||||
|
||||
override val supportedPacketTypes = arrayOf(PACKET_TYPE_MOUSEPAD_KEYBOARDSTATE)
|
||||
override val outgoingPacketTypes = arrayOf(PACKET_TYPE_MOUSEPAD_REQUEST)
|
||||
|
||||
companion object {
|
||||
const val PACKET_TYPE_MOUSEPAD_REQUEST: String = "kdeconnect.mousepad.request"
|
||||
private const val PACKET_TYPE_MOUSEPAD_KEYBOARDSTATE = "kdeconnect.mousepad.keyboardstate"
|
||||
}
|
||||
}
|
@@ -7,7 +7,6 @@ package org.kde.kdeconnect.Plugins.MprisPlugin
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.preference.PreferenceManager
|
||||
@@ -35,6 +34,8 @@ import java.net.MalformedURLException
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.lifecycle.Lifecycle
|
||||
|
||||
private typealias MprisPlayerCallback = (MprisPlayer) -> Unit
|
||||
|
||||
@@ -351,6 +352,42 @@ class MprisNowPlayingFragment : Fragment(), VolumeKeyListener {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
activity?.addMenuProvider(object : MenuProvider {
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) = Unit
|
||||
override fun onPrepareMenu(menu: Menu) {
|
||||
menu.clear()
|
||||
if (!targetPlayer?.url.isNullOrEmpty()) {
|
||||
menu.add(0, MENU_OPEN_URL, Menu.NONE, R.string.mpris_open_url)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
val targetPlayer = targetPlayer
|
||||
if (targetPlayer != null && menuItem.itemId == MENU_OPEN_URL) {
|
||||
try {
|
||||
val url = targetPlayer.url
|
||||
.let { VideoUrlsHelper.convertToAndFromYoutubeTvLinks(it) }
|
||||
.let { VideoUrlsHelper.formatUriWithSeek(it, targetPlayer.position) }
|
||||
.toUri()
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, url)
|
||||
startActivity(browserIntent)
|
||||
targetPlayer.sendPause()
|
||||
return true
|
||||
} catch (e: MalformedURLException) {
|
||||
e.printStackTrace()
|
||||
Toast.makeText(requireContext(), getString(R.string.cant_open_url), Toast.LENGTH_LONG).show()
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
e.printStackTrace()
|
||||
Toast.makeText(requireContext(), getString(R.string.cant_open_url), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
||||
}
|
||||
|
||||
override fun onVolumeUp() {
|
||||
updateVolume(DEFAULT_VOLUME_STEP)
|
||||
}
|
||||
@@ -359,33 +396,6 @@ class MprisNowPlayingFragment : Fragment(), VolumeKeyListener {
|
||||
updateVolume(-DEFAULT_VOLUME_STEP)
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
menu.clear()
|
||||
if (!targetPlayer?.url.isNullOrEmpty()) {
|
||||
menu.add(0, MENU_OPEN_URL, Menu.NONE, R.string.mpris_open_url)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
val targetPlayer = targetPlayer
|
||||
if (targetPlayer != null && item.itemId == MENU_OPEN_URL) {
|
||||
try {
|
||||
val url = VideoUrlsHelper.formatUriWithSeek(targetPlayer.url, targetPlayer.position).toString()
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||
startActivity(browserIntent)
|
||||
targetPlayer.sendPause()
|
||||
return true
|
||||
} catch (e: MalformedURLException) {
|
||||
e.printStackTrace()
|
||||
Toast.makeText(requireContext(), getString(R.string.cant_open_url), Toast.LENGTH_LONG).show()
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
e.printStackTrace()
|
||||
Toast.makeText(requireContext(), getString(R.string.cant_open_url), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
if (targetPlayer != null) {
|
||||
outState.putString("targetPlayer", targetPlayerName)
|
||||
|
@@ -12,7 +12,6 @@ import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.DrawableRes
|
||||
@@ -334,8 +333,11 @@ class MprisPlugin : Plugin() {
|
||||
(playerStatus.url.startsWith("http://") || playerStatus.url.startsWith("https://"))
|
||||
) {
|
||||
try {
|
||||
val url = VideoUrlsHelper.formatUriWithSeek(playerStatus.url, playerStatus.position).toString()
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||
val url = playerStatus.url
|
||||
.let { VideoUrlsHelper.convertToAndFromYoutubeTvLinks(it) }
|
||||
.let { VideoUrlsHelper.formatUriWithSeek(it, playerStatus.position) }
|
||||
.toUri()
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, url)
|
||||
val pendingIntent = PendingIntent.getActivity(context, 0, browserIntent, PendingIntent.FLAG_IMMUTABLE)
|
||||
|
||||
val notificationManager = ContextCompat.getSystemService(context, NotificationManager::class.java)
|
||||
|
@@ -10,6 +10,11 @@ import android.content.Context
|
||||
import android.preference.PreferenceManager
|
||||
import android.util.Base64
|
||||
import androidx.core.content.ContextCompat
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkAll
|
||||
import io.mockk.verify
|
||||
import org.junit.After
|
||||
import org.junit.Assert
|
||||
import org.junit.Before
|
||||
@@ -25,23 +30,14 @@ import org.kde.kdeconnect.Helpers.DeviceHelper
|
||||
import org.kde.kdeconnect.Helpers.SecurityHelpers.RsaHelper
|
||||
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper
|
||||
import org.kde.kdeconnect.PairingHandler.PairingCallback
|
||||
import org.mockito.ArgumentMatchers
|
||||
import org.mockito.MockedStatic
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.invocation.InvocationOnMock
|
||||
import java.security.cert.CertificateException
|
||||
|
||||
class DeviceTest {
|
||||
private lateinit var context: Context
|
||||
private lateinit var mockBase64: MockedStatic<Base64>
|
||||
private lateinit var preferenceManager: MockedStatic<PreferenceManager>
|
||||
private lateinit var contextCompat: MockedStatic<ContextCompat>
|
||||
private val context: Context = mockk()
|
||||
|
||||
// Creating a paired device before each test case
|
||||
@Before
|
||||
fun setUp() {
|
||||
// Save new test device in settings
|
||||
|
||||
val deviceId = "testDevice"
|
||||
val name = "Test Device"
|
||||
val encodedCertificate = """
|
||||
@@ -63,19 +59,13 @@ class DeviceTest {
|
||||
7n+KOQ==
|
||||
""".trimIndent()
|
||||
|
||||
val context = Mockito.mock(Context::class.java)
|
||||
val mockBase64 = Mockito.mockStatic(Base64::class.java)
|
||||
|
||||
mockBase64.`when`<Any> {
|
||||
Base64.encodeToString(ArgumentMatchers.any(ByteArray::class.java), ArgumentMatchers.anyInt())
|
||||
}.thenAnswer { invocation: InvocationOnMock ->
|
||||
java.util.Base64.getMimeEncoder().encodeToString(invocation.arguments[0] as ByteArray)
|
||||
// implement android.util.Base64 using java.util.Base64
|
||||
mockkStatic(android.util.Base64::class)
|
||||
every { android.util.Base64.encodeToString(any<ByteArray>(), any()) } answers {
|
||||
java.util.Base64.getMimeEncoder().encodeToString(firstArg())
|
||||
}
|
||||
|
||||
mockBase64.`when`<Any> {
|
||||
Base64.decode(ArgumentMatchers.anyString(), ArgumentMatchers.anyInt())
|
||||
}.thenAnswer { invocation: InvocationOnMock ->
|
||||
java.util.Base64.getMimeDecoder().decode(invocation.arguments[0] as String)
|
||||
every { android.util.Base64.decode(any<String>(), any()) } answers {
|
||||
java.util.Base64.getMimeDecoder().decode(firstArg<String>())
|
||||
}
|
||||
|
||||
// Store device information needed to create a Device object in a future
|
||||
@@ -85,42 +75,32 @@ class DeviceTest {
|
||||
editor.putString("deviceType", DeviceType.PHONE.toString())
|
||||
editor.putString("certificate", encodedCertificate)
|
||||
editor.apply()
|
||||
Mockito.`when`(context.getSharedPreferences(ArgumentMatchers.eq(deviceId), ArgumentMatchers.eq(Context.MODE_PRIVATE))).thenReturn(deviceSettings)
|
||||
every { context.getSharedPreferences(deviceId, Context.MODE_PRIVATE) } returns deviceSettings
|
||||
|
||||
// Store the device as trusted
|
||||
val trustedSettings = MockSharedPreference()
|
||||
trustedSettings.edit().putBoolean(deviceId, true).apply()
|
||||
Mockito.`when`(context.getSharedPreferences(ArgumentMatchers.eq("trusted_devices"), ArgumentMatchers.eq(Context.MODE_PRIVATE))).thenReturn(trustedSettings)
|
||||
every { context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE) } returns trustedSettings
|
||||
|
||||
// Store an untrusted device
|
||||
val untrustedSettings = MockSharedPreference()
|
||||
Mockito.`when`(context.getSharedPreferences(ArgumentMatchers.eq("unpairedTestDevice"), ArgumentMatchers.eq(Context.MODE_PRIVATE))).thenReturn(untrustedSettings)
|
||||
every { context.getSharedPreferences("unpairedTestDevice", Context.MODE_PRIVATE) } returns untrustedSettings
|
||||
|
||||
// Default shared prefs, including our own private key
|
||||
val preferenceManager = Mockito.mockStatic(PreferenceManager::class.java)
|
||||
mockkStatic(PreferenceManager::class)
|
||||
val defaultSettings = MockSharedPreference()
|
||||
preferenceManager.`when`<Any> {
|
||||
PreferenceManager.getDefaultSharedPreferences(ArgumentMatchers.any(Context::class.java))
|
||||
}.thenReturn(defaultSettings)
|
||||
every { PreferenceManager.getDefaultSharedPreferences(any()) } returns defaultSettings
|
||||
|
||||
RsaHelper.initialiseRsaKeys(context)
|
||||
|
||||
val contextCompat = Mockito.mockStatic(ContextCompat::class.java)
|
||||
contextCompat.`when`<Any> {
|
||||
ContextCompat.getSystemService(context!!, NotificationManager::class.java)
|
||||
}.thenReturn(Mockito.mock(NotificationManager::class.java))
|
||||
mockkStatic(ContextCompat::class)
|
||||
every { ContextCompat.getSystemService(context, NotificationManager::class.java) } returns mockk(relaxed = true)
|
||||
|
||||
this.context = context
|
||||
this.mockBase64 = mockBase64
|
||||
this.preferenceManager = preferenceManager
|
||||
this.contextCompat = contextCompat
|
||||
mockkStatic(android.util.Log::class)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
mockBase64.close()
|
||||
preferenceManager.close()
|
||||
contextCompat.close()
|
||||
unmockkAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -210,8 +190,7 @@ class DeviceTest {
|
||||
fakeNetworkPacket["deviceName"] = "Unpaired Test Device"
|
||||
fakeNetworkPacket["protocolVersion"] = DeviceHelper.PROTOCOL_VERSION
|
||||
fakeNetworkPacket["deviceType"] = DeviceType.PHONE.toString()
|
||||
val certificateString =
|
||||
"""
|
||||
val certificateString = """
|
||||
MIIDVzCCAj+gAwIBAgIBCjANBgkqhkiG9w0BAQUFADBVMS8wLQYDVQQDDCZfZGExNzlhOTFfZjA2
|
||||
NF80NzhlX2JlOGNfMTkzNWQ3NTQ0ZDU0XzEMMAoGA1UECgwDS0RFMRQwEgYDVQQLDAtLZGUgY29u
|
||||
bmVjdDAeFw0xNTA2MDMxMzE0MzhaFw0yNTA2MDMxMzE0MzhaMFUxLzAtBgNVBAMMJl9kYTE3OWE5
|
||||
@@ -233,12 +212,13 @@ class DeviceTest {
|
||||
val certificate = SslHelper.parseCertificate(certificateBytes)
|
||||
val deviceInfo = fromIdentityPacketAndCert(fakeNetworkPacket, certificate)
|
||||
|
||||
val linkProvider = Mockito.mock(LanLinkProvider::class.java)
|
||||
Mockito.`when`(linkProvider.name).thenReturn("LanLinkProvider")
|
||||
val link = Mockito.mock(LanLink::class.java)
|
||||
Mockito.`when`(link.linkProvider).thenReturn(linkProvider)
|
||||
Mockito.`when`(link.deviceId).thenReturn(deviceId)
|
||||
Mockito.`when`(link.deviceInfo).thenReturn(deviceInfo)
|
||||
val linkProvider = mockk<LanLinkProvider>()
|
||||
every { linkProvider.name } returns "LanLinkProvider"
|
||||
val link = mockk<LanLink>()
|
||||
every { link.linkProvider } returns linkProvider
|
||||
every { link.deviceId } returns deviceId
|
||||
every { link.deviceInfo } returns deviceInfo
|
||||
every { link.addPacketReceiver(any()) } returns Unit
|
||||
val device = Device(context, link)
|
||||
|
||||
Assert.assertNotNull(device)
|
||||
@@ -261,7 +241,6 @@ class DeviceTest {
|
||||
)
|
||||
Assert.assertEquals(settings.getString("deviceType", "tablet"), "phone")
|
||||
|
||||
// Cleanup for unpaired test device
|
||||
preferences.edit().remove(device.deviceId).apply()
|
||||
settings.edit().clear().apply()
|
||||
}
|
||||
@@ -269,7 +248,7 @@ class DeviceTest {
|
||||
@Test
|
||||
@Throws(CertificateException::class)
|
||||
fun testUnpair() {
|
||||
val pairingCallback = Mockito.mock(PairingCallback::class.java)
|
||||
val pairingCallback = mockk<PairingCallback>(relaxed = true)
|
||||
val device = Device(context, "testDevice")
|
||||
device.addPairingCallback(pairingCallback)
|
||||
|
||||
@@ -280,6 +259,6 @@ class DeviceTest {
|
||||
val preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE)
|
||||
Assert.assertFalse(preferences.getBoolean(device.deviceId, false))
|
||||
|
||||
Mockito.verify(pairingCallback, Mockito.times(1)).unpaired()
|
||||
verify(exactly = 1) { pairingCallback.unpaired() }
|
||||
}
|
||||
}
|
||||
|
@@ -6,54 +6,54 @@
|
||||
package org.kde.kdeconnect.Helpers
|
||||
|
||||
import android.content.Context
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkAll
|
||||
import org.junit.After
|
||||
import org.junit.Assert
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper
|
||||
import org.kde.kdeconnect.MockSharedPreference
|
||||
import org.mockito.ArgumentMatchers
|
||||
import org.mockito.MockedStatic
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.invocation.InvocationOnMock
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.Base64
|
||||
|
||||
class SSLHelperTest {
|
||||
private lateinit var context: Context
|
||||
private val context: Context = mockk()
|
||||
private lateinit var sharedPreferences: MockSharedPreference
|
||||
private val certificateBase64 = "MIIBkzCCATmgAwIBAgIBATAKBggqhkjOPQQDBDBTMS0wKwYDVQQDDCRlZTA2MWE3NV9lNDAzXzRlY2NfOTI2MV81ZmZlMjcyMmY2OTgxFDASBgNVBAsMC0tERSBDb25uZWN0MQwwCgYDVQQKDANLREUwHhcNMjMwOTE1MjIwMDAwWhcNMzQwOTE1MjIwMDAwWjBTMS0wKwYDVQQDDCRlZTA2MWE3NV9lNDAzXzRlY2NfOTI2MV81ZmZlMjcyMmY2OTgxFDASBgNVBAsMC0tERSBDb25uZWN0MQwwCgYDVQQKDANLREUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASqOIKTm5j6x8DKgYSkItLmjCgIXP0gkOW6bmVvloDGsYnvqYLMFGe7YW8g8lT/qPBTEfDOM4UpQ8X6jidE+XrnMAoGCCqGSM49BAMEA0gAMEUCIEpk6VNpbt3tfbWDf0TmoJftRq3wAs3Dke7d5vMZlivyAiEA/ZXtSRqPjs/2RN9SynKhSUA9/z0PNq6LYoAaC6TdomM="
|
||||
private val certificateBase64 = """
|
||||
MIIBkzCCATmgAwIBAgIBATAKBggqhkjOPQQDBDBTMS0wKwYDVQQDDCRlZTA2MWE3NV9lNDAzXzRl
|
||||
Y2NfOTI2MV81ZmZlMjcyMmY2OTgxFDASBgNVBAsMC0tERSBDb25uZWN0MQwwCgYDVQQKDANLREUw
|
||||
HhcNMjMwOTE1MjIwMDAwWhcNMzQwOTE1MjIwMDAwWjBTMS0wKwYDVQQDDCRlZTA2MWE3NV9lNDAz
|
||||
XzRlY2NfOTI2MV81ZmZlMjcyMmY2OTgxFDASBgNVBAsMC0tERSBDb25uZWN0MQwwCgYDVQQKDANL
|
||||
REUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASqOIKTm5j6x8DKgYSkItLmjCgIXP0gkOW6bmVv
|
||||
loDGsYnvqYLMFGe7YW8g8lT/qPBTEfDOM4UpQ8X6jidE+XrnMAoGCCqGSM49BAMEA0gAMEUCIEpk
|
||||
6VNpbt3tfbWDf0TmoJftRq3wAs3Dke7d5vMZlivyAiEA/ZXtSRqPjs/2RN9SynKhSUA9/z0PNq6L
|
||||
YoAaC6TdomM=
|
||||
""".trimIndent().replace("\n", "\r\n") // the mime encoder adds \r\n line endings
|
||||
private val certificateHash = "fc:1f:b3:d3:d3:3b:23:42:e4:5c:74:b1:a6:13:dc:df:e5:e1:f0:29:d6:68:24:9f:50:49:52:a9:a8:04:1e:31:"
|
||||
private val deviceId = "testDevice"
|
||||
private val certificateKey = "certificate"
|
||||
private lateinit var mockBase64: MockedStatic<android.util.Base64>
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
context = Mockito.mock(Context::class.java)
|
||||
sharedPreferences = MockSharedPreference()
|
||||
Mockito.`when`(context.getSharedPreferences(deviceId, Context.MODE_PRIVATE)).thenReturn(sharedPreferences)
|
||||
every { context.getSharedPreferences(deviceId, Context.MODE_PRIVATE) } returns sharedPreferences
|
||||
|
||||
val mockBase64 = Mockito.mockStatic(android.util.Base64::class.java)
|
||||
|
||||
mockBase64.`when`<Any> {
|
||||
android.util.Base64.encodeToString(ArgumentMatchers.any(ByteArray::class.java), ArgumentMatchers.anyInt())
|
||||
}.thenAnswer { invocation: InvocationOnMock ->
|
||||
Base64.getMimeEncoder().encodeToString(invocation.arguments[0] as ByteArray)
|
||||
// implement android.util.Base64 using java.util.Base64
|
||||
mockkStatic(android.util.Base64::class)
|
||||
every { android.util.Base64.encodeToString(any<ByteArray>(), any()) } answers {
|
||||
Base64.getMimeEncoder().encodeToString(firstArg())
|
||||
}
|
||||
|
||||
mockBase64.`when`<Any> {
|
||||
android.util.Base64.decode(ArgumentMatchers.anyString(), ArgumentMatchers.anyInt())
|
||||
}.thenAnswer { invocation: InvocationOnMock ->
|
||||
Base64.getMimeDecoder().decode(invocation.arguments[0] as String)
|
||||
every { android.util.Base64.decode(any<String>(), any()) } answers {
|
||||
Base64.getMimeDecoder().decode(firstArg<String>())
|
||||
}
|
||||
|
||||
this.mockBase64 = mockBase64
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
mockBase64.close()
|
||||
unmockkAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -81,7 +81,7 @@ class SSLHelperTest {
|
||||
fun getExpectedCertificate() {
|
||||
sharedPreferences.edit().putString(certificateKey, certificateBase64).apply()
|
||||
val cert = SslHelper.getDeviceCertificate(context, deviceId)
|
||||
Assert.assertEquals(certificateBase64, Base64.getEncoder().encodeToString(cert.encoded))
|
||||
Assert.assertEquals(certificateBase64, Base64.getMimeEncoder().encodeToString(cert.encoded))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -94,7 +94,7 @@ class SSLHelperTest {
|
||||
|
||||
@Test
|
||||
fun parseCertificate() {
|
||||
val bytes = Base64.getDecoder().decode(certificateBase64)
|
||||
val bytes = Base64.getMimeDecoder().decode(certificateBase64)
|
||||
val cert = SslHelper.parseCertificate(bytes)
|
||||
val hash = SslHelper.getCertificateHash(cert)
|
||||
Assert.assertEquals(certificateHash, hash)
|
||||
|
@@ -1,12 +1,11 @@
|
||||
package org.kde.kdeconnect.Helpers
|
||||
|
||||
import android.net.Uri
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.`when`
|
||||
import java.net.URI
|
||||
import java.util.ArrayList
|
||||
|
||||
class StorageHelperTest {
|
||||
@Test
|
||||
@@ -19,11 +18,11 @@ class StorageHelperTest {
|
||||
"content://com.android.externalstorage.documents/tree/primary:DCIM" to "DCIM",
|
||||
"content://com.android.externalstorage.documents/tree/primary:Download/bla" to "Download/bla",
|
||||
).mapKeys { entry ->
|
||||
val mockUri = mock(Uri::class.java)
|
||||
val mockUri = mockk<Uri>()
|
||||
// e.g. "content://com.android.providers.downloads.documents/tree/downloads" -> ["tree", "downloads"]
|
||||
val pathSegments = URI.create(entry.key).path.split("/").drop(1)
|
||||
val pathSegmentsJavaList: java.util.ArrayList<String> = ArrayList(pathSegments)
|
||||
`when`(mockUri.pathSegments).thenReturn(pathSegmentsJavaList)
|
||||
every { mockUri.pathSegments } returns pathSegmentsJavaList
|
||||
return@mapKeys mockUri
|
||||
}
|
||||
|
||||
@@ -35,11 +34,11 @@ class StorageHelperTest {
|
||||
|
||||
@Test
|
||||
fun testMissingTree() {
|
||||
val mockUri = mock(Uri::class.java)
|
||||
val pathSegmentsJavaList: java.util.ArrayList<String> = ArrayList(listOf("branch", "downloads"))
|
||||
`when`(mockUri.pathSegments).thenReturn(pathSegmentsJavaList)
|
||||
val mockUri = mockk<Uri>()
|
||||
val pathSegmentsJavaList = ArrayList(listOf("branch", "downloads"))
|
||||
every { mockUri.pathSegments } returns pathSegmentsJavaList
|
||||
Assert.assertThrows(IllegalArgumentException::class.java) {
|
||||
StorageHelper.getDisplayName(mockUri)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,48 +5,82 @@
|
||||
*/
|
||||
package org.kde.kdeconnect.Helpers
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockkObject
|
||||
import io.mockk.unmockkObject
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
|
||||
class VideoUrlsHelperTest {
|
||||
|
||||
@Test
|
||||
fun checkYoutubeURL() {
|
||||
fun checkYoutubeTvLinksConversion() {
|
||||
fun check(isTv: Boolean, input: String, expected: String) {
|
||||
mockkObject(DeviceHelper)
|
||||
every { DeviceHelper.isTv } returns isTv
|
||||
val formatted = VideoUrlsHelper.convertToAndFromYoutubeTvLinks(input)
|
||||
Assert.assertEquals(expected, formatted)
|
||||
unmockkObject(DeviceHelper)
|
||||
}
|
||||
val complexTvLink = "https://www.youtube.com/tv?is_account_switch=1&hrld=2&fltor=1#/watch?v=ZN471HiQD3o&t=13"
|
||||
val tvLink = "https://www.youtube.com/tv#/watch?v=ZN471HiQD3o&t=13"
|
||||
val pcLink = "https://www.youtube.com/watch?v=ZN471HiQD3o&t=13"
|
||||
val unrelatedLink = "https://www.youtube.com/healthz"
|
||||
check(isTv = true, input = pcLink, expected = tvLink)
|
||||
check(isTv = true, input = tvLink, expected = tvLink)
|
||||
check(isTv = true, input = complexTvLink, expected = complexTvLink)
|
||||
check(isTv = true, input = unrelatedLink, expected = unrelatedLink)
|
||||
check(isTv = false, input = pcLink, expected = pcLink)
|
||||
check(isTv = false, input = tvLink, expected = pcLink)
|
||||
check(isTv = false, input = complexTvLink, expected = pcLink)
|
||||
check(isTv = false, input = unrelatedLink, expected = unrelatedLink)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun checkYoutubeURLWithoutTime() {
|
||||
val url = "https://www.youtube.com/watch?v=ovX5G0O5ZvA"
|
||||
val formatted = VideoUrlsHelper.formatUriWithSeek(url, 51_000L)
|
||||
val expected = "https://www.youtube.com/watch?v=ovX5G0O5ZvA&t=51"
|
||||
Assert.assertEquals(expected, formatted)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun checkYoutubeURLWithTime() {
|
||||
val url = "https://www.youtube.com/watch?v=ovX5G0O5ZvA&t=13"
|
||||
val formatted = VideoUrlsHelper.formatUriWithSeek(url, 51_000L)
|
||||
val expected = "https://www.youtube.com/watch?v=ovX5G0O5ZvA&t=51"
|
||||
Assert.assertEquals(expected, formatted.toString())
|
||||
Assert.assertEquals(expected, formatted)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun checkYoutubeURLSubSecond() {
|
||||
val url = "https://www.youtube.com/watch?v=ovX5G0O5ZvA&t=13"
|
||||
val formatted = VideoUrlsHelper.formatUriWithSeek(url, 450L)
|
||||
val expected = "https://www.youtube.com/watch?v=ovX5G0O5ZvA&t=13"
|
||||
Assert.assertEquals(expected, formatted.toString())
|
||||
fun checkVimeoURLWithOtherArgsWithoutTime() {
|
||||
val url = "https://vimeo.com/347119375?foo=bar"
|
||||
val formatted = VideoUrlsHelper.formatUriWithSeek(url, 51_000L)
|
||||
val expected = "https://vimeo.com/347119375?foo=bar&t=51s"
|
||||
Assert.assertEquals(expected, formatted)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun checkVimeoURL() {
|
||||
fun checkVimeoURLWithOtherArgsWithTime() {
|
||||
val url = "https://vimeo.com/347119375?foo=bar&t=13s"
|
||||
val formatted = VideoUrlsHelper.formatUriWithSeek(url, 51_000L)
|
||||
val expected = "https://vimeo.com/347119375?foo=bar&t=51s"
|
||||
Assert.assertEquals(expected, formatted.toString())
|
||||
Assert.assertEquals(expected, formatted)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun checkVimeoURLSubSecond() {
|
||||
val url = "https://vimeo.com/347119375?foo=bar&t=13s"
|
||||
val formatted = VideoUrlsHelper.formatUriWithSeek(url, 450L)
|
||||
val expected = "https://vimeo.com/347119375?foo=bar&t=13s"
|
||||
Assert.assertEquals(expected, formatted.toString())
|
||||
fun checkVimeoURLWithoutTime() {
|
||||
val url = "https://vimeo.com/347119375"
|
||||
val formatted = VideoUrlsHelper.formatUriWithSeek(url, 51_000L)
|
||||
val expected = "https://vimeo.com/347119375?t=51s"
|
||||
Assert.assertEquals(expected, formatted)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun checkVimeoURLParamOrderCrash() {
|
||||
fun checkVimeoURLWithTime() {
|
||||
val url = "https://vimeo.com/347119375?t=13s"
|
||||
val formatted = VideoUrlsHelper.formatUriWithSeek(url, 51_000L)
|
||||
val expected = "https://vimeo.com/347119375?t=51s"
|
||||
Assert.assertEquals(expected, formatted.toString())
|
||||
Assert.assertEquals(expected, formatted)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -54,7 +88,7 @@ class VideoUrlsHelperTest {
|
||||
val url = "https://www.dailymotion.com/video/xnopyt?foo=bar&start=13"
|
||||
val formatted = VideoUrlsHelper.formatUriWithSeek(url, 51_000L)
|
||||
val expected = "https://www.dailymotion.com/video/xnopyt?foo=bar&start=51"
|
||||
Assert.assertEquals(expected, formatted.toString())
|
||||
Assert.assertEquals(expected, formatted)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -62,7 +96,7 @@ class VideoUrlsHelperTest {
|
||||
val url = "https://www.twitch.tv/videos/123?foo=bar&t=1h2m3s"
|
||||
val formatted = VideoUrlsHelper.formatUriWithSeek(url, 10_000_000)
|
||||
val expected = "https://www.twitch.tv/videos/123?foo=bar&t=02h46m40s"
|
||||
Assert.assertEquals(expected, formatted.toString())
|
||||
Assert.assertEquals(expected, formatted)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -70,21 +104,21 @@ class VideoUrlsHelperTest {
|
||||
val url = "https://example.org/cool_video.mp4"
|
||||
val formatted = VideoUrlsHelper.formatUriWithSeek(url, 51_000L)
|
||||
val expected = "https://example.org/cool_video.mp4"
|
||||
Assert.assertEquals(expected, formatted.toString())
|
||||
Assert.assertEquals(expected, formatted)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun checkPeerTubeURL() {
|
||||
val validUrls = mapOf(
|
||||
"https://video.blender.org/w/472h2s5srBFmAThiZVw96R?start=01m27s" to "https://video.blender.org/w/472h2s5srBFmAThiZVw96R?start=01m30s",
|
||||
"https://video.blender.org/w/472h2s5srBFmAThiZVw96R" to "https://video.blender.org/w/472h2s5srBFmAThiZVw96R?start=01m30s",
|
||||
"https://video.blender.org/w/mDyZP2TrdjjjNRMoVUgPM2?start=01m27s" to "https://video.blender.org/w/mDyZP2TrdjjjNRMoVUgPM2?start=01m30s",
|
||||
"https://video.blender.org/w/evhMcVhvK6VeAKJwCSuHSe?start=01m27s" to "https://video.blender.org/w/evhMcVhvK6VeAKJwCSuHSe?start=01m30s",
|
||||
"https://video.blender.org/w/54tzKpEguEEu26Hi8Lcpna?start=01m27s" to "https://video.blender.org/w/54tzKpEguEEu26Hi8Lcpna?start=01m30s",
|
||||
"https://video.blender.org/w/evhMcVhvK6VeAKJwCSuHSe#potato" to "https://video.blender.org/w/evhMcVhvK6VeAKJwCSuHSe?start=01m30s#potato",
|
||||
"https://video.blender.org/w/54tzKpEguEEu26Hi8Lcpna?start=01m27s#potato" to "https://video.blender.org/w/54tzKpEguEEu26Hi8Lcpna?start=01m30s#potato",
|
||||
"https://video.blender.org/w/o5VtGNQaNpFNNHiJbLy4eM?start=01m27s" to "https://video.blender.org/w/o5VtGNQaNpFNNHiJbLy4eM?start=01m30s",
|
||||
)
|
||||
for ((from, to) in validUrls) {
|
||||
val formatted = VideoUrlsHelper.formatUriWithSeek(from, 90_000L)
|
||||
Assert.assertEquals(to, formatted.toString())
|
||||
Assert.assertEquals(to, formatted)
|
||||
}
|
||||
val invalidUrls = listOf(
|
||||
"https://video.blender.org/w/472h2s5srBFmAOhiZVw96R?start=01m27s", // invalid character (O)
|
||||
@@ -96,7 +130,7 @@ class VideoUrlsHelperTest {
|
||||
)
|
||||
for (url in invalidUrls) {
|
||||
val formatted = VideoUrlsHelper.formatUriWithSeek(url, 90_000L)
|
||||
Assert.assertEquals(url, formatted.toString()) // should not modify the URL
|
||||
Assert.assertEquals(url, formatted) // should not modify the URL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,13 +5,13 @@
|
||||
*/
|
||||
package org.kde.kdeconnect
|
||||
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.mockk
|
||||
import org.json.JSONException
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import org.kde.kdeconnect.DeviceInfo.Companion.fromIdentityPacketAndCert
|
||||
import org.kde.kdeconnect.NetworkPacket.Companion.unserialize
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.internal.util.collections.Sets
|
||||
import java.security.cert.Certificate
|
||||
|
||||
class NetworkPacketTest {
|
||||
@@ -46,10 +46,8 @@ class NetworkPacketTest {
|
||||
|
||||
@Test
|
||||
fun testIdentity() {
|
||||
val cert = Mockito.mock(Certificate::class.java)
|
||||
|
||||
val deviceInfo =
|
||||
DeviceInfo("myid", cert, "myname", DeviceType.TV, 12, Sets.newSet("ASDFG"), Sets.newSet("QWERTY"))
|
||||
val cert = mockk<Certificate>()
|
||||
val deviceInfo = DeviceInfo("myid", cert, "myname", DeviceType.TV, 12, setOf("ASDFG"), setOf("QWERTY"))
|
||||
|
||||
val np = deviceInfo.toIdentityPacket()
|
||||
|
||||
|
Reference in New Issue
Block a user