вторник, 30 июня 2015 г.

Android GCM. Часть 1. Регистрация устройства.

Ни одно современное мобильное приложение сейчас не обходится без пуш уведомлений - сообщений, рассылаемых сервером на устройства клиентов. Google предоставляет сервис Cloud Messaging (GCM) для рассылки уведомлений на устройства Android.

Здесь будет рассказано, как интегрировать поддержку GCM в свое приложение.
(Реализация на серверной стороне будет рассмотрена отдельно)

Архитектура


Реализация GCM включает в себя 3 составляющих:
1. Сервер приложения, с которого исходят уведомления;
2. Мобильное устройство, которое принимает уведомления;
3. GCM серверы, которые отправляют сообщения с сервера приложения на мобильные устройства.


Перед началом работы


Для разработки понадобиться:
1. Установить Google Play services SDK, если он еще не установлен у вас. Инструкция по установке здесь.
2. Подключить Google Play Services для GCM в приложении, добавив строчку compile 'com.google.android.gms:play-services-gcm:7.5.0' в Gradle. Так же убедитесь, что все ссылки на Google Play Services в Gradle имеют одинаковую версию, иначе вы не сможете запустить отладку. Инструкция здесь.
3. Создать приложение в Google Developer Console и активировать в нем GCM API, как описано здесь. Это забота серверной стороны. Клиентскому приложению нужен только номер созданного приложения из Google Developer Console. Если вы создаете только клиентское приложение, уточните этот номер у разработчика серверной части.

Редактирование файла Manifest


Добавьте следующие строчки в manifest приложения:

1. <uses-permission android:name="android.permission.INTERNET" />
Для того, чтобы приложение могло отправить идентификатор устройства на сервер.

2. <uses-permission android:name="android.permission.WAKE_LOCK" />
Предотвращает засыпание процессора во время приема сообщений.

3. <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
Для разрешения приложению регистрировать и принимать сообщения

4. <permission android:name="com.example.gcm.permission.C2D_MESSAGE"
       android:protectionLevel="signature" />
<uses-permission android:name="com.example.gcm.permission.C2D_MESSAGE" />
Нужен для запрета другим приложениям принимать уведомления создаваемого приложения.
Здесь нужно заменить com.example.gcm на имя пакета вашего приложения.

5. <uses-permission android:name="android.permission.GET_ACCOUNTS" />
Было в описании, но не сказано, для чего нужно.

Полная инструкция здесь.

Регистрация устройства


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

Токен будем хранить в Shared Preferences. Так же нужно хранить информацию о том, отправлен токен на сервер или нет. Сначала напишем методы для этого:

// Имя Shared Preferences
private static final String SHARED_PREF_NAME = "Preferences";
// Ключ токена
private static final String PREF_DEVICE_TOKEN = "DeviceToken";
// Флаг, отвечающий за то, отправлен токен на сервер или нет
private static final String PREF_DEVICE_TOKEN_SENT = "DeviceTokenSent";

// Возвращает Shared Preferences

public static SharedPreferences getSharedPreferences(Context context) {
    return context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE);
}

// Сохраняет токен в Shared Preferences

public static void setDeviceToken(Context context, String deviceToken) {
    SharedPreferences sp = getSharedPreferences(context);
    SharedPreferences.Editor editor = sp.edit();
    editor.putString(PREF_DEVICE_TOKEN, deviceToken);
    editor.commit();
}

// Достает токен из Shared Preferences

public static String getDeviceToken(Context context) {
    SharedPreferences sp = getSharedPreferences(context);
    return sp.getString(PREF_DEVICE_TOKEN, null);
}

// Возвращает true, если токен отправлен на сервер

public static boolean isDeviceTokenSent(Context context) {
    SharedPreferences sp = getSharedPreferences(context);
    return sp.getBoolean(PREF_DEVICE_TOKEN_SENT, false);
}

// Задает флаг, отправлен токен на сервер или нет

public static void setDeviceTokenSend(Context context, boolean sent) {
    SharedPreferences sp = getSharedPreferences(context);
    SharedPreferences.Editor editor = sp.edit();
    editor.putBoolean(PREF_DEVICE_TOKEN_SENT, sent);
    editor.commit();
}

Теперь пишем метод для создания и сохранения токена устройства. Для получения токена на мобильном устройстве используется Instance ID API:

// Номер проекта в Google Developer Console
public final static String PROJECT_NUMBER = "111233456789";
// Пространство для получение токена. Равно строке "GCM"
public final static String SCOPE = GoogleCloudMessaging.INSTANCE_ID_SCOPE;

// Возвращает токен устройства.

