All checks were successful
continuous-integration/drone/push Build is passing
38 KiB
38 KiB
🚨 ПОЛНОЕ ТЕХНИЧЕСКОЕ ЗАДАНИЕ: Доработка модуля экстренных сообщений для мобильного приложения
📋 ОБЗОР ПРОЕКТА
Цель: Интегрировать мобильное приложение с 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. Следуйте этапам последовательно для успешной реализации! 🚀