Хабрахабр

[Из песочницы] (Не)оригинальное поздравление на 8 марта

Я думаю, многие в детстве рисовали самодельные открытки для мам, сестер, бабушек. В школе так уж точно. Однако, после определенного возраста увлечение подобными вещами остается уделом очень маленького числа людей. Вряд ли вы хоть раз дарили что-то подобное своей девушке/жене. А ведь им наверняка понравится, психология и всё такое.

Фрейд был бы доволен

Естественно, мы, программисты, народ плечистый, нас не заманишь бумагой шелковистой, поэтому открытку делать я решил в виде мобильного приложения, а не клеить на коленке из картона и цветной бумаги. Благо, смартфоны сейчас даже у бездомных есть.

Дисклеймер

Я ни коим образом не претендую на качество программного кода, представленного в посте, на оригинальность идеи либо ее реализации.
Я не несу ответственности за ваши воспаленные от ужасного кода глаза и оскорбление чувств программистов.
Я понимаю, что большинство принятых мной решений при разработке были неидальными, и надеюсь, что данный пост убережет других разработчиков от аналогичных промахов

Идея и анализ задачи

Итак, для успешного, яркого и запоминающегося поздравления нужен, что? Правильно, Вау-эффект! Для этого следует сделать все незаметно для объекта поздравления, и добавить немного программистской магии. На мое счастье, в Android её хватает, как белой так и черной.

Представьте, вы написали и установили приложение в телефон к жертве виновнику всей затеи, а потом что? "Запусти, пожалуйста, вон то приложение" или "Дай телефон на секунду… Смотри!". Вау-эффекта не будет, даже не надейтесь.
Поэтому самое лучшее, что пришло мне в голову — предустановить приложение (хотел замаскировать, но не успел, да и не пришлось, в общем-то), а в момент Х запустить его удаленно, с помощью магии, удивив ничего не подозревающего маггла.

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

Уже на стадии проектирования я совершил свою первую и самую критическую ошибку. Я решил, что успею за короткий срок сделать красивую открытку с кучей анимаций, не имея никакого опыта работы с этой самой анимацией и её оптимизацией. Не скажу, что визуально получилось плохо, но и без той анимации, что я добавил, всё было бы превосходно, и проблем стало б меньше.

Инструментарий

Для работы нам понадобятся:

  • Любая IDE для андроид-разработки
  • Фото жертвы и стоковые изображения для открытки
  • Google Firebase либо Google Cloud Firestore
  • Утилита curl либо любое другое средство отправки POST-запросов

Реализация

Для создания анимаций я воспользовался библиотекой WowoViewPager. Не советую ее использовать для подобных проектов.

Описание и минусы

Библиотека позволяет создавать анимированные слайды, работающие через ViewPager. Перелистывание по умолчанию осуществляется свайпами. Можно полностью настроить движение любых view-элементов, скорость и тип анимации. Поддерживаются gif и svg анимации.

Основным минусом, на мой взгляд, является требование все элементы хранить в одном xml-файле. Библиотека, судя по всему, не рассчитана на большое количество слайдов (в оригинальном примере их всего максимум 4). В моем случае 21 слайд и 16 jpeg-фото вызвало "отжирание" более 200 Мб памяти.

Расписывать процесс создания анимации я не стану, поскольку у каждого программиста свои костыли и велосипеды. Хочу лишь упомянуть, что для сокрытия тулбара и панели уведомлений можно воспользоваться флагами

getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);

В процессе работы с библиотекой анимирования меня поджидал несколько неожиданный трабл. Классическая формула центрирования по абсолютным координатам

(screenWidth-viewWidth)/2

не работала; опытным путем я выяснил, что нужный эффект дает абсолютно аналогичная формула:

screenWidth/2 - viewWidth/2

Однако, в одном исключительном случае, фотография центрировалась только по формуле

screenWidth/3.5 - viewWidth/3.5

При том, что разница в размерах составляла всего 3 пикселя по оси Y (!sic)

WAT?

Именно так я себя чувствовал в тот момент. Мне неизвестно, с чем это связано — с особенностями координат в андроид, багами библиотеки, или моими корявыми руками. Может быть, в комментариях кто-то сталкивался с аналогичными проблемами.

После реализации самой открытки, настал черед магии.
Прежде всего, создадим новый проект в Firebase Console.
При создании проекта укажите название и страну.
Следующим шагом, нужно подключить Firebase к написанной открытке. На выбор есть два варианта: Realtime Database и Cloud Firestore. В этом конкретном случае разницы нет, со своей задачей оба сервиса справляются прекрасно. В чем глобальная разница — я не знаю. Я использовал Cloud Firestore. По ссылке есть официальный туториал.

1) Укажите зависимости Gradle на уровне проекта

 dependencies { ... classpath 'com.google.gms:google-services:3.2.0' ... }

2) Укажите зависимости Gradle на уровне модуля app. Сразу учтем зависимость для получения push-уведомлений

 compile 'com.google.firebase:firebase-core:11.8.0' compile 'com.google.firebase:firebase-firestore:11.8.0' compile 'com.google.firebase:firebase-messaging:11.8.0'

3) Создайте и добавьте в манифест сервис для получения токена Firebase и дальнейшей записи в Cloud Firestore. Токен будет нам нужен для удаленного запуска приложения. Замечу, что у меня нет функции удаления старого токена из базы, не успел реализовать. Если вам потребуется, пример есть выше по ссылке.

Запись в манифесте

<application>
... <service android:name=".FirebaseIdService"> <intent-filter> <action android:name="com.google.firebase.INSTANCE_ID_EVENT"/> </intent-filter> </service>
...
</application>

