mirror of
https://invent.kde.org/network/kdeconnect-android.git
synced 2025-12-12 20:35:58 +01:00
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user