diff --git a/build.gradle b/build.gradle index 99f8f542..25c59e9e 100644 --- a/build.gradle +++ b/build.gradle @@ -1,91 +1,89 @@ buildscript { repositories { jcenter() google() } dependencies { classpath 'com.android.tools.build:gradle:3.1.2' } } apply plugin: 'com.android.application' android { buildToolsVersion '27.0.3' compileSdkVersion 25 defaultConfig { minSdkVersion 9 targetSdkVersion 25 //multiDexEnabled true //testInstrumentationRunner "com.android.test.runner.MultiDexTestRunner" } dexOptions { javaMaxHeapSize "2g" } compileOptions { - // Use Java 1.7, requires minSdk 8 - //SSHD requires mina when running on JDK < 7 - sourceCompatibility JavaVersion.VERSION_1_7 - targetCompatibility JavaVersion.VERSION_1_7 + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } sourceSets { main { manifest.srcFile 'AndroidManifest.xml' java.srcDirs = ['src'] resources.srcDirs = ['resources'] res.srcDirs = ['res'] assets.srcDirs = ['assets'] } androidTest { java.srcDirs = ['tests'] } } packagingOptions { pickFirst "META-INF/DEPENDENCIES" pickFirst "META-INF/LICENSE" pickFirst "META-INF/NOTICE" pickFirst "META-INF/BCKEY.SF" pickFirst "META-INF/BCKEY.DSA" pickFirst "META-INF/INDEX.LIST" pickFirst "META-INF/io.netty.versions.properties" } lintOptions { abortOnError false checkReleaseBuilds false } buildTypes { debug { minifyEnabled false useProguard false } release { //keep on 'release', set to 'all' when testing to make sure proguard is not deleting important stuff minifyEnabled true useProguard true proguardFiles getDefaultProguardFile('proguard-android.txt'),'proguard-rules.pro' } } } dependencies { repositories { jcenter() google() } implementation 'com.android.support:support-v4:25.4.0' implementation 'com.android.support:appcompat-v7:25.4.0' implementation 'com.android.support:design:25.4.0' implementation 'com.jakewharton:disklrucache:2.0.2' //For caching album art bitmaps implementation 'org.apache.sshd:sshd-core:0.8.0' //0.9 seems to fail on Android 6 and 1.+ requires java.nio.file, which doesn't exist in Android implementation 'com.madgag.spongycastle:pkix:1.54.0.0' //For SSL certificate generation // Testing androidTestImplementation 'org.mockito:mockito-core:1.10.19' androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:1.1'// Because mockito has some problems with dex environment androidTestImplementation 'org.skyscreamer:jsonassert:1.3.0' testImplementation 'junit:junit:4.12' } diff --git a/src/org/kde/kdeconnect/Backends/LanBackend/LanLink.java b/src/org/kde/kdeconnect/Backends/LanBackend/LanLink.java index fff03bca..d7d17b97 100644 --- a/src/org/kde/kdeconnect/Backends/LanBackend/LanLink.java +++ b/src/org/kde/kdeconnect/Backends/LanBackend/LanLink.java @@ -1,301 +1,298 @@ /* * 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.Backends.LanBackend; import android.content.Context; import android.util.Log; import org.json.JSONObject; import org.kde.kdeconnect.Backends.BaseLink; import org.kde.kdeconnect.Backends.BasePairingHandler; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Helpers.SecurityHelpers.RsaHelper; import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper; import org.kde.kdeconnect.Helpers.StringsHelper; import org.kde.kdeconnect.NetworkPacket; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketTimeoutException; import java.nio.channels.NotYetConnectedException; import java.security.PublicKey; import javax.net.ssl.SSLSocket; public class LanLink extends BaseLink { public interface LinkDisconnectedCallback { void linkDisconnected(LanLink brokenLink); } public enum ConnectionStarted { Locally, Remotely } private ConnectionStarted connectionSource; // If the other device sent me a broadcast, // I should not close the connection with it // because it's probably trying to find me and // potentially ask for pairing. private volatile Socket socket = null; private LinkDisconnectedCallback callback; @Override public void disconnect() { Log.i("LanLink/Disconnect","socket:"+ socket.hashCode()); try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } //Returns the old socket public Socket reset(final Socket newSocket, ConnectionStarted connectionSource) throws IOException { Socket oldSocket = socket; socket = newSocket; this.connectionSource = connectionSource; if (oldSocket != null) { oldSocket.close(); //This should cancel the readThread } //Log.e("LanLink", "Start listening"); //Create a thread to take care of incoming data for the new socket - new Thread(new Runnable() { - @Override - public void run() { - try { - BufferedReader reader = new BufferedReader(new InputStreamReader(newSocket.getInputStream(), StringsHelper.UTF8)); - while (true) { - String packet; - try { - packet = reader.readLine(); - } catch (SocketTimeoutException e) { - continue; - } - if (packet == null) { - throw new IOException("End of stream"); - } - if (packet.isEmpty()) { - continue; - } - NetworkPacket np = NetworkPacket.unserialize(packet); - receivedNetworkPacket(np); + new Thread(() -> { + try { + BufferedReader reader = new BufferedReader(new InputStreamReader(newSocket.getInputStream(), StringsHelper.UTF8)); + while (true) { + String packet; + try { + packet = reader.readLine(); + } catch (SocketTimeoutException e) { + continue; + } + if (packet == null) { + throw new IOException("End of stream"); } - } catch (Exception e) { - Log.i("LanLink", "Socket closed: " + newSocket.hashCode() + ". Reason: " + e.getMessage()); - try { Thread.sleep(300); } catch (InterruptedException ignored) {} // Wait a bit because we might receive a new socket meanwhile - boolean thereIsaANewSocket = (newSocket != socket); - if (!thereIsaANewSocket) { - callback.linkDisconnected(LanLink.this); + if (packet.isEmpty()) { + continue; } + NetworkPacket np = NetworkPacket.unserialize(packet); + receivedNetworkPacket(np); + } + } catch (Exception e) { + Log.i("LanLink", "Socket closed: " + newSocket.hashCode() + ". Reason: " + e.getMessage()); + try { Thread.sleep(300); } catch (InterruptedException ignored) {} // Wait a bit because we might receive a new socket meanwhile + boolean thereIsaANewSocket = (newSocket != socket); + if (!thereIsaANewSocket) { + callback.linkDisconnected(LanLink.this); } } }).start(); return oldSocket; } public LanLink(Context context, String deviceId, LanLinkProvider linkProvider, Socket socket, ConnectionStarted connectionSource) throws IOException { super(context, deviceId, linkProvider); callback = linkProvider; reset(socket, connectionSource); } @Override public String getName() { return "LanLink"; } @Override public BasePairingHandler getPairingHandler(Device device, BasePairingHandler.PairingHandlerCallback callback) { return new LanPairingHandler(device, callback); } //Blocking, do not call from main thread private boolean sendPacketInternal(NetworkPacket np, final Device.SendPacketStatusCallback callback, PublicKey key) { if (socket == null) { Log.e("KDE/sendPacket", "Not yet connected"); callback.onFailure(new NotYetConnectedException()); return false; } try { //Prepare socket for the payload final ServerSocket server; if (np.hasPayload()) { server = LanLinkProvider.openServerSocketOnFreePort(LanLinkProvider.PAYLOAD_TRANSFER_MIN_PORT); JSONObject payloadTransferInfo = new JSONObject(); payloadTransferInfo.put("port", server.getLocalPort()); np.setPayloadTransferInfo(payloadTransferInfo); } else { server = null; } //Encrypt if key provided if (key != null) { np = RsaHelper.encrypt(np, key); } //Log.e("LanLink/sendPacket", np.getType()); //Send body of the network package try { OutputStream writer = socket.getOutputStream(); writer.write(np.serialize().getBytes(StringsHelper.UTF8)); writer.flush(); } catch (Exception e) { disconnect(); //main socket is broken, disconnect throw e; } //Send payload if (server != null) { Socket payloadSocket = null; OutputStream outputStream = null; InputStream inputStream = null; try { //Wait a maximum of 10 seconds for the other end to establish a connection with our socket, close it afterwards server.setSoTimeout(10*1000); payloadSocket = server.accept(); //Convert to SSL if needed if (socket instanceof SSLSocket) { payloadSocket = SslHelper.convertToSslSocket(context, payloadSocket, getDeviceId(), true, false); } outputStream = payloadSocket.getOutputStream(); inputStream = np.getPayload(); Log.i("KDE/LanLink", "Beginning to send payload"); byte[] buffer = new byte[4096]; int bytesRead; long size = np.getPayloadSize(); long progress = 0; long timeSinceLastUpdate = -1; while ((bytesRead = inputStream.read(buffer)) != -1) { //Log.e("ok",""+bytesRead); progress += bytesRead; outputStream.write(buffer, 0, bytesRead); if (size > 0) { if (timeSinceLastUpdate + 500 < System.currentTimeMillis()) { //Report progress every half a second long percent = ((100 * progress) / size); callback.onProgressChanged((int) percent); timeSinceLastUpdate = System.currentTimeMillis(); } } } outputStream.flush(); outputStream.close(); Log.i("KDE/LanLink", "Finished sending payload ("+progress+" bytes written)"); } finally { try { server.close(); } catch (Exception e) { } try { payloadSocket.close(); } catch (Exception e) { } try { inputStream.close(); } catch (Exception e) { } try { outputStream.close(); } catch (Exception e) { } } } callback.onSuccess(); return true; } catch (Exception e) { if (callback != null) { callback.onFailure(e); } return false; } finally { //Make sure we close the payload stream, if any InputStream stream = np.getPayload(); try { stream.close(); } catch (Exception e) { } } } //Blocking, do not call from main thread @Override public boolean sendPacket(NetworkPacket np, Device.SendPacketStatusCallback callback) { return sendPacketInternal(np, callback, null); } //Blocking, do not call from main thread @Override public boolean sendPacketEncrypted(NetworkPacket np, Device.SendPacketStatusCallback callback, PublicKey key) { return sendPacketInternal(np, callback, key); } private void receivedNetworkPacket(NetworkPacket np) { if (np.getType().equals(NetworkPacket.PACKET_TYPE_ENCRYPTED)) { try { np = RsaHelper.decrypt(np, privateKey); } catch(Exception e) { e.printStackTrace(); Log.e("KDE/onPacketReceived","Exception decrypting the package"); } } if (np.hasPayloadTransferInfo()) { Socket payloadSocket = new Socket(); try { int tcpPort = np.getPayloadTransferInfo().getInt("port"); InetSocketAddress deviceAddress = (InetSocketAddress) socket.getRemoteSocketAddress(); payloadSocket.connect(new InetSocketAddress(deviceAddress.getAddress(), tcpPort)); // Use ssl if existing link is on ssl if (socket instanceof SSLSocket) { payloadSocket = SslHelper.convertToSslSocket(context, payloadSocket, getDeviceId(), true, true); } np.setPayload(payloadSocket.getInputStream(), np.getPayloadSize()); } catch (Exception e) { try { payloadSocket.close(); } catch(Exception ignored) { } e.printStackTrace(); Log.e("KDE/LanLink", "Exception connecting to payload remote socket"); } } packageReceived(np); } @Override public boolean linkShouldBeKeptAlive() { return true; //FIXME: Current implementation is broken, so for now we will keep links always established //We keep the remotely initiated connections, since the remotes require them if they want to request //pairing to us, or connections that are already paired. //return (connectionSource == ConnectionStarted.Remotely); } } diff --git a/src/org/kde/kdeconnect/Backends/LanBackend/LanLinkProvider.java b/src/org/kde/kdeconnect/Backends/LanBackend/LanLinkProvider.java index 9db10620..6574809a 100644 --- a/src/org/kde/kdeconnect/Backends/LanBackend/LanLinkProvider.java +++ b/src/org/kde/kdeconnect/Backends/LanBackend/LanLinkProvider.java @@ -1,508 +1,487 @@ /* * 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.Backends.LanBackend; import android.content.Context; import android.content.SharedPreferences; import android.os.Build; import android.preference.PreferenceManager; import android.util.Base64; import android.util.Log; import org.kde.kdeconnect.Backends.BaseLink; import org.kde.kdeconnect.Backends.BaseLinkProvider; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Helpers.DeviceHelper; import org.kde.kdeconnect.Helpers.NetworkHelper; import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper; import org.kde.kdeconnect.Helpers.StringsHelper; import org.kde.kdeconnect.NetworkPacket; import org.kde.kdeconnect.UserInterface.CustomDevicesActivity; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; import java.security.cert.Certificate; import java.util.ArrayList; import java.util.HashMap; import java.util.Timer; import java.util.TimerTask; import javax.net.SocketFactory; import javax.net.ssl.HandshakeCompletedEvent; import javax.net.ssl.HandshakeCompletedListener; import javax.net.ssl.SSLSocket; /** * This BaseLinkProvider creates {@link LanLink}s to other devices on the same * WiFi network. The first packet sent over a socket must be an * {@link NetworkPacket#createIdentityPacket(Context)}. * * @see #identityPacketReceived(NetworkPacket, Socket, LanLink.ConnectionStarted) */ public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDisconnectedCallback { public static final int MIN_VERSION_WITH_SSL_SUPPORT = 6; public static final int MIN_VERSION_WITH_NEW_PORT_SUPPORT = 7; final static int MIN_PORT_LEGACY = 1714; final static int MIN_PORT = 1716; final static int MAX_PORT = 1764; final static int PAYLOAD_TRANSFER_MIN_PORT = 1739; final Context context; private final HashMap visibleComputers = new HashMap<>(); //Links by device id ServerSocket tcpServer; private DatagramSocket udpServer; private DatagramSocket udpServerOldPort; boolean listening = false; // To prevent infinte loop between Android < IceCream because both device can only broadcast identity package but cannot connect via TCP ArrayList reverseConnectionBlackList = new ArrayList<>(); @Override // SocketClosedCallback public void linkDisconnected(LanLink brokenLink) { String deviceId = brokenLink.getDeviceId(); visibleComputers.remove(deviceId); connectionLost(brokenLink); } //They received my UDP broadcast and are connecting to me. The first thing they sned should be their identity. void tcpPacketReceived(Socket socket) throws Exception { NetworkPacket networkPacket; try { BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); String message = reader.readLine(); networkPacket = NetworkPacket.unserialize(message); //Log.e("TcpListener","Received TCP package: "+networkPacket.serialize()); } catch (Exception e) { e.printStackTrace(); return; } if (!networkPacket.getType().equals(NetworkPacket.PACKET_TYPE_IDENTITY)) { Log.e("KDE/LanLinkProvider", "Expecting an identity package instead of " + networkPacket.getType()); return; } Log.i("KDE/LanLinkProvider", "Identity package received from a TCP connection from " + networkPacket.getString("deviceName")); identityPacketReceived(networkPacket, socket, LanLink.ConnectionStarted.Locally); } //I've received their broadcast and should connect to their TCP socket and send my identity. void udpPacketReceived(DatagramPacket packet) throws Exception { final InetAddress address = packet.getAddress(); try { String message = new String(packet.getData(), StringsHelper.UTF8); final NetworkPacket identityPacket = NetworkPacket.unserialize(message); final String deviceId = identityPacket.getString("deviceId"); if (!identityPacket.getType().equals(NetworkPacket.PACKET_TYPE_IDENTITY)) { Log.e("KDE/LanLinkProvider", "Expecting an UDP identity package"); return; } else { String myId = DeviceHelper.getDeviceId(context); if (deviceId.equals(myId)) { //Ignore my own broadcast return; } } if (identityPacket.getInt("protocolVersion") >= MIN_VERSION_WITH_NEW_PORT_SUPPORT && identityPacket.getInt("tcpPort") < MIN_PORT) { Log.w("KDE/LanLinkProvider", "Ignoring a udp broadcast from legacy port because it comes from a device which knows about the new port."); return; } Log.i("KDE/LanLinkProvider", "Broadcast identity package received from " + identityPacket.getString("deviceName")); int tcpPort = identityPacket.getInt("tcpPort", MIN_PORT); SocketFactory socketFactory = SocketFactory.getDefault(); Socket socket = socketFactory.createSocket(address, tcpPort); configureSocket(socket); OutputStream out = socket.getOutputStream(); NetworkPacket myIdentity = NetworkPacket.createIdentityPacket(context); out.write(myIdentity.serialize().getBytes()); out.flush(); identityPacketReceived(identityPacket, socket, LanLink.ConnectionStarted.Remotely); } catch (Exception e) { Log.e("KDE/LanLinkProvider", "Cannot connect to " + address); e.printStackTrace(); if (!reverseConnectionBlackList.contains(address)) { Log.w("KDE/LanLinkProvider", "Blacklisting " + address); reverseConnectionBlackList.add(address); new Timer().schedule(new TimerTask() { @Override public void run() { reverseConnectionBlackList.remove(address); } }, 5 * 1000); // Try to cause a reverse connection onNetworkChange(); } } } void configureSocket(Socket socket) { try { socket.setKeepAlive(true); } catch (SocketException e) { e.printStackTrace(); } } /** * Called when a new 'identity' packet is received. Those are passed here by * {@link #tcpPacketReceived(Socket)} and {@link #udpPacketReceived(DatagramPacket)}. *

* If the remote device should be connected, this calls {@link #addLink}. * Otherwise, if there was an Exception, we unpair from that device. *

* * @param identityPacket identity of a remote device * @param socket a new Socket, which should be used to receive packets from the remote device * @param connectionStarted which side started this connection */ private void identityPacketReceived(final NetworkPacket identityPacket, final Socket socket, final LanLink.ConnectionStarted connectionStarted) { String myId = DeviceHelper.getDeviceId(context); final String deviceId = identityPacket.getString("deviceId"); if (deviceId.equals(myId)) { Log.e("KDE/LanLinkProvider", "Somehow I'm connected to myself, ignoring. This should not happen."); return; } // If I'm the TCP server I will be the SSL client and viceversa. final boolean clientMode = (connectionStarted == LanLink.ConnectionStarted.Locally); // Add ssl handler if device uses new protocol try { if (identityPacket.getInt("protocolVersion") >= MIN_VERSION_WITH_SSL_SUPPORT) { SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE); boolean isDeviceTrusted = preferences.getBoolean(deviceId, false); if (isDeviceTrusted && !SslHelper.isCertificateStored(context, deviceId)) { //Device paired with and old version, we can't use it as we lack the certificate - BackgroundService.RunCommand(context, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - Device device = service.getDevice(deviceId); - if (device == null) return; - device.unpair(); - //Retry as unpaired - identityPacketReceived(identityPacket, socket, connectionStarted); - } + BackgroundService.RunCommand(context, service -> { + Device device = service.getDevice(deviceId); + if (device == null) return; + device.unpair(); + //Retry as unpaired + identityPacketReceived(identityPacket, socket, connectionStarted); }); } Log.i("KDE/LanLinkProvider", "Starting SSL handshake with " + identityPacket.getString("deviceName") + " trusted:" + isDeviceTrusted); final SSLSocket sslsocket = SslHelper.convertToSslSocket(context, socket, deviceId, isDeviceTrusted, clientMode); - sslsocket.addHandshakeCompletedListener(new HandshakeCompletedListener() { - @Override - public void handshakeCompleted(HandshakeCompletedEvent event) { - String mode = clientMode ? "client" : "server"; - try { - Certificate certificate = event.getPeerCertificates()[0]; - identityPacket.set("certificate", Base64.encodeToString(certificate.getEncoded(), 0)); - Log.i("KDE/LanLinkProvider", "Handshake as " + mode + " successful with " + identityPacket.getString("deviceName") + " secured with " + event.getCipherSuite()); - addLink(identityPacket, sslsocket, connectionStarted); - } catch (Exception e) { - Log.e("KDE/LanLinkProvider", "Handshake as " + mode + " failed with " + identityPacket.getString("deviceName")); - e.printStackTrace(); - BackgroundService.RunCommand(context, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - Device device = service.getDevice(deviceId); - if (device == null) return; - device.unpair(); - } - }); - } + sslsocket.addHandshakeCompletedListener(event -> { + String mode = clientMode ? "client" : "server"; + try { + Certificate certificate = event.getPeerCertificates()[0]; + identityPacket.set("certificate", Base64.encodeToString(certificate.getEncoded(), 0)); + Log.i("KDE/LanLinkProvider", "Handshake as " + mode + " successful with " + identityPacket.getString("deviceName") + " secured with " + event.getCipherSuite()); + addLink(identityPacket, sslsocket, connectionStarted); + } catch (Exception e) { + Log.e("KDE/LanLinkProvider", "Handshake as " + mode + " failed with " + identityPacket.getString("deviceName")); + e.printStackTrace(); + BackgroundService.RunCommand(context, service -> { + Device device = service.getDevice(deviceId); + if (device == null) return; + device.unpair(); + }); } }); //Handshake is blocking, so do it on another thread and free this thread to keep receiving new connection - new Thread(new Runnable() { - @Override - public void run() { - try { - sslsocket.startHandshake(); - } catch (Exception e) { - Log.e("KDE/LanLinkProvider", "Handshake failed with " + identityPacket.getString("deviceName")); - e.printStackTrace(); - - //String[] ciphers = sslsocket.getSupportedCipherSuites(); - //for (String cipher : ciphers) { - // Log.i("SupportedCiphers","cipher: " + cipher); - //} - } + new Thread(() -> { + try { + sslsocket.startHandshake(); + } catch (Exception e) { + Log.e("KDE/LanLinkProvider", "Handshake failed with " + identityPacket.getString("deviceName")); + e.printStackTrace(); + + //String[] ciphers = sslsocket.getSupportedCipherSuites(); + //for (String cipher : ciphers) { + // Log.i("SupportedCiphers","cipher: " + cipher); + //} } }).start(); } else { addLink(identityPacket, socket, connectionStarted); } } catch (Exception e) { e.printStackTrace(); } } /** * Add or update a link in the {@link #visibleComputers} map. This method is synchronized, which ensures that only one * link is operated on at a time. *

* Without synchronization, the call to {@link SslHelper#parseCertificate(byte[])} in * {@link Device#addLink(NetworkPacket, BaseLink)} crashes on some devices running Oreo 8.1 (SDK level 27). *

* * @param identityPacket representation of remote device * @param socket a new Socket, which should be used to receive packets from the remote device * @param connectionOrigin which side started this connection * @throws IOException if an exception is thrown by {@link LanLink#reset(Socket, LanLink.ConnectionStarted)} */ private synchronized void addLink(final NetworkPacket identityPacket, Socket socket, LanLink.ConnectionStarted connectionOrigin) throws IOException { String deviceId = identityPacket.getString("deviceId"); LanLink currentLink = visibleComputers.get(deviceId); if (currentLink != null) { //Update old link Log.i("KDE/LanLinkProvider", "Reusing same link for device " + deviceId); final Socket oldSocket = currentLink.reset(socket, connectionOrigin); //Log.e("KDE/LanLinkProvider", "Replacing socket. old: "+ oldSocket.hashCode() + " - new: "+ socket.hashCode()); } else { Log.i("KDE/LanLinkProvider", "Creating a new link for device " + deviceId); //Let's create the link LanLink link = new LanLink(context, deviceId, this, socket, connectionOrigin); visibleComputers.put(deviceId, link); connectionAccepted(identityPacket, link); } } public LanLinkProvider(Context context) { this.context = context; } private DatagramSocket setupUdpListener(int udpPort) { final DatagramSocket server; try { server = new DatagramSocket(udpPort); server.setReuseAddress(true); server.setBroadcast(true); } catch (SocketException e) { Log.e("LanLinkProvider", "Error creating udp server"); e.printStackTrace(); return null; } - new Thread(new Runnable() { - @Override - public void run() { - while (listening) { - final int bufferSize = 1024 * 512; - byte[] data = new byte[bufferSize]; - DatagramPacket packet = new DatagramPacket(data, bufferSize); - try { - server.receive(packet); - udpPacketReceived(packet); - } catch (Exception e) { - e.printStackTrace(); - Log.e("LanLinkProvider", "UdpReceive exception"); - } + new Thread(() -> { + while (listening) { + final int bufferSize = 1024 * 512; + byte[] data = new byte[bufferSize]; + DatagramPacket packet = new DatagramPacket(data, bufferSize); + try { + server.receive(packet); + udpPacketReceived(packet); + } catch (Exception e) { + e.printStackTrace(); + Log.e("LanLinkProvider", "UdpReceive exception"); } - Log.w("UdpListener", "Stopping UDP listener"); } + Log.w("UdpListener", "Stopping UDP listener"); }).start(); return server; } private void setupTcpListener() { try { tcpServer = openServerSocketOnFreePort(MIN_PORT); - new Thread(new Runnable() { - @Override - public void run() { - while (listening) { - try { - Socket socket = tcpServer.accept(); - configureSocket(socket); - tcpPacketReceived(socket); - } catch (Exception e) { - e.printStackTrace(); - Log.e("LanLinkProvider", "TcpReceive exception"); - } + new Thread(() -> { + while (listening) { + try { + Socket socket = tcpServer.accept(); + configureSocket(socket); + tcpPacketReceived(socket); + } catch (Exception e) { + e.printStackTrace(); + Log.e("LanLinkProvider", "TcpReceive exception"); } - Log.w("TcpListener", "Stopping TCP listener"); } + Log.w("TcpListener", "Stopping TCP listener"); }).start(); } catch (Exception e) { e.printStackTrace(); } } static ServerSocket openServerSocketOnFreePort(int minPort) throws IOException { int tcpPort = minPort; while (tcpPort < MAX_PORT) { try { ServerSocket candidateServer = new ServerSocket(); candidateServer.bind(new InetSocketAddress(tcpPort)); Log.i("KDE/LanLink", "Using port " + tcpPort); return candidateServer; } catch (IOException e) { tcpPort++; } } Log.e("KDE/LanLink", "No ports available"); throw new IOException("No ports available"); } private void broadcastUdpPacket() { if (NetworkHelper.isOnMobileNetwork(context)) { Log.w("LanLinkProvider", "On 3G network, not sending broadcast."); return; } - new Thread(new Runnable() { - @Override - public void run() { + new Thread(() -> { - String deviceListPrefs = PreferenceManager.getDefaultSharedPreferences(context).getString(CustomDevicesActivity.KEY_CUSTOM_DEVLIST_PREFERENCE, ""); - ArrayList iplist = new ArrayList<>(); - if (!deviceListPrefs.isEmpty()) { - iplist = CustomDevicesActivity.deserializeIpList(deviceListPrefs); - } - iplist.add("255.255.255.255"); //Default: broadcast. + String deviceListPrefs = PreferenceManager.getDefaultSharedPreferences(context).getString(CustomDevicesActivity.KEY_CUSTOM_DEVLIST_PREFERENCE, ""); + ArrayList iplist = new ArrayList<>(); + if (!deviceListPrefs.isEmpty()) { + iplist = CustomDevicesActivity.deserializeIpList(deviceListPrefs); + } + iplist.add("255.255.255.255"); //Default: broadcast. - NetworkPacket identity = NetworkPacket.createIdentityPacket(context); - identity.set("tcpPort", MIN_PORT); - DatagramSocket socket = null; - byte[] bytes = null; - try { - socket = new DatagramSocket(); - socket.setReuseAddress(true); - socket.setBroadcast(true); - bytes = identity.serialize().getBytes(StringsHelper.UTF8); - } catch (Exception e) { - e.printStackTrace(); - Log.e("KDE/LanLinkProvider", "Failed to create DatagramSocket"); - } + NetworkPacket identity = NetworkPacket.createIdentityPacket(context); + identity.set("tcpPort", MIN_PORT); + DatagramSocket socket = null; + byte[] bytes = null; + try { + socket = new DatagramSocket(); + socket.setReuseAddress(true); + socket.setBroadcast(true); + bytes = identity.serialize().getBytes(StringsHelper.UTF8); + } catch (Exception e) { + e.printStackTrace(); + Log.e("KDE/LanLinkProvider", "Failed to create DatagramSocket"); + } - if (bytes != null) { - //Log.e("KDE/LanLinkProvider","Sending packet to "+iplist.size()+" ips"); - for (String ipstr : iplist) { - try { - InetAddress client = InetAddress.getByName(ipstr); - socket.send(new DatagramPacket(bytes, bytes.length, client, MIN_PORT)); - socket.send(new DatagramPacket(bytes, bytes.length, client, MIN_PORT_LEGACY)); - //Log.i("KDE/LanLinkProvider","Udp identity package sent to address "+client); - } catch (Exception e) { - e.printStackTrace(); - Log.e("KDE/LanLinkProvider", "Sending udp identity package failed. Invalid address? (" + ipstr + ")"); - } + if (bytes != null) { + //Log.e("KDE/LanLinkProvider","Sending packet to "+iplist.size()+" ips"); + for (String ipstr : iplist) { + try { + InetAddress client = InetAddress.getByName(ipstr); + socket.send(new DatagramPacket(bytes, bytes.length, client, MIN_PORT)); + socket.send(new DatagramPacket(bytes, bytes.length, client, MIN_PORT_LEGACY)); + //Log.i("KDE/LanLinkProvider","Udp identity package sent to address "+client); + } catch (Exception e) { + e.printStackTrace(); + Log.e("KDE/LanLinkProvider", "Sending udp identity package failed. Invalid address? (" + ipstr + ")"); } } + } - if (socket != null) { - socket.close(); - } - + if (socket != null) { + socket.close(); } + }).start(); } @Override public void onStart() { //Log.i("KDE/LanLinkProvider", "onStart"); if (!listening) { listening = true; udpServer = setupUdpListener(MIN_PORT); udpServerOldPort = setupUdpListener(MIN_PORT_LEGACY); // Due to certificate request from SSL server to client, the certificate request message from device with latest android version to device with // old android version causes a FATAL ALERT message stating that incorrect certificate request // Server is disabled on these devices and using a reverse connection strategy. This works well for connection of these devices with kde // and newer android versions. Although devices with android version less than ICS cannot connect to other devices who also have android version less // than ICS because server is disabled on both if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { Log.w("KDE/LanLinkProvider", "Not starting a TCP server because it's not supported on Android < 14. Operating only as client."); } else { setupTcpListener(); } broadcastUdpPacket(); } } @Override public void onNetworkChange() { broadcastUdpPacket(); } @Override public void onStop() { //Log.i("KDE/LanLinkProvider", "onStop"); listening = false; try { tcpServer.close(); } catch (Exception e) { e.printStackTrace(); } try { udpServer.close(); } catch (Exception e) { e.printStackTrace(); } try { udpServerOldPort.close(); } catch (Exception e) { e.printStackTrace(); } } @Override public String getName() { return "LanLinkProvider"; } } diff --git a/src/org/kde/kdeconnect/BackgroundService.java b/src/org/kde/kdeconnect/BackgroundService.java index 8f131eb0..46686b01 100644 --- a/src/org/kde/kdeconnect/BackgroundService.java +++ b/src/org/kde/kdeconnect/BackgroundService.java @@ -1,348 +1,336 @@ /* * 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.Service; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.os.Binder; import android.os.Build; import android.os.IBinder; import android.util.Log; import org.kde.kdeconnect.Backends.BaseLink; import org.kde.kdeconnect.Backends.BaseLinkProvider; //import org.kde.kdeconnect.Backends.BluetoothBackend.BluetoothLinkProvider; import org.kde.kdeconnect.Backends.LanBackend.LanLinkProvider; import org.kde.kdeconnect.Helpers.SecurityHelpers.RsaHelper; import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper; 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; public class BackgroundService extends Service { private static BackgroundService instance; public interface DeviceListChangedCallback { void onDeviceListChanged(); } 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; } public boolean acquireDiscoveryMode(Object key) { boolean wasEmpty = discoveryModeAcquisitions.isEmpty(); discoveryModeAcquisitions.add(key); if (wasEmpty) { onNetworkChange(); } //Log.e("acquireDiscoveryMode",key.getClass().getName() +" ["+discoveryModeAcquisitions.size()+"]"); return wasEmpty; } public 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, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - boolean refreshed = service.acquireDiscoveryMode(activity); - if (!refreshed && forceNetworkRefresh) { - service.onNetworkChange(); - } + BackgroundService.RunCommand(activity, service -> { + boolean refreshed = service.acquireDiscoveryMode(activity); + if (!refreshed && forceNetworkRefresh) { + service.onNetworkChange(); } }); } public static void removeGuiInUseCounter(final Context activity) { - BackgroundService.RunCommand(activity, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - //If no user interface is open, close the connections open to other devices - service.releaseDiscoveryMode(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(); } } 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)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { // linkProviders.add(new BluetoothLinkProvider(this)); } } public ArrayList getLinkProviders() { return linkProviders; } public Device getDevice(String id) { return devices.get(id); } private void cleanDevices() { - new Thread(new Runnable() { - @Override - public void run() { - for (Device d : devices.values()) { - if (!d.isPaired() && !d.isPairRequested() && !d.isPairRequestedByPeer() && !d.deviceShouldBeKeptAlive()) { - d.disconnect(); - } + 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); registerReceiver(new KdeConnectBroadcastReceiver(), filter); Log.i("KDE/BackgroundService", "Service not started yet, initializing..."); initializeSecurityParameters(); loadRememberedDevicesFromSettings(); registerLinkProviders(); //Link Providers need to be already registered addConnectionListener(deviceListener); for (BaseLinkProvider a : linkProviders) { a.onStart(); } } void initializeSecurityParameters() { RsaHelper.initialiseRsaKeys(this); SslHelper.initialiseCertificate(this); } @Override public void onDestroy() { 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(); } return Service.START_STICKY; } public static void Start(Context c) { RunCommand(c, null); } public static void RunCommand(final Context c, final InstanceCallback callback) { - new Thread(new Runnable() { - @Override - public void run() { - if (callback != null) { - mutex.lock(); - try { - callbacks.add(callback); - } finally { - mutex.unlock(); - } + new Thread(() -> { + if (callback != null) { + mutex.lock(); + try { + callbacks.add(callback); + } finally { + mutex.unlock(); } - Intent serviceIntent = new Intent(c, BackgroundService.class); - c.startService(serviceIntent); } + Intent serviceIntent = new Intent(c, BackgroundService.class); + c.startService(serviceIntent); }).start(); } } diff --git a/src/org/kde/kdeconnect/Device.java b/src/org/kde/kdeconnect/Device.java index 11c338da..581786f4 100644 --- a/src/org/kde/kdeconnect/Device.java +++ b/src/org/kde/kdeconnect/Device.java @@ -1,917 +1,912 @@ /* * 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.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.os.Build; import android.preference.PreferenceManager; import android.support.v4.app.NotificationCompat; import android.support.v4.content.ContextCompat; import android.util.Base64; import android.util.Log; import org.kde.kdeconnect.Backends.BaseLink; import org.kde.kdeconnect.Backends.BasePairingHandler; import org.kde.kdeconnect.Backends.LanBackend.LanLinkProvider; import org.kde.kdeconnect.Helpers.NotificationHelper; 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.security.KeyFactory; import java.security.PrivateKey; import java.security.PublicKey; import java.security.cert.Certificate; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Vector; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; public class Device implements BaseLink.PacketReceiver { private final Context context; private final String deviceId; private String name; public PublicKey publicKey; public Certificate certificate; private int notificationId; private int protocolVersion; private DeviceType deviceType; private PairStatus pairStatus; private final CopyOnWriteArrayList pairingCallback = new CopyOnWriteArrayList<>(); private Map pairingHandlers = new HashMap<>(); private final CopyOnWriteArrayList links = new CopyOnWriteArrayList<>(); private List m_supportedPlugins = new ArrayList<>(); private final ConcurrentHashMap plugins = new ConcurrentHashMap<>(); private final ConcurrentHashMap failedPlugins = new ConcurrentHashMap<>(); private final ConcurrentHashMap pluginsWithoutPermissions = new ConcurrentHashMap<>(); private final ConcurrentHashMap pluginsWithoutOptionalPermissions = new ConcurrentHashMap<>(); private Map> pluginsByIncomingInterface = new HashMap<>(); private final SharedPreferences settings; private final CopyOnWriteArrayList pluginsChangedListeners = new CopyOnWriteArrayList<>(); public interface PluginsChangedListener { void onPluginsChanged(Device device); } public enum PairStatus { NotPaired, Paired } public enum DeviceType { Phone, Tablet, Computer; public static DeviceType FromString(String s) { if ("tablet".equals(s)) return Tablet; if ("phone".equals(s)) return Phone; return Computer; //Default } public String toString() { switch (this) { case Tablet: return "tablet"; case Phone: return "phone"; default: return "desktop"; } } } public interface PairingCallback { void incomingRequest(); void pairingSuccessful(); void pairingFailed(String error); void unpaired(); } //Remembered trusted device, we need to wait for a incoming devicelink to communicate Device(Context context, String deviceId) { settings = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE); //Log.e("Device","Constructor A"); this.context = context; this.deviceId = deviceId; this.name = settings.getString("deviceName", context.getString(R.string.unknown_device)); this.pairStatus = PairStatus.Paired; this.protocolVersion = NetworkPacket.ProtocolVersion; //We don't know it yet this.deviceType = DeviceType.FromString(settings.getString("deviceType", "desktop")); try { String publicKeyStr = settings.getString("publicKey", null); if (publicKeyStr != null) { byte[] publicKeyBytes = Base64.decode(publicKeyStr, 0); publicKey = KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(publicKeyBytes)); } } catch (Exception e) { e.printStackTrace(); Log.e("KDE/Device", "Exception deserializing stored public key for device"); } //Assume every plugin is supported until addLink is called and we can get the actual list m_supportedPlugins = new Vector<>(PluginFactory.getAvailablePlugins()); //Do not load plugins yet, the device is not present //reloadPluginsFromSettings(); } //Device known via an incoming connection sent to us via a devicelink, we know everything but we don't trust it yet Device(Context context, NetworkPacket np, BaseLink dl) { //Log.e("Device","Constructor B"); this.context = context; this.deviceId = np.getString("deviceId"); this.name = context.getString(R.string.unknown_device); //We read it in addLink this.pairStatus = PairStatus.NotPaired; this.protocolVersion = 0; this.deviceType = DeviceType.Computer; this.publicKey = null; settings = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE); addLink(np, dl); } public String getName() { return name != null ? name : context.getString(R.string.unknown_device); } public Drawable getIcon() { int drawableId; switch (deviceType) { case Phone: drawableId = R.drawable.ic_device_phone; break; case Tablet: drawableId = R.drawable.ic_device_tablet; break; default: drawableId = R.drawable.ic_device_laptop; } return ContextCompat.getDrawable(context, drawableId); } public DeviceType getDeviceType() { return deviceType; } public String getDeviceId() { return deviceId; } public Context getContext() { return context; } //Returns 0 if the version matches, < 0 if it is older or > 0 if it is newer public int compareProtocolVersion() { return protocolVersion - NetworkPacket.ProtocolVersion; } // // Pairing-related functions // public boolean isPaired() { return pairStatus == PairStatus.Paired; } /* Asks all pairing handlers that, is pair requested? */ public boolean isPairRequested() { boolean pairRequested = false; for (BasePairingHandler ph : pairingHandlers.values()) { pairRequested = pairRequested || ph.isPairRequested(); } return pairRequested; } /* Asks all pairing handlers that, is pair requested by peer? */ public boolean isPairRequestedByPeer() { boolean pairRequestedByPeer = false; for (BasePairingHandler ph : pairingHandlers.values()) { pairRequestedByPeer = pairRequestedByPeer || ph.isPairRequestedByPeer(); } return pairRequestedByPeer; } public void addPairingCallback(PairingCallback callback) { pairingCallback.add(callback); } public void removePairingCallback(PairingCallback callback) { pairingCallback.remove(callback); } public void requestPairing() { Resources res = context.getResources(); if (isPaired()) { for (PairingCallback cb : pairingCallback) { cb.pairingFailed(res.getString(R.string.error_already_paired)); } return; } if (!isReachable()) { for (PairingCallback cb : pairingCallback) { cb.pairingFailed(res.getString(R.string.error_not_reachable)); } return; } for (BasePairingHandler ph : pairingHandlers.values()) { ph.requestPairing(); } } public void unpair() { for (BasePairingHandler ph : pairingHandlers.values()) { ph.unpair(); } unpairInternal(); // Even if there are no pairing handlers, unpair } /** * This method does not send an unpair package, instead it unpairs internally by deleting trusted device info. . Likely to be called after sending package from * pairing handler */ private void unpairInternal() { //Log.e("Device","Unpairing (unpairInternal)"); pairStatus = PairStatus.NotPaired; SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE); preferences.edit().remove(deviceId).apply(); SharedPreferences devicePreferences = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE); devicePreferences.edit().clear().apply(); for (PairingCallback cb : pairingCallback) cb.unpaired(); reloadPluginsFromSettings(); } /* This method should be called after pairing is done from pairing handler. Calling this method again should not create any problem as most of the things will get over writter*/ private void pairingDone() { //Log.e("Device", "Storing as trusted, deviceId: "+deviceId); hidePairingNotification(); pairStatus = PairStatus.Paired; //Store as trusted device SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE); preferences.edit().putBoolean(deviceId, true).apply(); SharedPreferences.Editor editor = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE).edit(); editor.putString("deviceName", name); editor.putString("deviceType", deviceType.toString()); editor.apply(); reloadPluginsFromSettings(); for (PairingCallback cb : pairingCallback) { cb.pairingSuccessful(); } } /* This method is called after accepting pair request form GUI */ public void acceptPairing() { Log.i("KDE/Device", "Accepted pair request started by the other device"); for (BasePairingHandler ph : pairingHandlers.values()) { ph.acceptPairing(); } } /* This method is called after rejecting pairing from GUI */ public void rejectPairing() { Log.i("KDE/Device", "Rejected pair request started by the other device"); //Log.e("Device","Unpairing (rejectPairing)"); pairStatus = PairStatus.NotPaired; for (BasePairingHandler ph : pairingHandlers.values()) { ph.rejectPairing(); } for (PairingCallback cb : pairingCallback) { cb.pairingFailed(context.getString(R.string.error_canceled_by_user)); } } // // Notification related methods used during pairing // public int getNotificationId() { return notificationId; } public void displayPairingNotification() { hidePairingNotification(); notificationId = (int) System.currentTimeMillis(); Intent intent = new Intent(getContext(), MainActivity.class); intent.putExtra("deviceId", getDeviceId()); intent.putExtra("notificationId", notificationId); PendingIntent pendingIntent = PendingIntent.getActivity(getContext(), 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); Intent acceptIntent = new Intent(getContext(), MainActivity.class); Intent rejectIntent = new Intent(getContext(), MainActivity.class); acceptIntent.putExtra("deviceId", getDeviceId()); acceptIntent.putExtra("notificationId", notificationId); acceptIntent.setAction("action " + System.currentTimeMillis()); acceptIntent.putExtra(MainActivity.PAIR_REQUEST_STATUS, MainActivity.PAIRING_ACCEPTED); rejectIntent.putExtra("deviceId", getDeviceId()); rejectIntent.putExtra("notificationId", notificationId); rejectIntent.setAction("action " + System.currentTimeMillis()); rejectIntent.putExtra(MainActivity.PAIR_REQUEST_STATUS, MainActivity.PAIRING_REJECTED); PendingIntent acceptedPendingIntent = PendingIntent.getActivity(getContext(), 2, acceptIntent, PendingIntent.FLAG_ONE_SHOT); PendingIntent rejectedPendingIntent = PendingIntent.getActivity(getContext(), 4, rejectIntent, PendingIntent.FLAG_ONE_SHOT); Resources res = getContext().getResources(); Notification noti = new NotificationCompat.Builder(getContext()) .setContentTitle(res.getString(R.string.pairing_request_from, getName())) .setContentText(res.getString(R.string.tap_to_answer)) .setContentIntent(pendingIntent) .setTicker(res.getString(R.string.pair_requested)) .setSmallIcon(R.drawable.ic_notification) .addAction(R.drawable.ic_accept_pairing, res.getString(R.string.pairing_accept), acceptedPendingIntent) .addAction(R.drawable.ic_reject_pairing, res.getString(R.string.pairing_reject), rejectedPendingIntent) .setAutoCancel(true) .setDefaults(Notification.DEFAULT_ALL) .build(); final NotificationManager notificationManager = (NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE); NotificationHelper.notifyCompat(notificationManager, notificationId, noti); BackgroundService.addGuiInUseCounter(context); } public void hidePairingNotification() { final NotificationManager notificationManager = (NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.cancel(notificationId); BackgroundService.removeGuiInUseCounter(context); } // // ComputerLink-related functions // public boolean isReachable() { return !links.isEmpty(); } public void addLink(NetworkPacket identityPacket, BaseLink link) { //FilesHelper.LogOpenFileCount(); this.protocolVersion = identityPacket.getInt("protocolVersion"); if (identityPacket.has("deviceName")) { this.name = identityPacket.getString("deviceName", this.name); SharedPreferences.Editor editor = settings.edit(); editor.putString("deviceName", this.name); editor.apply(); } if (identityPacket.has("deviceType")) { this.deviceType = DeviceType.FromString(identityPacket.getString("deviceType", "desktop")); } if (identityPacket.has("certificate")) { String certificateString = identityPacket.getString("certificate"); try { byte[] certificateBytes = Base64.decode(certificateString, 0); certificate = SslHelper.parseCertificate(certificateBytes); Log.i("KDE/Device", "Got certificate "); } catch (Exception e) { e.printStackTrace(); Log.e("KDE/Device", "Error getting certificate"); } } links.add(link); try { SharedPreferences globalSettings = PreferenceManager.getDefaultSharedPreferences(context); byte[] privateKeyBytes = Base64.decode(globalSettings.getString("privateKey", ""), 0); PrivateKey privateKey = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(privateKeyBytes)); link.setPrivateKey(privateKey); } catch (Exception e) { e.printStackTrace(); Log.e("KDE/Device", "Exception reading our own private key"); //Should not happen } Log.i("KDE/Device", "addLink " + link.getLinkProvider().getName() + " -> " + getName() + " active links: " + links.size()); if (!pairingHandlers.containsKey(link.getName())) { BasePairingHandler.PairingHandlerCallback callback = new BasePairingHandler.PairingHandlerCallback() { @Override public void incomingRequest() { for (PairingCallback cb : pairingCallback) { cb.incomingRequest(); } } @Override public void pairingDone() { Device.this.pairingDone(); } @Override public void pairingFailed(String error) { for (PairingCallback cb : pairingCallback) { cb.pairingFailed(error); } } @Override public void unpaired() { unpairInternal(); } }; pairingHandlers.put(link.getName(), link.getPairingHandler(this, callback)); } Set outgoingCapabilities = identityPacket.getStringSet("outgoingCapabilities", null); Set incomingCapabilities = identityPacket.getStringSet("incomingCapabilities", null); if (incomingCapabilities != null && outgoingCapabilities != null) { m_supportedPlugins = new Vector<>(PluginFactory.pluginsForCapabilities(context, incomingCapabilities, outgoingCapabilities)); } else { m_supportedPlugins = new Vector<>(PluginFactory.getAvailablePlugins()); } link.addPacketReceiver(this); reloadPluginsFromSettings(); } public void removeLink(BaseLink link) { //FilesHelper.LogOpenFileCount(); /* Remove pairing handler corresponding to that link too if it was the only link*/ boolean linkPresent = false; for (BaseLink bl : links) { if (bl.getName().equals(link.getName())) { linkPresent = true; break; } } if (!linkPresent) { pairingHandlers.remove(link.getName()); } link.removePacketReceiver(this); links.remove(link); Log.i("KDE/Device", "removeLink: " + link.getLinkProvider().getName() + " -> " + getName() + " active links: " + links.size()); if (links.isEmpty()) { reloadPluginsFromSettings(); } } @Override public void onPacketReceived(NetworkPacket np) { hackToMakeRetrocompatiblePacketTypes(np); if (NetworkPacket.PACKET_TYPE_PAIR.equals(np.getType())) { Log.i("KDE/Device", "Pair package"); for (BasePairingHandler ph : pairingHandlers.values()) { try { ph.packageReceived(np); } catch (Exception e) { e.printStackTrace(); Log.e("PairingPacketReceived", "Exception"); } } } else if (isPaired()) { //If capabilities are not supported, iterate all plugins Collection targetPlugins = pluginsByIncomingInterface.get(np.getType()); if (targetPlugins != null && !targetPlugins.isEmpty()) { for (String pluginKey : targetPlugins) { Plugin plugin = plugins.get(pluginKey); try { plugin.onPacketReceived(np); } catch (Exception e) { e.printStackTrace(); Log.e("KDE/Device", "Exception in " + plugin.getPluginKey() + "'s onPacketReceived()"); //try { Log.e("KDE/Device", "NetworkPacket:" + np.serialize()); } catch (Exception _) { } } } } else { Log.w("Device", "Ignoring packet with type " + np.getType() + " because no plugin can handle it"); } } else { //Log.e("KDE/onPacketReceived","Device not paired, will pass package to unpairedPacketListeners"); // If it is pair package, it should be captured by "if" at start // If not and device is paired, it should be captured by isPaired // Else unpair, this handles the situation when one device unpairs, but other dont know like unpairing when wi-fi is off unpair(); //If capabilities are not supported, iterate all plugins Collection targetPlugins = pluginsByIncomingInterface.get(np.getType()); if (targetPlugins != null && !targetPlugins.isEmpty()) { for (String pluginKey : targetPlugins) { Plugin plugin = plugins.get(pluginKey); try { plugin.onUnpairedDevicePacketReceived(np); } catch (Exception e) { e.printStackTrace(); Log.e("KDE/Device", "Exception in " + plugin.getDisplayName() + "'s onPacketReceived() in unPairedPacketListeners"); } } } else { Log.e("Device", "Ignoring packet with type " + np.getType() + " because no plugin can handle it"); } } } public static abstract class SendPacketStatusCallback { public abstract void onSuccess(); public abstract void onFailure(Throwable e); public void onProgressChanged(int percent) { } } private SendPacketStatusCallback defaultCallback = new SendPacketStatusCallback() { @Override public void onSuccess() { } @Override public void onFailure(Throwable e) { if (e != null) { e.printStackTrace(); } else { Log.e("KDE/sendPacket", "Unknown (null) exception"); } } }; public void sendPacket(NetworkPacket np) { sendPacket(np, defaultCallback); } public boolean sendPacketBlocking(NetworkPacket np) { return sendPacketBlocking(np, defaultCallback); } //Async public void sendPacket(final NetworkPacket np, final SendPacketStatusCallback callback) { - new Thread(new Runnable() { - @Override - public void run() { - sendPacketBlocking(np, callback); - } - }).start(); + new Thread(() -> sendPacketBlocking(np, callback)).start(); } public boolean sendPacketBlocking(final NetworkPacket np, final SendPacketStatusCallback callback) { /* if (!m_outgoingCapabilities.contains(np.getType()) && !NetworkPacket.protocolPacketTypes.contains(np.getType())) { Log.e("Device/sendPacket", "Plugin tried to send an undeclared package: " + np.getType()); Log.w("Device/sendPacket", "Declared outgoing package types: " + Arrays.toString(m_outgoingCapabilities.toArray())); } */ hackToMakeRetrocompatiblePacketTypes(np); boolean useEncryption = (protocolVersion < LanLinkProvider.MIN_VERSION_WITH_SSL_SUPPORT && (!np.getType().equals(NetworkPacket.PACKET_TYPE_PAIR) && isPaired())); boolean success = false; //Make a copy to avoid concurrent modification exception if the original list changes for (final BaseLink link : links) { if (link == null) continue; //Since we made a copy, maybe somebody destroyed the link in the meanwhile if (useEncryption) { success = link.sendPacketEncrypted(np, callback, publicKey); } else { success = link.sendPacket(np, callback); } if (success) break; //If the link didn't call sendSuccess(), try the next one } if (!success) { Log.e("KDE/sendPacket", "No device link (of " + links.size() + " available) could send the package. Packet " + np.getType() + " to " + name + " lost!"); } return success; } // // Plugin-related functions // public T getPlugin(Class pluginClass) { return (T) getPlugin(Plugin.getPluginKey(pluginClass)); } public T getPlugin(Class pluginClass, boolean includeFailed) { return (T) getPlugin(Plugin.getPluginKey(pluginClass), includeFailed); } public Plugin getPlugin(String pluginKey) { return getPlugin(pluginKey, false); } public Plugin getPlugin(String pluginKey, boolean includeFailed) { Plugin plugin = plugins.get(pluginKey); if (includeFailed && plugin == null) { plugin = failedPlugins.get(pluginKey); } return plugin; } private synchronized boolean addPlugin(final String pluginKey) { Plugin existing = plugins.get(pluginKey); if (existing != null) { if (existing.getMinSdk() > Build.VERSION.SDK_INT) { Log.i("KDE/addPlugin", "Min API level not fulfilled " + pluginKey); return false; } //Log.w("KDE/addPlugin","plugin already present:" + pluginKey); if (existing.checkOptionalPermissions()) { Log.i("KDE/addPlugin", "Optional Permissions OK " + pluginKey); pluginsWithoutOptionalPermissions.remove(pluginKey); } else { Log.e("KDE/addPlugin", "No optional permission " + pluginKey); pluginsWithoutOptionalPermissions.put(pluginKey, existing); } return true; } final Plugin plugin = PluginFactory.instantiatePluginForDevice(context, pluginKey, this); if (plugin == null) { Log.e("KDE/addPlugin", "could not instantiate plugin: " + pluginKey); //Can't put a null //failedPlugins.put(pluginKey, null); return false; } if (plugin.getMinSdk() > Build.VERSION.SDK_INT) { Log.i("KDE/addPlugin", "Min API level not fulfilled" + pluginKey); return false; } boolean success; try { success = plugin.onCreate(); } catch (Exception e) { success = false; e.printStackTrace(); Log.e("KDE/addPlugin", "Exception loading plugin " + pluginKey); } if (success) { //Log.e("addPlugin","added " + pluginKey); failedPlugins.remove(pluginKey); plugins.put(pluginKey, plugin); } else { Log.e("KDE/addPlugin", "plugin failed to load " + pluginKey); plugins.remove(pluginKey); failedPlugins.put(pluginKey, plugin); } if (!plugin.checkRequiredPermissions()) { Log.e("KDE/addPlugin", "No permission " + pluginKey); plugins.remove(pluginKey); pluginsWithoutPermissions.put(pluginKey, plugin); success = false; } else { Log.i("KDE/addPlugin", "Permissions OK " + pluginKey); pluginsWithoutPermissions.remove(pluginKey); if (plugin.checkOptionalPermissions()) { Log.i("KDE/addPlugin", "Optional Permissions OK " + pluginKey); pluginsWithoutOptionalPermissions.remove(pluginKey); } else { Log.e("KDE/addPlugin", "No optional permission " + pluginKey); pluginsWithoutOptionalPermissions.put(pluginKey, plugin); } } return success; } private synchronized boolean removePlugin(String pluginKey) { Plugin plugin = plugins.remove(pluginKey); Plugin failedPlugin = failedPlugins.remove(pluginKey); if (plugin == null) { if (failedPlugin == null) { //Not found return false; } plugin = failedPlugin; } try { plugin.onDestroy(); //Log.e("removePlugin","removed " + pluginKey); } catch (Exception e) { e.printStackTrace(); Log.e("KDE/removePlugin", "Exception calling onDestroy for plugin " + pluginKey); } return true; } public void setPluginEnabled(String pluginKey, boolean value) { settings.edit().putBoolean(pluginKey, value).apply(); reloadPluginsFromSettings(); } public boolean isPluginEnabled(String pluginKey) { boolean enabledByDefault = PluginFactory.getPluginInfo(context, pluginKey).isEnabledByDefault(); return settings.getBoolean(pluginKey, enabledByDefault); } public void reloadPluginsFromSettings() { failedPlugins.clear(); HashMap> newPluginsByIncomingInterface = new HashMap<>(); for (String pluginKey : m_supportedPlugins) { PluginFactory.PluginInfo pluginInfo = PluginFactory.getPluginInfo(context, pluginKey); boolean pluginEnabled = false; boolean listenToUnpaired = pluginInfo.listenToUnpaired(); if ((isPaired() || listenToUnpaired) && isReachable()) { pluginEnabled = isPluginEnabled(pluginKey); } if (pluginEnabled) { boolean success = addPlugin(pluginKey); if (success) { for (String packageType : pluginInfo.getSupportedPacketTypes()) { packageType = hackToMakeRetrocompatiblePacketTypes(packageType); ArrayList plugins = newPluginsByIncomingInterface.get(packageType); if (plugins == null) plugins = new ArrayList<>(); plugins.add(pluginKey); newPluginsByIncomingInterface.put(packageType, plugins); } } } else { removePlugin(pluginKey); } } pluginsByIncomingInterface = newPluginsByIncomingInterface; onPluginsChanged(); } public void onPluginsChanged() { for (PluginsChangedListener listener : pluginsChangedListeners) { listener.onPluginsChanged(Device.this); } } public ConcurrentHashMap getLoadedPlugins() { return plugins; } public ConcurrentHashMap getFailedPlugins() { return failedPlugins; } public ConcurrentHashMap getPluginsWithoutPermissions() { return pluginsWithoutPermissions; } public ConcurrentHashMap getPluginsWithoutOptionalPermissions() { return pluginsWithoutOptionalPermissions; } public void addPluginsChangedListener(PluginsChangedListener listener) { pluginsChangedListeners.add(listener); } public void removePluginsChangedListener(PluginsChangedListener listener) { pluginsChangedListeners.remove(listener); } public void disconnect() { for (BaseLink link : links) { link.disconnect(); } } public boolean deviceShouldBeKeptAlive() { SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE); if (preferences.contains(getDeviceId())) { //Log.e("DeviceShouldBeKeptAlive", "because it's a paired device"); return true; //Already paired } for (BaseLink l : links) { if (l.linkShouldBeKeptAlive()) { return true; } } return false; } public List getSupportedPlugins() { return m_supportedPlugins; } public void hackToMakeRetrocompatiblePacketTypes(NetworkPacket np) { if (protocolVersion >= 6) return; np.mType = np.getType().replace(".request", ""); } public String hackToMakeRetrocompatiblePacketTypes(String type) { if (protocolVersion >= 6) return type; return type.replace(".request", ""); } } diff --git a/src/org/kde/kdeconnect/KdeConnectBroadcastReceiver.java b/src/org/kde/kdeconnect/KdeConnectBroadcastReceiver.java index fec3b5e5..af3583b7 100644 --- a/src/org/kde/kdeconnect/KdeConnectBroadcastReceiver.java +++ b/src/org/kde/kdeconnect/KdeConnectBroadcastReceiver.java @@ -1,90 +1,76 @@ /* * 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.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.net.ConnectivityManager; import android.net.wifi.WifiManager; import android.util.Log; public class KdeConnectBroadcastReceiver extends BroadcastReceiver { public void onReceive(Context context, Intent intent) { //Log.e("KdeConnect", "Broadcast event: "+intent.getAction()); String action = intent.getAction(); switch (action) { case Intent.ACTION_PACKAGE_REPLACED: Log.i("KdeConnect", "UpdateReceiver"); if (!intent.getData().getSchemeSpecificPart().equals(context.getPackageName())) { Log.i("KdeConnect", "Ignoring, it's not me!"); return; } - BackgroundService.RunCommand(context, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { + BackgroundService.RunCommand(context, service -> { - } }); break; case Intent.ACTION_BOOT_COMPLETED: Log.i("KdeConnect", "KdeConnectBroadcastReceiver"); - BackgroundService.RunCommand(context, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { + BackgroundService.RunCommand(context, service -> { - } }); break; case WifiManager.SUPPLICANT_CONNECTION_CHANGE_ACTION: case WifiManager.WIFI_STATE_CHANGED_ACTION: case ConnectivityManager.CONNECTIVITY_ACTION: Log.i("KdeConnect", "Connection state changed, trying to connect"); - BackgroundService.RunCommand(context, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - service.onDeviceListChanged(); - service.onNetworkChange(); - } + BackgroundService.RunCommand(context, service -> { + service.onDeviceListChanged(); + service.onNetworkChange(); }); break; case Intent.ACTION_SCREEN_ON: - BackgroundService.RunCommand(context, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - service.onNetworkChange(); - } - }); + BackgroundService.RunCommand(context, BackgroundService::onNetworkChange); break; default: Log.i("BroadcastReceiver", "Ignoring broadcast event: " + intent.getAction()); break; } } } diff --git a/src/org/kde/kdeconnect/Plugins/ClibpoardPlugin/ClipboardListener.java b/src/org/kde/kdeconnect/Plugins/ClibpoardPlugin/ClipboardListener.java index 619daa84..c1fd7419 100644 --- a/src/org/kde/kdeconnect/Plugins/ClibpoardPlugin/ClipboardListener.java +++ b/src/org/kde/kdeconnect/Plugins/ClibpoardPlugin/ClipboardListener.java @@ -1,115 +1,109 @@ /* * 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.ClibpoardPlugin; import android.annotation.TargetApi; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.os.Build; import android.os.Handler; import android.os.Looper; import java.util.HashSet; @TargetApi(Build.VERSION_CODES.HONEYCOMB) public class ClipboardListener { public interface ClipboardObserver { void clipboardChanged(String content); } private HashSet observers = new HashSet<>(); private final Context context; private String currentContent; private ClipboardManager cm = null; private ClipboardManager.OnPrimaryClipChangedListener listener; private static ClipboardListener _instance = null; public static ClipboardListener instance(Context context) { if (_instance == null) { _instance = new ClipboardListener(context); } return _instance; } public void registerObserver(ClipboardObserver observer) { observers.add(observer); } public void removeObserver(ClipboardObserver observer) { observers.remove(observer); } ClipboardListener(final Context ctx) { context = ctx; if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { return; } - new Handler(Looper.getMainLooper()).post(new Runnable() { - @Override - public void run() { - cm = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); - listener = new ClipboardManager.OnPrimaryClipChangedListener() { - @Override - public void onPrimaryClipChanged() { - try { - - ClipData.Item item = cm.getPrimaryClip().getItemAt(0); - String content = item.coerceToText(context).toString(); - - if (content.equals(currentContent)) { - return; - } - - currentContent = content; - - for (ClipboardObserver observer : observers) { - observer.clipboardChanged(content); - } - - } catch (Exception e) { - //Probably clipboard was not text - } + new Handler(Looper.getMainLooper()).post(() -> { + cm = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + listener = () -> { + try { + + ClipData.Item item = cm.getPrimaryClip().getItemAt(0); + String content = item.coerceToText(context).toString(); + + if (content.equals(currentContent)) { + return; + } + + currentContent = content; + + for (ClipboardObserver observer : observers) { + observer.clipboardChanged(content); } - }; - cm.addPrimaryClipChangedListener(listener); - } + + } catch (Exception e) { + //Probably clipboard was not text + } + }; + cm.addPrimaryClipChangedListener(listener); }); } @SuppressWarnings("deprecation") public void setText(String text) { currentContent = text; if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { android.text.ClipboardManager clipboard = (android.text.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); clipboard.setText(text); } else { cm.setText(text); } } } diff --git a/src/org/kde/kdeconnect/Plugins/ClibpoardPlugin/ClipboardPlugin.java b/src/org/kde/kdeconnect/Plugins/ClibpoardPlugin/ClipboardPlugin.java index c98839dc..d68649a2 100644 --- a/src/org/kde/kdeconnect/Plugins/ClibpoardPlugin/ClipboardPlugin.java +++ b/src/org/kde/kdeconnect/Plugins/ClibpoardPlugin/ClipboardPlugin.java @@ -1,87 +1,84 @@ /* * 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.ClibpoardPlugin; import android.os.Build; import org.kde.kdeconnect.NetworkPacket; import org.kde.kdeconnect.Plugins.Plugin; import org.kde.kdeconnect_tp.R; public class ClipboardPlugin extends Plugin { public final static String PACKET_TYPE_CLIPBOARD = "kdeconnect.clipboard"; @Override public String getDisplayName() { return context.getResources().getString(R.string.pref_plugin_clipboard); } @Override public String getDescription() { return context.getResources().getString(R.string.pref_plugin_clipboard_desc); } @Override public boolean isEnabledByDefault() { //Disabled by default due to just one direction sync(incoming clipboard change) in early version of android. return (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB); } @Override public boolean onPacketReceived(NetworkPacket np) { String content = np.getString("content"); ClipboardListener.instance(context).setText(content); return true; } - private ClipboardListener.ClipboardObserver observer = new ClipboardListener.ClipboardObserver() { - @Override - public void clipboardChanged(String content) { - NetworkPacket np = new NetworkPacket(ClipboardPlugin.PACKET_TYPE_CLIPBOARD); - np.set("content", content); - device.sendPacket(np); - } + private ClipboardListener.ClipboardObserver observer = content -> { + NetworkPacket np = new NetworkPacket(ClipboardPlugin.PACKET_TYPE_CLIPBOARD); + np.set("content", content); + device.sendPacket(np); }; @Override public boolean onCreate() { ClipboardListener.instance(context).registerObserver(observer); return true; } @Override public void onDestroy() { ClipboardListener.instance(context).removeObserver(observer); } @Override public String[] getSupportedPacketTypes() { return new String[]{PACKET_TYPE_CLIPBOARD}; } @Override public String[] getOutgoingPacketTypes() { return new String[]{PACKET_TYPE_CLIPBOARD}; } } diff --git a/src/org/kde/kdeconnect/Plugins/FindMyPhonePlugin/FindMyPhoneActivity.java b/src/org/kde/kdeconnect/Plugins/FindMyPhonePlugin/FindMyPhoneActivity.java index 0886dfc1..e1f3cd5a 100644 --- a/src/org/kde/kdeconnect/Plugins/FindMyPhonePlugin/FindMyPhoneActivity.java +++ b/src/org/kde/kdeconnect/Plugins/FindMyPhonePlugin/FindMyPhoneActivity.java @@ -1,119 +1,114 @@ /* Copyright 2018 Nicolas Fella * Copyright 2015 David Edmundson * * 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.FindMyPhonePlugin; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.media.AudioManager; import android.media.MediaPlayer; import android.net.Uri; import android.os.Bundle; import android.preference.PreferenceManager; import android.provider.Settings; import android.util.Log; import android.view.View; import android.view.Window; import android.view.WindowManager; import org.kde.kdeconnect.UserInterface.ThemeUtil; import org.kde.kdeconnect_tp.R; public class FindMyPhoneActivity extends Activity { private MediaPlayer mediaPlayer; private int previousVolume; private AudioManager audioManager; @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); if (mediaPlayer != null) { // If this activity was already open and we received the ring packet again, just finish it finish(); } // otherwise the activity will become active again } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ThemeUtil.setUserPreferredTheme(this); setContentView(R.layout.activity_find_my_phone); audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); Window window = this.getWindow(); window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); - findViewById(R.id.bFindMyPhone).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - finish(); - } - }); + findViewById(R.id.bFindMyPhone).setOnClickListener(view -> finish()); } @Override protected void onStart() { super.onStart(); try { // Make sure we are heard even when the phone is silent, restore original volume later previousVolume = audioManager.getStreamVolume(AudioManager.STREAM_ALARM); audioManager.setStreamVolume(AudioManager.STREAM_ALARM, audioManager.getStreamMaxVolume(AudioManager.STREAM_ALARM), 0); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); Uri ringtone; String ringtoneString = prefs.getString("select_ringtone", ""); if (ringtoneString.isEmpty()) { ringtone = Settings.System.DEFAULT_RINGTONE_URI; } else { ringtone = Uri.parse(ringtoneString); } mediaPlayer = new MediaPlayer(); mediaPlayer.setDataSource(this, ringtone); mediaPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); mediaPlayer.setLooping(true); mediaPlayer.prepare(); mediaPlayer.start(); } catch (Exception e) { Log.e("FindMyPhoneActivity", "Exception", e); } } @Override protected void onStop() { super.onStop(); if (mediaPlayer != null) { mediaPlayer.stop(); } audioManager.setStreamVolume(AudioManager.STREAM_ALARM, previousVolume, 0); } } diff --git a/src/org/kde/kdeconnect/Plugins/MousePadPlugin/KeyListenerView.java b/src/org/kde/kdeconnect/Plugins/MousePadPlugin/KeyListenerView.java index d5a409f1..371353d4 100644 --- a/src/org/kde/kdeconnect/Plugins/MousePadPlugin/KeyListenerView.java +++ b/src/org/kde/kdeconnect/Plugins/MousePadPlugin/KeyListenerView.java @@ -1,178 +1,175 @@ /* * Copyright 2014 Saikrishna Arcot * * 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.os.Build; import android.util.AttributeSet; import android.util.SparseIntArray; import android.view.KeyEvent; import android.view.View; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.NetworkPacket; public class KeyListenerView extends View { private String deviceId; private static SparseIntArray SpecialKeysMap = new SparseIntArray(); static { int i = 0; SpecialKeysMap.put(KeyEvent.KEYCODE_DEL, ++i); // 1 SpecialKeysMap.put(KeyEvent.KEYCODE_TAB, ++i); // 2 SpecialKeysMap.put(KeyEvent.KEYCODE_ENTER, 12); ++i; // 3 is not used, return is 12 instead SpecialKeysMap.put(KeyEvent.KEYCODE_DPAD_LEFT, ++i); // 4 SpecialKeysMap.put(KeyEvent.KEYCODE_DPAD_UP, ++i); // 5 SpecialKeysMap.put(KeyEvent.KEYCODE_DPAD_RIGHT, ++i); // 6 SpecialKeysMap.put(KeyEvent.KEYCODE_DPAD_DOWN, ++i); // 7 SpecialKeysMap.put(KeyEvent.KEYCODE_PAGE_UP, ++i); // 8 SpecialKeysMap.put(KeyEvent.KEYCODE_PAGE_DOWN, ++i); // 9 if (Build.VERSION.SDK_INT >= 11) { SpecialKeysMap.put(KeyEvent.KEYCODE_MOVE_HOME, ++i); // 10 SpecialKeysMap.put(KeyEvent.KEYCODE_MOVE_END, ++i); // 11 SpecialKeysMap.put(KeyEvent.KEYCODE_NUMPAD_ENTER, ++i); // 12 SpecialKeysMap.put(KeyEvent.KEYCODE_FORWARD_DEL, ++i); // 13 SpecialKeysMap.put(KeyEvent.KEYCODE_ESCAPE, ++i); // 14 SpecialKeysMap.put(KeyEvent.KEYCODE_SYSRQ, ++i); // 15 SpecialKeysMap.put(KeyEvent.KEYCODE_SCROLL_LOCK, ++i); // 16 ++i; // 17 ++i; // 18 ++i; // 19 ++i; // 20 SpecialKeysMap.put(KeyEvent.KEYCODE_F1, ++i); // 21 SpecialKeysMap.put(KeyEvent.KEYCODE_F2, ++i); // 22 SpecialKeysMap.put(KeyEvent.KEYCODE_F3, ++i); // 23 SpecialKeysMap.put(KeyEvent.KEYCODE_F4, ++i); // 24 SpecialKeysMap.put(KeyEvent.KEYCODE_F5, ++i); // 25 SpecialKeysMap.put(KeyEvent.KEYCODE_F6, ++i); // 26 SpecialKeysMap.put(KeyEvent.KEYCODE_F7, ++i); // 27 SpecialKeysMap.put(KeyEvent.KEYCODE_F8, ++i); // 28 SpecialKeysMap.put(KeyEvent.KEYCODE_F9, ++i); // 29 SpecialKeysMap.put(KeyEvent.KEYCODE_F10, ++i); // 30 SpecialKeysMap.put(KeyEvent.KEYCODE_F11, ++i); // 31 SpecialKeysMap.put(KeyEvent.KEYCODE_F12, ++i); // 21 } } public void setDeviceId(String id) { deviceId = id; } public KeyListenerView(Context context, AttributeSet set) { super(context, set); setFocusable(true); setFocusableInTouchMode(true); } @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { if (android.os.Build.VERSION.SDK_INT >= 11) { outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN; } return new KeyInputConnection(this, true); } @Override public boolean onCheckIsTextEditor() { return true; } public void sendChars(CharSequence chars) { final NetworkPacket np = new NetworkPacket(MousePadPlugin.PACKET_TYPE_MOUSEPAD_REQUEST); np.set("key", chars.toString()); sendKeyPressPacket(np); } private void sendKeyPressPacket(final NetworkPacket np) { - BackgroundService.RunCommand(getContext(), new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - Device device = service.getDevice(deviceId); - MousePadPlugin mousePadPlugin = device.getPlugin(MousePadPlugin.class); - if (mousePadPlugin == null) return; - mousePadPlugin.sendKeyboardPacket(np); - } + BackgroundService.RunCommand(getContext(), service -> { + Device device = service.getDevice(deviceId); + MousePadPlugin mousePadPlugin = device.getPlugin(MousePadPlugin.class); + if (mousePadPlugin == null) return; + mousePadPlugin.sendKeyboardPacket(np); }); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK) { //We don't want to swallow the back button press return false; } // NOTE: Most keyboards, and specifically the Android default keyboard when // entering non-ascii characters, will not trigger KeyEvent events as documented // here: http://developer.android.com/reference/android/view/KeyEvent.html //Log.e("KeyDown", "------------"); //Log.e("KeyDown", "keyChar:" + (int) event.getDisplayLabel()); //Log.e("KeyDown", "utfChar:" + (char)event.getUnicodeChar()); //Log.e("KeyDown", "intUtfChar:" + event.getUnicodeChar()); final NetworkPacket np = new NetworkPacket(MousePadPlugin.PACKET_TYPE_MOUSEPAD_REQUEST); boolean modifier = false; if (event.isAltPressed()) { np.set("alt", true); modifier = true; } if (Build.VERSION.SDK_INT >= 11) { if (event.isCtrlPressed()) { np.set("ctrl", true); modifier = true; } } if (event.isShiftPressed()) { np.set("shift", true); } int specialKey = SpecialKeysMap.get(keyCode, -1); if (specialKey != -1) { np.set("specialKey", specialKey); } else if (event.getDisplayLabel() != 0 && modifier) { //Alt will change the utf symbol to non-ascii characters, we want the plain original letter //Since getDisplayLabel will always have a value, we have to check for special keys before char keyCharacter = event.getDisplayLabel(); np.set("key", new String(new char[]{keyCharacter}).toLowerCase()); } else { //A normal key, but still not handled by the KeyInputConnection (happens with numbers) np.set("key", new String(new char[]{(char) event.getUnicodeChar()})); } sendKeyPressPacket(np); return true; } } diff --git a/src/org/kde/kdeconnect/Plugins/MousePadPlugin/MousePadActivity.java b/src/org/kde/kdeconnect/Plugins/MousePadPlugin/MousePadActivity.java index 7078a5f3..5de6dc7c 100644 --- a/src/org/kde/kdeconnect/Plugins/MousePadPlugin/MousePadActivity.java +++ b/src/org/kde/kdeconnect/Plugins/MousePadPlugin/MousePadActivity.java @@ -1,434 +1,407 @@ /* * 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.support.v7.app.AppCompatActivity; 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.Device; import org.kde.kdeconnect.UserInterface.ThemeUtil; import org.kde.kdeconnect_tp.R; public class MousePadActivity extends AppCompatActivity implements GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener, MousePadGestureDetector.OnGestureListener { 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 float mPrevX; private float mPrevY; private float mCurrentX; private float mCurrentY; private float mCurrentSensitivity; private int scrollDirection = 1; boolean isScrolling = false; float accumulatedDistanceY = 0; private GestureDetector mDetector; private MousePadGestureDetector mMousePadGestureDetector; 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, this); mDetector.setOnDoubleTapListener(this); keyListenerView = (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)); doubleTapAction = ClickType.fromString(doubleTapSetting); tripleTapAction = ClickType.fromString(tripleTapSetting); 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; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { final View decorView = getWindow().getDecorView(); - decorView.setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener() { - @Override - public void onSystemUiVisibilityChange(int visibility) { - if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) { + decorView.setOnSystemUiVisibilityChangeListener(visibility -> { + if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) { - int fullscreenType = 0; + int fullscreenType = 0; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { - 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.ICE_CREAM_SANDWICH) { + fullscreenType |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; + } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - fullscreenType |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; - } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + fullscreenType |= View.SYSTEM_UI_FLAG_FULLSCREEN; + } - getWindow().getDecorView().setSystemUiVisibility(fullscreenType); + 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.RunCommand(this, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - Device device = service.getDevice(deviceId); - MousePadPlugin mousePadPlugin = device.getPlugin(MousePadPlugin.class); - if (mousePadPlugin == null) return; - mousePadPlugin.sendMouseDelta(mCurrentX - mPrevX, mCurrentY - mPrevY, mCurrentSensitivity); - mPrevX = mCurrentX; - mPrevY = mCurrentY; - } + BackgroundService.RunCommand(this, service -> { + Device device = service.getDevice(deviceId); + MousePadPlugin mousePadPlugin = device.getPlugin(MousePadPlugin.class); + if (mousePadPlugin == null) return; + mousePadPlugin.sendMouseDelta(mCurrentX - mPrevX, mCurrentY - mPrevY, mCurrentSensitivity); + 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 (android.os.Build.VERSION.SDK_INT >= 12) { // MotionEvent.getAxisValue is >= 12 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.RunCommand(this, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - Device device = service.getDevice(deviceId); - MousePadPlugin mousePadPlugin = device.getPlugin(MousePadPlugin.class); - if (mousePadPlugin == null) return; - mousePadPlugin.sendSingleHold(); - } + BackgroundService.RunCommand(this, service -> { + Device device = service.getDevice(deviceId); + MousePadPlugin mousePadPlugin = device.getPlugin(MousePadPlugin.class); + if (mousePadPlugin == null) return; + mousePadPlugin.sendSingleHold(); }); } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { return false; } @Override public boolean onSingleTapConfirmed(MotionEvent e) { - BackgroundService.RunCommand(this, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - Device device = service.getDevice(deviceId); - MousePadPlugin mousePadPlugin = device.getPlugin(MousePadPlugin.class); - if (mousePadPlugin == null) return; - mousePadPlugin.sendSingleClick(); - } + BackgroundService.RunCommand(this, service -> { + Device device = service.getDevice(deviceId); + MousePadPlugin mousePadPlugin = device.getPlugin(MousePadPlugin.class); + if (mousePadPlugin == null) return; + mousePadPlugin.sendSingleClick(); }); return true; } @Override public boolean onDoubleTap(MotionEvent e) { - BackgroundService.RunCommand(this, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - Device device = service.getDevice(deviceId); - MousePadPlugin mousePadPlugin = device.getPlugin(MousePadPlugin.class); - if (mousePadPlugin == null) return; - mousePadPlugin.sendDoubleClick(); - } + BackgroundService.RunCommand(this, service -> { + Device device = service.getDevice(deviceId); + MousePadPlugin mousePadPlugin = device.getPlugin(MousePadPlugin.class); + if (mousePadPlugin == null) return; + 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.RunCommand(this, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - Device device = service.getDevice(deviceId); - MousePadPlugin mousePadPlugin = device.getPlugin(MousePadPlugin.class); - if (mousePadPlugin == null) return; - mousePadPlugin.sendMiddleClick(); - } + BackgroundService.RunCommand(this, service -> { + Device device = service.getDevice(deviceId); + MousePadPlugin mousePadPlugin = device.getPlugin(MousePadPlugin.class); + if (mousePadPlugin == null) return; + mousePadPlugin.sendMiddleClick(); }); } private void sendRightClick() { - BackgroundService.RunCommand(this, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - Device device = service.getDevice(deviceId); - MousePadPlugin mousePadPlugin = device.getPlugin(MousePadPlugin.class); - if (mousePadPlugin == null) return; - mousePadPlugin.sendRightClick(); - } + BackgroundService.RunCommand(this, service -> { + Device device = service.getDevice(deviceId); + MousePadPlugin mousePadPlugin = device.getPlugin(MousePadPlugin.class); + if (mousePadPlugin == null) return; + mousePadPlugin.sendRightClick(); }); } private void sendSingleHold() { - BackgroundService.RunCommand(this, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - Device device = service.getDevice(deviceId); - MousePadPlugin mousePadPlugin = device.getPlugin(MousePadPlugin.class); - if (mousePadPlugin == null) return; - mousePadPlugin.sendSingleHold(); - } + BackgroundService.RunCommand(this, service -> { + Device device = service.getDevice(deviceId); + MousePadPlugin mousePadPlugin = device.getPlugin(MousePadPlugin.class); + if (mousePadPlugin == null) return; + mousePadPlugin.sendSingleHold(); }); } private void sendScroll(final float y) { - BackgroundService.RunCommand(this, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - Device device = service.getDevice(deviceId); - MousePadPlugin mousePadPlugin = device.getPlugin(MousePadPlugin.class); - if (mousePadPlugin == null) return; - mousePadPlugin.sendScroll(0, y); - } + BackgroundService.RunCommand(this, service -> { + Device device = service.getDevice(deviceId); + MousePadPlugin mousePadPlugin = device.getPlugin(MousePadPlugin.class); + if (mousePadPlugin == null) return; + mousePadPlugin.sendScroll(0, y); }); } private void showKeyboard() { InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); 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/MprisActivity.java b/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisActivity.java index d12e9ff1..2e939e73 100644 --- a/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisActivity.java +++ b/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisActivity.java @@ -1,485 +1,411 @@ /* * 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.MprisPlugin; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.v4.graphics.drawable.DrawableCompat; import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.view.KeyEvent; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.SeekBar; import android.widget.Spinner; import android.widget.TextView; import org.kde.kdeconnect.Backends.BaseLink; import org.kde.kdeconnect.Backends.BaseLinkProvider; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.NetworkPacket; import org.kde.kdeconnect.UserInterface.ThemeUtil; import org.kde.kdeconnect_tp.R; import java.util.List; public class MprisActivity extends AppCompatActivity { private String deviceId; private final Handler positionSeekUpdateHandler = new Handler(); private Runnable positionSeekUpdateRunnable = null; private MprisPlugin.MprisPlayer targetPlayer = null; private static String milisToProgress(long milis) { int length = (int) (milis / 1000); //From milis to seconds StringBuilder text = new StringBuilder(); int minutes = length / 60; if (minutes > 60) { int hours = minutes / 60; minutes = minutes % 60; text.append(hours).append(':'); if (minutes < 10) text.append('0'); } text.append(minutes).append(':'); int seconds = (length % 60); if (seconds < 10) text.append('0'); // needed to show length properly (eg 4:05 instead of 4:5) text.append(seconds); return text.toString(); } protected void connectToPlugin(final String targetPlayerName) { - BackgroundService.RunCommand(this, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { + BackgroundService.RunCommand(this, service -> { + + final Device device = service.getDevice(deviceId); + final MprisPlugin mpris = device.getPlugin(MprisPlugin.class); + if (mpris == null) { + Log.e("MprisActivity", "device has no mpris plugin!"); + return; + } + targetPlayer = mpris.getPlayerStatus(targetPlayerName); - final Device device = service.getDevice(deviceId); - final MprisPlugin mpris = device.getPlugin(MprisPlugin.class); - if (mpris == null) { - Log.e("MprisActivity", "device has no mpris plugin!"); - return; + mpris.setPlayerStatusUpdatedHandler("activity", new Handler() { + @Override + public void handleMessage(Message msg) { + runOnUiThread(() -> updatePlayerStatus(mpris)); } - targetPlayer = mpris.getPlayerStatus(targetPlayerName); + }); + + mpris.setPlayerListUpdatedHandler("activity", new Handler() { + @Override + public void handleMessage(Message msg) { + final List playerList = mpris.getPlayerList(); + final ArrayAdapter adapter = new ArrayAdapter<>(MprisActivity.this, + android.R.layout.simple_spinner_item, + playerList.toArray(new String[playerList.size()]) + ); + + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + runOnUiThread(() -> { + Spinner spinner = (Spinner) findViewById(R.id.player_spinner); + //String prevPlayer = (String)spinner.getSelectedItem(); + spinner.setAdapter(adapter); + + if (playerList.isEmpty()) { + findViewById(R.id.no_players).setVisibility(View.VISIBLE); + spinner.setVisibility(View.GONE); + ((TextView) findViewById(R.id.now_playing_textview)).setText(""); + } else { + findViewById(R.id.no_players).setVisibility(View.GONE); + spinner.setVisibility(View.VISIBLE); + } - mpris.setPlayerStatusUpdatedHandler("activity", new Handler() { - @Override - public void handleMessage(Message msg) { - runOnUiThread(new Runnable() { + spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override - public void run() { - updatePlayerStatus(mpris); - } - }); - } - }); + public void onItemSelected(AdapterView arg0, View arg1, int pos, long id) { - mpris.setPlayerListUpdatedHandler("activity", new Handler() { - @Override - public void handleMessage(Message msg) { - final List playerList = mpris.getPlayerList(); - final ArrayAdapter adapter = new ArrayAdapter<>(MprisActivity.this, - android.R.layout.simple_spinner_item, - playerList.toArray(new String[playerList.size()]) - ); - - adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - runOnUiThread(new Runnable() { - @Override - public void run() { - Spinner spinner = (Spinner) findViewById(R.id.player_spinner); - //String prevPlayer = (String)spinner.getSelectedItem(); - spinner.setAdapter(adapter); - - if (playerList.isEmpty()) { - findViewById(R.id.no_players).setVisibility(View.VISIBLE); - spinner.setVisibility(View.GONE); - ((TextView) findViewById(R.id.now_playing_textview)).setText(""); - } else { - findViewById(R.id.no_players).setVisibility(View.GONE); - spinner.setVisibility(View.VISIBLE); - } + if (pos >= playerList.size()) return; - spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView arg0, View arg1, int pos, long id) { - - if (pos >= playerList.size()) return; - - String player = playerList.get(pos); - if (targetPlayer != null && player.equals(targetPlayer.getPlayer())) { - return; //Player hasn't actually changed - } - targetPlayer = mpris.getPlayerStatus(player); - updatePlayerStatus(mpris); - - if (targetPlayer.isPlaying()) { - MprisMediaSession.getInstance().playerSelected(targetPlayer); - } - } - - @Override - public void onNothingSelected(AdapterView arg0) { - targetPlayer = null; - } - }); - - if (targetPlayer == null) { - //If no player is selected, try to select a playing player - targetPlayer = mpris.getPlayingPlayer(); - } - //Try to select the specified player - if (targetPlayer != null) { - int targetIndex = adapter.getPosition(targetPlayer.getPlayer()); - if (targetIndex >= 0) { - spinner.setSelection(targetIndex); - } else { - targetPlayer = null; - } - } - //If no player selected, select the first one (if any) - if (targetPlayer == null && !playerList.isEmpty()) { - targetPlayer = mpris.getPlayerStatus(playerList.get(0)); - spinner.setSelection(0); + String player = playerList.get(pos); + if (targetPlayer != null && player.equals(targetPlayer.getPlayer())) { + return; //Player hasn't actually changed } + targetPlayer = mpris.getPlayerStatus(player); updatePlayerStatus(mpris); + + if (targetPlayer.isPlaying()) { + MprisMediaSession.getInstance().playerSelected(targetPlayer); + } + } + + @Override + public void onNothingSelected(AdapterView arg0) { + targetPlayer = null; } }); - } - }); - } + if (targetPlayer == null) { + //If no player is selected, try to select a playing player + targetPlayer = mpris.getPlayingPlayer(); + } + //Try to select the specified player + if (targetPlayer != null) { + int targetIndex = adapter.getPosition(targetPlayer.getPlayer()); + if (targetIndex >= 0) { + spinner.setSelection(targetIndex); + } else { + targetPlayer = null; + } + } + //If no player selected, select the first one (if any) + if (targetPlayer == null && !playerList.isEmpty()) { + targetPlayer = mpris.getPlayerStatus(playerList.get(0)); + spinner.setSelection(0); + } + updatePlayerStatus(mpris); + }); + } + }); + }); } private final BaseLinkProvider.ConnectionReceiver connectionReceiver = new BaseLinkProvider.ConnectionReceiver() { @Override public void onConnectionReceived(NetworkPacket identityPacket, BaseLink link) { connectToPlugin(null); } @Override public void onConnectionLost(BaseLink link) { } }; @Override protected void onDestroy() { super.onDestroy(); - BackgroundService.RunCommand(MprisActivity.this, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - service.removeConnectionListener(connectionReceiver); - } - }); + BackgroundService.RunCommand(MprisActivity.this, service -> service.removeConnectionListener(connectionReceiver)); } private void updatePlayerStatus(MprisPlugin mpris) { MprisPlugin.MprisPlayer playerStatus = targetPlayer; if (playerStatus == null) { //No player with that name found, just display "empty" data playerStatus = mpris.getEmptyPlayer(); } String song = playerStatus.getCurrentSong(); TextView nowPlaying = (TextView) findViewById(R.id.now_playing_textview); if (!nowPlaying.getText().toString().equals(song)) { nowPlaying.setText(song); } Bitmap albumArt = playerStatus.getAlbumArt(); if (albumArt == null) { Drawable placeholder_art = DrawableCompat.wrap(getResources().getDrawable(R.drawable.ic_album_art_placeholder)); DrawableCompat.setTint(placeholder_art, getResources().getColor(R.color.primary)); ((ImageView) findViewById(R.id.album_art)).setImageDrawable(placeholder_art); } else { ((ImageView) findViewById(R.id.album_art)).setImageBitmap(albumArt); } if (playerStatus.isSeekAllowed()) { ((TextView) findViewById(R.id.time_textview)).setText(milisToProgress(playerStatus.getLength())); SeekBar positionSeek = (SeekBar) findViewById(R.id.positionSeek); positionSeek.setMax((int) (playerStatus.getLength())); positionSeek.setProgress((int) (playerStatus.getPosition())); findViewById(R.id.progress_slider).setVisibility(View.VISIBLE); } else { findViewById(R.id.progress_slider).setVisibility(View.GONE); } int volume = playerStatus.getVolume(); ((SeekBar) findViewById(R.id.volume_seek)).setProgress(volume); boolean isPlaying = playerStatus.isPlaying(); if (isPlaying) { ((ImageButton) findViewById(R.id.play_button)).setImageResource(R.drawable.ic_pause_black); findViewById(R.id.play_button).setEnabled(playerStatus.isPauseAllowed()); } else { ((ImageButton) findViewById(R.id.play_button)).setImageResource(R.drawable.ic_play_black); findViewById(R.id.play_button).setEnabled(playerStatus.isPlayAllowed()); } findViewById(R.id.volume_layout).setVisibility(playerStatus.isSetVolumeAllowed() ? View.VISIBLE : View.INVISIBLE); findViewById(R.id.rew_button).setVisibility(playerStatus.isSeekAllowed() ? View.VISIBLE : View.GONE); findViewById(R.id.ff_button).setVisibility(playerStatus.isSeekAllowed() ? View.VISIBLE : View.GONE); //Show and hide previous/next buttons simultaneously if (playerStatus.isGoPreviousAllowed() || playerStatus.isGoNextAllowed()) { findViewById(R.id.prev_button).setVisibility(View.VISIBLE); findViewById(R.id.prev_button).setEnabled(playerStatus.isGoPreviousAllowed()); findViewById(R.id.next_button).setVisibility(View.VISIBLE); findViewById(R.id.next_button).setEnabled(playerStatus.isGoNextAllowed()); } else { findViewById(R.id.prev_button).setVisibility(View.GONE); findViewById(R.id.next_button).setVisibility(View.GONE); } } /** * Change current volume with provided step. * * @param step step size volume change */ private void updateVolume(int step) { if (targetPlayer == null) { return; } final int currentVolume = targetPlayer.getVolume(); if (currentVolume < 100 || currentVolume > 0) { int newVolume = currentVolume + step; if (newVolume > 100) { newVolume = 100; } else if (newVolume < 0) { newVolume = 0; } targetPlayer.setVolume(newVolume); } } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_VOLUME_UP: updateVolume(5); return true; case KeyEvent.KEYCODE_VOLUME_DOWN: updateVolume(-5); return true; default: return super.onKeyDown(keyCode, event); } } @Override public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_VOLUME_UP: return true; case KeyEvent.KEYCODE_VOLUME_DOWN: return true; default: return super.onKeyUp(keyCode, event); } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ThemeUtil.setUserPreferredTheme(this); setContentView(R.layout.activity_mpris); final String targetPlayerName = getIntent().getStringExtra("player"); getIntent().removeExtra("player"); deviceId = getIntent().getStringExtra("deviceId"); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); String interval_time_str = prefs.getString(getString(R.string.mpris_time_key), getString(R.string.mpris_time_default)); final int interval_time = Integer.parseInt(interval_time_str); - BackgroundService.RunCommand(MprisActivity.this, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - service.addConnectionListener(connectionReceiver); - } - }); + BackgroundService.RunCommand(MprisActivity.this, service -> service.addConnectionListener(connectionReceiver)); connectToPlugin(targetPlayerName); - findViewById(R.id.play_button).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - BackgroundService.RunCommand(MprisActivity.this, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - if (targetPlayer == null) return; - targetPlayer.playPause(); - } - }); - } - }); + findViewById(R.id.play_button).setOnClickListener(view -> BackgroundService.RunCommand(MprisActivity.this, service -> { + if (targetPlayer == null) return; + targetPlayer.playPause(); + })); - findViewById(R.id.prev_button).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - BackgroundService.RunCommand(MprisActivity.this, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - if (targetPlayer == null) return; - targetPlayer.previous(); - } - }); - } - }); + findViewById(R.id.prev_button).setOnClickListener(view -> BackgroundService.RunCommand(MprisActivity.this, service -> { + if (targetPlayer == null) return; + targetPlayer.previous(); + })); - findViewById(R.id.rew_button).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - BackgroundService.RunCommand(MprisActivity.this, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - if (targetPlayer == null) return; - targetPlayer.seek(interval_time * -1); - } - }); - } - }); + findViewById(R.id.rew_button).setOnClickListener(view -> BackgroundService.RunCommand(MprisActivity.this, service -> { + if (targetPlayer == null) return; + targetPlayer.seek(interval_time * -1); + })); - findViewById(R.id.ff_button).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - BackgroundService.RunCommand(MprisActivity.this, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - if (targetPlayer == null) return; - targetPlayer.seek(interval_time); - } - }); - } - }); + findViewById(R.id.ff_button).setOnClickListener(view -> BackgroundService.RunCommand(MprisActivity.this, service -> { + if (targetPlayer == null) return; + targetPlayer.seek(interval_time); + })); - findViewById(R.id.next_button).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - BackgroundService.RunCommand(MprisActivity.this, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - if (targetPlayer == null) return; - targetPlayer.next(); - } - }); - } - }); + findViewById(R.id.next_button).setOnClickListener(view -> BackgroundService.RunCommand(MprisActivity.this, service -> { + if (targetPlayer == null) return; + targetPlayer.next(); + })); ((SeekBar) findViewById(R.id.volume_seek)).setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int i, boolean b) { } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(final SeekBar seekBar) { - BackgroundService.RunCommand(MprisActivity.this, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - if (targetPlayer == null) return; - targetPlayer.setVolume(seekBar.getProgress()); - } + BackgroundService.RunCommand(MprisActivity.this, service -> { + if (targetPlayer == null) return; + targetPlayer.setVolume(seekBar.getProgress()); }); } }); - positionSeekUpdateRunnable = new Runnable() { - @Override - public void run() { - final SeekBar positionSeek = (SeekBar) findViewById(R.id.positionSeek); - BackgroundService.RunCommand(MprisActivity.this, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - if (targetPlayer != null) { - positionSeek.setProgress((int) (targetPlayer.getPosition())); - } - positionSeekUpdateHandler.removeCallbacks(positionSeekUpdateRunnable); - positionSeekUpdateHandler.postDelayed(positionSeekUpdateRunnable, 1000); - } - }); - } - + positionSeekUpdateRunnable = () -> { + final SeekBar positionSeek = (SeekBar) findViewById(R.id.positionSeek); + BackgroundService.RunCommand(MprisActivity.this, service -> { + if (targetPlayer != null) { + positionSeek.setProgress((int) (targetPlayer.getPosition())); + } + positionSeekUpdateHandler.removeCallbacks(positionSeekUpdateRunnable); + positionSeekUpdateHandler.postDelayed(positionSeekUpdateRunnable, 1000); + }); }; positionSeekUpdateHandler.postDelayed(positionSeekUpdateRunnable, 200); ((SeekBar) findViewById(R.id.positionSeek)).setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean byUser) { ((TextView) findViewById(R.id.progress_textview)).setText(milisToProgress(progress)); } @Override public void onStartTrackingTouch(SeekBar seekBar) { positionSeekUpdateHandler.removeCallbacks(positionSeekUpdateRunnable); } @Override public void onStopTrackingTouch(final SeekBar seekBar) { - BackgroundService.RunCommand(MprisActivity.this, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - if (targetPlayer != null) { - targetPlayer.setPosition(seekBar.getProgress()); - } - positionSeekUpdateHandler.postDelayed(positionSeekUpdateRunnable, 200); + BackgroundService.RunCommand(MprisActivity.this, service -> { + if (targetPlayer != null) { + targetPlayer.setPosition(seekBar.getProgress()); } + positionSeekUpdateHandler.postDelayed(positionSeekUpdateRunnable, 200); }); } }); findViewById(R.id.now_playing_textview).setSelected(true); } @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 207f4ea5..077dc0b1 100644 --- a/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisMediaSession.java +++ b/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisMediaSession.java @@ -1,423 +1,420 @@ /* * 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.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 android.support.v7.app.NotificationCompat; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.Device; import org.kde.kdeconnect_tp.R; import java.util.HashSet; /** * 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 { public final static int MPRIS_MEDIA_NOTIFICATION_ID = 0x91b70463; // echo MprisNotification | md5sum | head -c 8 public final static String MPRIS_MEDIA_SESSION_TAG = "org.kde.kdeconnect_tp.media_session"; private static 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 HashSet mprisDevices = new HashSet<>(); private Context context; private MediaSessionCompat mediaSession; //Callback for mpris plugin updates private Handler mediaNotificationHandler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { updateMediaNotification(); } }; //Callback for control via the media session API private 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, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService 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); + 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; + } - //If the player disappeared (and no other playing one found), just remove the notification - if (notificationPlayer == null) { - closeMediaNotification(); - return; - } + //Make sure our information is up-to-date + updateCurrentPlayer(service); - //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(); + //If the player disappeared (and no other playing one found), just remove the notification + if (notificationPlayer == null) { + closeMediaNotification(); + return; + } - //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()); - } - if (!notificationPlayer.getAlbum().isEmpty()) { - metadata.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, notificationPlayer.getAlbum()); - } - if (notificationPlayer.getLength() > 0) { - metadata.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, notificationPlayer.getLength()); - } + //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(); - Bitmap albumArt = notificationPlayer.getAlbumArt(); - if (albumArt != null) { - metadata.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, albumArt); - } + //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()); + } + if (!notificationPlayer.getAlbum().isEmpty()) { + metadata.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, notificationPlayer.getAlbum()); + } + if (notificationPlayer.getLength() > 0) { + metadata.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, notificationPlayer.getLength()); + } - mediaSession.setMetadata(metadata.build()); - PlaybackStateCompat.Builder playbackState = new PlaybackStateCompat.Builder(); + Bitmap albumArt = notificationPlayer.getAlbumArt(); + if (albumArt != null) { + metadata.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, albumArt); + } - if (notificationPlayer.isPlaying()) { - playbackState.setState(PlaybackStateCompat.STATE_PLAYING, notificationPlayer.getPosition(), 1.0f); - } else { - playbackState.setState(PlaybackStateCompat.STATE_PAUSED, notificationPlayer.getPosition(), 0.0f); - } + mediaSession.setMetadata(metadata.build()); + PlaybackStateCompat.Builder playbackState = new PlaybackStateCompat.Builder(); - //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()); - PendingIntent piOpenActivity = PendingIntent.getActivity(service, 0, iOpenActivity, PendingIntent.FLAG_UPDATE_CURRENT); - - //Create the notification - final NotificationCompat.Builder notification = new NotificationCompat.Builder(service); - notification - .setAutoCancel(false) - .setContentIntent(piOpenActivity) - .setSmallIcon(R.drawable.ic_play_white) - .setShowWhen(false) - .setColor(service.getResources().getColor(R.color.primary)) - .setVisibility(android.support.v4.app.NotificationCompat.VISIBILITY_PUBLIC); - - 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 (notificationPlayer.isPlaying()) { + playbackState.setState(PlaybackStateCompat.STATE_PLAYING, notificationPlayer.getPosition(), 1.0f); + } else { + playbackState.setState(PlaybackStateCompat.STATE_PAUSED, notificationPlayer.getPosition(), 0.0f); + } - if (albumArt != null) { - notification.setLargeIcon(albumArt); - } + //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()); + PendingIntent piOpenActivity = PendingIntent.getActivity(service, 0, iOpenActivity, PendingIntent.FLAG_UPDATE_CURRENT); + + //Create the notification + final NotificationCompat.Builder notification = new NotificationCompat.Builder(service); + notification + .setAutoCancel(false) + .setContentIntent(piOpenActivity) + .setSmallIcon(R.drawable.ic_play_white) + .setShowWhen(false) + .setColor(service.getResources().getColor(R.color.primary)) + .setVisibility(android.support.v4.app.NotificationCompat.VISIBILITY_PUBLIC); + + 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 (!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); - } + if (albumArt != null) { + notification.setLargeIcon(albumArt); + } - //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); - } + 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); + } - //Use the MediaStyle notification, so it feels like other media players. That also allows adding actions - NotificationCompat.MediaStyle mediaStyle = new NotificationCompat.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); + //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); + } - //Display the notification - mediaSession.setActive(true); - final NotificationManager nm = (NotificationManager) service.getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(MPRIS_MEDIA_NOTIFICATION_ID, notification.build()); + //Use the MediaStyle notification, so it feels like other media players. That also allows adding actions + NotificationCompat.MediaStyle mediaStyle = new NotificationCompat.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); + + //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/NotificationsPlugin/NotificationFilterActivity.java b/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationFilterActivity.java index 0497d148..2fa05b2c 100644 --- a/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationFilterActivity.java +++ b/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationFilterActivity.java @@ -1,188 +1,172 @@ /* * Copyright 2015 Vineet Garg * * 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.NotificationsPlugin; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.CheckedTextView; import android.widget.ListView; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.Helpers.StringsHelper; import org.kde.kdeconnect.UserInterface.ThemeUtil; import org.kde.kdeconnect_tp.R; import java.util.Arrays; import java.util.Comparator; import java.util.List; public class NotificationFilterActivity extends AppCompatActivity { private AppDatabase appDatabase; static class AppListInfo { String pkg; String name; Drawable icon; boolean isEnabled; } private AppListInfo[] apps; class AppListAdapter extends BaseAdapter { @Override public int getCount() { return apps.length; } @Override public AppListInfo getItem(int position) { return apps[position]; } @Override public long getItemId(int position) { return position; } public View getView(int position, View view, ViewGroup parent) { if (view == null) { LayoutInflater inflater = getLayoutInflater(); view = inflater.inflate(android.R.layout.simple_list_item_multiple_choice, null, true); } CheckedTextView checkedTextView = (CheckedTextView) view; checkedTextView.setText(apps[position].name); checkedTextView.setCompoundDrawablesWithIntrinsicBounds(apps[position].icon, null, null, null); checkedTextView.setCompoundDrawablePadding((int) (8 * getResources().getDisplayMetrics().density)); return view; } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ThemeUtil.setUserPreferredTheme(this); setContentView(R.layout.activity_notification_filter); appDatabase = new AppDatabase(NotificationFilterActivity.this, false); - new Thread(new Runnable() { - @Override - public void run() { - - PackageManager packageManager = getPackageManager(); - List appList = packageManager.getInstalledApplications(0); - int count = appList.size(); - - apps = new AppListInfo[count]; - for (int i = 0; i < count; i++) { - ApplicationInfo appInfo = appList.get(i); - apps[i] = new AppListInfo(); - apps[i].pkg = appInfo.packageName; - apps[i].name = appInfo.loadLabel(packageManager).toString(); - apps[i].icon = resizeIcon(appInfo.loadIcon(packageManager), 48); - apps[i].isEnabled = appDatabase.isEnabled(appInfo.packageName); - } - - Arrays.sort(apps, new Comparator() { - @Override - public int compare(AppListInfo lhs, AppListInfo rhs) { - return StringsHelper.compare(lhs.name, rhs.name); - } - }); - - runOnUiThread(new Runnable() { - @Override - public void run() { - displayAppList(); - } - }); + new Thread(() -> { + + PackageManager packageManager = getPackageManager(); + List appList = packageManager.getInstalledApplications(0); + int count = appList.size(); + + apps = new AppListInfo[count]; + for (int i = 0; i < count; i++) { + ApplicationInfo appInfo = appList.get(i); + apps[i] = new AppListInfo(); + apps[i].pkg = appInfo.packageName; + apps[i].name = appInfo.loadLabel(packageManager).toString(); + apps[i].icon = resizeIcon(appInfo.loadIcon(packageManager), 48); + apps[i].isEnabled = appDatabase.isEnabled(appInfo.packageName); } + + Arrays.sort(apps, (lhs, rhs) -> StringsHelper.compare(lhs.name, rhs.name)); + + runOnUiThread(this::displayAppList); }).start(); } private void displayAppList() { final ListView listView = (ListView) findViewById(R.id.lvFilterApps); AppListAdapter adapter = new AppListAdapter(); listView.setAdapter(adapter); listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); - listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView adapterView, View view, int i, long l) { - boolean checked = listView.isItemChecked(i); - appDatabase.setEnabled(apps[i].pkg, checked); - apps[i].isEnabled = checked; - } + listView.setOnItemClickListener((adapterView, view, i, l) -> { + boolean checked = listView.isItemChecked(i); + appDatabase.setEnabled(apps[i].pkg, checked); + apps[i].isEnabled = checked; }); for (int i = 0; i < apps.length; i++) { listView.setItemChecked(i, apps[i].isEnabled); } listView.setVisibility(View.VISIBLE); findViewById(R.id.spinner).setVisibility(View.GONE); } @Override protected void onStart() { super.onStart(); BackgroundService.addGuiInUseCounter(this); } @Override protected void onStop() { super.onStop(); BackgroundService.removeGuiInUseCounter(this); } private Drawable resizeIcon(Drawable icon, int maxSize) { Resources res = getResources(); //Convert to display pixels maxSize = (int) (maxSize * res.getDisplayMetrics().density); Bitmap bitmap = Bitmap.createBitmap(maxSize, maxSize, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); icon.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); icon.draw(canvas); return new BitmapDrawable(res, bitmap); } } diff --git a/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationsPlugin.java b/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationsPlugin.java index 6a76cc9b..3c8a5a97 100644 --- a/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationsPlugin.java +++ b/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationsPlugin.java @@ -1,598 +1,576 @@ /* * 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.NotificationsPlugin; import android.annotation.TargetApi; import android.app.Activity; import android.app.AlertDialog; import android.app.Notification; import android.app.PendingIntent; import android.app.RemoteInput; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.os.Build; import android.os.Bundle; import android.provider.Settings; import android.service.notification.StatusBarNotification; import android.support.annotation.RequiresApi; import android.support.v4.app.NotificationCompat; import android.util.Log; import org.kde.kdeconnect.Helpers.AppsHelper; import org.kde.kdeconnect.NetworkPacket; import org.kde.kdeconnect.Plugins.Plugin; import org.kde.kdeconnect.UserInterface.MainActivity; import org.kde.kdeconnect.UserInterface.SettingsActivity; import org.kde.kdeconnect_tp.R; import java.io.ByteArrayOutputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.HashMap; import java.util.Map; @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) public class NotificationsPlugin extends Plugin implements NotificationReceiver.NotificationListener { private final static String PACKET_TYPE_NOTIFICATION = "kdeconnect.notification"; private final static String PACKET_TYPE_NOTIFICATION_REQUEST = "kdeconnect.notification.request"; private final static String PACKET_TYPE_NOTIFICATION_REPLY = "kdeconnect.notification.reply"; private AppDatabase appDatabase; private Map pendingIntents; private boolean serviceReady; @Override public String getDisplayName() { return context.getResources().getString(R.string.pref_plugin_notifications); } @Override public String getDescription() { return context.getResources().getString(R.string.pref_plugin_notifications_desc); } @Override public boolean hasSettings() { return true; } @Override public void startPreferencesActivity(final SettingsActivity parentActivity) { if (hasPermission()) { Intent intent = new Intent(parentActivity, NotificationFilterActivity.class); parentActivity.startActivity(intent); } else { getErrorDialog(parentActivity).show(); } } private boolean hasPermission() { String notificationListenerList = Settings.Secure.getString(context.getContentResolver(), "enabled_notification_listeners"); return (notificationListenerList != null && notificationListenerList.contains(context.getPackageName())); } @Override public boolean onCreate() { if (!hasPermission()) return false; pendingIntents = new HashMap<>(); appDatabase = new AppDatabase(context, true); - NotificationReceiver.RunCommand(context, new NotificationReceiver.InstanceCallback() { - @Override - public void onServiceStart(NotificationReceiver service) { + NotificationReceiver.RunCommand(context, service -> { - service.addListener(NotificationsPlugin.this); + service.addListener(NotificationsPlugin.this); - serviceReady = service.isConnected(); + serviceReady = service.isConnected(); - if (serviceReady) { - sendCurrentNotifications(service); - } + if (serviceReady) { + sendCurrentNotifications(service); } }); return true; } @Override public void onDestroy() { - NotificationReceiver.RunCommand(context, new NotificationReceiver.InstanceCallback() { - @Override - public void onServiceStart(NotificationReceiver service) { - service.removeListener(NotificationsPlugin.this); - } - }); + NotificationReceiver.RunCommand(context, service -> service.removeListener(NotificationsPlugin.this)); } @Override public void onListenerConnected(NotificationReceiver service) { serviceReady = true; sendCurrentNotifications(service); } @Override public void onNotificationRemoved(StatusBarNotification statusBarNotification) { if (statusBarNotification == null) { Log.w("onNotificationRemoved", "notification is null"); return; } String id = getNotificationKeyCompat(statusBarNotification); NetworkPacket np = new NetworkPacket(PACKET_TYPE_NOTIFICATION); np.set("id", id); np.set("isCancel", true); device.sendPacket(np); } @Override public void onNotificationPosted(StatusBarNotification statusBarNotification) { sendNotification(statusBarNotification); } private void sendNotification(StatusBarNotification statusBarNotification) { Notification notification = statusBarNotification.getNotification(); if ((notification.flags & Notification.FLAG_FOREGROUND_SERVICE) != 0 || (notification.flags & Notification.FLAG_ONGOING_EVENT) != 0 || (notification.flags & Notification.FLAG_LOCAL_ONLY) != 0 || (notification.flags & NotificationCompat.FLAG_GROUP_SUMMARY) != 0) //The notification that groups other notifications { //This is not a notification we want! return; } if (!appDatabase.isEnabled(statusBarNotification.getPackageName())) { return; // we dont want notification from this app } String key = getNotificationKeyCompat(statusBarNotification); String packageName = statusBarNotification.getPackageName(); String appName = AppsHelper.appNameLookup(context, packageName); if ("com.facebook.orca".equals(packageName) && (statusBarNotification.getId() == 10012) && "Messenger".equals(appName) && notification.tickerText == null) { //HACK: Hide weird Facebook empty "Messenger" notification that is actually not shown in the phone return; } if ("com.android.systemui".equals(packageName) && "low_battery".equals(statusBarNotification.getTag())) { //HACK: Android low battery notification are posted again every few seconds. Ignore them, as we already have a battery indicator. return; } NetworkPacket np = new NetworkPacket(PACKET_TYPE_NOTIFICATION); if (packageName.equals("org.kde.kdeconnect_tp")) { //Make our own notifications silent :) np.set("silent", true); np.set("requestAnswer", true); //For compatibility with old desktop versions of KDE Connect that don't support "silent" } try { Bitmap appIcon = null; Context foreignContext = context.createPackageContext(statusBarNotification.getPackageName(), 0); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { appIcon = iconToBitmap(foreignContext, notification.getLargeIcon()); } else { appIcon = notification.largeIcon; } //appIcon = drawableToBitmap(context.getResources().getDrawable(R.drawable.icon)); if (appIcon == null) { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { appIcon = iconToBitmap(foreignContext, notification.getSmallIcon()); } else { PackageManager pm = context.getPackageManager(); Resources foreignResources = pm.getResourcesForApplication(statusBarNotification.getPackageName()); Drawable foreignIcon = foreignResources.getDrawable(notification.icon); appIcon = drawableToBitmap(foreignIcon); } } if (appIcon != null) { ByteArrayOutputStream outStream = new ByteArrayOutputStream(); appIcon.compress(Bitmap.CompressFormat.PNG, 90, outStream); byte[] bitmapData = outStream.toByteArray(); np.setPayload(bitmapData); np.set("payloadHash", getChecksum(bitmapData)); } } catch (Exception e) { e.printStackTrace(); Log.e("NotificationsPlugin", "Error retrieving icon"); } RepliableNotification rn = extractRepliableNotification(statusBarNotification); if (rn.pendingIntent != null) { np.set("requestReplyId", rn.id); pendingIntents.put(rn.id, rn); } np.set("id", key); np.set("appName", appName == null ? packageName : appName); np.set("isClearable", statusBarNotification.isClearable()); np.set("ticker", getTickerText(notification)); np.set("title", getNotificationTitle(notification)); np.set("text", getNotificationText(notification)); np.set("time", Long.toString(statusBarNotification.getPostTime())); device.sendPacket(np); } private Bitmap drawableToBitmap(Drawable drawable) { if (drawable == null) return null; Bitmap res; if (drawable.getIntrinsicWidth() > 128 || drawable.getIntrinsicHeight() > 128) { res = Bitmap.createBitmap(96, 96, Bitmap.Config.ARGB_8888); } else if (drawable.getIntrinsicWidth() <= 64 || drawable.getIntrinsicHeight() <= 64) { res = Bitmap.createBitmap(96, 96, Bitmap.Config.ARGB_8888); } else { res = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); } Canvas canvas = new Canvas(res); drawable.setBounds(0, 0, res.getWidth(), res.getHeight()); drawable.draw(canvas); return res; } @RequiresApi(Build.VERSION_CODES.M) private Bitmap iconToBitmap(Context foreignContext, Icon icon) { if (icon == null) return null; return drawableToBitmap(icon.loadDrawable(foreignContext)); } @RequiresApi(api = Build.VERSION_CODES.KITKAT_WATCH) private void replyToNotification(String id, String message) { if (pendingIntents.isEmpty() || !pendingIntents.containsKey(id)) { Log.e("NotificationsPlugin", "No such notification"); return; } RepliableNotification repliableNotification = pendingIntents.get(id); if (repliableNotification == null) { Log.e("NotificationsPlugin", "No such notification"); return; } RemoteInput[] remoteInputs = new RemoteInput[repliableNotification.remoteInputs.size()]; Intent localIntent = new Intent(); localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); Bundle localBundle = new Bundle(); int i = 0; for (RemoteInput remoteIn : repliableNotification.remoteInputs) { getDetailsOfNotification(remoteIn); remoteInputs[i] = remoteIn; localBundle.putCharSequence(remoteInputs[i].getResultKey(), message); i++; } RemoteInput.addResultsToIntent(remoteInputs, localIntent, localBundle); try { repliableNotification.pendingIntent.send(context, 0, localIntent); } catch (PendingIntent.CanceledException e) { Log.e("NotificationPlugin", "replyToNotification error: " + e.getMessage()); } pendingIntents.remove(id); } @RequiresApi(api = Build.VERSION_CODES.KITKAT_WATCH) private void getDetailsOfNotification(RemoteInput remoteInput) { //Some more details of RemoteInput... no idea what for but maybe it will be useful at some point String resultKey = remoteInput.getResultKey(); String label = remoteInput.getLabel().toString(); Boolean canFreeForm = remoteInput.getAllowFreeFormInput(); if (remoteInput.getChoices() != null && remoteInput.getChoices().length > 0) { String[] possibleChoices = new String[remoteInput.getChoices().length]; for (int i = 0; i < remoteInput.getChoices().length; i++) { possibleChoices[i] = remoteInput.getChoices()[i].toString(); } } } private String getNotificationTitle(Notification notification) { final String TITLE_KEY = "android.title"; final String TEXT_KEY = "android.text"; String title = ""; if (notification != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { try { Bundle extras = notification.extras; title = extras.getString(TITLE_KEY); } catch (Exception e) { Log.w("NotificationPlugin", "problem parsing notification extras for " + notification.tickerText); e.printStackTrace(); } } } //TODO Add compat for under Kitkat devices return title; } private RepliableNotification extractRepliableNotification(StatusBarNotification statusBarNotification) { RepliableNotification repliableNotification = new RepliableNotification(); if (statusBarNotification != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { try { Boolean reply = false; //works for WhatsApp, but not for Telegram if (statusBarNotification.getNotification().actions != null) { for (Notification.Action act : statusBarNotification.getNotification().actions) { if (act != null && act.getRemoteInputs() != null) { repliableNotification.remoteInputs.addAll(Arrays.asList(act.getRemoteInputs())); repliableNotification.pendingIntent = act.actionIntent; reply = true; break; } } repliableNotification.packageName = statusBarNotification.getPackageName(); repliableNotification.tag = statusBarNotification.getTag();//TODO find how to pass Tag with sending PendingIntent, might fix Hangout problem } } catch (Exception e) { Log.w("NotificationPlugin", "problem extracting notification wear for " + statusBarNotification.getNotification().tickerText); e.printStackTrace(); } } } return repliableNotification; } private String getNotificationText(Notification notification) { final String TEXT_KEY = "android.text"; String text = ""; if (notification != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { try { Bundle extras = notification.extras; Object extraTextExtra = extras.get(TEXT_KEY); if (extraTextExtra != null) text = extraTextExtra.toString(); } catch (Exception e) { Log.w("NotificationPlugin", "problem parsing notification extras for " + notification.tickerText); e.printStackTrace(); } } } //TODO Add compat for under Kitkat devices return text; } /** * Returns the ticker text of the notification. * If device android version is KitKat or newer, the title and text of the notification is used * instead the ticker text. */ private String getTickerText(Notification notification) { final String TITLE_KEY = "android.title"; final String TEXT_KEY = "android.text"; String ticker = ""; if (notification != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { try { Bundle extras = notification.extras; String extraTitle = extras.getString(TITLE_KEY); String extraText = null; Object extraTextExtra = extras.get(TEXT_KEY); if (extraTextExtra != null) extraText = extraTextExtra.toString(); if (extraTitle != null && extraText != null && !extraText.isEmpty()) { ticker = extraTitle + ": " + extraText; } else if (extraTitle != null) { ticker = extraTitle; } else if (extraText != null) { ticker = extraText; } } catch (Exception e) { Log.w("NotificationPlugin", "problem parsing notification extras for " + notification.tickerText); e.printStackTrace(); } } if (ticker.isEmpty()) { ticker = (notification.tickerText != null) ? notification.tickerText.toString() : ""; } } return ticker; } private void sendCurrentNotifications(NotificationReceiver service) { StatusBarNotification[] notifications = service.getActiveNotifications(); for (StatusBarNotification notification : notifications) { sendNotification(notification); } } @Override public boolean onPacketReceived(final NetworkPacket np) { if (np.getBoolean("request")) { if (serviceReady) { - NotificationReceiver.RunCommand(context, new NotificationReceiver.InstanceCallback() { - @Override - public void onServiceStart(NotificationReceiver service) { - sendCurrentNotifications(service); - } - }); + NotificationReceiver.RunCommand(context, this::sendCurrentNotifications); } } else if (np.has("cancel")) { - NotificationReceiver.RunCommand(context, new NotificationReceiver.InstanceCallback() { - @Override - public void onServiceStart(NotificationReceiver service) { - String dismissedId = np.getString("cancel"); - cancelNotificationCompat(service, dismissedId); - } + NotificationReceiver.RunCommand(context, service -> { + String dismissedId = np.getString("cancel"); + cancelNotificationCompat(service, dismissedId); }); } else if (np.has("requestReplyId") && np.has("message")) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { replyToNotification(np.getString("requestReplyId"), np.getString("message")); } } return true; } @Override public AlertDialog getErrorDialog(final Activity deviceActivity) { return new AlertDialog.Builder(deviceActivity) .setTitle(R.string.pref_plugin_notifications) .setMessage(R.string.no_permissions) - .setPositiveButton(R.string.open_settings, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - Intent intent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"); - deviceActivity.startActivityForResult(intent, MainActivity.RESULT_NEEDS_RELOAD); - } + .setPositiveButton(R.string.open_settings, (dialogInterface, i) -> { + Intent intent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"); + deviceActivity.startActivityForResult(intent, MainActivity.RESULT_NEEDS_RELOAD); }) - .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - //Do nothing - } + .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { + //Do nothing }) .create(); } @Override public String[] getSupportedPacketTypes() { return new String[]{PACKET_TYPE_NOTIFICATION_REQUEST, PACKET_TYPE_NOTIFICATION_REPLY}; } @Override public String[] getOutgoingPacketTypes() { return new String[]{PACKET_TYPE_NOTIFICATION}; } //For compat with API<21, because lollipop changed the way to cancel notifications private static void cancelNotificationCompat(NotificationReceiver service, String compatKey) { if (Build.VERSION.SDK_INT >= 21) { service.cancelNotification(compatKey); } else { int first = compatKey.indexOf(':'); if (first == -1) { Log.e("cancelNotificationCompa", "Not formated like a notification key: " + compatKey); return; } int last = compatKey.lastIndexOf(':'); String packageName = compatKey.substring(0, first); String tag = compatKey.substring(first + 1, last); if (tag.length() == 0) tag = null; String idString = compatKey.substring(last + 1); int id; try { id = Integer.parseInt(idString); } catch (Exception e) { id = 0; } service.cancelNotification(packageName, tag, id); } } private static String getNotificationKeyCompat(StatusBarNotification statusBarNotification) { String result; // first check if it's one of our remoteIds String tag = statusBarNotification.getTag(); if (tag != null && tag.startsWith("kdeconnectId:")) result = Integer.toString(statusBarNotification.getId()); else if (Build.VERSION.SDK_INT >= 21) { result = statusBarNotification.getKey(); } else { String packageName = statusBarNotification.getPackageName(); int id = statusBarNotification.getId(); String safePackageName = (packageName == null) ? "" : packageName; String safeTag = (tag == null) ? "" : tag; result = safePackageName + ":" + safeTag + ":" + id; } return result; } private String getChecksum(byte[] data) { try { MessageDigest md = MessageDigest.getInstance("MD5"); md.update(data); return bytesToHex(md.digest()); } catch (NoSuchAlgorithmException e) { Log.e("KDEConnect", "Error while generating checksum", e); } return null; } private static String bytesToHex(byte[] bytes) { char[] hexArray = "0123456789ABCDEF".toCharArray(); char[] hexChars = new char[bytes.length * 2]; for (int j = 0; j < bytes.length; j++) { int v = bytes[j] & 0xFF; hexChars[j * 2] = hexArray[v >>> 4]; hexChars[j * 2 + 1] = hexArray[v & 0x0F]; } return new String(hexChars).toLowerCase(); } @Override public int getMinSdk() { return Build.VERSION_CODES.JELLY_BEAN_MR2; } } diff --git a/src/org/kde/kdeconnect/Plugins/Plugin.java b/src/org/kde/kdeconnect/Plugins/Plugin.java index bfa23c30..95e346b1 100644 --- a/src/org/kde/kdeconnect/Plugins/Plugin.java +++ b/src/org/kde/kdeconnect/Plugins/Plugin.java @@ -1,289 +1,276 @@ /* * 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.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import android.os.Build; import android.support.annotation.StringRes; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.view.View; import android.widget.Button; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.NetworkPacket; import org.kde.kdeconnect.UserInterface.PluginSettingsActivity; import org.kde.kdeconnect.UserInterface.SettingsActivity; import org.kde.kdeconnect_tp.R; public abstract class Plugin { protected Device device; protected Context context; protected int permissionExplanation = R.string.permission_explanation; protected int optionalPermissionExplanation = R.string.optional_permission_explanation; public final void setContext(Context context, Device device) { this.device = device; this.context = context; } /** * To receive the network package from the unpaired device, override * listensToUnpairedDevices to return true and this method. */ public boolean onUnpairedDevicePacketReceived(NetworkPacket np) { return false; } /** * Returns whether this plugin should be loaded or not, to listen to NetworkPackets * from the unpaired devices. By default, returns false. */ public boolean listensToUnpairedDevices() { return false; } /** * Return the internal plugin name, that will be used as a * unique key to distinguish it. Use the class name as key. */ public String getPluginKey() { return getPluginKey(this.getClass()); } public static String getPluginKey(Class p) { return p.getSimpleName(); } /** * Return the human-readable plugin name. This function can * access this.context to provide translated text. */ public abstract String getDisplayName(); /** * Return the human-readable description of this plugin. This * function can access this.context to provide translated text. */ public abstract String getDescription(); /** * Return the action name displayed in the main activity, that * will call startMainActivity when clicked */ public String getActionName() { return getDisplayName(); } /** * Return an icon associated to this plugin. This function can * access this.context to load the image from resources. */ public Drawable getIcon() { return null; } /** * Return true if this plugin should be enabled on new devices. * This function can access this.context and perform compatibility * checks with the Android version, but can not access this.device. */ public boolean isEnabledByDefault() { return true; } /** * Return true if this plugin needs an specific UI settings. */ public boolean hasSettings() { return false; } /** * If hasSettings returns true, this will be called when the user * wants to access this plugin preferences and should launch some * kind of interface. The default implementation will launch a * SettingsActivity with content from "yourplugin"_preferences.xml. */ public void startPreferencesActivity(SettingsActivity parentActivity) { Intent intent = new Intent(parentActivity, PluginSettingsActivity.class); intent.putExtra("plugin_display_name", getDisplayName()); intent.putExtra("plugin_key", getPluginKey()); parentActivity.startActivity(intent); } /** * Return true if the plugin should display something in the Device main view */ public boolean hasMainActivity() { return false; } /** * Implement here what your plugin should do when clicked */ public void startMainActivity(Activity parentActivity) { } /** * Return true if the entry for this app should appear in the context menu instead of the main view */ public boolean displayInContextMenu() { return false; } /** * Initialize the listeners and structures in your plugin. * Should return true if initialization was successful. */ public boolean onCreate() { return true; } /** * Finish any ongoing operations, remove listeners... so * this object could be garbage collected. */ public void onDestroy() { } /** * Called when a plugin receives a package. By convention we return true * when we have done something in response to the package or false * otherwise, even though that value is unused as of now. */ public boolean onPacketReceived(NetworkPacket np) { return false; } /** * Should return the list of NetworkPacket types that this plugin can handle */ public abstract String[] getSupportedPacketTypes(); /** * Should return the list of NetworkPacket types that this plugin can send */ public abstract String[] getOutgoingPacketTypes(); /** * Creates a button that will be displayed in the user interface * It can open an activity or perform any other action that the * plugin would wants to expose to the user. Return null if no * button should be displayed. */ @Deprecated public Button getInterfaceButton(final Activity activity) { if (!hasMainActivity()) return null; Button b = new Button(activity); b.setText(getActionName()); - b.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - startMainActivity(activity); - } - }); + b.setOnClickListener(view -> startMainActivity(activity)); return b; } public String[] getRequiredPermissions() { return new String[0]; } public String[] getOptionalPermissions() { return new String[0]; } //Permission from Manifest.permission.* protected boolean isPermissionGranted(String permission) { int result = ContextCompat.checkSelfPermission(context, permission); return (result == PackageManager.PERMISSION_GRANTED); } protected boolean arePermissionsGranted(String[] permissions) { for (String permission : permissions) { if (!isPermissionGranted(permission)) { return false; } } return true; } protected AlertDialog requestPermissionDialog(Activity activity, String permissions, @StringRes int reason) { return requestPermissionDialog(activity, new String[]{permissions}, reason); } protected AlertDialog requestPermissionDialog(final Activity activity, final String[] permissions, @StringRes int reason) { return new AlertDialog.Builder(activity) .setTitle(getDisplayName()) .setMessage(reason) - .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - ActivityCompat.requestPermissions(activity, permissions, 0); - } - }) - .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - //Do nothing - } + .setPositiveButton(R.string.ok, (dialogInterface, i) -> ActivityCompat.requestPermissions(activity, permissions, 0)) + .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { + //Do nothing }) .create(); } /** * If onCreate returns false, should create a dialog explaining * the problem (and how to fix it, if possible) to the user. */ public AlertDialog getErrorDialog(Activity deviceActivity) { return null; } public AlertDialog getPermissionExplanationDialog(Activity deviceActivity) { return requestPermissionDialog(deviceActivity, getRequiredPermissions(), permissionExplanation); } public AlertDialog getOptionalPermissionExplanationDialog(Activity deviceActivity) { return requestPermissionDialog(deviceActivity, getOptionalPermissions(), optionalPermissionExplanation); } public boolean checkRequiredPermissions() { return arePermissionsGranted(getRequiredPermissions()); } public boolean checkOptionalPermissions() { return arePermissionsGranted(getOptionalPermissions()); } public int getMinSdk() { return Build.VERSION_CODES.BASE; } } diff --git a/src/org/kde/kdeconnect/Plugins/RemoteKeyboardPlugin/RemoteKeyboardPlugin.java b/src/org/kde/kdeconnect/Plugins/RemoteKeyboardPlugin/RemoteKeyboardPlugin.java index 620de15c..855798aa 100644 --- a/src/org/kde/kdeconnect/Plugins/RemoteKeyboardPlugin/RemoteKeyboardPlugin.java +++ b/src/org/kde/kdeconnect/Plugins/RemoteKeyboardPlugin/RemoteKeyboardPlugin.java @@ -1,402 +1,392 @@ /* * Copyright 2017 Holger Kaelberer * * 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.RemoteKeyboardPlugin; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.SystemClock; import android.preference.PreferenceManager; import android.support.v4.content.ContextCompat; import android.support.v4.util.Pair; import android.util.Log; import android.util.SparseIntArray; import android.view.KeyEvent; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputConnection; import org.kde.kdeconnect.NetworkPacket; import org.kde.kdeconnect.Plugins.Plugin; import org.kde.kdeconnect_tp.R; import java.util.ArrayList; import java.util.concurrent.locks.ReentrantLock; public class RemoteKeyboardPlugin extends Plugin { public final static String PACKET_TYPE_MOUSEPAD_REQUEST = "kdeconnect.mousepad.request"; public final static String PACKET_TYPE_MOUSEPAD_ECHO = "kdeconnect.mousepad.echo"; public final static String PACKET_TYPE_MOUSEPAD_KEYBOARDSTATE = "kdeconnect.mousepad.keyboardstate"; /** * Track and expose plugin instances to allow for a 'connected'-indicator in the IME: */ private static ArrayList instances = new ArrayList(); private static ReentrantLock instancesLock = new ReentrantLock(true); public static ArrayList getInstances() { return instances; } public static ArrayList acquireInstances() { instancesLock.lock(); return getInstances(); } public static ArrayList releaseInstances() { instancesLock.unlock(); return getInstances(); } public static boolean isConnected() { return instances.size() > 0; } private static SparseIntArray specialKeyMap = new SparseIntArray(); static { int i = 0; specialKeyMap.put(++i, KeyEvent.KEYCODE_DEL); // 1 specialKeyMap.put(++i, KeyEvent.KEYCODE_TAB); // 2 ++i; //specialKeyMap.put(++i, KeyEvent.KEYCODE_ENTER, 12); // 3 is not used specialKeyMap.put(++i, KeyEvent.KEYCODE_DPAD_LEFT); // 4 specialKeyMap.put(++i, KeyEvent.KEYCODE_DPAD_UP); // 5 specialKeyMap.put(++i, KeyEvent.KEYCODE_DPAD_RIGHT); // 6 specialKeyMap.put(++i, KeyEvent.KEYCODE_DPAD_DOWN); // 7 specialKeyMap.put(++i, KeyEvent.KEYCODE_PAGE_UP); // 8 specialKeyMap.put(++i, KeyEvent.KEYCODE_PAGE_DOWN); // 9 if (Build.VERSION.SDK_INT >= 11) { specialKeyMap.put(++i, KeyEvent.KEYCODE_MOVE_HOME); // 10 specialKeyMap.put(++i, KeyEvent.KEYCODE_MOVE_END); // 11 specialKeyMap.put(++i, KeyEvent.KEYCODE_ENTER); // 12 specialKeyMap.put(++i, KeyEvent.KEYCODE_FORWARD_DEL); // 13 specialKeyMap.put(++i, KeyEvent.KEYCODE_ESCAPE); // 14 specialKeyMap.put(++i, KeyEvent.KEYCODE_SYSRQ); // 15 specialKeyMap.put(++i, KeyEvent.KEYCODE_SCROLL_LOCK); // 16 ++i; // 17 ++i; // 18 ++i; // 19 ++i; // 20 specialKeyMap.put(++i, KeyEvent.KEYCODE_F1); // 21 specialKeyMap.put(++i, KeyEvent.KEYCODE_F2); // 22 specialKeyMap.put(++i, KeyEvent.KEYCODE_F3); // 23 specialKeyMap.put(++i, KeyEvent.KEYCODE_F4); // 24 specialKeyMap.put(++i, KeyEvent.KEYCODE_F5); // 25 specialKeyMap.put(++i, KeyEvent.KEYCODE_F6); // 26 specialKeyMap.put(++i, KeyEvent.KEYCODE_F7); // 27 specialKeyMap.put(++i, KeyEvent.KEYCODE_F8); // 28 specialKeyMap.put(++i, KeyEvent.KEYCODE_F9); // 29 specialKeyMap.put(++i, KeyEvent.KEYCODE_F10); // 30 specialKeyMap.put(++i, KeyEvent.KEYCODE_F11); // 31 specialKeyMap.put(++i, KeyEvent.KEYCODE_F12); // 21 } } @Override public boolean onCreate() { Log.d("RemoteKeyboardPlugin", "Creating for device " + device.getName()); acquireInstances(); try { instances.add(this); } finally { releaseInstances(); } if (RemoteKeyboardService.instance != null) - RemoteKeyboardService.instance.handler.post(new Runnable() { - @Override - public void run() { - RemoteKeyboardService.instance.updateInputView(); - } - }); + RemoteKeyboardService.instance.handler.post(() -> RemoteKeyboardService.instance.updateInputView()); return true; } @Override public void onDestroy() { acquireInstances(); try { if (instances.contains(this)) { instances.remove(this); if (instances.size() < 1 && RemoteKeyboardService.instance != null) - RemoteKeyboardService.instance.handler.post(new Runnable() { - @Override - public void run() { - RemoteKeyboardService.instance.updateInputView(); - } - }); + RemoteKeyboardService.instance.handler.post(() -> RemoteKeyboardService.instance.updateInputView()); } } finally { releaseInstances(); } Log.d("RemoteKeyboardPlugin", "Destroying for device " + device.getName()); } @Override public String getDisplayName() { return context.getString(R.string.pref_plugin_remotekeyboard); } @Override public String getDescription() { return context.getString(R.string.pref_plugin_remotekeyboard_desc); } @Override public Drawable getIcon() { return ContextCompat.getDrawable(context, R.drawable.ic_action_keyboard); } @Override public boolean hasSettings() { return true; } @Override public boolean hasMainActivity() { return false; } @Override public String[] getSupportedPacketTypes() { return new String[]{PACKET_TYPE_MOUSEPAD_REQUEST}; } @Override public String[] getOutgoingPacketTypes() { return new String[]{PACKET_TYPE_MOUSEPAD_ECHO, PACKET_TYPE_MOUSEPAD_KEYBOARDSTATE}; } private boolean isValidSpecialKey(int key) { return (specialKeyMap.get(key, 0) > 0); } private int getCharPos(ExtractedText extractedText, char ch, boolean forward) { int pos = -1; if (extractedText != null) { if (!forward) // backward pos = extractedText.text.toString().lastIndexOf(" ", extractedText.selectionEnd - 2); else pos = extractedText.text.toString().indexOf(" ", extractedText.selectionEnd + 1); return pos; } return pos; } private int currentTextLength(ExtractedText extractedText) { if (extractedText != null) return extractedText.text.length(); return -1; } private int currentCursorPos(ExtractedText extractedText) { if (extractedText != null) return extractedText.selectionEnd; return -1; } private Pair currentSelection(ExtractedText extractedText) { if (extractedText != null) return new Pair<>(extractedText.selectionStart, extractedText.selectionEnd); return new Pair<>(-1, -1); } private boolean handleSpecialKey(int key, boolean shift, boolean ctrl, boolean alt) { int keyEvent = specialKeyMap.get(key, 0); if (keyEvent == 0) return false; InputConnection inputConn = RemoteKeyboardService.instance.getCurrentInputConnection(); // Log.d("RemoteKeyboardPlugin", "Handling special key " + key + " translated to " + keyEvent + " shift=" + shift + " ctrl=" + ctrl + " alt=" + alt); // special sequences: if (ctrl && (keyEvent == KeyEvent.KEYCODE_DPAD_RIGHT)) { // Ctrl + right -> next word ExtractedText extractedText = inputConn.getExtractedText(new ExtractedTextRequest(), 0); int pos = getCharPos(extractedText, ' ', keyEvent == KeyEvent.KEYCODE_DPAD_RIGHT); if (pos == -1) pos = currentTextLength(extractedText); else pos++; int startPos = pos; int endPos = pos; if (shift) { // Shift -> select word (otherwise jump) Pair sel = currentSelection(extractedText); int cursor = currentCursorPos(extractedText); // Log.d("RemoteKeyboardPlugin", "Selection (to right): " + sel.first + " / " + sel.second + " cursor: " + cursor); startPos = cursor; if (sel.first < cursor || // active selection from left to right -> grow sel.first > sel.second) // active selection from right to left -> shrink startPos = sel.first; } inputConn.setSelection(startPos, endPos); } else if (ctrl && keyEvent == KeyEvent.KEYCODE_DPAD_LEFT) { // Ctrl + left -> previous word ExtractedText extractedText = inputConn.getExtractedText(new ExtractedTextRequest(), 0); int pos = getCharPos(extractedText, ' ', keyEvent == KeyEvent.KEYCODE_DPAD_RIGHT); if (pos == -1) pos = 0; else pos++; int startPos = pos; int endPos = pos; if (shift) { Pair sel = currentSelection(extractedText); int cursor = currentCursorPos(extractedText); // Log.d("RemoteKeyboardPlugin", "Selection (to left): " + sel.first + " / " + sel.second + " cursor: " + cursor); startPos = cursor; if (cursor < sel.first || // active selection from right to left -> grow sel.first < sel.second) // active selection from right to left -> shrink startPos = sel.first; } inputConn.setSelection(startPos, endPos); } else if (shift && (keyEvent == KeyEvent.KEYCODE_DPAD_LEFT || keyEvent == KeyEvent.KEYCODE_DPAD_RIGHT || keyEvent == KeyEvent.KEYCODE_DPAD_UP || keyEvent == KeyEvent.KEYCODE_DPAD_DOWN || keyEvent == KeyEvent.KEYCODE_MOVE_HOME || keyEvent == KeyEvent.KEYCODE_MOVE_END)) { // Shift + up/down/left/right/home/end long now = SystemClock.uptimeMillis(); inputConn.sendKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_SHIFT_LEFT, 0, 0)); inputConn.sendKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyEvent, 0, KeyEvent.META_SHIFT_LEFT_ON)); inputConn.sendKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_UP, keyEvent, 0, KeyEvent.META_SHIFT_LEFT_ON)); inputConn.sendKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_SHIFT_LEFT, 0, 0)); } else if (keyEvent == KeyEvent.KEYCODE_NUMPAD_ENTER || keyEvent == KeyEvent.KEYCODE_ENTER) { // Enter key EditorInfo editorInfo = RemoteKeyboardService.instance.getCurrentInputEditorInfo(); // Log.d("RemoteKeyboardPlugin", "Enter: " + editorInfo.imeOptions); if (editorInfo != null && (((editorInfo.imeOptions & EditorInfo.IME_FLAG_NO_ENTER_ACTION) == 0) || ctrl)) { // Ctrl+Return overrides IME_FLAG_NO_ENTER_ACTION (FIXME: make configurable?) // check for special DONE/GO/etc actions first: int[] actions = {EditorInfo.IME_ACTION_GO, EditorInfo.IME_ACTION_NEXT, EditorInfo.IME_ACTION_SEND, EditorInfo.IME_ACTION_SEARCH, EditorInfo.IME_ACTION_DONE}; // note: DONE should be last or we might hide the ime instead of "go" for (int i = 0; i < actions.length; i++) { if ((editorInfo.imeOptions & actions[i]) == actions[i]) { // Log.d("RemoteKeyboardPlugin", "Enter-action: " + actions[i]); inputConn.performEditorAction(actions[i]); return true; } } } else { // else: fall back to regular Enter-event: // Log.d("RemoteKeyboardPlugin", "Enter: normal keypress"); inputConn.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyEvent)); inputConn.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyEvent)); } } else { // default handling: inputConn.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyEvent)); inputConn.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyEvent)); } return true; } private boolean handleVisibleKey(String key, boolean shift, boolean ctrl, boolean alt) { // Log.d("RemoteKeyboardPlugin", "Handling visible key " + key + " shift=" + shift + " ctrl=" + ctrl + " alt=" + alt + " " + key.equalsIgnoreCase("c") + " " + key.length()); if (key.isEmpty()) return false; InputConnection inputConn = RemoteKeyboardService.instance.getCurrentInputConnection(); if (inputConn == null) return false; // ctrl+c/v/x if (key.equalsIgnoreCase("c") && ctrl) { return inputConn.performContextMenuAction(android.R.id.copy); } else if (key.equalsIgnoreCase("v") && ctrl) return inputConn.performContextMenuAction(android.R.id.paste); else if (key.equalsIgnoreCase("x") && ctrl) return inputConn.performContextMenuAction(android.R.id.cut); else if (key.equalsIgnoreCase("a") && ctrl) return inputConn.performContextMenuAction(android.R.id.selectAll); // Log.d("RemoteKeyboardPlugin", "Committing visible key '" + key + "'"); inputConn.commitText(key, key.length()); return true; } private boolean handleEvent(NetworkPacket np) { if (np.has("specialKey") && isValidSpecialKey(np.getInt("specialKey"))) return handleSpecialKey(np.getInt("specialKey"), np.getBoolean("shift"), np.getBoolean("ctrl"), np.getBoolean("alt")); // try visible key return handleVisibleKey(np.getString("key"), np.getBoolean("shift"), np.getBoolean("ctrl"), np.getBoolean("alt")); } @Override public boolean onPacketReceived(NetworkPacket np) { if (!np.getType().equals(PACKET_TYPE_MOUSEPAD_REQUEST) || (!np.has("key") && !np.has("specialKey"))) { // expect at least key OR specialKey Log.e("RemoteKeyboardPlugin", "Invalid package for remotekeyboard plugin!"); return false; } if (RemoteKeyboardService.instance == null) { Log.i("RemoteKeyboardPlugin", "Remote keyboard is not the currently selected input method, dropping key"); return false; } if (!RemoteKeyboardService.instance.visible && PreferenceManager.getDefaultSharedPreferences(context).getBoolean(context.getString(R.string.remotekeyboard_editing_only), true)) { Log.i("RemoteKeyboardPlugin", "Remote keyboard is currently not visible, dropping key"); return false; } if (!handleEvent(np)) { Log.i("RemoteKeyboardPlugin", "Could not handle event!"); return false; } if (np.getBoolean("sendAck")) { NetworkPacket reply = new NetworkPacket(PACKET_TYPE_MOUSEPAD_ECHO); reply.set("key", np.getString("key")); if (np.has("specialKey")) reply.set("specialKey", np.getInt("specialKey")); if (np.has("shift")) reply.set("shift", np.getBoolean("shift")); if (np.has("ctrl")) reply.set("ctrl", np.getBoolean("ctrl")); if (np.has("alt")) reply.set("alt", np.getBoolean("alt")); reply.set("isAck", true); device.sendPacket(reply); } return true; } public void notifyKeyboardState(boolean state) { Log.d("RemoteKeyboardPlugin", "Keyboardstate changed to " + state); NetworkPacket np = new NetworkPacket(PACKET_TYPE_MOUSEPAD_KEYBOARDSTATE); np.set("state", state); device.sendPacket(np); } } diff --git a/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/AddCommandDialog.java b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/AddCommandDialog.java index 35cfc75f..afdb3ba2 100644 --- a/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/AddCommandDialog.java +++ b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/AddCommandDialog.java @@ -1,57 +1,53 @@ package org.kde.kdeconnect.Plugins.RunCommandPlugin; import android.app.Dialog; import android.content.DialogInterface; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.app.DialogFragment; import android.support.v7.app.AlertDialog; import android.text.Editable; import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; import android.widget.EditText; import org.kde.kdeconnect_tp.R; public class AddCommandDialog extends DialogFragment { private EditText nameField; private EditText commandField; @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setTitle(R.string.add_command); LayoutInflater inflater = getActivity().getLayoutInflater(); View view = inflater.inflate(R.layout.addcommanddialog, null); nameField = (EditText) view.findViewById(R.id.addcommand_name); commandField = (EditText) view.findViewById(R.id.addcommand_command); builder.setView(view); - builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { + builder.setPositiveButton(R.string.ok, (dialog, id) -> { - if (getActivity() instanceof RunCommandActivity) { + if (getActivity() instanceof RunCommandActivity) { - String name = nameField.getText().toString(); - String command = commandField.getText().toString(); + String name = nameField.getText().toString(); + String command = commandField.getText().toString(); - ((RunCommandActivity) getActivity()).dialogResult(name, command); - } + ((RunCommandActivity) getActivity()).dialogResult(name, command); } }); - builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - } + builder.setNegativeButton(R.string.cancel, (dialog, id) -> { }); return builder.create(); } } diff --git a/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandActivity.java b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandActivity.java index ab233cbb..1580a142 100644 --- a/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandActivity.java +++ b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandActivity.java @@ -1,188 +1,157 @@ /* * 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.os.Bundle; import android.support.design.widget.FloatingActionButton; import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.view.View; import android.widget.AdapterView; import android.widget.ListView; import android.widget.TextView; 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 java.util.Comparator; public class RunCommandActivity extends AppCompatActivity { private String deviceId; - private final RunCommandPlugin.CommandsChangedCallback commandsChangedCallback = new RunCommandPlugin.CommandsChangedCallback() { - @Override - public void update() { - updateView(); - } - }; + private final RunCommandPlugin.CommandsChangedCallback commandsChangedCallback = this::updateView; private void updateView() { - BackgroundService.RunCommand(this, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(final BackgroundService 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.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; + } - runOnUiThread(new Runnable() { - @Override - public void run() { - ListView view = (ListView) findViewById(R.id.runcommandslist); - - final ArrayList 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, new Comparator() { - @Override - public int compare(ListAdapter.Item lhs, ListAdapter.Item 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(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView adapterView, View view, int i, long l) { - CommandEntry entry = (CommandEntry) commandItems.get(i); - plugin.runCommand(entry.getKey()); - } - }); - - - TextView explanation = (TextView) 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); + runOnUiThread(() -> { + ListView view = (ListView) findViewById(R.id.runcommandslist); + + final ArrayList 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 = (TextView) 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 = (FloatingActionButton) findViewById(R.id.add_command_button); addCommandButton.setVisibility(canAddCommands ? View.VISIBLE : View.GONE); - addCommandButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - new AddCommandDialog().show(getSupportFragmentManager(), "addcommanddialog"); - } - }); + addCommandButton.setOnClickListener(view -> new AddCommandDialog().show(getSupportFragmentManager(), "addcommanddialog")); updateView(); } @Override protected void onResume() { super.onResume(); - BackgroundService.RunCommand(this, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(final BackgroundService service) { + 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; - } - plugin.addCommandsUpdatedCallback(commandsChangedCallback); + 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; } + plugin.addCommandsUpdatedCallback(commandsChangedCallback); }); } @Override protected void onPause() { super.onPause(); - BackgroundService.RunCommand(this, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(final BackgroundService service) { + 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; - } - plugin.removeCommandsUpdatedCallback(commandsChangedCallback); + 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; } + plugin.removeCommandsUpdatedCallback(commandsChangedCallback); }); } public void dialogResult(final String cmdName, final String cmdCmd) { - BackgroundService.RunCommand(this, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - Device device = service.getDevice(deviceId); - RunCommandPlugin plugin = device.getPlugin(RunCommandPlugin.class); - if(!cmdName.isEmpty() && !cmdCmd.isEmpty()) { - plugin.addCommand(cmdName, cmdCmd); - } + BackgroundService.RunCommand(this, service -> { + Device device = service.getDevice(deviceId); + RunCommandPlugin plugin = device.getPlugin(RunCommandPlugin.class); + if(!cmdName.isEmpty() && !cmdCmd.isEmpty()) { + plugin.addCommand(cmdName, cmdCmd); } }); } } diff --git a/src/org/kde/kdeconnect/Plugins/SharePlugin/SendFileActivity.java b/src/org/kde/kdeconnect/Plugins/SharePlugin/SendFileActivity.java index 953ce824..10c34710 100644 --- a/src/org/kde/kdeconnect/Plugins/SharePlugin/SendFileActivity.java +++ b/src/org/kde/kdeconnect/Plugins/SharePlugin/SendFileActivity.java @@ -1,113 +1,110 @@ /* * 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.support.v7.app.AppCompatActivity; import android.util.Log; import android.widget.Toast; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.UserInterface.ThemeUtil; import org.kde.kdeconnect_tp.R; import java.util.ArrayList; public class SendFileActivity extends AppCompatActivity { 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.RunCommand(this, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - Device device = service.getDevice(mDeviceId); - if (device == null) { - Log.e("SendFileActivity", "Device is null"); - finish(); - return; - } - SharePlugin.queuedSendUriList(getApplicationContext(), device, uris); + BackgroundService.RunCommand(this, service -> { + Device device = service.getDevice(mDeviceId); + if (device == null) { + Log.e("SendFileActivity", "Device is null"); + finish(); + return; } + SharePlugin.queuedSendUriList(getApplicationContext(), device, 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 d9265d4a..02cc5295 100644 --- a/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java +++ b/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java @@ -1,223 +1,179 @@ /* * 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 . -*/ + * along with this program. If not, see . + */ package org.kde.kdeconnect.Plugins.SharePlugin; import android.content.Intent; import android.os.Bundle; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.app.ActionBar; import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; 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; 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, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - service.onNetworkChange(); - } - }); + BackgroundService.RunCommand(ShareActivity.this, BackgroundService::onNetworkChange); mSwipeRefreshLayout.setRefreshing(true); - new Thread(new Runnable() { - @Override - public void run() { - try { - Thread.sleep(1500); - } catch (InterruptedException ignored) { - } - runOnUiThread(new Runnable() { - @Override - public void run() { - mSwipeRefreshLayout.setRefreshing(false); - } - }); + 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, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(final BackgroundService service) { + BackgroundService.RunCommand(this, service -> { - Collection devices = service.getDevices().values(); - final ArrayList devicesList = new ArrayList<>(); - final ArrayList items = new ArrayList<>(); + Collection devices = service.getDevices().values(); + final ArrayList devicesList = new ArrayList<>(); + final ArrayList items = new ArrayList<>(); - items.add(new SectionItem(getString(R.string.share_to))); + items.add(new SectionItem(getString(R.string.share_to))); - for (Device d : devices) { - if (d.isReachable() && d.isPaired()) { - devicesList.add(d); - items.add(new EntryItem(d.getName())); - } + for (Device d : devices) { + if (d.isReachable() && d.isPaired()) { + devicesList.add(d); + items.add(new EntryItem(d.getName())); } + } + + runOnUiThread(() -> { + ListView list = (ListView) findViewById(R.id.devices_list); + list.setAdapter(new ListAdapter(ShareActivity.this, items)); + list.setOnItemClickListener((adapterView, view, i, l) -> { - runOnUiThread(new Runnable() { - @Override - public void run() { - ListView list = (ListView) findViewById(R.id.devices_list); - list.setAdapter(new ListAdapter(ShareActivity.this, items)); - list.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView adapterView, View view, int i, long l) { - - Device device = devicesList.get(i - 1); //NOTE: -1 because of the title! - SharePlugin.share(intent, device); - finish(); - } - }); - } + Device device = devicesList.get(i - 1); //NOTE: -1 because of the title! + SharePlugin.share(intent, device); + finish(); }); + }); - } }); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ThemeUtil.setUserPreferredTheme(this); setContentView(R.layout.devices_list); ActionBar actionBar = getSupportActionBar(); mSwipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.refresh_list_layout); mSwipeRefreshLayout.setOnRefreshListener( - new SwipeRefreshLayout.OnRefreshListener() { - @Override - public void onRefresh() { - updateComputerListAction(); - } - } + 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.RunCommand(this, new BackgroundService.InstanceCallback() { - - @Override - public void onServiceStart(BackgroundService service) { - Log.d("DirectShare", "sharing to " + service.getDevice(deviceId).getName()); - Device device = service.getDevice(deviceId); - if (device.isReachable() && device.isPaired()) { - SharePlugin.share(intent, device); - } - finish(); + BackgroundService.RunCommand(this, service -> { + Log.d("DirectShare", "sharing to " + service.getDevice(deviceId).getName()); + Device device = service.getDevice(deviceId); + if (device.isReachable() && device.isPaired()) { + SharePlugin.share(intent, device); } + finish(); }); } else { BackgroundService.addGuiInUseCounter(this); - BackgroundService.RunCommand(this, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - service.onNetworkChange(); - service.addDeviceListChangedCallback("ShareActivity", new BackgroundService.DeviceListChangedCallback() { - @Override - public void onDeviceListChanged() { - updateComputerList(); - } - }); - } + BackgroundService.RunCommand(this, service -> { + service.onNetworkChange(); + service.addDeviceListChangedCallback("ShareActivity", this::updateComputerList); }); updateComputerList(); } } @Override protected void onStop() { - BackgroundService.RunCommand(this, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - service.removeDeviceListChangedCallback("ShareActivity"); - } - }); + BackgroundService.RunCommand(this, service -> service.removeDeviceListChangedCallback("ShareActivity")); BackgroundService.removeGuiInUseCounter(this); super.onStop(); } } diff --git a/src/org/kde/kdeconnect/Plugins/SharePlugin/SharePlugin.java b/src/org/kde/kdeconnect/Plugins/SharePlugin/SharePlugin.java index 38883e03..cb3b66dc 100644 --- a/src/org/kde/kdeconnect/Plugins/SharePlugin/SharePlugin.java +++ b/src/org/kde/kdeconnect/Plugins/SharePlugin/SharePlugin.java @@ -1,470 +1,464 @@ /* * 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.Manifest; import android.app.Activity; import android.app.DownloadManager; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.ClipboardManager; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.database.Cursor; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.provider.MediaStore; import android.support.v4.app.NotificationCompat; import android.support.v4.app.TaskStackBuilder; import android.support.v4.content.ContextCompat; import android.support.v4.provider.DocumentFile; import android.util.Log; import android.widget.Toast; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Helpers.FilesHelper; import org.kde.kdeconnect.Helpers.MediaStoreHelper; import org.kde.kdeconnect.Helpers.NotificationHelper; import org.kde.kdeconnect.NetworkPacket; import org.kde.kdeconnect.Plugins.Plugin; import org.kde.kdeconnect.UserInterface.SettingsActivity; import org.kde.kdeconnect_tp.R; import java.io.File; import java.io.FileNotFoundException; import java.io.InputStream; import java.io.OutputStream; import java.net.URL; import java.util.ArrayList; public class SharePlugin extends Plugin { public final static String PACKET_TYPE_SHARE_REQUEST = "kdeconnect.share.request"; final static boolean openUrlsDirectly = true; @Override public boolean onCreate() { optionalPermissionExplanation = R.string.share_optional_permission_explanation; return true; } @Override public String getDisplayName() { return context.getResources().getString(R.string.pref_plugin_sharereceiver); } @Override public Drawable getIcon() { return ContextCompat.getDrawable(context, R.drawable.share_plugin_action); } @Override public String getDescription() { return context.getResources().getString(R.string.pref_plugin_sharereceiver_desc); } @Override public boolean hasMainActivity() { return true; } @Override public String getActionName() { return context.getString(R.string.send_files); } @Override public void startMainActivity(Activity parentActivity) { Intent intent = new Intent(parentActivity, SendFileActivity.class); intent.putExtra("deviceId", device.getDeviceId()); parentActivity.startActivity(intent); } @Override public boolean hasSettings() { return true; } @Override public boolean onPacketReceived(NetworkPacket np) { try { if (np.hasPayload()) { Log.i("SharePlugin", "hasPayload"); if (isPermissionGranted(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { receiveFile(np); } else { Log.i("SharePlugin", "no Permission for Storage"); } } else if (np.has("text")) { Log.i("SharePlugin", "hasText"); receiveText(np); } else if (np.has("url")) { receiveUrl(np); } else { Log.e("SharePlugin", "Error: Nothing attached!"); } } catch (Exception e) { Log.e("SharePlugin", "Exception"); e.printStackTrace(); } return true; } private void receiveUrl(NetworkPacket np) { String url = np.getString("url"); Log.i("SharePlugin", "hasUrl: " + url); Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); if (openUrlsDirectly) { context.startActivity(browserIntent); } else { Resources res = context.getResources(); TaskStackBuilder stackBuilder = TaskStackBuilder.create(context); stackBuilder.addNextIntent(browserIntent); PendingIntent resultPendingIntent = stackBuilder.getPendingIntent( 0, PendingIntent.FLAG_UPDATE_CURRENT ); Notification noti = new NotificationCompat.Builder(context) .setContentTitle(res.getString(R.string.received_url_title, device.getName())) .setContentText(res.getString(R.string.received_url_text, url)) .setContentIntent(resultPendingIntent) .setTicker(res.getString(R.string.received_url_title, device.getName())) .setSmallIcon(R.drawable.ic_notification) .setAutoCancel(true) .setDefaults(Notification.DEFAULT_ALL) .build(); NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); NotificationHelper.notifyCompat(notificationManager, (int) System.currentTimeMillis(), noti); } } private void receiveText(NetworkPacket np) { String text = np.getString("text"); if (Build.VERSION.SDK_INT >= 11) { ClipboardManager cm = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); cm.setText(text); } else { android.text.ClipboardManager clipboard = (android.text.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); clipboard.setText(text); } Toast.makeText(context, R.string.shareplugin_text_saved, Toast.LENGTH_LONG).show(); } private void receiveFile(NetworkPacket np) { final InputStream input = np.getPayload(); final long fileLength = np.getPayloadSize(); final String originalFilename = np.getString("filename", Long.toString(System.currentTimeMillis())); //We need to check for already existing files only when storing in the default path. //User-defined paths use the new Storage Access Framework that already handles this. final boolean customDestination = ShareSettingsActivity.isCustomDestinationEnabled(context); final String defaultPath = ShareSettingsActivity.getDefaultDestinationDirectory().getAbsolutePath(); final String filename = customDestination ? originalFilename : FilesHelper.findNonExistingNameForNewFile(defaultPath, originalFilename); String displayName = FilesHelper.getFileNameWithoutExt(filename); final String mimeType = FilesHelper.getMimeTypeFromFile(filename); if ("*/*".equals(mimeType)) { displayName = filename; } final DocumentFile destinationFolderDocument = ShareSettingsActivity.getDestinationDirectory(context); final DocumentFile destinationDocument = destinationFolderDocument.createFile(mimeType, displayName); final OutputStream destinationOutput; try { destinationOutput = context.getContentResolver().openOutputStream(destinationDocument.getUri()); } catch (FileNotFoundException e) { e.printStackTrace(); return; } final Uri destinationUri = destinationDocument.getUri(); final ShareNotification notification = new ShareNotification(device, filename); notification.show(); - new Thread(new Runnable() { - @Override - public void run() { - try { - byte data[] = new byte[4096]; - long progress = 0, prevProgressPercentage = -1; - int count; - long lastUpdate = 0; - while ((count = input.read(data)) >= 0) { - progress += count; - destinationOutput.write(data, 0, count); - if (fileLength > 0) { - if (progress >= fileLength) break; - long progressPercentage = (progress * 100 / fileLength); - if (progressPercentage != prevProgressPercentage && - System.currentTimeMillis() - lastUpdate > 100) { - prevProgressPercentage = progressPercentage; - lastUpdate = System.currentTimeMillis(); - - notification.setProgress((int) progressPercentage); - notification.show(); - } + new Thread(() -> { + try { + byte data[] = new byte[4096]; + long progress = 0, prevProgressPercentage = -1; + int count; + long lastUpdate = 0; + while ((count = input.read(data)) >= 0) { + progress += count; + destinationOutput.write(data, 0, count); + if (fileLength > 0) { + if (progress >= fileLength) break; + long progressPercentage = (progress * 100 / fileLength); + if (progressPercentage != prevProgressPercentage && + System.currentTimeMillis() - lastUpdate > 100) { + prevProgressPercentage = progressPercentage; + lastUpdate = System.currentTimeMillis(); + + notification.setProgress((int) progressPercentage); + notification.show(); } - //else Log.e("SharePlugin", "Infinite loop? :D"); } + //else Log.e("SharePlugin", "Infinite loop? :D"); + } - destinationOutput.flush(); + destinationOutput.flush(); - Log.i("SharePlugin", "Transfer finished: " + destinationUri.getPath()); + Log.i("SharePlugin", "Transfer finished: " + destinationUri.getPath()); - //Update the notification and allow to open the file from it - notification.setFinished(true); - notification.setURI(destinationUri, mimeType); - notification.show(); + //Update the notification and allow to open the file from it + notification.setFinished(true); + notification.setURI(destinationUri, mimeType); + notification.show(); - if (!customDestination && Build.VERSION.SDK_INT >= 12) { - Log.i("SharePlugin", "Adding to downloads"); - DownloadManager manager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); - manager.addCompletedDownload(destinationUri.getLastPathSegment(), device.getName(), true, mimeType, destinationUri.getPath(), fileLength, false); - } else { - //Make sure it is added to the Android Gallery anyway - MediaStoreHelper.indexFile(context, destinationUri); - } + if (!customDestination && Build.VERSION.SDK_INT >= 12) { + Log.i("SharePlugin", "Adding to downloads"); + DownloadManager manager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + manager.addCompletedDownload(destinationUri.getLastPathSegment(), device.getName(), true, mimeType, destinationUri.getPath(), fileLength, false); + } else { + //Make sure it is added to the Android Gallery anyway + MediaStoreHelper.indexFile(context, destinationUri); + } + } catch (Exception e) { + Log.e("SharePlugin", "Receiver thread exception"); + e.printStackTrace(); + notification.setFinished(false); + notification.show(); + } finally { + try { + destinationOutput.close(); + } catch (Exception e) { + } + try { + input.close(); } catch (Exception e) { - Log.e("SharePlugin", "Receiver thread exception"); - e.printStackTrace(); - notification.setFinished(false); - notification.show(); - } finally { - try { - destinationOutput.close(); - } catch (Exception e) { - } - try { - input.close(); - } catch (Exception e) { - } } } }).start(); } @Override public void startPreferencesActivity(SettingsActivity parentActivity) { Intent intent = new Intent(parentActivity, ShareSettingsActivity.class); intent.putExtra("plugin_display_name", getDisplayName()); intent.putExtra("plugin_key", getPluginKey()); parentActivity.startActivity(intent); } static void queuedSendUriList(Context context, final Device device, final ArrayList uriList) { //Read all the data early, as we only have permissions to do it while the activity is alive final ArrayList toSend = new ArrayList<>(); for (Uri uri : uriList) { toSend.add(uriToNetworkPacket(context, uri)); } //Callback that shows a progress notification final NotificationUpdateCallback notificationUpdateCallback = new NotificationUpdateCallback(context, device, toSend); //Do the sending in background - new Thread(new Runnable() { - @Override - public void run() { - //Actually send the files - try { - for (NetworkPacket np : toSend) { - boolean success = device.sendPacketBlocking(np, notificationUpdateCallback); - if (!success) { - Log.e("SharePlugin", "Error sending files"); - return; - } + new Thread(() -> { + //Actually send the files + try { + for (NetworkPacket np : toSend) { + boolean success = device.sendPacketBlocking(np, notificationUpdateCallback); + if (!success) { + Log.e("SharePlugin", "Error sending files"); + return; } - } catch (Exception e) { - e.printStackTrace(); } + } catch (Exception e) { + e.printStackTrace(); } }).start(); } //Create the network package from the URI private static NetworkPacket uriToNetworkPacket(final Context context, final Uri uri) { try { ContentResolver cr = context.getContentResolver(); InputStream inputStream = cr.openInputStream(uri); NetworkPacket np = new NetworkPacket(PACKET_TYPE_SHARE_REQUEST); long size = -1; if (uri.getScheme().equals("file")) { // file:// is a non media uri, so we cannot query the ContentProvider np.set("filename", uri.getLastPathSegment()); try { size = new File(uri.getPath()).length(); } catch (Exception e) { Log.e("SendFileActivity", "Could not obtain file size"); e.printStackTrace(); } } else { // Probably a content:// uri, so we query the Media content provider Cursor cursor = null; try { String[] proj = {MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.SIZE, MediaStore.MediaColumns.DISPLAY_NAME}; cursor = cr.query(uri, proj, null, null, null); int column_index = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA); cursor.moveToFirst(); String path = cursor.getString(column_index); np.set("filename", Uri.parse(path).getLastPathSegment()); size = new File(path).length(); } catch (Exception unused) { Log.w("SendFileActivity", "Could not resolve media to a file, trying to get info as media"); try { int column_index = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME); cursor.moveToFirst(); String name = cursor.getString(column_index); np.set("filename", name); } catch (Exception e) { e.printStackTrace(); Log.e("SendFileActivity", "Could not obtain file name"); } try { int column_index = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE); cursor.moveToFirst(); //For some reason this size can differ from the actual file size! size = cursor.getInt(column_index); } catch (Exception e) { Log.e("SendFileActivity", "Could not obtain file size"); e.printStackTrace(); } } finally { try { cursor.close(); } catch (Exception e) { } } } np.setPayload(inputStream, size); return np; } catch (Exception e) { Log.e("SendFileActivity", "Exception sending files"); e.printStackTrace(); return null; } } public static void share(Intent intent, Device device) { Bundle extras = intent.getExtras(); if (extras != null) { if (extras.containsKey(Intent.EXTRA_STREAM)) { try { ArrayList uriList; if (!Intent.ACTION_SEND.equals(intent.getAction())) { uriList = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); } else { Uri uri = extras.getParcelable(Intent.EXTRA_STREAM); uriList = new ArrayList<>(); uriList.add(uri); } SharePlugin.queuedSendUriList(device.getContext(), device, uriList); } catch (Exception e) { Log.e("ShareActivity", "Exception"); e.printStackTrace(); } } else if (extras.containsKey(Intent.EXTRA_TEXT)) { String text = extras.getString(Intent.EXTRA_TEXT); String subject = extras.getString(Intent.EXTRA_SUBJECT); //Hack: Detect shared youtube videos, so we can open them in the browser instead of as text if (subject != null && subject.endsWith("YouTube")) { int index = text.indexOf(": http://youtu.be/"); if (index > 0) { text = text.substring(index + 2); //Skip ": " } } boolean isUrl; try { new URL(text); isUrl = true; } catch (Exception e) { isUrl = false; } NetworkPacket np = new NetworkPacket(SharePlugin.PACKET_TYPE_SHARE_REQUEST); if (isUrl) { np.set("url", text); } else { np.set("text", text); } device.sendPacket(np); } } } @Override public String[] getSupportedPacketTypes() { return new String[]{PACKET_TYPE_SHARE_REQUEST}; } @Override public String[] getOutgoingPacketTypes() { return new String[]{PACKET_TYPE_SHARE_REQUEST}; } @Override public String[] getOptionalPermissions() { return new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}; } } diff --git a/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareSettingsActivity.java b/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareSettingsActivity.java index b20ab99e..483f5cec 100644 --- a/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareSettingsActivity.java +++ b/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareSettingsActivity.java @@ -1,125 +1,119 @@ package org.kde.kdeconnect.Plugins.SharePlugin; import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.preference.CheckBoxPreference; import android.preference.Preference; import android.preference.PreferenceManager; import android.support.v4.provider.DocumentFile; import android.util.Log; import org.kde.kdeconnect.UserInterface.PluginSettingsActivity; import java.io.File; public class ShareSettingsActivity extends PluginSettingsActivity { private final static String PREFERENCE_CUSTOMIZE_DESTINATION = "share_destination_custom"; private final static String PREFERENCE_DESTINATION = "share_destination_folder_uri"; private static final int RESULT_PICKER = Activity.RESULT_FIRST_USER; private Preference filePicker; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); final CheckBoxPreference customDownloads = (CheckBoxPreference) findPreference("share_destination_custom"); filePicker = findPreference("share_destination_folder_preference"); if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)) { - customDownloads.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - updateFilePickerStatus((Boolean) newValue); - return true; - } + customDownloads.setOnPreferenceChangeListener((preference, newValue) -> { + updateFilePickerStatus((Boolean) newValue); + return true; }); - filePicker.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); - startActivityForResult(intent, RESULT_PICKER); - return true; - } + filePicker.setOnPreferenceClickListener(preference -> { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + startActivityForResult(intent, RESULT_PICKER); + return true; }); } else { customDownloads.setEnabled(false); filePicker.setEnabled(false); } boolean customized = PreferenceManager.getDefaultSharedPreferences(this).getBoolean(PREFERENCE_CUSTOMIZE_DESTINATION, false); updateFilePickerStatus(customized); } void updateFilePickerStatus(boolean enabled) { filePicker.setEnabled(enabled); String path = PreferenceManager.getDefaultSharedPreferences(this).getString(PREFERENCE_DESTINATION, null); if (enabled && path != null) { filePicker.setSummary(Uri.parse(path).getPath()); } else { filePicker.setSummary(getDefaultDestinationDirectory().getAbsolutePath()); } } public static File getDefaultDestinationDirectory() { return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); } public static boolean isCustomDestinationEnabled(Context context) { return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(PREFERENCE_CUSTOMIZE_DESTINATION, false); } //Will return the appropriate directory, whether it is customized or not public static DocumentFile getDestinationDirectory(Context context) { if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean(PREFERENCE_CUSTOMIZE_DESTINATION, false)) { String path = PreferenceManager.getDefaultSharedPreferences(context).getString(PREFERENCE_DESTINATION, null); if (path != null) { //There should be no way to enter here on api level < kitkat DocumentFile treeDocumentFile = DocumentFile.fromTreeUri(context, Uri.parse(path)); if (treeDocumentFile.canWrite()) { //Checks for FLAG_DIR_SUPPORTS_CREATE on directories return treeDocumentFile; } else { //Maybe permission was revoked Log.w("SharePlugin", "Share destination is not writable, falling back to default path."); } } } try { getDefaultDestinationDirectory().mkdirs(); } catch (Exception e) { e.printStackTrace(); } return DocumentFile.fromFile(getDefaultDestinationDirectory()); } @TargetApi(Build.VERSION_CODES.KITKAT) @Override public void onActivityResult(int requestCode, int resultCode, Intent resultData) { if (requestCode == RESULT_PICKER && resultCode == Activity.RESULT_OK && resultData != null) { Uri uri = resultData.getData(); getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); Preference filePicker = findPreference("share_destination_folder_preference"); filePicker.setSummary(uri.getPath()); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); prefs.edit().putString(PREFERENCE_DESTINATION, uri.toString()).apply(); } } } diff --git a/src/org/kde/kdeconnect/UserInterface/CustomDevicesActivity.java b/src/org/kde/kdeconnect/UserInterface/CustomDevicesActivity.java index 27c2e3a5..2f9b024b 100644 --- a/src/org/kde/kdeconnect/UserInterface/CustomDevicesActivity.java +++ b/src/org/kde/kdeconnect/UserInterface/CustomDevicesActivity.java @@ -1,189 +1,171 @@ /* * Copyright 2014 Achilleas Koutsou * * 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.UserInterface; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.v7.app.AppCompatActivity; import android.text.TextUtils; import android.view.KeyEvent; import android.view.View; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.EditText; import android.widget.ListView; import android.widget.TextView; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect_tp.R; import java.util.ArrayList; import java.util.Collections; public class CustomDevicesActivity extends AppCompatActivity { public static final String KEY_CUSTOM_DEVLIST_PREFERENCE = "device_list_preference"; private static final String IP_DELIM = ","; private ListView list; private ArrayList ipAddressList = new ArrayList<>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); initializeDeviceList(this); ThemeUtil.setUserPreferredTheme(this); setContentView(R.layout.custom_ip_list); list = (ListView) findViewById(android.R.id.list); list.setOnItemClickListener(onClickListener); list.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, ipAddressList)); - findViewById(android.R.id.button1).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - addNewDevice(); - } - }); + findViewById(android.R.id.button1).setOnClickListener(v -> addNewDevice()); EditText ipEntryBox = (EditText) findViewById(R.id.ip_edittext); - ipEntryBox.setOnEditorActionListener(new TextView.OnEditorActionListener() { - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - if (actionId == EditorInfo.IME_ACTION_SEND) { - addNewDevice(); - return true; - } - return false; + ipEntryBox.setOnEditorActionListener((v, actionId, event) -> { + if (actionId == EditorInfo.IME_ACTION_SEND) { + addNewDevice(); + return true; } + return false; }); } boolean dialogAlreadyShown = false; - private AdapterView.OnItemClickListener onClickListener = new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, final int position, final long id) { + private AdapterView.OnItemClickListener onClickListener = (parent, view, position, id) -> { - if (dialogAlreadyShown) { - return; - } + if (dialogAlreadyShown) { + return; + } - // remove touched item after confirmation - DialogInterface.OnClickListener confirmationListener = new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - switch (which) { - case DialogInterface.BUTTON_POSITIVE: - ipAddressList.remove(position); - saveList(); - break; - case DialogInterface.BUTTON_NEGATIVE: - break; - } - } - }; - - AlertDialog.Builder builder = new AlertDialog.Builder(CustomDevicesActivity.this); - builder.setMessage("Delete " + ipAddressList.get(position) + " ?"); - builder.setPositiveButton("Yes", confirmationListener); - builder.setNegativeButton("No", confirmationListener); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { //DismissListener - dialogAlreadyShown = true; - builder.setOnDismissListener(new DialogInterface.OnDismissListener() { - @Override - public void onDismiss(DialogInterface dialog) { - dialogAlreadyShown = false; - } - }); + // remove touched item after confirmation + DialogInterface.OnClickListener confirmationListener = (dialog, which) -> { + switch (which) { + case DialogInterface.BUTTON_POSITIVE: + ipAddressList.remove(position); + saveList(); + break; + case DialogInterface.BUTTON_NEGATIVE: + break; } + }; - builder.show(); + AlertDialog.Builder builder = new AlertDialog.Builder(CustomDevicesActivity.this); + builder.setMessage("Delete " + ipAddressList.get(position) + " ?"); + builder.setPositiveButton("Yes", confirmationListener); + builder.setNegativeButton("No", confirmationListener); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { //DismissListener + dialogAlreadyShown = true; + builder.setOnDismissListener(dialog -> dialogAlreadyShown = false); } + + builder.show(); }; private void addNewDevice() { EditText ipEntryBox = (EditText) findViewById(R.id.ip_edittext); String enteredText = ipEntryBox.getText().toString().trim(); if (!enteredText.isEmpty()) { // don't add empty string (after trimming) ipAddressList.add(enteredText); } saveList(); // clear entry box ipEntryBox.setText(""); InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); View focus = getCurrentFocus(); if (focus != null && inputManager != null) { inputManager.hideSoftInputFromWindow(focus.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); } } private void saveList() { String serialized = TextUtils.join(IP_DELIM, ipAddressList); PreferenceManager.getDefaultSharedPreferences(CustomDevicesActivity.this).edit().putString( KEY_CUSTOM_DEVLIST_PREFERENCE, serialized).apply(); ((ArrayAdapter) list.getAdapter()).notifyDataSetChanged(); } public static ArrayList deserializeIpList(String serialized) { ArrayList ipList = new ArrayList<>(); Collections.addAll(ipList, serialized.split(IP_DELIM)); return ipList; } private void initializeDeviceList(Context context) { String deviceListPrefs = PreferenceManager.getDefaultSharedPreferences(context).getString( KEY_CUSTOM_DEVLIST_PREFERENCE, ""); if (deviceListPrefs.isEmpty()) { PreferenceManager.getDefaultSharedPreferences(context).edit().putString( KEY_CUSTOM_DEVLIST_PREFERENCE, deviceListPrefs).apply(); } else { ipAddressList = deserializeIpList(deviceListPrefs); } } @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/UserInterface/DeviceFragment.java b/src/org/kde/kdeconnect/UserInterface/DeviceFragment.java index f66de81d..982a21df 100644 --- a/src/org/kde/kdeconnect/UserInterface/DeviceFragment.java +++ b/src/org/kde/kdeconnect/UserInterface/DeviceFragment.java @@ -1,544 +1,477 @@ /* * 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.UserInterface; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; import android.support.v4.app.Fragment; import android.util.Log; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.ListView; import android.widget.TextView; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Helpers.NetworkHelper; import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper; import org.kde.kdeconnect.Plugins.Plugin; import org.kde.kdeconnect.UserInterface.List.CustomItem; import org.kde.kdeconnect.UserInterface.List.ListAdapter; import org.kde.kdeconnect.UserInterface.List.PluginItem; import org.kde.kdeconnect.UserInterface.List.SmallEntryItem; import org.kde.kdeconnect_tp.R; import java.util.ArrayList; import java.util.Collection; import java.util.ConcurrentModificationException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * Main view. Displays the current device and its plugins */ public class DeviceFragment extends Fragment { static final String ARG_DEVICE_ID = "deviceId"; static final String ARG_FROM_DEVICE_LIST = "fromDeviceList"; View rootView; static String mDeviceId; //Static because if we get here by using the back button in the action bar, the extra deviceId will not be set. Device device; MainActivity mActivity; ArrayList pluginListItems; public DeviceFragment() { } public DeviceFragment(String deviceId, boolean fromDeviceList) { Bundle args = new Bundle(); args.putString(ARG_DEVICE_ID, deviceId); args.putBoolean(ARG_FROM_DEVICE_LIST, fromDeviceList); this.setArguments(args); } public DeviceFragment(String deviceId, MainActivity activity) { this.mActivity = activity; Bundle args = new Bundle(); args.putString(ARG_DEVICE_ID, deviceId); this.setArguments(args); } @Override public void onAttach(Activity activity) { super.onAttach(activity); mActivity = ((MainActivity) getActivity()); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { rootView = inflater.inflate(R.layout.activity_device, container, false); final String deviceId = getArguments().getString(ARG_DEVICE_ID); if (deviceId != null) { mDeviceId = deviceId; } setHasOptionsMenu(true); //Log.e("DeviceFragment", "device: " + deviceId); - BackgroundService.RunCommand(mActivity, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - device = service.getDevice(mDeviceId); - if (device == null) { - Log.e("DeviceFragment", "Trying to display a device fragment but the device is not present"); - mActivity.onDeviceSelected(null); - return; - } + BackgroundService.RunCommand(mActivity, service -> { + device = service.getDevice(mDeviceId); + if (device == null) { + Log.e("DeviceFragment", "Trying to display a device fragment but the device is not present"); + mActivity.onDeviceSelected(null); + return; + } - mActivity.getSupportActionBar().setTitle(device.getName()); + mActivity.getSupportActionBar().setTitle(device.getName()); - device.addPairingCallback(pairingCallback); - device.addPluginsChangedListener(pluginsChangedListener); + device.addPairingCallback(pairingCallback); + device.addPluginsChangedListener(pluginsChangedListener); - refreshUI(); + refreshUI(); - } }); final Button pairButton = (Button) rootView.findViewById(R.id.pair_button); - pairButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - pairButton.setVisibility(View.GONE); - ((TextView) rootView.findViewById(R.id.pair_message)).setText(""); - rootView.findViewById(R.id.pair_progress).setVisibility(View.VISIBLE); - BackgroundService.RunCommand(mActivity, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - device = service.getDevice(deviceId); - if (device == null) return; - device.requestPairing(); - } - }); - } + pairButton.setOnClickListener(view -> { + pairButton.setVisibility(View.GONE); + ((TextView) rootView.findViewById(R.id.pair_message)).setText(""); + rootView.findViewById(R.id.pair_progress).setVisibility(View.VISIBLE); + BackgroundService.RunCommand(mActivity, service -> { + device = service.getDevice(deviceId); + if (device == null) return; + device.requestPairing(); + }); }); - rootView.findViewById(R.id.accept_button).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - BackgroundService.RunCommand(mActivity, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - if (device != null) { - device.acceptPairing(); - rootView.findViewById(R.id.pairing_buttons).setVisibility(View.GONE); - } - } - }); + rootView.findViewById(R.id.accept_button).setOnClickListener(view -> BackgroundService.RunCommand(mActivity, service -> { + if (device != null) { + device.acceptPairing(); + rootView.findViewById(R.id.pairing_buttons).setVisibility(View.GONE); } - }); + })); - rootView.findViewById(R.id.reject_button).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - BackgroundService.RunCommand(mActivity, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - if (device != null) { - //Remove listener so buttons don't show for a while before changing the view - device.removePluginsChangedListener(pluginsChangedListener); - device.removePairingCallback(pairingCallback); - device.rejectPairing(); - } - mActivity.onDeviceSelected(null); - } - }); + rootView.findViewById(R.id.reject_button).setOnClickListener(view -> BackgroundService.RunCommand(mActivity, service -> { + if (device != null) { + //Remove listener so buttons don't show for a while before changing the view + device.removePluginsChangedListener(pluginsChangedListener); + device.removePairingCallback(pairingCallback); + device.rejectPairing(); } - }); + mActivity.onDeviceSelected(null); + })); return rootView; } - final Device.PluginsChangedListener pluginsChangedListener = new Device.PluginsChangedListener() { - @Override - public void onPluginsChanged(final Device device) { - refreshUI(); - } - }; + final Device.PluginsChangedListener pluginsChangedListener = device -> refreshUI(); @Override public void onDestroyView() { - BackgroundService.RunCommand(mActivity, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - Device device = service.getDevice(mDeviceId); - if (device == null) return; - device.removePluginsChangedListener(pluginsChangedListener); - device.removePairingCallback(pairingCallback); - } + BackgroundService.RunCommand(mActivity, service -> { + Device device = service.getDevice(mDeviceId); + if (device == null) return; + device.removePluginsChangedListener(pluginsChangedListener); + device.removePairingCallback(pairingCallback); }); super.onDestroyView(); } @Override public void onPrepareOptionsMenu(Menu menu) { //Log.e("DeviceFragment", "onPrepareOptionsMenu"); super.onPrepareOptionsMenu(menu); menu.clear(); if (device == null) { return; } //Plugins button list final Collection plugins = device.getLoadedPlugins().values(); for (final Plugin p : plugins) { if (!p.displayInContextMenu()) { continue; } - menu.add(p.getActionName()).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - p.startMainActivity(mActivity); - return true; - } + menu.add(p.getActionName()).setOnMenuItemClickListener(item -> { + p.startMainActivity(mActivity); + return true; }); } - menu.add(R.string.device_menu_plugins).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem menuItem) { - Intent intent = new Intent(mActivity, SettingsActivity.class); - intent.putExtra("deviceId", mDeviceId); - startActivity(intent); - return true; - } + menu.add(R.string.device_menu_plugins).setOnMenuItemClickListener(menuItem -> { + Intent intent = new Intent(mActivity, SettingsActivity.class); + intent.putExtra("deviceId", mDeviceId); + startActivity(intent); + return true; }); if (device.isReachable()) { - menu.add(R.string.encryption_info_title).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem menuItem) { - Context context = mActivity; - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(context.getResources().getString(R.string.encryption_info_title)); - builder.setPositiveButton(context.getResources().getString(R.string.ok), new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - dialog.dismiss(); - } - }); + menu.add(R.string.encryption_info_title).setOnMenuItemClickListener(menuItem -> { + Context context = mActivity; + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(context.getResources().getString(R.string.encryption_info_title)); + builder.setPositiveButton(context.getResources().getString(R.string.ok), (dialog, id) -> dialog.dismiss()); - if (device.certificate == null) { - builder.setMessage(R.string.encryption_info_msg_no_ssl); - } else { - builder.setMessage(context.getResources().getString(R.string.my_device_fingerprint) + "\n" + SslHelper.getCertificateHash(SslHelper.certificate) + "\n\n" - + context.getResources().getString(R.string.remote_device_fingerprint) + "\n" + SslHelper.getCertificateHash(device.certificate)); - } - builder.create().show(); - return true; + if (device.certificate == null) { + builder.setMessage(R.string.encryption_info_msg_no_ssl); + } else { + builder.setMessage(context.getResources().getString(R.string.my_device_fingerprint) + "\n" + SslHelper.getCertificateHash(SslHelper.certificate) + "\n\n" + + context.getResources().getString(R.string.remote_device_fingerprint) + "\n" + SslHelper.getCertificateHash(device.certificate)); } + builder.create().show(); + return true; }); } if (device.isPaired()) { - menu.add(R.string.device_menu_unpair).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem menuItem) { - //Remove listener so buttons don't show for a while before changing the view - device.removePluginsChangedListener(pluginsChangedListener); - device.removePairingCallback(pairingCallback); - device.unpair(); - mActivity.onDeviceSelected(null); - return true; - } + menu.add(R.string.device_menu_unpair).setOnMenuItemClickListener(menuItem -> { + //Remove listener so buttons don't show for a while before changing the view + device.removePluginsChangedListener(pluginsChangedListener); + device.removePairingCallback(pairingCallback); + device.unpair(); + mActivity.onDeviceSelected(null); + return true; }); } } @Override public void onResume() { super.onResume(); getView().setFocusableInTouchMode(true); getView().requestFocus(); - getView().setOnKeyListener(new View.OnKeyListener() { - @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { - if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { - boolean fromDeviceList = getArguments().getBoolean(ARG_FROM_DEVICE_LIST, false); - // Handle back button so we go to the list of devices in case we came from there - if (fromDeviceList) { - mActivity.onDeviceSelected(null); - return true; - } + getView().setOnKeyListener((v, keyCode, event) -> { + if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { + boolean fromDeviceList = getArguments().getBoolean(ARG_FROM_DEVICE_LIST, false); + // Handle back button so we go to the list of devices in case we came from there + if (fromDeviceList) { + mActivity.onDeviceSelected(null); + return true; } - return false; } + return false; }); } void refreshUI() { //Log.e("DeviceFragment", "refreshUI"); if (device == null || rootView == null) { return; } //Once in-app, there is no point in keep displaying the notification if any device.hidePairingNotification(); mActivity.runOnUiThread(new Runnable() { @Override public void run() { if (device.isPairRequestedByPeer()) { ((TextView) rootView.findViewById(R.id.pair_message)).setText(R.string.pair_requested); rootView.findViewById(R.id.pairing_buttons).setVisibility(View.VISIBLE); rootView.findViewById(R.id.pair_progress).setVisibility(View.GONE); rootView.findViewById(R.id.pair_button).setVisibility(View.GONE); rootView.findViewById(R.id.pair_request).setVisibility(View.VISIBLE); } else { boolean paired = device.isPaired(); boolean reachable = device.isReachable(); boolean onData = NetworkHelper.isOnMobileNetwork(getContext()); rootView.findViewById(R.id.pairing_buttons).setVisibility(paired ? View.GONE : View.VISIBLE); rootView.findViewById(R.id.error_message_container).setVisibility((paired && !reachable) ? View.VISIBLE : View.GONE); rootView.findViewById(R.id.not_reachable_message).setVisibility((paired && !reachable && !onData) ? View.VISIBLE : View.GONE); rootView.findViewById(R.id.on_data_message).setVisibility((paired && !reachable && onData) ? View.VISIBLE : View.GONE); try { pluginListItems = new ArrayList<>(); //Plugins button list final Collection plugins = device.getLoadedPlugins().values(); for (final Plugin p : plugins) { if (!p.hasMainActivity()) continue; if (p.displayInContextMenu()) continue; - pluginListItems.add(new PluginItem(p, new View.OnClickListener() { - @Override - public void onClick(View v) { - p.startMainActivity(mActivity); - } - })); + pluginListItems.add(new PluginItem(p, v -> p.startMainActivity(mActivity))); } createPluginsList(device.getFailedPlugins(), R.string.plugins_failed_to_load, new PluginClickListener() { @Override void action() { plugin.getErrorDialog(mActivity).show(); } }); createPluginsList(device.getPluginsWithoutPermissions(), R.string.plugins_need_permission, new PluginClickListener() { @Override void action() { plugin.getPermissionExplanationDialog(mActivity).show(); } }); createPluginsList(device.getPluginsWithoutOptionalPermissions(), R.string.plugins_need_optional_permission, new PluginClickListener() { @Override void action() { plugin.getOptionalPermissionExplanationDialog(mActivity).show(); } }); ListView buttonsList = (ListView) rootView.findViewById(R.id.buttons_list); ListAdapter adapter = new ListAdapter(mActivity, pluginListItems); buttonsList.setAdapter(adapter); mActivity.invalidateOptionsMenu(); } catch (IllegalStateException e) { e.printStackTrace(); //Ignore: The activity was closed while we were trying to update it } catch (ConcurrentModificationException e) { Log.e("DeviceActivity", "ConcurrentModificationException"); this.run(); //Try again } } } }); } final Device.PairingCallback pairingCallback = new Device.PairingCallback() { @Override public void incomingRequest() { refreshUI(); } @Override public void pairingSuccessful() { refreshUI(); } @Override public void pairingFailed(final String error) { - mActivity.runOnUiThread(new Runnable() { - @Override - public void run() { - if (rootView == null) return; - ((TextView) rootView.findViewById(R.id.pair_message)).setText(error); - rootView.findViewById(R.id.pair_progress).setVisibility(View.GONE); - rootView.findViewById(R.id.pair_button).setVisibility(View.VISIBLE); - rootView.findViewById(R.id.pair_request).setVisibility(View.GONE); - refreshUI(); - } + mActivity.runOnUiThread(() -> { + if (rootView == null) return; + ((TextView) rootView.findViewById(R.id.pair_message)).setText(error); + rootView.findViewById(R.id.pair_progress).setVisibility(View.GONE); + rootView.findViewById(R.id.pair_button).setVisibility(View.VISIBLE); + rootView.findViewById(R.id.pair_request).setVisibility(View.GONE); + refreshUI(); }); } @Override public void unpaired() { - mActivity.runOnUiThread(new Runnable() { - @Override - public void run() { - if (rootView == null) return; - ((TextView) rootView.findViewById(R.id.pair_message)).setText(R.string.device_not_paired); - rootView.findViewById(R.id.pair_progress).setVisibility(View.GONE); - rootView.findViewById(R.id.pair_button).setVisibility(View.VISIBLE); - rootView.findViewById(R.id.pair_request).setVisibility(View.GONE); - refreshUI(); - } + mActivity.runOnUiThread(() -> { + if (rootView == null) return; + ((TextView) rootView.findViewById(R.id.pair_message)).setText(R.string.device_not_paired); + rootView.findViewById(R.id.pair_progress).setVisibility(View.GONE); + rootView.findViewById(R.id.pair_button).setVisibility(View.VISIBLE); + rootView.findViewById(R.id.pair_request).setVisibility(View.GONE); + refreshUI(); }); } }; public static void acceptPairing(final String devId, final MainActivity activity) { final DeviceFragment frag = new DeviceFragment(devId, activity); - BackgroundService.RunCommand(activity, new BackgroundService.InstanceCallback() { - public void onServiceStart(BackgroundService service) { - Device dev = service.getDevice(devId); - if (dev == null) { - Log.w("rejectPairing", "Device no longer exists: " + devId); - return; - } - activity.getSupportActionBar().setTitle(dev.getName()); + BackgroundService.RunCommand(activity, service -> { + Device dev = service.getDevice(devId); + if (dev == null) { + Log.w("rejectPairing", "Device no longer exists: " + devId); + return; + } + activity.getSupportActionBar().setTitle(dev.getName()); - dev.addPairingCallback(frag.pairingCallback); - dev.addPluginsChangedListener(frag.pluginsChangedListener); + dev.addPairingCallback(frag.pairingCallback); + dev.addPluginsChangedListener(frag.pluginsChangedListener); - frag.device = dev; - frag.device.acceptPairing(); + frag.device = dev; + frag.device.acceptPairing(); - frag.refreshUI(); + frag.refreshUI(); - } }); } public static void rejectPairing(final String devId, final MainActivity activity) { final DeviceFragment frag = new DeviceFragment(devId, activity); - BackgroundService.RunCommand(activity, new BackgroundService.InstanceCallback() { - public void onServiceStart(BackgroundService service) { - Device dev = service.getDevice(devId); - if (dev == null) { - Log.w("rejectPairing", "Device no longer exists: " + devId); - return; - } - activity.getSupportActionBar().setTitle(dev.getName()); + BackgroundService.RunCommand(activity, service -> { + Device dev = service.getDevice(devId); + if (dev == null) { + Log.w("rejectPairing", "Device no longer exists: " + devId); + return; + } + activity.getSupportActionBar().setTitle(dev.getName()); - dev.addPairingCallback(frag.pairingCallback); - dev.addPluginsChangedListener(frag.pluginsChangedListener); + dev.addPairingCallback(frag.pairingCallback); + dev.addPluginsChangedListener(frag.pluginsChangedListener); - frag.device = dev; + frag.device = dev; - //Remove listener so buttons don't show for a while before changing the view - frag.device.removePluginsChangedListener(frag.pluginsChangedListener); - frag.device.removePairingCallback(frag.pairingCallback); - frag.device.rejectPairing(); - activity.onDeviceSelected(null); + //Remove listener so buttons don't show for a while before changing the view + frag.device.removePluginsChangedListener(frag.pluginsChangedListener); + frag.device.removePairingCallback(frag.pairingCallback); + frag.device.rejectPairing(); + activity.onDeviceSelected(null); - frag.refreshUI(); - } + frag.refreshUI(); }); } void createPluginsList(ConcurrentHashMap plugins, int headerText, PluginClickListener onClickListener) { if (!plugins.isEmpty()) { TextView header = new TextView(mActivity); header.setPadding( 0, ((int) (28 * getResources().getDisplayMetrics().density)), 0, ((int) (8 * getResources().getDisplayMetrics().density)) ); header.setOnClickListener(null); header.setOnLongClickListener(null); header.setText(headerText); pluginListItems.add(new CustomItem(header)); for (Map.Entry entry : plugins.entrySet()) { String pluginKey = entry.getKey(); final Plugin plugin = entry.getValue(); if (device.isPluginEnabled(pluginKey)) { if (plugin == null) { pluginListItems.add(new SmallEntryItem(pluginKey)); } else { PluginClickListener listener = onClickListener.clone(); listener.plugin = plugin; pluginListItems.add(new SmallEntryItem(plugin.getDisplayName(), listener)); } } } } } private abstract class PluginClickListener implements View.OnClickListener, Cloneable { Plugin plugin; @Override public void onClick(View v) { action(); } @Override public PluginClickListener clone() { try { return (PluginClickListener) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return null; } abstract void action(); } } diff --git a/src/org/kde/kdeconnect/UserInterface/List/PairingDeviceItem.java b/src/org/kde/kdeconnect/UserInterface/List/PairingDeviceItem.java index 2cb77a8b..7bff491d 100644 --- a/src/org/kde/kdeconnect/UserInterface/List/PairingDeviceItem.java +++ b/src/org/kde/kdeconnect/UserInterface/List/PairingDeviceItem.java @@ -1,87 +1,82 @@ /* * 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.UserInterface.List; import android.view.LayoutInflater; import android.view.View; import android.widget.ImageView; import android.widget.TextView; import org.kde.kdeconnect.Device; import org.kde.kdeconnect_tp.R; public class PairingDeviceItem implements ListAdapter.Item { public interface Callback { void pairingClicked(Device d); } private final Callback callback; private final Device device; public PairingDeviceItem(Device device, Callback callback) { this.device = device; this.callback = callback; } public Device getDevice() { return this.device; } @Override public View inflateView(LayoutInflater layoutInflater) { final View v = layoutInflater.inflate(R.layout.list_item_with_icon_entry, null); ImageView icon = (ImageView) v.findViewById(R.id.list_item_entry_icon); icon.setImageDrawable(device.getIcon()); TextView titleView = (TextView) v.findViewById(R.id.list_item_entry_title); titleView.setText(device.getName()); if (device.compareProtocolVersion() != 0) { TextView summaryView = (TextView) v.findViewById(R.id.list_item_entry_summary); if (device.compareProtocolVersion() > 0) { summaryView.setText(R.string.protocol_version_newer); summaryView.setVisibility(View.VISIBLE); } else { //FIXME: Uncoment when we decide old versions are old enough to notify the user. summaryView.setVisibility(View.GONE); /* summaryView.setText(R.string.protocol_version_older); summaryView.setVisibility(View.VISIBLE); */ } } else { v.findViewById(R.id.list_item_entry_summary).setVisibility(View.GONE); } - v.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - callback.pairingClicked(device); - } - }); + v.setOnClickListener(v1 -> callback.pairingClicked(device)); return v; } } diff --git a/src/org/kde/kdeconnect/UserInterface/MainActivity.java b/src/org/kde/kdeconnect/UserInterface/MainActivity.java index 6f6b2538..540cf248 100644 --- a/src/org/kde/kdeconnect/UserInterface/MainActivity.java +++ b/src/org/kde/kdeconnect/UserInterface/MainActivity.java @@ -1,393 +1,350 @@ package org.kde.kdeconnect.UserInterface; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.annotation.RequiresApi; import android.support.design.widget.NavigationView; import android.support.v4.app.Fragment; import android.support.v4.view.GravityCompat; import android.support.v4.widget.DrawerLayout; import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBarDrawerToggle; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.SwitchCompat; import android.support.v7.widget.Toolbar; import android.text.TextUtils; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.CompoundButton; import android.widget.EditText; import android.widget.TextView; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Helpers.DeviceHelper; import org.kde.kdeconnect_tp.R; import java.util.Collection; import java.util.HashMap; public class MainActivity extends AppCompatActivity { private static final String STATE_SELECTED_DEVICE = "selected_device"; public static final int RESULT_NEEDS_RELOAD = Activity.RESULT_FIRST_USER; public static final String PAIR_REQUEST_STATUS = "pair_req_status"; public static final String PAIRING_ACCEPTED = "accepted"; public static final String PAIRING_REJECTED = "rejected"; private NavigationView mNavigationView; private DrawerLayout mDrawerLayout; private String mCurrentDevice; private SharedPreferences preferences; private final HashMap mMapMenuToDeviceId = new HashMap<>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // We need to set this theme before the call to 'setContentView' below ThemeUtil.setUserPreferredTheme(this); setContentView(R.layout.activity_main); mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); mNavigationView = (NavigationView) findViewById(R.id.navigation_drawer); View mDrawerHeader = mNavigationView.getHeaderView(0); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); ActionBar actionBar = getSupportActionBar(); ActionBarDrawerToggle mDrawerToggle = new ActionBarDrawerToggle(this, /* host Activity */ mDrawerLayout, /* DrawerLayout object */ R.string.open, /* "open drawer" description */ R.string.close /* "close drawer" description */ ); mDrawerLayout.addDrawerListener(mDrawerToggle); mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START); actionBar.setDisplayHomeAsUpEnabled(true); mDrawerToggle.setDrawerIndicatorEnabled(true); mDrawerToggle.syncState(); String deviceName = DeviceHelper.getDeviceName(this); TextView nameView = (TextView) mDrawerHeader.findViewById(R.id.device_name); nameView.setText(deviceName); - View.OnClickListener renameListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - renameDevice(); - } - }; + View.OnClickListener renameListener = v -> renameDevice(); mDrawerHeader.findViewById(R.id.kdeconnect_label).setOnClickListener(renameListener); mDrawerHeader.findViewById(R.id.device_name).setOnClickListener(renameListener); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { addDarkModeSwitch((ViewGroup) mDrawerHeader); } - mNavigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() { - @Override - public boolean onNavigationItemSelected(MenuItem menuItem) { + mNavigationView.setNavigationItemSelectedListener(menuItem -> { - String deviceId = mMapMenuToDeviceId.get(menuItem); - onDeviceSelected(deviceId); + String deviceId = mMapMenuToDeviceId.get(menuItem); + onDeviceSelected(deviceId); - mDrawerLayout.closeDrawer(mNavigationView); + mDrawerLayout.closeDrawer(mNavigationView); - return true; - } + return true; }); preferences = getSharedPreferences(STATE_SELECTED_DEVICE, Context.MODE_PRIVATE); String savedDevice; String pairStatus = ""; if (getIntent().hasExtra("forceOverview")) { Log.i("MainActivity", "Requested to start main overview"); savedDevice = null; } else if (getIntent().hasExtra("deviceId")) { Log.i("MainActivity", "Loading selected device from parameter"); savedDevice = getIntent().getStringExtra("deviceId"); if (getIntent().hasExtra(PAIR_REQUEST_STATUS)) { pairStatus = getIntent().getStringExtra(PAIR_REQUEST_STATUS); } } else if (savedInstanceState != null) { Log.i("MainActivity", "Loading selected device from saved activity state"); savedDevice = savedInstanceState.getString(STATE_SELECTED_DEVICE); } else { Log.i("MainActivity", "Loading selected device from persistent storage"); savedDevice = preferences.getString(STATE_SELECTED_DEVICE, null); } //if pairStatus is not empty, then the decision has been made... if (!pairStatus.equals("")) { Log.i("MainActivity", "pair status is " + pairStatus); onNewDeviceSelected(savedDevice, pairStatus); } onDeviceSelected(savedDevice); } /** * Adds a {@link SwitchCompat} to the bottom of the navigation header for * toggling dark mode on and off. Call from {@link #onCreate(Bundle)}. *

- * Only supports android ICS and higher because {@link SwitchCompat} - * requires that. + * Only supports android ICS and higher because {@link SwitchCompat} + * requires that. *

* * @param drawerHeader the layout which should contain the switch */ @RequiresApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) private void addDarkModeSwitch(ViewGroup drawerHeader) { getLayoutInflater().inflate(R.layout.nav_dark_mode_switch, drawerHeader); SwitchCompat darkThemeSwitch = (SwitchCompat) drawerHeader.findViewById(R.id.dark_theme); darkThemeSwitch.setChecked(ThemeUtil.shouldUseDarkTheme(this)); darkThemeSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @RequiresApi(Build.VERSION_CODES.HONEYCOMB) @Override public void onCheckedChanged(CompoundButton darkThemeSwitch, boolean isChecked) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(MainActivity.this); boolean isDarkAlready = prefs.getBoolean("darkTheme", false); if (isDarkAlready != isChecked) { prefs.edit().putBoolean("darkTheme", isChecked).apply(); MainActivity.this.recreate(); } } }); } //like onNewDeviceSelected but assumes that the new device is simply requesting to be paired //and can't be null private void onNewDeviceSelected(String deviceId, String pairStatus) { mCurrentDevice = deviceId; preferences.edit().putString(STATE_SELECTED_DEVICE, mCurrentDevice).apply(); for (HashMap.Entry entry : mMapMenuToDeviceId.entrySet()) { boolean selected = TextUtils.equals(entry.getValue(), deviceId); //null-safe entry.getKey().setChecked(selected); } if (pairStatus.equals(PAIRING_ACCEPTED)) { DeviceFragment.acceptPairing(deviceId, this); } else { DeviceFragment.rejectPairing(deviceId, this); } } @Override public void onBackPressed() { if (mDrawerLayout.isDrawerOpen(mNavigationView)) { mDrawerLayout.closeDrawer(mNavigationView); } else { super.onBackPressed(); } } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { mDrawerLayout.openDrawer(mNavigationView); return true; } else { return super.onOptionsItemSelected(item); } } private void updateComputerList() { //Log.e("MainActivity", "UpdateComputerList"); - BackgroundService.RunCommand(MainActivity.this, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(final BackgroundService service) { - - Menu menu = mNavigationView.getMenu(); - - menu.clear(); - mMapMenuToDeviceId.clear(); - - int id = 0; - Collection devices = service.getDevices().values(); - for (Device device : devices) { - if (device.isReachable() && device.isPaired()) { - MenuItem item = menu.add(0, id++, 0, device.getName()); - item.setIcon(device.getIcon()); - item.setCheckable(true); - item.setChecked(device.getDeviceId().equals(mCurrentDevice)); - mMapMenuToDeviceId.put(item, device.getDeviceId()); - } - } + BackgroundService.RunCommand(MainActivity.this, service -> { + + Menu menu = mNavigationView.getMenu(); + + menu.clear(); + mMapMenuToDeviceId.clear(); - MenuItem item = menu.add(99, id++, 0, R.string.pair_new_device); - item.setIcon(R.drawable.ic_action_content_add_circle_outline); - item.setCheckable(true); - item.setChecked(mCurrentDevice == null); - mMapMenuToDeviceId.put(item, null); + int id = 0; + Collection devices = service.getDevices().values(); + for (Device device : devices) { + if (device.isReachable() && device.isPaired()) { + MenuItem item = menu.add(0, id++, 0, device.getName()); + item.setIcon(device.getIcon()); + item.setCheckable(true); + item.setChecked(device.getDeviceId().equals(mCurrentDevice)); + mMapMenuToDeviceId.put(item, device.getDeviceId()); + } } + + MenuItem item = menu.add(99, id++, 0, R.string.pair_new_device); + item.setIcon(R.drawable.ic_action_content_add_circle_outline); + item.setCheckable(true); + item.setChecked(mCurrentDevice == null); + mMapMenuToDeviceId.put(item, null); }); } @Override protected void onStart() { super.onStart(); BackgroundService.addGuiInUseCounter(this, true); - BackgroundService.RunCommand(this, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - service.addDeviceListChangedCallback("MainActivity", new BackgroundService.DeviceListChangedCallback() { - @Override - public void onDeviceListChanged() { - updateComputerList(); - } - }); - } - }); + BackgroundService.RunCommand(this, service -> service.addDeviceListChangedCallback("MainActivity", this::updateComputerList)); updateComputerList(); } @Override protected void onStop() { BackgroundService.removeGuiInUseCounter(this); - BackgroundService.RunCommand(this, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - service.removeDeviceListChangedCallback("MainActivity"); - } - }); + BackgroundService.RunCommand(this, service -> service.removeDeviceListChangedCallback("MainActivity")); super.onStop(); } //TODO: Make it accept two parameters, a constant with the type of screen and the device id in //case the screen is for a device, or even three parameters and the third one be the plugin id? //This way we can keep adding more options with null device id (eg: about, help...) public void onDeviceSelected(String deviceId, boolean fromDeviceList) { mCurrentDevice = deviceId; preferences.edit().putString(STATE_SELECTED_DEVICE, mCurrentDevice).apply(); for (HashMap.Entry entry : mMapMenuToDeviceId.entrySet()) { boolean selected = TextUtils.equals(entry.getValue(), deviceId); //null-safe entry.getKey().setChecked(selected); } Fragment fragment; if (deviceId == null) { fragment = new PairingFragment(); } else { fragment = new DeviceFragment(deviceId, fromDeviceList); } getSupportFragmentManager() .beginTransaction() .replace(R.id.container, fragment) .commit(); } public void onDeviceSelected(String deviceId) { onDeviceSelected(deviceId, false); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putString(STATE_SELECTED_DEVICE, mCurrentDevice); } @Override protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); String savedDevice = savedInstanceState.getString(STATE_SELECTED_DEVICE); onDeviceSelected(savedDevice); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case RESULT_NEEDS_RELOAD: - BackgroundService.RunCommand(this, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - Device device = service.getDevice(mCurrentDevice); - device.reloadPluginsFromSettings(); - } + BackgroundService.RunCommand(this, service -> { + Device device = service.getDevice(mCurrentDevice); + device.reloadPluginsFromSettings(); }); break; default: super.onActivityResult(requestCode, resultCode, data); } } @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { for (int result : grantResults) { if (result == PackageManager.PERMISSION_GRANTED) { //New permission granted, reload plugins - BackgroundService.RunCommand(this, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - Device device = service.getDevice(mCurrentDevice); - device.reloadPluginsFromSettings(); - } + BackgroundService.RunCommand(this, service -> { + Device device = service.getDevice(mCurrentDevice); + device.reloadPluginsFromSettings(); }); } } } public void renameDevice() { final TextView nameView = (TextView) mNavigationView.findViewById(R.id.device_name); final EditText deviceNameEdit = new EditText(MainActivity.this); String deviceName = DeviceHelper.getDeviceName(MainActivity.this); deviceNameEdit.setText(deviceName); deviceNameEdit.setPadding( ((int) (18 * getResources().getDisplayMetrics().density)), ((int) (16 * getResources().getDisplayMetrics().density)), ((int) (18 * getResources().getDisplayMetrics().density)), ((int) (12 * getResources().getDisplayMetrics().density)) ); new AlertDialog.Builder(MainActivity.this) .setView(deviceNameEdit) - .setPositiveButton(R.string.device_rename_confirm, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - String deviceName = deviceNameEdit.getText().toString(); - DeviceHelper.setDeviceName(MainActivity.this, deviceName); - nameView.setText(deviceName); - BackgroundService.RunCommand(MainActivity.this, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(final BackgroundService service) { - service.onNetworkChange(); - } - }); - } + .setPositiveButton(R.string.device_rename_confirm, (dialog, which) -> { + String deviceName1 = deviceNameEdit.getText().toString(); + DeviceHelper.setDeviceName(MainActivity.this, deviceName1); + nameView.setText(deviceName1); + BackgroundService.RunCommand(MainActivity.this, BackgroundService::onNetworkChange); }) - .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - } + .setNegativeButton(R.string.cancel, (dialog, which) -> { }) .setTitle(R.string.device_rename_title) .show(); } } diff --git a/src/org/kde/kdeconnect/UserInterface/PairingFragment.java b/src/org/kde/kdeconnect/UserInterface/PairingFragment.java index 030cdd90..4c3112fe 100644 --- a/src/org/kde/kdeconnect/UserInterface/PairingFragment.java +++ b/src/org/kde/kdeconnect/UserInterface/PairingFragment.java @@ -1,295 +1,253 @@ /* * 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 . -*/ + * along with this program. If not, see . + */ package org.kde.kdeconnect.UserInterface; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.os.Bundle; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ListView; import android.widget.TextView; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Helpers.NetworkHelper; import org.kde.kdeconnect.UserInterface.List.ListAdapter; import org.kde.kdeconnect.UserInterface.List.PairingDeviceItem; import org.kde.kdeconnect.UserInterface.List.SectionItem; import org.kde.kdeconnect_tp.R; import java.util.ArrayList; import java.util.Collection; /** * The view that the user will see when there are no devices paired, or when you choose "add a new device" from the sidebar. */ public class PairingFragment extends Fragment implements PairingDeviceItem.Callback { private static final int RESULT_PAIRING_SUCCESFUL = Activity.RESULT_FIRST_USER; private View rootView; private SwipeRefreshLayout mSwipeRefreshLayout; private MainActivity mActivity; boolean listRefreshCalledThisFrame = false; TextView headerText; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { //Log.e("PairingFragmen", "OnCreateView"); mActivity.getSupportActionBar().setTitle(R.string.pairing_title); setHasOptionsMenu(true); rootView = inflater.inflate(R.layout.devices_list, container, false); View listRootView = rootView.findViewById(R.id.devices_list); mSwipeRefreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.refresh_list_layout); mSwipeRefreshLayout.setOnRefreshListener( - new SwipeRefreshLayout.OnRefreshListener() { - @Override - public void onRefresh() { - updateComputerListAction(); - } - } + this::updateComputerListAction ); headerText = new TextView(inflater.getContext()); headerText.setText(getString(R.string.pairing_description)); headerText.setPadding(0, (int) (16 * getResources().getDisplayMetrics().density), 0, (int) (12 * getResources().getDisplayMetrics().density)); ((ListView) listRootView).addHeaderView(headerText); return rootView; } @Override public void onAttach(Context context) { super.onAttach(context); mActivity = ((MainActivity) getActivity()); } private void updateComputerListAction() { updateComputerList(); - BackgroundService.RunCommand(mActivity, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - service.onNetworkChange(); - } - }); + BackgroundService.RunCommand(mActivity, BackgroundService::onNetworkChange); mSwipeRefreshLayout.setRefreshing(true); - new Thread(new Runnable() { - @Override - public void run() { - try { - Thread.sleep(1500); - } catch (InterruptedException ignored) { - } - mActivity.runOnUiThread(new Runnable() { - @Override - public void run() { - mSwipeRefreshLayout.setRefreshing(false); - } - }); + new Thread(() -> { + try { + Thread.sleep(1500); + } catch (InterruptedException ignored) { } + mActivity.runOnUiThread(() -> mSwipeRefreshLayout.setRefreshing(false)); }).start(); } private void updateComputerList() { - BackgroundService.RunCommand(mActivity, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(final BackgroundService service) { - mActivity.runOnUiThread(new Runnable() { - @Override - public void run() { - - if (!isAdded()) { - //Fragment is not attached to an activity. We will crash if we try to do anything here. - return; - } - - if (listRefreshCalledThisFrame) { - // This makes sure we don't try to call list.getFirstVisiblePosition() - // twice per frame, because the second time the list hasn't been drawn - // yet and it would always return 0. - return; - } - listRefreshCalledThisFrame = true; - - headerText.setText(getString(NetworkHelper.isOnMobileNetwork(getContext()) ? R.string.on_data_message : R.string.pairing_description)); - //Disable tap animation - headerText.setOnClickListener(null); - headerText.setOnLongClickListener(null); - - try { - Collection devices = service.getDevices().values(); - final ArrayList items = new ArrayList<>(); - - SectionItem connectedSection; - Resources res = getResources(); - - connectedSection = new SectionItem(res.getString(R.string.category_connected_devices)); - items.add(connectedSection); - for (Device device : devices) { - if (device.isReachable() && device.isPaired()) { - items.add(new PairingDeviceItem(device, PairingFragment.this)); - connectedSection.isEmpty = false; - } - } - if (connectedSection.isEmpty) { - items.remove(items.size() - 1); //Remove connected devices section if empty - } - - SectionItem availableSection = new SectionItem(res.getString(R.string.category_not_paired_devices)); - items.add(availableSection); - for (Device device : devices) { - if (device.isReachable() && !device.isPaired()) { - items.add(new PairingDeviceItem(device, PairingFragment.this)); - availableSection.isEmpty = false; - } - } - if (availableSection.isEmpty && !connectedSection.isEmpty) { - items.remove(items.size() - 1); //Remove remembered devices section if empty - } - - SectionItem rememberedSection = new SectionItem(res.getString(R.string.category_remembered_devices)); - items.add(rememberedSection); - for (Device device : devices) { - if (!device.isReachable() && device.isPaired()) { - items.add(new PairingDeviceItem(device, PairingFragment.this)); - rememberedSection.isEmpty = false; - } - } - if (rememberedSection.isEmpty) { - items.remove(items.size() - 1); //Remove remembered devices section if empty - } - - final ListView list = (ListView) rootView.findViewById(R.id.devices_list); - - //Store current scroll - int index = list.getFirstVisiblePosition(); - View v = list.getChildAt(0); - int top = (v == null) ? 0 : (v.getTop() - list.getPaddingTop()); - - list.setAdapter(new ListAdapter(mActivity, items)); - - //Restore scroll - list.setSelectionFromTop(index, top); - } catch (IllegalStateException e) { - e.printStackTrace(); - //Ignore: The activity was closed while we were trying to update it - } finally { - listRefreshCalledThisFrame = false; - } + BackgroundService.RunCommand(mActivity, service -> mActivity.runOnUiThread(() -> { + + if (!isAdded()) { + //Fragment is not attached to an activity. We will crash if we try to do anything here. + return; + } + + if (listRefreshCalledThisFrame) { + // This makes sure we don't try to call list.getFirstVisiblePosition() + // twice per frame, because the second time the list hasn't been drawn + // yet and it would always return 0. + return; + } + listRefreshCalledThisFrame = true; + + headerText.setText(getString(NetworkHelper.isOnMobileNetwork(getContext()) ? R.string.on_data_message : R.string.pairing_description)); + //Disable tap animation + headerText.setOnClickListener(null); + headerText.setOnLongClickListener(null); + + try { + Collection devices = service.getDevices().values(); + final ArrayList items = new ArrayList<>(); + + SectionItem connectedSection; + Resources res = getResources(); + + connectedSection = new SectionItem(res.getString(R.string.category_connected_devices)); + items.add(connectedSection); + for (Device device : devices) { + if (device.isReachable() && device.isPaired()) { + items.add(new PairingDeviceItem(device, PairingFragment.this)); + connectedSection.isEmpty = false; + } + } + if (connectedSection.isEmpty) { + items.remove(items.size() - 1); //Remove connected devices section if empty + } + + SectionItem availableSection = new SectionItem(res.getString(R.string.category_not_paired_devices)); + items.add(availableSection); + for (Device device : devices) { + if (device.isReachable() && !device.isPaired()) { + items.add(new PairingDeviceItem(device, PairingFragment.this)); + availableSection.isEmpty = false; } - }); + } + if (availableSection.isEmpty && !connectedSection.isEmpty) { + items.remove(items.size() - 1); //Remove remembered devices section if empty + } + + SectionItem rememberedSection = new SectionItem(res.getString(R.string.category_remembered_devices)); + items.add(rememberedSection); + for (Device device : devices) { + if (!device.isReachable() && device.isPaired()) { + items.add(new PairingDeviceItem(device, PairingFragment.this)); + rememberedSection.isEmpty = false; + } + } + if (rememberedSection.isEmpty) { + items.remove(items.size() - 1); //Remove remembered devices section if empty + } + + final ListView list = (ListView) rootView.findViewById(R.id.devices_list); + //Store current scroll + int index = list.getFirstVisiblePosition(); + View v = list.getChildAt(0); + int top = (v == null) ? 0 : (v.getTop() - list.getPaddingTop()); + + list.setAdapter(new ListAdapter(mActivity, items)); + + //Restore scroll + list.setSelectionFromTop(index, top); + } catch (IllegalStateException e) { + e.printStackTrace(); + //Ignore: The activity was closed while we were trying to update it + } finally { + listRefreshCalledThisFrame = false; } - }); - } + })); +} @Override public void onStart() { super.onStart(); - BackgroundService.RunCommand(mActivity, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - service.addDeviceListChangedCallback("PairingFragment", new BackgroundService.DeviceListChangedCallback() { - @Override - public void onDeviceListChanged() { - updateComputerList(); - } - }); - } - }); + BackgroundService.RunCommand(mActivity, service -> service.addDeviceListChangedCallback("PairingFragment", this::updateComputerList)); updateComputerList(); } @Override public void onStop() { super.onStop(); mSwipeRefreshLayout.setEnabled(false); - BackgroundService.RunCommand(mActivity, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - service.removeDeviceListChangedCallback("PairingFragment"); - } - }); + BackgroundService.RunCommand(mActivity, service -> service.removeDeviceListChangedCallback("PairingFragment")); } @Override public void pairingClicked(Device device) { mActivity.onDeviceSelected(device.getDeviceId(), !device.isPaired() || !device.isReachable()); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case RESULT_PAIRING_SUCCESFUL: if (resultCode == 1) { String deviceId = data.getStringExtra("deviceId"); mActivity.onDeviceSelected(deviceId); } break; default: super.onActivityResult(requestCode, resultCode, data); } } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.pairing, menu); } @Override public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { case R.id.menu_refresh: updateComputerListAction(); break; case R.id.menu_rename: mActivity.renameDevice(); break; case R.id.menu_custom_device_list: startActivity(new Intent(mActivity, CustomDevicesActivity.class)); break; default: break; } return true; } } diff --git a/src/org/kde/kdeconnect/UserInterface/PluginPreference.java b/src/org/kde/kdeconnect/UserInterface/PluginPreference.java index f81c534e..8d9a5646 100644 --- a/src/org/kde/kdeconnect/UserInterface/PluginPreference.java +++ b/src/org/kde/kdeconnect/UserInterface/PluginPreference.java @@ -1,72 +1,66 @@ package org.kde.kdeconnect.UserInterface; import android.preference.CheckBoxPreference; import android.view.View; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Plugins.Plugin; import org.kde.kdeconnect.Plugins.PluginFactory; import org.kde.kdeconnect_tp.R; public class PluginPreference extends CheckBoxPreference { final Device device; final String pluginKey; final View.OnClickListener listener; public PluginPreference(final SettingsActivity activity, final String pluginKey, final Device device) { super(activity); setLayoutResource(R.layout.preference_with_button); this.device = device; this.pluginKey = pluginKey; PluginFactory.PluginInfo info = PluginFactory.getPluginInfo(activity, pluginKey); setTitle(info.getDisplayName()); setSummary(info.getDescription()); setChecked(device.isPluginEnabled(pluginKey)); Plugin plugin = device.getPlugin(pluginKey, true); if (info.hasSettings() && plugin != null) { - this.listener = new View.OnClickListener() { - @Override - public void onClick(View v) { - Plugin plugin = device.getPlugin(pluginKey, true); - if (plugin != null) { - plugin.startPreferencesActivity(activity); - } else { //Could happen if the device is not connected anymore - activity.finish(); //End this activity so we go to the "device not reachable" screen - } + this.listener = v -> { + Plugin plugin1 = device.getPlugin(pluginKey, true); + if (plugin1 != null) { + plugin1.startPreferencesActivity(activity); + } else { //Could happen if the device is not connected anymore + activity.finish(); //End this activity so we go to the "device not reachable" screen } }; } else { this.listener = null; } } @Override protected void onBindView(View root) { super.onBindView(root); final View button = root.findViewById(R.id.settingsButton); if (listener == null) { button.setVisibility(View.GONE); } else { button.setEnabled(isChecked()); button.setOnClickListener(listener); } - root.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - boolean newState = !device.isPluginEnabled(pluginKey); - setChecked(newState); //It actually works on API<14 - button.setEnabled(newState); - device.setPluginEnabled(pluginKey, newState); - } + root.setOnClickListener(v -> { + boolean newState = !device.isPluginEnabled(pluginKey); + setChecked(newState); //It actually works on API<14 + button.setEnabled(newState); + device.setPluginEnabled(pluginKey, newState); }); } } diff --git a/src/org/kde/kdeconnect/UserInterface/SettingsActivity.java b/src/org/kde/kdeconnect/UserInterface/SettingsActivity.java index 029b28b2..274c7847 100644 --- a/src/org/kde/kdeconnect/UserInterface/SettingsActivity.java +++ b/src/org/kde/kdeconnect/UserInterface/SettingsActivity.java @@ -1,92 +1,84 @@ /* * 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.UserInterface; import android.os.Bundle; import android.preference.PreferenceScreen; import android.view.MenuItem; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.Device; import java.util.List; public class SettingsActivity extends AppCompatPreferenceActivity { static private String deviceId; //Static because if we get here by using the back button in the action bar, the extra deviceId will not be set. @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); final PreferenceScreen preferenceScreen = getPreferenceManager().createPreferenceScreen(this); setPreferenceScreen(preferenceScreen); if (getIntent().hasExtra("deviceId")) { deviceId = getIntent().getStringExtra("deviceId"); } - BackgroundService.RunCommand(getApplicationContext(), new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - final Device device = service.getDevice(deviceId); - if (device == null) { - SettingsActivity.this.runOnUiThread(new Runnable() { - @Override - public void run() { - SettingsActivity.this.finish(); - } - }); - return; - } - List plugins = device.getSupportedPlugins(); - for (final String pluginKey : plugins) { - PluginPreference pref = new PluginPreference(SettingsActivity.this, pluginKey, device); - preferenceScreen.addPreference(pref); - } + BackgroundService.RunCommand(getApplicationContext(), service -> { + final Device device = service.getDevice(deviceId); + if (device == null) { + SettingsActivity.this.runOnUiThread(SettingsActivity.this::finish); + return; + } + List plugins = device.getSupportedPlugins(); + for (final String pluginKey : plugins) { + PluginPreference pref = new PluginPreference(SettingsActivity.this, pluginKey, device); + preferenceScreen.addPreference(pref); } }); } @Override public boolean onOptionsItemSelected(MenuItem item) { //ActionBar's back button if (item.getItemId() == android.R.id.home) { finish(); return true; } else { return super.onOptionsItemSelected(item); } } @Override protected void onStart() { super.onStart(); BackgroundService.addGuiInUseCounter(this); } @Override protected void onStop() { super.onStop(); BackgroundService.removeGuiInUseCounter(this); } } diff --git a/tests/org/kde/kdeconnect/LanLinkTest.java b/tests/org/kde/kdeconnect/LanLinkTest.java index d7077929..2b254eae 100644 --- a/tests/org/kde/kdeconnect/LanLinkTest.java +++ b/tests/org/kde/kdeconnect/LanLinkTest.java @@ -1,260 +1,244 @@ /* * Copyright 2015 Vineet Garg * * 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.test.AndroidTestCase; import android.util.Log; import org.json.JSONException; import org.json.JSONObject; import org.kde.kdeconnect.Backends.LanBackend.LanLink; import org.kde.kdeconnect.Backends.LanBackend.LanLinkProvider; import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.Socket; public class LanLinkTest extends AndroidTestCase { LanLink badLanLink; LanLink goodLanLink; OutputStream badOutputStream; OutputStream goodOutputStream; Device.SendPacketStatusCallback callback; @Override protected void setUp() throws Exception { super.setUp(); System.setProperty("dexmaker.dexcache", getContext().getCacheDir().getPath()); LanLinkProvider linkProvider = Mockito.mock(LanLinkProvider.class); Mockito.when(linkProvider.getName()).thenReturn("LanLinkProvider"); callback = Mockito.mock(Device.SendPacketStatusCallback.class); goodOutputStream = Mockito.mock(OutputStream.class); badOutputStream = Mockito.mock(OutputStream.class); Mockito.doThrow(new IOException("AAA")).when(badOutputStream).write(Mockito.any(byte[].class)); Socket socketMock = Mockito.mock(Socket.class); Mockito.when(socketMock.getRemoteSocketAddress()).thenReturn(new InetSocketAddress(5000)); Mockito.when(socketMock.getOutputStream()).thenReturn(goodOutputStream); Socket socketBadMock = Mockito.mock(Socket.class); Mockito.when(socketBadMock.getRemoteSocketAddress()).thenReturn(new InetSocketAddress(5000)); Mockito.when(socketBadMock.getOutputStream()).thenReturn(badOutputStream); goodLanLink = new LanLink(getContext(), "testDevice", linkProvider, socketMock, LanLink.ConnectionStarted.Remotely); badLanLink = new LanLink(getContext(), "testDevice", linkProvider, socketBadMock, LanLink.ConnectionStarted.Remotely); } @Override protected void tearDown() throws Exception { super.tearDown(); } public void testSendPacketSuccess() throws JSONException { NetworkPacket testPacket = Mockito.mock(NetworkPacket.class); Mockito.when(testPacket.getType()).thenReturn("kdeconnect.test"); Mockito.when(testPacket.getBoolean("isTesting")).thenReturn(true); Mockito.when(testPacket.getString("testName")).thenReturn("testSendPacketSuccess"); Mockito.when(testPacket.serialize()).thenReturn("{\"id\":123,\"type\":\"kdeconnect.test\",\"body\":{\"isTesting\":true,\"testName\":\"testSendPacketSuccess\"}}"); goodLanLink.sendPacket(testPacket, callback); Mockito.verify(callback).onSuccess(); } public void testSendPacketFail() throws JSONException { NetworkPacket testPacket = Mockito.mock(NetworkPacket.class); Mockito.when(testPacket.getType()).thenReturn("kdeconnect.test"); Mockito.when(testPacket.getBoolean("isTesting")).thenReturn(true); Mockito.when(testPacket.getString("testName")).thenReturn("testSendPacketFail"); Mockito.when(testPacket.serialize()).thenReturn("{\"id\":123,\"type\":\"kdeconnect.test\",\"body\":{\"isTesting\":true,\"testName\":\"testSendPacketFail\"}}"); badLanLink.sendPacket(testPacket, callback); Mockito.verify(callback).onFailure(Mockito.any(RuntimeException.class)); } public void testSendPayload() throws Exception { class Downloader extends Thread { NetworkPacket np; ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); public void setNetworkPacket(NetworkPacket networkPacket) { this.np = networkPacket; } public ByteArrayOutputStream getOutputStream() { return outputStream; } @Override public void run() { try { Socket socket = null; try { socket = new Socket(); int tcpPort = np.getPayloadTransferInfo().getInt("port"); InetSocketAddress address = new InetSocketAddress(5000); socket.connect(new InetSocketAddress(address.getAddress(), tcpPort)); np.setPayload(socket.getInputStream(), np.getPayloadSize()); } catch (Exception e) { try { socket.close(); } catch (Exception ignored) { throw ignored; } e.printStackTrace(); Log.e("KDE/LanLinkTest", "Exception connecting to remote socket"); throw e; } final InputStream input = np.getPayload(); final long fileLength = np.getPayloadSize(); byte data[] = new byte[1024]; long progress = 0, prevProgressPercentage = 0; int count; while ((count = input.read(data)) >= 0) { progress += count; outputStream.write(data, 0, count); if (fileLength > 0) { if (progress >= fileLength) break; long progressPercentage = (progress * 100 / fileLength); if (progressPercentage != prevProgressPercentage) { prevProgressPercentage = progressPercentage; } } } outputStream.close(); input.close(); } catch (Exception e) { Log.e("Downloader Test", "Exception"); e.printStackTrace(); } } } final Downloader downloader = new Downloader(); // Using byte array for payload, try to use input stream as used in real device String dataString = "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + " Cras vel erat et ante fringilla tristique. Sed consequat ligula at interdum " + "rhoncus. Integer semper enim felis, id sodales tellus aliquet eget." + " Sed fringilla ac metus eget dictum. Aliquam euismod non sem sit" + " amet dapibus. Interdum et malesuada fames ac ante ipsum primis " + "in faucibus. Nam et ligula placerat, varius justo eu, convallis " + "lorem. Nam consequat consequat tortor et gravida. Praesent " + "ultricies tortor eget ex elementum gravida. Suspendisse aliquet " + "erat a orci feugiat dignissim."; // reallyLongString contains dataString 16 times String reallyLongString = dataString + dataString; reallyLongString = reallyLongString + reallyLongString; reallyLongString = reallyLongString + reallyLongString; reallyLongString = reallyLongString + reallyLongString; final byte[] data = reallyLongString.getBytes(); final JSONObject sharePacketJson = new JSONObject("{\"id\":123,\"body\":{\"filename\":\"data.txt\"},\"payloadTransferInfo\":{},\"payloadSize\":8720,\"type\":\"kdeconnect.share\"}"); // Mocking share package final NetworkPacket sharePacket = Mockito.mock(NetworkPacket.class); Mockito.when(sharePacket.getType()).thenReturn("kdeconnect.share"); Mockito.when(sharePacket.hasPayload()).thenReturn(true); Mockito.when(sharePacket.hasPayloadTransferInfo()).thenReturn(true); - Mockito.doAnswer(new Answer() { - @Override - public Object answer(InvocationOnMock invocationOnMock) throws Throwable { - return sharePacketJson.toString(); - } - }).when(sharePacket).serialize(); + Mockito.doAnswer(invocationOnMock -> sharePacketJson.toString()).when(sharePacket).serialize(); Mockito.when(sharePacket.getPayload()).thenReturn(new ByteArrayInputStream(data)); Mockito.when(sharePacket.getPayloadSize()).thenReturn((long) data.length); - Mockito.doAnswer(new Answer() { - @Override - public Object answer(InvocationOnMock invocationOnMock) throws Throwable { - return sharePacketJson.getJSONObject("payloadTransferInfo"); - } - }).when(sharePacket).getPayloadTransferInfo(); - Mockito.doAnswer(new Answer() { - @Override - public Object answer(InvocationOnMock invocationOnMock) throws Throwable { - JSONObject object = (JSONObject) invocationOnMock.getArguments()[0]; + Mockito.doAnswer(invocationOnMock -> sharePacketJson.getJSONObject("payloadTransferInfo")).when(sharePacket).getPayloadTransferInfo(); + Mockito.doAnswer(invocationOnMock -> { + JSONObject object = (JSONObject) invocationOnMock.getArguments()[0]; - sharePacketJson.put("payloadTransferInfo", object); - return null; - } + sharePacketJson.put("payloadTransferInfo", object); + return null; }).when(sharePacket).setPayloadTransferInfo(Mockito.any(JSONObject.class)); - Mockito.doAnswer(new Answer() { - @Override - public Object answer(InvocationOnMock invocationOnMock) throws Throwable { + Mockito.doAnswer(invocationOnMock -> { - Log.e("LanLinkTest", "Write to stream"); - String stringNetworkPacket = new String((byte[]) invocationOnMock.getArguments()[0]); - final NetworkPacket np = NetworkPacket.unserialize(stringNetworkPacket); + Log.e("LanLinkTest", "Write to stream"); + String stringNetworkPacket = new String((byte[]) invocationOnMock.getArguments()[0]); + final NetworkPacket np = NetworkPacket.unserialize(stringNetworkPacket); - downloader.setNetworkPacket(np); - downloader.start(); + downloader.setNetworkPacket(np); + downloader.start(); - return stringNetworkPacket.length(); - } + return stringNetworkPacket.length(); }).when(goodOutputStream).write(Mockito.any(byte[].class)); goodLanLink.sendPacket(sharePacket, callback); try { // Wait 1 secs for downloader to finish (if some error, it will continue and assert will fail) downloader.join(1 * 1000); } catch (Exception e) { e.printStackTrace(); throw e; } assertEquals(new String(data), new String(downloader.getOutputStream().toByteArray())); Mockito.verify(callback).onSuccess(); } }