Integrating ATP into a Modern Android App
This guide demonstrates how to integrate an existingATPClient
into a modern Android application using dependency injection (Hilt) and following architectural best practices.
Dependency Injection Setup
First, set up the ATP module in your Hilt configuration:Copy
@Module
@InstallIn(SingletonComponent::class)
object ATPModule {
@Provides
@Singleton
fun provideATPClient(
@ApplicationContext context: Context,
authManager: AuthManager
): ATPClient {
return ATPClient(
baseUrl = BuildConfig.ATP_BASE_URL,
token = authManager.getUserToken(),
context = context
)
}
@Provides
@Singleton
fun provideNotificationManager(
atpClient: ATPClient,
@ApplicationContext context: Context
): ATPNotificationManager {
return ATPNotificationManager(atpClient, context)
}
@Provides
@Singleton
fun provideResponseHandler(
atpClient: ATPClient,
workManager: WorkManager
): ATPResponseHandler {
return ATPResponseHandler(atpClient, workManager)
}
}
Notification Manager
Create a manager class to handle the notification lifecycle:Copy
@Singleton
class ATPNotificationManager @Inject constructor(
private val atpClient: ATPClient,
private val context: Context
) {
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE)
as NotificationManager
init {
createNotificationChannel()
// Collect notifications from ATP client
CoroutineScope(Dispatchers.Default).launch {
atpClient.notifications.collect { notification ->
showNotification(notification)
}
}
// Collect status updates
CoroutineScope(Dispatchers.Default).launch {
atpClient.statusUpdates.collect { update ->
if (update.status == "invalidated" || update.status == "responded") {
cancelNotification(update.notification_id)
}
}
}
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
"ATP Notifications",
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "Notifications requiring human decision"
}
notificationManager.createNotificationChannel(channel)
}
}
private fun showNotification(notification: Notification) {
// Create intent for notification tap action
val tapIntent = Intent(context, NotificationActivity::class.java).apply {
putExtra(EXTRA_NOTIFICATION_ID, notification.id)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val tapPendingIntent = PendingIntent.getActivity(
context,
notification.id.hashCode(),
tapIntent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
// Build notification
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(notification.context.title)
.setContentText(notification.context.description)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(tapPendingIntent)
.setAutoCancel(true)
// Add action buttons for simple responses
notification.actions.forEach { action ->
if (action.response_type == "simple") {
val actionIntent = Intent(context, NotificationActionReceiver::class.java).apply {
this.action = ACTION_RESPOND
putExtra(EXTRA_NOTIFICATION_ID, notification.id)
putExtra(EXTRA_ACTION_ID, action.id)
}
val actionPendingIntent = PendingIntent.getBroadcast(
context,
action.id.hashCode(),
actionIntent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
builder.addAction(0, action.label, actionPendingIntent)
}
}
// Show notification
notificationManager.notify(notification.id.hashCode(), builder.build())
}
fun cancelNotification(notificationId: String) {
notificationManager.cancel(notificationId.hashCode())
}
companion object {
const val CHANNEL_ID = "atp_notifications"
const val ACTION_RESPOND = "com.example.app.ACTION_RESPOND"
const val EXTRA_NOTIFICATION_ID = "notification_id"
const val EXTRA_ACTION_ID = "action_id"
}
}
Response Handler
Create a handler for responses with retry support:Copy
@Singleton
class ATPResponseHandler @Inject constructor(
private val atpClient: ATPClient,
private val workManager: WorkManager
) {
/**
* Submit a response to a notification
*/
suspend fun respondToNotification(
notificationId: String,
actionId: String,
responseData: Any?
): Result<ResponseResult> {
return try {
val result = atpClient.respond(notificationId, actionId, responseData)
Result.success(result)
} catch (e: Exception) {
if (isTerminalError(e)) {
// Don't retry terminal errors
Result.failure(e)
} else {
// Queue for retry
enqueueResponseRetry(notificationId, actionId, responseData)
Result.failure(e)
}
}
}
/**
* Queue a response for retry using WorkManager
*/
private fun enqueueResponseRetry(
notificationId: String,
actionId: String,
responseData: Any?
) {
val data = workDataOf(
KEY_NOTIFICATION_ID to notificationId,
KEY_ACTION_ID to actionId,
KEY_RESPONSE_DATA to responseData?.toString()
)
val responseWork = OneTimeWorkRequestBuilder<ResponseWorker>()
.setInputData(data)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
10, TimeUnit.SECONDS
)
.build()
workManager.enqueueUniqueWork(
"response_$notificationId",
ExistingWorkPolicy.REPLACE,
responseWork
)
}
private fun isTerminalError(error: Exception): Boolean {
val message = error.message?.lowercase() ?: ""
return message.contains("already_responded") ||
message.contains("expired") ||
message.contains("invalidated") ||
message.contains("invalid_response_data")
}
companion object {
const val KEY_NOTIFICATION_ID = "notification_id"
const val KEY_ACTION_ID = "action_id"
const val KEY_RESPONSE_DATA = "response_data"
}
}
Response Worker
Create a WorkManager worker for handling response retries:Copy
class ResponseWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
@Inject
lateinit var atpClient: ATPClient
override suspend fun doWork(): Result {
val notificationId = inputData.getString(ATPResponseHandler.KEY_NOTIFICATION_ID) ?: return Result.failure()
val actionId = inputData.getString(ATPResponseHandler.KEY_ACTION_ID) ?: return Result.failure()
val responseDataString = inputData.getString(ATPResponseHandler.KEY_RESPONSE_DATA)
// Parse response data based on type
val responseData = parseResponseData(responseDataString)
return try {
atpClient.respond(notificationId, actionId, responseData)
Result.success()
} catch (e: Exception) {
// Check if we should retry
val isTerminal = e.message?.lowercase()?.let { message ->
message.contains("already_responded") ||
message.contains("expired") ||
message.contains("invalidated") ||
message.contains("invalid_response_data")
} ?: false
if (isTerminal) {
Result.failure()
} else if (runAttemptCount < 3) {
Result.retry()
} else {
Result.failure()
}
}
}
private fun parseResponseData(responseDataString: String?): Any? {
return when {
responseDataString == null -> null
responseDataString == "true" -> true
responseDataString == "false" -> false
responseDataString.toDoubleOrNull() != null -> responseDataString.toDouble()
else -> responseDataString
}
}
}
BroadcastReceiver for Notification Actions
Create a receiver for handling direct notification responses:Copy
class NotificationActionReceiver : BroadcastReceiver() {
@Inject
lateinit var responseHandler: ATPResponseHandler
@Inject
lateinit var notificationManager: ATPNotificationManager
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == ATPNotificationManager.ACTION_RESPOND) {
val notificationId = intent.getStringExtra(ATPNotificationManager.EXTRA_NOTIFICATION_ID) ?: return
val actionId = intent.getStringExtra(ATPNotificationManager.EXTRA_ACTION_ID) ?: return
// Respond in the background
CoroutineScope(Dispatchers.IO).launch {
try {
responseHandler.respondToNotification(notificationId, actionId, null)
// Cancel the notification
withContext(Dispatchers.Main) {
notificationManager.cancelNotification(notificationId)
}
} catch (e: Exception) {
Log.e("ATP", "Failed to respond: ${e.message}")
}
}
}
}
}
ViewModel for Notification Handling
Create a ViewModel to interact with notifications in your UI:Copy
@HiltViewModel
class NotificationViewModel @Inject constructor(
private val atpClient: ATPClient,
private val responseHandler: ATPResponseHandler
) : ViewModel() {
// Notification storage
private val _notifications = MutableStateFlow<List<Notification>>(emptyList())
val notifications: StateFlow<List<Notification>> = _notifications.asStateFlow()
// Loading state
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
// Response status
private val _responseStatus = MutableStateFlow<ResponseStatus?>(null)
val responseStatus: StateFlow<ResponseStatus?> = _responseStatus.asStateFlow()
init {
// Collect notifications from ATP client
viewModelScope.launch {
atpClient.notifications.collect { notification ->
_notifications.update { currentList ->
currentList + notification
}
}
}
// Collect status updates
viewModelScope.launch {
atpClient.statusUpdates.collect { update ->
if (update.status == "invalidated" || update.status == "responded") {
_notifications.update { currentList ->
currentList.filter { it.id != update.notification_id }
}
}
}
}
// Load initial notifications
loadNotifications()
}
/**
* Load existing notifications
*/
fun loadNotifications() {
viewModelScope.launch {
_isLoading.value = true
try {
val response = atpClient.fetchPendingNotifications()
_notifications.value = response.notifications
} catch (e: Exception) {
Log.e("ATP", "Failed to load notifications: ${e.message}")
} finally {
_isLoading.value = false
}
}
}
/**
* Respond to a notification
*/
fun respondToNotification(
notificationId: String,
actionId: String,
responseData: Any?
) {
viewModelScope.launch {
_isLoading.value = true
try {
val result = responseHandler.respondToNotification(
notificationId,
actionId,
responseData
)
result.onSuccess {
_responseStatus.value = ResponseStatus.Success
// Remove notification from the list
_notifications.update { currentList ->
currentList.filter { it.id != notificationId }
}
}.onFailure { error ->
_responseStatus.value = ResponseStatus.Error(error.message ?: "Unknown error")
}
} finally {
_isLoading.value = false
}
}
}
/**
* Clear response status
*/
fun clearResponseStatus() {
_responseStatus.value = null
}
/**
* Response status sealed class
*/
sealed class ResponseStatus {
object Success : ResponseStatus()
data class Error(val message: String) : ResponseStatus()
}
}
Application Setup
In your Application class, initialize ATP:Copy
@HiltAndroidApp
class MyApplication : Application() {
@Inject
lateinit var atpClient: ATPClient
override fun onCreate() {
super.onCreate()
// Start watching for notifications
atpClient.startWatching()
}
}
Ensuring Proper Background Processing
Configure a reliable fallback for environments where WebSockets might not be reliable:Copy
@HiltWorker
class NotificationPollingWorker @AssistedInject constructor(
@Assisted context: Context,
@Assisted params: WorkerParameters,
private val atpClient: ATPClient,
private val notificationManager: ATPNotificationManager
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
try {
// Fetch pending notifications
val response = atpClient.fetchPendingNotifications()
// Process each notification
response.notifications.forEach { notification ->
// Show system notification (handled by notificationManager through the flow)
}
return Result.success()
} catch (e: Exception) {
return if (runAttemptCount < 3) Result.retry() else Result.failure()
}
}
companion object {
/**
* Schedule periodic polling
*/
fun schedule(workManager: WorkManager) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val pollingWork = PeriodicWorkRequestBuilder<NotificationPollingWorker>(
15, TimeUnit.MINUTES,
5, TimeUnit.MINUTES
)
.setConstraints(constraints)
.build()
workManager.enqueueUniquePeriodicWork(
"atp_notification_polling",
ExistingWorkPolicy.REPLACE,
pollingWork
)
}
}
}
Copy
@HiltAndroidApp
class MyApplication : Application() {
@Inject
lateinit var atpClient: ATPClient
@Inject
lateinit var workManager: WorkManager
override fun onCreate() {
super.onCreate()
// Start watching for notifications
atpClient.startWatching()
// Schedule fallback polling
NotificationPollingWorker.schedule(workManager)
}
}
Conclusion
This implementation demonstrates how to integrate an existingATPClient
into a modern Android application using:
- Hilt for dependency injection
- WorkManager for background processing and retry logic
- ViewModel for connecting ATP with the UI layer
- Coroutines and Flow for reactive data handling
- BroadcastReceiver for notification actions
- Separation of concerns - Each component has a clear responsibility
- Single source of truth - ViewModel manages notification state
- Reactive approach - Changes to notifications are observed via Flow
- Robust error handling - Includes retry logic for transient failures
- Dependency injection - Components are provided via Hilt
- Background processing - Uses WorkManager for reliable background tasks