Rewrite connectivity plugin

Continue the work in 494cb8e333 to not only
register a single subscriptions listener but also one single connectivity
listener for each subscription. This moves most logic to this new shared
helper class and leaves a very simple plugin implementation. This new
implementation also avoids sending packets when the connectivity status
hasn't changed, as well as incomplete updates when starting.
This commit is contained in:
Albert Vaca Cintora
2025-10-08 01:21:50 +02:00
parent 7e0c1ff388
commit da997f4865
7 changed files with 402 additions and 468 deletions

View File

@@ -1,92 +0,0 @@
/*
* SPDX-FileCopyrightText: 2014 The Android Open Source Project
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.kde.kdeconnect.Plugins.ConnectivityReportPlugin;
import android.os.Build;
import android.telephony.CellInfo;
import android.telephony.SignalStrength;
public class ASUUtils {
/**
* Implementation of SignalStrength.toLevel usable from API Level 7+
*/
public static int signalStrengthToLevel(SignalStrength signalStrength) {
int level;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
level = signalStrength.getLevel();
} else {
// Should work on all supported versions, uses copied functions from modern SDKs
// Needs testing
int gsmLevel = signalStrength.getGsmSignalStrength();
if (gsmLevel >= 0 && gsmLevel <= 31) {
// Convert getGsmSignalStrength range (0..31) to getLevel range (0..4)
gsmLevel = gsmLevel * 4 / 31;
} else {
gsmLevel = 0;
}
int cdmaLevel = getCdmaLevel(signalStrength.getCdmaDbm(), signalStrength.getCdmaEcio());
int evdoLevel = getEvdoLevel(signalStrength.getEvdoDbm(), signalStrength.getEvdoSnr());
level = Math.max(gsmLevel, Math.max(cdmaLevel, evdoLevel));
}
return level;
}
/**
* Get cdma as level 0..4
* Adapted from CellSignalStrengthCdma.java
*/
private static int getCdmaLevel(int cdmaDbm, int cdmaEcio) {
int levelDbm;
int levelEcio;
if (cdmaDbm == CellInfo.UNAVAILABLE) levelDbm = 0;
else if (cdmaDbm >= -75) levelDbm = 4;
else if (cdmaDbm >= -85) levelDbm = 3;
else if (cdmaDbm >= -95) levelDbm = 2;
else if (cdmaDbm >= -100) levelDbm = 1;
else levelDbm = 0;
// Ec/Io are in dB*10
if (cdmaEcio == CellInfo.UNAVAILABLE) levelEcio = 0;
else if (cdmaEcio >= -90) levelEcio = 4;
else if (cdmaEcio >= -110) levelEcio = 3;
else if (cdmaEcio >= -130) levelEcio = 2;
else if (cdmaEcio >= -150) levelEcio = 1;
else levelEcio = 0;
return Math.min(levelDbm, levelEcio);
}
/**
* Get Evdo as level 0..4
* Adapted from CellSignalStrengthCdma.java
*/
private static int getEvdoLevel(int evdoDbm, int evdoSnr) {
int levelEvdoDbm;
int levelEvdoSnr;
if (evdoDbm == CellInfo.UNAVAILABLE) levelEvdoDbm = 0;
else if (evdoDbm >= -65) levelEvdoDbm = 4;
else if (evdoDbm >= -75) levelEvdoDbm = 3;
else if (evdoDbm >= -90) levelEvdoDbm = 2;
else if (evdoDbm >= -105) levelEvdoDbm = 1;
else levelEvdoDbm = 0;
if (evdoSnr == CellInfo.UNAVAILABLE) levelEvdoSnr = 0;
else if (evdoSnr >= 7) levelEvdoSnr = 4;
else if (evdoSnr >= 5) levelEvdoSnr = 3;
else if (evdoSnr >= 3) levelEvdoSnr = 2;
else if (evdoSnr >= 1) levelEvdoSnr = 1;
else levelEvdoSnr = 0;
return Math.min(levelEvdoDbm, levelEvdoSnr);
}
}

View File

