Внедряем кросс-платформенные пуш-уведомления: начало

  • Tutorial

Добрый день! Меня зовут Владимир Столяров, я бэкенд-разработчик в команде Клиентские коммуникации в ДомКлике. В этой статье я расскажу о том, как внедрить кросс-платформенные пуш-уведомления. Хотя про это уже написано немало, я бы хотел рассказать о некоторых нюансах, с которыми нам пришлось столкнуться в процессе внедрения. Для лучшего понимания происходящего также напишем с вами небольшое веб-приложение, способное принимать пуши.



Для начала нужно понять, куда мы вообще хотим отправлять пуши. В нашем случае это веб-сайт, iOS-приложение и Android-приложение.


Начнем с веб-пушей. Для их получения браузер подключается к своему пуш-серверу, идентифицируется и принимает уведомления в сервис-воркер (в нем срабатывает событие push). Нюанс тут в том, что у каждого браузера пуш-сервис свой:


  • У Firefox он называется Mozilla Push Service. Его исходный код и спецификация протокола открыты, чем мы позже воспользовались.
  • У Chrome это Google Cloud Messaging (не Firebase Cloud Messaging, что можно увидеть по именам доменов в исходном коде), и так далее.

Хорошая новость для нас в том, что веб-пуши стандартизированы IETF (https://datatracker.ietf.org/wg/webpush/documents/), и поддерживать разные форматы API для каждого браузера как на клиенте, так и на сервере нам не придется.


Теперь рассмотрим устройства на базе Android. Здесь есть несколько вариантов:


  • Если в системе установлены Google Apps, то можно воспользоваться Firebase Cloud Messaging.
  • Если у вас устройство от Huawei без Google Apps, то можно использовать Huawei Push Kit.
  • Можно написать собственный пуш-сервер или воспользоваться готовыми проектами, например, https://bubu1.eu/openpush/, благо открытость платформы позволяет.

Далее идет iOS. В отличие от Android, способ отправить уведомления на устройства Apple всего один — использовать Apple Push Notification service (APNs).


Может возникнуть логичный вопрос: неужели придется поддерживать всё это многообразие стандартов, API и прочего на серверной стороне? На самом деле, всё не так уж и плохо, так как Firebase Cloud Messaging, помимо отправки на Android, покрывает еще и веб-пуши и работает с APNs. Так мы пришли к следующей схеме: на устройства Huawei без Google Apps отправляем через Huawei Push Kit, в остальных случаях пользуемся Firebase Cloud Messaging.



Несмотря на многообразие стандартов и сервисов, схема работы будет примерно одинаковой:


  1. Клиент подключается к пуш-серверу и получает уникальный идентификатор — токен.
  2. Клиент отправляет токен серверу конкретного приложения, чтобы стать связаным с учетной записью пользователя.
  3. Сервер приложений начинает по полученному токену отправлять пуши для конкретного пользователя.

Попробуем написать тестовое приложение


Для начала просто получим пуш-токен от Firebase и попробуем отправить пуш. Нужно зарегистрировать проект в консоли Firebase и получить конфигурацию для веб-приложения. Для корректного функционирования будет нужен локальный HTTP-сервер с передачей статики.


Сделаем страницу с одной кнопкой и необходимыми скриптами:


simple_example.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Тестовый пример приема пушей</title>
    </head>
    <body>
    <script src="https://www.gstatic.com/firebasejs/7.14.5/firebase-app.js"></script>
    <script src="https://www.gstatic.com/firebasejs/7.14.5/firebase-messaging.js"></script>
    <script>
        function toClipboard(text) {
            const tmp = document.createElement('textarea');
            tmp.hidden = true;
            tmp.value = text;
            window.document.body.appendChild(tmp);
            tmp.select();
            window.document.execCommand("copy");
            alert("Copied the text: " + text);
            window.document.body.removeChild(tmp);
        }
    </script>
    <button onclick="enableNotifications()">Получать уведомления</button>
    <div id="pushTokenLayer" hidden>
        Firebase token <code id="pushTokenValue" style="cursor:pointer" onclick="toClipboard(this.innerText)"></code><br/>
    </div>
    <script>
        async function enableNotifications() {
            // Insert your firebase project config here
            const firebaseConfig = {};

            const app = firebase.initializeApp(firebaseConfig);
            const messaging = app.messaging();

            const permission = await Notification.requestPermission();
            if (permission !== 'granted') {
                console.log("user denied notifications")
            }

            const token = await messaging.getToken();

            window.document.getElementById("pushTokenLayer").removeAttribute("hidden");

            const pushTokenValue = window.document.getElementById("pushTokenValue");
            pushTokenValue.innerText = token
        }
    </script>
    </body>
</html>

Также потребуется скрипт сервис-воркера. По умолчанию он подгружается автоматически по пути /firebase-messaging-sw.js. Для начала будем использовать готовый скрипт отсюда.


Открываем страницу, нажимаем на кнопку, разрешаем уведомления в браузере и копируем отображенный токен. Для удобства работы с API вручную можно создать долговременный ключ сервера (не сервисный аккаунт). Делаем простой запрос:


curl -X POST 'https://fcm.googleapis.com/fcm/send' \
-H 'Authorization: key=<fcm server key>' \
-H 'Content-Type: application/json' \
-d '{
 "to" : "<токен из браузера>",
 "notification" : {
     "body" : "Body of Your Notification",
     "title": "Title of Your Notification"
 }
}'

