diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml index 75c34092..27fddcb6 100644 --- a/res/values-gl/strings.xml +++ b/res/values-gl/strings.xml @@ -1,296 +1,297 @@ KDE Connect Non conectado a ningún dispositivo Conectado a: %s Notificador de telefonía Envíe notificacións de chamadas entrantes. Informe da batería Envíe periodicamente un informe sobre o estado da batería. Revelador do sistema de ficheiros Permite examinar o sistema de ficheiros do dispositivo remotamente. Sincronización do portapapeis Comparta o contido do portapapeis. Entrada remota Use o teléfono ou tableta como área táctil e teclado. Mando da presentación Use o dispositivo para cambiar de dispositiva nunha presentación Recibir teclas premidas remotamente. Recibir eventos de teclas premidas de dispositivos remotos. Controis multimedia Fornece un mando a distancia para o reprodutor. Executar unha orde Provocar ordes remotas desde o teléfono ou tableta. Sincronizador de contactos Permitir sincronizar o caderno de contactos do dispositivo Ping - Envíe e reciba pings. + Envíe e reciba pings Sincronización de notificacións - Acceda ás súas notificacións desde outros dispositivos. + Acceda ás súas notificacións desde outros dispositivos Recibir notificacións - Recibir notificacións do outro dispositivo e mostralas en Android. + Recibir notificacións do outro dispositivo e mostralas en Android Compartir e recibir - Comparta ficheiros e enderezos URL entre dispositivos. - Esta funcionalidade non está dispoñíbel para a súa versión de Android. + Comparta ficheiros e enderezos URL entre dispositivos + Esta funcionalidade non está dispoñíbel para a súa versión de Android Non hai dispositivos. Aceptar Cancelar Abrir a configuración - Debe conceder permisos para acceder ás notificacións. - Para poder controlar os seus reprodutores de son e vídeo ten que garantir acceso ás notificacións. + Debe conceder permisos para acceder ás notificacións + Para poder controlar os seus reprodutores de son e vídeo ten que garantir acceso ás notificacións + Para recibir presións de tecla ten que activar o teclado remoto de KDE Connect Enviar un ping Control multimedia Xestionar teclas remotas só ao editar. Non hai ningunha conexión de teclado remoto activa, estableza unha en kdeconnect. A conexión de teclado remoto está activa. Hai máis dunha conexión de teclado remoto, seleccione o dispositivo para configurar. Entrada remota Mova un dedo na pantalla para mover o cursor do rato. Toque para facer clic, e use dous ou tres dedos para os botóns secundario e central. Use dous dedos para desprazar. Prema durante un tempo para arrastrar e soltar. Definir a acción de tocar con dous dedos Definir a acción de tocar con tres dedos Definir a sensibilidade do punteiro táctil Definir a aceleración do punteiro Inverter a dirección de desprazamento Clic dereito Clic central Nada O máis lento Lento Predeterminado Por riba do predeterminado O máis rápido Ningunha A máis débil Máis débil Media Máis forte A máis forte Dispositivos conectados Dispositivos dispoñíbeis Dispositivos coñecidos Configuración do complemento Desemparellarse O dispositivo emparellado está fóra do alcance. Emparellar cun novo dispositivo Dispositivo descoñecido Dispositivo fóra do alcance Xa solicitou emparellarse. O dispositivo xa está emparellado. Non se puido enviar o paquete. Esgotouse o tempo límite Cancelouno o usuario. Cancelouse remotamente Recibiuse unha clave incorrecta. Información do cifrado O outro dispositivo non usa unha versión recente de KDE Connect, usarase un método obsoleto de cifrado. A pegada SHA1 do certificado do seu dispositivo é: A pegada SHA1 do certificado do dispositivo remoto é: Solicitude de emparellamento Solicitude de emparellamento de %1s. Recibiuse unha ligazón de %1s Toque para abrir «%1s». Recibindo %1$d ficheiro de %2$s Recibindo %1$d ficheiros de %2$s Ficheiro: %1s (Ficheiro %2$d de %3$d) : %1$s Enviando un ficheiro a %1s Enviando os ficheiros a %1s Enviouse %1$d ficheiro. Enviáronse %1$d de %2$d ficheiros. Recibiuse un ficheiro de %1$s Recibíronse %2$d ficheiros de %1$s A recepción do ficheiro de %1$s fallou A recepción de %2$d de %3$d ficheiros de %1$s fallou Toque para abrir «%1s». Non se pode crear o ficheiro %s Enviouse o ficheiro a %1s %1s Non se puido enviar o ficheiro a %1s %1s Toque para contestar Conectar de novo Enviar un clic secundario Enviar un clic central Mostrar o teclado O dispositivo non está emparellado Solicitar emparellarse Aceptar Rexeitar Dispositivo Emparellar o dispositivo Configuración Reproducir Deter Anterior Retroceder Cara a adiante Seguinte Volume Configuración de son e vídeo Botóns de avanzar e retroceder Axuste o tempo que avanzar ou retroceder ao premer 10 segundos 20 segundos 30 segundos 1 minuto 2 minutos Mostrar a notificación de control de reprodución. Permitir controlar os reprodutores sen abrir KDE Connect Compartir con… Este dispositivo usa unha versión vella do protocolo. Este dispositivo usa unha versión máis nova do protocolo. Configuración xeral Configuración Configuración de %s Nome do dispositivo %s Nome de dispositivo incorrecto Recibiuse un texto e gardouse no portapapeis Lista de dispositivos personalizada Emparellar cun novo dispositivo Desemparellarse de %s Engadir dispositivos por IP Eliminar %s? Eliminouse o dispositivo personalizado Se o seu dispositivo non se detecta automaticamente pode engadir o seu enderezo IP ou nome de máquina premendo o botón flotante de acción Engadir un dispositivo Desfacer Notificacións sonoras Vibrar e reproducir un son ao recibir un ficheiro. Personalizar o directorio de destino Os ficheiros recibidos aparecerán en «Descargas». Os ficheiros almacenaranse no directorio de abaixo. Directorio de destino Compartir Compartir «%s» Filtro de notificacións As notificacións sincronizaranse para os seguintes aplicativos. Almacenamento interno Tarxeta SD %d Tarxeta SD (só lectura) Imaxes da cámara Engadir o dispositivo Nome de máquina ou enderezo IP Tarxetas SD detectadas Editar a tarxeta SD Lugares de almacenamento configurados Engadir un lugar de almacenamento Editar un lugar de almacenamento Engadir un atallo ao cartafol de cámara Engadir un atallo ao cartafol da cámara Non engadir un atallo ao cartafol da cámara Lugar de almacenamento Este lugar xa está configurado premer para seleccionar Nome para mostrar Este nome para mostrar xa está a usarse O nome para mostrar non pode estar baleiro Eliminar Non se detectaron tarxetas SD Non se configuraron localizacións de almacenamento Para acceder a ficheiro remotamente ten que configurar lugares de almacenamento Engadir unha nome ou IP Nome de máquina ou IP Non se atoparon reprodutores. %1$s en %2$s Enviar ficheiros Dispositivos con KDE Connect Outros dispositivos que estean a executar KDE Connect na mesma rede deberían aparecer aquí. Emparellouse co dispositivo Renomear o dispositivo Renomear Actualizar Este dispositivo emparellado está fóra do alcance. Asegúrese de que está conectado á mesma rede. Parece que está usando unha conexión de datos de móbil. KDE Connect só funciona en redes locais. Non hai navegadores de ficheiros instalados. Enviar unha mensaxe de texto Enviar mensaxes de texto desde o seu escritorio O dispositivo non é compatíbel con este complemento. Atopar o móbil Atopar a tableta Atopar o meu televisor Reproduce un son de chamada no dispositivo para que poida atopalo. Atopado Abrir Pechar Debe conceder permisos para acceder ao almacenamento. Algúns complementos necesitan permisos para funcionar (toque para máis información): Este complemento necesita permisos para funcionar. Ten que conceder permisos adicionais para activar todas as funcións. Algúns complementos teñen funcionalidades desactivadas por mor dunha falta de permisos (toque para máis información): Para compartir ficheiros entre o teléfono e o escritorio ten que dar acceso ao almacenamento do teléfono. Para ler e escribir SMS desde o escritorio ten que dar permiso de SMS. Para ver as chamadas de teléfono e os SMS desde o escritorio ten que dar permiso a chamadas de teléfono e a SMS. Para ver o nome dun contacto en vez dun número de teléfono ten que dar acceso aos contactos do teléfono. Para compartir o caderno de contactos co escritorio ten que dar permiso de contactos Seleccione un son de chamada Números bloqueados Non mostrar chamadas nin SMS destes números. Indique un número por liña. Portada da obra actual. Icona do dispositivo. Icona da configuración. Pantalla completa Saír da presentación Pode bloquear o dispositivo e usar as teclas de volume para ir aos fotogramas anterior e seguinte Engadir unha orde Non hai ordes rexistradas. Pode engadir novas ordes desde a configuración do sistema de KDE Connect. Pode engadir ordes no escritorio. Control do reprodutor de multimedia Controlar os reprodutores do seu móbil desde outro dispositivo. Tema escuro Outras notificacións Indicador persistente Control de reprodución Transferencia de ficheiros Deter o reprodutor actual Copiar o URL no portapapeis Copiouse no portapapeis O dispositivo está fóra de alcance O dispositivo non está emparellado Non hai tal dispositivo O dispositivo non ten o complemento de «Executar unha orde» activado Atopar un dispositivo remoto Facer soar o dispositivo remoto Facer soar Volume do sistema Controlar o volume do sistema do dispositivo remoto Silenciar Todo Dispositivos Nome do dispositivo Tema escuro Máis opcións Pode atopar as opcións específicas dun dispositivo baixo «Configuración do complemento» desde un dispositivo. Mostrar unha notificación persistente Notificación persistente Toque para activar ou desactivar na configuración de notificacións Opcións adicionais Opcións de intimidade Definir as súas opcións de protección da intimidade Nova notificación Bloquear o contido das notificacións Bloquear as imaxes nas notificacións Notificacións desde outros dispositivos Iniciar a cámara Iniciar o aplicativo da cámara para facilitar sacar e transferir imaxes diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml index cf473174..5af8f1e0 100644 --- a/res/values-zh-rTW/strings.xml +++ b/res/values-zh-rTW/strings.xml @@ -1,291 +1,292 @@ KDE 連線 沒有連線到任何裝置 已連線到:%s 電話通知器 傳送未接來電的通知 電池報告 定期回報電池狀態 顯示檔案系統 同意讓遠端可以瀏覽檔案系統 同步剪貼板 分享剪貼板的內容 遠端輸入 使用您的智慧型手機或者平板來模擬觸碰板與鍵盤 遠端投影片 使用您的裝置來切換簡報中的投影片 接收遠端按鍵輸入 從遠端裝置接收按鍵輸入活動 多媒體控制 成為您多媒體播放器的遙控器 執行指令 從您的智慧型手機或者平板當中觸發遠端設備上的命令 同步聯絡人工具 允許同步裝置的通訊錄 Ping回應封包 傳送與接收Ping回應封包 同步通知 存取其他設備上的通知 接收通知 在Android上顯示從其他裝置收到的通知 分享與接收 在兩個設備當中互相分享URL網址與檔案 這個功能無法在您的Android版本上執行。 沒有裝置 OK 取消 開啟設定 您需要授予存取通知的權限 為了要能控制您的媒體播放器,您需要提供「通知」的權限 + 若要接收鍵盤按鍵事件,您需要啟用 KDE 連線遠端鍵盤功能 傳送Ping回應封包 多媒體控制 當編輯時只處理遠端按鍵 這裡沒有建立在 kdeconnect 之上的使用中遠端鍵盤連線。 遠端鍵盤連線為啟用狀態 這裡有兩個以上的遠端鍵盤連結,選擇一個裝置以設定。 遠端輸入 在您的智慧型手機的螢幕上移動手指頭,用來控制電腦螢幕的鼠標。點擊表示滑鼠的左鍵,使用兩隻/三隻手指頭點擊來表示滑鼠的右鍵/中鍵。使用兩隻手指頭捲動。長按則表示要拖拉。 設定兩隻手指頭點擊的動作 設定三隻手指頭點擊的動作 設定觸碰板的靈敏度 設定指針加速度 滾動方向相反 右鍵點擊 中鍵點擊 最慢 預設 高於預設值 最快 沒有加速度 最脆弱 脆弱 中等 安全 最安全 已連接的設備 可連接的設備 已記住的設備 擴展插件設定 取消配對 配對的設備無法連接 配對新設備 不明的設備 設備無法連接 已請求配對 裝置已經配對 無法傳送封包 逾時 使用者中斷 被其他同等功能應用中斷 接收的密鑰無效 加密資訊 其他的設備沒有使用新版本的KDE連線,使用傳統的加密模式。 您設備上的SHA1指紋辨識認證是: 您遠端設備上的SHA1指紋辨識認證是: 已請求配對 從 %1s 來的配對請求 已從 %1s 連線接收 點擊開啟 \'%1s\' 正在從 %2$s 接收 %1$d 個檔案 (檔案 %2$d/%3$d):%1$s 正在將檔案發送到 %1s 正在將檔案發送到 %1s 傳送 %1$d 個檔案,共 %2$d 個檔案。 已從 %1$s 接收 %2$d 個檔案 無法從 %1$s 接收到 %2$d/%3$d 個檔案 點擊開啟 \'%1s\' 無法建立 %s 檔案 將檔案傳送到 %1s %1s 傳送到 %1s 的檔案失敗 %1s 點擊即可應答 重新連線 傳送右鍵點擊 傳送中鍵點擊 顯示鍵盤 裝置未配對 請求配對 同意 回絕 裝置 配對裝置 設定 播放 暫停 往前 往後 快轉 下一首 音量 多媒體設定 往前/往後按鍵 調整按下時往前 / 往後的時間 10秒鐘 20秒鐘 30秒鐘 1分鐘 2分鐘 顯示媒體控制項通知 允許控制您的媒體播放器而不需要開啟 KDE 連線 分享給… 這個裝置使用舊版本的通訊協定 此設備使用較新的通訊協定 通用設定 設定 %s 設定 設備名稱 %s 無效的設備名稱 已接收文字,並且儲存到剪貼簿 自定義設備列表 配對新設備 未配對 %s 以IP來新增設備 刪除 %s? 已刪除自訂裝置 若未自動偵測到您的裝置,您可透過點選「浮動動作按鈕」來新增該裝置的IP 位址或主機名稱 新增裝置 復原 通知方式 當接收檔案時發出振動以及播放聲音 自訂目標路徑 接收到的檔案將會在 Downloads 上顯示。 檔案將會儲存在下方所示的資料夾 目標路徑 分享 分享「%s」 通知過濾器 將會以您選擇的App應用程式啟用同步通知 內部儲存空間 SD卡 %d SD卡 (唯讀) 相機圖片 新增裝置 主機名稱 或 IP 位址 已偵測到 SD 卡 編輯 SD 卡 已設定儲存空間位置 新增儲存空間位置 編輯儲存空間位置 新增相機資料夾的捷徑 新增連結到相機資料夾的捷徑 請勿新增連結到相機資料夾的捷徑 儲存空間位置 此位置已被設定 按一下選擇 顯示名稱 此顯示名稱已被使用 顯示名稱不得空白 刪除 未偵測到 SD 卡 未設定儲存空間位置 若要遠端存取檔案,您需先設定儲存空間位置 增加 host/IP 主機名稱或 IP 沒有發現播放器 %1$s on %2$s 傳送檔案 KDE連線裝置 在您相同網域當中,有其他有執行KDE連線的裝置會出現在這裡。 裝置已配對 更改裝置名稱 更改名稱 刷新 此配對的裝置無法連接。 請確保它連接到與您相同的網域。 看起來你現在正使用行動數據連線。KDE 連線只在區域網路上運作。 沒有安裝此檔案的瀏覽程式 傳送簡訊 傳送文字簡訊到您的電腦桌面 這個擴充插件並不支援您的手機 找尋我的手機 找尋我的平板 尋找我的電視 讓這個裝置發出聲響讓您能找到它 找到 開啟 關閉 您需要授予儲存權限 部份的附加元件需要權限才能運作(點擊以取得更多資訊) 這附加元件需要權限以運作 你需要授予延伸的權限以啟用所有的功能 部份的附加元件因為缺乏權限,而導致功能被停用。(點擊以了解更多資訊): 為了要在您的手機與電腦之間分享檔案,你需要同意存取手機的儲存空間。 為了要在您的個人電腦上讀取與撰寫簡訊,你需要提供簡訊的權限。 為了要在您的電腦上檢視手機通話與簡訊,你需要提供手機通話與簡訊的權限。 為了要讓聯絡人名稱取代手機號碼,您需要提供手機通訊錄的權限。 為了要與電腦分享您的通訊錄,您必須提供「聯絡人」的權限 選擇一個鈴聲 已封鎖號碼 不顯示這些號碼的來電與簡訊。請在一行指定一個電話號碼。 目前媒體的專輯圖像 裝置圖示 設定圖示 全螢幕 離開簡報模式 您能鎖定裝置並使用音量鍵前往上 / 下一張投影片 增加一行指令 沒有指令被註冊 您現在可以在 KDE 連線系統設定增加新的指令 您可以在電腦上增加指令 控制媒體播放器 從另外一個裝置操控您手機的媒體播放器 暗色主題 其他通知 一致指標 多媒體控制 檔案傳輸 停止目前播放器 複製 URL 至剪貼簿 已複製到剪貼簿 無法聯絡裝置 裝置未配對 此處沒有裝置 裝置未啟用「執行指令外掛程式」 尋找遠端裝置 使遠端裝置響鈴 響鈴 系統音量 控制遠端裝置的系統音量 靜音 全部 裝置 裝置名稱 暗色主題 更多設定 各裝置設定可在裝置內的「外掛程式設定」底下找到。 顯示一致設定 一致化通知 點觸以在「通知設定」啟用或停用 延伸選項 隱私權選項 設定隱私權選項 新通知 擋住通知內容 擋住通知中的圖片 其他裝置上的通知 啟動相機 開啟相機應用程式以輕鬆拍攝並傳輸相片 diff --git a/src/org/kde/kdeconnect/BackgroundService.java b/src/org/kde/kdeconnect/BackgroundService.java index 1aff1e7c..4511a1fa 100644 --- a/src/org/kde/kdeconnect/BackgroundService.java +++ b/src/org/kde/kdeconnect/BackgroundService.java @@ -1,440 +1,440 @@ /* * Copyright 2014 Albert Vaca Cintora * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.kde.kdeconnect; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.net.ConnectivityManager; import android.os.Binder; import android.os.Build; import android.os.IBinder; import android.text.TextUtils; import android.util.Log; import org.kde.kdeconnect.Backends.BaseLink; import org.kde.kdeconnect.Backends.BaseLinkProvider; import org.kde.kdeconnect.Backends.LanBackend.LanLinkProvider; import org.kde.kdeconnect.Helpers.NotificationHelper; import org.kde.kdeconnect.Helpers.SecurityHelpers.RsaHelper; import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper; import org.kde.kdeconnect.Plugins.Plugin; import org.kde.kdeconnect.Plugins.PluginFactory; import org.kde.kdeconnect.UserInterface.MainActivity; import org.kde.kdeconnect_tp.R; import java.util.ArrayList; import java.util.HashSet; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import androidx.core.app.NotificationCompat; //import org.kde.kdeconnect.Backends.BluetoothBackend.BluetoothLinkProvider; public class BackgroundService extends Service { private static final int FOREGROUND_NOTIFICATION_ID = 1; private static BackgroundService instance; public interface DeviceListChangedCallback { void onDeviceListChanged(); } public interface PluginCallback { void run(T plugin); } private final ConcurrentHashMap deviceListChangedCallbacks = new ConcurrentHashMap<>(); private final ArrayList linkProviders = new ArrayList<>(); private final ConcurrentHashMap devices = new ConcurrentHashMap<>(); private final HashSet discoveryModeAcquisitions = new HashSet<>(); public static BackgroundService getInstance() { return instance; } private boolean acquireDiscoveryMode(Object key) { boolean wasEmpty = discoveryModeAcquisitions.isEmpty(); discoveryModeAcquisitions.add(key); if (wasEmpty) { onNetworkChange(); } //Log.e("acquireDiscoveryMode",key.getClass().getName() +" ["+discoveryModeAcquisitions.size()+"]"); return wasEmpty; } private void releaseDiscoveryMode(Object key) { boolean removed = discoveryModeAcquisitions.remove(key); //Log.e("releaseDiscoveryMode",key.getClass().getName() +" ["+discoveryModeAcquisitions.size()+"]"); if (removed && discoveryModeAcquisitions.isEmpty()) { cleanDevices(); } } public static void addGuiInUseCounter(Context activity) { addGuiInUseCounter(activity, false); } public static void addGuiInUseCounter(final Context activity, final boolean forceNetworkRefresh) { BackgroundService.RunCommand(activity, service -> { boolean refreshed = service.acquireDiscoveryMode(activity); if (!refreshed && forceNetworkRefresh) { service.onNetworkChange(); } }); } public static void removeGuiInUseCounter(final Context activity) { BackgroundService.RunCommand(activity, service -> { //If no user interface is open, close the connections open to other devices service.releaseDiscoveryMode(activity); }); } private final Device.PairingCallback devicePairingCallback = new Device.PairingCallback() { @Override public void incomingRequest() { onDeviceListChanged(); } @Override public void pairingSuccessful() { onDeviceListChanged(); } @Override public void pairingFailed(String error) { onDeviceListChanged(); } @Override public void unpaired() { onDeviceListChanged(); } }; public void onDeviceListChanged() { for (DeviceListChangedCallback callback : deviceListChangedCallbacks.values()) { callback.onDeviceListChanged(); } if (NotificationHelper.isPersistentNotificationEnabled(this)) { //Update the foreground notification with the currently connected device list NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); nm.notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification()); } } private void loadRememberedDevicesFromSettings() { //Log.e("BackgroundService", "Loading remembered trusted devices"); SharedPreferences preferences = getSharedPreferences("trusted_devices", Context.MODE_PRIVATE); Set trustedDevices = preferences.getAll().keySet(); for (String deviceId : trustedDevices) { //Log.e("BackgroundService", "Loading device "+deviceId); if (preferences.getBoolean(deviceId, false)) { Device device = new Device(this, deviceId); devices.put(deviceId, device); device.addPairingCallback(devicePairingCallback); } } } private void registerLinkProviders() { linkProviders.add(new LanLinkProvider(this)); // linkProviders.add(new LoopbackLinkProvider(this)); // linkProviders.add(new BluetoothLinkProvider(this)); } public ArrayList getLinkProviders() { return linkProviders; } public Device getDevice(String id) { return devices.get(id); } private void cleanDevices() { new Thread(() -> { for (Device d : devices.values()) { if (!d.isPaired() && !d.isPairRequested() && !d.isPairRequestedByPeer() && !d.deviceShouldBeKeptAlive()) { d.disconnect(); } } }).start(); } private final BaseLinkProvider.ConnectionReceiver deviceListener = new BaseLinkProvider.ConnectionReceiver() { @Override public void onConnectionReceived(final NetworkPacket identityPacket, final BaseLink link) { String deviceId = identityPacket.getString("deviceId"); Device device = devices.get(deviceId); if (device != null) { Log.i("KDE/BackgroundService", "addLink, known device: " + deviceId); device.addLink(identityPacket, link); } else { Log.i("KDE/BackgroundService", "addLink,unknown device: " + deviceId); device = new Device(BackgroundService.this, identityPacket, link); if (device.isPaired() || device.isPairRequested() || device.isPairRequestedByPeer() || link.linkShouldBeKeptAlive() || !discoveryModeAcquisitions.isEmpty()) { devices.put(deviceId, device); device.addPairingCallback(devicePairingCallback); } else { device.disconnect(); } } onDeviceListChanged(); } @Override public void onConnectionLost(BaseLink link) { Device d = devices.get(link.getDeviceId()); Log.i("KDE/onConnectionLost", "removeLink, deviceId: " + link.getDeviceId()); if (d != null) { d.removeLink(link); if (!d.isReachable() && !d.isPaired()) { //Log.e("onConnectionLost","Removing connection device because it was not paired"); devices.remove(link.getDeviceId()); d.removePairingCallback(devicePairingCallback); } } else { //Log.d("KDE/onConnectionLost","Removing connection to unknown device"); } onDeviceListChanged(); } }; public ConcurrentHashMap getDevices() { return devices; } public void onNetworkChange() { for (BaseLinkProvider a : linkProviders) { a.onNetworkChange(); } } public void addConnectionListener(BaseLinkProvider.ConnectionReceiver cr) { for (BaseLinkProvider a : linkProviders) { a.addConnectionReceiver(cr); } } public void removeConnectionListener(BaseLinkProvider.ConnectionReceiver cr) { for (BaseLinkProvider a : linkProviders) { a.removeConnectionReceiver(cr); } } public void addDeviceListChangedCallback(String key, DeviceListChangedCallback callback) { deviceListChangedCallbacks.put(key, callback); } public void removeDeviceListChangedCallback(String key) { deviceListChangedCallbacks.remove(key); } //This will called only once, even if we launch the service intent several times @Override public void onCreate() { super.onCreate(); instance = this; // Register screen on listener IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON); // See: https://developer.android.com/reference/android/net/ConnectivityManager.html#CONNECTIVITY_ACTION if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); } registerReceiver(new KdeConnectBroadcastReceiver(), filter); Log.i("KDE/BackgroundService", "Service not started yet, initializing..."); PluginFactory.initPluginInfo(getBaseContext()); initializeSecurityParameters(); NotificationHelper.initializeChannels(this); loadRememberedDevicesFromSettings(); registerLinkProviders(); //Link Providers need to be already registered addConnectionListener(deviceListener); for (BaseLinkProvider a : linkProviders) { a.onStart(); } } public void changePersistentNotificationVisibility(boolean visible) { NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); if (visible) { nm.notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification()); } else { stopForeground(true); Start(this); } } private Notification createForegroundNotification() { //Why is this needed: https://developer.android.com/guide/components/services#Foreground Intent intent = new Intent(this, MainActivity.class); PendingIntent pi = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); NotificationCompat.Builder notification = new NotificationCompat.Builder(this, NotificationHelper.Channels.PERSISTENT); notification .setSmallIcon(R.drawable.ic_notification) .setOngoing(true) .setContentIntent(pi) .setPriority(NotificationCompat.PRIORITY_MIN) //MIN so it's not shown in the status bar before Oreo, on Oreo it will be bumped to LOW .setShowWhen(false) .setAutoCancel(false); notification.setGroup("BackgroundService"); ArrayList connectedDevices = new ArrayList<>(); for (Device device : getDevices().values()) { if (device.isReachable() && device.isPaired()) { connectedDevices.add(device.getName()); } } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { //Pre-oreo, the notification will have an empty title line without this notification.setContentTitle(getString(R.string.kde_connect)); } if (connectedDevices.isEmpty()) { notification.setContentText(getString(R.string.foreground_notification_no_devices)); } else { notification.setContentText(getString(R.string.foreground_notification_devices, TextUtils.join(", ", connectedDevices))); } return notification.build(); } private void initializeSecurityParameters() { RsaHelper.initialiseRsaKeys(this); SslHelper.initialiseCertificate(this); } @Override public void onDestroy() { stopForeground(true); for (BaseLinkProvider a : linkProviders) { a.onStop(); } super.onDestroy(); } @Override public IBinder onBind(Intent intent) { return new Binder(); } //To use the service from the gui public interface InstanceCallback { void onServiceStart(BackgroundService service); } private final static ArrayList callbacks = new ArrayList<>(); private final static Lock mutex = new ReentrantLock(true); @Override public int onStartCommand(Intent intent, int flags, int startId) { //This will be called for each intent launch, even if the service is already started and it is reused mutex.lock(); try { for (InstanceCallback c : callbacks) { c.onServiceStart(this); } callbacks.clear(); } finally { mutex.unlock(); } if (NotificationHelper.isPersistentNotificationEnabled(this)) { startForeground(FOREGROUND_NOTIFICATION_ID, createForegroundNotification()); } return Service.START_STICKY; } private static void Start(Context c) { RunCommand(c, null); } public static void RunCommand(final Context c, final InstanceCallback callback) { new Thread(() -> { if (callback != null) { mutex.lock(); try { callbacks.add(callback); } finally { mutex.unlock(); } } Intent serviceIntent = new Intent(c, BackgroundService.class); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { c.startForegroundService(serviceIntent); } else { c.startService(serviceIntent); } }).start(); } - public static void runWithPlugin(final Context c, final String deviceId, final Class pluginClass, final PluginCallback cb) { + public static void RunWithPlugin(final Context c, final String deviceId, final Class pluginClass, final PluginCallback cb) { RunCommand(c, service -> { Device device = service.getDevice(deviceId); if (device == null) { Log.e("BackgroundService", "Device " + deviceId + " not found"); return; } final T plugin = device.getPlugin(pluginClass); if (plugin == null) { Log.e("BackgroundService", "Device " + device.getName() + " does not have plugin " + pluginClass.getName()); return; } cb.run(plugin); }); } } diff --git a/src/org/kde/kdeconnect/Plugins/MousePadPlugin/MousePadActivity.java b/src/org/kde/kdeconnect/Plugins/MousePadPlugin/MousePadActivity.java index a48e834e..866a97c6 100644 --- a/src/org/kde/kdeconnect/Plugins/MousePadPlugin/MousePadActivity.java +++ b/src/org/kde/kdeconnect/Plugins/MousePadPlugin/MousePadActivity.java @@ -1,382 +1,382 @@ /* * Copyright 2014 Ahmed I. Khalil * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.kde.kdeconnect.Plugins.MousePadPlugin; import android.content.Context; import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; import android.view.GestureDetector; import android.view.HapticFeedbackConstants; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.inputmethod.InputMethodManager; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.UserInterface.ThemeUtil; import org.kde.kdeconnect_tp.R; import androidx.appcompat.app.AppCompatActivity; public class MousePadActivity extends AppCompatActivity implements GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener, MousePadGestureDetector.OnGestureListener { private String deviceId; private final static float MinDistanceToSendScroll = 2.5f; // touch gesture scroll private final static float MinDistanceToSendGenericScroll = 0.1f; // real mouse scroll wheel event private final static float StandardDpi = 240.0f; // = hdpi private float mPrevX; private float mPrevY; private float mCurrentX; private float mCurrentY; private float mCurrentSensitivity; private float displayDpiMultiplier; private int scrollDirection = 1; private boolean isScrolling = false; private float accumulatedDistanceY = 0; private GestureDetector mDetector; private MousePadGestureDetector mMousePadGestureDetector; private PointerAccelerationProfile mPointerAccelerationProfile; private PointerAccelerationProfile.MouseDelta mouseDelta; // to be reused on every touch move event private KeyListenerView keyListenerView; enum ClickType { RIGHT, MIDDLE, NONE; static ClickType fromString(String s) { switch (s) { case "right": return RIGHT; case "middle": return MIDDLE; default: return NONE; } } } private ClickType doubleTapAction, tripleTapAction; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ThemeUtil.setUserPreferredTheme(this); setContentView(R.layout.activity_mousepad); deviceId = getIntent().getStringExtra("deviceId"); getWindow().getDecorView().setHapticFeedbackEnabled(true); mDetector = new GestureDetector(this, this); mMousePadGestureDetector = new MousePadGestureDetector(this); mDetector.setOnDoubleTapListener(this); keyListenerView = findViewById(R.id.keyListener); keyListenerView.setDeviceId(deviceId); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); if (prefs.getBoolean(getString(R.string.mousepad_scroll_direction), false)) { scrollDirection = -1; } else { scrollDirection = 1; } String doubleTapSetting = prefs.getString(getString(R.string.mousepad_double_tap_key), getString(R.string.mousepad_default_double)); String tripleTapSetting = prefs.getString(getString(R.string.mousepad_triple_tap_key), getString(R.string.mousepad_default_triple)); String sensitivitySetting = prefs.getString(getString(R.string.mousepad_sensitivity_key), getString(R.string.mousepad_default_sensitivity)); String accelerationProfileName = prefs.getString(getString(R.string.mousepad_acceleration_profile_key), getString(R.string.mousepad_default_acceleration_profile)); mPointerAccelerationProfile = PointerAccelerationProfileFactory.getProfileWithName(accelerationProfileName); doubleTapAction = ClickType.fromString(doubleTapSetting); tripleTapAction = ClickType.fromString(tripleTapSetting); //Technically xdpi and ydpi should be handled separately, //but since ydpi is usually almost equal to xdpi, only xdpi is used for the multiplier. displayDpiMultiplier = StandardDpi / getResources().getDisplayMetrics().xdpi; switch (sensitivitySetting) { case "slowest": mCurrentSensitivity = 0.2f; break; case "aboveSlowest": mCurrentSensitivity = 0.5f; break; case "default": mCurrentSensitivity = 1.0f; break; case "aboveDefault": mCurrentSensitivity = 1.5f; break; case "fastest": mCurrentSensitivity = 2.0f; break; default: mCurrentSensitivity = 1.0f; return; } final View decorView = getWindow().getDecorView(); decorView.setOnSystemUiVisibilityChangeListener(visibility -> { if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) { int fullscreenType = 0; fullscreenType |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { fullscreenType |= View.SYSTEM_UI_FLAG_FULLSCREEN; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { fullscreenType |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; } getWindow().getDecorView().setSystemUiVisibility(fullscreenType); } }); } @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.menu_mousepad, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_right_click: sendRightClick(); return true; case R.id.menu_middle_click: sendMiddleClick(); return true; case R.id.menu_show_keyboard: showKeyboard(); return true; default: return super.onOptionsItemSelected(item); } } @Override public boolean onTouchEvent(MotionEvent event) { if (mMousePadGestureDetector.onTouchEvent(event)) { return true; } if (mDetector.onTouchEvent(event)) { return true; } int actionType = event.getAction(); if (isScrolling) { if (actionType == MotionEvent.ACTION_UP) { isScrolling = false; } else { return false; } } switch (actionType) { case MotionEvent.ACTION_DOWN: mPrevX = event.getX(); mPrevY = event.getY(); break; case MotionEvent.ACTION_MOVE: mCurrentX = event.getX(); mCurrentY = event.getY(); - BackgroundService.runWithPlugin(this, deviceId, MousePadPlugin.class, plugin -> { + BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, plugin -> { float deltaX = (mCurrentX - mPrevX) * displayDpiMultiplier * mCurrentSensitivity; float deltaY = (mCurrentY - mPrevY) * displayDpiMultiplier * mCurrentSensitivity; // Run the mouse delta through the pointer acceleration profile mPointerAccelerationProfile.touchMoved(deltaX, deltaY, event.getEventTime()); mouseDelta = mPointerAccelerationProfile.commitAcceleratedMouseDelta(mouseDelta); plugin.sendMouseDelta(mouseDelta.x, mouseDelta.y); mPrevX = mCurrentX; mPrevY = mCurrentY; }); break; } return true; } @Override public boolean onDown(MotionEvent e) { return false; } @Override public void onShowPress(MotionEvent e) { //From GestureDetector, left empty } @Override public boolean onSingleTapUp(MotionEvent e) { return false; } @Override public boolean onGenericMotionEvent(MotionEvent e) { if (e.getAction() == MotionEvent.ACTION_SCROLL) { final float distanceY = e.getAxisValue(MotionEvent.AXIS_VSCROLL); accumulatedDistanceY += distanceY; if (accumulatedDistanceY > MinDistanceToSendGenericScroll || accumulatedDistanceY < -MinDistanceToSendGenericScroll) { sendScroll(accumulatedDistanceY); accumulatedDistanceY = 0; } } return super.onGenericMotionEvent(e); } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, final float distanceX, final float distanceY) { // If only one thumb is used then cancel the scroll gesture if (e2.getPointerCount() <= 1) { return false; } isScrolling = true; accumulatedDistanceY += distanceY; if (accumulatedDistanceY > MinDistanceToSendScroll || accumulatedDistanceY < -MinDistanceToSendScroll) { sendScroll(scrollDirection * accumulatedDistanceY); accumulatedDistanceY = 0; } return true; } @Override public void onLongPress(MotionEvent e) { getWindow().getDecorView().performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); - BackgroundService.runWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendSingleHold); + BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendSingleHold); } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { return false; } @Override public boolean onSingleTapConfirmed(MotionEvent e) { - BackgroundService.runWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendSingleClick); + BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendSingleClick); return true; } @Override public boolean onDoubleTap(MotionEvent e) { - BackgroundService.runWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendDoubleClick); + BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendDoubleClick); return true; } @Override public boolean onDoubleTapEvent(MotionEvent e) { return false; } @Override public boolean onTripleFingerTap(MotionEvent ev) { switch (tripleTapAction) { case RIGHT: sendRightClick(); break; case MIDDLE: sendMiddleClick(); break; default: } return true; } @Override public boolean onDoubleFingerTap(MotionEvent ev) { switch (doubleTapAction) { case RIGHT: sendRightClick(); break; case MIDDLE: sendMiddleClick(); break; default: } return true; } private void sendMiddleClick() { - BackgroundService.runWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendMiddleClick); + BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendMiddleClick); } private void sendRightClick() { - BackgroundService.runWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendRightClick); + BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendRightClick); } private void sendScroll(final float y) { - BackgroundService.runWithPlugin(this, deviceId, MousePadPlugin.class, plugin -> plugin.sendScroll(0, y)); + BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, plugin -> plugin.sendScroll(0, y)); } //TODO: Does not work on KitKat with or without requestFocus() private void showKeyboard() { InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); keyListenerView.requestFocus(); imm.toggleSoftInputFromWindow(keyListenerView.getWindowToken(), 0, 0); } @Override protected void onStart() { super.onStart(); BackgroundService.addGuiInUseCounter(this); } @Override protected void onStop() { super.onStop(); BackgroundService.removeGuiInUseCounter(this); } } diff --git a/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisMediaSession.java b/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisMediaSession.java index fe5ffe10..30ebd8e3 100644 --- a/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisMediaSession.java +++ b/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisMediaSession.java @@ -1,435 +1,436 @@ /* * Copyright 2017 Matthijs Tijink * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.kde.kdeconnect.Plugins.MprisPlugin; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.preference.PreferenceManager; import android.support.v4.media.MediaMetadataCompat; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Helpers.NotificationHelper; import org.kde.kdeconnect_tp.R; import java.util.HashSet; import androidx.core.app.NotificationCompat; import androidx.core.app.TaskStackBuilder; import androidx.media.app.NotificationCompat.MediaStyle; /** * Controls the mpris media control notification *

* There are two parts to this: * - The notification (with buttons etc.) * - The media session (via MediaSessionCompat; for lock screen control on * older Android version. And in the future for lock screen album covers) */ public class MprisMediaSession implements SharedPreferences.OnSharedPreferenceChangeListener { private final static int MPRIS_MEDIA_NOTIFICATION_ID = 0x91b70463; // echo MprisNotification | md5sum | head -c 8 private final static String MPRIS_MEDIA_SESSION_TAG = "org.kde.kdeconnect_tp.media_session"; private static final MprisMediaSession instance = new MprisMediaSession(); public static MprisMediaSession getInstance() { return instance; } public static MediaSessionCompat getMediaSession() { return instance.mediaSession; } //Holds the device and player displayed in the notification private String notificationDevice = null; private MprisPlugin.MprisPlayer notificationPlayer = null; //Holds the device ids for which we can display a notification private final HashSet mprisDevices = new HashSet<>(); private Context context; private MediaSessionCompat mediaSession; //Callback for mpris plugin updates private final Handler mediaNotificationHandler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { updateMediaNotification(); } }; //Callback for control via the media session API private final MediaSessionCompat.Callback mediaSessionCallback = new MediaSessionCompat.Callback() { @Override public void onPlay() { notificationPlayer.play(); } @Override public void onPause() { notificationPlayer.pause(); } @Override public void onSkipToNext() { notificationPlayer.next(); } @Override public void onSkipToPrevious() { notificationPlayer.previous(); } @Override public void onStop() { notificationPlayer.stop(); } }; /** * Called by the mpris plugin when it wants media control notifications for its device *

* Can be called multiple times, once for each device * * @param _context The context * @param mpris The mpris plugin * @param device The device id */ public void onCreate(Context _context, MprisPlugin mpris, String device) { if (mprisDevices.isEmpty()) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(_context); prefs.registerOnSharedPreferenceChangeListener(this); } context = _context; mprisDevices.add(device); mpris.setPlayerListUpdatedHandler("media_notification", mediaNotificationHandler); mpris.setPlayerStatusUpdatedHandler("media_notification", mediaNotificationHandler); updateMediaNotification(); } /** * Called when a device disconnects/does not want notifications anymore *

* Can be called multiple times, once for each device * * @param mpris The mpris plugin * @param device The device id */ public void onDestroy(MprisPlugin mpris, String device) { mprisDevices.remove(device); mpris.removePlayerStatusUpdatedHandler("media_notification"); mpris.removePlayerListUpdatedHandler("media_notification"); updateMediaNotification(); if (mprisDevices.isEmpty()) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); prefs.unregisterOnSharedPreferenceChangeListener(this); } } /** * Updates which device+player we're going to use in the notification *

* Prefers playing devices/mpris players, but tries to keep displaying the same * player and device, while possible. * * @param service The background service */ private void updateCurrentPlayer(BackgroundService service) { Device device = null; MprisPlugin.MprisPlayer playing = null; //First try the previously displayed player if (notificationDevice != null && mprisDevices.contains(notificationDevice) && notificationPlayer != null) { device = service.getDevice(notificationDevice); } MprisPlugin mpris = null; if (device != null) { mpris = device.getPlugin(MprisPlugin.class); } if (mpris != null) { playing = mpris.getPlayerStatus(notificationPlayer.getPlayer()); } //If nonexistant or not playing, try a different player for the same device if ((playing == null || !playing.isPlaying()) && mpris != null) { MprisPlugin.MprisPlayer playingPlayer = mpris.getPlayingPlayer(); //Only replace the previously found player if we really found one if (playingPlayer != null) { playing = playingPlayer; } } //If nonexistant or not playing, try a different player for another device if (playing == null || !playing.isPlaying()) { for (Device otherDevice : service.getDevices().values()) { //First, check if we actually display notification for this device if (!mprisDevices.contains(otherDevice.getDeviceId())) continue; mpris = otherDevice.getPlugin(MprisPlugin.class); if (mpris == null) continue; MprisPlugin.MprisPlayer playingPlayer = mpris.getPlayingPlayer(); //Only replace the previously found player if we really found one if (playingPlayer != null) { playing = playingPlayer; device = otherDevice; break; } } } //Update the last-displayed device and player notificationDevice = device == null ? null : device.getDeviceId(); notificationPlayer = playing; } /** * Update the media control notification */ private void updateMediaNotification() { BackgroundService.RunCommand(context, service -> { //If the user disabled the media notification, do not show it SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); if (!prefs.getBoolean(context.getString(R.string.mpris_notification_key), true)) { closeMediaNotification(); return; } //Make sure our information is up-to-date updateCurrentPlayer(service); //If the player disappeared (and no other playing one found), just remove the notification if (notificationPlayer == null) { closeMediaNotification(); return; } //Update the metadata and playback status if (mediaSession == null) { mediaSession = new MediaSessionCompat(context, MPRIS_MEDIA_SESSION_TAG); mediaSession.setCallback(mediaSessionCallback); mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); } MediaMetadataCompat.Builder metadata = new MediaMetadataCompat.Builder(); //Fallback because older KDE connect versions do not support getTitle() if (!notificationPlayer.getTitle().isEmpty()) { metadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, notificationPlayer.getTitle()); } else { metadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, notificationPlayer.getCurrentSong()); } if (!notificationPlayer.getArtist().isEmpty()) { metadata.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, notificationPlayer.getArtist()); metadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, notificationPlayer.getArtist()); } if (!notificationPlayer.getAlbum().isEmpty()) { metadata.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, notificationPlayer.getAlbum()); } if (notificationPlayer.getLength() > 0) { metadata.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, notificationPlayer.getLength()); } Bitmap albumArt = notificationPlayer.getAlbumArt(); if (albumArt != null) { metadata.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, albumArt); } mediaSession.setMetadata(metadata.build()); PlaybackStateCompat.Builder playbackState = new PlaybackStateCompat.Builder(); if (notificationPlayer.isPlaying()) { playbackState.setState(PlaybackStateCompat.STATE_PLAYING, notificationPlayer.getPosition(), 1.0f); } else { playbackState.setState(PlaybackStateCompat.STATE_PAUSED, notificationPlayer.getPosition(), 0.0f); } //Create all actions (previous/play/pause/next) Intent iPlay = new Intent(service, MprisMediaNotificationReceiver.class); iPlay.setAction(MprisMediaNotificationReceiver.ACTION_PLAY); iPlay.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice); iPlay.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayer()); PendingIntent piPlay = PendingIntent.getBroadcast(service, 0, iPlay, PendingIntent.FLAG_UPDATE_CURRENT); NotificationCompat.Action.Builder aPlay = new NotificationCompat.Action.Builder( R.drawable.ic_play_white, service.getString(R.string.mpris_play), piPlay); Intent iPause = new Intent(service, MprisMediaNotificationReceiver.class); iPause.setAction(MprisMediaNotificationReceiver.ACTION_PAUSE); iPause.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice); iPause.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayer()); PendingIntent piPause = PendingIntent.getBroadcast(service, 0, iPause, PendingIntent.FLAG_UPDATE_CURRENT); NotificationCompat.Action.Builder aPause = new NotificationCompat.Action.Builder( R.drawable.ic_pause_white, service.getString(R.string.mpris_pause), piPause); Intent iPrevious = new Intent(service, MprisMediaNotificationReceiver.class); iPrevious.setAction(MprisMediaNotificationReceiver.ACTION_PREVIOUS); iPrevious.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice); iPrevious.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayer()); PendingIntent piPrevious = PendingIntent.getBroadcast(service, 0, iPrevious, PendingIntent.FLAG_UPDATE_CURRENT); NotificationCompat.Action.Builder aPrevious = new NotificationCompat.Action.Builder( R.drawable.ic_previous_white, service.getString(R.string.mpris_previous), piPrevious); Intent iNext = new Intent(service, MprisMediaNotificationReceiver.class); iNext.setAction(MprisMediaNotificationReceiver.ACTION_NEXT); iNext.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice); iNext.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayer()); PendingIntent piNext = PendingIntent.getBroadcast(service, 0, iNext, PendingIntent.FLAG_UPDATE_CURRENT); NotificationCompat.Action.Builder aNext = new NotificationCompat.Action.Builder( R.drawable.ic_next_white, service.getString(R.string.mpris_next), piNext); Intent iOpenActivity = new Intent(service, MprisActivity.class); iOpenActivity.putExtra("deviceId", notificationDevice); iOpenActivity.putExtra("player", notificationPlayer.getPlayer()); /* TODO: Remove when Min SDK >= 16 The only way the intent extra's are delivered on API 14 and 15 is by either using a different requestCode every time or using PendingIntent.FLAG_CANCEL_CURRENT instead of PendingIntent.FLAG_UPDATE_CURRENT */ PendingIntent piOpenActivity = TaskStackBuilder.create(context) .addNextIntentWithParentStack(iOpenActivity) .getPendingIntent(Build.VERSION.SDK_INT > 15 ? 0 : (int)System.currentTimeMillis(), PendingIntent.FLAG_UPDATE_CURRENT); NotificationCompat.Builder notification = new NotificationCompat.Builder(context, NotificationHelper.Channels.MEDIA_CONTROL); notification .setAutoCancel(false) .setContentIntent(piOpenActivity) .setSmallIcon(R.drawable.ic_play_white) .setShowWhen(false) .setColor(service.getResources().getColor(R.color.primary)) - .setVisibility(androidx.core.app.NotificationCompat.VISIBILITY_PUBLIC); + .setVisibility(androidx.core.app.NotificationCompat.VISIBILITY_PUBLIC) + .setSubText(service.getDevice(notificationDevice).getName()); if (!notificationPlayer.getTitle().isEmpty()) { notification.setContentTitle(notificationPlayer.getTitle()); } else { notification.setContentTitle(notificationPlayer.getCurrentSong()); } //Only set the notification body text if we have an author and/or album if (!notificationPlayer.getArtist().isEmpty() && !notificationPlayer.getAlbum().isEmpty()) { notification.setContentText(notificationPlayer.getArtist() + " - " + notificationPlayer.getAlbum() + " (" + notificationPlayer.getPlayer() + ")"); } else if (!notificationPlayer.getArtist().isEmpty()) { notification.setContentText(notificationPlayer.getArtist() + " (" + notificationPlayer.getPlayer() + ")"); } else if (!notificationPlayer.getAlbum().isEmpty()) { notification.setContentText(notificationPlayer.getAlbum() + " (" + notificationPlayer.getPlayer() + ")"); } else { notification.setContentText(notificationPlayer.getPlayer()); } if (albumArt != null) { notification.setLargeIcon(albumArt); } if (!notificationPlayer.isPlaying()) { Intent iCloseNotification = new Intent(service, MprisMediaNotificationReceiver.class); iCloseNotification.setAction(MprisMediaNotificationReceiver.ACTION_CLOSE_NOTIFICATION); iCloseNotification.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice); iCloseNotification.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayer()); PendingIntent piCloseNotification = PendingIntent.getActivity(service, 0, iCloseNotification, PendingIntent.FLAG_UPDATE_CURRENT); notification.setDeleteIntent(piCloseNotification); } //Add media control actions int numActions = 0; long playbackActions = 0; if (notificationPlayer.isGoPreviousAllowed()) { notification.addAction(aPrevious.build()); playbackActions |= PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; ++numActions; } if (notificationPlayer.isPlaying() && notificationPlayer.isPauseAllowed()) { notification.addAction(aPause.build()); playbackActions |= PlaybackStateCompat.ACTION_PAUSE; ++numActions; } if (!notificationPlayer.isPlaying() && notificationPlayer.isPlayAllowed()) { notification.addAction(aPlay.build()); playbackActions |= PlaybackStateCompat.ACTION_PLAY; ++numActions; } if (notificationPlayer.isGoNextAllowed()) { notification.addAction(aNext.build()); playbackActions |= PlaybackStateCompat.ACTION_SKIP_TO_NEXT; ++numActions; } playbackState.setActions(playbackActions); mediaSession.setPlaybackState(playbackState.build()); //Only allow deletion if no music is notificationPlayer if (notificationPlayer.isPlaying()) { notification.setOngoing(true); } else { notification.setOngoing(false); } //Use the MediaStyle notification, so it feels like other media players. That also allows adding actions MediaStyle mediaStyle = new MediaStyle(); if (numActions == 1) { mediaStyle.setShowActionsInCompactView(0); } else if (numActions == 2) { mediaStyle.setShowActionsInCompactView(0, 1); } else if (numActions >= 3) { mediaStyle.setShowActionsInCompactView(0, 1, 2); } mediaStyle.setMediaSession(mediaSession.getSessionToken()); notification.setStyle(mediaStyle); notification.setGroup("MprisMediaSession"); //Display the notification mediaSession.setActive(true); final NotificationManager nm = (NotificationManager) service.getSystemService(Context.NOTIFICATION_SERVICE); nm.notify(MPRIS_MEDIA_NOTIFICATION_ID, notification.build()); }); } public void closeMediaNotification() { //Remove the notification NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); nm.cancel(MPRIS_MEDIA_NOTIFICATION_ID); //Clear the current player and media session notificationPlayer = null; if (mediaSession != null) { mediaSession.release(); mediaSession = null; } } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { updateMediaNotification(); } public void playerSelected(MprisPlugin.MprisPlayer player) { notificationPlayer = player; updateMediaNotification(); } } diff --git a/src/org/kde/kdeconnect/Plugins/PhotoPlugin/PhotoActivity.java b/src/org/kde/kdeconnect/Plugins/PhotoPlugin/PhotoActivity.java index 5cb252a1..b689b7ea 100644 --- a/src/org/kde/kdeconnect/Plugins/PhotoPlugin/PhotoActivity.java +++ b/src/org/kde/kdeconnect/Plugins/PhotoPlugin/PhotoActivity.java @@ -1,72 +1,72 @@ package org.kde.kdeconnect.Plugins.PhotoPlugin; import android.content.Intent; import android.net.Uri; import android.os.Environment; import android.provider.MediaStore; import org.kde.kdeconnect.BackgroundService; import java.io.File; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.FileProvider; public class PhotoActivity extends AppCompatActivity { private Uri photoURI; private PhotoPlugin plugin; @Override protected void onStart() { super.onStart(); - BackgroundService.runWithPlugin(this, getIntent().getStringExtra("deviceId"), PhotoPlugin.class, plugin -> { + BackgroundService.RunWithPlugin(this, getIntent().getStringExtra("deviceId"), PhotoPlugin.class, plugin -> { this.plugin = plugin; }); Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); if (takePictureIntent.resolveActivity(getPackageManager()) != null) { File photoFile = null; try { photoFile = createImageFile(); } catch (IOException ignored) { } // Continue only if the File was successfully created if (photoFile != null) { photoURI = FileProvider.getUriForFile(this, "org.kde.kdeconnect_tp.fileprovider", photoFile); takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI); startActivityForResult(takePictureIntent, 1); } } } private File createImageFile() throws IOException { // Create an image file name String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); String imageFileName = "JPEG_" + timeStamp + "_"; File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES); return File.createTempFile( imageFileName, /* prefix */ ".jpg", /* suffix */ storageDir /* directory */ ); } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == -1) { plugin.sendPhoto(photoURI); } finish(); } } diff --git a/src/org/kde/kdeconnect/Plugins/PluginFactory.java b/src/org/kde/kdeconnect/Plugins/PluginFactory.java index 95f2d0c9..1e29eea9 100644 --- a/src/org/kde/kdeconnect/Plugins/PluginFactory.java +++ b/src/org/kde/kdeconnect/Plugins/PluginFactory.java @@ -1,222 +1,222 @@ /* * Copyright 2014 Albert Vaca Cintora * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.kde.kdeconnect.Plugins; import android.content.Context; import android.graphics.drawable.Drawable; import android.util.Log; import org.atteo.classindex.IndexAnnotated; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Plugins.BatteryPlugin.BatteryPlugin; import org.kde.kdeconnect.Plugins.ClibpoardPlugin.ClipboardPlugin; import org.kde.kdeconnect.Plugins.ContactsPlugin.ContactsPlugin; import org.kde.kdeconnect.Plugins.FindMyPhonePlugin.FindMyPhonePlugin; import org.kde.kdeconnect.Plugins.FindRemoteDevicePlugin.FindRemoteDevicePlugin; import org.kde.kdeconnect.Plugins.MousePadPlugin.MousePadPlugin; import org.kde.kdeconnect.Plugins.MprisPlugin.MprisPlugin; import org.kde.kdeconnect.Plugins.NotificationsPlugin.NotificationsPlugin; import org.kde.kdeconnect.Plugins.PhotoPlugin.PhotoPlugin; import org.kde.kdeconnect.Plugins.PingPlugin.PingPlugin; import org.kde.kdeconnect.Plugins.PresenterPlugin.PresenterPlugin; import org.kde.kdeconnect.Plugins.ReceiveNotificationsPlugin.ReceiveNotificationsPlugin; import org.kde.kdeconnect.Plugins.RemoteKeyboardPlugin.RemoteKeyboardPlugin; import org.kde.kdeconnect.Plugins.RunCommandPlugin.RunCommandPlugin; import org.kde.kdeconnect.Plugins.SftpPlugin.SftpPlugin; import org.kde.kdeconnect.Plugins.SharePlugin.SharePlugin; -import org.kde.kdeconnect.Plugins.SystemvolumePlugin.SystemvolumePlugin; +import org.kde.kdeconnect.Plugins.SystemvolumePlugin.SystemVolumePlugin; import org.kde.kdeconnect.Plugins.TelephonyPlugin.TelephonyPlugin; import java.util.Collections; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; public class PluginFactory { @IndexAnnotated public @interface LoadablePlugin { } //Annotate plugins with this so PluginFactory finds them public static class PluginInfo { PluginInfo(String displayName, String description, Drawable icon, boolean enabledByDefault, boolean hasSettings, boolean listenToUnpaired, String[] supportedPacketTypes, String[] outgoingPacketTypes, Class instantiableClass) { this.displayName = displayName; this.description = description; this.icon = icon; this.enabledByDefault = enabledByDefault; this.hasSettings = hasSettings; this.listenToUnpaired = listenToUnpaired; HashSet incoming = new HashSet<>(); if (supportedPacketTypes != null) Collections.addAll(incoming, supportedPacketTypes); this.supportedPacketTypes = Collections.unmodifiableSet(incoming); HashSet outgoing = new HashSet<>(); if (outgoingPacketTypes != null) Collections.addAll(outgoing, outgoingPacketTypes); this.outgoingPacketTypes = Collections.unmodifiableSet(outgoing); this.instantiableClass = instantiableClass; } public String getDisplayName() { return displayName; } public String getDescription() { return description; } public Drawable getIcon() { return icon; } public boolean hasSettings() { return hasSettings; } public boolean isEnabledByDefault() { return enabledByDefault; } public boolean listenToUnpaired() { return listenToUnpaired; } Set getOutgoingPacketTypes() { return outgoingPacketTypes; } public Set getSupportedPacketTypes() { return supportedPacketTypes; } Class getInstantiableClass() { return instantiableClass; } private final String displayName; private final String description; private final Drawable icon; private final boolean enabledByDefault; private final boolean hasSettings; private final boolean listenToUnpaired; private final Set supportedPacketTypes; private final Set outgoingPacketTypes; private final Class instantiableClass; } private static final Map pluginInfo = new ConcurrentHashMap<>(); static Class pluginClasses[] = { BatteryPlugin.class, ClipboardPlugin.class, ContactsPlugin.class, FindMyPhonePlugin.class, FindRemoteDevicePlugin.class, MousePadPlugin.class, MprisPlugin.class, //MprisReceiverPlugin.class, //Breaks in Android 4 NotificationsPlugin.class, PhotoPlugin.class, PingPlugin.class, PresenterPlugin.class, ReceiveNotificationsPlugin.class, RemoteKeyboardPlugin.class, RunCommandPlugin.class, SftpPlugin.class, SharePlugin.class, //SMSPlugin.class, - SystemvolumePlugin.class, + SystemVolumePlugin.class, TelephonyPlugin.class, }; public static PluginInfo getPluginInfo(String pluginKey) { return pluginInfo.get(pluginKey); } public static void initPluginInfo(Context context) { try { for (Class pluginClass : pluginClasses) { Plugin p = ((Plugin) pluginClass.newInstance()); p.setContext(context, null); PluginInfo info = new PluginInfo(p.getDisplayName(), p.getDescription(), p.getIcon(), p.isEnabledByDefault(), p.hasSettings(), p.listensToUnpairedDevices(), p.getSupportedPacketTypes(), p.getOutgoingPacketTypes(), p.getClass()); pluginInfo.put(p.getPluginKey(), info); } } catch (Exception e) { throw new RuntimeException(e); } Log.i("PluginFactory","Loaded "+pluginInfo.size()+" plugins"); } public static Set getAvailablePlugins() { return pluginInfo.keySet(); } public static Plugin instantiatePluginForDevice(Context context, String pluginKey, Device device) { PluginInfo info = pluginInfo.get(pluginKey); try { Plugin plugin = info.getInstantiableClass().newInstance(); plugin.setContext(context, device); return plugin; } catch (Exception e) { Log.e("PluginFactory", "Could not instantiate plugin: " + pluginKey); e.printStackTrace(); return null; } } public static Set getIncomingCapabilities() { HashSet capabilities = new HashSet<>(); for (PluginInfo plugin : pluginInfo.values()) { capabilities.addAll(plugin.getSupportedPacketTypes()); } return capabilities; } public static Set getOutgoingCapabilities() { HashSet capabilities = new HashSet<>(); for (PluginInfo plugin : pluginInfo.values()) { capabilities.addAll(plugin.getOutgoingPacketTypes()); } return capabilities; } public static Set pluginsForCapabilities(Set incoming, Set outgoing) { HashSet plugins = new HashSet<>(); for (Map.Entry entry : pluginInfo.entrySet()) { String pluginId = entry.getKey(); PluginInfo info = entry.getValue(); //Check incoming against outgoing if (Collections.disjoint(outgoing, info.getSupportedPacketTypes()) && Collections.disjoint(incoming, info.getOutgoingPacketTypes())) { Log.i("PluginFactory", "Won't load " + pluginId + " because of unmatched capabilities"); continue; //No capabilities in common, do not load this plugin } plugins.add(pluginId); } return plugins; } } diff --git a/src/org/kde/kdeconnect/Plugins/PresenterPlugin/PresenterActivity.java b/src/org/kde/kdeconnect/Plugins/PresenterPlugin/PresenterActivity.java index 26ca6352..fedea60c 100644 --- a/src/org/kde/kdeconnect/Plugins/PresenterPlugin/PresenterActivity.java +++ b/src/org/kde/kdeconnect/Plugins/PresenterPlugin/PresenterActivity.java @@ -1,141 +1,141 @@ /* * Copyright 2014 Ahmed I. Khalil * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.kde.kdeconnect.Plugins.PresenterPlugin; import android.content.Context; import android.os.Build; import android.os.Bundle; import android.os.PowerManager; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.UserInterface.ThemeUtil; import org.kde.kdeconnect_tp.R; import androidx.appcompat.app.AppCompatActivity; import androidx.media.VolumeProviderCompat; public class PresenterActivity extends AppCompatActivity { private MediaSessionCompat mMediaSession; private PresenterPlugin plugin; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ThemeUtil.setUserPreferredTheme(this); setContentView(R.layout.activity_presenter); final String deviceId = getIntent().getStringExtra("deviceId"); - BackgroundService.runWithPlugin(this, deviceId, PresenterPlugin.class, plugin -> runOnUiThread(() -> { + BackgroundService.RunWithPlugin(this, deviceId, PresenterPlugin.class, plugin -> runOnUiThread(() -> { this.plugin = plugin; findViewById(R.id.next_button).setOnClickListener(v -> plugin.sendNext()); findViewById(R.id.previous_button).setOnClickListener(v -> plugin.sendPrevious()); })); } @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.menu_presenter, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch(item.getItemId()) { case R.id.fullscreen: plugin.sendFullscreen(); return true; case R.id.exit_presentation: plugin.sendEsc(); return true; default: return super.onContextItemSelected(item); } } @Override protected void onStart() { super.onStart(); BackgroundService.addGuiInUseCounter(this); if (mMediaSession != null) { mMediaSession.setActive(true); return; } createMediaSession(); //Mediasession will keep } @Override protected void onStop() { super.onStop(); BackgroundService.removeGuiInUseCounter(this); if (mMediaSession != null) { PowerManager pm = (PowerManager) this.getSystemService(Context.POWER_SERVICE); boolean screenOn; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { screenOn = pm.isInteractive(); } else { screenOn = pm.isScreenOn(); } if (screenOn) { mMediaSession.release(); } // else we are in the lockscreen, keep the mediasession } } private void createMediaSession() { mMediaSession = new MediaSessionCompat(this, "kdeconnect"); mMediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); mMediaSession.setPlaybackState(new PlaybackStateCompat.Builder() .setState(PlaybackStateCompat.STATE_PLAYING, 0, 0) .build()); mMediaSession.setPlaybackToRemote(getVolumeProvider()); mMediaSession.setActive(true); } private VolumeProviderCompat getVolumeProvider() { final int VOLUME_UP = 1; final int VOLUME_DOWN = -1; return new VolumeProviderCompat(VolumeProviderCompat.VOLUME_CONTROL_RELATIVE, 1, 0) { @Override public void onAdjustVolume(int direction) { if (direction == VOLUME_UP) { plugin.sendNext(); } else if (direction == VOLUME_DOWN) { plugin.sendPrevious(); } } }; } } diff --git a/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandActivity.java b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandActivity.java index 7958e8db..d3e96f26 100644 --- a/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandActivity.java +++ b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandActivity.java @@ -1,201 +1,176 @@ /* * Copyright 2015 Aleix Pol Gonzalez * Copyright 2015 Albert Vaca Cintora * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.kde.kdeconnect.Plugins.RunCommandPlugin; import android.content.ClipboardManager; import android.content.Context; import android.os.Build; import android.os.Bundle; -import android.util.Log; import android.view.ContextMenu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import com.google.android.material.floatingactionbutton.FloatingActionButton; import org.json.JSONException; import org.json.JSONObject; import org.kde.kdeconnect.BackgroundService; -import org.kde.kdeconnect.Device; import org.kde.kdeconnect.UserInterface.List.ListAdapter; import org.kde.kdeconnect.UserInterface.ThemeUtil; import org.kde.kdeconnect_tp.R; import java.util.ArrayList; import java.util.Collections; import androidx.annotation.RequiresApi; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; public class RunCommandActivity extends AppCompatActivity { private String deviceId; private final RunCommandPlugin.CommandsChangedCallback commandsChangedCallback = this::updateView; private ArrayList commandItems; private void updateView() { - BackgroundService.RunCommand(this, service -> { - - final Device device = service.getDevice(deviceId); - final RunCommandPlugin plugin = device.getPlugin(RunCommandPlugin.class); - if (plugin == null) { - Log.e("RunCommandActivity", "device has no runcommand plugin!"); - return; - } + BackgroundService.RunWithPlugin(this, deviceId, RunCommandPlugin.class, plugin -> { runOnUiThread(() -> { ListView view = findViewById(R.id.runcommandslist); registerForContextMenu(view); commandItems = new ArrayList<>(); for (JSONObject obj : plugin.getCommandList()) { try { commandItems.add(new CommandEntry(obj.getString("name"), obj.getString("command"), obj.getString("key"))); } catch (JSONException e) { e.printStackTrace(); } } Collections.sort(commandItems, (lhs, rhs) -> { String lName = ((CommandEntry) lhs).getName(); String rName = ((CommandEntry) rhs).getName(); return lName.compareTo(rName); }); ListAdapter adapter = new ListAdapter(RunCommandActivity.this, commandItems); view.setAdapter(adapter); view.setOnItemClickListener((adapterView, view1, i, l) -> { CommandEntry entry = (CommandEntry) commandItems.get(i); plugin.runCommand(entry.getKey()); }); TextView explanation = findViewById(R.id.addcomand_explanation); String text = getString(R.string.addcommand_explanation); if (!plugin.canAddCommand()) { text += "\n" + getString(R.string.addcommand_explanation2); } explanation.setText(text); explanation.setVisibility(commandItems.isEmpty() ? View.VISIBLE : View.GONE); }); }); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ThemeUtil.setUserPreferredTheme(this); setContentView(R.layout.activity_runcommand); deviceId = getIntent().getStringExtra("deviceId"); boolean canAddCommands = BackgroundService.getInstance().getDevice(deviceId).getPlugin(RunCommandPlugin.class).canAddCommand(); FloatingActionButton addCommandButton = findViewById(R.id.add_command_button); - addCommandButton.setVisibility(canAddCommands ? View.VISIBLE : View.GONE); - - addCommandButton.setOnClickListener(view -> BackgroundService.RunCommand(RunCommandActivity.this, service -> { - - final Device device = service.getDevice(deviceId); - final RunCommandPlugin plugin = device.getPlugin(RunCommandPlugin.class); - if (plugin == null) { - Log.e("RunCommandActivity", "device has no runcommand plugin!"); - return; - } + if (canAddCommands) { + addCommandButton.show(); + } else { + addCommandButton.hide(); + } - plugin.sendSetupPacket(); + addCommandButton.setOnClickListener(v -> { - AlertDialog dialog = new AlertDialog.Builder(RunCommandActivity.this) - .setTitle(R.string.add_command) - .setMessage(R.string.add_command_description) - .setPositiveButton(R.string.ok, null) - .create(); - dialog.show(); + BackgroundService.RunWithPlugin(RunCommandActivity.this, deviceId, RunCommandPlugin.class, plugin -> { + plugin.sendSetupPacket(); + AlertDialog dialog = new AlertDialog.Builder(RunCommandActivity.this) + .setTitle(R.string.add_command) + .setMessage(R.string.add_command_description) + .setPositiveButton(R.string.ok, null) + .create(); + dialog.show(); + }); - })); + }); updateView(); } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.runcommand_context, menu); } @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB) @Override public boolean onContextItemSelected(MenuItem item) { AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); if (item.getItemId() == R.id.copy_url_to_clipboard) { CommandEntry entry = (CommandEntry) commandItems.get(info.position); String url = "kdeconnect://runcommand/" + deviceId + "/" + entry.getKey(); ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); cm.setText(url); Toast toast = Toast.makeText(this, R.string.clipboard_toast, Toast.LENGTH_SHORT); toast.show(); } return false; } @Override protected void onResume() { super.onResume(); - BackgroundService.RunCommand(this, service -> { - - final Device device = service.getDevice(deviceId); - final RunCommandPlugin plugin = device.getPlugin(RunCommandPlugin.class); - if (plugin == null) { - Log.e("RunCommandActivity", "device has no runcommand plugin!"); - return; - } + BackgroundService.RunWithPlugin(this, deviceId, RunCommandPlugin.class, plugin -> { plugin.addCommandsUpdatedCallback(commandsChangedCallback); }); } @Override protected void onPause() { super.onPause(); - BackgroundService.RunCommand(this, service -> { - - final Device device = service.getDevice(deviceId); - final RunCommandPlugin plugin = device.getPlugin(RunCommandPlugin.class); - if (plugin == null) { - Log.e("RunCommandActivity", "device has no runcommand plugin!"); - return; - } + BackgroundService.RunWithPlugin(this, deviceId, RunCommandPlugin.class, plugin -> { plugin.removeCommandsUpdatedCallback(commandsChangedCallback); }); } } diff --git a/src/org/kde/kdeconnect/Plugins/SharePlugin/SendFileActivity.java b/src/org/kde/kdeconnect/Plugins/SharePlugin/SendFileActivity.java index 37847a5e..9dfbe308 100644 --- a/src/org/kde/kdeconnect/Plugins/SharePlugin/SendFileActivity.java +++ b/src/org/kde/kdeconnect/Plugins/SharePlugin/SendFileActivity.java @@ -1,102 +1,102 @@ /* * Copyright 2014 Albert Vaca Cintora * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.kde.kdeconnect.Plugins.SharePlugin; import android.app.Activity; import android.content.ClipData; import android.content.Intent; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.util.Log; import android.widget.Toast; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.UserInterface.ThemeUtil; import org.kde.kdeconnect_tp.R; import java.util.ArrayList; import androidx.appcompat.app.AppCompatActivity; public class SendFileActivity extends AppCompatActivity { private String mDeviceId; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ThemeUtil.setUserPreferredTheme(this); mDeviceId = getIntent().getStringExtra("deviceId"); Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setType("*/*"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); } intent.addCategory(Intent.CATEGORY_OPENABLE); try { startActivityForResult( Intent.createChooser(intent, getString(R.string.send_files)), Activity.RESULT_FIRST_USER); } catch (android.content.ActivityNotFoundException ex) { Toast.makeText(this, R.string.no_file_browser, Toast.LENGTH_SHORT).show(); finish(); } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case Activity.RESULT_FIRST_USER: if (resultCode == RESULT_OK) { final ArrayList uris = new ArrayList<>(); Uri uri = data.getData(); if (uri != null) { uris.add(uri); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { ClipData clipdata = data.getClipData(); if (clipdata != null) { for (int i = 0; i < clipdata.getItemCount(); i++) { uris.add(clipdata.getItemAt(i).getUri()); } } } if (uris.isEmpty()) { Log.w("SendFileActivity", "No files to send?"); } else { - BackgroundService.runWithPlugin(this, mDeviceId, SharePlugin.class, plugin -> plugin.queuedSendUriList(uris)); + BackgroundService.RunWithPlugin(this, mDeviceId, SharePlugin.class, plugin -> plugin.queuedSendUriList(uris)); } } finish(); break; default: super.onActivityResult(requestCode, resultCode, data); } } } diff --git a/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java b/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java index f93e6bfa..affa4e45 100644 --- a/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java +++ b/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java @@ -1,173 +1,173 @@ /* * Copyright 2014 Albert Vaca Cintora * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.kde.kdeconnect.Plugins.SharePlugin; import android.content.Intent; import android.os.Bundle; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.widget.ListView; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.UserInterface.List.EntryItem; import org.kde.kdeconnect.UserInterface.List.ListAdapter; import org.kde.kdeconnect.UserInterface.List.SectionItem; import org.kde.kdeconnect.UserInterface.ThemeUtil; import org.kde.kdeconnect_tp.R; import java.util.ArrayList; import java.util.Collection; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; public class ShareActivity extends AppCompatActivity { private SwipeRefreshLayout mSwipeRefreshLayout; @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.refresh, menu); return true; } @Override public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { case R.id.menu_refresh: updateComputerListAction(); break; default: break; } return true; } private void updateComputerListAction() { updateComputerList(); BackgroundService.RunCommand(ShareActivity.this, BackgroundService::onNetworkChange); mSwipeRefreshLayout.setRefreshing(true); new Thread(() -> { try { Thread.sleep(1500); } catch (InterruptedException ignored) { } runOnUiThread(() -> mSwipeRefreshLayout.setRefreshing(false)); }).start(); } private void updateComputerList() { final Intent intent = getIntent(); String action = intent.getAction(); if (!Intent.ACTION_SEND.equals(action) && !Intent.ACTION_SEND_MULTIPLE.equals(action)) { finish(); return; } BackgroundService.RunCommand(this, service -> { Collection devices = service.getDevices().values(); final ArrayList devicesList = new ArrayList<>(); final ArrayList items = new ArrayList<>(); SectionItem section = new SectionItem(getString(R.string.share_to)); items.add(section); for (Device d : devices) { if (d.isReachable() && d.isPaired()) { devicesList.add(d); items.add(new EntryItem(d.getName())); section.isEmpty = false; } } runOnUiThread(() -> { ListView list = findViewById(R.id.devices_list); list.setAdapter(new ListAdapter(ShareActivity.this, items)); list.setOnItemClickListener((adapterView, view, i, l) -> { Device device = devicesList.get(i - 1); //NOTE: -1 because of the title! - BackgroundService.runWithPlugin(this, device.getDeviceId(), SharePlugin.class, plugin -> plugin.share(intent)); + BackgroundService.RunWithPlugin(this, device.getDeviceId(), SharePlugin.class, plugin -> plugin.share(intent)); finish(); }); }); }); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ThemeUtil.setUserPreferredTheme(this); setContentView(R.layout.devices_list); ActionBar actionBar = getSupportActionBar(); mSwipeRefreshLayout = findViewById(R.id.refresh_list_layout); mSwipeRefreshLayout.setOnRefreshListener( this::updateComputerListAction ); if (actionBar != null) { actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_HOME | ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_CUSTOM); } } @Override protected void onStart() { super.onStart(); final Intent intent = getIntent(); final String deviceId = intent.getStringExtra("deviceId"); if (deviceId != null) { - BackgroundService.runWithPlugin(this, deviceId, SharePlugin.class, plugin -> { + BackgroundService.RunWithPlugin(this, deviceId, SharePlugin.class, plugin -> { plugin.share(intent); finish(); }); } else { BackgroundService.addGuiInUseCounter(this); BackgroundService.RunCommand(this, service -> { service.onNetworkChange(); service.addDeviceListChangedCallback("ShareActivity", this::updateComputerList); }); updateComputerList(); } } @Override protected void onStop() { BackgroundService.RunCommand(this, service -> service.removeDeviceListChangedCallback("ShareActivity")); BackgroundService.removeGuiInUseCounter(this); super.onStop(); } } diff --git a/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemvolumePlugin.java b/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemVolumePlugin.java similarity index 94% rename from src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemvolumePlugin.java rename to src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemVolumePlugin.java index 01b81b22..3b5b306f 100644 --- a/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemvolumePlugin.java +++ b/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemVolumePlugin.java @@ -1,151 +1,151 @@ /* * Copyright 2018 Nicolas Fella * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.kde.kdeconnect.Plugins.SystemvolumePlugin; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.kde.kdeconnect.NetworkPacket; import org.kde.kdeconnect.Plugins.Plugin; import org.kde.kdeconnect.Plugins.PluginFactory; import org.kde.kdeconnect_tp.R; import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; +import java.util.concurrent.ConcurrentHashMap; @PluginFactory.LoadablePlugin -public class SystemvolumePlugin extends Plugin { +public class SystemVolumePlugin extends Plugin { private final static String PACKET_TYPE_SYSTEMVOLUME = "kdeconnect.systemvolume"; private final static String PACKET_TYPE_SYSTEMVOLUME_REQUEST = "kdeconnect.systemvolume.request"; public interface SinkListener { void sinksChanged(); } - private final HashMap sinks; + private final ConcurrentHashMap sinks; private final ArrayList listeners; - public SystemvolumePlugin() { - sinks = new HashMap<>(); + public SystemVolumePlugin() { + sinks = new ConcurrentHashMap<>(); listeners = new ArrayList<>(); } @Override public String getDisplayName() { return context.getResources().getString(R.string.pref_plugin_systemvolume); } @Override public String getDescription() { return context.getResources().getString(R.string.pref_plugin_systemvolume_desc); } @Override public boolean onPacketReceived(NetworkPacket np) { if (np.has("sinkList")) { sinks.clear(); try { JSONArray sinkArray = np.getJSONArray("sinkList"); for (int i = 0; i < sinkArray.length(); i++) { JSONObject sinkObj = sinkArray.getJSONObject(i); Sink sink = new Sink(sinkObj); sinks.put(sink.getName(), sink); } } catch (JSONException e) { e.printStackTrace(); } for (SinkListener l : listeners) { l.sinksChanged(); } } else { String name = np.getString("name"); if (sinks.containsKey(name)) { if (np.has("volume")) { sinks.get(name).setVolume(np.getInt("volume")); } if (np.has("muted")) { sinks.get(name).setMute(np.getBoolean("muted")); } } } return true; } void sendVolume(String name, int volume) { NetworkPacket np = new NetworkPacket(PACKET_TYPE_SYSTEMVOLUME_REQUEST); np.set("volume", volume); np.set("name", name); device.sendPacket(np); } void sendMute(String name, boolean mute) { NetworkPacket np = new NetworkPacket(PACKET_TYPE_SYSTEMVOLUME_REQUEST); np.set("muted", mute); np.set("name", name); device.sendPacket(np); } void requestSinkList() { NetworkPacket np = new NetworkPacket(PACKET_TYPE_SYSTEMVOLUME_REQUEST); np.set("requestSinks", true); device.sendPacket(np); } @Override public boolean hasMainActivity() { return false; } @Override public boolean displayInContextMenu() { return false; } @Override public String[] getSupportedPacketTypes() { return new String[]{PACKET_TYPE_SYSTEMVOLUME}; } @Override public String[] getOutgoingPacketTypes() { return new String[]{PACKET_TYPE_SYSTEMVOLUME_REQUEST}; } Collection getSinks() { return sinks.values(); } void addSinkListener(SinkListener listener) { listeners.add(listener); } void removeSinkListener(SinkListener listener) { listeners.remove(listener); } } diff --git a/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemvolumeFragment.java b/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemvolumeFragment.java index aa09417e..c102528f 100644 --- a/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemvolumeFragment.java +++ b/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemvolumeFragment.java @@ -1,153 +1,153 @@ /* * Copyright 2018 Nicolas Fella * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.kde.kdeconnect.Plugins.SystemvolumePlugin; import android.app.Activity; import android.content.Context; import android.os.Bundle; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ImageButton; import android.widget.SeekBar; import android.widget.TextView; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect_tp.R; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.ListFragment; -public class SystemvolumeFragment extends ListFragment implements Sink.UpdateListener, SystemvolumePlugin.SinkListener { +public class SystemvolumeFragment extends ListFragment implements Sink.UpdateListener, SystemVolumePlugin.SinkListener { - private SystemvolumePlugin plugin; + private SystemVolumePlugin plugin; private Activity activity; private SinkAdapter adapter; private Context context; private boolean tracking; @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); getListView().setDivider(null); setListAdapter(new SinkAdapter(getContext(), new Sink[0])); } @Override public void updateSink(final Sink sink) { // Don't set progress while the slider is moved if (!tracking) { activity.runOnUiThread(() -> adapter.notifyDataSetChanged()); } } public void connectToPlugin(final String deviceId) { - BackgroundService.runWithPlugin(activity, deviceId, SystemvolumePlugin.class, plugin -> { + BackgroundService.RunWithPlugin(activity, deviceId, SystemVolumePlugin.class, plugin -> { this.plugin = plugin; plugin.addSinkListener(SystemvolumeFragment.this); plugin.requestSinkList(); }); } @Override public void onAttach(Context context) { super.onAttach(context); activity = getActivity(); this.context = context; } @Override public void sinksChanged() { for (Sink sink : plugin.getSinks()) { sink.addListener(SystemvolumeFragment.this); } activity.runOnUiThread(() -> { adapter = new SinkAdapter(context, plugin.getSinks().toArray(new Sink[0])); setListAdapter(adapter); }); } private class SinkAdapter extends ArrayAdapter { private SinkAdapter(@NonNull Context context, @NonNull Sink[] objects) { super(context, R.layout.list_item_systemvolume, objects); } @NonNull @Override public View getView(final int position, @Nullable View convertView, @NonNull ViewGroup parent) { View view = getLayoutInflater().inflate(R.layout.list_item_systemvolume, parent, false); UIListener listener = new UIListener(getItem(position)); ((TextView) view.findViewById(R.id.systemvolume_label)).setText(getItem(position).getDescription()); final SeekBar seekBar = view.findViewById(R.id.systemvolume_seek); seekBar.setMax(getItem(position).getMaxVolume()); seekBar.setProgress(getItem(position).getVolume()); seekBar.setOnSeekBarChangeListener(listener); ImageButton button = view.findViewById(R.id.systemvolume_mute); int iconRes = getItem(position).isMute() ? R.drawable.ic_volume_mute_black : R.drawable.ic_volume_black; button.setImageResource(iconRes); button.setOnClickListener(listener); return view; } } private class UIListener implements SeekBar.OnSeekBarChangeListener, ImageButton.OnClickListener { private final Sink sink; private UIListener(Sink sink) { this.sink = sink; } @Override public void onProgressChanged(final SeekBar seekBar, int i, boolean b) { BackgroundService.RunCommand(activity, service -> plugin.sendVolume(sink.getName(), seekBar.getProgress())); } @Override public void onStartTrackingTouch(SeekBar seekBar) { tracking = true; } @Override public void onStopTrackingTouch(final SeekBar seekBar) { tracking = false; BackgroundService.RunCommand(activity, service -> plugin.sendVolume(sink.getName(), seekBar.getProgress())); } @Override public void onClick(View view) { plugin.sendMute(sink.getName(), !sink.isMute()); } } }