Код сервиса

public class FirebaseIdService extends FirebaseInstanceIdService
{ @Override public void onTokenRefresh() { //получаем токен в случае его обновления String refreshedToken = FirebaseInstanceId.getInstance().getToken(); Log.d("TOKEN REFRESH", refreshedToken); //получаем экземпляр класса FirebaseFirestore db = FirebaseFirestore.getInstance(); Map<String, Object> data = new HashMap<>(); //помещаем токен в hashmap data.put("token", refreshedToken); //devices - название документа в базе данных db.collection("devices") .add(data) .addOnSuccessListener(new OnSuccessListener<DocumentReference>() { @Override public void onSuccess(DocumentReference documentReference) { Log.d("FIREBASE", "Data added, id: " + documentReference.getId()); } }) .addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { Log.d("FIREBASE", "Data adding failed, exception: \n" + e); } }); }
}

4) Создаем и добавляем в манифест сервис для получения push-уведомлений от Firebase. Ранее мы уже добавили зависимость в Gradle.

Я не буду объяснять принципы создания и работы push-уведомлений в android. Для этого есть профильные сайты. Приведенный пример должен сработать в большинстве смартфонов с андроид 4.2.2 и выше.

Запись в манифесте

<application>
...
<service android:name=".FirebaseMessageHandleService"> <intent-filter> <action android:name="com.google.firebase.MESSAGING_EVENT"/> </intent-filter> </service>
...
</application>

Код сервиса

public class FirebaseMessageHandleService extends FirebaseMessagingService
{ @Override public void onMessageReceived(RemoteMessage remoteMessage) { super.onMessageReceived(remoteMessage); { //Создаем интент для запуска уведомления Intent intent = new Intent(this, MainActivity.class); /** * если экземпляр данной Activity уже существует, * то все Activity, находящиеся поверх нее разрушаются, * и этот экземпляр становится вершиной стека. * Также вызовется onNewIntent() */ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); /** * Создаем интент для доступа к правам текущего приложения * Если уже есть такой интент - стираем его */ PendingIntent contentIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); //Получаем объект менеджера уведомлений NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); //Метод устарел, но я взял из другого проекта, и лень переписывать. Не продакшн ведь. NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this); //Указываем параметры уведомления: иконку, заголовок, текст, интент для запуска. notificationBuilder.setContentIntent(contentIntent); notificationBuilder.setSmallIcon(R.drawable.ic_launcher_background); notificationBuilder.setContentTitle("Вам открытка!"); notificationBuilder.setContentText("Нажмите на меня для открытия"); //Создаем объект класса Notification Notification notification = notificationBuilder.build(); notification.defaults = Notification.DEFAULT_SOUND; notificationManager.notify(1, notification); /** * Немного магии. * Именно эта небольшая часть кода запускает активити * при получении push-уведомления из Firebase * Сработает даже при заблокированном экране */ Intent intent1 = new Intent(getApplicationContext(), MainActivity.class); intent1.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent1); } }
}

Оффтоп - забавный момент

Дописывал открытку я уже ранним утром 8 марта. Катастрофически не успевал, до будильника оставались считанные минуты, а я еще не проверял, как работает приложение в смартфоне девушки (у нас разные разрешения экрана). У меня был доступ по отпечатку пальца. При настройке отладки по USB потребовалась перезагрузка. После перезагрузки оказалось, что я не помню графический пароль. В ту секунду я почувствовал себя полным дебилом. В шаге от финиша я не мог проверить работу приложения (facepalm). Пришлось будить девушку и заставить ее разблокировать, благо, спросонья она ничего не соображала и ввела пароль на автомате.

Финальная стадия разработки. Осталось только подготовить запрос для отправки в Firebase для инициализации push-уведомления.

Важный момент:
Firebase позволяет отправлять уведомление двумя способами: простым и кастомизируемым.
Простой вариант подразумевает отправку только двух полей — заголовка и текста уведомления. Такой тип сообщения можно отправить прямо из консоли Firebase, однако уведомление будет получено только в том случае, если есть работающий процесс приложения. Он нам не подходит, поскольку приложение никак не должно проявлять себя до момента X.

Кастомизируемый вариант позволяет отправлять сообщения в json-формате объемом до 4Кб с любым содержанием. Такое сообщение придет, даже если сообщение очищено из стека ранее запущенных приложений. (Однако, если приложение и его сервисы были убиты каким-нибудь чистильщиком памяти, уведомление не придет до момента перезапуска приложения). Минус кастомизируемого способа в том, что его можно отправить только POST-запросом к серверам Firebase.

Для отправки запроса я воспользовался утилитой curl. Поскольку код писался из-под Linux Mint, мне достаточно было запустить терминал. Вы можете использовать любой другой инструмент, например Postman.

Заголовоки запроса

"Authorization: key=<ваш ключ>"
Ключ можно посмотреть в настройках проекта Firebase (нажать на шестеренку)

"Content-Type: application/json"

Тело запроса

{ "to":"Firebase-токен устройства", "data":{"любые поля":"любые значения"}, "priority":10 //по умолчанию
}

Готовая команда для curl

curl -X POST --header "Authorization: key=your_key" --Header "Content-Type: application/json" https://fcm.googleapis.com/fcm/send -d "{\"to\" : \"firebase_token\" , \"data\":{\"name\" : \"value\"} , \"priority\" : 10}"

Теперь остается только в нужный момент отправить запрос!

Результат


Девушке понравилось, вау-эффект был достигнут 🙂

Теги
Показать больше

Похожие статьи

Кнопка «Наверх»
Закрыть