(Не)оригинальное поздравление на 8 марта

в 10:44, , рубрики: java, ошибки и грабли, плохой код, поздравление, Программирование на Android, Разработка под android

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

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

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

Дисклеймер

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

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

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

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

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

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

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

(Не)оригинальное поздравление на 8 марта - 2

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

  • Любая 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 (нажать на шестеренку)

(Не)оригинальное поздравление на 8 марта - 4

"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}"

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

Результат

(Не)оригинальное поздравление на 8 марта - 5
(Не)оригинальное поздравление на 8 марта - 6

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

Автор: crazytosser00

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js