mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-30 13:35:18 +00:00
configuration message
This commit is contained in:
parent
e4a1de24f5
commit
03ff1d1941
@ -77,6 +77,7 @@ import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
|
|||||||
import org.thoughtcrime.securesms.loki.database.LokiUserDatabase;
|
import org.thoughtcrime.securesms.loki.database.LokiUserDatabase;
|
||||||
import org.thoughtcrime.securesms.loki.database.SharedSenderKeysDatabase;
|
import org.thoughtcrime.securesms.loki.database.SharedSenderKeysDatabase;
|
||||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol;
|
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol;
|
||||||
|
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol;
|
||||||
import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation;
|
import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation;
|
||||||
import org.thoughtcrime.securesms.loki.utilities.Broadcaster;
|
import org.thoughtcrime.securesms.loki.utilities.Broadcaster;
|
||||||
import org.thoughtcrime.securesms.loki.utilities.UiModeUtilities;
|
import org.thoughtcrime.securesms.loki.utilities.UiModeUtilities;
|
||||||
@ -115,7 +116,6 @@ import org.session.libsignal.service.loki.protocol.meta.SessionMetaProtocol;
|
|||||||
import org.session.libsignal.service.loki.protocol.sessionmanagement.SessionManagementProtocol;
|
import org.session.libsignal.service.loki.protocol.sessionmanagement.SessionManagementProtocol;
|
||||||
import org.session.libsignal.service.loki.protocol.sessionmanagement.SessionManagementProtocolDelegate;
|
import org.session.libsignal.service.loki.protocol.sessionmanagement.SessionManagementProtocolDelegate;
|
||||||
import org.session.libsignal.service.loki.protocol.shelved.multidevice.DeviceLink;
|
import org.session.libsignal.service.loki.protocol.shelved.multidevice.DeviceLink;
|
||||||
import org.session.libsignal.service.loki.protocol.shelved.multidevice.MultiDeviceProtocol;
|
|
||||||
import org.session.libsignal.service.loki.protocol.shelved.syncmessages.SyncMessagesProtocol;
|
import org.session.libsignal.service.loki.protocol.shelved.syncmessages.SyncMessagesProtocol;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@ -206,7 +206,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||||||
SessionMetaProtocol.Companion.configureIfNeeded(apiDB, userPublicKey);
|
SessionMetaProtocol.Companion.configureIfNeeded(apiDB, userPublicKey);
|
||||||
SyncMessagesProtocol.Companion.configureIfNeeded(apiDB, userPublicKey);
|
SyncMessagesProtocol.Companion.configureIfNeeded(apiDB, userPublicKey);
|
||||||
}
|
}
|
||||||
MultiDeviceProtocol.Companion.configureIfNeeded(apiDB);
|
|
||||||
SessionManagementProtocol.Companion.configureIfNeeded(sessionResetImpl, sskDatabase, this);
|
SessionManagementProtocol.Companion.configureIfNeeded(sessionResetImpl, sskDatabase, this);
|
||||||
setUpP2PAPIIfNeeded();
|
setUpP2PAPIIfNeeded();
|
||||||
PushNotificationAPI.Companion.configureIfNeeded(BuildConfig.DEBUG);
|
PushNotificationAPI.Companion.configureIfNeeded(BuildConfig.DEBUG);
|
||||||
@ -249,6 +248,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||||||
startPollingIfNeeded();
|
startPollingIfNeeded();
|
||||||
publicChatManager.markAllAsNotCaughtUp();
|
publicChatManager.markAllAsNotCaughtUp();
|
||||||
publicChatManager.startPollersIfNeeded();
|
publicChatManager.startPollersIfNeeded();
|
||||||
|
MultiDeviceProtocol.syncConfigurationIfNeeded(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -150,6 +150,16 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
|
|||||||
return new Reader(cursor);
|
return new Reader(cursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<GroupRecord> getAllGroups() {
|
||||||
|
Reader reader = getGroups();
|
||||||
|
GroupRecord record;
|
||||||
|
List<GroupRecord> groups = new LinkedList<>();
|
||||||
|
while ((record = reader.getNext()) != null) {
|
||||||
|
if (record.isActive()) { groups.add(record); }
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
public @NonNull List<Recipient> getGroupMembers(String groupId, boolean includeSelf) {
|
public @NonNull List<Recipient> getGroupMembers(String groupId, boolean includeSelf) {
|
||||||
List<Address> members = getCurrentMembers(groupId);
|
List<Address> members = getCurrentMembers(groupId);
|
||||||
List<Recipient> recipients = new LinkedList<>();
|
List<Recipient> recipients = new LinkedList<>();
|
||||||
|
@ -371,6 +371,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
return DatabaseFactory.getLokiAPIDatabase(context).getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
|
return DatabaseFactory.getLokiAPIDatabase(context).getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getAllGroups(): List<GroupRecord> {
|
||||||
|
return DatabaseFactory.getGroupDatabase(context).allGroups
|
||||||
|
}
|
||||||
|
|
||||||
override fun setProfileSharing(address: Address, value: Boolean) {
|
override fun setProfileSharing(address: Address, value: Boolean) {
|
||||||
val recipient = Recipient.from(context, address, false)
|
val recipient = Recipient.from(context, address, false)
|
||||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, value)
|
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, value)
|
||||||
|
@ -2,8 +2,6 @@ package org.thoughtcrime.securesms.loki.activities
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.os.AsyncTask
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.loader.app.LoaderManager
|
import androidx.loader.app.LoaderManager
|
||||||
import androidx.loader.content.Loader
|
import androidx.loader.content.Loader
|
||||||
@ -20,15 +18,12 @@ import org.thoughtcrime.securesms.conversation.ConversationActivity
|
|||||||
import org.session.libsession.messaging.threads.Address
|
import org.session.libsession.messaging.threads.Address
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||||
import org.thoughtcrime.securesms.groups.GroupManager
|
|
||||||
import org.thoughtcrime.securesms.loki.utilities.fadeIn
|
import org.thoughtcrime.securesms.loki.utilities.fadeIn
|
||||||
import org.thoughtcrime.securesms.loki.utilities.fadeOut
|
import org.thoughtcrime.securesms.loki.utilities.fadeOut
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
import org.thoughtcrime.securesms.mms.GlideApp
|
||||||
import org.session.libsession.messaging.threads.recipients.Recipient
|
import org.session.libsession.messaging.threads.recipients.Recipient
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsignal.libsignal.util.guava.Optional
|
|
||||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2
|
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2
|
||||||
import java.lang.ref.WeakReference
|
|
||||||
|
|
||||||
//TODO Refactor to avoid using kotlinx.android.synthetic
|
//TODO Refactor to avoid using kotlinx.android.synthetic
|
||||||
class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks<List<String>> {
|
class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks<List<String>> {
|
||||||
@ -122,6 +117,7 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
|
|||||||
openConversationActivity(this, threadID, Recipient.from(this, Address.fromSerialized(groupID), false))
|
openConversationActivity(this, threadID, Recipient.from(this, Address.fromSerialized(groupID), false))
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
@ -84,6 +84,8 @@ object ClosedGroupsProtocolV2 {
|
|||||||
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID)
|
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID)
|
||||||
// Notify the PN server
|
// Notify the PN server
|
||||||
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
||||||
|
// Force sync configuration message
|
||||||
|
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(context)
|
||||||
// Fulfill the promise
|
// Fulfill the promise
|
||||||
deferred.resolve(groupID)
|
deferred.resolve(groupID)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
package org.thoughtcrime.securesms.loki.protocol
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
import org.session.libsignal.service.api.push.SignalServiceAddress
|
||||||
|
import org.session.libsignal.utilities.logging.Log
|
||||||
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
|
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
|
||||||
|
import org.thoughtcrime.securesms.loki.utilities.recipient
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
object MultiDeviceProtocol {
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun syncConfigurationIfNeeded(context: Context) {
|
||||||
|
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||||
|
val lastSyncTime = TextSecurePreferences.getLastConfigurationSyncTime(context)
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
if (now - lastSyncTime < 2 * 24 * 60 * 60 * 1000) return
|
||||||
|
val configurationMessage = ConfigurationMessage.getCurrent()
|
||||||
|
val serializedMessage = configurationMessage.toProto()!!.toByteArray()
|
||||||
|
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
|
||||||
|
val address = SignalServiceAddress(userPublicKey)
|
||||||
|
val recipient = recipient(context, userPublicKey)
|
||||||
|
val udAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient)
|
||||||
|
try {
|
||||||
|
messageSender.sendMessage(0, address, udAccess.get().targetUnidentifiedAccess,
|
||||||
|
Date().time, serializedMessage, false, configurationMessage.ttl.toInt(), false,
|
||||||
|
true, false, true, false)
|
||||||
|
TextSecurePreferences.setLastConfigurationSyncTime(context, now)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d("Loki", "Failed to send configuration message due to error: $e.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun forceSyncConfigurationNowIfNeeded(context: Context) {
|
||||||
|
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||||
|
val configurationMessage = ConfigurationMessage.getCurrent()
|
||||||
|
val serializedMessage = configurationMessage.toProto()!!.toByteArray()
|
||||||
|
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
|
||||||
|
val address = SignalServiceAddress(userPublicKey)
|
||||||
|
val recipient = recipient(context, userPublicKey)
|
||||||
|
val udAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient)
|
||||||
|
try {
|
||||||
|
messageSender.sendMessage(0, address, udAccess.get().targetUnidentifiedAccess,
|
||||||
|
Date().time, serializedMessage, false, configurationMessage.ttl.toInt(), false,
|
||||||
|
true, false, true, false)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d("Loki", "Failed to send configuration message due to error: $e.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -108,6 +108,8 @@ interface StorageProtocol {
|
|||||||
fun isClosedGroup(publicKey: String): Boolean
|
fun isClosedGroup(publicKey: String): Boolean
|
||||||
fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): MutableList<ECKeyPair>
|
fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): MutableList<ECKeyPair>
|
||||||
fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair?
|
fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair?
|
||||||
|
// Groups
|
||||||
|
fun getAllGroups(): List<GroupRecord>
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
fun setProfileSharing(address: Address, value: Boolean)
|
fun setProfileSharing(address: Address, value: Boolean)
|
||||||
|
@ -0,0 +1,105 @@
|
|||||||
|
package org.session.libsession.messaging.messages.control
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString
|
||||||
|
import org.session.libsession.messaging.MessagingConfiguration
|
||||||
|
import org.session.libsession.messaging.threads.Address
|
||||||
|
import org.session.libsession.utilities.GroupUtil
|
||||||
|
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
|
||||||
|
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
|
||||||
|
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||||
|
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||||
|
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||||
|
import org.session.libsignal.service.loki.utilities.toHexString
|
||||||
|
|
||||||
|
class ConfigurationMessage(val closedGroups: List<ClosedGroup>, val openGroups: List<String>): ControlMessage() {
|
||||||
|
|
||||||
|
class ClosedGroup(val publicKey: String, val name: String, val encryptionKeyPair: ECKeyPair, val members: List<String>, val admins: List<String>) {
|
||||||
|
val isValid: Boolean get() = members.isNotEmpty() && admins.isNotEmpty()
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromProto(proto: SignalServiceProtos.ConfigurationMessage.ClosedGroup): ClosedGroup? {
|
||||||
|
if (!proto.hasPublicKey() || !proto.hasName() || !proto.hasEncryptionKeyPair()) return null
|
||||||
|
val publicKey = proto.publicKey.toByteArray().toHexString()
|
||||||
|
val name = proto.name
|
||||||
|
val encryptionKeyPairAsProto = proto.encryptionKeyPair
|
||||||
|
val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray().removing05PrefixIfNeeded()),
|
||||||
|
DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray()))
|
||||||
|
val members = proto.membersList.map { it.toByteArray().toHexString() }
|
||||||
|
val admins = proto.adminsList.map { it.toByteArray().toHexString() }
|
||||||
|
return ClosedGroup(publicKey, name, encryptionKeyPair, members, admins)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toProto(): SignalServiceProtos.ConfigurationMessage.ClosedGroup? {
|
||||||
|
val result = SignalServiceProtos.ConfigurationMessage.ClosedGroup.newBuilder()
|
||||||
|
result.publicKey = ByteString.copyFrom(publicKey.toByteArray())
|
||||||
|
result.name = name
|
||||||
|
val encryptionKeyPairAsProto = SignalServiceProtos.KeyPair.newBuilder()
|
||||||
|
encryptionKeyPairAsProto.publicKey = ByteString.copyFrom(encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
|
||||||
|
encryptionKeyPairAsProto.privateKey = ByteString.copyFrom(encryptionKeyPair.privateKey.serialize())
|
||||||
|
result.encryptionKeyPair = encryptionKeyPairAsProto.build()
|
||||||
|
result.addAllMembers(members.map { ByteString.copyFrom(it.toByteArray()) })
|
||||||
|
result.addAllAdmins(admins.map { ByteString.copyFrom(it.toByteArray()) })
|
||||||
|
return result.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val ttl: Long = 4 * 24 * 60 * 60 * 1000
|
||||||
|
override val isSelfSendValid: Boolean = true
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun getCurrent(): ConfigurationMessage {
|
||||||
|
val closedGroups = mutableListOf<ClosedGroup>()
|
||||||
|
val openGroups = mutableListOf<String>()
|
||||||
|
val storage = MessagingConfiguration.shared.storage
|
||||||
|
val groups = storage.getAllGroups()
|
||||||
|
for (groupRecord in groups) {
|
||||||
|
if (groupRecord.isClosedGroup) {
|
||||||
|
if (!groupRecord.members.contains(Address.fromSerialized(storage.getUserPublicKey()!!))) continue
|
||||||
|
val groupPublicKey = GroupUtil.getDecodedGroupID(groupRecord.encodedId) // TODO: Check if this is correct. Does it need to be double decoded?
|
||||||
|
if (!storage.isClosedGroup(groupPublicKey)) continue
|
||||||
|
val encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: continue
|
||||||
|
val closedGroup = ClosedGroup(groupPublicKey, groupRecord.title, encryptionKeyPair, groupRecord.members.map { it.serialize() }, groupRecord.admins.map { it.serialize() })
|
||||||
|
closedGroups.add(closedGroup)
|
||||||
|
}
|
||||||
|
if (groupRecord.isOpenGroup) {
|
||||||
|
val threadID = storage.getThreadID(groupRecord.encodedId) ?: continue
|
||||||
|
val openGroup = storage.getOpenGroup(threadID) ?: continue
|
||||||
|
openGroups.add(openGroup.server)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ConfigurationMessage(closedGroups, openGroups)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromProto(proto: SignalServiceProtos.Content): ConfigurationMessage? {
|
||||||
|
if (!proto.hasConfigurationMessage()) return null
|
||||||
|
val configurationProto = proto.configurationMessage
|
||||||
|
val closedGroups = configurationProto.closedGroupsList.mapNotNull { ClosedGroup.fromProto(it) }
|
||||||
|
val openGroups = configurationProto.openGroupsList
|
||||||
|
return ConfigurationMessage(closedGroups, openGroups)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toProto(): SignalServiceProtos.Content? {
|
||||||
|
val configurationProto = SignalServiceProtos.ConfigurationMessage.newBuilder()
|
||||||
|
configurationProto.addAllClosedGroups(closedGroups.mapNotNull { it.toProto() })
|
||||||
|
configurationProto.addAllOpenGroups(openGroups)
|
||||||
|
val contentProto = SignalServiceProtos.Content.newBuilder()
|
||||||
|
contentProto.configurationMessage = configurationProto.build()
|
||||||
|
return contentProto.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return """
|
||||||
|
ConfigurationMessage(
|
||||||
|
closedGroups: ${(closedGroups)}
|
||||||
|
openGroups: ${(openGroups)}
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
}
|
@ -128,6 +128,29 @@ object TextSecurePreferences {
|
|||||||
private const val FCM_TOKEN = "pref_fcm_token"
|
private const val FCM_TOKEN = "pref_fcm_token"
|
||||||
private const val LAST_FCM_TOKEN_UPLOAD_TIME = "pref_last_fcm_token_upload_time_2"
|
private const val LAST_FCM_TOKEN_UPLOAD_TIME = "pref_last_fcm_token_upload_time_2"
|
||||||
|
|
||||||
|
// region Multi Device
|
||||||
|
private const val IS_USING_MULTI_DEVICE = "pref_is_using_multi_device"
|
||||||
|
private const val LAST_CONFIGURATION_SYNC_TIME = "pref_last_configuration_sync_time"
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun isUsingMultiDevice(context: Context): Boolean {
|
||||||
|
return getBooleanPreference(context, IS_USING_MULTI_DEVICE, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun setIsUsingMultiDevice(context: Context, value: Boolean) {
|
||||||
|
setBooleanPreference(context, IS_USING_MULTI_DEVICE, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getLastConfigurationSyncTime(context: Context): Long {
|
||||||
|
return getLongPreference(context, LAST_CONFIGURATION_SYNC_TIME, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun setLastConfigurationSyncTime(context: Context, value: Long) {
|
||||||
|
setLongPreference(context, LAST_CONFIGURATION_SYNC_TIME, value)
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun isUsingFCM(context: Context): Boolean {
|
fun isUsingFCM(context: Context): Boolean {
|
||||||
|
Loading…
Reference in New Issue
Block a user