Инструкция по использованию PushService для Android#

Общая информация#

PushService — это мощная Android-библиотека для управления push-уведомлениями с расширенными возможностями персонализации, локального хранения и аналитики. Библиотека предоставляет единый фасад для работы с FCM, Huawei Push-Kit, Rustore и обладает богатым API для интеграции с вашим приложением.

Основные возможности библиотеки:

  • регистрация в FCM и управление токенами;

  • персонализация с привязкой пользователей по телефону/ID;

  • локальное хранение истории уведомлений;

  • кастомизация внешнего вида уведомлений;

  • аналитика и отслеживание действий;

  • гибкие настройки каналов и важности уведомлений.

Быстрый старт#

Инициализация SDK

class MainActivity : AppCompatActivity() {

    private lateinit var notificationSdk: NotificationSdk

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Инициализируем SDK
        notificationSdk = NotificationSdk.getInstance(this)

        // Настраиваем и регистрируем push-токен
        setupPushNotifications()
    }

    private fun setupPushNotifications() {
        notificationSdk
            .setLogsEnabled(BuildConfig.DEBUG) // Логи только в debug-сборке
            .setLocalDatabaseEnabled(true) // Включаем локальное хранение
            .setNotificationIcon(
                R.drawable.ic_notification, // Иконка уведомления
                R.color.notification_accent  // Цвет иконки
            )
            .setNotificationImportance(NotificationImportance.High) // Высокая важность
            .setNotificationChannelName("Важные уведомления") // Имя канала
            .registerPushToken(
                onSuccess = { token ->
                    Log.d("Push", "Push Token: $token")
                    // Сохраните токен или отправьте на ваш сервер
                },
                onError = { error ->
                    Log.e("Push", "Ошибка регистрации: ${error.message}")
                }
            )
    }
}

Управление пользователями#

Привязка пользователя (персонализация)

class UserManager {

    fun loginUser(
        sdk: NotificationSdk,
        userId: String,
        phoneNumber: String
    ) {
        // Номер телефона должен начинаться с "7" и содержать 11 цифр
        val formattedPhone = formatPhoneNumber(phoneNumber)

        sdk.personalize(
            user = userId,
            phone = formattedPhone,
            onSuccess = { userProfile ->
                Log.d("User", "Пользователь авторизован: ${userProfile.externalUserId}")
                // Обновите UI или сохраните данные профиля
                updateUI(userProfile)
            },
            onError = { error ->
                Log.e("User", "Ошибка авторизации: ${error.message}")
                showErrorToUser(error.message)
            }
        )
    }

    private fun formatPhoneNumber(phone: String): String {
        // Приводим номер к формату, начинающемуся с "7"
        val cleanPhone = phone.replace(Regex("[^\\d]"), "")
        return when {
            cleanPhone.startsWith("8") && cleanPhone.length == 11 ->
                "7${cleanPhone.substring(1)}"
            cleanPhone.startsWith("7") && cleanPhone.length == 11 ->
                cleanPhone
            else -> throw IllegalArgumentException("Некорректный номер телефона")
        }
    }

    fun logoutUser(sdk: NotificationSdk) {
        sdk.depersonalize(
            onSuccess = {
                Log.d("User", "Пользователь разлогинен")
                // Очистите локальные данные и UI
                clearUserData()
            },
            onError = { error ->
                Log.e("User", "Ошибка при выходе: ${error.message}")
            }
        )
    }

    fun getCurrentUserProfile(sdk: NotificationSdk) {
        sdk.getUserProfile(
            onSuccess = { profile ->
                Log.d("User", "Профиль: ID=${profile.id}, Phone=${profile.phone}")
                displayUserProfile(profile)
            },
            onError = { error ->
                Log.e("User", "Ошибка получения профиля: ${error.message}")
            }
        )
    }
}

Управление настройками установки

class InstallationManager {

