Files
chat/docs/MOBILE_APP_INTEGRATION_GUIDE.md
Andrey K. Choi 3050e084fa
All checks were successful
continuous-integration/drone/push Build is passing
main functions commit
2025-10-19 19:50:00 +09:00

38 KiB
Raw Blame History

🚨 ПОЛНОЕ ТЕХНИЧЕСКОЕ ЗАДАНИЕ: Доработка модуля экстренных сообщений для мобильного приложения

📋 ОБЗОР ПРОЕКТА

Цель: Интегрировать мобильное приложение с Emergency Service через WebSocket и REST API Архитектура: Микросервисная система с JWT аутентификацией Основные компоненты: Emergency Service (порт 8002), API Gateway (порт 8000), WebSocket подключения


🔧 ЭТАП 1: НАСТРОЙКА АУТЕНТИФИКАЦИИ

1.1 Удаление временных токенов

Проблема: В коде используются временные токены вида temp_token_for_${email}

Задачи:

// ❌ УДАЛИТЬ ЭТО:
val tempToken = "temp_token_for_${userEmail}"
headers["Authorization"] = "Bearer $tempToken"

// ✅ ЗАМЕНИТЬ НА:
val jwtToken = authManager.getValidJwtToken()
headers["Authorization"] = "Bearer $jwtToken"

1.2 Реализация JWT аутентификации

Создать класс AuthManager:

class AuthManager {
    private val baseUrl = "http://YOUR_SERVER:8000"
    private var jwtToken: String? = null
    private var refreshToken: String? = null
    