Получаем уведомление:



Тут есть важный момент, о котором можно забыть: пуш-токен никак не связан с пользователем приложения, он связан с конкретным получателем (т.е. с клиентской конфигурацией). На мобильных устройствах он связан с конкретной инсталляцией приложения.


Посмотрим, что нам приходит в браузер. В документации описан колбэк setBackgroundMessageHandler. Модифицируем сервис-воркер, добавив в конец файла код:


messaging.setBackgroundMessageHandler((payload) => {
  console.log('Message received. ', payload);
  // ...
});

Открываем консоль сервис-воркера, снова отправляем пуш… и ничего не видим в консоли, хотя уведомление отобразилось. Почему же? Ответ в есть в документации:


Note: If you set notification fields in your message payload, your setBackgroundMessageHandler callback is not called, and instead the SDK displays a notification based on your payload.

В нашем случае в запросе есть поле notification и данный колбэк не вызывается. Это довольно важное замечание, к нему мы вернемся дальше.


Тем не менее, можно это обойти, обрабатывая входящие пуши вручную. Поменяем содержимое firebase-messaging-sw.js:


firebase-messaging-sw.js
self.addEventListener("push", event => { event.waitUntil(onPush(event)) });

async function onPush(event) {
    const push = event.data.json();
    console.log("push received", push)

    const { notification = {} } = {...push};

    await self.registration.showNotification(notification.title, {
        body: notification.body,
    })
}

Здесь мы считываем полезную нагрузку в json и парсим ее в js-объект, который будет выведен в консоль, заодно показывая уведомление. Обратите внимание на waitUntil внутри обработчика: он нужен для того, чтобы сервис-воркер не завершил работу до окончания асинхронного вызова onPush.


Теперь добавим пользователей в наше приложение


Для удобства заведем новую страницу:


