KDE Connect Remote Device Network Status Plugin POC

The students list for GSoC 2020 Accepted Proposals was announced today and my Proposal was not accepted.. But during the process of working with My Possible Mentors over the past few months was really helpful and helped me gain a lot of experience. My possible mentors were Simon Redman, Piyyush Aggarwal, Philip C.C , Nicolas Fella from KDE Organisation and I worked with them very closely this time also. I have previously worked with them on SoK and also on some bugs on Android 10 in Ring the Phone Plugin. The team under KDE is just awesome in terms of beginner support.

Abstract on KDE Connect

For those of you who don’t know

KDE Connect is a project that enables communication between all your devices. Here’s a few things KDE Connect can do:

My plan was to make a plugin that shares the remote devices network status using Android SDKs whose implementation has been discussed in detail below. Implementation given below.

The Android Side required writing a plugin that uses the Android SDK’s ConnectivityManager, SubscriptionManager, WifiManager, NetworkCapabilities. CellSignalStrengthCdma, and PhoneStateListener

SubcriptionManager:

The SubscriptionManager will be able to give us a list of all active SIM(s) available on the device using the function ​getActiveSubscriptionInfoList​() ​which returns a list. The SubcriptionManager’s​ ​getActiveDataSubscriptionId​() ​will be able to tell us the current active SIM for data if on data , which can be determined by the ​ConnectivityManager​ as discussed below. SubcriptionManager also has various other methods and fields that can prove very useful during development.

ConnectivityManager:

The ConnectivityManager is able to give us the information on the type of connection the device currently has. ConnectivityManager has fields that check if it is SIM Data or WiFi Network by using methods from ​NetworkCapabilities​.

WifiManager:

If we detect that the current connection is Wi-Fi we can show Wi-Firelated data too. We can take help from the WifiManager’s ​getScanResults()​ to get data on the Wi-Fi with help from other methods like ​calculateSignalLevel() ​and ​getRssi()​ of WifiInfo. Thus we can calculate the strength of the Wi-Fi network.

NetworkCapabilities:

We can get NetworkCapabilities Object from ConnectivityManager Object and tell if the current network is Data Card Based. And use the NetworkCapability Object’s methods like getLinkDownstreamBandwidthKbps()​ ​and​ ​getLinkUpstreamBandwidthKbps() ​to calculate network speeds.

CellSignalStrengthCdma:

The CellSignalStrength class of Telephony of Android SDK has a method called ​getEvdoLevel() which returns the signal strength of the current connection in a level of 0-4 which can be directly mapped to signal level GUI on the desktop app and can be used to show the signal level.

PhoneStateListener

We will be taking help from PhoneStateListener for broadcast receiving and retrieving info about the SIM Network.

The proof of concept code can be found in this commit in my fork of KDE Connect Android Repository.

Currently I am able to send all kinds of Information from Android Side to the Desktop Side. The current implementation in Proof of Concept requires Android P but by providing various fallbacks and some restrictions we can make the plugin work on lower versions of Android.

The proof of Concept uses the NetworkPacket of KDE Connect to send data like all Plugins. Getting any info about the networks is very easy in the current implementation. It is worth noting that we can add a version field so that there can be changes made to the packet structure later on. We just need to call the necessary functions on the objects that are already there from Android API.

The onCreate()

In the onCreate() in the Proof of Concept we are initialising and setting up all the listeners the snippet can be seen below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
@RequiresApi(api = Build.VERSION_CODES.P)
    @Override
    public boolean onCreate() {
        Log.d ("NetworkStatusPlugin", "onCreate: The Network Status Plugin is created");
        subMan= (SubscriptionManager) context.getSystemService (Context.TELEPHONY_SUBSCRIPTION_SERVICE);
        //Listens to Changes in Subscriptions of Device, needs to reset PhoneStateListeners
        new Thread(() -> {
            quitLooper2 = false;
            Looper.prepare();
            subManList= new OnSubscriptionsChangedListener ( ){
                @Override
                public void onSubscriptionsChanged( ) {
                    super.onSubscriptionsChanged ();
                    setupPhoneStateListeners();

                    try {
                        sendPacket();
                    } catch (JSONException e) {
                        e.printStackTrace ( );
                    }


                    if (quitLooper2) {
                        Looper.myLooper().quitSafely ();
                    }
                }
            };
            subMan.addOnSubscriptionsChangedListener (subManList);
            Looper.loop();
        }).start();




        connectivityManager =
                (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            if( connectivityManager != null)
            connectivityManager.registerDefaultNetworkCallback(networkCallback);
        } else {
            request = new NetworkRequest.Builder()
                    .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET).build();
            connectivityManager.registerNetworkCallback(request, networkCallback);
        }
        IntentFilter rssiFilter = new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
        context.registerReceiver(myRssiChangeReceiver, rssiFilter);

        WifiManager wifiMan=(WifiManager)context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
        if(wifiMan != null)
            wifiMan.startScan();
        return true;
    }

