Достаточно крупное обновление исправляющее ошибки в андроид клиенте, улучшение безопасности получения ID пользователя, рассылка одного пуша на группу пользователей в канале, а также API для работы со списками пользователей.
Сегодня в рубрике:
  1. Используем PushAll и Telegram при разработке в команде
  2. Иконки сожрали мой трафик — помогите!
  3. Почему я вижу лишь их кусок?
  4. Эмоджи убийцы!
  5. Как объединить тысячу итераций отправки уведомлений в одну (multicast)
  6. Воруем личные дан Получаем список пользователей, подписанных на канал
  7. Подписываемся на пуш уведомления ваших друзей (уязвимость)
  8. Не ждем отправки всех уведомлений, выполняем все в фоне.


Опишу в стиле проблемы и её решения

Иконки уведомлений обрезались


Ко мне обращались с проблемой, что на Android 4.* иконки обрезаются. Если точнее, на старых версих не было автоматической подгонки иконок.

Решилось это просто:

if(icon != null){
    Resources res = getApplicationContext().getResources();
    int height = (int) res.getDimension(android.R.dimen.notification_large_icon_height);
    int width = (int) res.getDimension(android.R.dimen.notification_large_icon_width);
    int realwidth=icon.getWidth();
    int realheight=icon.getHeight();
    if(realwidth>realheight){
        height=height * realheight / realwidth;
    }else{
        width=width * realwidth / realheight;
    }
    icon=Bitmap.createScaledBitmap(icon, width, height, true);
}


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

Приложение скачивало по 100 МБ трафика в месяц в виде иконок


Эту проблему уже заметил я, и я посчитал, что она очень серьезная. Также в неё можно включить такие проблемы:
  1. Долгая доставка уведомления при загрузке картинки через нестабильное соединение
  2. Отсутствие иконки при плохом соединении
  3. Отсутствие иконки в режиме экономии энергии.

Про последнее хочу рассказать подробнее. Дело в том, что Android, в новых версиях, в режиме энергосбережения, отключает фоновую передачу всех не системных приложений. Это приводит к тому, что уведомление приходит, а вот иконку мы загрузить не можем.

Решение: создать алгоритм кэширования.

Сначала я долго бился над тем, чтобы алгоритм использовал стандартные средства кэша HTTP.

connection.setUseCaches(true);


Я нашел даже решение для переопределения алгоритма кэширования, т.к. стандартного не было, и оно вроде как работало, за одним исключением — в случае если сети нет — иконка все равно не загрузится. То есть Android блокировал именно все HTTP запросы, даже если идет проверка кэширования.

Тогда я написал свой метод, который проверяет наличие md5-имени файла в папке кэша. И если он есть, то использует его. Сеть вообще не используется. Это достаточно жесткое кэширование, то есть для смены изображения, нужно всегда менять его название, иначе будет загружаться старое.

public Bitmap getBitmapFromURL(String strURL) {
    Bitmap myBitmap;
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inPreferredConfig = Bitmap.Config.RGB_565;
    File file = new File(cacheDir, md5(strURL));
    FileInputStream finput=null;
    if (file.exists()) {
        try {
            finput = new FileInputStream(file);
            myBitmap=BitmapFactory.decodeStream(finput, null, options);
            finput.close();
            return myBitmap;
        } catch (IOException e){
            return BitmapFactory.decodeResource(
                    getResources(), R.drawable.gcm_cloud, options);
        }
    }

    try {
        URL url = new URL(strURL);
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        //connection.setUseCaches(true);
        connection.setDoInput(true);
        connection.connect();

        if (connection.getContentLength() < 524288){
            InputStream input = connection.getInputStream();

            FileOutputStream output = new FileOutputStream(file);
            int bufferSize = 1024;
            byte[] buffer = new byte[bufferSize];
            int len = 0;
            while ((len = input.read(buffer)) != -1) {
                output.write(buffer, 0, len);
            }
            output.close();
            finput = new FileInputStream(file);
            myBitmap = BitmapFactory.decodeStream(finput, null, options);
            finput.close();
        }else{
            myBitmap=BitmapFactory.decodeResource(
                getResources(), R.drawable.gcm_cloud, options);
        }
        return myBitmap;
    } catch (IOException e) {
        //e.printStackTrace();
        return null;
    }
}