user_example.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Тестовый пример приема пушей</title>
    </head>
    <body>
    <script src="https://www.gstatic.com/firebasejs/7.14.5/firebase-app.js"></script>
    <script src="https://www.gstatic.com/firebasejs/7.14.5/firebase-messaging.js"></script>
    <script>
        function toClipboard(text) {
            const tmp = document.createElement('textarea');
            tmp.hidden = true;
            tmp.value = text;
            window.document.body.appendChild(tmp);
            tmp.select();
            window.document.execCommand("copy");
            alert("Copied the text: " + text);
            window.document.body.removeChild(tmp);
        }
    </script>
    <form onsubmit="enableNotifications(this); return false" action="#">
        User ID <input type="number" name="userID" required/>
        <input type="submit" value="Получать уведомления"/>
    </form>
    <div id="pushTokenLayer" hidden>
        Firebase token <code id="pushTokenValue" style="cursor:pointer" onclick="toClipboard(this.innerText)"></code><br/>
        <button onclick="logout()">Выйти</button>
    </div>
    <script>
        // Insert your firebase project config here
        const firebaseConfig = {};

        const app = firebase.initializeApp(firebaseConfig);
        const messaging = app.messaging(); // this fails if browser not supported

        async function getMe() {
            const resp = await fetch(`${window.location.origin}/api/v1/users/me`, {
                credentials: "include",
            });

            if (resp.status === 401) {
                return null;
            }
            if (!resp.ok) {
                throw `unexpected status code ${resp.status}`
            }

            return await resp.json();
        }

        async function sendToken(token) {
            const me = await getMe();
            if (me === null) {
                console.error("unauthorized on send token");
                return;
            }

            window.localStorage.getItem("push-token-user");

            const resp = await fetch(`${window.location.origin}/api/v1/tokens`, {
                method: "POST",
                body: JSON.stringify({
                    token: {token: token, platform: "web"}
                }),
                credentials: "include",
            })
            if (!resp.ok) {
                console.error("send token failed");
                return;
            }

            // put current user to local storage for comparison
            window.localStorage.setItem("push-token-user", JSON.stringify(me));
        }

        getMe().
            then(me => {
                if (!me) {
                    // if user not authorized we must invalidate firebase registration
                    // to prevent receiving pushes for unauthorized user
                    // this may happen i.e. if 'deleteToken' failed on logout
                    console.log(`user unauthorized, invalidate fcm registration`);
                    window.localStorage.removeItem("push-token-user");
                    messaging.deleteToken();
                    return null;
                }

                // if user authorized and it's not user that received push token earlier
                // we also must invalidate token to prevent receiving pushes for wrong user
                // this may happen if i.e. user not logged out explicitly
                let pushTokenUser = window.localStorage.getItem("push-token-user");
                if (pushTokenUser && JSON.parse(pushTokenUser).id !== me.id) {
                    console.log("token for wrong user, invalidate fcm registration");
                    window.localStorage.removeItem("push-token-user");
                    messaging.deleteToken();
                    pushTokenUser = null;
                }

                // if user authorized and permission granted but token wasn't send we should re-send it
                if (!pushTokenUser && Notification.permission === "granted") {
                    console.log("token not sent to server while notification permission granted");
                    messaging.getToken().then(sendToken);
                }
            }).
            catch(e => console.log("get me error", e))

        // according to sources of firebase-js-sdk source code registration token refreshed once a week
        messaging.onTokenRefresh(async () => {
            const newToken = await messaging.getToken();
            pushTokenValue.innerText = newToken;
            console.log(`updated token to ${newToken}`)
            await sendToken(newToken)
        })

        async function enableNotifications(form) {
            const loginResponse = await fetch(`${window.location.origin}/api/v1/users/login`, {
                method: "POST",
                body: JSON.stringify({
                    id: Number(form.elements.userID.value),
                })
            })
            if (!loginResponse.ok) {
                alert("login failed");
                return;
            }

            const permission = await Notification.requestPermission();
            if (permission !== 'granted') {
                console.log("user denied notifications")
                return;
            }

            const token = await messaging.getToken();

            window.document.getElementById("pushTokenLayer").removeAttribute("hidden");

            const pushTokenValue = window.document.getElementById("pushTokenValue");
            pushTokenValue.innerText = token

            await sendToken(token)
        }

        async function logout() {
            const messaging = firebase.messaging();
            await messaging.deleteToken();
            console.log(`deleted token from firebase`)
            window.document.getElementById("pushTokenLayer").setAttribute("hidden", "");
            await fetch(`${window.location.origin}/api/v1/users/logout`, {
                method: "POST",
                credentials: "include",
            })
        }
    </script>
    </body>
