All checks were successful
continuous-integration/drone/push Build is passing
1184 lines
38 KiB
Markdown
1184 lines
38 KiB
Markdown
# 🚨 ПОЛНОЕ ТЕХНИЧЕСКОЕ ЗАДАНИЕ: Доработка модуля экстренных сообщений для мобильного приложения
|
||
|
||
## 📋 ОБЗОР ПРОЕКТА
|
||
|
||
**Цель:** Интегрировать мобильное приложение с Emergency Service через WebSocket и REST API
|
||
**Архитектура:** Микросервисная система с JWT аутентификацией
|
||
**Основные компоненты:** Emergency Service (порт 8002), API Gateway (порт 8000), WebSocket подключения
|
||
|
||
---
|
||
|
||
## 🔧 ЭТАП 1: НАСТРОЙКА АУТЕНТИФИКАЦИИ
|
||
|
||
### 1.1 Удаление временных токенов
|
||
**Проблема:** В коде используются временные токены вида `temp_token_for_${email}`
|
||
|
||
**Задачи:**
|
||
```kotlin
|
||
// ❌ УДАЛИТЬ ЭТО:
|
||
val tempToken = "temp_token_for_${userEmail}"
|
||
headers["Authorization"] = "Bearer $tempToken"
|
||
|
||
// ✅ ЗАМЕНИТЬ НА:
|
||
val jwtToken = authManager.getValidJwtToken()
|
||
headers["Authorization"] = "Bearer $jwtToken"
|
||
```
|
||
|
||
### 1.2 Реализация JWT аутентификации
|
||
**Создать класс AuthManager:**
|
||
|
||
```kotlin
|
||
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 Модели данных для аутентификации
|
||
```kotlin
|
||
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
|
||
```kotlin
|
||
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
|
||
```kotlin
|
||
// Запросы
|
||
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
|
||
```kotlin
|
||
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
|
||
```kotlin
|
||
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
|
||
```kotlin
|
||
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
|
||
```kotlin
|
||
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
|
||
```kotlin
|
||
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)
|
||
```kotlin
|
||
@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
|
||
```kotlin
|
||
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
|
||
```kotlin
|
||
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
|
||
```kotlin
|
||
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
|
||
```kotlin
|
||
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
|
||
```kotlin
|
||
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
|
||
```kotlin
|
||
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
|
||
```gradle
|
||
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
|
||
```kotlin
|
||
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. Следуйте этапам последовательно для успешной реализации! 🚀 |