diff --git a/src/org/kde/kdeconnect/Backends/LanBackend/LanLinkProvider.java b/src/org/kde/kdeconnect/Backends/LanBackend/LanLinkProvider.java index 08995aa6..ebf45f48 100644 --- a/src/org/kde/kdeconnect/Backends/LanBackend/LanLinkProvider.java +++ b/src/org/kde/kdeconnect/Backends/LanBackend/LanLinkProvider.java @@ -1,457 +1,456 @@ /* * 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.SecurityHelpers.SslHelper; import org.kde.kdeconnect.Helpers.StringsHelper; import org.kde.kdeconnect.Helpers.TrustedNetworkHelper; 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 long lastBroadcast = 0; private final static long delayBetweenBroadcasts = 500; 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())); 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) { - if (TrustedNetworkHelper.isNotTrustedNetwork(context)) { - Log.w("LanLinkProvider", "Current WiFi isn't a Trusted Network"); - return; - } - 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(SSLSocket, 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 (System.currentTimeMillis() < lastBroadcast + delayBetweenBroadcasts) { Log.i("LanLinkProvider", "broadcastUdpPacket: relax cowboy"); return; } lastBroadcast = System.currentTimeMillis(); - if (TrustedNetworkHelper.isNotTrustedNetwork(context)) { - Log.w("LanLinkProvider", "Current WiFi isn't a Trusted Network"); - return; - } - new Thread(() -> { ArrayList iplist = CustomDevicesActivity .getCustomDeviceList(PreferenceManager.getDefaultSharedPreferences(context)); - iplist.add("255.255.255.255"); //Default: broadcast. + + if (TrustedNetworkHelper.isTrustedNetwork(context)) { + iplist.add("255.255.255.255"); //Default: broadcast. + } else { + Log.i("LanLinkProvider", "Current network isn't trusted, not broadcasting"); + } + + if (iplist.isEmpty()) { + return; + } 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/TrustedNetworkHelper.java b/src/org/kde/kdeconnect/Helpers/TrustedNetworkHelper.java index 7f677493..040ca644 100644 --- a/src/org/kde/kdeconnect/Helpers/TrustedNetworkHelper.java +++ b/src/org/kde/kdeconnect/Helpers/TrustedNetworkHelper.java @@ -1,97 +1,93 @@ package org.kde.kdeconnect.Helpers; import java.util.Arrays; import java.util.Collections; import java.util.List; import android.Manifest; import android.content.Context; import android.content.pm.PackageManager; import android.net.wifi.SupplicantState; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.preference.PreferenceManager; import android.text.TextUtils; import android.util.Log; import androidx.core.content.ContextCompat; import org.kde.kdeconnect.UserInterface.PermissionsAlertDialogFragment; import org.kde.kdeconnect_tp.R; public class TrustedNetworkHelper { private static final String KEY_CUSTOM_TRUSTED_NETWORKS = "trusted_network_preference"; private static final String KEY_CUSTOM_TRUST_ALL_NETWORKS = "trust_all_network_preference"; private static final String NETWORK_SSID_DELIMITER = "#_#"; private static final String NOT_AVAILABLE_SSID_RESULT = ""; private final Context context; public TrustedNetworkHelper(Context context) { this.context = context; } public List read() { String serializeTrustedNetwork = PreferenceManager.getDefaultSharedPreferences(context).getString( KEY_CUSTOM_TRUSTED_NETWORKS, ""); if (serializeTrustedNetwork.isEmpty()) return Collections.emptyList(); return Arrays.asList(serializeTrustedNetwork.split(NETWORK_SSID_DELIMITER)); } public void update(List trustedNetworks) { String serialized = TextUtils.join(NETWORK_SSID_DELIMITER, trustedNetworks); PreferenceManager.getDefaultSharedPreferences(context).edit().putString( KEY_CUSTOM_TRUSTED_NETWORKS, serialized).apply(); } public boolean allAllowed() { if (!hasPermissions()) { return true; } return PreferenceManager .getDefaultSharedPreferences(context) .getBoolean(KEY_CUSTOM_TRUST_ALL_NETWORKS, Boolean.TRUE); } public void allAllowed(boolean isChecked) { PreferenceManager .getDefaultSharedPreferences(context) .edit() .putBoolean(KEY_CUSTOM_TRUST_ALL_NETWORKS, isChecked) .apply(); } public boolean hasPermissions() { int result = ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION); return (result == PackageManager.PERMISSION_GRANTED); } public String currentSSID() { WifiManager wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE); - Log.d("Fou", "get"); if (wifiManager == null) return ""; WifiInfo wifiInfo = wifiManager.getConnectionInfo(); if (wifiInfo.getSupplicantState() != SupplicantState.COMPLETED) { - Log.d("Fou", "fooo"); return ""; } String ssid = wifiInfo.getSSID(); if (ssid.equalsIgnoreCase(NOT_AVAILABLE_SSID_RESULT)){ - Log.d("Fou", "navail"); return ""; } - Log.d("Fou", "retn"); return ssid; } - public static boolean isNotTrustedNetwork(Context context) { + public static boolean isTrustedNetwork(Context context) { TrustedNetworkHelper trustedNetworkHelper = new TrustedNetworkHelper(context); if (trustedNetworkHelper.allAllowed()){ - return false; + return true; } - return trustedNetworkHelper.read().indexOf(trustedNetworkHelper.currentSSID()) == -1; + return trustedNetworkHelper.read().contains(trustedNetworkHelper.currentSSID()); } } diff --git a/src/org/kde/kdeconnect/UserInterface/PairingFragment.java b/src/org/kde/kdeconnect/UserInterface/PairingFragment.java index 9fd7c73f..2f4d5c23 100644 --- a/src/org/kde/kdeconnect/UserInterface/PairingFragment.java +++ b/src/org/kde/kdeconnect/UserInterface/PairingFragment.java @@ -1,323 +1,323 @@ /* * 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.content.Context; import android.content.Intent; import android.content.res.Resources; import android.net.ConnectivityManager; import android.net.LinkProperties; import android.net.Network; import android.net.NetworkInfo; import android.net.NetworkRequest; import android.os.Build; import android.os.Bundle; import android.provider.Settings; 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 androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Helpers.TrustedNetworkHelper; 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; private boolean listRefreshCalledThisFrame = false; private TextView headerText; private TextView noWifiHeader; private TextView notTrustedText; private Object networkChangeListener; @Override public View onCreateView(@NonNull 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 = rootView.findViewById(R.id.refresh_list_layout); mSwipeRefreshLayout.setOnRefreshListener( this::updateComputerListAction ); notTrustedText = (TextView) inflater.inflate(R.layout.pairing_explanation_not_trusted, null); notTrustedText.setOnClickListener(null); notTrustedText.setOnLongClickListener(null); headerText = (TextView) inflater.inflate(R.layout.pairing_explanation_text, null); headerText.setOnClickListener(null); headerText.setOnLongClickListener(null); noWifiHeader = (TextView) inflater.inflate(R.layout.pairing_explanation_text_no_wifi, null); noWifiHeader.setOnClickListener(view -> { startActivity(new Intent(Settings.ACTION_WIFI_SETTINGS)); }); ((ListView) listRootView).addHeaderView(headerText); if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { networkChangeListener = new ConnectivityManager.NetworkCallback() { @Override public void onAvailable(Network network) { updateDeviceList(); } @Override public void onLost(Network network) { updateDeviceList(); } @Override public void onLinkPropertiesChanged(Network network, LinkProperties linkProperties) { updateDeviceList(); } }; ConnectivityManager connManager = (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE); connManager.registerNetworkCallback(new NetworkRequest.Builder().build(), (ConnectivityManager.NetworkCallback) networkChangeListener); } return rootView; } @Override public void onDestroyView() { if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { ConnectivityManager connManager = (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE); connManager.unregisterNetworkCallback((ConnectivityManager.NetworkCallback) networkChangeListener); } super.onDestroyView(); } @Override public void onAttach(Context context) { super.onAttach(context); mActivity = ((MainActivity) getActivity()); } private void updateComputerListAction() { updateDeviceList(); BackgroundService.RunCommand(mActivity, BackgroundService::onNetworkChange); mSwipeRefreshLayout.setRefreshing(true); new Thread(() -> { try { Thread.sleep(1500); } catch (InterruptedException ignored) { } mActivity.runOnUiThread(() -> mSwipeRefreshLayout.setRefreshing(false)); }).start(); } private void updateDeviceList() { 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; Collection devices = service.getDevices().values(); boolean someDevicesReachable = false; for (Device device : devices) { if (device.isReachable()) { someDevicesReachable = true; } } ((ListView) rootView.findViewById(R.id.devices_list)).removeHeaderView(headerText); ((ListView) rootView.findViewById(R.id.devices_list)).removeHeaderView(noWifiHeader); ((ListView) rootView.findViewById(R.id.devices_list)).removeHeaderView(notTrustedText); ConnectivityManager connManager = (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo wifi = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI); //Check if we're on Wi-Fi. If we still see a device, don't do anything special if (someDevicesReachable || wifi.isConnected()) { - if (TrustedNetworkHelper.isNotTrustedNetwork(getContext())) { - ((ListView) rootView.findViewById(R.id.devices_list)).addHeaderView(notTrustedText); - } else { + if (TrustedNetworkHelper.isTrustedNetwork(getContext())) { ((ListView) rootView.findViewById(R.id.devices_list)).addHeaderView(headerText); + } else { + ((ListView) rootView.findViewById(R.id.devices_list)).addHeaderView(notTrustedText); } } else { ((ListView) rootView.findViewById(R.id.devices_list)).addHeaderView(noWifiHeader); } try { 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 = 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) { //Ignore: The activity was closed while we were trying to update it } finally { listRefreshCalledThisFrame = false; } })); } @Override public void onStart() { super.onStart(); mSwipeRefreshLayout.setEnabled(true); BackgroundService.RunCommand(mActivity, service -> service.addDeviceListChangedCallback("PairingFragment", this::updateDeviceList)); updateDeviceList(); } @Override public void onStop() { super.onStop(); mSwipeRefreshLayout.setEnabled(false); 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_custom_device_list: startActivity(new Intent(mActivity, CustomDevicesActivity.class)); break; case R.id.menu_trusted_networks: startActivity(new Intent(mActivity, TrustedNetworksActivity.class)); break; default: break; } return true; } } diff --git a/src/org/kde/kdeconnect/UserInterface/TrustedNetworksActivity.java b/src/org/kde/kdeconnect/UserInterface/TrustedNetworksActivity.java index bdb96dd0..e6ad33b8 100644 --- a/src/org/kde/kdeconnect/UserInterface/TrustedNetworksActivity.java +++ b/src/org/kde/kdeconnect/UserInterface/TrustedNetworksActivity.java @@ -1,122 +1,138 @@ package org.kde.kdeconnect.UserInterface; import android.Manifest; +import android.content.pm.PackageManager; import android.os.Bundle; import android.view.View; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.CheckBox; import android.widget.ListView; +import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import org.kde.kdeconnect.Helpers.TrustedNetworkHelper; import org.kde.kdeconnect_tp.R; import java.util.ArrayList; import java.util.List; public class TrustedNetworksActivity extends AppCompatActivity { private List trustedNetworks; private ListView trustedNetworksView; private CheckBox allowAllCheckBox; private TrustedNetworkHelper trustedNetworkHelper; + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + boolean grantedPermission = false; + for (int result : grantResults) { + if (result == PackageManager.PERMISSION_GRANTED) { + grantedPermission = true; + break; + } + } + if (grantedPermission) { + allowAllCheckBox.setChecked(false); + } + } + @Override protected void onCreate(Bundle savedInstanceState) { ThemeUtil.setUserPreferredTheme(this); super.onCreate(savedInstanceState); setContentView(R.layout.trusted_network_list); trustedNetworksView = findViewById(android.R.id.list); trustedNetworkHelper = new TrustedNetworkHelper(getApplicationContext()); trustedNetworks = new ArrayList<>(trustedNetworkHelper.read()); allowAllCheckBox = findViewById(R.id.trust_all_networks_checkBox); allowAllCheckBox.setOnCheckedChangeListener((v, isChecked) -> { if (trustedNetworkHelper.hasPermissions()) { trustedNetworkHelper.allAllowed(isChecked); updateTrustedNetworkListView(); addNetworkButton(); } else { allowAllCheckBox.setChecked(true); // Disable unchecking it new PermissionsAlertDialogFragment.Builder() .setTitle(R.string.location_permission_needed_title) .setMessage(R.string.location_permission_needed_desc) .setPositiveButton(R.string.ok) .setNegativeButton(R.string.cancel) .setPermissions(new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}) .setRequestCode(0) .create().show(getSupportFragmentManager(), null); } }); allowAllCheckBox.setChecked(trustedNetworkHelper.allAllowed()); updateTrustedNetworkListView(); } private void updateEmptyListMessage() { boolean isVisible = trustedNetworks.isEmpty() && !trustedNetworkHelper.allAllowed(); findViewById(R.id.trusted_network_list_empty) .setVisibility(isVisible ? View.VISIBLE : View.GONE ); } private void updateTrustedNetworkListView() { Boolean allAllowed = trustedNetworkHelper.allAllowed(); updateEmptyListMessage(); trustedNetworksView.setVisibility(allAllowed ? View.GONE : View.VISIBLE); if (allAllowed){ return; } trustedNetworksView.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, trustedNetworks)); trustedNetworksView.setOnItemClickListener((parent, view, position, id) -> { String targetItem = trustedNetworks.get(position); new AlertDialog.Builder(TrustedNetworksActivity.this) .setMessage("Delete " + targetItem + " ?") .setPositiveButton("Yes", (dialog, which) -> { trustedNetworks.remove(position); trustedNetworkHelper.update(trustedNetworks); ((ArrayAdapter) trustedNetworksView.getAdapter()).notifyDataSetChanged(); addNetworkButton(); updateEmptyListMessage(); }) .setNegativeButton("No", null) .show(); }); addNetworkButton(); } private void addNetworkButton() { Button addButton = findViewById(android.R.id.button1); if (trustedNetworkHelper.allAllowed()) { addButton.setVisibility(View.GONE); return; } final String currentSSID = trustedNetworkHelper.currentSSID(); if (!currentSSID.isEmpty() && trustedNetworks.indexOf(currentSSID) == -1) { String buttonText = getString(R.string.add_trusted_network, currentSSID); addButton.setText(buttonText); addButton.setOnClickListener(v -> { if (trustedNetworks.indexOf(currentSSID) != -1){ return; } trustedNetworks.add(currentSSID); trustedNetworkHelper.update(trustedNetworks); ((ArrayAdapter) trustedNetworksView.getAdapter()).notifyDataSetChanged(); v.setVisibility(View.GONE); updateEmptyListMessage(); }); addButton.setVisibility(View.VISIBLE); } else { addButton.setVisibility(View.GONE); } } }