Предисловие
Я начинающий Android разработчик, за плечами у меня около 1,5 года опыта в данной сфере. Взялся я за довольно-таки большой проект, в команде кроме меня никого нет, а бекенд писать я не умею. Решено было в качестве платформы выбрать Firebase. Так как специфика моего приложения требовала постоянной работы и получения данных из базы в фоне, я просто вставил все EventListener-ы в сервис и был доволен. До того самого момента, когда я решил написать iOS версию. Выучив Swift я ринулся в бой. Firebase SDK благо оказались очень хороши и похожи для обеих систем, так что я быстро написал основную часть и… Почему не работает?
Суть проблемы и постановка задачи
iOS мягко говоря не уважает приложения работающие в фоне. Единственный способ пробудить приложение которое убила система (а убивает она их из-за любого чиха) — уведомления через APNS. К тому же, на Android 6+ постоянное соединение не держится и уведомления в итоге приходят с задержкой от 5 минут до 2 часов (на 7.1), если они реализованы не через GCM. Хорошо, что Firebase Cloud Messaging поддерживает и APNS, и GCM. Плохо, что для этого нужен дополнительный сервер. Было бы круто, если б уведомления автоматически отправлялись по определённым изменениям в базе данных. Инженеры обещают сделать нечто подобное в следующем году… А работать то должно уже сейчас.
Собственно, на то чтобы реализовать полноценный сервер с авторизацией и XMPP не у всех есть желание / знания / ресурсы. Итак, у нас есть две проблемы — авторизация пользователя, который хочет отправить push и собственно его отправка. Это в моём случае. Если вам нужно просто отслеживать появление новых данных в базе (например статей) и отправлять уведомление всем, кто подписан на эту тему — то всё ещё проще.
Подготовка
Изначально всё было написано на Python, но ситуация приключилась аналогичной из одной из недавних статей.
На Python возникли проблемы с повторным открытием устройства на чтение — во второй раз данные уже не читались. Мы не стали разбираться и просто переписали то же самое на Golang — после этого все заработало.
Итак, как это работает? Мы используем Firebase REST API чтобы следить за изменениями интересующих нас веток, и в случае добавления новых элементов отправляем пуш через FCM. Где оно работает? Да где угодно. И это одно из главных преимуществ. Вам не обязательно иметь статический IP и приличный хостинг (но это напрямую зависит от количества отправляемых пушей).
Но перед тем как перейти к делу, нужно понимать две вещи.
Во-первых, слежение за всей базой потребует предварительной её загрузки. А если «сервер-помошник» лежал (или перемещался на другой компьютер) — то он загрузит всё заново и заново отправит пуши. Для решения этой проблемы я создал в корне БД ветку notif — в неё пользователи (либо загрузчик контента) добавляют уведомления, которые нужно разослать пользователям, а сервер их удаляет после отправки. Использую я вот такую структуру:
"notif" { "$key" { // Автоматически сгенерированный методом push() ключ "from": "2vgajTP5Vd...", // UID пользователя "to": "all_users", // Либо название темы, либо UID "value": "Hello, Habr!", // Опциональное значение, например сообщение из чата "type": "message"// Тип сообщения, нужен устройству для корректного отображения } }
Во-вторых, нам нужно знать куда отправлять. Поэтому я создал ещё одну ветку «tokens» в которую устройства записывают токены регистрации в FCM. Тонкости реализации на клиентских устройствах это уже тема для отдельной статьи. Храню я их в этой ветке в формате:
"tokens" { "userId": "fcmToken" }
Также, чтобы сообщение нельзя было отправить от чужого имени или получать чужие, я дополнил Firebase Database Rules:
{ "rules": { /// Тут куча других правил "notif": { ".read": "false", "$key": { ".write": "auth != null && newData.child('from').val() === auth.uid" } }, "tokens": { ".read": "false", "$key": { ".write": "auth != null && $key == auth.uid" } } } }
Также нам понадобятся ключи и библиотеки:
- Firebase Database Secret — для чтения данных с запретом на чтение, охохо (тут мог бы быть смайлик). Получить его можно в настройках Firebase Console.

- FCM API key — для отправки пушей. Получить можно там же, на следующей вкладке.
- FireGo — для слежения за базой данных
- FCM — не писать же самому?
Реализация (ну наконец-то!)
Для упрощения примера я убрал из него кэширование токенов, удаление устаревших и проверку покупок приложения через Android publisher API, но если что-то из этого вам интересно — пишите в комментарии, поделюсь полным кодом.
Итак, основная часть программы:
package main import ( "github.com/zabawaba99/firego" "github.com/edganiukov/fcm" "fmt" "log" ) const ( //TODO вставьте сюда свои ключи FDBSecret = "P3cUiIQytto**************NzQM5TrzERjEDO" FCMAPIKey = "AIzaSyDXjRG**************8oOCMrPj18JVD8" DAY_IN_SEC = 86400 // Названия веток в базе TOKENS = "tokens" NOTIFICATIONS = "notif" ) var ( FBDB = firego.New("https://kidgl.firebaseio.com", nil) // Объект для доступа к базе данных FCM, _ = fcm.NewClient(FCMAPIKey) // Объект для отправки пушей ) func main() { FBDB.Auth(FDBSecret) FBDB.Child(NOTIFICATIONS).ChildAdded(gotPush) // Процесс, не умирай, подумай for { var res string fmt.Scanln(&res) if res == "exit" { return } else { println(`Type "exit" to stop service`) } } }
Функция ChildAdded принимает на вход функцию, которую она будет вызывать в случае изменений в базе. Исполняется это всё в отдельном потоке (а может и не в одном, откуда мне знать), так называемом Goroutine. Поэтому, чтобы программа не завершилась, я использую вечный цикл (а она всё равно завершиться от какого-нибудь исключения, перезапуск осуществляется bash-скриптом который на вход принимает stderr).
С этим всё ясно, теперь функция gotPush:
func gotPush(snapshot firego.DataSnapshot, previousChildKey string) { // Мы получили этот пуш, в базе он больше не нужен FBDB.Child(NOTIFICATIONS).Child(snapshot.Key).Remove() // Разбираем его на запчасти data := snapshot.Value.(map[string]string{}) from := data["from"] to := data["to"] typ := data["type"] // Получаем сам токен, потому что мы знаем кому отправлять, но не знаем куда var token string FBDB.Child(TOKENS).Child(to).Value(&token) msg := &fcm.Message{ Token: token, // Data - это всё, что будет доставлено на устройство пользователя Data: &fcm.Data{ "from": from, "type": typ, "value": data["value"], }, CollapseKey: typ + from + to, // Используется для замещения старых уведомлений новыми Priority: "high", ContentAvailable: true, TimeToLive: DAY_IN_SEC, // Наличие этого параметра повышает вероятность доставки пуша } response, err := FCM.Send(msg) if (err!=nil) { log.Println(err) } println("Отправлено: ", response.Success) println("Ошибок: ", response.Failure) if response.Results[0].Unregistered() { // TODO: Приложение удалено с устройства, его можно удалить из базы или оповестить других пользователей об удалении } }
Ну в общем-то и всё, можно запускать и радоваться жизни пушам. В моём случае ещё понадобилось скомпилировать для linux на макбуке, я думаю многим тоже пригодиться `env GOOS=linux GOARCH=amd64 go build backend_helper.go`
Спасибо за прочтение!
ссылка на оригинал статьи https://habrahabr.ru/post/314350/
Добавить комментарий