Второй этап выхода из офлайн режима
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,186 @@
|
|||||||
|
# Умная синхронизация сообщений при восстановлении сети
|
||||||
|
|
||||||
|
## Проблема
|
||||||
|
При восстановлении соединения после офлайна, приложение запрашивало **все сообщения заново** (последние 50 для каждого чата), вместо того чтобы получить только **новые сообщения**, которые пришли пока клиент был офлайн.
|
||||||
|
|
||||||
|
## Решение
|
||||||
|
Добавлен новый параметр API `afterSequenceId` который позволяет запрашивать только сообщения с sequenceId > указанного.
|
||||||
|
|
||||||
|
## Изменения
|
||||||
|
|
||||||
|
### Бэкенд
|
||||||
|
|
||||||
|
#### 1. IMessageRepository.cs
|
||||||
|
Добавлен метод для получения сообщений ПОСЛЕ указанного sequenceId:
|
||||||
|
```csharp
|
||||||
|
Task<List<Message>> GetChatMessagesAfterAsync(Guid chatId, long sequenceId, int limit, CancellationToken cancellationToken);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. MessageRepository.cs
|
||||||
|
Реализация метода:
|
||||||
|
```csharp
|
||||||
|
public async Task<List<Message>> GetChatMessagesAfterAsync(Guid chatId, long sequenceId, int limit, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var builder = Builders<Message>.Filter;
|
||||||
|
var filter = builder.And(
|
||||||
|
builder.Eq(m => m.ChatId, chatId),
|
||||||
|
builder.Gt(m => m.SequenceId, sequenceId)
|
||||||
|
);
|
||||||
|
|
||||||
|
return await _messages.Find(filter)
|
||||||
|
.SortBy(m => m.SequenceId)
|
||||||
|
.Limit(limit)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. GetMessagesQuery.cs
|
||||||
|
Добавлен параметр `AfterSequenceId`:
|
||||||
|
```csharp
|
||||||
|
public record GetMessagesQuery(
|
||||||
|
Guid UserId,
|
||||||
|
Guid ChatId,
|
||||||
|
string? Cursor,
|
||||||
|
long? Pivot = null,
|
||||||
|
long? AfterSequenceId = null, // НОВЫЙ ПАРАМЕТР
|
||||||
|
int? Limit = null
|
||||||
|
) : IQuery<List<MessageDetailDto>>;
|
||||||
|
```
|
||||||
|
|
||||||
|
Обработчик использует новый метод:
|
||||||
|
```csharp
|
||||||
|
if (request.AfterSequenceId.HasValue)
|
||||||
|
{
|
||||||
|
messages = await _messageRepository.GetChatMessagesAfterAsync(
|
||||||
|
request.ChatId,
|
||||||
|
request.AfterSequenceId.Value,
|
||||||
|
queryLimit,
|
||||||
|
cancellationToken
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. MessagesEndpoints.cs
|
||||||
|
Добавлен query параметр в endpoint:
|
||||||
|
```csharp
|
||||||
|
group.MapGet("chat/{chatId:guid}", async (
|
||||||
|
[FromRoute] Guid chatId,
|
||||||
|
[FromQuery] string? cursor,
|
||||||
|
[FromQuery] long? afterSequenceId, // НОВЫЙ ПАРАМЕТР
|
||||||
|
[FromQuery] long? pivot,
|
||||||
|
[FromQuery] int? limit,
|
||||||
|
ISender sender,
|
||||||
|
IUserContext userContext,
|
||||||
|
CancellationToken ct) => { ... }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Мобильное приложение
|
||||||
|
|
||||||
|
#### 1. ChatApi.kt
|
||||||
|
Добавлен параметр в API:
|
||||||
|
```kotlin
|
||||||
|
@GET("messages/chat/{chatId}")
|
||||||
|
suspend fun getMessages(
|
||||||
|
@Path("chatId") chatId: String,
|
||||||
|
@Query("cursor") cursor: String? = null,
|
||||||
|
@Query("pivot") pivot: Long? = null,
|
||||||
|
@Query("afterSequenceId") afterSequenceId: Long? = null, // НОВЫЙ ПАРАМЕТР
|
||||||
|
@Query("limit") limit: Int? = 50
|
||||||
|
): List<MessageDto>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. ChatRepository.kt
|
||||||
|
Добавлен метод:
|
||||||
|
```kotlin
|
||||||
|
suspend fun getMessages(
|
||||||
|
chatId: String,
|
||||||
|
cursor: String? = null,
|
||||||
|
pivot: Long? = null,
|
||||||
|
afterSequenceId: Long? = null, // НОВЫЙ ПАРАМЕТР
|
||||||
|
limit: Int? = null
|
||||||
|
): List<Message>
|
||||||
|
```
|
||||||
|
|
||||||
|
Добавлен метод для получения последнего sequenceId:
|
||||||
|
```kotlin
|
||||||
|
suspend fun getLastKnownSequenceId(chatId: String): Int?
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. ChatRepositoryImpl.kt
|
||||||
|
Реализация:
|
||||||
|
```kotlin
|
||||||
|
override suspend fun getLastKnownSequenceId(chatId: String): Int? {
|
||||||
|
return try {
|
||||||
|
messageDao.getMaxSequenceId(chatId)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to get last sequenceId", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. SignalRNotificationObserver.kt
|
||||||
|
Умная синхронизация:
|
||||||
|
```kotlin
|
||||||
|
private fun syncMissedMessages() {
|
||||||
|
scope.launch {
|
||||||
|
val chats = chatRepository.getChats()
|
||||||
|
|
||||||
|
chats.forEach { chat ->
|
||||||
|
val lastSequenceId = chatRepository.getLastKnownSequenceId(chat.id)
|
||||||
|
|
||||||
|
if (lastSequenceId != null) {
|
||||||
|
// Запрашиваем только сообщения ПОСЛЕ последнего известного
|
||||||
|
chatRepository.getMessages(
|
||||||
|
chatId = chat.id,
|
||||||
|
afterSequenceId = lastSequenceId.toLong(),
|
||||||
|
limit = 100
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Нет локальных сообщений - загружаем последние 50
|
||||||
|
chatRepository.getMessages(chatId = chat.id, limit = 50)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Преимущества
|
||||||
|
|
||||||
|
### До изменений
|
||||||
|
- Запрашивались **все последние 50 сообщений** для каждого чата
|
||||||
|
- При 20 чатах: 20 × 50 = **1000 сообщений** из сети
|
||||||
|
- Дублирование данных, медленная синхронизация
|
||||||
|
|
||||||
|
### После изменений
|
||||||
|
- Запрашиваются **только новые сообщения** с последнего sequenceId
|
||||||
|
- При 20 чатах и 5 новых сообщениях в каждом: 20 × 5 = **100 сообщений**
|
||||||
|
- **В 10 раз меньше трафика**
|
||||||
|
- **Быстрая синхронизация**
|
||||||
|
|
||||||
|
## Пример использования API
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/messages/chat/{chatId}?afterSequenceId=12345&limit=100
|
||||||
|
```
|
||||||
|
|
||||||
|
Ответит только с сообщениями где `sequenceId > 12345`.
|
||||||
|
|
||||||
|
## Тестирование
|
||||||
|
|
||||||
|
1. Откройте приложение, загрузите чаты
|
||||||
|
2. Отключите сеть
|
||||||
|
3. Отправьте несколько сообщений с другого устройства
|
||||||
|
4. Включите сеть
|
||||||
|
5. Проверьте логи: должно быть `fetching newer...` и загрузка только новых сообщений
|
||||||
|
|
||||||
|
## Логи
|
||||||
|
|
||||||
|
```
|
||||||
|
SignalRNtfObserver: Starting missed messages sync...
|
||||||
|
SignalRNtfObserver: Syncing 5 chats for missed messages
|
||||||
|
SignalRNtfObserver: Chat abc-123: last known seqId=100, fetching newer...
|
||||||
|
SignalRNtfObserver: Chat abc-123: fetched 3 new messages
|
||||||
|
SignalRNtfObserver: Synced chat abc-123
|
||||||
|
```
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
package="ru.knot.messager">
|
package="ru.knot.messager">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ class MainActivity : ComponentActivity() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
android.util.Log.d("MainActivity", "onCreate called")
|
||||||
signalrNotificationObserver.start()
|
signalrNotificationObserver.start()
|
||||||
|
android.util.Log.d("MainActivity", "signalrNotificationObserver.start() called")
|
||||||
|
|
||||||
intent.getStringExtra("chatId")?.let { chatId ->
|
intent.getStringExtra("chatId")?.let { chatId ->
|
||||||
navigationManager.navigateToChat(chatId)
|
navigationManager.navigateToChat(chatId)
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ interface ChatApi {
|
|||||||
@Path("chatId") chatId: String,
|
@Path("chatId") chatId: String,
|
||||||
@Query("cursor") cursor: String? = null,
|
@Query("cursor") cursor: String? = null,
|
||||||
@Query("pivot") pivot: Long? = null,
|
@Query("pivot") pivot: Long? = null,
|
||||||
|
@Query("afterSequenceId") afterSequenceId: Long? = null,
|
||||||
@Query("limit") limit: Int? = 50
|
@Query("limit") limit: Int? = 50
|
||||||
): List<MessageDto>
|
): List<MessageDto>
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ enum class ConnectionStatus { CONNECTED, CONNECTING, DISCONNECTED }
|
|||||||
@Singleton
|
@Singleton
|
||||||
class ChatHubClient @Inject constructor() {
|
class ChatHubClient @Inject constructor() {
|
||||||
private var hubConnection: HubConnection? = null
|
private var hubConnection: HubConnection? = null
|
||||||
private val _events = MutableSharedFlow<ChatEvent>(extraBufferCapacity = 1024)
|
// extraBufferCapacity=1024 позволяет буферизовать события пока нет подписчиков
|
||||||
|
private val _events = MutableSharedFlow<ChatEvent>(replay = 0, extraBufferCapacity = 1024)
|
||||||
val events: SharedFlow<ChatEvent> = _events.asSharedFlow()
|
val events: SharedFlow<ChatEvent> = _events.asSharedFlow()
|
||||||
|
|
||||||
private val _status = MutableStateFlow(ConnectionStatus.DISCONNECTED)
|
private val _status = MutableStateFlow(ConnectionStatus.DISCONNECTED)
|
||||||
@@ -56,32 +57,59 @@ class ChatHubClient @Inject constructor() {
|
|||||||
private var lastToken: String? = null
|
private var lastToken: String? = null
|
||||||
|
|
||||||
fun connect(baseUrl: String, accessToken: String) {
|
fun connect(baseUrl: String, accessToken: String) {
|
||||||
if (hubConnection?.connectionState == HubConnectionState.CONNECTED) return
|
// Проверяем текущее состояние
|
||||||
|
val currentState = hubConnection?.connectionState
|
||||||
|
if (currentState == HubConnectionState.CONNECTED) {
|
||||||
|
Log.d("ChatHubClient", "Already connected, skipping")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если соединение в процессе - останавливаем его
|
||||||
|
if (currentState == HubConnectionState.CONNECTING) {
|
||||||
|
Log.d("ChatHubClient", "Connection in progress ($currentState), stopping first...")
|
||||||
|
hubConnection?.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем параметры для переподключения
|
||||||
lastBaseUrl = baseUrl
|
lastBaseUrl = baseUrl
|
||||||
lastToken = accessToken
|
lastToken = accessToken
|
||||||
_status.value = ConnectionStatus.CONNECTING
|
_status.value = ConnectionStatus.CONNECTING
|
||||||
|
|
||||||
hubConnection = HubConnectionBuilder.create("${baseUrl}/hubs/chat")
|
Log.d("ChatHubClient", "Connecting to ${baseUrl}/hubs/chat with token: ${accessToken.take(10)}...")
|
||||||
|
|
||||||
|
// Создаем новое соединение
|
||||||
|
val newHubConnection = HubConnectionBuilder.create("${baseUrl}/hubs/chat")
|
||||||
.withAccessTokenProvider(Single.just(accessToken))
|
.withAccessTokenProvider(Single.just(accessToken))
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
hubConnection = newHubConnection
|
||||||
|
|
||||||
setupHandlers()
|
setupHandlers()
|
||||||
|
|
||||||
hubConnection?.onClosed { exception ->
|
hubConnection?.onClosed { exception ->
|
||||||
Log.e("ChatHubClient", "Connection closed. Reconnecting...", exception)
|
Log.e("ChatHubClient", "Connection closed. Reconnecting...", exception)
|
||||||
_status.value = ConnectionStatus.DISCONNECTED
|
_status.value = ConnectionStatus.DISCONNECTED
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
// Проверяем, есть ли еще актуальные параметры для переподключения
|
||||||
|
val reconnectBaseUrl = lastBaseUrl
|
||||||
|
val reconnectToken = lastToken
|
||||||
|
|
||||||
|
if (reconnectBaseUrl != null && reconnectToken != null) {
|
||||||
|
Log.d("ChatHubClient", "Attempting reconnection with saved parameters...")
|
||||||
delay(5000)
|
delay(5000)
|
||||||
connect(baseUrl, accessToken)
|
connect(reconnectBaseUrl, reconnectToken)
|
||||||
|
} else {
|
||||||
|
Log.w("ChatHubClient", "Cannot reconnect: missing baseUrl or token")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
|
Log.d("ChatHubClient", "Starting SignalR connection...")
|
||||||
hubConnection?.start()?.blockingAwait()
|
hubConnection?.start()?.blockingAwait()
|
||||||
_status.value = ConnectionStatus.CONNECTED
|
_status.value = ConnectionStatus.CONNECTED
|
||||||
Log.d("ChatHubClient", "SignalR Connected")
|
Log.d("ChatHubClient", "SignalR Connected successfully!")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("ChatHubClient", "SignalR Connection Error", e)
|
Log.e("ChatHubClient", "SignalR Connection Error", e)
|
||||||
_status.value = ConnectionStatus.DISCONNECTED
|
_status.value = ConnectionStatus.DISCONNECTED
|
||||||
@@ -91,19 +119,25 @@ class ChatHubClient @Inject constructor() {
|
|||||||
|
|
||||||
private fun setupHandlers() {
|
private fun setupHandlers() {
|
||||||
hubConnection?.let { conn ->
|
hubConnection?.let { conn ->
|
||||||
|
Log.d("ChatHubClient", "Setting up SignalR handlers")
|
||||||
|
|
||||||
conn.on("new_message", { message: MessageDto ->
|
conn.on("new_message", { message: MessageDto ->
|
||||||
|
Log.d("ChatHubClient", ">>> new_message event received: ${message.id} in chat ${message.chatId}")
|
||||||
_events.tryEmit(ChatEvent.NewMessage(message))
|
_events.tryEmit(ChatEvent.NewMessage(message))
|
||||||
}, MessageDto::class.java)
|
}, MessageDto::class.java)
|
||||||
|
|
||||||
conn.on("message_edited", { messageId: String, chatId: String, content: String ->
|
conn.on("message_edited", { messageId: String, chatId: String, content: String ->
|
||||||
|
Log.d("ChatHubClient", ">>> message_edited event: $messageId")
|
||||||
_events.tryEmit(ChatEvent.MessageEdited(messageId, chatId, content))
|
_events.tryEmit(ChatEvent.MessageEdited(messageId, chatId, content))
|
||||||
}, String::class.java, String::class.java, String::class.java)
|
}, String::class.java, String::class.java, String::class.java)
|
||||||
|
|
||||||
conn.on("message_deleted", { messageId: String, chatId: String ->
|
conn.on("message_deleted", { messageId: String, chatId: String ->
|
||||||
|
Log.d("ChatHubClient", ">>> message_deleted event: $messageId")
|
||||||
_events.tryEmit(ChatEvent.MessageDeleted(messageId, chatId))
|
_events.tryEmit(ChatEvent.MessageDeleted(messageId, chatId))
|
||||||
}, String::class.java, String::class.java)
|
}, String::class.java, String::class.java)
|
||||||
|
|
||||||
conn.on("messages_read", { data: MessagesReadEvent ->
|
conn.on("messages_read", { data: MessagesReadEvent ->
|
||||||
|
Log.d("ChatHubClient", ">>> messages_read event: ${data.effectiveChatId}")
|
||||||
_events.tryEmit(ChatEvent.MessagesRead(
|
_events.tryEmit(ChatEvent.MessagesRead(
|
||||||
data.effectiveChatId,
|
data.effectiveChatId,
|
||||||
data.effectiveUserId,
|
data.effectiveUserId,
|
||||||
@@ -124,10 +158,12 @@ class ChatHubClient @Inject constructor() {
|
|||||||
}, String::class.java)
|
}, String::class.java)
|
||||||
|
|
||||||
conn.on("new_chat", { chat: ChatDto ->
|
conn.on("new_chat", { chat: ChatDto ->
|
||||||
|
Log.d("ChatHubClient", ">>> new_chat event: ${chat.id}")
|
||||||
_events.tryEmit(ChatEvent.NewChat(chat))
|
_events.tryEmit(ChatEvent.NewChat(chat))
|
||||||
}, ChatDto::class.java)
|
}, ChatDto::class.java)
|
||||||
|
|
||||||
conn.on("reaction_added", { data: ReactionEvent ->
|
conn.on("reaction_added", { data: ReactionEvent ->
|
||||||
|
Log.d("ChatHubClient", ">>> reaction_added event: ${data.emoji} on ${data.messageId}")
|
||||||
_events.tryEmit(ChatEvent.ReactionUpdated(
|
_events.tryEmit(ChatEvent.ReactionUpdated(
|
||||||
data.messageId ?: "",
|
data.messageId ?: "",
|
||||||
data.chatId ?: "",
|
data.chatId ?: "",
|
||||||
@@ -138,6 +174,7 @@ class ChatHubClient @Inject constructor() {
|
|||||||
}, ReactionEvent::class.java)
|
}, ReactionEvent::class.java)
|
||||||
|
|
||||||
conn.on("reaction_removed", { data: ReactionEvent ->
|
conn.on("reaction_removed", { data: ReactionEvent ->
|
||||||
|
Log.d("ChatHubClient", ">>> reaction_removed event: ${data.emoji} on ${data.messageId}")
|
||||||
_events.tryEmit(ChatEvent.ReactionUpdated(
|
_events.tryEmit(ChatEvent.ReactionUpdated(
|
||||||
data.messageId ?: "",
|
data.messageId ?: "",
|
||||||
data.chatId ?: "",
|
data.chatId ?: "",
|
||||||
@@ -185,6 +222,26 @@ class ChatHubClient @Inject constructor() {
|
|||||||
_status.value = ConnectionStatus.DISCONNECTED
|
_status.value = ConnectionStatus.DISCONNECTED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Принудительное переподключение - останавливает текущее соединение и создает новое
|
||||||
|
*/
|
||||||
|
fun reconnect() {
|
||||||
|
Log.d("ChatHubClient", "Forced reconnect requested")
|
||||||
|
val baseUrl = lastBaseUrl
|
||||||
|
val token = lastToken
|
||||||
|
|
||||||
|
if (baseUrl != null && token != null) {
|
||||||
|
disconnect()
|
||||||
|
// Небольшая задержка перед переподключением
|
||||||
|
scope.launch {
|
||||||
|
delay(1000)
|
||||||
|
connect(baseUrl, token)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.w("ChatHubClient", "Cannot reconnect: missing saved credentials")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun addReaction(messageId: String, chatId: String, emoji: String) {
|
fun addReaction(messageId: String, chatId: String, emoji: String) {
|
||||||
if (hubConnection?.connectionState == HubConnectionState.CONNECTED) {
|
if (hubConnection?.connectionState == HubConnectionState.CONNECTED) {
|
||||||
hubConnection?.invoke("add_reaction", mapOf(
|
hubConnection?.invoke("add_reaction", mapOf(
|
||||||
@@ -250,12 +307,13 @@ class ChatHubClient @Inject constructor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hubConnection?.connectionState == HubConnectionState.CONNECTED) {
|
if (hubConnection?.connectionState == HubConnectionState.CONNECTED) {
|
||||||
|
Log.d("ChatHubClient", "Joining chat room: $chatId")
|
||||||
hubConnection?.invoke("join_chat", chatId)
|
hubConnection?.invoke("join_chat", chatId)
|
||||||
?.doOnError { Log.e("ChatHubClient", "join_chat error", it) }
|
?.doOnError { Log.e("ChatHubClient", "join_chat error", it) }
|
||||||
?.subscribe()
|
?.subscribe()
|
||||||
Log.d("ChatHubClient", "Joined chat room: $chatId")
|
Log.d("ChatHubClient", "Joined chat room: $chatId")
|
||||||
} else {
|
} else {
|
||||||
Log.e("ChatHubClient", "Failed to join chat room $chatId: Not connected")
|
Log.e("ChatHubClient", "Failed to join chat room $chatId: Not connected (state=${hubConnection?.connectionState})")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package chats.data.remote.signalr
|
package chats.data.remote.signalr
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import core.network.NetworkManager
|
||||||
|
import core.network.ServerConfig
|
||||||
import core.notifications.data.ActiveChatTracker
|
import core.notifications.data.ActiveChatTracker
|
||||||
import core.notifications.data.NotificationHelper
|
import core.notifications.data.NotificationHelper
|
||||||
import core.security.TokenManager
|
import core.security.TokenManager
|
||||||
|
import chats.data.sync.MessageSyncWorker
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -12,8 +15,15 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.flow.filterIsInstance
|
import kotlinx.coroutines.flow.filterIsInstance
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
import chats.data.remote.signalr.ConnectionStatus
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class SignalRNotificationObserver @Inject constructor(
|
class SignalRNotificationObserver @Inject constructor(
|
||||||
@@ -21,41 +31,92 @@ class SignalRNotificationObserver @Inject constructor(
|
|||||||
private val activeChatTracker: ActiveChatTracker,
|
private val activeChatTracker: ActiveChatTracker,
|
||||||
private val tokenManager: TokenManager,
|
private val tokenManager: TokenManager,
|
||||||
private val chatRepository: chats.domain.repository.ChatRepository,
|
private val chatRepository: chats.domain.repository.ChatRepository,
|
||||||
|
private val serverConfig: ServerConfig,
|
||||||
|
private val networkManager: NetworkManager,
|
||||||
@ApplicationContext private val context: Context
|
@ApplicationContext private val context: Context
|
||||||
) {
|
) {
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
||||||
private var isStarted = false
|
private var isStarted = false
|
||||||
private val processedMessageIds = mutableSetOf<String>()
|
private val processedMessageIds = mutableSetOf<String>()
|
||||||
|
|
||||||
fun refresh() {
|
// OkHttpClient для ping запроса
|
||||||
scope.launch {
|
private val pingClient = OkHttpClient.Builder()
|
||||||
try {
|
.connectTimeout(5, TimeUnit.SECONDS)
|
||||||
val chats = chatRepository.getChats()
|
.readTimeout(5, TimeUnit.SECONDS)
|
||||||
val total = chats.sumOf { it.unreadCount }
|
.build()
|
||||||
activeChatTracker.setTotalUnreadCount(total)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// Ignore load error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
if (isStarted) return
|
if (isStarted) return
|
||||||
isStarted = true
|
isStarted = true
|
||||||
|
|
||||||
|
// Запускаем мониторинг сети
|
||||||
|
networkManager.startMonitoring()
|
||||||
|
|
||||||
|
// Принудительно обновляем состояние сети при старте
|
||||||
|
networkManager.refreshNetworkState()
|
||||||
|
|
||||||
|
// Подключаемся к SignalR при старте приложения
|
||||||
|
connectSignalR()
|
||||||
|
|
||||||
// Initial count load
|
// Initial count load
|
||||||
refresh()
|
refresh()
|
||||||
|
|
||||||
|
// Запускаем периодическую проверку подключения SignalR
|
||||||
|
startConnectionHealthCheck()
|
||||||
|
|
||||||
|
// Слушаем восстановление сети и переподключаем SignalR
|
||||||
|
networkManager.isOnline
|
||||||
|
.filter { it } // Только переход в онлайн
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.onEach {
|
||||||
|
android.util.Log.d("SignalRNtfObserver", "Network restored! Reconnecting SignalR and syncing...")
|
||||||
|
// Небольшая задержка чтобы сеть стабилизировалась
|
||||||
|
kotlinx.coroutines.delay(1500)
|
||||||
|
reconnectOnNetworkRestored()
|
||||||
|
}
|
||||||
|
.launchIn(scope)
|
||||||
|
|
||||||
|
// Также отслеживаем состояние SignalR для переподключения
|
||||||
|
signalrClient.status
|
||||||
|
.filter { it == ConnectionStatus.DISCONNECTED }
|
||||||
|
.onEach {
|
||||||
|
android.util.Log.d("SignalRNtfObserver", "SignalR disconnected, checking network...")
|
||||||
|
// Если мы offline и SignalR отключен - не делаем ничего
|
||||||
|
// Подключимся когда сеть восстановится
|
||||||
|
}
|
||||||
|
.launchIn(scope)
|
||||||
|
|
||||||
|
// При восстановлении соединения SignalR обновляем список чатов и вступаем в них
|
||||||
|
signalrClient.status
|
||||||
|
.filter { it == ConnectionStatus.CONNECTED }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.onEach {
|
||||||
|
android.util.Log.d("SignalRNtfObserver", "SignalR connected, refreshing chats and joining rooms")
|
||||||
|
onSignalRConnected()
|
||||||
|
}
|
||||||
|
.launchIn(scope)
|
||||||
|
|
||||||
|
// Слушаем события SignalR для уведомлений и обновления списка чатов
|
||||||
|
android.util.Log.d("SignalRNtfObserver", "Starting to listen to signalrClient.events")
|
||||||
signalrClient.events
|
signalrClient.events
|
||||||
.onEach { event ->
|
.onEach { event ->
|
||||||
|
android.util.Log.d("SignalRNtfObserver", ">>> Received event: ${event::class.simpleName}")
|
||||||
when (event) {
|
when (event) {
|
||||||
is ChatEvent.NewMessage -> {
|
is ChatEvent.NewMessage -> {
|
||||||
val currentUserId = tokenManager.getUserId()
|
val currentUserId = tokenManager.getUserId()
|
||||||
val message = event.message
|
val message = event.message
|
||||||
|
|
||||||
|
android.util.Log.d("SignalRNtfObserver", "New message: ${message.id} from ${message.senderId}, current: $currentUserId")
|
||||||
|
|
||||||
// Don't show if it's our message or already processed
|
// Don't show if it's our message or already processed
|
||||||
if (message.senderId == currentUserId) return@onEach
|
if (message.senderId == currentUserId) {
|
||||||
if (processedMessageIds.contains(message.id)) return@onEach
|
android.util.Log.d("SignalRNtfObserver", "Skipping - own message")
|
||||||
|
return@onEach
|
||||||
|
}
|
||||||
|
if (processedMessageIds.contains(message.id)) {
|
||||||
|
android.util.Log.d("SignalRNtfObserver", "Skipping - already processed")
|
||||||
|
return@onEach
|
||||||
|
}
|
||||||
|
|
||||||
// Mark as processed
|
// Mark as processed
|
||||||
processedMessageIds.add(message.id)
|
processedMessageIds.add(message.id)
|
||||||
@@ -70,8 +131,12 @@ class SignalRNotificationObserver @Inject constructor(
|
|||||||
refresh()
|
refresh()
|
||||||
|
|
||||||
// Don't show if this chat is currently open
|
// Don't show if this chat is currently open
|
||||||
if (activeChatTracker.currentChatId.value == message.chatId) return@onEach
|
if (activeChatTracker.currentChatId.value == message.chatId) {
|
||||||
|
android.util.Log.d("SignalRNtfObserver", "Skipping - chat is open: ${message.chatId}")
|
||||||
|
return@onEach
|
||||||
|
}
|
||||||
|
|
||||||
|
android.util.Log.d("SignalRNtfObserver", "Showing notification for chat: ${message.chatId}")
|
||||||
NotificationHelper.showNotification(
|
NotificationHelper.showNotification(
|
||||||
context = context,
|
context = context,
|
||||||
title = message.sender?.displayName ?: "Новое сообщение",
|
title = message.sender?.displayName ?: "Новое сообщение",
|
||||||
@@ -83,12 +148,236 @@ class SignalRNotificationObserver @Inject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
is ChatEvent.MessagesRead -> {
|
is ChatEvent.MessagesRead -> {
|
||||||
|
android.util.Log.d("SignalRNtfObserver", "Messages read event")
|
||||||
// If anyone read messages, sync our total count
|
// If anyone read messages, sync our total count
|
||||||
refresh()
|
refresh()
|
||||||
}
|
}
|
||||||
else -> Unit
|
else -> {
|
||||||
|
android.util.Log.d("SignalRNtfObserver", "Unhandled event: ${event::class.simpleName}")
|
||||||
|
Unit
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.launchIn(scope)
|
.launchIn(scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun startConnectionHealthCheck() {
|
||||||
|
// Каждые 10 секунд проверяем подключение и при необходимости переподключаемся
|
||||||
|
scope.launch {
|
||||||
|
var consecutiveFailures = 0
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
kotlinx.coroutines.delay(10000)
|
||||||
|
val currentStatus = signalrClient.status.value
|
||||||
|
val isOnline = networkManager.isOnline.value
|
||||||
|
|
||||||
|
if (isOnline && currentStatus == ConnectionStatus.DISCONNECTED) {
|
||||||
|
consecutiveFailures++
|
||||||
|
android.util.Log.d("SignalRNtfObserver", "Health check: Network online but SignalR disconnected (failures: $consecutiveFailures), reconnecting...")
|
||||||
|
signalrClient.reconnect()
|
||||||
|
} else if (isOnline && currentStatus == ConnectionStatus.CONNECTED) {
|
||||||
|
// Сбрасываем счетчик ошибок при успешном подключении
|
||||||
|
consecutiveFailures = 0
|
||||||
|
android.util.Log.d("SignalRNtfObserver", "Health check: Connection healthy")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onSignalRConnected() {
|
||||||
|
android.util.Log.d("SignalRNtfObserver", "SignalR connected - syncing missed messages...")
|
||||||
|
refresh()
|
||||||
|
// Вступаем во все чаты для получения событий
|
||||||
|
joinAllChats()
|
||||||
|
// Синхронизируем пропущенные сообщения
|
||||||
|
syncMissedMessages()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Синхронизирует пропущенные сообщения после восстановления соединения
|
||||||
|
* Запрашивает только НОВЫЕ сообщения с последнего известного sequenceId
|
||||||
|
*/
|
||||||
|
private fun syncMissedMessages() {
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
android.util.Log.d("SignalRNtfObserver", "Starting missed messages sync...")
|
||||||
|
val chats = chatRepository.getChats()
|
||||||
|
android.util.Log.d("SignalRNtfObserver", "Syncing ${chats.size} chats for missed messages")
|
||||||
|
|
||||||
|
// Запрашиваем только новые сообщения для каждого чата
|
||||||
|
chats.forEach { chat ->
|
||||||
|
try {
|
||||||
|
// Получаем последний известный sequenceId из локальной базы
|
||||||
|
val lastSequenceId = chatRepository.getLastKnownSequenceId(chat.id)
|
||||||
|
|
||||||
|
if (lastSequenceId != null) {
|
||||||
|
// Запрашиваем сообщения ПОСЛЕ lastSequenceId (только новые)
|
||||||
|
android.util.Log.d("SignalRNtfObserver", "Chat ${chat.id}: last known seqId=$lastSequenceId, fetching newer...")
|
||||||
|
chatRepository.getMessages(
|
||||||
|
chatId = chat.id,
|
||||||
|
afterSequenceId = lastSequenceId.toLong(),
|
||||||
|
limit = 100
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Нет локальных сообщений - загружаем последние 50
|
||||||
|
android.util.Log.d("SignalRNtfObserver", "Chat ${chat.id}: no local messages, fetching last 50")
|
||||||
|
chatRepository.getMessages(chatId = chat.id, limit = 50)
|
||||||
|
}
|
||||||
|
|
||||||
|
android.util.Log.d("SignalRNtfObserver", "Synced chat ${chat.id}")
|
||||||
|
// Небольшая пауза между чатами чтобы не перегружать сервер
|
||||||
|
kotlinx.coroutines.delay(100)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("SignalRNtfObserver", "Failed to sync chat ${chat.id}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
android.util.Log.d("SignalRNtfObserver", "Missed messages sync completed")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("SignalRNtfObserver", "Sync failed", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает последний известный sequenceId для чата из локальной базы
|
||||||
|
*/
|
||||||
|
private suspend fun getLastKnownSequenceId(chatId: String): Int? {
|
||||||
|
return try {
|
||||||
|
chatRepository.getLastKnownSequenceId(chatId)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("SignalRNtfObserver", "Failed to get last sequenceId for $chatId", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun reconnectOnNetworkRestored() {
|
||||||
|
android.util.Log.d("SignalRNtfObserver", "Network restored, starting reconnection sequence...")
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
// 1. Сначала делаем HTTP ping запрос чтобы "разбудить" сетевой стек
|
||||||
|
android.util.Log.d("SignalRNtfObserver", "Sending HTTP ping to wake up network...")
|
||||||
|
val pingSuccess = sendHttpPing()
|
||||||
|
android.util.Log.d("SignalRNtfObserver", "HTTP ping result: $pingSuccess")
|
||||||
|
|
||||||
|
// 2. Небольшая пауза для стабилизации
|
||||||
|
kotlinx.coroutines.delay(1000)
|
||||||
|
|
||||||
|
// 3. Принудительное переподключение SignalR
|
||||||
|
android.util.Log.d("SignalRNtfObserver", "Forcing SignalR reconnect...")
|
||||||
|
signalrClient.reconnect()
|
||||||
|
|
||||||
|
// 4. Ждем пока SignalR подключится (максимум 10 секунд)
|
||||||
|
var waitCount = 0
|
||||||
|
while (signalrClient.status.value != ConnectionStatus.CONNECTED && waitCount < 20) {
|
||||||
|
kotlinx.coroutines.delay(500)
|
||||||
|
waitCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signalrClient.status.value == ConnectionStatus.CONNECTED) {
|
||||||
|
android.util.Log.d("SignalRNtfObserver", "SignalR reconnected, syncing missed messages...")
|
||||||
|
// 5. Синхронизируем пропущенные сообщения
|
||||||
|
syncMissedMessages()
|
||||||
|
} else {
|
||||||
|
android.util.Log.w("SignalRNtfObserver", "SignalR failed to reconnect within timeout")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Запускаем синхронизацию отложенных сообщений
|
||||||
|
MessageSyncWorker.scheduleSync(context)
|
||||||
|
android.util.Log.d("SignalRNtfObserver", "Outgoing sync worker scheduled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отправляет HTTP ping запрос для активации сетевого соединения
|
||||||
|
*/
|
||||||
|
private suspend fun sendHttpPing(): Boolean {
|
||||||
|
return try {
|
||||||
|
val baseUrl = serverConfig.getBaseUrl()
|
||||||
|
val token = tokenManager.getToken()
|
||||||
|
|
||||||
|
if (baseUrl.isBlank() || token == null) {
|
||||||
|
android.util.Log.w("SignalRNtfObserver", "Cannot ping: missing baseUrl or token")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val url = "${baseUrl.removeSuffix("/api/")}/api/auth/refresh"
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.post(okhttp3.RequestBody.create(null, "{}"))
|
||||||
|
.addHeader("Authorization", "Bearer $token")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response = pingClient.newCall(request).execute()
|
||||||
|
val success = response.isSuccessful || response.code == 401 // 401 OK для refresh
|
||||||
|
android.util.Log.d("SignalRNtfObserver", "HTTP ping to $url: ${response.code}")
|
||||||
|
response.close()
|
||||||
|
success
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("SignalRNtfObserver", "HTTP ping failed", e)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun connectSignalR() {
|
||||||
|
val token = tokenManager.getToken()
|
||||||
|
val baseUrl = serverConfig.getBaseUrl()
|
||||||
|
|
||||||
|
if (token == null || baseUrl.isBlank()) {
|
||||||
|
android.util.Log.w("SignalRNtfObserver", "Cannot connect SignalR: token=${token != null}, baseUrl=$baseUrl")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val isOnline = networkManager.isOnline.value
|
||||||
|
if (!isOnline) {
|
||||||
|
android.util.Log.w("SignalRNtfObserver", "Cannot connect SignalR: network is offline")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем текущее состояние SignalR
|
||||||
|
val currentStatus = signalrClient.status.value
|
||||||
|
if (currentStatus == ConnectionStatus.CONNECTED) {
|
||||||
|
android.util.Log.d("SignalRNtfObserver", "SignalR already connected, skipping")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
android.util.Log.d("SignalRNtfObserver", "Connecting SignalR with token: ${token.take(10)}..., baseUrl: $baseUrl")
|
||||||
|
signalrClient.connect(baseUrl.removeSuffix("/api/"), token)
|
||||||
|
|
||||||
|
// Если подключение не удалось в течение 5 секунд - пробуем снова
|
||||||
|
scope.launch {
|
||||||
|
kotlinx.coroutines.delay(5000)
|
||||||
|
if (signalrClient.status.value == ConnectionStatus.DISCONNECTED) {
|
||||||
|
android.util.Log.w("SignalRNtfObserver", "SignalR connection timeout, retrying with reconnect()...")
|
||||||
|
signalrClient.reconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun joinAllChats() {
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
val chats = chatRepository.getChats()
|
||||||
|
android.util.Log.d("SignalRNtfObserver", "Joining ${chats.size} chat rooms")
|
||||||
|
chats.forEach { chat ->
|
||||||
|
signalrClient.joinChat(chat.id)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("SignalRNtfObserver", "Failed to join chats", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refresh() {
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
val chats = chatRepository.getChats()
|
||||||
|
val total = chats.sumOf { it.unreadCount }
|
||||||
|
activeChatTracker.setTotalUnreadCount(total)
|
||||||
|
android.util.Log.d("SignalRNtfObserver", "Refreshed chats: ${chats.size}, total unread: $total")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("SignalRNtfObserver", "Failed to refresh", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,12 +140,12 @@ class ChatRepositoryImpl @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getMessages(
|
override suspend fun getMessages(
|
||||||
chatId: String, cursor: String?, pivot: Long?, limit: Int?
|
chatId: String, cursor: String?, pivot: Long?, afterSequenceId: Long?, limit: Int?
|
||||||
): List<Message> {
|
): List<Message> {
|
||||||
val currentUserId = tokenManager.getUserId() ?: ""
|
val currentUserId = tokenManager.getUserId() ?: ""
|
||||||
return try {
|
return try {
|
||||||
Log.d(TAG, "Fetching messages from API: chatId=$chatId")
|
Log.d(TAG, "Fetching messages from API: chatId=$chatId, afterSequenceId=$afterSequenceId, limit=$limit")
|
||||||
val messages = api.getMessages(chatId, cursor = cursor, limit = limit)
|
val messages = api.getMessages(chatId, cursor = cursor, pivot = pivot, afterSequenceId = afterSequenceId, limit = limit)
|
||||||
|
|
||||||
if (messages.isNotEmpty()) {
|
if (messages.isNotEmpty()) {
|
||||||
val entities = messages.map { it.toEntity(baseUrl, currentUserId, gson) }
|
val entities = messages.map { it.toEntity(baseUrl, currentUserId, gson) }
|
||||||
@@ -160,6 +160,15 @@ class ChatRepositoryImpl @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getLastKnownSequenceId(chatId: String): Int? {
|
||||||
|
return try {
|
||||||
|
messageDao.getMaxSequenceId(chatId)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to get last sequenceId", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun sendMessage(
|
override suspend fun sendMessage(
|
||||||
chatId: String, content: String?, type: String,
|
chatId: String, content: String?, type: String,
|
||||||
attachments: List<chats.data.remote.api.AttachmentRequest>?,
|
attachments: List<chats.data.remote.api.AttachmentRequest>?,
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ interface ChatRepository {
|
|||||||
fun getMessagesPaging(chatId: String): Flow<PagingData<Message>>
|
fun getMessagesPaging(chatId: String): Flow<PagingData<Message>>
|
||||||
|
|
||||||
// Загрузка из сети (для начальной синхронизации)
|
// Загрузка из сети (для начальной синхронизации)
|
||||||
suspend fun getMessages(chatId: String, cursor: String? = null, pivot: Long? = null, limit: Int? = null): List<Message>
|
suspend fun getMessages(chatId: String, cursor: String? = null, pivot: Long? = null, afterSequenceId: Long? = null, limit: Int? = null): List<Message>
|
||||||
|
|
||||||
|
// Получить последний известный sequenceId для чата (из локальной базы)
|
||||||
|
suspend fun getLastKnownSequenceId(chatId: String): Int?
|
||||||
|
|
||||||
suspend fun sendMessage(
|
suspend fun sendMessage(
|
||||||
chatId: String,
|
chatId: String,
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import chats.data.remote.signalr.ChatEvent
|
import chats.data.remote.signalr.ChatEvent
|
||||||
import chats.data.remote.signalr.ChatHubClient
|
import chats.data.remote.signalr.ChatHubClient
|
||||||
|
import chats.data.remote.signalr.ConnectionStatus
|
||||||
import chats.data.repository.toDomain
|
import chats.data.repository.toDomain
|
||||||
import chats.domain.model.Message
|
import chats.domain.model.Message
|
||||||
import chats.domain.repository.ChatRepository
|
import chats.domain.repository.ChatRepository
|
||||||
|
import core.network.NetworkManager
|
||||||
import core.network.ServerConfig
|
import core.network.ServerConfig
|
||||||
import core.security.TokenManager
|
import core.security.TokenManager
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
@@ -59,6 +61,7 @@ class ChatDetailViewModel @Inject constructor(
|
|||||||
private val tokenManager: TokenManager,
|
private val tokenManager: TokenManager,
|
||||||
private val activeChatTracker: core.notifications.data.ActiveChatTracker,
|
private val activeChatTracker: core.notifications.data.ActiveChatTracker,
|
||||||
private val signalrNotificationObserver: chats.data.remote.signalr.SignalRNotificationObserver,
|
private val signalrNotificationObserver: chats.data.remote.signalr.SignalRNotificationObserver,
|
||||||
|
private val networkManager: NetworkManager,
|
||||||
@dagger.hilt.android.qualifiers.ApplicationContext private val context: android.content.Context
|
@dagger.hilt.android.qualifiers.ApplicationContext private val context: android.content.Context
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
@@ -114,12 +117,39 @@ class ChatDetailViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadChatInfo(chatId)
|
loadChatInfo(chatId)
|
||||||
|
observeSignalRStatus(chatId)
|
||||||
observeSignalREvents(chatId)
|
observeSignalREvents(chatId)
|
||||||
|
observeNetworkStatus(chatId)
|
||||||
|
|
||||||
// Initial sync from network
|
// Initial sync from network
|
||||||
refreshMessages(chatId)
|
refreshMessages(chatId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun observeNetworkStatus(chatId: String) {
|
||||||
|
// При восстановлении сети обновляем сообщения
|
||||||
|
networkManager.isOnline
|
||||||
|
.filter { it } // Только переход в онлайн
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.onEach {
|
||||||
|
android.util.Log.d(TAG, "Network restored in chat detail, refreshing messages")
|
||||||
|
kotlinx.coroutines.delay(1000) // Дадим сети стабилизироваться
|
||||||
|
refreshMessages(chatId)
|
||||||
|
}
|
||||||
|
.launchIn(viewModelScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observeSignalRStatus(chatId: String) {
|
||||||
|
// При переподключении SignalR обновляем сообщения
|
||||||
|
signalrClient.status
|
||||||
|
.filter { it == ConnectionStatus.CONNECTED }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.onEach {
|
||||||
|
android.util.Log.d(TAG, "SignalR connected, refreshing messages for chat $chatId")
|
||||||
|
refreshMessages(chatId)
|
||||||
|
}
|
||||||
|
.launchIn(viewModelScope)
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateMessages(messages: List<Message>) {
|
private fun updateMessages(messages: List<Message>) {
|
||||||
val sortedMessages = messages.sortedByDescending { it.sequenceId }
|
val sortedMessages = messages.sortedByDescending { it.sequenceId }
|
||||||
_state.update { it.copy(
|
_state.update { it.copy(
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import chats.domain.model.Chat
|
import chats.domain.model.Chat
|
||||||
import chats.domain.repository.ChatRepository
|
import chats.domain.repository.ChatRepository
|
||||||
import chats.data.remote.signalr.ChatHubClient
|
import chats.data.remote.signalr.ChatHubClient
|
||||||
|
import chats.data.remote.signalr.ConnectionStatus
|
||||||
import chats.data.remote.signalr.ChatEvent
|
import chats.data.remote.signalr.ChatEvent
|
||||||
|
import core.network.NetworkManager
|
||||||
import core.network.ServerConfig
|
import core.network.ServerConfig
|
||||||
import core.security.TokenManager
|
import core.security.TokenManager
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
@@ -26,52 +28,34 @@ data class ChatListState(
|
|||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ChatListViewModel @Inject constructor(
|
class ChatListViewModel @Inject constructor(
|
||||||
private val repository: ChatRepository,
|
private val repository: ChatRepository,
|
||||||
private val authRepository: auth.domain.repository.AuthRepository,
|
private val hubClient: ChatHubClient,
|
||||||
private val signalrClient: ChatHubClient,
|
|
||||||
private val serverConfig: ServerConfig,
|
private val serverConfig: ServerConfig,
|
||||||
private val tokenManager: TokenManager
|
private val tokenManager: TokenManager,
|
||||||
|
private val networkManager: NetworkManager
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _state = MutableStateFlow(ChatListState())
|
private val _state = MutableStateFlow(ChatListState())
|
||||||
val state: StateFlow<ChatListState> = _state.asStateFlow()
|
val state: StateFlow<ChatListState> = _state.asStateFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
updatePushToken()
|
|
||||||
|
|
||||||
val isStoriesEnabled = try {
|
|
||||||
serverConfig.getServerConfig().features.stories
|
|
||||||
} catch (e: Exception) {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
_state.update { it.copy(isStoriesEnabled = isStoriesEnabled) }
|
|
||||||
|
|
||||||
val token = tokenManager.getToken()
|
|
||||||
val baseUrl = serverConfig.getBaseUrl()
|
|
||||||
|
|
||||||
// Подключаемся к SignalR только если есть токен И введён URL сервера
|
|
||||||
if (token != null && baseUrl.isNotBlank()) {
|
|
||||||
signalrClient.connect(baseUrl.removeSuffix("/api/"), token)
|
|
||||||
}
|
|
||||||
|
|
||||||
loadChats()
|
loadChats()
|
||||||
|
observeSignalRStatus()
|
||||||
observeSignalREvents()
|
observeSignalREvents()
|
||||||
|
observeNetworkStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updatePushToken() {
|
private fun observeNetworkStatus() {
|
||||||
com.google.firebase.messaging.FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
|
// При восстановлении сети обновляем чаты
|
||||||
if (task.isSuccessful) {
|
networkManager.isOnline
|
||||||
val token = task.result
|
.filter { it } // Только переход в онлайн
|
||||||
viewModelScope.launch {
|
.distinctUntilChanged()
|
||||||
try {
|
.onEach {
|
||||||
authRepository.updatePushToken(token)
|
android.util.Log.d(TAG, "Network restored in chat list, refreshing chats")
|
||||||
} catch (e: Exception) {
|
kotlinx.coroutines.delay(1000) // Дадим сети стабилизироваться
|
||||||
android.util.Log.e("ChatListVM", "Failed to update push token: ${e.message}")
|
repository.getChats()
|
||||||
}
|
}
|
||||||
|
.launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun getCurrentUserId(): String = tokenManager.getUserId() ?: ""
|
private fun getCurrentUserId(): String = tokenManager.getUserId() ?: ""
|
||||||
|
|
||||||
@@ -102,6 +86,19 @@ class ChatListViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun observeSignalRStatus() {
|
||||||
|
// Наблюдаем за статусом подключения SignalR и обновляем чаты при переподключении
|
||||||
|
hubClient.status
|
||||||
|
.filter { it == ConnectionStatus.CONNECTED }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.onEach {
|
||||||
|
android.util.Log.d(TAG, "SignalR connected, refreshing chats")
|
||||||
|
// При переподключении обновляем чаты из сети
|
||||||
|
repository.getChats()
|
||||||
|
}
|
||||||
|
.launchIn(viewModelScope)
|
||||||
|
}
|
||||||
|
|
||||||
private fun sortChats(chats: List<Chat>): List<Chat> {
|
private fun sortChats(chats: List<Chat>): List<Chat> {
|
||||||
return chats.sortedWith(compareByDescending<Chat> {
|
return chats.sortedWith(compareByDescending<Chat> {
|
||||||
it.name.equals("Избранное", ignoreCase = true) || it.name.equals("Saved Messages", ignoreCase = true)
|
it.name.equals("Избранное", ignoreCase = true) || it.name.equals("Saved Messages", ignoreCase = true)
|
||||||
@@ -109,15 +106,17 @@ class ChatListViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun observeSignalREvents() {
|
private fun observeSignalREvents() {
|
||||||
signalrClient.events
|
val baseUrl = serverConfig.getBaseUrl().removeSuffix("/api/")
|
||||||
|
android.util.Log.d(TAG, "Starting to observe SignalR events")
|
||||||
|
hubClient.events
|
||||||
.onEach { event ->
|
.onEach { event ->
|
||||||
|
android.util.Log.d(TAG, ">>> ChatListVM received event: ${event::class.simpleName}")
|
||||||
when (event) {
|
when (event) {
|
||||||
is ChatEvent.NewMessage -> {
|
is ChatEvent.NewMessage -> {
|
||||||
updateChatsWithNewMessage(event)
|
updateChatsWithNewMessage(event)
|
||||||
}
|
}
|
||||||
is ChatEvent.NewChat -> {
|
is ChatEvent.NewChat -> {
|
||||||
val currentUserId = getCurrentUserId()
|
val currentUserId = getCurrentUserId()
|
||||||
val baseUrl = serverConfig.getBaseUrl().removeSuffix("/api/")
|
|
||||||
_state.update { it.copy(chats = listOf(event.chat.toDomain(currentUserId, baseUrl)) + it.chats) }
|
_state.update { it.copy(chats = listOf(event.chat.toDomain(currentUserId, baseUrl)) + it.chats) }
|
||||||
}
|
}
|
||||||
is ChatEvent.MessagesRead -> {
|
is ChatEvent.MessagesRead -> {
|
||||||
@@ -144,8 +143,13 @@ class ChatListViewModel @Inject constructor(
|
|||||||
val currentUserId = getCurrentUserId()
|
val currentUserId = getCurrentUserId()
|
||||||
|
|
||||||
_state.update { currentState ->
|
_state.update { currentState ->
|
||||||
val updatedChats = currentState.chats.map { chat ->
|
val chatIndex = currentState.chats.indexOfFirst {
|
||||||
if (chat.id.equals(event.message.chatId, ignoreCase = true)) {
|
it.id.equals(event.message.chatId, ignoreCase = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chatIndex >= 0) {
|
||||||
|
// Чат есть в списке - обновляем его
|
||||||
|
val chat = currentState.chats[chatIndex]
|
||||||
val isMyMessage = event.message.senderId == currentUserId
|
val isMyMessage = event.message.senderId == currentUserId
|
||||||
val isAlreadySeen = chat.lastMessage?.id == event.message.id
|
val isAlreadySeen = chat.lastMessage?.id == event.message.id
|
||||||
|
|
||||||
@@ -160,14 +164,26 @@ class ChatListViewModel @Inject constructor(
|
|||||||
android.util.Log.d("ChatListVM", "Message ${event.message.id} -> Count ${chat.unreadCount} -> $newCount")
|
android.util.Log.d("ChatListVM", "Message ${event.message.id} -> Count ${chat.unreadCount} -> $newCount")
|
||||||
}
|
}
|
||||||
|
|
||||||
chat.copy(
|
val updatedChat = chat.copy(
|
||||||
lastMessage = lastMsgDomain,
|
lastMessage = lastMsgDomain,
|
||||||
unreadCount = newCount
|
unreadCount = newCount
|
||||||
)
|
)
|
||||||
} else chat
|
|
||||||
}
|
|
||||||
|
|
||||||
|
val updatedChats = currentState.chats.toMutableList()
|
||||||
|
updatedChats[chatIndex] = updatedChat
|
||||||
currentState.copy(chats = sortChats(updatedChats))
|
currentState.copy(chats = sortChats(updatedChats))
|
||||||
|
} else {
|
||||||
|
// Чата нет в списке - обновляем весь список из репозитория
|
||||||
|
android.util.Log.d("ChatListVM", "Chat ${event.message.chatId} not found in list, refreshing from repository")
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
repository.getChats() // Это обновит Room и Flow
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("ChatListVM", "Failed to refresh chats", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentState
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
package core.network
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.Network
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import android.net.NetworkRequest
|
||||||
|
import android.util.Log
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Менеджер состояния сети
|
||||||
|
* Отслеживает подключение к интернету через NetworkCallback + BroadcastReceiver
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
class NetworkManager @Inject constructor(
|
||||||
|
private val context: Context
|
||||||
|
) {
|
||||||
|
private val _isOnline = MutableStateFlow(isNetworkAvailable())
|
||||||
|
val isOnline: StateFlow<Boolean> = _isOnline.asStateFlow()
|
||||||
|
|
||||||
|
private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
|
||||||
|
private var networkReceiver: NetworkReceiver? = null
|
||||||
|
private var isMonitoring = false
|
||||||
|
|
||||||
|
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
|
||||||
|
override fun onAvailable(network: Network) {
|
||||||
|
Log.d("NetworkManager", "Network available - callback")
|
||||||
|
_isOnline.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLost(network: Network) {
|
||||||
|
Log.d("NetworkManager", "Network lost - callback")
|
||||||
|
_isOnline.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCapabilitiesChanged(
|
||||||
|
network: Network,
|
||||||
|
networkCapabilities: NetworkCapabilities
|
||||||
|
) {
|
||||||
|
val hasInternet = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
|
val hasValidated = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||||
|
Log.d("NetworkManager", "Capabilities changed: hasInternet=$hasInternet, hasValidated=$hasValidated")
|
||||||
|
_isOnline.value = hasInternet && hasValidated
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUnavailable() {
|
||||||
|
Log.d("NetworkManager", "Network unavailable - callback")
|
||||||
|
_isOnline.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startMonitoring() {
|
||||||
|
if (isMonitoring) {
|
||||||
|
Log.w("NetworkManager", "Already monitoring, skipping")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isMonitoring = true
|
||||||
|
val cm = connectivityManager ?: run {
|
||||||
|
Log.e("NetworkManager", "ConnectivityManager is null")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Регистрируем NetworkCallback для активного отслеживания
|
||||||
|
val networkRequest = NetworkRequest.Builder()
|
||||||
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
cm.registerNetworkCallback(networkRequest, networkCallback)
|
||||||
|
Log.d("NetworkManager", "Registered NetworkCallback")
|
||||||
|
|
||||||
|
// 2. Регистрируем BroadcastReceiver как запасной механизм
|
||||||
|
// (на Android 10+ работает только для foreground приложений)
|
||||||
|
val receiver = NetworkReceiver(this)
|
||||||
|
networkReceiver = receiver
|
||||||
|
val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
|
||||||
|
try {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
context.registerReceiver(receiver, filter)
|
||||||
|
Log.d("NetworkManager", "Registered NetworkReceiver")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w("NetworkManager", "Failed to register BroadcastReceiver: ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Инициализируем текущее состояние
|
||||||
|
_isOnline.value = isNetworkAvailable()
|
||||||
|
Log.d("NetworkManager", "Initial network state: ${_isOnline.value}")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("NetworkManager", "Error setting up network monitoring", e)
|
||||||
|
_isOnline.value = isNetworkAvailable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopMonitoring() {
|
||||||
|
if (!isMonitoring) return
|
||||||
|
isMonitoring = false
|
||||||
|
|
||||||
|
try {
|
||||||
|
connectivityManager?.unregisterNetworkCallback(networkCallback)
|
||||||
|
Log.d("NetworkManager", "Unregistered NetworkCallback")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("NetworkManager", "Error unregistering NetworkCallback", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
networkReceiver?.let {
|
||||||
|
context.unregisterReceiver(it)
|
||||||
|
it.cleanup()
|
||||||
|
networkReceiver = null
|
||||||
|
Log.d("NetworkManager", "Unregistered NetworkReceiver")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("NetworkManager", "Error unregistering NetworkReceiver", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isNetworkAvailable(): Boolean {
|
||||||
|
return try {
|
||||||
|
val network = connectivityManager?.activeNetwork
|
||||||
|
val capabilities = connectivityManager?.getNetworkCapabilities(network)
|
||||||
|
val available = capabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) == true
|
||||||
|
Log.d("NetworkManager", "isNetworkAvailable check: $available")
|
||||||
|
available
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("NetworkManager", "Error checking network", e)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun notifyNetworkRestored() {
|
||||||
|
Log.d("NetworkManager", "Network restored notified - forcing state update")
|
||||||
|
_isOnline.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Принудительно проверяет текущее состояние сети и уведомляет подписчиков
|
||||||
|
*/
|
||||||
|
fun refreshNetworkState() {
|
||||||
|
val currentState = isNetworkAvailable()
|
||||||
|
Log.d("NetworkManager", "Refreshed network state: $currentState")
|
||||||
|
_isOnline.value = currentState
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package core.network
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.util.Log
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BroadcastReceiver для надежного отслеживания изменений сети
|
||||||
|
* Используется как дополнение к NetworkCallback для лучшей надежности
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
class NetworkReceiver @Inject constructor(
|
||||||
|
private val networkManager: NetworkManager
|
||||||
|
) : BroadcastReceiver() {
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
||||||
|
private var lastKnownState: Boolean = false
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
if (intent.action == ConnectivityManager.CONNECTIVITY_ACTION) {
|
||||||
|
val isConnected = isNetworkAvailable(context)
|
||||||
|
|
||||||
|
Log.d("NetworkReceiver", "Broadcast received: isConnected=$isConnected, lastKnown=$lastKnownState")
|
||||||
|
|
||||||
|
// Отправляем уведомление только если состояние изменилось
|
||||||
|
if (isConnected != lastKnownState) {
|
||||||
|
lastKnownState = isConnected
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
if (isConnected) {
|
||||||
|
Log.d("NetworkReceiver", "Network connected - notifying NetworkManager")
|
||||||
|
// Небольшая задержка для стабилизации сети
|
||||||
|
kotlinx.coroutines.delay(500)
|
||||||
|
networkManager.notifyNetworkRestored()
|
||||||
|
} else {
|
||||||
|
Log.d("NetworkReceiver", "Network disconnected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isNetworkAvailable(context: Context): Boolean {
|
||||||
|
return try {
|
||||||
|
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
|
||||||
|
val network = cm?.activeNetwork
|
||||||
|
val capabilities = cm?.getNetworkCapabilities(network)
|
||||||
|
capabilities?.hasCapability(android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET) == true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("NetworkReceiver", "Error checking network", e)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cleanup() {
|
||||||
|
scope.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user