// Если его еще не существует, то создает его и сохраняет в Shared Preferences
public static String createDeviceToken(Context context) {
    String token = getDeviceToken(context);
    if(token == null) {
        token = InstanceID.getInstance(context).getToken(PROJECT_NUMBER, SCOPE, null);
        setDeviceToken(context, token);
    }
    return token;
}

Номер проекта можно найти во вкладке Overview:



Метод getToken в Instance ID API является долгой операцией, следовательно createDeviceToken должен быть выполнен в отдельном потоке.

// Отправляет токен на сервер.
// Если токена не существует, создает его.
public static void createAndSendDeviceToken(final Context context) {
    // Если токен уже отправлен на сервер, то ничего не делаем
    if(isDeviceTokenSent(context)) {
        return;
    }

    new AsyncTask<Void, Void, String>() {

        @Override
        protected String doInBackground(Void... params) {
            try {
                return createDeviceToken(context)
            } catch (IOException e) {
                e.printStackTrace();
                return null;
            }
        }

        @Override

        protected void onPostExecute(String s) {
            super.onPostExecute(s);

            if (s != null) {

                sendTokenToServer(s);
            } else {
                // Действие при ошибке
            }
        }
    }.execute();
}

Осталось реализовать отправку токена на сервер в методе sendTokenToServer. Если токен успешно отправлен, запомните это вызовом метода setDeviceTokenSend(context, true);

четверг, 26 марта 2015 г.

Convert mkv to mp4 on Linux



Ставший популярным видео формат mvk поддерживается не всеми плеерами и редакторами. Однако, для операционных систем Linux доступен инструмент под названием avconv, поставляемый с пакетом libav-tools, с помощью которого можно преобразовывать видео файлы из одного формата в другой.

Установите его следующей командой:

sudo apt-get install libav-tools

Для преобразования файла из формата mkv в mp4 воспользуйтесь командой:

avconv -i input.mkv -codec copy output.mp4

Это автоматически преобразует файл в формат mp4 путем копирования потоков без их перекодирования (-code copy).

среда, 25 марта 2015 г.

Android. Add custom button to SearchView

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


Добавления этой функциональности в приложение хорошо описано в этих статьях:

Однако, добавление собственных кнопок в SearchView не такая уж и простая задача.

Для этого необходимо:
1. Создать экземпляр кнопки;
2. Найти в разметке SearchView элемент, в который можно поместить кнопку;
3. Поместить кнопку в найденный View.

Разметка кнопки


В папке layout создайте xml файл для кнопки, которую нужно поместить в SearchView. Я назвал его search_view_custom_button.xml:

<?xml version="1.0" encoding="utf-8"?>
<ImageButton
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/custom_button"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:layout_gravity="center_vertical"
    android:paddingLeft="16dp"
    android:paddingRight="16dp"
    android:src="@drawable/custom_button_icon"
    android:background="?android:attr/selectableItemBackground"
    android:focusable="true"/>

Задайте кнопке id и изображение (src).
Атрибуту android:background задано стандартное значение "?android:attr/selectableItemBackground", которое обеспечивает анимацию нажатия на кнопку в строке поиска.

Создание экземпляра кнопки


"Надуйте" экземпляр кнопки из только что созданной разметки:

final ImageButton customButton = (ImageButton) getLayoutInflater().inflate(R.layout.search_view_custom_button, null);

Добавление кнопки в SearchView


Разметка SearchView имеет следующую структуру:

<LinearLayout android:id="@+id/search_bar">
  <TextView android:id="@+id/search_badge/> <ImageView android:id="@+id/search_button"/>
  <LinearLayout android:id="@+id/search_edit_frame"
    <ImageView android:id="@+id/search_mag_icon"/>
    <LinearLayout android:id="@+id/search_plate">
      <view class="android.widget.SearchView$SearchAutoComplete"
          android:id="@+id/search_src_text"/>
      <ImageView android:id="@+id/search_close_btn"/>
    </LinearLayout>
    <LinearLayout android:id="@+id/submit_area">
      <ImageView android:id="@+id/search_go_btn"/>
      <ImageView android:id="@+id/search_voice_btn"
    </LinearLayout>
  </LinearLayout>
</LinearLayout>


search_plate - Элемент со строкой поиска и кнопкой очистки.
submit_area - Элемент с кнопкой голосового поиска.

Добавить кнопку можно в любую часть разметки. В рамках примера добавим кнопку рядом с кнопкой голосового поиска:

SearchView searchView = (SearchView) menu.findItem(R.id.search).getActionView();
LinearLayout searchViewSubmitArea = (LinearLayout) searchView.findViewById(R.id.submit_area);
searchViewSubmitArea.addView(searchViewCustomButton);