</html>

Нам понадобится простенький бэкенд, писать будем на Go. Тут приведу только пример кода, отвечающего за хранилище токенов:


inmemory.go
type MemoryStorage struct {
    mu          sync.RWMutex
    userTokens  map[uint64][]Token
    tokenOwners map[string]uint64
}

func NewMemoryStorage() *MemoryStorage {
    return &MemoryStorage{
        userTokens:  map[uint64][]Token{},
        tokenOwners: map[string]uint64{},
    }
}

type Token struct {
    Token    string `json:"token"`
    Platform string `json:"platform"`
}

func (ms *MemoryStorage) SaveToken(ctx context.Context, userID uint64, token Token) error {
    ms.mu.Lock()
    defer ms.mu.Unlock()

    owner, ok := ms.tokenOwners[token.Token]
    // if old user comes with some token it's ok
    if owner == userID {
        return nil
    }
    // if new user come with existing token we
    // should change it's owner to prevent push target mismatch
    if ok {
        ms.deleteTokenFromUser(token.Token, owner)
    }

    ut := ms.userTokens[userID]
    ut = append(ut, token)
    ms.userTokens[userID] = ut

    ms.tokenOwners[token.Token] = userID

    return nil
}

func (ms *MemoryStorage) deleteTokenFromUser(token string, userID uint64) {
    ut := ms.userTokens[userID]
    for i, t := range ut {
        if t.Token == token {
            ut[i], ut[len(ut)-1] = ut[len(ut)-1], Token{}
            ut = ut[:len(ut)-1]
            break
        }
    }
    ms.userTokens[userID] = ut
}

func (ms *MemoryStorage) UserTokens(ctx context.Context, userID uint64) ([]Token, error) {
    ms.mu.RLock()
    defer ms.mu.RUnlock()

    tokens := ms.userTokens[userID]
    ret := make([]Token, len(tokens))
    copy(ret, tokens)

    return ret, nil
}

func (ms *MemoryStorage) DeleteTokens(ctx context.Context, tokens []string) error {
    ms.mu.Lock()
    defer ms.mu.Unlock()

    for _, token := range tokens {
        user, ok := ms.tokenOwners[token]
        if !ok {
            return nil
        }

        ms.deleteTokenFromUser(token, user)
    }
    return nil
}

В бэкенд также добавлен код отправки пушей конкретному пользователю.


Базово мы обеспечиваем следующую функциональность:


  • При входе пользователя запрашиваем разрешение на показ уведомлений, и при согласии получаем пуш-токен и отправляем его на сервер. Пуш-токен привязывается к пользователю с помощью сессионных/авторизационных кук.
  • При выходе мы инвалидируем пуш-токен. При следующей попытке отправить его Firebase нам ответит ошибкой, что такой токен не существует, и мы удалим его из хранилища.
  • При обновлении пуш-токена мы отправляем новый в хранилище. Старый токен мы удалим уже при следующей отправке пуша.

Опишу несколько важных моментов, на которые мы наткнулись уже на практике (они отражены в примере):


  • Пуш-токены имеют ограниченный срок жизни, однако узнать его из самого токена, на первый взгляд, невозможно. Судя по коду firebase-js-sdk, этот срок чуть больше недели, так как колбэк на обновление токена onTokenRefresh вызывается раз в неделю.
  • Разные пользователи могут прислать одинаковые пуш-токены. Такое возможно в случае, если запрос на инвалидацию в Firebase не прошел успешно. Для решения этой проблемы мы в данном случае меняем владельца токена.
  • У пользователя может завершиться сессия без явного логаута. Т.е. пользователь больше не авторизован, однако уведомления продолжают поступать. Способ решения этой проблемы зависит от архитектуры приложения. Мы при отправке пуш-токена на сервер сохраняем идентификатор пользователя еще и локально, при каждой загрузке страницы сверяя его с ответом на запрос о текущем пользователе. Если значения различаются или пользователь не авторизован, то пуш-токен инвалидируется. Однако у такого подхода всё же есть один недостаток: инвалидация происходит только в случае загрузки страницы сайта.
  • Сохраняйте платформу, с которой получен токен. Это поможет при дальнейшей кастомизации: например, добавить возможность ответа в чат прямо из пуша (в Android/iOS можно, в браузере — нет), кнопки и прочее.