@@ -0,0 +1,100 @@
/*
* SPDX-FileCopyrightText: 2025 Albert Vaca Cintora <albertvaka@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.Plugins.ConnectivityReportPlugin
import android.os.Build
import android.telephony.CellInfo
import android.telephony.SignalStrength
import android.telephony.TelephonyManager
import kotlin.math.max
import kotlin.math.min
object ASUUtils {
fun signalStrengthToLevel(signalStrength: SignalStrength?): Int {
if (signalStrength == null) return 0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return signalStrength.level
} else {
// Should work on all supported versions, uses copied functions from modern SDKs
// Needs testing
var gsmLevel = signalStrength.gsmSignalStrength
if (gsmLevel >= 0 && gsmLevel <= 31) {
// Convert getGsmSignalStrength range (0..31) to getLevel range (0..4)
gsmLevel = gsmLevel * 4 / 31
} else {
gsmLevel = 0
}
val cdmaLevel = getCdmaLevel(signalStrength.cdmaDbm, signalStrength.cdmaEcio)
val evdoLevel = getEvdoLevel(signalStrength.evdoDbm, signalStrength.evdoSnr)
return max(gsmLevel, max(cdmaLevel, evdoLevel))
}
}
/**
* Get cdma as level 0..4
*/
private fun getCdmaLevel(cdmaDbm: Int, cdmaEcio: Int): Int {
val levelDbm: Int = if (cdmaDbm == CellInfo.UNAVAILABLE) 0
else if (cdmaDbm >= -75) 4
else if (cdmaDbm >= -85) 3
else if (cdmaDbm >= -95) 2
else if (cdmaDbm >= -100) 1
else 0
// Ec/Io are in dB*10
val levelEcio: Int = if (cdmaEcio == CellInfo.UNAVAILABLE) 0
else if (cdmaEcio >= -90) 4
else if (cdmaEcio >= -110) 3
else if (cdmaEcio >= -130) 2
else if (cdmaEcio >= -150) 1
else 0
return min(levelDbm, levelEcio)
}
/**
* Get Evdo as level 0..4
*/
private fun getEvdoLevel(evdoDbm: Int, evdoSnr: Int): Int {
val levelEvdoDbm: Int = if (evdoDbm == CellInfo.UNAVAILABLE) 0
else if (evdoDbm >= -65) 4
else if (evdoDbm >= -75) 3
else if (evdoDbm >= -90) 2
else if (evdoDbm >= -105) 1
else 0
val levelEvdoSnr: Int = if (evdoSnr == CellInfo.UNAVAILABLE) 0
else if (evdoSnr >= 7) 4
else if (evdoSnr >= 5) 3
else if (evdoSnr >= 3) 2
else if (evdoSnr >= 1) 1
else 0
return min(levelEvdoDbm, levelEvdoSnr)
}
fun networkTypeToString(networkType: Int): String {
return when (networkType) {
TelephonyManager.NETWORK_TYPE_NR -> "5G"
TelephonyManager.NETWORK_TYPE_LTE -> "LTE"
TelephonyManager.NETWORK_TYPE_CDMA, TelephonyManager.NETWORK_TYPE_TD_SCDMA -> "CDMA"
TelephonyManager.NETWORK_TYPE_EDGE -> "EDGE"
TelephonyManager.NETWORK_TYPE_GPRS -> "GPRS"
TelephonyManager.NETWORK_TYPE_GSM -> "GSM"
TelephonyManager.NETWORK_TYPE_HSDPA, TelephonyManager.NETWORK_TYPE_HSPA, TelephonyManager.NETWORK_TYPE_HSPAP, TelephonyManager.NETWORK_TYPE_HSUPA -> "HSPA"
TelephonyManager.NETWORK_TYPE_UMTS -> "UMTS"
TelephonyManager.NETWORK_TYPE_EHRPD, TelephonyManager.NETWORK_TYPE_EVDO_0, TelephonyManager.NETWORK_TYPE_EVDO_A, TelephonyManager.NETWORK_TYPE_EVDO_B, TelephonyManager.NETWORK_TYPE_1xRTT -> "CDMA2000"
TelephonyManager.NETWORK_TYPE_IDEN -> "iDEN"
TelephonyManager.NETWORK_TYPE_IWLAN, TelephonyManager.NETWORK_TYPE_UNKNOWN -> "Unknown"
else -> "Unknown"
}
}
}