public static final String md5(final String s) {
    final String MD5 = "MD5";
    try {
        // Create MD5 Hash
        MessageDigest digest = java.security.MessageDigest
                .getInstance(MD5);
        digest.update(s.getBytes());
        byte messageDigest[] = digest.digest();

        // Create Hex String
        StringBuilder hexString = new StringBuilder();
        for (byte aMessageDigest : messageDigest) {
            String h = Integer.toHexString(0xFF & aMessageDigest);
            while (h.length() < 2)
                h = "0" + h;
            hexString.append(h);
        }
        return hexString.toString();

    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
    }
    return "";
}


Выше я описал итоговый алгоритм генерации битмапа из ссылки с проверкой кэша и MD5 функцию, которую нашел в сети. Думаю, код прост и в комментариях нет необходимости.

Также в алгоритме используется ограничение размера картинки в 512 килобайт.



Итог: при подписке на 20 каналов пользователь не грузит больше по 1-5 мегабайт в день. Также уведомления приходят намного быстрее, а иконки отображаются даже если сети нет, конечно же при условии, что канал не использует каждый раз новые иконки.

Любой мог внедрить callback ссылку на свой сайт и «привязать» свои устройства к чужим аккаунтам


Странно что мы не додумались до этого ранее, но это так. Мы добавили новые методы защиты.

Новый адрес такой: АДРЕС?pushalluserid=ID&time=UNIXTIME&sign=ПОДПИСЬ.

То есть в GET вам будет передан параметр «pushalluserid» с ID пользователя, а также параметры для проверки.
Для проверки подписи используйте md5($key.$pushalluserid.$time.$ipAddress).

Где ipAddress:
$ipAddress = $_SERVER['REMOTE_ADDR'];
Где $key — ключ вашего канала. Где $time — UNIXTIME.

Вы можете сами установить необходимый уровень проверки, к примеру после проверки времени и IP считать ключ валидным 1 минуту. Также следите за тем, чтобы время на вашем сервере было точное. При желании можно считать ссылку валидной вообще секунд 5.
Также остается работать обратная совместимость. То есть после введения этого метода безопасности ваши текущие приложения продолжают работать. Но вам нужно как можно быстрее реализовать защиту на своей стороне.

Как итог имеем проверку и безопасность, реализованную без дополнительных запросов. Главное признавать свои ошибки и вовремя их исправлять.

Почему уже целые сутки через RSS пушится один и тот же пост?!



В общем как то так, получилось даже зловеще немного.
Что случилось? Да вот что,



Тинькофф вставил эмоджи в пост. Тоже самое было еще с одним каналом. Так как на сервере стоит utf8_unicode_ci, а она трехбайтовая, то mysql рубил посты по эмоджи. У проблемы есть 2 решения, это использовать utf8mb4 или вырезать эмоджи.
Первое требует времени, применил второй костыль:

function removeEmoji($text) {

    $clean_text = "";

    // Match Emoticons
    $regexEmoticons = '/[\x{1F600}-\x{1F64F}]/u';
    $clean_text = preg_replace($regexEmoticons, '', $text);

    // Match Miscellaneous Symbols and Pictographs
    $regexSymbols = '/[\x{1F300}-\x{1F5FF}]/u';
    $clean_text = preg_replace($regexSymbols, '', $clean_text);

    // Match Transport And Map Symbols
    $regexTransport = '/[\x{1F680}-\x{1F6FF}]/u';
    $clean_text = preg_replace($regexTransport, '', $clean_text);

    // Match Miscellaneous Symbols
    $regexMisc = '/[\x{2600}-\x{26FF}]/u';
    $clean_text = preg_replace($regexMisc, '', $clean_text);

    // Match Dingbats
    $regexDingbats = '/[\x{2700}-\x{27BF}]/u';
    $clean_text = preg_replace($regexDingbats, '', $clean_text);

    return $clean_text;
}


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


Эта проблема назревала уже давно. На данный момент один из каналов использует unicast для рассылки нескольким людям, у него объемы не такие крупные, а вот новому партнеру будет необходимо рассылать больше 10 тысяч уведомлений за раз, поэтому multicast встал в приоритет.

Особенности такие:
  1. Фильтр — учитывается (в unicast нет, что позволяет уведомить пользователя сквозь фильтр)
  2. За раз можно выбрать 1000 пользователей в массиве. (конечно, такие объемы надо пересылать через POST)
  3. Вам не нужно делать дополнительное уведомление и пересылать все данные, пришлите lid (ID пуша в логе) и новые отправки закрепятся за ним, более того, вы должны это делать, иначе вам будет сложно отслеживать статистику и работать в дальнейшем с уведомлением