И вот, грабли собраны, доработки выложены в прод. Пуши ходят… или не ходят? Самое время поговорить про


Надежность


Никаких методов подтверждения доставки от клиента серверу приложений изначально не предусмотрено, хотя в Huawei над этим задумались и сделали. Поэтому нам придется реализовывать эту функциональность самим. Первое, что приходит в голову — отправлять на сервер HTTP-запрос при получении пуша. Для этого нам потребуется идентифицировать каждый пуш, благо и Firebase, и Huawei это позволяют: можно пробросить произвольные данные при отправке уведомления.


Идея следующая: мы генерируем одноразовый токен подтверждения (в нашем случае это просто UUID) и отправляем его в пуше. Клиент при получении и показе пуша делает HTTP-запрос, в который включается присланный токен подтверждения. Немного дорабатываем бекенд и firebase-messaging-sw.js:


firebase-messaging-sw.js
self.addEventListener("push", event => { event.waitUntil(onPush(event)) });

async function onPush(event) {
    const push = event.data.json();
    console.log("push received", push)

    const { notification = {}, data = {} } = {...push};

    await self.registration.showNotification(notification.title, {
        body: notification.body,
    })

    if (data.id) {
        await fetch(`${self.location.origin}/api/v1/notifications/${data.id}/confirm`, { method: "POST" })
    }
}

И если с вебом нам хватило такой простой доработки, то с мобильными устройствами всё несколько сложнее. Помните про замечание в документации о setBackgroundMessageHandler? Так вот, дело в том, что в Firebase (да и в Huawei) есть не совсем очевидное (по API) разделение на, условно, информационные пуши (если есть поле notification) и data-пуши. По задумке, информационные пуши никак не обрабатываются приложением и на их основе сразу формируется уведомление, а data-пуши попадают в специальный обработчик и дальше приложение решает, что делать.


Если при получении веб-пушей с ними можно работать до показа, отказавшись от firebase-js-sdk в сервис-воркере, то в Android так не получится. Поэтому для Android мы перенесли всю нужную информацию исключительно в data и перестали отправлять notification, что позволило нам реализовать подтверждение доставки.


Для APNs же достаточно просто выставить mutable-content в 1, и тогда при обработке пуша можно будет выполнять некоторый код, но с довольно серьезными ограничениями, хотя этого вполне достаточно для простого HTTP-запроса. Собственно, именно из-за ограничений iOS при подтверждении пуша не задействуется пользовательская сессия, а используются одноразовые токены.


Отсюда вытекает еще одна интересная особенность: data-пуши можно использовать не только для уведомлений пользователя, но и для доставки какой-либо информации, настроек и прочего в приложение. Примерно таким способом, например, Telegram обходил блокировки, отправляя адреса серверов через пуши.


Мы же используем механизм подтверждения следующим образом: для некоторых типов уведомлений, если не дождались подтверждения через, скажем, 15 минут после отправки, отправляется смс. А чтобы исключить случай, когда после смс приходит пуш, можно выставить TTL при его отправке.


Также этот механизм позволяет нам собирать статистику по доставляемости. Исходя из собранных данных, доставляемость пушей по платформам получается примерно следующей:


  • Android (исключая Huawei) — 40 %
  • Web — 50 %
  • iOS — 70 %

Для Huawei достаточное количество данных еще не накоплено. Следует сказать о том, что мы не предпринимали никаких действий по увеличению доли доставленных пушей, как это сделали, например, в Яндекс.Почте.


Итоговая архитектура получается примерно такой:



Полную версию проекта можно взять на GitHub.