    fun updatePushSettings(
        sdk: NotificationSdk,
        pushEnabled: Boolean,
        isPrimaryDevice: Boolean
    ) {
        // Получаем текущие настройки
        sdk.getInstallationInfo(
            onSuccess = { installationInfo ->
                // Обновляем настройки
                val updatedSettings = installationInfo.settings.map { setting ->
                    // Можно изменить значения настроек по необходимости
                    setting.copy(value = if (setting.name == "notifications") "enabled" else setting.value)
                }

                // Сохраняем изменения
                sdk.updateInstallationInfo(
                    pushEnabled = pushEnabled,
                    settings = updatedSettings,
                    primary = isPrimaryDevice,
                    onSuccess = {
                        Log.d("Settings", "Настройки обновлены")
                    },
                    onError = { error ->
                        Log.e("Settings", "Ошибка обновления: ${error.message}")
                    }
                )
            },
            onError = { error ->
                Log.e("Settings", "Ошибка получения настроек: ${error.message}")
            }
        )
    }
}

Работа с уведомлениями#

Получение истории уведомлений

class NotificationHistory {

    fun loadRecentNotifications(sdk: NotificationSdk) {
        sdk.getNotificationsFromHistory(
            limit = 50,
            startFromId = 0L,
            dateFrom = null,
            dateTo = null,
            sortAsc = false, // Новые сначала
            onSuccess = { notifications ->
                Log.d("History", "Загружено ${notifications.size} уведомлений")
                displayNotifications(notifications)
            },
            onError = { error ->
                Log.e("History", "Ошибка загрузки: ${error.message}")
            }
        )
    }

    fun loadNotificationsByDateRange(
        sdk: NotificationSdk,
        fromDate: Date,
        toDate: Date
    ) {
        sdk.getNotificationsFromHistory(
            limit = 100,
            startFromId = 0L,
            dateFrom = fromDate,
            dateTo = toDate,
            sortAsc = true, // Старые сначала
            onSuccess = { notifications ->
                processNotificationsByDate(notifications)
            },
            onError = { error ->
                handleError(error)
            }
        )
    }

    fun markNotificationAsRead(
        sdk: NotificationSdk,
        notification: NotificationDto
    ) {
        // Помечаем уведомление как прочитанное
        notification.setOpened()

        sdk.updateNotificationFromHistory(
            notificationDto = notification,
            onSuccess = {
                Log.d("History", "Уведомление отмечено как прочитанное")
            },
            onError = { error ->
                Log.e("History", "Ошибка обновления статуса: ${error.message}")
            }
        )
    }

    fun deleteNotification(sdk: NotificationSdk, notificationId: Long) {
        sdk.deleteNotificationFromHistory(
            notificationId = notificationId,
            onSuccess = {
                Log.d("History", "Уведомление удалено")
                refreshNotificationsList()
            },
            onError = { error ->
                Log.e("History", "Ошибка удаления: ${error.message}")
            }
        )
    }

    fun clearAllNotifications(sdk: NotificationSdk) {
        sdk.deleteNotificationsFromHistory(
            onSuccess = {
                Log.d("History", "Вся история очищена")
                updateEmptyState()
            },
            onError = { error ->
                Log.e("History", "Ошибка очистки: ${error.message}")
            }
        )
    }
}

Кастомизация уведомлений#

Создание кастомного NotificationCustomizer

import im.zgr.pushservice.notification.NotificationCustomizer
import androidx.core.app.NotificationCompat
import android.content.Context
import android.graphics.Bitmap

class MyNotificationCustomizer : NotificationCustomizer {