View File

@@ -0,0 +1,208 @@
/*
* SPDX-FileCopyrightText: 2025 Albert Vaca Cintora <albertvaka@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.Plugins.ConnectivityReportPlugin
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.telephony.PhoneStateListener
import android.telephony.SignalStrength
import android.telephony.SubscriptionManager
import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener
import android.telephony.TelephonyManager
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
/**
* Registers a listener for changes in connectivity for the device.
*/
@SuppressLint("MissingPermission")
class ConnectivityListener(context: Context) {
val context : Context = context.applicationContext
data class SubscriptionState(var signalStrength: Int = 0, var networkType: String = "Unknown") {
@RequiresApi(Build.VERSION_CODES.P)
constructor(tm: TelephonyManager) : this(ASUUtils.signalStrengthToLevel(tm.signalStrength), ASUUtils.networkTypeToString(tm.dataNetworkType))
}
interface StateCallback {
fun statesChanged(states: Map<Int, SubscriptionState>)
}
companion object {
private const val TAG: String = "ConnectivityListener"
private var instance: ConnectivityListener? = null
@JvmStatic
fun getInstance(context: Context): ConnectivityListener {
if (instance == null) {
instance = ConnectivityListener(context)
}
return instance!!
}
}
private val connectivityListeners = mutableMapOf<Int?, PhoneStateListener?>()
private val states = mutableMapOf<Int, SubscriptionState>() // by subscription ID
private val externalListeners = mutableSetOf<StateCallback>()
private val activeIDs = mutableSetOf<Int>()
private fun statesChanged() {
externalListeners.forEach {
it.statesChanged(states)
}
}
val subscriptionsListener: OnSubscriptionsChangedListener by lazy {
@RequiresApi(Build.VERSION_CODES.N)
object : OnSubscriptionsChangedListener() {
override fun onSubscriptionsChanged() {
val nextSubs = getActiveSubscriptionIDs().toSet()
val addedSubs = nextSubs - activeIDs
val removedSubs = activeIDs - nextSubs
activeIDs.removeAll(removedSubs)
activeIDs.addAll(addedSubs)
val tm = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
for (subID in removedSubs) {
Log.i(TAG, "Removed subscription ID $subID")
tm.listen(connectivityListeners.get(subID), PhoneStateListener.LISTEN_NONE)
connectivityListeners.remove(subID)
states.remove(subID)
statesChanged()
}
for (subID in addedSubs) {
val subTm = tm.createForSubscriptionId(subID)
Log.i(TAG, "Added subscription ID $subID")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
states[subID] = SubscriptionState(subTm)
} else {
states[subID] = SubscriptionState()
}
val listener = createListenerForSubscription(subID)
connectivityListeners[subID] = listener
subTm.listen(listener, PhoneStateListener.LISTEN_SIGNAL_STRENGTHS or PhoneStateListener.LISTEN_DATA_CONNECTION_STATE)
}
}
}
}
fun listenStateChanges(listener: StateCallback) {
var wasEmpty : Boolean
synchronized(externalListeners) {
wasEmpty = externalListeners.isEmpty()
externalListeners.add(listener)
listener.statesChanged(states)
}
Log.d(TAG, "listeners: ${externalListeners.size}")
if (wasEmpty) {
startListening()
}
}
fun cancelActiveListener(listener: StateCallback) {
var isEmpty : Boolean
synchronized(externalListeners) {
externalListeners.remove(listener)
isEmpty = externalListeners.isEmpty()
}
if (isEmpty) {
stopListening()
}
}
private fun startListening() {
runOnMainThread {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// Multi-SIM supported on Nougat+
val sm = ContextCompat.getSystemService(context, SubscriptionManager::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
sm?.addOnSubscriptionsChangedListener(context.mainExecutor, subscriptionsListener)
} else {
sm?.addOnSubscriptionsChangedListener(subscriptionsListener)
}
} else {
// Fallback to single SIM
connectivityListeners.put(0, createListenerForSubscription(0))
states.put(0, SubscriptionState())
}
}
}
private fun stopListening() {
runOnMainThread {
val tm = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val sm = ContextCompat.getSystemService(context, SubscriptionManager::class.java)
sm?.removeOnSubscriptionsChangedListener(subscriptionsListener)
}
for (subID in connectivityListeners.keys) {
Log.i(TAG, "Removed subscription ID $subID")
tm.listen(connectivityListeners.get(subID), PhoneStateListener.LISTEN_NONE)
}
connectivityListeners.clear()
states.clear()
}
}
private fun runOnMainThread(r: Runnable) {
Handler(Looper.getMainLooper()).post(r)
}
private fun createListenerForSubscription(subID: Int): PhoneStateListener {
return object : PhoneStateListener() {
override fun onSignalStrengthsChanged(signalStrength: SignalStrength) {
val state = states[subID]
if (state != null) {
val newStrength = ASUUtils.signalStrengthToLevel(signalStrength)
if (newStrength != state.signalStrength) {
state.signalStrength = newStrength
statesChanged()
}
}
}
override fun onDataConnectionStateChanged(ignore: Int, networkType: Int) {
val state = states[subID]
if (state != null) {
val newNetworkType = ASUUtils.networkTypeToString(networkType)
if (newNetworkType != state.networkType) {
state.networkType = newNetworkType
statesChanged()
}
}
}
}
}
/**
* Get all subscriptionIDs (SIM cards) of the device
*/
@Throws(SecurityException::class)
fun getActiveSubscriptionIDs(): List<Int> {
val subscriptionManager = ContextCompat.getSystemService(context, SubscriptionManager::class.java)
if (subscriptionManager == null) {
Log.w(TAG, "Could not get SubscriptionManager")
return emptyList()
}
val subscriptionInfos = subscriptionManager.activeSubscriptionInfoList
if (subscriptionInfos == null) {
// This happens when there is no SIM card inserted
Log.w(TAG, "Could not get SubscriptionInfos")
return emptyList()
}
return subscriptionInfos.map { it.subscriptionId }
}
}

