2
0
mirror of https://github.com/KDE/kdeconnect-android synced 2025-10-19 14:26:49 +00:00

Compare commits

...

15 Commits

Author SHA1 Message Date
Albert Vaca Cintora
d6fa07a614 Release 1.33.9 2025-10-12 00:36:21 +02:00
Albert Vaca Cintora
21d58b1ca2 Rename locks 2025-10-12 00:21:12 +02:00
Albert Vaca Cintora
5c614d9094 Make ConnectionMultiplexer non-null 2025-10-12 00:21:12 +02:00
Albert Vaca Cintora
f32bc29700 Add missing withLock call 2025-10-12 00:21:12 +02:00
Albert Vaca Cintora
9d1687c995 Make socket non-null 2025-10-12 00:21:12 +02:00
Albert Vaca Cintora
d1a565737a Try to fix another crash
Exception java.lang.NullPointerException: Attempt to write to field 'java.lang.Integer android.telephony.PhoneStateListener.mSubId' on a null object reference
  at android.telephony.TelephonyRegistryManager.listenForSubscriber (TelephonyRegistryManager.java:226)
  at android.telephony.TelephonyManager.listen (TelephonyManager.java:6706)
2025-10-12 00:11:59 +02:00
Albert Vaca Cintora
ee139314e1 Try to fix crash
Exception java.lang.IllegalStateException: telephony service is null.
  at android.telephony.TelephonyRegistryManager.listenFromListener (TelephonyRegistryManager.java:307)
  at android.telephony.TelephonyManager.listen (TelephonyManager.java:6886)
2025-10-12 00:06:42 +02:00
Albert Vaca Cintora
937f77ced7 Improve wording
BUG: 510434
2025-10-11 23:46:09 +02:00
l10n daemon script
c8d2f4d97c GIT_SILENT made messages (after extraction) 2025-10-11 10:57:21 +00:00
Albert Vaca Cintora
83c0617145 Fix formatUriWithSeek not being able to add the time parameter
Since the conversion to Kotlin in c9fb81363d,
the code could modify a parameter but not add it.
2025-10-11 12:16:56 +02:00
Albert Vaca Cintora
3b40e008ee Use addMenuProvider instead of the deprecated onPrepareOptionsMenu 2025-10-11 12:13:31 +02:00
Albert Vaca Cintora
193f1e4bac Implement conversion between YouTube and YouTubeTV URLs
Convert links to or from YouTube TV depending on whether we are a TV when using the "continue playing here" feature.
2025-10-11 12:11:27 +02:00
Albert Vaca Cintora
a24b50ae3e Migrate tests to mockk 2025-10-10 01:11:42 +02:00
Albert Vaca Cintora
a9216ee636 Port MousePadPlugin to Kotlin 2025-10-09 23:22:58 +02:00
Albert Vaca Cintora
673352d12b Port BigscreenActivity to kotlin 2025-10-09 23:11:23 +02:00
26 changed files with 634 additions and 613 deletions

View File

@@ -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

View 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).

View File

@@ -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" }

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}
/**

View File

@@ -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()

View File

@@ -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;
}
}

View File

@@ -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
}
}

View File

@@ -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;
}
}

View 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"
}
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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() }
}
}

View File

@@ -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)

View File

@@ -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)
}
}
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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()