The SetupPhoneStateListeners()

This is a function that Sets up PhoneStateListeners. There is a chance that this function is called multiple times, not only at the start but whenever there is a change in Subscriptions. So we need to stop all the current listeners as they are invalid, stop their threads and loops to avoid load on the processor, and initialise new listeners and threads.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
    //Function to set up, on PhoneStateListeners,
    @RequiresApi(api = Build.VERSION_CODES.N) // need to provide fallback, to be implemented during GSoC
    public void setupPhoneStateListeners() {

        telMan = (TelephonyManager) context.getSystemService (Context.TELEPHONY_SERVICE);
        //resetting all listeners, since change in Sim networks during runtime can call this function
        for (int i = 0; i < mTelephonyManagers.size ( ); i++) {
            mTelephonyManagers.get (i).listen (mPhoneStateListeners.get (i), PhoneStateListener.LISTEN_NONE);
        }
        mPhoneStateListeners.clear ( );
        mTelephonyManagers.clear ( );
        quitLooper=true; // quiting all current loops
        //need to check for permission, run-time request needs to implemented and the plugin needs to check for these during startup
        if (ActivityCompat.checkSelfPermission (context, Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {
            // here to request the missing permissions, and then overriding
            //   public void onRequestPermissionsResult(int requestCode, String[] permissions,
            //                                          int[] grantResults)
            // to handle the case where the user grants the permission. See the documentation
            // for ActivityCompat#requestPermissions for more details.
            Log.d ("READ_PHONE_STATE", "setupPhoneStateListeners: Permission Denied Phone State");
            return;
        }
        //PhoneStateListener requires that looper is not null
        final List<SubscriptionInfo> subInfoList = SubscriptionManager.from (context).getActiveSubscriptionInfoList ( );
        if (subInfoList != null) {
            mHasTelephony = true;
            for (int i = 0; i < subInfoList.size ( ); i++) {
                subTelMan = telMan.createForSubscriptionId (subInfoList.get (i).getSubscriptionId ( ));
                new Thread (() -> {
                    quitLooper = false;
                    Looper.prepare ( );
                    subListner = new PhoneStateListener ( ) {
                        @RequiresApi(api = Build.VERSION_CODES.P)
                        @Override
                        public void onSignalStrengthsChanged(SignalStrength signalStrength) {
                            super.onSignalStrengthsChanged (signalStrength);
                            try {
                                sendPacket ( );
                            } catch (JSONException e) {
                                e.printStackTrace ( );
                            }
                            if (quitLooper) {
                                Objects.requireNonNull (Looper.myLooper ( )).quitSafely ( );
                            }
                        }
                    };
                    subTelMan.listen (subListner, PhoneStateListener.LISTEN_SIGNAL_STRENGTHS);
                    Looper.loop ( );
                }).start ( );

                mPhoneStateListeners.add (subListner);
                mTelephonyManagers.add (subTelMan);
            }
        } else mHasTelephony = false;
    }

The sendPacket()

It is self explanatory it sends a Packet, it calls other functions that get the relevant data. I think those functions could be moved to TelephonyHelper.java as they can be useful for other Plugins.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@RequiresApi(api = Build.VERSION_CODES.P)
    public void sendPacket() throws JSONException {
        if (ActivityCompat.checkSelfPermission (context, Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {
            // TODO: Consider calling
            //    ActivityCompat#requestPermissions
            // here to request the missing permissions, and then overriding
            //   public void onRequestPermissionsResult(int requestCode, String[] permissions,
            //                                          int[] grantResults)
            // to handle the case where the user grants the permission. See the documentation
            // for ActivityCompat#requestPermissions for more details.
            Log.d ("sendPacket", "sendPacket: Permission not Granted Read Phone State");
            return;
        }
        List<SubscriptionInfo> subInfoList = SubscriptionManager.from (context).getActiveSubscriptionInfoList ( );
        networkStatus.set("noOfSIMS",subInfoList.size ());
        networkStatus.set("hasTelephony",mHasTelephony);
        hasWifi= wifiState () ;
        networkStatus.set("hasWifi",hasWifi);
        if(hasWifi){
            networkStatus.set("wifiLevel",getWifiLevel());
        }
        JSONArray array= new JSONArray ();
        for(int i=0; i< subInfoList.size ();i++){
            JSONObject obj = new JSONObject ();
            obj.put ("simNo",i);
            obj.put ("carrierId",subInfoList.get(i).getCarrierId ());
            obj.put ("carrierName",subInfoList.get(i).getCarrierName ());
            obj.put("simSlot",subInfoList.get (i).getSimSlotIndex ());
            obj.put("subId",subInfoList.get (i).getSubscriptionId ());
            obj.put("level", getSignalStrengthLevel(subInfoList.get(i).getSubscriptionId ()));
            array.put(obj);
        }
        networkStatus.set("simDetails",array);
        device.sendPacket (networkStatus);
        Log.d("DEBUG", "noOfSIMS :"+ networkStatus.getInt ("noOfSIMS")+ "Tel"+networkStatus.getBoolean ("hasTelephony")+ networkStatus.getJSONArray("simDetails").toString ()+ "Wifi"+networkStatus.getBoolean ("hasWifi")+ "level"+networkStatus.getInt ("level"));
    }

The Helper Functions

I was proposing that these could be moved into the TelephonyHelper.java to be used by other Plugins as these information can be quite helpful.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
    public boolean wifiState() {
        WifiManager wifiManager = (WifiManager) context.getApplicationContext ( ).getSystemService (Context.WIFI_SERVICE);

        if (wifiManager.isWifiEnabled ( )) {
            if (wifiManager.getConnectionInfo ( ).getBSSID ( ) != null)
                return true; // Wifi is enable and it is connected..
            else
                return false;// Wifi is enabled but not connected..
        } else {
            return false; //Wifi is disabled..
        }
    }


    // Gets Wifi Level in the range of 0-4
    public int getWifiLevel() {
        WifiManager wifiManager = (WifiManager) context.getApplicationContext ( ).getSystemService (Context.WIFI_SERVICE);
        WifiInfo wifiInfo = wifiManager.getConnectionInfo ( );
        final int level = WifiManager.calculateSignalLevel (wifiInfo.getRssi ( ), 5);
        return level;

    }
    // Gets the Signal Level of Each SIM
    @RequiresApi(api = Build.VERSION_CODES.P)
    public int getSignalStrengthLevel(int subId){

        TelephonyManager tm= (TelephonyManager)context.getSystemService (Context.TELEPHONY_SERVICE);
        assert tm != null;
        TelephonyManager stm= tm.createForSubscriptionId (subId);
        return Objects.requireNonNull (stm.getSignalStrength ( )).getLevel ();
    }

The onDestroy()

In the onDestroy() we will be unregistering all receivers and stopping all threads. The code is self-explanatory.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
   @RequiresApi(api = Build.VERSION_CODES.P)
    @Override
    public void onDestroy() {
        context.unregisterReceiver(myRssiChangeReceiver);
        subMan.removeOnSubscriptionsChangedListener (subManList);
        connectivityManager.unregisterNetworkCallback (networkCallback);
        for (int i = 0; i < mTelephonyManagers.size ( ); i++) {
            mTelephonyManagers.get (i).listen (mPhoneStateListeners.get (i), PhoneStateListener.LISTEN_NONE);

        }
        mPhoneStateListeners.clear ( );
        mTelephonyManagers.clear ( );
        quitLooper=true;
        quitLooper2=true;





    }

Other Points of Interest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
  @RequiresApi(api = Build.VERSION_CODES.P)
    private ConnectivityManager.NetworkCallback networkCallback = new ConnectivityManager.NetworkCallback ( ) {
        @Override
        public void onAvailable(Network network) {
            try {
                sendPacket ( );
            } catch (JSONException e) {
                e.printStackTrace ( );
            }
        }

        @Override
        public void onLost(Network network) {
            try {
                sendPacket ( );
            } catch (JSONException e) {
                e.printStackTrace ( );
            }
        }

        @Override
        public void onCapabilitiesChanged(Network network, NetworkCapabilities networkCapabilities) {
            try {
                sendPacket ( );
            } catch (JSONException e) {
                e.printStackTrace ( );
            }
        }

        @Override
        public void onLinkPropertiesChanged(Network network, LinkProperties linkProperties) {
            try {
                sendPacket ( );
            } catch (JSONException e) {
                e.printStackTrace ( );
            }

        }
    };


The current code on the desktop side is able to receive updates from the Android Side and print it out as Debug Statements on to the Console.The current implementation on the Desktop side is barebones and just uses the skeleton for a plugin to get debug statements on the terminal.

You can read my full Proposal here