    suspend fun login(email: String, password: String): Result<LoginResponse> {
        val loginData = LoginRequest(email, password)
        
        return try {
            val response = apiService.login(loginData)
            if (response.isSuccessful) {
                val authData = response.body()!!
                jwtToken = authData.access_token
                saveTokens(authData.access_token, authData.refresh_token)
                Result.success(authData)
            } else {
                Result.failure(Exception("Login failed: ${response.code()}"))
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
    
    fun getValidJwtToken(): String? {
        return if (isTokenValid(jwtToken)) jwtToken else null
    }
    
    private fun isTokenValid(token: String?): Boolean {
        // Проверка срока действия JWT токена
        if (token.isNullOrEmpty()) return false
        
        return try {
            val payload = decodeJwtPayload(token)
            val exp = payload["exp"] as? Long ?: 0
            exp > System.currentTimeMillis() / 1000
        } catch (e: Exception) {
            false
        }
    }
}

1.3 Модели данных для аутентификации

data class LoginRequest(
    val email: String,
    val password: String
)

data class LoginResponse(
    val access_token: String,
    val token_type: String,
    val user: UserData
)

data class UserData(
    val id: Int,
    val email: String,
    val first_name: String?,
    val last_name: String?
)

🌐 ЭТАП 2: НАСТРОЙКА REST API CLIENT

2.1 Создание Emergency API Service

interface EmergencyApiService {
    
    @POST("api/v1/alert")
    suspend fun createAlert(
        @Body alertRequest: CreateAlertRequest,
        @Header("Authorization") auth: String
    ): Response<EmergencyAlertResponse>
    
    @GET("api/v1/alerts/my")
    suspend fun getMyAlerts(
        @Header("Authorization") auth: String
    ): Response<List<EmergencyAlertResponse>>
    
    @GET("api/v1/alerts/active")
    suspend fun getActiveAlerts(
        @Header("Authorization") auth: String
    ): Response<List<EmergencyAlertResponse>>
    
    @GET("api/v1/alerts/nearby")
    suspend fun getNearbyAlerts(
        @Query("latitude") latitude: Double,
        @Query("longitude") longitude: Double,
        @Query("radius") radius: Int,
        @Header("Authorization") auth: String
    ): Response<List<NearbyAlertResponse>>
    
    @POST("api/v1/alert/{alertId}/respond")
    suspend fun respondToAlert(
        @Path("alertId") alertId: Int,
        @Body response: AlertResponseRequest,
        @Header("Authorization") auth: String
    ): Response<EmergencyResponseResponse>
    
    @POST("api/v1/report")
    suspend fun createReport(
        @Body reportRequest: CreateReportRequest,
        @Header("Authorization") auth: String
    ): Response<EmergencyReportResponse>
    
    @POST("api/v1/safety-check")
    suspend fun createSafetyCheck(
        @Body safetyCheck: SafetyCheckRequest,
        @Header("Authorization") auth: String
    ): Response<SafetyCheckResponse>
    
    @GET("api/v1/stats")
    suspend fun getStatistics(
        @Header("Authorization") auth: String
    ): Response<EmergencyStatistics>
}

2.2 Модели данных для Emergency API

// Запросы
data class CreateAlertRequest(
    val alert_type: String, // "medical", "fire", "police", "other"
    val latitude: Double,
    val longitude: Double,
    val address: String? = null,
    val description: String
)

data class CreateReportRequest(
    val incident_type: String,
    val latitude: Double,
    val longitude: Double,
    val address: String? = null,
    val description: String,
    val severity: String // "low", "medium", "high"
)

data class SafetyCheckRequest(
    val latitude: Double,
    val longitude: Double,
    val status: String, // "safe", "unsafe", "need_help"
    val message: String? = null
)

data class AlertResponseRequest(
    val response_type: String, // "help_on_way", "safe_now", "false_alarm"
    val message: String? = null
)

// Ответы
data class EmergencyAlertResponse(
    val id: Int,
    val alert_type: String,
    val latitude: Double,
    val longitude: Double,
    val address: String?,
    val description: String,
    val status: String,
    val created_at: String,
    val responded_users_count: Int
)

data class NearbyAlertResponse(
    val id: Int,
    val alert_type: String,
    val latitude: Double,
    val longitude: Double,
    val address: String?,
    val distance: Double,
    val created_at: String,
    val responded_users_count: Int
)

data class EmergencyResponseResponse(
    val id: Int,
    val response_type: String,
    val message: String?,
    val created_at: String
)

data class EmergencyReportResponse(
    val id: Int,
    val incident_type: String,
    val latitude: Double,
    val longitude: Double,
    val address: String?,
    val description: String,
    val severity: String,
    val status: String,
    val created_at: String
)

data class SafetyCheckResponse(
    val id: Int,
    val latitude: Double,
    val longitude: Double,
    val status: String,
    val message: String?,
    val created_at: String
)

data class EmergencyStatistics(
    val total_alerts: Int,
    val active_alerts: Int,
    val resolved_alerts: Int,
    val total_reports: Int,
    val my_alerts_count: Int,
    val my_responses_count: Int
)

📡 ЭТАП 3: РЕАЛИЗАЦИЯ WEBSOCKET ПОДКЛЮЧЕНИЯ

3.1 WebSocket Manager

class EmergencyWebSocketManager(
    private val authManager: AuthManager,
    private val coroutineScope: CoroutineScope
) {
    private var webSocket: WebSocket? = null
    private var isConnected = false
    private val listeners = mutableListOf<EmergencyWebSocketListener>()
    
    fun connect() {
        val jwtToken = authManager.getValidJwtToken()
        if (jwtToken == null) {
            notifyError("No valid JWT token available")
            return
        }
        
        val request = Request.Builder()
            .url("ws://YOUR_SERVER:8002/api/v1/emergency/ws/current_user_id?token=$jwtToken")
            .build()
            
        val client = OkHttpClient.Builder()
            .readTimeout(30, TimeUnit.SECONDS)
            .build()
            
        webSocket = client.newWebSocket(request, object : WebSocketListener() {
            override fun onOpen(webSocket: WebSocket, response: Response) {
                isConnected = true
                notifyConnectionOpened()
                
                // Отправляем ping для поддержания соединения
                startPingKeepAlive()
            }
            
            override fun onMessage(webSocket: WebSocket, text: String) {
                handleMessage(text)
            }
            
            override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
                isConnected = false
                notifyConnectionClosing(code, reason)
            }
            
            override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
                isConnected = false
                notifyError("WebSocket connection failed: ${t.message}")
                
                // Автоматическое переподключение
                scheduleReconnect()
            }
        })
    }
    
    private fun handleMessage(jsonMessage: String) {
        try {
            val message = Json.decodeFromString<WebSocketMessage>(jsonMessage)
            
            when (message.type) {
                "connection_established" -> {
                    notifyConnectionEstablished(message.user_id)
                }
                "emergency_alert" -> {
                    val alertData = Json.decodeFromString<EmergencyAlertResponse>(
                        Json.encodeToString(message.data)
                    )
                    notifyNewAlert(alertData)
                }
                "alert_update" -> {
                    val updateData = Json.decodeFromString<AlertUpdateData>(
                        Json.encodeToString(message.data)
                    )
                    notifyAlertUpdate(updateData)
                }
                "alert_response" -> {
                    val responseData = Json.decodeFromString<EmergencyResponseResponse>(
                        Json.encodeToString(message.data)
                    )
                    notifyAlertResponse(responseData)
                }
            }
        } catch (e: Exception) {
            notifyError("Failed to parse WebSocket message: ${e.message}")
        }
    }
    
    fun sendMessage(message: Any) {
        if (isConnected) {
            val jsonMessage = Json.encodeToString(message)
            webSocket?.send(jsonMessage)
        }
    }
    
    fun addListener(listener: EmergencyWebSocketListener) {
        listeners.add(listener)
    }
    
    fun removeListener(listener: EmergencyWebSocketListener) {
        listeners.remove(listener)
    }
    
    private fun startPingKeepAlive() {
        coroutineScope.launch {
            while (isConnected) {
                delay(30_000) // Ping каждые 30 секунд
                sendMessage(mapOf("type" to "ping"))
            }
        }
    }
    
    private fun scheduleReconnect() {
        coroutineScope.launch {
            delay(5_000) // Ждем 5 секунд перед переподключением
            connect()
        }
    }
}

interface EmergencyWebSocketListener {
    fun onConnectionOpened()
    fun onConnectionEstablished(userId: Int)
    fun onNewAlert(alert: EmergencyAlertResponse)
    fun onAlertUpdate(update: AlertUpdateData)
    fun onAlertResponse(response: EmergencyResponseResponse)
    fun onConnectionClosing(code: Int, reason: String)
    fun onError(error: String)
}

@Serializable
data class WebSocketMessage(
    val type: String,
    val data: JsonElement? = null,
    val user_id: Int? = null,
    val message: String? = null
)

@Serializable
data class AlertUpdateData(
    val alert_id: Int,
    val status: String,
    val responded_users_count: Int
)

🎯 ЭТАП 4: СОЗДАНИЕ UI КОМПОНЕНТОВ

4.1 Emergency Fragment/Activity

class EmergencyFragment : Fragment(), EmergencyWebSocketListener {
    
