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.
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
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.
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
.
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.
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.
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.
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.
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;
}
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;
}
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"));
}
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 ();
}
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;
}
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