    override fun customize(
        builder: NotificationCompat.Builder,
        context: Context,
        title: String,
        text: String,
        bitmap: Bitmap?,
        customPayload: String?
    ) {
        // Кастомизируем стиль уведомления
        builder.apply {
            // Устанавливаем расширенный стиль текста
            setStyle(
                NotificationCompat.BigTextStyle()
                    .bigText(text)
                    .setBigContentTitle(title)
            )

            // Добавляем изображение, если есть
            bitmap?.let {
                setStyle(
                    NotificationCompat.BigPictureStyle()
                        .bigPicture(it)
                        .setBigContentTitle(title)
                        .setSummaryText(text)
                )
            }

            // Обрабатываем кастомные данные
            customPayload?.let { payload ->
                try {
                    val jsonData = JSONObject(payload)

                    // Добавляем действия на основе payload
                    if (jsonData.has("actions")) {
                        addCustomActions(this, jsonData.getJSONArray("actions"), context)
                    }

                    // Устанавливаем приоритет на основе данных
                    if (jsonData.has("priority")) {
                        setPriority(jsonData.getInt("priority"))
                    }

                } catch (e: Exception) {
                    Log.e("Customizer", "Ошибка парсинга payload: ${e.message}")
                }
            }

            // Добавляем системные действия
            addAction(
                R.drawable.ic_mark_read,
                "Прочитано",
                createReadActionPendingIntent(context)
            )

            addAction(
                R.drawable.ic_delete,
                "Удалить",
                createDeleteActionPendingIntent(context)
            )
        }
    }

    private fun addCustomActions(
        builder: NotificationCompat.Builder,
        actions: JSONArray,
        context: Context
    ) {
        for (i in 0 until actions.length()) {
            val action = actions.getJSONObject(i)
            val title = action.getString("title")
            val actionId = action.getString("id")

            builder.addAction(
                R.drawable.ic_action,
                title,
                createCustomActionPendingIntent(context, actionId)
            )
        }
    }
}

Подключение кастомизатора

class NotificationSetup {

    fun setupCustomNotifications(sdk: NotificationSdk) {
        sdk.setNotificationCustomizer(MyNotificationCustomizer::class.java)
            .setNotificationIcon(R.drawable.ic_notification, R.color.primary)
            .setNotificationChannelName("Персонализированные уведомления")
            .setNotificationImportance(NotificationImportance.High)
    }
}

Подписка на события#

Отслеживание получения уведомлений

class NotificationListener : AppCompatActivity() {

    private lateinit var sdk: NotificationSdk

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        sdk = NotificationSdk.getInstance(this)
        subscribeToNotificationEvents()
    }

    private fun subscribeToNotificationEvents() {
        // Подписываемся на получение новых сообщений
        sdk.subscribeOnAppMessage { messageId ->
            Log.d("Notification", "Получено новое сообщение: $messageId")

            // Загружаем полные данные сообщения
            sdk.fetch(
                id = messageId,
                onSuccess = { messageResponse ->
                    messageResponse?.let {
                        handleNewMessage(it)
                    } ?: Log.w("Notification", "Сообщение не найдено или удалено")
                },
                onError = { error ->
                    Log.e("Notification", "Ошибка загрузки сообщения: ${error.message}")
                }
            )
        }

        // Подписываемся на действия с уведомлениями
        sdk.subscribeOnAppMessageAction { actionData ->
            Log.d("Action", "Действие с уведомлением: ${actionData.notificationEvent}")

            when (actionData.notificationEvent) {
                NotificationEvent.OPENED -> {
                    handleNotificationOpened(actionData.notificationDto)
                }
                NotificationEvent.DISMISSED -> {
                    handleNotificationDismissed(actionData.notificationDto)
                }
                NotificationEvent.CUSTOM_ACTION -> {
                    actionData.notificationCustomActionDto?.let { customAction ->
                        handleCustomAction(actionData.notificationDto, customAction)
                    }
                }
            }
        }
    }

    private fun handleNewMessage(message: MessageResponse) {
        // Обновляем счетчик непрочитанных
        updateUnreadCounter()

        // Показываем badge или обновляем UI
        updateNotificationBadge()

        // Отправляем аналитику
        trackNotificationReceived(message)
    }

    private fun handleNotificationOpened(notification: NotificationDto) {
        // Открываем соответствующий экран
        when (notification.contentCategory) {
            NotificationDto.HTML -> openWebView(notification.contentUrl)
            NotificationDto.IMAGE -> openImageViewer(notification.contentUrl)
            NotificationDto.VIDEO -> openVideoPlayer(notification.contentUrl)
            NotificationDto.YOUTUBE -> openYouTubeVideo(notification.contentUrl)
            else -> openDefaultScreen(notification)
        }

        // Помечаем как прочитанное
        notification.setOpened()
        sdk.updateNotificationFromHistory(notification)
    }

    override fun onDestroy() {
        super.onDestroy()
        // Отписываемся от событий
        sdk.subscribeOnAppMessage(null)
        sdk.subscribeOnAppMessageAction(null)
    }
}

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