    private lateinit var binding: FragmentEmergencyBinding
    private lateinit var emergencyRepository: EmergencyRepository
    private lateinit var webSocketManager: EmergencyWebSocketManager
    private lateinit var viewModel: EmergencyViewModel
    
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        binding = FragmentEmergencyBinding.inflate(inflater, container, false)
        return binding.root
    }
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        setupWebSocket()
        setupUI()
        observeViewModel()
    }
    
    private fun setupWebSocket() {
        webSocketManager.addListener(this)
        webSocketManager.connect()
    }
    
    private fun setupUI() {
        binding.btnCreateAlert.setOnClickListener {
            showCreateAlertDialog()
        }
        
        binding.btnSafetyCheck.setOnClickListener {
            createSafetyCheck()
        }
        
        binding.btnViewNearby.setOnClickListener {
            loadNearbyAlerts()
        }
        
        binding.swipeRefresh.setOnRefreshListener {
            refreshData()
        }
    }
    
    private fun showCreateAlertDialog() {
        val dialog = CreateAlertDialog { alertRequest ->
            viewModel.createAlert(alertRequest)
        }
        dialog.show(parentFragmentManager, "CREATE_ALERT")
    }
    
    private fun createSafetyCheck() {
        locationManager.getCurrentLocation { location ->
            val safetyCheck = SafetyCheckRequest(
                latitude = location.latitude,
                longitude = location.longitude,
                status = "safe",
                message = "Regular safety check"
            )
            viewModel.createSafetyCheck(safetyCheck)
        }
    }
    
    // WebSocket Listener методы
    override fun onConnectionEstablished(userId: Int) {
        activity?.runOnUiThread {
            binding.connectionStatus.text = "Connected (User: $userId)"
            binding.connectionStatus.setTextColor(ContextCompat.getColor(requireContext(), R.color.green))
        }
    }
    
    override fun onNewAlert(alert: EmergencyAlertResponse) {
        activity?.runOnUiThread {
            showNewAlertNotification(alert)
            viewModel.refreshAlerts()
        }
    }
    
    override fun onAlertUpdate(update: AlertUpdateData) {
        activity?.runOnUiThread {
            viewModel.updateAlert(update)
        }
    }
    
    override fun onError(error: String) {
        activity?.runOnUiThread {
            binding.connectionStatus.text = "Error: $error"
            binding.connectionStatus.setTextColor(ContextCompat.getColor(requireContext(), R.color.red))
            Toast.makeText(context, error, Toast.LENGTH_SHORT).show()
        }
    }
    
    override fun onDestroy() {
        webSocketManager.removeListener(this)
        super.onDestroy()
    }
}