Также ниже опишу одновременно и возможность увести задачу в «фоновый» режим. То есть вам не нужно ждать, пока система отправить гуглу все данные, и поработает с телеграмом (что очень долго). Вы просто даете запрос и получаете ID пуша, с которым дальше работаете.

$ch = curl_init()

//цикл?

if($i==0)
$data=array(
    "type" => "multicast",
    "id" => "1",
    "key" => "7db783637a21ed5a8af94513239dada7",
    "text" => "Тестовое сообщение",
    "title" => "Заголовок",
    "background" => 1
  );  //первая итерация
else
$data=array(
    "type" => "multicast",
    "id" => "1",
    "key" => "7db783637a21ed5a8af94513239dada7",
    "lid" => $lid,
    "background" => 1
); //остальные итерации

$data['uids']=json_encode(array_slice($alluids, $i*1000, 1000, true)); //отрезаем 1000 записей для передачи на сервер.

curl_setopt_array($ch, array(
CURLOPT_URL => "https://pushall.ru/api.php",
CURLOPT_POSTFIELDS => $data,
  CURLOPT_SAFE_UPLOAD => true,
  CURLOPT_RETURNTRANSFER => true
));
$return=curl_exec($ch); //получить ответ или ошибку
$lid=json_decode($return,true)['lid'];
//закрываем цикл

curl_close($ch);


Код выше позволяет.
  1. 1. Выполнять задачи быстро, отправляя их в фон
  2. 2. Отправлять несколько тысяч уведомлений с одним и тем же ID в цикле
  3. 3. Использовать Keep-Alive для более быстрой коммуникации.


Background — доступен сегодня.
Multicast еще в разработке и тестируется, будет доступен через неделю.

Как получить списки пользователей?


Ранее, я описывал метод showlist. Теперь у него есть параметр — subtype=users
При использовании этого параметра, вы получаете список всех пользователей, а если добавить uid=ID пользователя, то можно выбрать и отдельного.

Вы получаете все данные, что и при просмотре списка через сайт.


Телеграм для групп, бизнеса и разработки


После добавления функции интеграции телеграма, мне очень многие писали с просьбами работы с группами.

Зачем это нужно?
  • Оказывается есть интернет магазины, которые обсуждают заказы в общем чате и координируют действия. Им удобно, когда с их магазина сыпется уведомление в общий чат.
  • Есть группы разработки, которые хотят получать в общий чат, коммиты, баги, обновления или пуши из их систем и далее уже в общем чате это обсуждать.


Вам нужно лишь добавить бота @PushAllGrBit в общий чат, написать /getid и прописать полученный ID в настройках канала.
При этом ваши подписчики все также смогут получать уведомления на свои устройства параллельно с телеграмом.

Итоги, что нас ждет дальше?


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

Для пользователей скоро будут доступны новые составные фильтры, системы оценок и отзывов на каналы и многое другое. В приложениях будут возможности настройки звука уведомлений и вибрации.
Задавайте вопросы по новым функциям, предлагайте новые возможности, которые вам необходимы.
Удачного дня!

Комментарии (4)


  1. jonasas
    18.07.2015 15:24

    А для чего Вы используете md5 хеш сумму? Почему бы не воспользоваться готовым методом Object.hashCode()?


    1. BupycNet Автор
      18.07.2015 16:50
      -4

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


    1. youROCK
      18.07.2015 19:21
      -2

      Я думаю, стандартный hashCode() будет давать на порядки больше коллизий, чем md5. По сути, при кешировании по md5 можно вообще не проверять, что это хеш соответствует запрошенному ключу, и за все время жизни существования вселенной вы не получите ни одной неправильно закешированной ссылки :)


      1. BupycNet Автор
        19.07.2015 08:57
        +1

        В общем то тоже об этом подумал, когда сейчас посмотрел.

        А вообще сам код под андроид нормальный? Я не так давно и не так много под него программирую, вот хотел узнать, вообще такой метод кэширования оптимален? На хабре встречал статью, где для кэширования изображений использовалась целая самописная библиотека, кэшировалось все на SD-карту и т.д. Тут же как я понимаю, cacheDir тоже должен быть на карте, если приложение перенесено на неё?
        Хотя сейчас это не так существенно, каналов не так много, чтобы забить хотя бы 10 мегабайт, само приложение весит 1 МБ, а сейчас телефоны уже почти все с большим внутренним хранилищем. При этом у меня есть несколько пользователей, у которых и на бюджетке, к примеру, моё приложение умещается, а больше ничего они скачать не могут.