View File

@@ -1,246 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 David Shlemayev <david.shlemayev@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.Plugins.ConnectivityReportPlugin;
import android.Manifest;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.telephony.PhoneStateListener;
import android.telephony.SignalStrength;
import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
import android.telephony.TelephonyManager;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import org.jetbrains.annotations.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
import org.kde.kdeconnect.Helpers.SMSHelper;
import org.kde.kdeconnect.Helpers.TelephonyHelper;
import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect.Plugins.PluginFactory;
import org.kde.kdeconnect_tp.R;
import java.util.HashMap;
import java.util.Objects;
@PluginFactory.LoadablePlugin
public class ConnectivityReportPlugin extends Plugin {
/**
* Packet used to report the current connectivity state
* <p>
* The body should contain a key "signalStrengths" which has a dict that maps
* a SubscriptionID (opaque value) to a dict with the connection info (See below)
* <p>
* For example:
* {
* "signalStrengths": {
* "6": {
* "networkType": "4G",
* "signalStrength": 3
* },
* "17": {
* "networkType": "HSPA",
* "signalStrength": 2
* },
* ...
* }
* }
*/
private final static String PACKET_TYPE_CONNECTIVITY_REPORT = "kdeconnect.connectivity_report";
private final NetworkPacket connectivityInfo = new NetworkPacket(PACKET_TYPE_CONNECTIVITY_REPORT);
private final HashMap<Integer, PhoneStateListener> listeners = new HashMap<>();
private final HashMap<Integer, SubscriptionState> states = new HashMap<>();
@RequiresApi(api = Build.VERSION_CODES.N)
TelephonySubscriptionsListener.SubscriptionCallback subListener = new TelephonySubscriptionsListener.SubscriptionCallback() {
@Override
public void onAdd(int subID) {
TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
TelephonyManager subTm = tm.createForSubscriptionId(subID);
Log.i("ConnectivityReport", "Added subscription ID " + subID);
states.put(subID, new SubscriptionState(subID));
PhoneStateListener listener = createListener(subID);
listeners.put(subID, listener);
subTm.listen(listener, PhoneStateListener.LISTEN_SIGNAL_STRENGTHS | PhoneStateListener.LISTEN_DATA_CONNECTION_STATE);
}
@Override
public void onRemove(int subID) {
Log.i("ConnectivityReport", "Removed subscription ID " + subID);
TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
tm.listen(listeners.get(subID), PhoneStateListener.LISTEN_NONE);
listeners.remove(subID);
states.remove(subID);
}
};
@Override
public @NonNull String getDisplayName() {
return context.getResources().getString(R.string.pref_plugin_connectivity_report);
}
@Override
public @NonNull String getDescription() {
return context.getResources().getString(R.string.pref_plugin_connectivity_report_desc);
}
private String networkTypeToString(int networkType) {
switch (networkType) {
case TelephonyManager.NETWORK_TYPE_NR:
return "5G";
case TelephonyManager.NETWORK_TYPE_LTE:
return "LTE";
case TelephonyManager.NETWORK_TYPE_CDMA:
case TelephonyManager.NETWORK_TYPE_TD_SCDMA:
return "CDMA";
case TelephonyManager.NETWORK_TYPE_EDGE:
return "EDGE";
case TelephonyManager.NETWORK_TYPE_GPRS:
return "GPRS";
case TelephonyManager.NETWORK_TYPE_GSM:
return "GSM";
case TelephonyManager.NETWORK_TYPE_HSDPA:
case TelephonyManager.NETWORK_TYPE_HSPA:
case TelephonyManager.NETWORK_TYPE_HSPAP:
case TelephonyManager.NETWORK_TYPE_HSUPA:
return "HSPA";
case TelephonyManager.NETWORK_TYPE_UMTS:
return "UMTS";
case TelephonyManager.NETWORK_TYPE_EHRPD:
case TelephonyManager.NETWORK_TYPE_EVDO_0:
case TelephonyManager.NETWORK_TYPE_EVDO_A:
case TelephonyManager.NETWORK_TYPE_EVDO_B:
case TelephonyManager.NETWORK_TYPE_1xRTT:
return "CDMA2000";
case TelephonyManager.NETWORK_TYPE_IDEN:
return "iDEN";
case TelephonyManager.NETWORK_TYPE_IWLAN:
case TelephonyManager.NETWORK_TYPE_UNKNOWN:
default:
return "Unknown";
}
}
private void serializeSignalStrengths() {
JSONObject signalStrengths = new JSONObject();
for (Integer subID : states.keySet()) {
try {
JSONObject subInfo = new JSONObject();
SubscriptionState subscriptionState = Objects.requireNonNull(states.get(subID));
subInfo.put("networkType", subscriptionState.networkType);
subInfo.put("signalStrength", subscriptionState.signalStrength);
signalStrengths.put(subID.toString(), subInfo);
} catch (JSONException e) {
e.printStackTrace();
}
}
connectivityInfo.set("signalStrengths", signalStrengths);
}
private void runWithLooper(Runnable r) {
// We use the MessageLooper to avoid creating an extra thread for this
new Handler(Objects.requireNonNull(SMSHelper.MessageLooper.getLooper())).post(r);
}
private PhoneStateListener createListener(Integer subID) {
return new PhoneStateListener() {
@Override
public void onSignalStrengthsChanged(SignalStrength signalStrength) {
int level = ASUUtils.signalStrengthToLevel(signalStrength);
SubscriptionState state = states.get(subID);
if (state != null) {
state.signalStrength = level;
}
serializeSignalStrengths();
getDevice().sendPacket(connectivityInfo);
//Log.i("ConnectivityReport", "signalStrength of #" + subID + " updated to " + level);
}
@Override
public void onDataConnectionStateChanged(int _state, int networkType) {
SubscriptionState state = states.get(subID);
if (state != null) {
state.networkType = networkTypeToString(networkType);
}
serializeSignalStrengths();
getDevice().sendPacket(connectivityInfo);
//Log.i("ConnectivityReport", "networkType of #" + subID + " updated to " + networkTypeToString(networkType));
}
};
}
@Override
public boolean onCreate() {
serializeSignalStrengths();
runWithLooper(() -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// Multi-SIM supported on Nougat+
TelephonySubscriptionsListener.getInstance(context).listenActiveSubscriptionIDs(subListener);
} else {
// Fallback to single SIM
listeners.put(0, createListener(0));
states.put(0, new SubscriptionState(0));
}
});
return true;
}
@Override
public void onDestroy() {
runWithLooper(() -> {
TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
TelephonySubscriptionsListener.getInstance(context).cancelActiveSubscriptionIDsListener(subListener);
}
for (Integer subID : listeners.keySet()) {
Log.i("ConnectivityReport", "Removed subscription ID " + subID);
tm.listen(listeners.get(subID), PhoneStateListener.LISTEN_NONE);
}
listeners.clear();
states.clear();
});
}
@Override
public boolean onPacketReceived(@NonNull NetworkPacket np) {
return false;
}
@Override
public @NonNull String[] getSupportedPacketTypes() {
return new String[]{};
}
@Override
public @NonNull String[] getOutgoingPacketTypes() {
return new String[]{PACKET_TYPE_CONNECTIVITY_REPORT};
}
@Override
public @NonNull String[] getRequiredPermissions() {
return new String[]{
Manifest.permission.READ_PHONE_STATE,
};
}
}

