среда, 1 июля 2015 г.

Меню

Ruby On Rails

Linux


Android


AngularJS

Android. Get absolute path of image

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

В каждом из этих случаев путь к изображению можно получить из Intent, переданный в метод onActivityResult:

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);

    Uri imagePath = data.getData();
}

Однако если вы захотите использовать этот путь для доступа к файлу, у вас ничего не получится.

Решение

Чтобы получить абсолютный путь к файлу в памяти телефона, воспользуйтесь этим методом:

private String getImagePath(Intent data) {
    Uri path = data.getData();

    String[] filePathColumn = { MediaStore.Images.Media.DATA };

    Cursor cursor = getActivity().getContentResolver().query(path,
            filePathColumn, null, null, null);
    cursor.moveToFirst();

    int columnIndex = cursor.getColumnIndex(filePathColumn[0]);
    String picturePath = cursor.getString(columnIndex);
    cursor.close();

    return picturePath;
}



Android. Upload file to server.

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

Общая теория и проблемы


Обычно для отправки файлов на сервер используется POST запрос протокола HTTP с типом содержимого multipart/form-data.

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

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

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


Реализация


Приведу сразу весь класс для загрузки изображений с комментариями. Класс представляет из себя AsyncTask, в котором выполняется POST запрос с типом содержимого multipart. Передаваемый файл записывается как один элемент multipart запроса.

/**
 * Загружает файл на сервер
 */
public class FilesUploadingTask extends AsyncTask<Void, Void, String> {

    // Конец строки
    private String lineEnd = "\r\n";
    // Два тире
    private String twoHyphens = "--";
    // Разделитель
    private String boundary =  "----WebKitFormBoundary9xFB2hiUhzqbBQ4M";

    // Переменные для считывания файла в оперативную память
    private int bytesRead, bytesAvailable, bufferSize;
    private byte[] buffer;
    private int maxBufferSize = 1*1024*1024;

    // Путь к файлу в памяти устройства
    private String filePath;

    // Адрес метода api для загрузки файла на сервер
    public static final String API_FILES_UPLOADING_PATH = "127.0.0.1/upload";

    // Ключ, под которым файл передается на сервер
    public static final String FORM_FILE_NAME = "file1";

    public FilesUploadingTask(String filePath) {
        this.filePath = filePath;
    }

    @Override
    protected String doInBackground(Void... params) {
        // Результат выполнения запроса, полученный от сервера
        String result = null;

        try {
            // Создание ссылки для отправки файла
            URL uploadUrl = new URL(API_FILES_UPLOADING_PATH);

            // Создание соединения для отправки файла
            HttpURLConnection connection = (HttpURLConnection) uploadUrl.openConnection();

            // Разрешение ввода соединению
            connection.setDoInput(true);
            // Разрешение вывода соединению
            connection.setDoOutput(true);
            // Отключение кеширования
            connection.setUseCaches(false);

            // Задание запросу типа POST
            connection.setRequestMethod("POST");

            // Задание необходимых свойств запросу
            connection.setRequestProperty("Connection", "Keep-Alive");
            connection.setRequestProperty("Content-Type", "multipart/form-data;boundary="+boundary);

            // Создание потока для записи в соединение
            DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream());

            // Формирование multipart контента

            // Начало контента
            outputStream.writeBytes(twoHyphens + boundary + lineEnd);
            // Заголовок элемента формы
            outputStream.writeBytes("Content-Disposition: form-data; name=\"" +
                    FORM_FILE_NAME + "\"; filename=\"" + filePath + "\"" + lineEnd);
            // Тип данных элемента формы
            outputStream.writeBytes("Content-Type: image/jpeg" + lineEnd);
            // Конец заголовка
            outputStream.writeBytes(lineEnd);

            // Поток для считывания файла в оперативную память
            FileInputStream fileInputStream = new FileInputStream(new File(filePath));

            bytesAvailable = fileInputStream.available();
            bufferSize = Math.min(bytesAvailable, maxBufferSize);
            buffer = new byte[bufferSize];

            // Считывание файла в оперативную память и запись его в соединение
            bytesRead = fileInputStream.read(buffer, 0, bufferSize);

            while (bytesRead > 0) {
                outputStream.write(buffer, 0, bufferSize);
                bytesAvailable = fileInputStream.available();
                bufferSize = Math.min(bytesAvailable, maxBufferSize);
                bytesRead = fileInputStream.read(buffer, 0, bufferSize);
            }

            // Конец элемента формы
            outputStream.writeBytes(lineEnd);
            outputStream.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd);

            // Получение ответа от сервера
            int serverResponseCode = connection.getResponseCode();

            // Закрытие соединений и потоков
            fileInputStream.close();
            outputStream.flush();
            outputStream.close();

            // Считка ответа от сервера в зависимости от успеха
            if(serverResponseCode == 200) {
                result = readStream(connection.getInputStream());
            } else {
                result = readStream(connection.getErrorStream());
            }
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (ProtocolException e) {
            e.printStackTrace();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        return result;
    }

    // Считка потока в строку
    public static String readStream(InputStream inputStream) throws IOException {
        StringBuffer buffer = new StringBuffer();
        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));

        String line;
        while ((line = reader.readLine()) != null) {
            buffer.append(line);
        }

        return buffer.toString();
    }
}

Использование


Чтобы загрузить файл на сервер, запустите выполнение AsyncTask:

new FilesUploadingTask(filePath).execute();






вторник, 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

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