diff --git a/src/org/kde/kdeconnect/Backends/BluetoothBackend/BluetoothLink.java b/src/org/kde/kdeconnect/Backends/BluetoothBackend/BluetoothLink.java index 33f972f9..1f80c2fe 100644 --- a/src/org/kde/kdeconnect/Backends/BluetoothBackend/BluetoothLink.java +++ b/src/org/kde/kdeconnect/Backends/BluetoothBackend/BluetoothLink.java @@ -1,222 +1,222 @@ /* * Copyright 2016 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.Backends.BluetoothBackend; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothServerSocket; import android.bluetooth.BluetoothSocket; import android.content.Context; import android.os.Build; import android.util.Log; import org.json.JSONException; 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.StringsHelper; import org.kde.kdeconnect.NetworkPacket; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.Reader; import java.nio.charset.Charset; import java.util.UUID; public class BluetoothLink extends BaseLink { private final BluetoothSocket socket; private final BluetoothLinkProvider linkProvider; private boolean continueAccepting = true; private final Thread receivingThread = new Thread(new Runnable() { @Override public void run() { StringBuilder sb = new StringBuilder(); - try { - Reader reader = new InputStreamReader(socket.getInputStream(), "UTF-8"); + try (Reader reader = new InputStreamReader(socket.getInputStream(), StringsHelper.UTF8)) { char[] buf = new char[512]; while (continueAccepting) { while (sb.indexOf("\n") == -1 && continueAccepting) { int charsRead; if ((charsRead = reader.read(buf)) > 0) { sb.append(buf, 0, charsRead); } } int endIndex = sb.indexOf("\n"); if (endIndex != -1) { String message = sb.substring(0, endIndex + 1); sb.delete(0, endIndex + 1); processMessage(message); } } } catch (IOException e) { Log.e("BluetoothLink/receiving", "Connection to " + socket.getRemoteDevice().getAddress() + " likely broken.", e); disconnect(); } } private void processMessage(String message) { NetworkPacket np; try { np = NetworkPacket.unserialize(message); } catch (JSONException e) { Log.e("BluetoothLink/receiving", "Unable to parse message.", e); return; } if (np.hasPayloadTransferInfo()) { BluetoothSocket transferSocket = null; try { UUID transferUuid = UUID.fromString(np.getPayloadTransferInfo().getString("uuid")); transferSocket = socket.getRemoteDevice().createRfcommSocketToServiceRecord(transferUuid); transferSocket.connect(); np.setPayload(new NetworkPacket.Payload(transferSocket.getInputStream(), np.getPayloadSize())); } catch (Exception e) { if (transferSocket != null) { try { transferSocket.close(); } catch (IOException ignored) { } } Log.e("BluetoothLink/receiving", "Unable to get payload", e); } } packageReceived(np); } }); public BluetoothLink(Context context, BluetoothSocket socket, String deviceId, BluetoothLinkProvider linkProvider) { super(context, deviceId, linkProvider); this.socket = socket; this.linkProvider = linkProvider; } public void startListening() { this.receivingThread.start(); } @Override public String getName() { return "BluetoothLink"; } @Override public BasePairingHandler getPairingHandler(Device device, BasePairingHandler.PairingHandlerCallback callback) { return new BluetoothPairingHandler(device, callback); } public void disconnect() { if (socket == null) { return; } continueAccepting = false; try { socket.close(); } catch (IOException ignored) { } linkProvider.disconnectedLink(this, getDeviceId(), socket); } private void sendMessage(NetworkPacket np) throws JSONException, IOException { byte[] message = np.serialize().getBytes(Charset.forName("UTF-8")); OutputStream socket = this.socket.getOutputStream(); Log.i("BluetoothLink", "Beginning to send message"); socket.write(message); Log.i("BluetoothLink", "Finished sending message"); } @Override public boolean sendPacket(NetworkPacket np, final Device.SendPacketStatusCallback callback) { /*if (!isConnected()) { Log.e("BluetoothLink", "sendPacketEncrypted failed: not connected"); callback.sendFailure(new Exception("Not connected")); return; }*/ try { BluetoothServerSocket serverSocket = null; if (np.hasPayload()) { UUID transferUuid = UUID.randomUUID(); serverSocket = BluetoothAdapter.getDefaultAdapter() .listenUsingRfcommWithServiceRecord("KDE Connect Transfer", transferUuid); JSONObject payloadTransferInfo = new JSONObject(); payloadTransferInfo.put("uuid", transferUuid.toString()); np.setPayloadTransferInfo(payloadTransferInfo); } sendMessage(np); if (serverSocket != null) { try (BluetoothSocket transferSocket = serverSocket.accept()) { serverSocket.close(); int idealBufferLength = 4096; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && transferSocket.getMaxReceivePacketSize() > 0) { idealBufferLength = transferSocket.getMaxReceivePacketSize(); } byte[] buffer = new byte[idealBufferLength]; int bytesRead; long progress = 0; InputStream stream = np.getPayload().getInputStream(); while ((bytesRead = stream.read(buffer)) != -1) { progress += bytesRead; transferSocket.getOutputStream().write(buffer, 0, bytesRead); if (np.getPayloadSize() > 0) { callback.onProgressChanged((int) (100 * progress / np.getPayloadSize())); } } transferSocket.getOutputStream().flush(); stream.close(); } catch (Exception e) { callback.onFailure(e); return false; } } callback.onSuccess(); return true; } catch (Exception e) { callback.onFailure(e); return false; } } @Override public boolean linkShouldBeKeptAlive() { return receivingThread.isAlive(); } /* public boolean isConnected() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) { return socket.isConnected(); } else { return true; } } */ } diff --git a/src/org/kde/kdeconnect/Backends/BluetoothBackend/BluetoothLinkProvider.java b/src/org/kde/kdeconnect/Backends/BluetoothBackend/BluetoothLinkProvider.java index 68556d76..918d0e4a 100644 --- a/src/org/kde/kdeconnect/Backends/BluetoothBackend/BluetoothLinkProvider.java +++ b/src/org/kde/kdeconnect/Backends/BluetoothBackend/BluetoothLinkProvider.java @@ -1,381 +1,384 @@ /* * Copyright 2016 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.Backends.BluetoothBackend; import android.annotation.TargetApi; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothServerSocket; import android.bluetooth.BluetoothSocket; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Build; import android.os.Parcelable; import android.util.Log; import org.kde.kdeconnect.Backends.BaseLinkProvider; import org.kde.kdeconnect.Device; +import org.kde.kdeconnect.Helpers.StringsHelper; import org.kde.kdeconnect.NetworkPacket; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.Reader; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.UUID; @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) public class BluetoothLinkProvider extends BaseLinkProvider { private static final UUID SERVICE_UUID = UUID.fromString("185f3df4-3268-4e3f-9fca-d4d5059915bd"); private static final int REQUEST_ENABLE_BT = 48; private final Context context; private final Map visibleComputers = new HashMap<>(); private final Map sockets = new HashMap<>(); private BluetoothAdapter bluetoothAdapter; private ServerRunnable serverRunnable; private ClientRunnable clientRunnable; private void addLink(NetworkPacket identityPacket, BluetoothLink link) { String deviceId = identityPacket.getString("deviceId"); Log.i("BluetoothLinkProvider", "addLink to " + deviceId); BluetoothLink oldLink = visibleComputers.get(deviceId); if (oldLink == link) { Log.e("BluetoothLinkProvider", "oldLink == link. This should not happen!"); return; } visibleComputers.put(deviceId, link); connectionAccepted(identityPacket, link); link.startListening(); if (oldLink != null) { Log.i("BluetoothLinkProvider", "Removing old connection to same device"); oldLink.disconnect(); } } public BluetoothLinkProvider(Context context) { this.context = context; bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); if (bluetoothAdapter == null) { Log.e("BluetoothLinkProvider", "No bluetooth adapter found."); } } @Override public void onStart() { if (bluetoothAdapter == null) { return; } if (!bluetoothAdapter.isEnabled()) { Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); Log.e("BluetoothLinkProvider", "Bluetooth adapter not enabled."); // TODO: next line needs to be called from an existing activity, so move it? // startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT); // TODO: Check result of the previous command, whether the user allowed bluetooth or not. return; } //This handles the case when I'm the existing device in the network and receive a hello package clientRunnable = new ClientRunnable(); new Thread(clientRunnable).start(); // I'm on a new network, let's be polite and introduce myself serverRunnable = new ServerRunnable(); new Thread(serverRunnable).start(); } @Override public void onNetworkChange() { onStop(); onStart(); } @Override public void onStop() { if (bluetoothAdapter == null || clientRunnable == null || serverRunnable == null) { return; } clientRunnable.stopProcessing(); serverRunnable.stopProcessing(); } @Override public String getName() { return "BluetoothLinkProvider"; } public void disconnectedLink(BluetoothLink link, String deviceId, BluetoothSocket socket) { sockets.remove(socket.getRemoteDevice()); visibleComputers.remove(deviceId); connectionLost(link); } private class ServerRunnable implements Runnable { private boolean continueProcessing = true; private BluetoothServerSocket serverSocket; void stopProcessing() { continueProcessing = false; if (serverSocket != null) { try { serverSocket.close(); } catch (IOException e) { Log.e("KDEConnect", "Exception", e); } } } @Override public void run() { try { serverSocket = bluetoothAdapter .listenUsingRfcommWithServiceRecord("KDE Connect", SERVICE_UUID); } catch (IOException e) { Log.e("KDEConnect", "Exception", e); return; } if (continueProcessing) { try { BluetoothSocket socket = serverSocket.accept(); connect(socket); } catch (Exception ignored) { } } } private void connect(BluetoothSocket socket) throws Exception { //socket.connect(); OutputStream outputStream = socket.getOutputStream(); if (sockets.containsKey(socket.getRemoteDevice())) { Log.i("BTLinkProvider/Server", "Received duplicate connection from " + socket.getRemoteDevice().getAddress()); socket.close(); return; } else { sockets.put(socket.getRemoteDevice(), socket); } Log.i("BTLinkProvider/Server", "Received connection from " + socket.getRemoteDevice().getAddress()); NetworkPacket np = NetworkPacket.createIdentityPacket(context); - byte[] message = np.serialize().getBytes("UTF-8"); + byte[] message = np.serialize().getBytes(StringsHelper.UTF8); outputStream.write(message); Log.i("BTLinkProvider/Server", "Sent identity package"); // Listen for the response StringBuilder sb = new StringBuilder(); - Reader reader = new InputStreamReader(socket.getInputStream(), "UTF-8"); - int charsRead; - char[] buf = new char[512]; - while (sb.lastIndexOf("\n") == -1 && (charsRead = reader.read(buf)) != -1) { - sb.append(buf, 0, charsRead); - } + try (Reader reader = new InputStreamReader(socket.getInputStream(), StringsHelper.UTF8)) { + int charsRead; + char[] buf = new char[512]; + while (sb.lastIndexOf("\n") == -1 && (charsRead = reader.read(buf)) != -1) { + sb.append(buf, 0, charsRead); + } - String response = sb.toString(); - final NetworkPacket identityPacket = NetworkPacket.unserialize(response); + String response = sb.toString(); + final NetworkPacket identityPacket = NetworkPacket.unserialize(response); - if (!identityPacket.getType().equals(NetworkPacket.PACKET_TYPE_IDENTITY)) { - Log.e("BTLinkProvider/Server", "2 Expecting an identity package"); - return; - } + if (!identityPacket.getType().equals(NetworkPacket.PACKET_TYPE_IDENTITY)) { + Log.e("BTLinkProvider/Server", "2 Expecting an identity package"); + return; + } - Log.i("BTLinkProvider/Server", "Received identity package"); + Log.i("BTLinkProvider/Server", "Received identity package"); - BluetoothLink link = new BluetoothLink(context, socket, - identityPacket.getString("deviceId"), BluetoothLinkProvider.this); + BluetoothLink link = new BluetoothLink(context, socket, + identityPacket.getString("deviceId"), BluetoothLinkProvider.this); + + addLink(identityPacket, link); + } - addLink(identityPacket, link); } } private class ClientRunnable extends BroadcastReceiver implements Runnable { private boolean continueProcessing = true; private final Map connectionThreads = new HashMap<>(); void stopProcessing() { continueProcessing = false; } @Override public void run() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_UUID); context.registerReceiver(this, filter); } while (continueProcessing) { connectToDevices(); try { Thread.sleep(15000); } catch (InterruptedException ignored) { } } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { context.unregisterReceiver(this); } } private void connectToDevices() { Set pairedDevices = bluetoothAdapter.getBondedDevices(); Log.i("BluetoothLinkProvider", "Bluetooth adapter paired devices: " + pairedDevices.size()); // Loop through paired devices for (BluetoothDevice device : pairedDevices) { if (sockets.containsKey(device)) { continue; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { device.fetchUuidsWithSdp(); } else { connectToDevice(device); } } } @Override @TargetApi(value = Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (action.equals(BluetoothDevice.ACTION_UUID)) { BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); Parcelable[] activeUuids = intent.getParcelableArrayExtra(BluetoothDevice.EXTRA_UUID); if (sockets.containsKey(device)) { return; } if (activeUuids == null) { return; } for (Parcelable uuid : activeUuids) { if (uuid.toString().equals(SERVICE_UUID.toString())) { connectToDevice(device); return; } } } } private void connectToDevice(BluetoothDevice device) { if (!connectionThreads.containsKey(device) || !connectionThreads.get(device).isAlive()) { Thread connectionThread = new Thread(new ClientConnect(device)); connectionThread.start(); connectionThreads.put(device, connectionThread); } } } private class ClientConnect implements Runnable { private final BluetoothDevice device; ClientConnect(BluetoothDevice device) { this.device = device; } @Override public void run() { connectToDevice(); } private void connectToDevice() { BluetoothSocket socket; try { socket = device.createRfcommSocketToServiceRecord(SERVICE_UUID); socket.connect(); sockets.put(device, socket); } catch (IOException e) { Log.e("BTLinkProvider/Client", "Could not connect to KDE Connect service on " + device.getAddress(), e); return; } Log.i("BTLinkProvider/Client", "Connected to " + device.getAddress()); try { int character; StringBuilder sb = new StringBuilder(); while (sb.lastIndexOf("\n") == -1 && (character = socket.getInputStream().read()) != -1) { sb.append((char) character); } String message = sb.toString(); final NetworkPacket identityPacket = NetworkPacket.unserialize(message); if (!identityPacket.getType().equals(NetworkPacket.PACKET_TYPE_IDENTITY)) { Log.e("BTLinkProvider/Client", "1 Expecting an identity package"); socket.close(); return; } Log.i("BTLinkProvider/Client", "Received identity package"); String myId = NetworkPacket.createIdentityPacket(context).getString("deviceId"); if (identityPacket.getString("deviceId").equals(myId)) { // Probably won't happen, but just to be safe socket.close(); return; } if (visibleComputers.containsKey(identityPacket.getString("deviceId"))) { return; } Log.i("BTLinkProvider/Client", "Identity package received, creating link"); final BluetoothLink link = new BluetoothLink(context, socket, identityPacket.getString("deviceId"), BluetoothLinkProvider.this); NetworkPacket np2 = NetworkPacket.createIdentityPacket(context); link.sendPacket(np2, new Device.SendPacketStatusCallback() { @Override public void onSuccess() { addLink(identityPacket, link); } @Override public void onFailure(Throwable e) { } }); } catch (Exception e) { Log.e("BTLinkProvider/Client", "Connection lost/disconnected on " + device.getAddress(), e); } } } } diff --git a/src/org/kde/kdeconnect/Backends/LanBackend/LanLink.java b/src/org/kde/kdeconnect/Backends/LanBackend/LanLink.java index f4847fa0..b87fe833 100644 --- a/src/org/kde/kdeconnect/Backends/LanBackend/LanLink.java +++ b/src/org/kde/kdeconnect/Backends/LanBackend/LanLink.java @@ -1,265 +1,262 @@ /* * 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.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 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 SSLSocket socket = null; private final LinkDisconnectedCallback callback; @Override public void disconnect() { Log.i("LanLink/Disconnect","socket:"+ socket.hashCode()); try { socket.close(); } catch (IOException e) { Log.e("LanLink", "Error", e); } } //Returns the old socket public SSLSocket reset(final SSLSocket newSocket, ConnectionStarted connectionSource) throws IOException { SSLSocket 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(() -> { - try { - BufferedReader reader = new BufferedReader(new InputStreamReader(newSocket.getInputStream(), StringsHelper.UTF8)); + 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); } } 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, SSLSocket 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 @Override public boolean sendPacket(NetworkPacket np, final Device.SendPacketStatusCallback callback) { 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; } //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; 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 payloadSocket = SslHelper.convertToSslSocket(context, payloadSocket, getDeviceId(), true, false); outputStream = payloadSocket.getOutputStream(); inputStream = np.getPayload().getInputStream(); 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 (!np.isCanceled() && (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(); Log.i("KDE/LanLink", "Finished sending payload ("+progress+" bytes written)"); } finally { try { server.close(); } catch (Exception ignored) { } try { payloadSocket.close(); } catch (Exception ignored) { } np.getPayload().close(); try { outputStream.close(); } catch (Exception ignored) { } } } if (!np.isCanceled()) { callback.onSuccess(); } return true; } catch (Exception e) { - if (callback != null) { - callback.onFailure(e); - } + callback.onFailure(e); return false; } finally { //Make sure we close the payload stream, if any if (np.hasPayload()) { np.getPayload().close(); } } } private void receivedNetworkPacket(NetworkPacket np) { 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)); payloadSocket = SslHelper.convertToSslSocket(context, payloadSocket, getDeviceId(), true, true); np.setPayload(new NetworkPacket.Payload(payloadSocket, np.getPayloadSize())); } catch (Exception e) { try { payloadSocket.close(); } catch(Exception ignored) { } Log.e("KDE/LanLink", "Exception connecting to payload remote socket", e); } } 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 b2d539fd..3ed3abe2 100644 --- a/src/org/kde/kdeconnect/Backends/LanBackend/LanLinkProvider.java +++ b/src/org/kde/kdeconnect/Backends/LanBackend/LanLinkProvider.java @@ -1,444 +1,443 @@ /* * 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.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.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 { private final static int MIN_PORT = 1716; private final static int MAX_PORT = 1764; final static int PAYLOAD_TRANSFER_MIN_PORT = 1739; private final Context context; private final HashMap visibleComputers = new HashMap<>(); //Links by device id private ServerSocket tcpServer; private DatagramSocket udpServer; private boolean listening = false; // To prevent infinte loop between Android < IceCream because both device can only broadcast identity package but cannot connect via TCP private final 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. private void tcpPacketReceived(Socket socket) { NetworkPacket networkPacket; - try { - BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); + 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) { Log.e("KDE/LanLinkProvider", "Exception while receiving TCP packet", e); 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. private void udpPacketReceived(DatagramPacket packet) { 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; } } 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); 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(); } } } private void configureSocket(Socket socket) { try { socket.setKeepAlive(true); } catch (SocketException e) { Log.e("LanLink", "Exception", e); } } /** * 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); // Do the SSL handshake try { 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, 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(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); 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(() -> { try { synchronized (this) { sslsocket.startHandshake(); } } catch (Exception e) { Log.e("KDE/LanLinkProvider", "Handshake failed with " + identityPacket.getString("deviceName"), e); //String[] ciphers = sslsocket.getSupportedCipherSuites(); //for (String cipher : ciphers) { // Log.i("SupportedCiphers","cipher: " + cipher); //} } }).start(); } catch (Exception e) { Log.e("LanLink", "Exception", e); } } /** * 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 void addLink(final NetworkPacket identityPacket, SSLSocket 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 void setupUdpListener() { try { udpServer = new DatagramSocket(MIN_PORT); udpServer.setReuseAddress(true); udpServer.setBroadcast(true); } catch (SocketException e) { Log.e("LanLinkProvider", "Error creating udp server", e); return; } new Thread(() -> { while (listening) { final int bufferSize = 1024 * 512; byte[] data = new byte[bufferSize]; DatagramPacket packet = new DatagramPacket(data, bufferSize); try { udpServer.receive(packet); udpPacketReceived(packet); } catch (Exception e) { Log.e("LanLinkProvider", "UdpReceive exception", e); } } Log.w("UdpListener", "Stopping UDP listener"); }).start(); } private void setupTcpListener() { try { tcpServer = openServerSocketOnFreePort(MIN_PORT); } catch (Exception e) { Log.e("LanLinkProvider", "Error creating tcp server", e); return; } new Thread(() -> { while (listening) { try { Socket socket = tcpServer.accept(); configureSocket(socket); tcpPacketReceived(socket); } catch (Exception e) { Log.e("LanLinkProvider", "TcpReceive exception", e); } } Log.w("TcpListener", "Stopping TCP listener"); }).start(); } 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++; if (tcpPort == MAX_PORT) { Log.e("KDE/LanLink", "No ports available"); throw e; //Propagate exception } } } throw new RuntimeException("This should not be reachable"); } private void broadcastUdpPacket() { if (NetworkHelper.isOnMobileNetwork(context)) { Log.w("LanLinkProvider", "On 3G network, not sending broadcast."); return; } new Thread(() -> { ArrayList iplist = CustomDevicesActivity .getCustomDeviceList(PreferenceManager.getDefaultSharedPreferences(context)); iplist.add("255.255.255.255"); //Default: broadcast. NetworkPacket identity = NetworkPacket.createIdentityPacket(context); int port = (tcpServer == null || !tcpServer.isBound()) ? MIN_PORT : tcpServer.getLocalPort(); identity.set("tcpPort", 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) { Log.e("KDE/LanLinkProvider", "Failed to create DatagramSocket", e); } 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)); //Log.i("KDE/LanLinkProvider","Udp identity package sent to address "+client); } catch (Exception e) { Log.e("KDE/LanLinkProvider", "Sending udp identity package failed. Invalid address? (" + ipstr + ")", e); } } } if (socket != null) { socket.close(); } }).start(); } @Override public void onStart() { //Log.i("KDE/LanLinkProvider", "onStart"); if (!listening) { listening = true; setupUdpListener(); setupTcpListener(); broadcastUdpPacket(); } } @Override public void onNetworkChange() { broadcastUdpPacket(); } @Override public void onStop() { //Log.i("KDE/LanLinkProvider", "onStop"); listening = false; try { tcpServer.close(); } catch (Exception e) { Log.e("LanLink", "Exception", e); } try { udpServer.close(); } catch (Exception e) { Log.e("LanLink", "Exception", e); } } @Override public String getName() { return "LanLinkProvider"; } } diff --git a/src/org/kde/kdeconnect/Helpers/ContactsHelper.java b/src/org/kde/kdeconnect/Helpers/ContactsHelper.java index 501783a8..5f2a40e2 100644 --- a/src/org/kde/kdeconnect/Helpers/ContactsHelper.java +++ b/src/org/kde/kdeconnect/Helpers/ContactsHelper.java @@ -1,438 +1,438 @@ /* * Copyright 2014 Albert Vaca Cintora * Copyright 2018 Simon Redman * * 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.Helpers; import android.annotation.TargetApi; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Build; import android.provider.ContactsContract; import android.provider.ContactsContract.PhoneLookup; import android.util.Base64; import android.util.Base64OutputStream; import android.util.Log; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import androidx.annotation.RequiresApi; import androidx.collection.LongSparseArray; public class ContactsHelper { /** * Lookup the name and photoID of a contact given a phone number */ @TargetApi(Build.VERSION_CODES.HONEYCOMB) public static Map phoneNumberLookup(Context context, String number) { Map contactInfo = new HashMap<>(); Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)); String[] columns = new String[]{ PhoneLookup.DISPLAY_NAME, PhoneLookup.PHOTO_URI /*, PhoneLookup.TYPE , PhoneLookup.LABEL , PhoneLookup.ID */ }; try (Cursor cursor = context.getContentResolver().query(uri, columns,null, null, null)) { // Take the first match only if (cursor != null && cursor.moveToFirst()) { int nameIndex = cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME); if (nameIndex != -1) { contactInfo.put("name", cursor.getString(nameIndex)); } nameIndex = cursor.getColumnIndex(PhoneLookup.PHOTO_URI); if (nameIndex != -1) { contactInfo.put("photoID", cursor.getString(nameIndex)); } } } catch (Exception ignored) { } return contactInfo; } public static String photoId64Encoded(Context context, String photoId) { if (photoId == null) { return ""; } Uri photoUri = Uri.parse(photoId); ByteArrayOutputStream encodedPhoto = new ByteArrayOutputStream(); try (InputStream input = context.getContentResolver().openInputStream(photoUri); Base64OutputStream output = new Base64OutputStream(encodedPhoto, Base64.DEFAULT)) { byte[] buffer = new byte[1024]; int len; //noinspection ConstantConditions while ((len = input.read(buffer)) != -1) { output.write(buffer, 0, len); } return encodedPhoto.toString(); } catch (Exception ex) { Log.e("ContactsHelper", ex.toString()); return ""; } } /** * Return all the NAME_RAW_CONTACT_IDS which contribute an entry to a Contact in the database *

* If the user has, for example, joined several contacts, on the phone, the IDs returned will * be representative of the joined contact *

* See here: https://developer.android.com/reference/android/provider/ContactsContract.Contacts.html * for more information about the connection between contacts and raw contacts * * @param context android.content.Context running the request * @return List of each NAME_RAW_CONTACT_ID in the Contacts database */ public static List getAllContactContactIDs(Context context) { ArrayList toReturn = new ArrayList<>(); // Define the columns we want to read from the Contacts database final String[] columns = new String[]{ ContactsContract.Contacts.LOOKUP_KEY }; Uri contactsUri = ContactsContract.Contacts.CONTENT_URI; try (Cursor contactsCursor = context.getContentResolver().query(contactsUri, columns, null, null, null)) { if (contactsCursor != null && contactsCursor.moveToFirst()) { do { uID contactID; int idIndex = contactsCursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY); if (idIndex != -1) { contactID = new uID(contactsCursor.getString(idIndex)); } else { // Something went wrong with this contact // If you are experiencing this, please open a bug report indicating how you got here Log.e("ContactsHelper", "Got a contact which does not have a LOOKUP_KEY"); continue; } toReturn.add(contactID); } while (contactsCursor.moveToNext()); } } return toReturn; } /** * Get VCards using the batch database query which requires Android API 21 * * @param context android.content.Context running the request * @param IDs collection of raw contact IDs to look up * @param lookupKeys * @return Mapping of raw contact IDs to corresponding VCard */ @SuppressWarnings("ALL") // Since this method is busted anyway @RequiresApi(Build.VERSION_CODES.LOLLIPOP) @Deprecated protected static Map getVCardsFast(Context context, Collection IDs, Map lookupKeys) { LongSparseArray toReturn = new LongSparseArray<>(); StringBuilder keys = new StringBuilder(); List orderedIDs = new ArrayList<>(IDs); for (Long ID : orderedIDs) { String key = lookupKeys.get(ID); keys.append(key); keys.append(':'); } // Remove trailing ':' keys.deleteCharAt(keys.length() - 1); Uri vcardURI = Uri.withAppendedPath( ContactsContract.Contacts.CONTENT_MULTI_VCARD_URI, Uri.encode(keys.toString())); ; StringBuilder vcardJumble = new StringBuilder(); - try (InputStream input = context.getContentResolver().openInputStream(vcardURI)) { - BufferedReader bufferedInput = new BufferedReader(new InputStreamReader(input)); + try (BufferedReader bufferedInput = new BufferedReader(new InputStreamReader(context.getContentResolver().openInputStream(vcardURI)))) { String line; while ((line = bufferedInput.readLine()) != null) { vcardJumble.append(line).append('\n'); } } catch (IOException e) { // If you are experiencing this, please open a bug report indicating how you got here Log.e("Contacts", "Exception while fetching vcards", e); } // At this point we are screwed: // There is no way to figure out, given the lookup we just made, which VCard belonges // to which ID. They appear to be in the same order as the request was made, but this // is (provably) unreliable. I am leaving this code in case it is useful, but unless // Android improves their API there is nothing we can do with it return null; } /** * Get VCards using serial database lookups. This is tragically slow, but the faster method using * * There is a faster API specified using ContactsContract.Contacts.CONTENT_MULTI_VCARD_URI, * but there does not seem to be a way to figure out which ID resulted in which VCard using that API * * @param context android.content.Context running the request * @param IDs collection of uIDs to look up * @return Mapping of uIDs to the corresponding VCard */ @SuppressWarnings("UnnecessaryContinue") private static Map getVCardsSlow(Context context, Collection IDs) { Map toReturn = new HashMap<>(); for (uID ID : IDs) { String lookupKey = ID.toString(); Uri vcardURI = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_VCARD_URI, lookupKey); try (InputStream input = context.getContentResolver().openInputStream(vcardURI)) { if (input == null) { throw new NullPointerException("ContentResolver did not give us a stream for the VCard for uID " + ID); } - BufferedReader bufferedInput = new BufferedReader(new InputStreamReader(input)); + try (BufferedReader bufferedInput = new BufferedReader(new InputStreamReader(input))) { + StringBuilder vCard = new StringBuilder(); + String line; + while ((line = bufferedInput.readLine()) != null) { + vCard.append(line).append('\n'); + } - StringBuilder vcard = new StringBuilder(); - String line; - while ((line = bufferedInput.readLine()) != null) { - vcard.append(line).append('\n'); + toReturn.put(ID, new VCardBuilder(vCard.toString())); } - toReturn.put(ID, new VCardBuilder(vcard.toString())); } catch (IOException e) { // If you are experiencing this, please open a bug report indicating how you got here Log.e("Contacts", "Exception while fetching vcards", e); } catch (NullPointerException e) { // If you are experiencing this, please open a bug report indicating how you got here Log.e("Contacts", "Exception while fetching vcards", e); } } return toReturn; } /** * Get the VCard for every specified raw contact ID * * @param context android.content.Context running the request * @param IDs collection of raw contact IDs to look up * @return Mapping of raw contact IDs to the corresponding VCard */ public static Map getVCardsForContactIDs(Context context, Collection IDs) { return getVCardsSlow(context, IDs); } /** * Return a mapping of contact IDs to a map of the requested data from the Contacts database *

* If for some reason there is no row associated with the contact ID in the database, * there will not be a corresponding field in the returned map * * @param context android.content.Context running the request * @param IDs collection of contact uIDs to look up * @param contactsProjection List of column names to extract, defined in ContactsContract.Contacts * @return mapping of contact uIDs to desired values, which are a mapping of column names to the data contained there */ @TargetApi(Build.VERSION_CODES.HONEYCOMB) // Needed for Cursor.getType(..) public static Map> getColumnsFromContactsForIDs(Context context, Collection IDs, String[] contactsProjection) { HashMap> toReturn = new HashMap<>(); if (IDs.isEmpty()) { return toReturn; } Uri contactsUri = ContactsContract.Contacts.CONTENT_URI; // Regardless of whether it was requested, we need to look up the uID column Set lookupProjection = new HashSet<>(Arrays.asList(contactsProjection)); lookupProjection.add(uID.COLUMN); // We need a selection which looks like " IN(?,?,...?)" with one ? per ID StringBuilder contactsSelection = new StringBuilder(uID.COLUMN); contactsSelection.append(" IN("); for (int i = 0; i < IDs.size(); i++) { contactsSelection.append("?,"); } // Remove trailing comma contactsSelection.deleteCharAt(contactsSelection.length() - 1); contactsSelection.append(")"); // We need selection arguments as simply a String representation of each ID List contactsArgs = new ArrayList<>(); for (uID ID : IDs) { contactsArgs.add(ID.toString()); } try (Cursor contactsCursor = context.getContentResolver().query( contactsUri, lookupProjection.toArray(new String[0]), contactsSelection.toString(), contactsArgs.toArray(new String[0]), null )) { if (contactsCursor != null && contactsCursor.moveToFirst()) { do { Map requestedData = new HashMap<>(); int lookupKeyIdx = contactsCursor.getColumnIndexOrThrow(uID.COLUMN); String lookupKey = contactsCursor.getString(lookupKeyIdx); // For each column, collect the data from that column for (String column : contactsProjection) { int index = contactsCursor.getColumnIndex(column); // Since we might be getting various kinds of data, Object is the best we can do Object data; int type; if (index == -1) { // This contact didn't have the requested column? Something is very wrong. // If you are experiencing this, please open a bug report indicating how you got here Log.e("ContactsHelper", "Got a contact which does not have a requested column"); continue; } type = contactsCursor.getType(index); switch (type) { case Cursor.FIELD_TYPE_INTEGER: data = contactsCursor.getInt(index); break; case Cursor.FIELD_TYPE_FLOAT: data = contactsCursor.getFloat(index); break; case Cursor.FIELD_TYPE_STRING: data = contactsCursor.getString(index); break; case Cursor.FIELD_TYPE_BLOB: data = contactsCursor.getBlob(index); break; default: Log.e("ContactsHelper", "Got an undefined type of column " + column); continue; } requestedData.put(column, data); } toReturn.put(new uID(lookupKey), requestedData); } while (contactsCursor.moveToNext()); } } return toReturn; } /** * This is a cheap ripoff of com.android.vcard.VCardBuilder *

* Maybe in the future that library will be made public and we can switch to using that! *

* The main similarity is the usage of .toString() to produce the finalized VCard and the * usage of .appendLine(String, String) to add stuff to the vcard */ public static class VCardBuilder { static final String VCARD_END = "END:VCARD"; // Written to terminate the vcard static final String VCARD_DATA_SEPARATOR = ":"; final StringBuilder vcardBody; /** * Take a partial vcard as a string and make a VCardBuilder * * @param vcard vcard to build upon */ VCardBuilder(String vcard) { // Remove the end tag. We will add it back on in .toString() vcard = vcard.substring(0, vcard.indexOf(VCARD_END)); vcardBody = new StringBuilder(vcard); } /** * Appends one line with a given property name and value. */ public void appendLine(final String propertyName, final String rawValue) { vcardBody.append(propertyName) .append(VCARD_DATA_SEPARATOR) .append(rawValue) .append("\n"); } public String toString() { return vcardBody.toString() + VCARD_END; } } /** * Essentially a typedef of the type used for a unique identifier */ public static class uID { /** * We use the LOOKUP_KEY column of the Contacts table as a unique ID, since that's what it's * for */ final String contactLookupKey; /** * Which Contacts column this uID is pulled from */ static final String COLUMN = ContactsContract.Contacts.LOOKUP_KEY; public uID(String lookupKey) { contactLookupKey = lookupKey; } public String toString() { return this.contactLookupKey; } @Override public int hashCode() { return contactLookupKey.hashCode(); } @Override public boolean equals(Object other) { if (other instanceof uID) { return contactLookupKey.equals(((uID) other).contactLookupKey); } return contactLookupKey.equals(other); } } }