Для корректного отображения кнопки настройте параметры разметки:

searchViewCustomButton.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT));

Готово! Кнопка добавлена прямо в строку поиска:









среда, 4 февраля 2015 г.

Poltergeist - Can not allocate memory

Poltergeist - замечательный Ruby Gem, который является оберткой для headless браузера PhantomJS. Он позволяет пользоваться этим браузером через Capybara - средство эмуляции пользователя в сети интернет.

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

При длительной работе с ним может возникнуть ошибка: Can not allocate memory - phantomjs. Это происходит из-за того, что он не может автоматически освободить память от сессий, которые уже не используются. Чтобы это предотвратить, используйте метод quit у драйвере сессии, чтобы освободить память:

session.driver.quit

Если по каким-либо причинам это не заработало, создайте файл config/initializers/poltergeist.rb и добавьте в него следующее содержимое:
https://gist.github.com/jonstokes/04b10617a497ec2cc8a8

Это позволит насильно убивать процесс браузера в системе, что поможет освободить неиспользуемую память.

вторник, 3 февраля 2015 г.

Run 32-bit program on a 64-bit system

Бывают случаи, когда нужно запустить 32-битную программу на 64-битной системе. Я столкнулся с этим, когда пытался установить web интерфейс базы данных postgresql.

Решение простое - установить библиотеки совместимости:
sudo apt-get install ia32-libs
Если не помогло, установите так же это:
sudo apt-get install lib32z1 lib32ncurses5 lib32bz2-1.0

вторник, 20 января 2015 г.

Android. How to change action bar title color programmatically

Если Вам необходимо программно поменять цвет заголовка у ActionBar, но мир взбунтовался против Вас и рекомендует делать это разметкой, то добро пожаловать сюда.

Сделать это проще простого.

Заголовок у ActionBar может быть задан строкой из ресурсов, строкой Java или экземпляром класса Spannable.

В Android есть класс SpannableString, наследник Spannable, который позволяет описать разметку и оформление строки.

Воспользуемся им и создадим заголовок необходимого цвета:

String titleText = "Заголовок";
Spannable title = new SpannableString(titleText);
title.setSpan(new ForegroundColorSpan(getResources().getColor(R.color.black)), 0, title.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
actionBar.setTitle(title);

Если смена цвета заголовка связана с меню, добавьте приведенный выше код в метод onCreateOptionsMenu у Activity.

понедельник, 19 января 2015 г.

Android. Save fragment after screen rotation

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

О пользователях нужно заботиться и сохранять введенные значения в формах после поворота экрана.

Причина


Activity во время своей жизни может множество раз менять свое состояние, вплоть до уничтожения и пересоздания. Это как раз и происходит во время поворота экрана. Activity уничтожается и создается снова.

Решение в лоб


Эта проблема довольно легко решается с помощью методов onSaveInstanceState и onRestoreInstanceState у Activity. В первом методе можно сохранить данные в виде пары Ключ/Значение, а во втором восстановить их. Эти данные будут сохранены после уничтожения Activity.

По данной теме можно найти множество материалов.

Однако, этот метод неуместен, если необходимо сохранять большое число данных.

Решение при использовании Fragments


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

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

this.setRetainInstance(true);

После этого скорректируйте код метода onCreate у Activity, чтобы восстановить сохраненный после пересоздания фрагмент:

private static String FRAGMENT_INSTANCE_NAME = "fragment";
MyFragment fragment = null;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.my_activity);

    // Восстанавливаем уже созданный фрагмент
    FragmentManager fm = getSupportFragmentManager();
    fragment = (MyFragment) fm.findFragmentByTag(FRAGMENT_INSTANCE_NAME);
    // Если фрагмент не сохранен, создаем новый экземпляр
    if(fragment == null){
        fragment = new MyFragment();
        fm.beginTransaction().add(R.id.container, fragment, FRAGMENT_INSTANCE_NAME).commit();
    }
}

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

View view = null;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    // Проверяем, создано ли представление фрагмента
    if(view == null) {
        // Если представления нет, создаем его
        view = inflater.inflate(R.layout.my_fragment, container, false);
    } else {
        // Если представление есть, удаляем его из разметки,
        // иначе возникнет ошибка при его добавлении
        ((ViewGroup) view.getParent()).removeView(view);
    }
    return view;
}

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

Внимание


Если вы используете адаптеры, то при повороте экрана они автоматически уничтожат и создадут заново каждый элемент, за который они отвечают. Это произойдет с вызовом метода getView.