View File

@@ -0,0 +1,94 @@
/*
* SPDX-FileCopyrightText: 2025 Albert Vaca Cintora <albertvaka@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.Plugins.ConnectivityReportPlugin
import android.Manifest
import android.util.Log
import org.json.JSONException
import org.json.JSONObject
import org.kde.kdeconnect.NetworkPacket
import org.kde.kdeconnect.Plugins.ConnectivityReportPlugin.ConnectivityListener.Companion.getInstance
import org.kde.kdeconnect.Plugins.ConnectivityReportPlugin.ConnectivityListener.SubscriptionState
import org.kde.kdeconnect.Plugins.Plugin
import org.kde.kdeconnect.Plugins.PluginFactory.LoadablePlugin
import org.kde.kdeconnect_tp.R
@LoadablePlugin
class ConnectivityReportPlugin : Plugin() {
override val displayName: String
get() = context.resources.getString(R.string.pref_plugin_connectivity_report)
override val description: String
get() = context.resources.getString(R.string.pref_plugin_connectivity_report_desc)
/**
* Packet used to report the current connectivity state
*
* The body should contain a key "signalStrengths" which has a dict that maps
* a SubscriptionID (opaque value) to a dict with the connection info (See below)
*
* For example:
* {
* "signalStrengths": {
* "6": {
* "networkType": "4G",
* "signalStrength": 3
* },
* "17": {
* "networkType": "HSPA",
* "signalStrength": 2
* },
* ...
* }
* }
*/
private val connectivityInfo = NetworkPacket(PACKET_TYPE_CONNECTIVITY_REPORT)
var listener = object : ConnectivityListener.StateCallback {
override fun statesChanged(states : Map<Int, SubscriptionState>) {
if (states.isEmpty()) {
return
}
val signalStrengths = JSONObject()
states.forEach { (subID: Int, subscriptionState: SubscriptionState) ->
try {
val subInfo = JSONObject()
subInfo.put("networkType", subscriptionState.networkType)
subInfo.put("signalStrength", subscriptionState.signalStrength)
signalStrengths.put(subID.toString(), subInfo)
} catch (e: JSONException) {
e.printStackTrace()
}
}
connectivityInfo["signalStrengths"] = signalStrengths
device.sendPacket(connectivityInfo)
}
}
override fun onCreate(): Boolean {
getInstance(context).listenStateChanges(listener)
return true
}
override fun onDestroy() {
getInstance(context).cancelActiveListener(listener)
}
override fun onPacketReceived(np: NetworkPacket): Boolean {
return false
}
override val supportedPacketTypes: Array<String> = emptyArray()
override val outgoingPacketTypes: Array<String> = arrayOf(PACKET_TYPE_CONNECTIVITY_REPORT)
override val requiredPermissions: Array<String> = arrayOf(Manifest.permission.READ_PHONE_STATE)
companion object {
private const val PACKET_TYPE_CONNECTIVITY_REPORT = "kdeconnect.connectivity_report"
}
}