4.2 Emergency ViewModel

class EmergencyViewModel(
    private val repository: EmergencyRepository
) : ViewModel() {
    
    private val _alerts = MutableLiveData<List<EmergencyAlertResponse>>()
    val alerts: LiveData<List<EmergencyAlertResponse>> = _alerts
    
    private val _nearbyAlerts = MutableLiveData<List<NearbyAlertResponse>>()
    val nearbyAlerts: LiveData<List<NearbyAlertResponse>> = _nearbyAlerts
    
    private val _isLoading = MutableLiveData<Boolean>()
    val isLoading: LiveData<Boolean> = _isLoading
    
    private val _error = MutableLiveData<String>()
    val error: LiveData<String> = _error
    
    fun createAlert(request: CreateAlertRequest) {
        viewModelScope.launch {
            _isLoading.value = true
            try {
                val result = repository.createAlert(request)
                if (result.isSuccess) {
                    // Обновляем список после создания
                    loadMyAlerts()
                } else {
                    _error.value = "Failed to create alert: ${result.exceptionOrNull()?.message}"
                }
            } catch (e: Exception) {
                _error.value = "Error creating alert: ${e.message}"
            } finally {
                _isLoading.value = false
            }
        }
    }
    
    fun loadMyAlerts() {
        viewModelScope.launch {
            _isLoading.value = true
            try {
                val result = repository.getMyAlerts()
                if (result.isSuccess) {
                    _alerts.value = result.getOrNull() ?: emptyList()
                } else {
                    _error.value = "Failed to load alerts: ${result.exceptionOrNull()?.message}"
                }
            } catch (e: Exception) {
                _error.value = "Error loading alerts: ${e.message}"
            } finally {
                _isLoading.value = false
            }
        }
    }
    
    fun loadNearbyAlerts(latitude: Double, longitude: Double, radius: Int = 5) {
        viewModelScope.launch {
            _isLoading.value = true
            try {
                val result = repository.getNearbyAlerts(latitude, longitude, radius)
                if (result.isSuccess) {
                    _nearbyAlerts.value = result.getOrNull() ?: emptyList()
                } else {
                    _error.value = "Failed to load nearby alerts: ${result.exceptionOrNull()?.message}"
                }
            } catch (e: Exception) {
                _error.value = "Error loading nearby alerts: ${e.message}"
            } finally {
                _isLoading.value = false
            }
        }
    }
    
    fun respondToAlert(alertId: Int, responseType: String, message: String? = null) {
        viewModelScope.launch {
            _isLoading.value = true
            try {
                val request = AlertResponseRequest(responseType, message)
                val result = repository.respondToAlert(alertId, request)
                if (result.isSuccess) {
                    // Обновляем список после ответа
                    loadMyAlerts()
                } else {
                    _error.value = "Failed to respond to alert: ${result.exceptionOrNull()?.message}"
                }
            } catch (e: Exception) {
                _error.value = "Error responding to alert: ${e.message}"
            } finally {
                _isLoading.value = false
            }
        }
    }
    
    fun createSafetyCheck(request: SafetyCheckRequest) {
        viewModelScope.launch {
            try {
                val result = repository.createSafetyCheck(request)
                if (result.isFailure) {
                    _error.value = "Failed to create safety check: ${result.exceptionOrNull()?.message}"
                }
            } catch (e: Exception) {
                _error.value = "Error creating safety check: ${e.message}"
            }
        }
    }
    
    fun updateAlert(update: AlertUpdateData) {
        val currentAlerts = _alerts.value?.toMutableList() ?: return
        val index = currentAlerts.indexOfFirst { it.id == update.alert_id }
        if (index != -1) {
            currentAlerts[index] = currentAlerts[index].copy(
                status = update.status,
                responded_users_count = update.responded_users_count
            )
            _alerts.value = currentAlerts
        }
    }
}