Отправка тестового уведомления

class TestingUtils {

    fun sendTestNotification(sdk: NotificationSdk) {
        sdk.requestNotification(
            onSuccess = {
                Log.d("Test", "Тестовое уведомление отправлено")
                showToast("Проверьте панель уведомлений")
            },
            onError = { error ->
                Log.e("Test", "Ошибка отправки тестового уведомления: ${error.message}")
                showErrorDialog(error.message)
            }
        )
    }
}

Модели данных#

NotificationDto

// Основная структура уведомления
data class NotificationDto(
    var id: Long?,                    // ID в локальной БД
    var pushServiceId: Int,           // ID в push-сервисе
    var text: String?,                // Текст уведомления
    var title: String?,               // Заголовок
    var contentUrl: String?,          // URL контента
    var contentCategory: String?,     // Категория: "html", "image", "video", "youtube"
    var customActionList: List<NotificationCustomActionDto>?, // Кастомные действия
    var date: Date,                   // Дата получения
    var customPayload: String?,       // JSON с дополнительными данными
    var status: String                // Статус: "Delivered", "Opened", "Swiped"
)

UserProfileDto

// Профиль пользователя
data class UserProfileDto(
    val id: Long,                     // Внутренний ID
    val phone: String?,               // Номер телефона
    val externalUserId: String?       // Внешний ID пользователя
)

InstallationInfoDto

// Информация об установке
data class InstallationInfoDto(
    val id: String,                           // ID установки
    var pushOsEnabled: Boolean?,              // Push включены в ОС
    var pushEnabled: Boolean,                 // Push включены в приложении
    val settings: List<InstallationInfoSettingDto>, // Настройки
    var primary: Boolean                      // Основное устройство
)

Цепочка вызовов

Все методы возвращают экземпляр NotificationSdk для удобного построения цепочек:

NotificationSdk.getInstance(context)
    .setLogsEnabled(true)
    .setLocalDatabaseEnabled(true)
    .setNotificationIcon(R.drawable.icon, R.color.accent)
    .registerPushToken({ token -> }, { error -> })

Лучшие практики#

1. Инициализация в Application

class MyApplication : Application() {

    companion object {
        lateinit var notificationSdk: NotificationSdk
            private set
    }

    override fun onCreate() {
        super.onCreate()

        notificationSdk = NotificationSdk.getInstance(this)
            .setLogsEnabled(BuildConfig.DEBUG)
            .setLocalDatabaseEnabled(true)
    }
}

2. Использование в MVVM архитектуре

class NotificationRepository @Inject constructor(
    private val sdk: NotificationSdk
) {

    fun getNotifications(limit: Int = 50): Flow<List<NotificationDto>> = flow {
        suspendCoroutine<List<NotificationDto>> { continuation ->
            sdk.getNotificationsFromHistory(
                limit = limit,
                startFromId = 0L,
                dateFrom = null,
                dateTo = null,
                onSuccess = { notifications ->
                    continuation.resume(notifications)
                },
                onError = { error ->
                    continuation.resumeWithException(error)
                }
            )
        }.let { emit(it) }
    }

    suspend fun markAsRead(notification: NotificationDto) {
        suspendCoroutine<Unit> { continuation ->
            notification.setOpened()
            sdk.updateNotificationFromHistory(
                notification,
                onSuccess = { continuation.resume(Unit) },
                onError = { continuation.resumeWithException(it) }
            )
        }
    }
}

3. Обработка ошибок

Всегда предусматривайте обработку ошибок:

sdk.personalize(userId, phone,
    onSuccess = { profile ->
        // Успешная обработка
    },
    onError = { throwable ->
        when (throwable) {
            is NetworkException -> showNetworkError()
            is ValidationException -> showValidationError(throwable.message)
            else -> showGenericError()
        }
    }
)