View File

@@ -1,17 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 David Shlemayev <david.shlemayev@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.Plugins.ConnectivityReportPlugin;
public class SubscriptionState {
final int subId;
int signalStrength = 0;
String networkType = "Unknown";
public SubscriptionState(int subId) {
this.subId = subId;
}
}

View File

@@ -1,113 +0,0 @@
package org.kde.kdeconnect.Plugins.ConnectivityReportPlugin
import android.content.Context
import android.os.Build
import android.telephony.SubscriptionManager
import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener
import android.util.Log
import androidx.core.content.ContextCompat
import org.kde.kdeconnect.Helpers.TelephonyHelper
/**
* Registers a listener for changes in subscriptionIDs for the device.
* This lets you identify additions/removals of SIM cards.
*/
class TelephonySubscriptionsListener(context: Context) {
interface SubscriptionCallback {
fun onAdd(subscriptionID: Int)
fun onRemove(subscriptionID: Int)
}
val context : Context = context.applicationContext
companion object {
private var instance: TelephonySubscriptionsListener? = null
@JvmStatic
fun getInstance(context: Context): TelephonySubscriptionsListener {
if (instance == null) {
instance = TelephonySubscriptionsListener(context)
}
return instance!!
}
}
val listeners = mutableSetOf<SubscriptionCallback>()
val activeIDs = mutableSetOf<Int>()
val systemListener: OnSubscriptionsChangedListener = object : OnSubscriptionsChangedListener() {
override fun onSubscriptionsChanged() {
val nextSubs = getActiveSubscriptionIDs().toSet()
val addedSubs = nextSubs - activeIDs
val removedSubs = activeIDs - nextSubs
activeIDs.removeAll(removedSubs)
activeIDs.addAll(addedSubs)
listeners.forEach { listener ->
removedSubs.forEach(listener::onRemove)
addedSubs.forEach(listener::onAdd)
}
}
}
fun listenActiveSubscriptionIDs(listener: SubscriptionCallback) {
var wasEmpty : Boolean
synchronized(listeners) {
wasEmpty = listeners.isEmpty()
listeners.add(listener)
}
Log.d("TelephonySubscriptionsListener", "listeners: ${listeners.size}")
if (wasEmpty) {
startListening()
}
}
fun cancelActiveSubscriptionIDsListener(listener: SubscriptionCallback) {
var isEmpty : Boolean
synchronized(listeners) {
listeners.remove(listener)
isEmpty = listeners.isEmpty()
}
if (isEmpty) {
stopListening()
}
}
private fun startListening() {
val sm = ContextCompat.getSystemService(context, SubscriptionManager::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
sm?.addOnSubscriptionsChangedListener(context.mainExecutor, systemListener)
} else {
sm?.addOnSubscriptionsChangedListener(systemListener)
}
}
private fun stopListening() {
val sm = ContextCompat.getSystemService(context, SubscriptionManager::class.java)
sm?.removeOnSubscriptionsChangedListener(systemListener)
}
/**
* Get all subscriptionIDs of the device
* As far as I can tell, this is essentially a way of identifying particular SIM cards
*/
@Throws(SecurityException::class)
fun getActiveSubscriptionIDs(): List<Int> {
val subscriptionManager = ContextCompat.getSystemService(context, SubscriptionManager::class.java)
if (subscriptionManager == null) {
Log.w(TelephonyHelper.LOGGING_TAG, "Could not get SubscriptionManager")
return emptyList()
}
val subscriptionInfos = subscriptionManager.activeSubscriptionInfoList
if (subscriptionInfos == null) {
// This happens when there is no SIM card inserted
Log.w(TelephonyHelper.LOGGING_TAG, "Could not get SubscriptionInfos")
return emptyList()
}
return subscriptionInfos.map { it.subscriptionId }
}
}