В следующих частях я планирую рассказать о дополнительных возможностях, предоставляемых пуш-сервисами, более детально о подводных камнях на клиентских платформах, а также о том, как можно принимать веб-пуши, не используя браузер.

ДомКлик
Место силы

Комментарии 15

    0
    Какое-то время сам рассматривал работу с push, но, из-за недостатка времени, пока отложил. И искал я реализации без использования fcm.
    Все упоминания и реализации push непременно связаны с fcm. Да, это, конечно, удобно, что не нужно заморачиваться с сервером и разницей реализации в разных браузера, но этот вариант не особо подходит для целей, когда одним из требований является работа только с локальными ресурсами (например, когда web недоступен).
    Плюс, хотелось бы понимать что происходит «под капотом» библиотек, которым безоговорочно доверяют.
      +3

      К сожалению, без FCM это будет сделать довольно тяжело и не будет нативной поддержки Chrome/Android (они жестко завязаны на FCM), да и сама технология подразумевает использование 3rd-party серверов.
      Про то, что происходит "под капотом" у Firebase SDK на клиентской стороне (в браузере) я тоже планирую написать.
      Отображение же уведомлений что в бразуерах, что на Android можно делать независимо от собственно пуш-системы. Как там у iOS с этим дела обстоят, сходу не могу сказать. Но для этого нужно будет поднимать собственную инфраструктуру, хотя те же OpenPush или AutoPush (существует аж в трех вариантах) могут с этим помочь

        +1
        Когда только начинал разбирать с этими уведомлениями, был неприятно удивлён, что нет API для них. Хотя, казалось бы, всё стандартизовано. Возможно, всё упирается в требование сервера, но можно было бы создать «общие» сервера (для fcm же есть) и при отправке запроса указывать адрес, например. Как появится время, буду заново разбирать. Может в каком-то моменте есть продвижения.
        +2
        Если нужны только web пуши, то там всё довольно несложно, гуглосервисы, вроде fcm, для этого не нужны. Само api стандартизировано и одинаково для любых браузеров (некоторые фичи, вроде кнопок, поддерживаются только хромом).
        Я реализовывал серверную часть на php с помощью вот этого компонента minishlink/web-push. Задача клиентской части в этом случае запросить разрешение на подписку и выслать на сервер три параметра endpoint, publicKey и authToken, это данные одного конкретного получателя.
        Сервис воркер остается примерно таким же, как у автора.
          0
          Спасибо, гляну, как появится время.
        +1
        Простите за банальный вопрос, но почему не вебсокеты или sse?
          +2

          Эти технологии решают несколько иные задачи. Если вебсокеты/sse нужны для обновления состояния приложения в реальном времени, то пуши — скорее для оповещения пользователя. Да, можно получать события по sse или вебсокету и показывать уведомления, однако с этим есть несколько проблем:


          • Нужно строить свою инфраструктуру почти что с нуля, это довольно дорого и долго
          • Пуш-уведомления приходят через одно единственное соединение на весь браузер или мобильное устроиство. Если в браузере никто не мешает перенести подключение в сервис-воркер, то в мобильных устроиствах это, во-первых, не всегда возможно (насколько я знаю, в iOS очень жесткие требования по фоновой работе), во-вторых, ускоряет разряд батареи
            +1

            Спасибо за ответ, всё по делу. по сути оповещения пользователя ту же задачу решают — обновление данных приложения. Отложенные или рилтайм — это уже вопрос реализации протокола и инфраструктуры. Я подобную систему делал на го, не могу сказать что это было мега дорого и проблематично, но конечно от задачи зависит. По поводу хттп соединений — современные фронтенд приложения их много открывают и ни разу на них не экономят. Мне трудно оценить стоимость хттп коннекта в единицах времени работы аккумуляторной батареи, но по опыту разработки мобильного натив софта не заметил влияния. Если покажете бенчмарки, буду признателен.

          +1
          Скажите — всегда интересовал вопрос, как оно работает под капотом Android. Телефон спит — но на связи? Какой то отдельный энергосберегающий процесс? Какой таймаут — пока телефон (а точнее приложение) будет разбужено? Слышал что технология основана на полуоткрытых соединениях. Тогда вопрос — сколько они «живут», сколько провайдер или роутер ждет пока по таймауту не упадет соединение?
            +2

            Судя по тому, что я читал, там действительно за общение с сервером отвечает отдельный процесс, который при получении данных отправляет нужному приложению Intent. Про использование полуоткрытых соединений ни разу не слышал, да и это вроде как ошибочная ситуация. Точнее сказать сложно, так как сам в этом не разбирался. Если разберусь, постараюсь об этом написать.

            +1

            Можете показать формат отправляемого уведомления, где разница между notification и data?

              +1

              Вариант с notification:


              Скрытый текст
              {
               "to" : "<token>",
               "notification" : {
                   "body" : "Body of Your Notification",
                   "title": "Title of Your Notification"
               }
              }

              Приходит вот так:


              Скрытый текст
              {"from":"514044135031","priority":"normal","notification":{"title":"Title of Your Notification","body":"Body of Your Notification"},"collapse_key":"do_not_collapse"}

              Вариант с data:


              Скрытый текст
              {
               "to" : "<token>",
               "data": {
                  "key1": "value1",
                  "key2": "value2"
               }
              }

              Приходит вот так:


              Скрытый текст
              {"data":{"key1":"value1","key2":"value2"},"from":"514044135031","priority":"normal","collapse_key":"do_not_collapse"}
              +1
              Посмотрел на OpenPush, но не нашёл ответа на главный (для меня) вопрос. В Android, начиная с версии 6.0, есть так называемый режим Doze. Суть его вкратце в том, что чем дольше приложение висит в background-е без взаимодействия с пользователем, тем реже и меньше квантов времени ему выделяется системным планировщиком. Т.е. даже если ты держишь собственное соединение с сервером и периодически шлёшь keep-alive пакеты, это никак не спасает – через некоторое время, проведённое в фоне, приложение будет фактически поставлено на паузу с очень редкими, короткими и нерегулярными пробуждениями. Единственный, кто может в этом случае разбудить приложение – системный сервис Google, который держит постоянное соединение с серверами Google, и на который не распространяются ограничения режима Doze (потому что это специализированный системный сервис, подписанный ключом Google, о котором знает производитель устройства, и потому даёт ему повышенные права). Вот этот-то сервис и может получить push (очевидно, что в данном случае это только Firebase Cloud Message) и разбудить заснувшее приложение, послав ему Intent, даже если оно сейчас doze-ировано. Чтобы такими же правами обладал сторонний сервис, надо просить каждого пользователя вручную выключать «Battery optimization» для вашего приложения (нет, автоматически это сделать нельзя). Но это дело гиблое, как правило. Многие проигнорируют, многие сначала сделают так, а потом, под влиянием ли момента, забыв ли, или ещё по какой-то причине, опять включат «Battery optimization». И все. Все ваши пуши будут доставляться с задержкой в несколько часов или же и вовсе будут теряться.

              Т.е. дело-то не в технологии как таковой. Альтернативы гугловскому firebase нет не потому, что там какая-то секретная волшебная технология, а потому, что тупо у сервиса права есть, которых нет у вашего приложения. И этот вопрос никак не проясняется на сайте OpenPush. Или я плохо искал?
                +2

                Openpush лично не испытывал (включил в статью, чтобы было понимание о наличии альтернативных решений), но, скорее всего, вы правы и "оптимизацию батареи" для него придется отключать вручную для стабильной работы. И, кстати, "невидимые" для пользователя data-пуши используют иногда в том числе и для того, чтобы "разбудить" приложение (мы сейчас исследуем эту возможность)

                  +2
                  «Разбудить» приложение — это хороший рабочий вариант. Размер пуша ограничен и много данных в нём не передашь. Хоть время работы приложения после получения пуша ограничено, но для пары тройки запросов и обновления данных в фоне хватает с головой (показать пуш, обновить ленту новостей и тп).

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое