kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
Fix #11: we now keep a record of past messages in the persistent service state
rodzic
a10e02ecdf
commit
5784138c96
|
@ -2,6 +2,7 @@
|
||||||
package com.geeksville.mesh;
|
package com.geeksville.mesh;
|
||||||
|
|
||||||
// Declare any non-default types here with import statements
|
// Declare any non-default types here with import statements
|
||||||
|
// import com.geeksville.mesh.DataPacket;
|
||||||
|
|
||||||
parcelable NodeInfo;
|
parcelable NodeInfo;
|
||||||
|
|
||||||
|
@ -45,6 +46,9 @@ interface IMeshService {
|
||||||
/// It returns a RadioConfig protobuf.
|
/// It returns a RadioConfig protobuf.
|
||||||
byte []getRadioConfig();
|
byte []getRadioConfig();
|
||||||
|
|
||||||
|
/// Return an list of MeshPacket protobuf (byte arrays) which were received while your client app was offline (recent messages only)
|
||||||
|
List getOldMessages();
|
||||||
|
|
||||||
/// This method is only intended for use in our GUI, so the user can set radio options
|
/// This method is only intended for use in our GUI, so the user can set radio options
|
||||||
/// It sets a RadioConfig protobuf
|
/// It sets a RadioConfig protobuf
|
||||||
void setRadioConfig(in byte []payload);
|
void setRadioConfig(in byte []payload);
|
||||||
|
@ -55,7 +59,7 @@ interface IMeshService {
|
||||||
String connectionState();
|
String connectionState();
|
||||||
|
|
||||||
// see com.geeksville.com.geeksville.mesh broadcast intents
|
// see com.geeksville.com.geeksville.mesh broadcast intents
|
||||||
// RECEIVED_OPAQUE for data received from other nodes
|
// RECEIVED_OPAQUE for data received from other nodes. payload will contain a DataPacket
|
||||||
// NODE_CHANGE for new IDs appearing or disappearing
|
// NODE_CHANGE for new IDs appearing or disappearing
|
||||||
// CONNECTION_CHANGED for losing/gaining connection to the packet radio
|
// CONNECTION_CHANGED for losing/gaining connection to the packet radio
|
||||||
}
|
}
|
||||||
|
|
|
@ -557,25 +557,16 @@ class MainActivity : AppCompatActivity(), Logging,
|
||||||
}
|
}
|
||||||
|
|
||||||
MeshService.ACTION_RECEIVED_DATA -> {
|
MeshService.ACTION_RECEIVED_DATA -> {
|
||||||
debug("TODO rxdata")
|
debug("received new data from service")
|
||||||
val sender =
|
|
||||||
intent.getStringExtra(EXTRA_SENDER)!!
|
|
||||||
val payload =
|
val payload =
|
||||||
intent.getByteArrayExtra(EXTRA_PAYLOAD)!!
|
intent.getParcelableExtra<DataPacket>(EXTRA_PAYLOAD)!!
|
||||||
val typ = intent.getIntExtra(EXTRA_TYP, -1)
|
|
||||||
|
|
||||||
when (typ) {
|
when (payload.dataType) {
|
||||||
MeshProtos.Data.Type.CLEAR_TEXT_VALUE -> {
|
MeshProtos.Data.Type.CLEAR_TEXT_VALUE -> {
|
||||||
// FIXME - use the real time from the packet
|
model.messagesState.addMessage(payload)
|
||||||
// FIXME - don't just slam in a new list each time, it probably causes extra drawing. Figure out how to be Compose smarter...
|
|
||||||
val msg = TextMessage(
|
|
||||||
sender,
|
|
||||||
payload.toString(utf8)
|
|
||||||
)
|
|
||||||
|
|
||||||
model.messagesState.addMessage(msg)
|
|
||||||
}
|
}
|
||||||
else -> TODO()
|
else ->
|
||||||
|
errormsg("Unhandled dataType ${payload.dataType}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MeshService.ACTION_MESH_CONNECTED -> {
|
MeshService.ACTION_MESH_CONNECTED -> {
|
||||||
|
@ -604,6 +595,11 @@ class MainActivity : AppCompatActivity(), Logging,
|
||||||
// We don't start listening for packets until after we are connected to the service
|
// We don't start listening for packets until after we are connected to the service
|
||||||
registerMeshReceiver()
|
registerMeshReceiver()
|
||||||
|
|
||||||
|
// Init our messages table with the service's record of past text messages
|
||||||
|
model.messagesState.messages.value = (service.oldMessages as List<DataPacket>).map {
|
||||||
|
TextMessage(it)
|
||||||
|
}
|
||||||
|
|
||||||
// We won't receive a notify for the initial state of connection, so we force an update here
|
// We won't receive a notify for the initial state of connection, so we force an update here
|
||||||
val connectionState =
|
val connectionState =
|
||||||
MeshService.ConnectionState.valueOf(service.connectionState())
|
MeshService.ConnectionState.valueOf(service.connectionState())
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.geeksville.mesh.ui.bearing
|
import com.geeksville.mesh.ui.bearing
|
||||||
import com.geeksville.mesh.ui.latLongToMeter
|
import com.geeksville.mesh.ui.latLongToMeter
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -16,7 +17,51 @@ import com.geeksville.mesh.ui.latLongToMeter
|
||||||
val Any?.anonymized: String
|
val Any?.anonymized: String
|
||||||
get() = if (this != null) ("..." + this.toString().takeLast(3)) else "null"
|
get() = if (this != null) ("..." + this.toString().takeLast(3)) else "null"
|
||||||
|
|
||||||
|
//
|
||||||
// model objects that directly map to the corresponding protobufs
|
// model objects that directly map to the corresponding protobufs
|
||||||
|
//
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A parcelable version of the protobuf MeshPacket + Data subpacket.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class DataPacket(
|
||||||
|
val from: String, // a nodeID string
|
||||||
|
val to: String, // a nodeID string
|
||||||
|
val rxTime: Long, // msecs since 1970
|
||||||
|
val id: Int,
|
||||||
|
val dataType: Int,
|
||||||
|
val bytes: ByteArray
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
// Autogenerated comparision, because we have a byte array
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as DataPacket
|
||||||
|
|
||||||
|
if (from != other.from) return false
|
||||||
|
if (to != other.to) return false
|
||||||
|
if (rxTime != other.rxTime) return false
|
||||||
|
if (id != other.id) return false
|
||||||
|
if (dataType != other.dataType) return false
|
||||||
|
if (!bytes.contentEquals(other.bytes)) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = from.hashCode()
|
||||||
|
result = 31 * result + to.hashCode()
|
||||||
|
result = 31 * result + rxTime.hashCode()
|
||||||
|
result = 31 * result + id
|
||||||
|
result = 31 * result + dataType
|
||||||
|
result = 31 * result + bytes.contentHashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class MeshUser(val id: String, val longName: String, val shortName: String) :
|
data class MeshUser(val id: String, val longName: String, val shortName: String) :
|
||||||
Parcelable {
|
Parcelable {
|
||||||
constructor(parcel: Parcel) : this(
|
constructor(parcel: Parcel) : this(
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.os.RemoteException
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.geeksville.android.BuildUtils.isEmulator
|
import com.geeksville.android.BuildUtils.isEmulator
|
||||||
import com.geeksville.android.Logging
|
import com.geeksville.android.Logging
|
||||||
|
import com.geeksville.mesh.DataPacket
|
||||||
import com.geeksville.mesh.MeshProtos
|
import com.geeksville.mesh.MeshProtos
|
||||||
import com.geeksville.mesh.utf8
|
import com.geeksville.mesh.utf8
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
@ -18,7 +19,14 @@ data class TextMessage(
|
||||||
val text: String,
|
val text: String,
|
||||||
val date: Date = Date(),
|
val date: Date = Date(),
|
||||||
val errorMessage: String? = null
|
val errorMessage: String? = null
|
||||||
)
|
) {
|
||||||
|
/// We can auto init from data packets
|
||||||
|
constructor(payload: DataPacket) : this(
|
||||||
|
payload.from,
|
||||||
|
payload.bytes.toString(utf8),
|
||||||
|
date = Date(payload.rxTime)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class MessagesState(private val ui: UIViewModel) : Logging {
|
class MessagesState(private val ui: UIViewModel) : Logging {
|
||||||
|
@ -41,10 +49,14 @@ class MessagesState(private val ui: UIViewModel) : Logging {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// add a message our GUI list of past msgs
|
/// add a message our GUI list of past msgs
|
||||||
fun addMessage(m: TextMessage) {
|
private fun addMessage(m: TextMessage) {
|
||||||
|
// FIXME - don't just slam in a new list each time, it probably causes extra drawing.
|
||||||
messages.value = messages.value!! + m
|
messages.value = messages.value!! + m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add a message that was encapsulated in a data packet
|
||||||
|
fun addMessage(payload: DataPacket) = addMessage(TextMessage(payload))
|
||||||
|
|
||||||
/// Send a message and added it to our GUI log
|
/// Send a message and added it to our GUI log
|
||||||
fun sendMessage(str: String, dest: String? = null) {
|
fun sendMessage(str: String, dest: String? = null) {
|
||||||
var error: String? = null
|
var error: String? = null
|
||||||
|
|
|
@ -29,7 +29,6 @@ import com.google.android.gms.common.api.ResolvableApiException
|
||||||
import com.google.android.gms.location.*
|
import com.google.android.gms.location.*
|
||||||
import com.google.protobuf.ByteString
|
import com.google.protobuf.ByteString
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.nio.charset.Charset
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
|
||||||
|
@ -99,9 +98,6 @@ class MeshService : Service(), Logging {
|
||||||
return intent
|
return intent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A model object for a Text message
|
|
||||||
data class TextMessage(val fromId: String, val text: String)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum class ConnectionState {
|
public enum class ConnectionState {
|
||||||
|
@ -249,15 +245,11 @@ class MeshService : Service(), Logging {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The RECEIVED_OPAQUE:
|
* The RECEIVED_OPAQUE:
|
||||||
* Payload will be the raw bytes which were contained within a MeshPacket.Opaque field
|
* Payload will be a DataPacket
|
||||||
* Sender will be a user ID string
|
|
||||||
* Type will be the Data.Type enum code for this payload
|
|
||||||
*/
|
*/
|
||||||
private fun broadcastReceivedData(senderId: String, payload: ByteArray, typ: Int) {
|
private fun broadcastReceivedData(payload: DataPacket) {
|
||||||
val intent = Intent(ACTION_RECEIVED_DATA)
|
val intent = Intent(ACTION_RECEIVED_DATA)
|
||||||
intent.putExtra(EXTRA_SENDER, senderId)
|
|
||||||
intent.putExtra(EXTRA_PAYLOAD, payload)
|
intent.putExtra(EXTRA_PAYLOAD, payload)
|
||||||
intent.putExtra(EXTRA_TYP, typ)
|
|
||||||
explicitBroadcast(intent)
|
explicitBroadcast(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -322,7 +314,7 @@ class MeshService : Service(), Logging {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A text message that has a arrived since the last notification update
|
/// A text message that has a arrived since the last notification update
|
||||||
private var recentReceivedText: TextMessage? = null
|
private var recentReceivedText: DataPacket? = null
|
||||||
|
|
||||||
private val summaryString
|
private val summaryString
|
||||||
get() = when (connectionState) {
|
get() = when (connectionState) {
|
||||||
|
@ -353,14 +345,14 @@ class MeshService : Service(), Logging {
|
||||||
// if(shortContent != null) builder.setContentText(shortContent)
|
// if(shortContent != null) builder.setContentText(shortContent)
|
||||||
|
|
||||||
// If a text message arrived include it with our notification
|
// If a text message arrived include it with our notification
|
||||||
recentReceivedText?.let { msg ->
|
recentReceivedText?.let { packet ->
|
||||||
// Try to show the human name of the sender if possible
|
// Try to show the human name of the sender if possible
|
||||||
val sender = nodeDBbyID[msg.fromId]?.user?.longName ?: msg.fromId
|
val sender = nodeDBbyID[packet.from]?.user?.longName ?: packet.from
|
||||||
builder.setContentText("Message from $sender")
|
builder.setContentText("Message from $sender")
|
||||||
|
|
||||||
builder.setStyle(
|
builder.setStyle(
|
||||||
NotificationCompat.BigTextStyle()
|
NotificationCompat.BigTextStyle()
|
||||||
.bigText(msg.text)
|
.bigText(packet.bytes.toString(utf8))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -453,8 +445,8 @@ class MeshService : Service(), Logging {
|
||||||
n
|
n
|
||||||
)
|
)
|
||||||
|
|
||||||
/// Map a nodenum to the nodeid string, or throw an exception if not present
|
/// Map a nodenum to the nodeid string, or return null if not present or no id found
|
||||||
private fun toNodeID(n: Int) = toNodeInfo(n).user?.id
|
private fun toNodeID(n: Int) = nodeDBbyNodeNum[n]?.user?.id
|
||||||
|
|
||||||
/// given a nodenum, return a db entry - creating if necessary
|
/// given a nodenum, return a db entry - creating if necessary
|
||||||
private fun getOrCreateNodeInfo(n: Int) =
|
private fun getOrCreateNodeInfo(n: Int) =
|
||||||
|
@ -528,50 +520,86 @@ class MeshService : Service(), Logging {
|
||||||
}.build()
|
}.build()
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
/// Update our model and resend as needed for a MeshPacket we just received from the radio
|
private val recentDataPackets = mutableListOf<DataPacket>()
|
||||||
private fun handleReceivedData(fromNum: Int, data: MeshProtos.Data) {
|
|
||||||
val bytes = data.payload.toByteArray()
|
|
||||||
val fromId = toNodeID(fromNum)
|
|
||||||
|
|
||||||
/// the sending node ID if possible, else just its number
|
/// Generate a DataPacket from a MeshPacket, or null if we didn't have enough data to do so
|
||||||
val fromString = fromId ?: fromId.toString()
|
private fun toDataPacket(packet: MeshPacket): DataPacket? {
|
||||||
|
return if (!packet.hasPayload() || !packet.payload.hasData()) {
|
||||||
|
// We never convert packets that are not DataPackets
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
val data = packet.payload.data
|
||||||
|
val bytes = data.payload.toByteArray()
|
||||||
|
val fromId = toNodeID(packet.from)
|
||||||
|
val toId = toNodeID(packet.to)
|
||||||
|
?: packet.to.toString() // FIXME, we don't currently have IDs specified for the broadcast address
|
||||||
|
|
||||||
fun forwardData() {
|
// If the rxTime was not set by the device (because device software was old), guess at a time
|
||||||
if (fromId == null)
|
val rxTime = if (packet.rxTime == 0) packet.rxTime else currentSecond()
|
||||||
warn("Ignoring data from $fromNum because we don't yet know its ID")
|
|
||||||
else {
|
|
||||||
debug("Received data from $fromId ${bytes.size}")
|
|
||||||
broadcastReceivedData(fromId, bytes, data.typValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
when (data.typValue) {
|
if (fromId != null) {
|
||||||
MeshProtos.Data.Type.CLEAR_TEXT_VALUE -> {
|
DataPacket(
|
||||||
val text = bytes.toString(Charset.forName("UTF-8"))
|
fromId,
|
||||||
|
toId,
|
||||||
debug("Received CLEAR_TEXT from $fromString")
|
rxTime * 1000L,
|
||||||
|
packet.id,
|
||||||
recentReceivedText = TextMessage(fromString, text)
|
data.typValue,
|
||||||
updateNotification()
|
bytes
|
||||||
forwardData()
|
|
||||||
}
|
|
||||||
|
|
||||||
MeshProtos.Data.Type.CLEAR_READACK_VALUE ->
|
|
||||||
warn(
|
|
||||||
"TODO ignoring CLEAR_READACK from $fromString"
|
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
MeshProtos.Data.Type.SIGNAL_OPAQUE_VALUE ->
|
warn("Ignoring data from ${packet.from} because we don't yet know its ID")
|
||||||
forwardData()
|
null
|
||||||
|
}
|
||||||
else -> TODO()
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
GeeksvilleApplication.analytics.track(
|
private fun rememberDataPacket(dataPacket: DataPacket) {
|
||||||
"data_receive",
|
// discard old messages if needed then add the new one
|
||||||
DataPair("num_bytes", bytes.size),
|
while (recentDataPackets.size > 20) // FIXME, we should instead serialize this list to flash on shutdown
|
||||||
DataPair("type", data.typValue)
|
recentDataPackets.removeAt(0)
|
||||||
)
|
recentDataPackets.add(dataPacket)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update our model and resend as needed for a MeshPacket we just received from the radio
|
||||||
|
private fun handleReceivedData(packet: MeshPacket) {
|
||||||
|
val data = packet.payload.data
|
||||||
|
val bytes = data.payload.toByteArray()
|
||||||
|
val fromId = toNodeID(packet.from)
|
||||||
|
val dataPacket = toDataPacket(packet)
|
||||||
|
|
||||||
|
if (dataPacket != null) {
|
||||||
|
debug("Received data from $fromId ${bytes.size}")
|
||||||
|
|
||||||
|
rememberDataPacket(dataPacket)
|
||||||
|
|
||||||
|
when (data.typValue) {
|
||||||
|
MeshProtos.Data.Type.CLEAR_TEXT_VALUE -> {
|
||||||
|
val text = bytes.toString(utf8)
|
||||||
|
|
||||||
|
debug("Received CLEAR_TEXT from $fromId")
|
||||||
|
|
||||||
|
recentReceivedText = dataPacket
|
||||||
|
updateNotification()
|
||||||
|
broadcastReceivedData(dataPacket)
|
||||||
|
}
|
||||||
|
|
||||||
|
MeshProtos.Data.Type.CLEAR_READACK_VALUE ->
|
||||||
|
warn(
|
||||||
|
"TODO ignoring CLEAR_READACK from $fromId"
|
||||||
|
)
|
||||||
|
|
||||||
|
MeshProtos.Data.Type.SIGNAL_OPAQUE_VALUE ->
|
||||||
|
broadcastReceivedData(dataPacket)
|
||||||
|
|
||||||
|
else -> TODO()
|
||||||
|
}
|
||||||
|
|
||||||
|
GeeksvilleApplication.analytics.track(
|
||||||
|
"data_receive",
|
||||||
|
DataPair("num_bytes", bytes.size),
|
||||||
|
DataPair("type", data.typValue)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update our DB of users based on someone sending out a User subpacket
|
/// Update our DB of users based on someone sending out a User subpacket
|
||||||
|
@ -636,12 +664,14 @@ class MeshService : Service(), Logging {
|
||||||
|
|
||||||
val p = packet.payload
|
val p = packet.payload
|
||||||
|
|
||||||
|
// If the rxTime was not set by the device (because device software was old), guess at a time
|
||||||
|
val rxTime = if (packet.rxTime == 0) packet.rxTime else currentSecond()
|
||||||
|
|
||||||
// Update last seen for the node that sent the packet, but also for _our node_ because anytime a packet passes
|
// Update last seen for the node that sent the packet, but also for _our node_ because anytime a packet passes
|
||||||
// through our node on the way to the phone that means that local node is also alive in the mesh
|
// through our node on the way to the phone that means that local node is also alive in the mesh
|
||||||
updateNodeInfo(fromNum) {
|
updateNodeInfo(fromNum) {
|
||||||
// Update our last seen based on any valid timestamps. If the device didn't provide a timestamp make one
|
// Update our last seen based on any valid timestamps. If the device didn't provide a timestamp make one
|
||||||
val lastSeen =
|
val lastSeen = rxTime
|
||||||
if (packet.rxTime != 0) packet.rxTime else currentSecond()
|
|
||||||
|
|
||||||
it.position = it.position?.copy(time = lastSeen)
|
it.position = it.position?.copy(time = lastSeen)
|
||||||
}
|
}
|
||||||
|
@ -653,7 +683,7 @@ class MeshService : Service(), Logging {
|
||||||
handleReceivedPosition(fromNum, p.position)
|
handleReceivedPosition(fromNum, p.position)
|
||||||
|
|
||||||
if (p.hasData())
|
if (p.hasData())
|
||||||
handleReceivedData(fromNum, p.data)
|
handleReceivedData(packet)
|
||||||
|
|
||||||
if (p.hasUser())
|
if (p.hasUser())
|
||||||
handleReceivedUser(fromNum, p.user)
|
handleReceivedUser(fromNum, p.user)
|
||||||
|
@ -955,6 +985,10 @@ class MeshService : Service(), Logging {
|
||||||
clientPackages[receiverName] = packageName
|
clientPackages[receiverName] = packageName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getOldMessages(): MutableList<DataPacket> {
|
||||||
|
return recentDataPackets
|
||||||
|
}
|
||||||
|
|
||||||
override fun getMyId() = toRemoteExceptions { myNodeID }
|
override fun getMyId() = toRemoteExceptions { myNodeID }
|
||||||
|
|
||||||
override fun setOwner(myId: String?, longName: String, shortName: String) =
|
override fun setOwner(myId: String?, longName: String, shortName: String) =
|
||||||
|
@ -988,6 +1022,11 @@ class MeshService : Service(), Logging {
|
||||||
it.payload = ByteString.copyFrom(payloadIn)
|
it.payload = ByteString.copyFrom(payloadIn)
|
||||||
}.build()
|
}.build()
|
||||||
}
|
}
|
||||||
|
// Keep a record of datapackets, so GUIs can show proper chat history
|
||||||
|
toDataPacket(packet)?.let {
|
||||||
|
rememberDataPacket(it)
|
||||||
|
}
|
||||||
|
|
||||||
// If radio is sleeping, queue the packet
|
// If radio is sleeping, queue the packet
|
||||||
when (connectionState) {
|
when (connectionState) {
|
||||||
ConnectionState.DEVICE_SLEEP ->
|
ConnectionState.DEVICE_SLEEP ->
|
||||||
|
|
Ładowanie…
Reference in New Issue