4.3 Emergency Repository

class EmergencyRepository(
    private val apiService: EmergencyApiService,
    private val authManager: AuthManager
) {
    
    private fun getAuthHeader(): String {
        val token = authManager.getValidJwtToken()
        return "Bearer $token"
    }
    
    suspend fun createAlert(request: CreateAlertRequest): Result<EmergencyAlertResponse> {
        return try {
            val response = apiService.createAlert(request, getAuthHeader())
            if (response.isSuccessful && response.body() != null) {
                Result.success(response.body()!!)
            } else {
                Result.failure(Exception("HTTP ${response.code()}: ${response.message()}"))
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
    
    suspend fun getMyAlerts(): Result<List<EmergencyAlertResponse>> {
        return try {
            val response = apiService.getMyAlerts(getAuthHeader())
            if (response.isSuccessful && response.body() != null) {
                Result.success(response.body()!!)
            } else {
                Result.failure(Exception("HTTP ${response.code()}: ${response.message()}"))
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
    
    suspend fun getActiveAlerts(): Result<List<EmergencyAlertResponse>> {
        return try {
            val response = apiService.getActiveAlerts(getAuthHeader())
            if (response.isSuccessful && response.body() != null) {
                Result.success(response.body()!!)
            } else {
                Result.failure(Exception("HTTP ${response.code()}: ${response.message()}"))
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
    
    suspend fun getNearbyAlerts(latitude: Double, longitude: Double, radius: Int): Result<List<NearbyAlertResponse>> {
        return try {
            val response = apiService.getNearbyAlerts(latitude, longitude, radius, getAuthHeader())
            if (response.isSuccessful && response.body() != null) {
                Result.success(response.body()!!)
            } else {
                Result.failure(Exception("HTTP ${response.code()}: ${response.message()}"))
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
    
    suspend fun respondToAlert(alertId: Int, request: AlertResponseRequest): Result<EmergencyResponseResponse> {
        return try {
            val response = apiService.respondToAlert(alertId, request, getAuthHeader())
            if (response.isSuccessful && response.body() != null) {
                Result.success(response.body()!!)
            } else {
                Result.failure(Exception("HTTP ${response.code()}: ${response.message()}"))
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
    
    suspend fun createReport(request: CreateReportRequest): Result<EmergencyReportResponse> {
        return try {
            val response = apiService.createReport(request, getAuthHeader())
            if (response.isSuccessful && response.body() != null) {
                Result.success(response.body()!!)
            } else {
                Result.failure(Exception("HTTP ${response.code()}: ${response.message()}"))
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
    
    suspend fun createSafetyCheck(request: SafetyCheckRequest): Result<SafetyCheckResponse> {
        return try {
            val response = apiService.createSafetyCheck(request, getAuthHeader())
            if (response.isSuccessful && response.body() != null) {
                Result.success(response.body()!!)
            } else {
                Result.failure(Exception("HTTP ${response.code()}: ${response.message()}"))
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
    
    suspend fun getStatistics(): Result<EmergencyStatistics> {
        return try {
            val response = apiService.getStatistics(getAuthHeader())
            if (response.isSuccessful && response.body() != null) {
                Result.success(response.body()!!)
            } else {
                Result.failure(Exception("HTTP ${response.code()}: ${response.message()}"))
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

🔧 ЭТАП 5: КОНФИГУРАЦИЯ И НАСТРОЙКА

5.1 Network Configuration

object NetworkConfig {
    const val BASE_URL = "http://YOUR_SERVER_IP:8000/"
    const val EMERGENCY_URL = "http://YOUR_SERVER_IP:8002/"
    const val WS_URL = "ws://YOUR_SERVER_IP:8002/"
    
    const val CONNECT_TIMEOUT = 30L
    const val READ_TIMEOUT = 30L
    const val WRITE_TIMEOUT = 30L
}

// Retrofit setup
val retrofit = Retrofit.Builder()
    .baseUrl(NetworkConfig.BASE_URL)
    .addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
    .client(
        OkHttpClient.Builder()
            .connectTimeout(NetworkConfig.CONNECT_TIMEOUT, TimeUnit.SECONDS)
            .readTimeout(NetworkConfig.READ_TIMEOUT, TimeUnit.SECONDS)
            .writeTimeout(NetworkConfig.WRITE_TIMEOUT, TimeUnit.SECONDS)
            .addInterceptor(AuthInterceptor(authManager))
            .build()
    )
    .build()

class AuthInterceptor(private val authManager: AuthManager) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val originalRequest = chain.request()
        
        // Добавляем JWT токен к запросам, если он есть
        val token = authManager.getValidJwtToken()
        return if (token != null && !originalRequest.header("Authorization")?.startsWith("Bearer") == true) {
            val newRequest = originalRequest.newBuilder()
                .header("Authorization", "Bearer $token")
                .build()
            chain.proceed(newRequest)
        } else {
            chain.proceed(originalRequest)
        }
    }
}

5.2 Dependency Injection (Hilt/Dagger)

@Module
@InstallIn(SingletonComponent::class)
object EmergencyModule {
    
    @Provides
    @Singleton
    fun provideAuthManager(@ApplicationContext context: Context): AuthManager {
        return AuthManager(context)
    }
    
    @Provides
    @Singleton
    fun provideEmergencyApiService(): EmergencyApiService {
        return retrofit.create(EmergencyApiService::class.java)
    }
    
    @Provides
    @Singleton
    fun provideEmergencyRepository(
        apiService: EmergencyApiService,
        authManager: AuthManager
    ): EmergencyRepository {
        return EmergencyRepository(apiService, authManager)
    }
    
    @Provides
    @Singleton
    fun provideEmergencyWebSocketManager(
        authManager: AuthManager,
        @ApplicationScope scope: CoroutineScope
    ): EmergencyWebSocketManager {
        return EmergencyWebSocketManager(authManager, scope)
    }
}

@ViewModelScope
class EmergencyViewModel @Inject constructor(
    private val repository: EmergencyRepository
) : ViewModel()

🧪 ЭТАП 6: ТЕСТИРОВАНИЕ И ОТЛАДКА

6.1 Unit Tests

class EmergencyRepositoryTest {
    
    @Mock
    private lateinit var apiService: EmergencyApiService
    
    @Mock
    private lateinit var authManager: AuthManager
    
    private lateinit var repository: EmergencyRepository
    
    @Before
    fun setup() {
        MockitoAnnotations.openMocks(this)
        repository = EmergencyRepository(apiService, authManager)
    }
    
    @Test
    fun `createAlert should return success when API call succeeds`() = runTest {
        // Arrange
        val request = CreateAlertRequest("medical", 55.7558, 37.6176, null, "Test alert")
        val response = EmergencyAlertResponse(1, "medical", 55.7558, 37.6176, null, "Test alert", "active", "2025-10-18T00:00:00Z", 0)
        
        `when`(authManager.getValidJwtToken()).thenReturn("valid_token")
        `when`(apiService.createAlert(any(), any())).thenReturn(Response.success(response))
        
        // Act
        val result = repository.createAlert(request)
        
        // Assert
        assertTrue(result.isSuccess)
        assertEquals(response, result.getOrNull())
    }
    
    @Test
    fun `createAlert should return failure when API call fails`() = runTest {
        // Arrange
        val request = CreateAlertRequest("medical", 55.7558, 37.6176, null, "Test alert")
        
        `when`(authManager.getValidJwtToken()).thenReturn("valid_token")
        `when`(apiService.createAlert(any(), any())).thenReturn(Response.error(500, "".toResponseBody()))
        
        // Act
        val result = repository.createAlert(request)
        
        // Assert
        assertTrue(result.isFailure)
    }
}

6.2 Integration Tests

class EmergencyIntegrationTest {
    
    private lateinit var webSocketManager: EmergencyWebSocketManager
    private lateinit var repository: EmergencyRepository
    
    @Test
    fun `should connect to WebSocket and receive messages`() = runTest {
        // Этот тест требует запущенного сервера
        val authManager = TestAuthManager() // Мок с валидным токеном
        webSocketManager = EmergencyWebSocketManager(authManager, this)
        
        var connectionEstablished = false
        var messageReceived = false
        
        webSocketManager.addListener(object : EmergencyWebSocketListener {
            override fun onConnectionEstablished(userId: Int) {
                connectionEstablished = true
            }
            
            override fun onNewAlert(alert: EmergencyAlertResponse) {
                messageReceived = true
            }
            
            // Другие методы...
        })
        
        webSocketManager.connect()
        
        // Ждем подключения
        delay(5000)
        
        assertTrue("WebSocket connection should be established", connectionEstablished)
    }
}

📱 ЭТАП 7: UI/UX УЛУЧШЕНИЯ

7.1 Push Notifications

class EmergencyNotificationService : FirebaseMessagingService() {
    
    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        super.onMessageReceived(remoteMessage)
        
        val data = remoteMessage.data
        when (data["type"]) {
            "emergency_alert" -> {
                showEmergencyAlertNotification(data)
            }
            "alert_response" -> {
                showAlertResponseNotification(data)
            }
            "safety_check_reminder" -> {
                showSafetyCheckReminder()
            }
        }
    }
    
    private fun showEmergencyAlertNotification(data: Map<String, String>) {
        val alertType = data["alert_type"] ?: "emergency"
        val location = data["address"] ?: "Unknown location"
        
        val notification = NotificationCompat.Builder(this, EMERGENCY_CHANNEL_ID)
            .setSmallIcon(R.drawable.ic_emergency)
            .setContentTitle("🚨 Emergency Alert: ${alertType.uppercase()}")
            .setContentText("Emergency situation reported near $location")
            .setPriority(NotificationCompat.PRIORITY_HIGH)
            .setCategory(NotificationCompat.CATEGORY_ALARM)
            .setAutoCancel(false)
            .addAction(R.drawable.ic_help, "Respond", createRespondPendingIntent(data["alert_id"]))
            .addAction(R.drawable.ic_view, "View Details", createViewPendingIntent(data["alert_id"]))
            .build()
            
        NotificationManagerCompat.from(this).notify(EMERGENCY_NOTIFICATION_ID, notification)
    }
}

7.2 Location Services Integration

class LocationManager @Inject constructor(
    @ApplicationContext private val context: Context
) {
    private val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
    
    @SuppressLint("MissingPermission")
    fun getCurrentLocation(callback: (Location) -> Unit) {
        if (hasLocationPermission()) {
            fusedLocationClient.lastLocation.addOnSuccessListener { location ->
                if (location != null) {
                    callback(location)
                } else {
                    requestNewLocationData(callback)
                }
            }
        } else {
            // Request location permissions
        }
    }
    
    @SuppressLint("MissingPermission")
    private fun requestNewLocationData(callback: (Location) -> Unit) {
        val locationRequest = LocationRequest.create().apply {
            priority = LocationRequest.PRIORITY_HIGH_ACCURACY
            interval = 0
            fastestInterval = 0
            numUpdates = 1
        }
        
        fusedLocationClient.requestLocationUpdates(
            locationRequest,
            object : LocationCallback() {
                override fun onLocationResult(locationResult: LocationResult) {
                    locationResult.lastLocation?.let { callback(it) }
                }
            },
            Looper.myLooper()
        )
    }
    
    private fun hasLocationPermission(): Boolean {
        return ContextCompat.checkSelfPermission(
            context,
            Manifest.permission.ACCESS_FINE_LOCATION
        ) == PackageManager.PERMISSION_GRANTED
    }
}

🔒 ЭТАП 8: БЕЗОПАСНОСТЬ И ОПТИМИЗАЦИЯ

8.1 Security Best Practices

object SecurityManager {
    
    // Шифрование токенов в SharedPreferences
    fun saveEncryptedToken(context: Context, token: String) {
        val sharedPrefs = EncryptedSharedPreferences.create(
            "secure_prefs",
            MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
            context,
            EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
            EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
        )
        
        sharedPrefs.edit()
            .putString("jwt_token", token)
            .apply()
    }
    
    // Certificate Pinning
    fun createSecureOkHttpClient(): OkHttpClient {
        val certificatePinner = CertificatePinner.Builder()
            .add("your-server.com", "sha256/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
            .build()
            
        return OkHttpClient.Builder()
            .certificatePinner(certificatePinner)
            .build()
    }
    
    // Защита от Man-in-the-Middle атак
    fun validateServerCertificate(hostname: String): Boolean {
        // Implement certificate validation logic
        return true
    }
}

8.2 Performance Optimization

class EmergencyDataCache @Inject constructor() {
    private val alertsCache = LruCache<String, List<EmergencyAlertResponse>>(50)
    private val cacheExpiry = mutableMapOf<String, Long>()
    private val cacheTimeout = 5 * 60 * 1000L // 5 minutes
    
    fun cacheAlerts(key: String, alerts: List<EmergencyAlertResponse>) {
        alertsCache.put(key, alerts)
        cacheExpiry[key] = System.currentTimeMillis() + cacheTimeout
    }
    
    fun getCachedAlerts(key: String): List<EmergencyAlertResponse>? {
        val expiry = cacheExpiry[key] ?: return null
        return if (System.currentTimeMillis() < expiry) {
            alertsCache.get(key)
        } else {
            alertsCache.remove(key)
            cacheExpiry.remove(key)
            null
        }
    }
}

📋 ЭТАП 9: ЧЕКЛИСТ ГОТОВНОСТИ

Обязательные компоненты:

  • Удалены все temp_token_ токены
  • Реализована JWT аутентификация
  • WebSocket подключение с правильным URL
  • REST API интеграция для всех endpoint'ов
  • Обработка ошибок сети и аутентификации
  • Push уведомления для экстренных сообщений
  • Location services для определения координат
  • UI для создания и просмотра экстренных вызовов

Тестирование:

  • Unit тесты для Repository и ViewModel
  • Integration тесты WebSocket подключения
  • UI тесты для критических сценариев
  • Тесты на различных сетевых условиях
  • Тесты безопасности токенов

Безопасность:

  • Шифрование токенов в хранилище
  • Certificate pinning
  • Проверка SSL сертификатов
  • Обфускация кода
  • Защита от reverse engineering

Производительность:

  • Кеширование данных
  • Оптимизация изображений
  • Минификация сетевых запросов
  • Фоновая синхронизация
  • Battery optimization

🚀 ЭТАП 10: РАЗВЕРТЫВАНИЕ

10.1 Build Configuration

android {
    buildTypes {
        debug {
            buildConfigField "String", "BASE_URL", "\"http://192.168.1.100:8000/\""
            buildConfigField "String", "WS_URL", "\"ws://192.168.1.100:8002/\""
        }
        release {
            buildConfigField "String", "BASE_URL", "\"https://your-production-server.com/\""
            buildConfigField "String", "WS_URL", "\"wss://your-production-server.com/\""
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

10.2 Monitoring and Analytics

class EmergencyAnalytics @Inject constructor() {
    
    fun trackEmergencyAlertCreated(alertType: String) {
        FirebaseAnalytics.getInstance(context).logEvent("emergency_alert_created") {
            param("alert_type", alertType)
            param("timestamp", System.currentTimeMillis())
        }
    }
    
    fun trackWebSocketConnection(success: Boolean, errorMessage: String? = null) {
        FirebaseAnalytics.getInstance(context).logEvent("websocket_connection") {
            param("success", success)
            errorMessage?.let { param("error", it) }
        }
    }
    
    fun trackResponseTime(endpoint: String, responseTime: Long) {
        FirebaseAnalytics.getInstance(context).logEvent("api_response_time") {
            param("endpoint", endpoint)
            param("response_time_ms", responseTime)
        }
    }
}

📞 КОНТАКТЫ И ПОДДЕРЖКА

Сервер endpoints для тестирования:

  • API Gateway: http://YOUR_SERVER:8000
  • Emergency Service: http://YOUR_SERVER:8002
  • WebSocket: ws://YOUR_SERVER:8002/api/v1/emergency/ws/current_user_id?token=JWT_TOKEN

Тестовые данные:

  • Email: shadow85@list.ru
  • Password: R0sebud1985

Документация API: /home/data/chat/docs/WEBSOCKET_AUTH_EXPLANATION.md


Этот промпт содержит полное пошаговое техническое задание для интеграции мобильного приложения с Emergency Service. Следуйте этапам последовательно для успешной реализации! 🚀