понедельник, 26 декабря 2011 г.

Java vs. Android

Мобильная платформа Google поддерживает относительно большое множество функционала Java Standard Edition. Но существуют части J2SE, которые по тем или иным соображениям не включены в Android(к примеру, java.applet, javax.print, java.awt, ...).
Итак, что же общего у Андроида и Java(сравнение с версией Java 2 Platform Standard Edition 5.0)?

Общее:

  • java.io - Файловый и потоковый ввод/вывод
  • java.lang (кроме java.lang.management) - Языковая поддержка и поддержка исключений
  • java.math - Большие числа, округление, точность
  • java.net - Сетевой ввод/вывод, URL-ы, сокеты
  • java.nio - Файловый и канальный ввод/вывод
  • java.security - Авторизация, сертификаты, открытые ключи
  • java.sql - Интерфейс баз данных
  • java.text - Форматирование, естественный язык, сопоставление
  • java.util (включай java.util.concurrent) - Lists, maps, sets, массивы, коллекции
  • javax.crypto - Шифры, открытые ключи
  • javax.net - Socket factories, SSL
  • javax.security (кроме javax.security.auth.kerberos, javax.security.auth.spi, и javax.security.sasl)
  • javax.sound - Звуковые и музыкальные эффекты
  • javax.sql (кроме javax.sql.rowset) - Интерфейсы БД
  • javax.xml.parsers - XML парсинг
  • org.w3c.dom (но не суб-пакеты) - DOM ноды и элементы
  • org.xml.sax - Примеры API для XML

Теперь о том, что не поддерживает платформа Android

Отсутствует поддержка следующих вещей:

  • java.applet
  • java.awt
  • java.beans
  • java.lang.management
  • java.rmi
  • javax.accessibility
  • javax.activity
  • javax.imageio
  • javax.management
  • javax.naming
  • javax.print
  • javax.rmi
  • javax.security.auth.kerberos
  • javax.security.auth.spi
  • javax.security.sasl
  • javax.swing
  • javax.transaction
  • javax.xml (кроме javax.xml.parsers)
  • org.ietf.*
  • org.omg.*
  • org.w3c.dom.* (субпакеты)

И наоборот:

В свою очередь, существуют плюшки, которые Андроид включает в себя, но которые отсутствуют в ствндартных поставках Java 2 Platform Standard Edition 5.0(так называемые, third-party libraries). Среди них следующее:

  • org.apache.commons.codec - Утилиты для кодировки и декодировки
  • org.apache.commons.httpclient - HTTP аутентификация, cookies, методы, и протокол
  • org.bluez - поддержка Bluetooth
  • org.json - JavaScript Object Notation

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

Оригинальная статья датирована 14 января 2008 года. Поэтому, данные могут быть не совсем достоверными. Я постараюсь по мере накопления знаний корректировать свой пост. Если что-то не так - не судите строго) Полезная критика приветствуется.

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

Пишем игру для Android с помощью AndEngine и Physics Box2D Extension

В жизни каждого программиста наступает время, когда ему надоедает писать унылые формы и обработчики и душа просит настоящего творчества.. Например, написать игру.
Давайте посмотрим, как это делается для нашей любимой платформы Android.
Всякая игра базируется на наборе логики, которая реализует поведение игровых объектов на экране, т.н. "графическом движке". В комплексе с этой логикй работает игровая физика, "звуковой" движок, и масса другого, очень непростого кода. Если у нас нет лишних полгода жизни или полмиллиона баксов, лучше не пытаться реализовать эту логику самому. Благо под Android создано уже несколько достаточно мощных "двигателей". Один из них мы сегодня и применим. Это AndEngine - свободно распространяемый 2D игровой движок, базирующийся на OpenGL.
Чтобы было интереснее и ближе к "жизни", мы используем в нашем приложении расширение Physics Box2D, которое позволит нам реализовать в игре гравитацию. Наша игра будет простой но вполне играбильной: мы будем складывать конструкции из деталей, которые будут появляться "из воздуха".
Итак, приступим.

понедельник, 12 декабря 2011 г.

Работаем с очередью RabbitMQ в java


Ничто так не помогает справиться с нагрузкой на web-приложение, как асинхронная обработка. Можете это цитировать :)
Одним из лучших решений для организации очередей сообщений для асинхронной обработки является на сегодня RabbitMQ. Тут я опишу как без особых проблем установить и задействовать в своём java-проекте этот замечательный инструмент.

Итак, устанавливаем:
  1. sudo aptitude install erlang
  2. sudo aptitude install rabbitmq-server
Готово.

Для работы с сервером очередей нам понадобится официальный java-клиент RabbitMQ. Берём его отсюда.
Из архива понадобится "rabbitmq-client.jar" и "commons-io-1.2.jar".

Теперь перед нами стоит классическая задача: реализовать передачу сообщения от одного java-приложения другому. При этом выключение получателя не будет приводить к потере сообщений: пока их не обработают RabbitMQ позаботится о их сохранности. Также мы реализуем многопоточную обработку сообщений получателем. Уверен, что это потребуется, ведь в реальных задачах время обработки сообщения существенно превышает время отправки.
Итак, приступим:

среда, 16 ноября 2011 г.

Простой способ создания Java-приложения с поддержкой плагинов

Все мы знаем, что "монолитное" приложение легче писать только в том случае, если оно небольшое. Если мы планируем расширение приложения, (особенно если делать это будут сторонние разработчики) стоит сразу задуматься о модульной структуре. "Но плагины, это же так сложно!" - думал я раньше. И ошибался. Плагины в java-приложении - это удивительно просто. Чтобы проиллюстрировать это сделаем простое Swing приложение с поддержкой плагинов.

понедельник, 14 ноября 2011 г.

Nginx+Redis: делаем асихронное web-приложение для больших нагрузок

Как работает обычное web-приложение?
Примерно так:
Проходит время, нагрузка растёт и узким местом становится база данных. Разработчики, стараясь снять с неё нагрузку, переходят к асинхронной схеме. Тут база данных не используется в каждом запросе. В основном используется только быстрое noSQL-хранилище или специализированный сервер очередей, для передачи заданий пулу обработчиков. Основную работу эти обработчики выполнят уже позже, когда довольный клиент получит быстрый ответ и пойдёт заниматься своими делами:


Но нагрузка может расти и дальше. Оставим в стороне "горизонтальное" масштабирование при котором мы строим кластера и плодим инстансы приложений - речь сейчас не об этом. Что в последней схеме становится узким местом? База данных уже не в счёт: спрятанная за слоем очередей и обработчиков, кэшей и буферов, она может чувствовать себя спокойно. Обработчики великолепно масштабируются, ведь они "висят" на безразмерной шине очереди. Nginx - один из самых высопроизводительных серверов, за него не беспокоимся. noSQL-хранилища тоже как правило замечтально держат нагрузку и масштабируются.
Что остаётся? К сожалению "крайним" остаётся наше web-приложение. Это оно разбирает запрос, авторизует его, создаёт обьекты, манипулирует с ними, сериализует в базу или в очередь. А потом ещё вычитать данные, сериализовать для ответа... Приложение может содержать неэффективный код, плохо масштабироваться... Кстати, а зачем нам оно вообще нужно? Давайте уберём его из схемы:


 Как же так? Очень просто. Задача получить данные из запроса и уложить в очередь вообще-то тривиальная. И обратная задача тоже. Зачем программировать тривиальные вещи? Всё уж сделано за нас :)
Nginx при помощи HttpRedis2Module может уложить в noSQL-хранилище Redis любые параметры из запроса в виде такого набора ключ-значение, который нам нужен. И вычитать нужный нам набор ключей для возврата клиенту. Вы не используете параметры запросов? У вас обмен с клиентской частью в формате JSON? Нет проблем! Используя set-misc-nginx-module мы можем прямо в конфиге nginx-а описать правила получения данных из запроса: UrlDecode, JSONDecode, Base64Decode и т.п.

Теперь посмотрим, как настроить такое "сверхтонкое" web-приложение:

воскресенье, 30 октября 2011 г.

Android: Как изменить размер BitmapDrawable

Недавно мне понадобилось получить из внешнего потока (InputStream) картинку и вывести ее на экран, но картинка была в одном размере и ее нужно было под разные экраны и условия уменьшить или увеличить.

Для начала получим из потока Drawable. Например с assets каталога:
  Drawable drawable = Drawable.createFromStream(act.getAssets().open(fileName), null);
И для того, чтобы изменить размер, я использую следующий метод:
  public static Drawable resizeDrawable(Drawable drawable, int newWidth, int newHeight) {
    Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
    int width = bitmap.getWidth();
    int height = bitmap.getHeight();
    float scaleWidth = ((float) newWidth) / width;
    float scaleHeight = ((float) newHeight) / height;
    Matrix matrix = new Matrix();
    matrix.postScale(scaleWidth, scaleHeight);
    Bitmap resizedBitmap = Bitmap.createBitmap(bitmap, 0, 0,
        width, height, matrix, true);
    return new BitmapDrawable(resizedBitmap);
  }
Этот метод я запостил скорей чтобы не забыть, потому как в проекте он уже не нужен, и дабы помочь ищущему. Но я думаю есть другие варианты решения, если кто наткнулся на пост и знает, то плиз в комментарии.


вторник, 18 октября 2011 г.

Простой инсталлятор для Linux средствами bash

Если вам приходилось ставить JDK на вашу Linux-машину, то вы знакомы с этим способом развёртывания приложения. И, если теперь ваша замечательная новая программа должна отправиться к благодарным пользователям, то почему бы не облегчить им жизнь с помощью простого "инсталлятора"? Вот увидите, клиенты это оценят.

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

Как это сделать?
Сначала создаём скрипт, а потом с помощью команды cat добавляем в него архив с файлами.
Например install.sh:
  1. #!/bin/bash
  2. A_F=`readlink -e "$0"`
  3. DIR=`dirname "$A_F"`
  4. TMP_ARH="tmp.tar.gz"
  5. cd $DIR
  6. echo "Creating temporary arhive $TMP_ARH"
  7. tail -n +15 "$0" > $TMP_ARH
  8. echo "Unpacking temporary arhive $TMP_ARH"
  9. tar xzf $TMP_ARH
  10. echo "Removing temporary arhive $TMP_ARH"
  11. rm -f $TMP_ARH
  12. echo "Installation is complete!"
  13. exit 0
  14. ######
Тут делаем следующее:

  • В строках 2-3 определяем директорию, в которой в данный момент находится скрипт, в строке 5 переходим туда. Все дальнейшие операции выполняем относительно этой директории. 
  • В строке 4 задаём имя временного архива.
  • В строке 7 "отрезаем" бинарную часть нашего скрипта (в данном случае она начинается со строки 15), которую добавим к скрипту позже. Бинарную часть сохраняем как временный архив. 
  • В строке 9 распаковываем архив в текущую директорию и удаляем его в строке 11
  • Завершаем выполнение скрипта в строке 13 (чтобы bash не решил выполнить и наши бинарные данные, расположенные ниже).
Как видно из скрипта в строке 15 начинается архив с нашим приложением. Добавим его:
cat my_app.tar.gz >> install.sh
После команды извлечения данных в вашем "инсталляторе" вы сможете сделать также все необходимые симлинки и выполнить другие операции по настройке системы. 

воскресенье, 16 октября 2011 г.

Расширим DAO c Hibernate Generic D.A.O. Framework

Когда я только начал изучать Hibernate я делал на каждую свою сущность отдельный DAO. Вскоре я наткнулся на популярную в сети статью "Не повторяйте DAO", которая используя Spring и дженерики создает обобщенный типизированный DAO. И через некоторое время данный подход возродился в некий фреймворк, который может работать с любой сущностью или коллекцией сущностей. Но смысла нету показывать мои костыли, если есть Hibernate Generic D.A.O. Framework, про который можно прочитать здесь, а я постараюсь показать пример его использования. Правда DAO нужно будет всеравно создавать для каждой сущности, но зато мы получим общие методы и интересный подход к составлению запросов в БД.

вторник, 11 октября 2011 г.

Как получить mime-type файла в Java

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

Способ первый: не заглядываем внутрь
Проще всего определить тип файла по его расширению. Знакомая с детства, понятная, быстрая и весьма неточная схема. Собственно, точность зависит от того, насколько полным и актуальным является используемый нами справочник расширений. Чтобы не хардкодить свой справочник используем, например javax.activation.MimetypesFileTypeMap:
import javax.activation.MimetypesFileTypeMap;
import java.io.File;

class GetMimeType {
 public static void main(String args[]) {
  File f = new File("gumby.gif");
  System.out.println("Mime Type of " + f.getName() + " is " +
             new MimetypesFileTypeMap().getContentType(f));
  // expected output :
  // "Mime Type of gumby.gif is image/gif"
 }
}

Для работы этого кода потребуется подключить библиотеку activation.jar, которую можно взять отсюда.
Библиотека ищет данные о типе файла в нескольких местах системы в следующем порядке:
  1. Programmatically added entries to the MimetypesFileTypeMap instance.
  2. The file .mime.types in the user's home directory.
  3. The file /lib/mime.types.
  4. The file or resources named META-INF/mime.types.
  5. The file or resource named META-INF/mimetypes.default (usually found only in the activation.jar file). 
Способ второй: Чтобы понять что это, соединимся с этим
Способ более надёжный, не требует дополнительных библиотек, но не рекомендуется к использованию в "промышленных масштабах" ввиду крайней медлительности.
import java.net.*;

public class FileUtils{
 public static String getMimeType(String fileUrl)
  throws java.io.IOException, MalformedURLException
 {
  String type = null;
  URL u = new URL(fileUrl);
  URLConnection uc = null;
  uc = u.openConnection();
  type = uc.getContentType();
  return type;
 }

 public static void main(String args[]) throws Exception {
  System.out.println(FileUtils.getMimeType("file://c:/temp/test.TXT"));
  // output : text/plain
 }
}

Тут мы практически открываем соединение с файлом по протоколу file:// и читаем заголовок content-type из полученного соединения.
Чуть более быстрый способ:

import java.net.FileNameMap;
import java.net.URLConnection;

public class FileUtils {

 public static String getMimeType(String fileUrl)
   throws java.io.IOException
  {
   FileNameMap fileNameMap = URLConnection.getFileNameMap();
   String type = fileNameMap.getContentTypeFor(fileUrl);

   return type;
  }

  public static void main(String args[]) throws Exception {
   System.out.println(FileUtils.getMimeType("file://c:/temp/test.TXT"));
   // output : text/plain
  }
 }
Принцип тут тот же.

Способ третий: Посмотрим-таки внутрь!
Самый надёжный способ, определяющий тип файла по его содержимому. Точен настолько, насколько это возможно, достаточно быстр. Но требует использования большого числа  сторонних библиотек. Например, используя Apache Tika:

import java.io.File;
import java.io.FileInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.AutoDetectParser;
import org.apache.tika.parser.Parser;
import org.apache.tika.sax.BodyContentHandler;
import org.xml.sax.ContentHandler;

public class Main {

  public static void main(String args[]) throws Exception {

  FileInputStream is = null;
  try {
   File f = new File("C:/Temp/mime/test.docx");
   is = new FileInputStream(f);

   ContentHandler contenthandler = new BodyContentHandler();
   Metadata metadata = new Metadata();
   metadata.set(Metadata.RESOURCE_NAME_KEY, f.getName());
   Parser parser = new AutoDetectParser();
   // OOXMLParser parser = new OOXMLParser();
   parser.parse(is, contenthandler, metadata);
   System.out.println("Mime: " + metadata.get(Metadata.CONTENT_TYPE));
   System.out.println("Title: " + metadata.get(Metadata.TITLE));
   System.out.println("Author: " + metadata.get(Metadata.AUTHOR));
   System.out.println("content: " + contenthandler.toString());
  }
  catch (Exception e) {
   e.printStackTrace();
  }
  finally {
    if (is != null) is.close();
  }
 }
}
Библиотека - часть поискового движка Lucene - подключает в рантайме около 20 (!) зависимостей (в общей сложности мегабайт на 18). Смотрите сами, нужно ли оно вам в проекте.

Более компактный вариант - использовать библиотеку JMimeMagic:

  public static String getMime(String path) {
    MagicMatch match = null;
    try {
      match = Magic.getMagicMatch(new File(path), true);
    } catch (Exception e){
      e.printStackTrace(System.err);
    }
    return match.getMimeType();
  }
Плюсы: всего две зависимости: Apache Common logging и Jakarta Oro (уже не разрабатывается). Вместе с зависимостями размер библиотеки едва превышает 150 Kb.
Минусы: сравнительно невысокая точность - "не узнаёт" несколько популярных форматов файлов, иногда "убивает" наше приложение с ошибкой OutOfMemoryError. На исправление ошибок вряд ли стоит рассчитывать: последняя версия вышла в 2006 году.

...или библиотеку Mime-Util:

import eu.medsea.mimeutil.MimeUtil;

public class Main {
  public static void main(String[] args) {
    MimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector");
    File f = new File ("c:/temp/mime/test.doc");
    Collection<?> mimeTypes = MimeUtil.getMimeTypes(f);
    System.out.println(mimeTypes);
    // output : application/msword
  }
}
Минусы: несколько больший размер, есть проблема с "узнаванием" текстовых файлов: все они (включая html) воспринимаются как "application/octet-stream". Плюсы: достаточно высокая точность определения типов, всего одна зависимость (SLF4J), более стабильная работа.

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

Изменение языка интерфейса в NetBeans 7.0+

После выхода 7.0 версии этой чудесной среды появилась весьма хорошая русская локализация. Но мне как и большинству программистам, ближе английский язык в IDE. Решив сменить язык интерфейса я полез в настройки, но к сожалению не нашел ни одного намека на данную функцию. После я прошелся по просторам интернета и нашел вот такие варианты решения:

1. Добавить в строку запуска параметр --locale en:US (FAQ)
Пример :  С:\Program Files\NetBeans 7.0.1\bin\netbeans.exe --locale en:US

2. Добавить --locale en_US в параметр netbeans_default_options в файл <netbeans folder>\etc\netbeans.conf
Пример (в файле С:\Program Files\NetBeans 7.0.1\etc\netbeans.conf):
netbeans_default_options=".... --locale en_US"

З.Ы. Данные решения можно применять как в Windows так и в Linux.

вторник, 4 октября 2011 г.

JSch: Работаем с удалённой машиной по ssh из java-приложения

Хочу рассказать об одной интересной находке: библиотеке JSch. Это реализация ssh соединения на java.  Не уникальная, само собой, но из полудесятка пересмотренных мной, именно она понравилась больше всего.
Возможности весьма впечатляют: аворизация по ключам, порт-форвардинг, соединение через прокси и ещё много чего. Также обрадовала простота работы с библиотекой. Вот, к примеру, реализация удалённой консоли в три десятка строк:

import com.jcraft.jsch.*;
import javax.swing.*;

public class Shell {

  public static void main(String[] arg) {
    try {
      JSch jsch = new JSch();
      String connstr = null;
      if (arg.length > 0) {
        connstr = arg[0];
      } else {
        connstr = JOptionPane.showInputDialog("Enter username:password@hostname", System.getProperty("user.name")+":<pass>"+"@localhost");
      }
      String user = connstr.substring(0, connstr.indexOf(':'));
      String pass = connstr.substring(connstr.indexOf(':')+1, connstr.indexOf('@'));
      String host = connstr.substring(connstr.indexOf('@') + 1);
      Session session = jsch.getSession(user, host, 22);
      session.setPassword(pass);
      session.setConfig("StrictHostKeyChecking", "no");
      session.connect();
      Channel channel = session.openChannel("shell");
      channel.setInputStream(System.in);
      channel.setOutputStream(System.out);
      channel.connect();
    } catch (Exception e) {
      System.out.println(e);
    }
  }
}

Тут мы делаем следующее:
  • Получаем от пользователя настройки подключения (как параметр вызова или диалоговым окном)
  • Создаём сессию и устанавливаем соединение
  • Создаём Channel (в данном случае для интерактивного исполнения команд, хотя есть и другие варианты)
  • "Заворачиваем" системные потоки ввода и вывода в полученный канал
  • наслаждаемся :)

понедельник, 3 октября 2011 г.

Упаковка и распаковка архива на Java

Java без посторонних библиотек поддерживает работу с несколькими видами архивов, в том числе и с самым популярным - Zip. Для этого используются классы из пакета java.util.zip. Давайте посмотрим, как с помощью этих инструментов можно упаковать и распаковать набор каталог с файлами и каталогами.
Упаковка:

  public static void pack(File directory, String to) throws IOException {
    URI base = directory.toURI();
    Deque<File> queue = new LinkedList<File>();
    queue.push(directory);
    OutputStream out = new FileOutputStream(new File(to));
    Closeable res = out;

    try {
      ZipOutputStream zout = new ZipOutputStream(out);
      res = zout;
      while (!queue.isEmpty()) {
        directory = queue.pop();
        for (File child : directory.listFiles()) {
          String name = base.relativize(child.toURI()).getPath();
          if (child.isDirectory()) {
            queue.push(child);
            name = name.endsWith("/") ? name : name + "/";
            zout.putNextEntry(new ZipEntry(name));
          } else {
            zout.putNextEntry(new ZipEntry(name));
            InputStream in = new FileInputStream(child);
            try {
              byte[] buffer = new byte[1024];
              while (true) {
                int readCount = in.read(buffer);
                if (readCount < 0) {
                  break;
                }
                zout.write(buffer, 0, readCount);
              }
            } finally {
              in.close();
            }
            zout.closeEntry();
          }
        }
      }
    } finally {
      res.close();
    }
  }



Этот метод интересен тем, что, в отличие от большинства методов для манипуляции с файлами, он нерекурсивный. Тут для обработки вложенных директорий используется очередь. Кстати, метод не мой, взят отсюда.
Обратный процесс также реализуем нерекурсивным методом.Проходим по всем entry в архиве, директории сразу создаём, файлы добавляем в очередь. Потом проходим по очереди и создаём файлы, копируя их из ZipInputStream в FileOutputStream:

  public static void unpack(String path, String dir_to) throws IOException {
    ZipFile zip = new
ZipFile(path);
    Enumeration entries = zip.entries();
    LinkedList<ZipEntry> zfiles = new LinkedList<ZipEntry>();
    while (entries.hasMoreElements()) {
      ZipEntry entry = (ZipEntry) entries.nextElement();
      if (entry.isDirectory()) {
        new File(dir_to+"/"+entry.getName()).mkdir();
      } else {
        zfiles.add(entry);
      }
    }
    for (ZipEntry entry : zfiles) {
      InputStream in = zip.getInputStream(entry);
      OutputStream out = new FileOutputStream(dir_to+"/"+entry.getName());
      byte[] buffer = new byte[1024];
      int len;
      while ((len = in.read(buffer)) >= 0)
        out.write(buffer, 0, len);
      in.close();
      out.close();
      }
    zip.close();
  }

вторник, 27 сентября 2011 г.

Операции с файлами в Java

На днях делал небольшой файловый менеджер и пришлось погрузиться в манипуляции с файлами. Задача в принципе тривиальная, если не работать с директориями или использовать Apache Commons IO. Но для проекта "весом" в 60 Kb использовать 155 Kb библиотеку для вызова трёх методов... Не наш это путь, верно? Сделаем всё сами.
Начнём с удаления файлов:

  public static void delete(String path_from) {
    File f = new File(path_from);
    if (f.isDirectory()) {
      String[] child = f.list();
      for (int i = 0; i < child.length; i++) {
        delete(path_from + "/" + child[i]);
      }
      f.delete();
    } else {
      f.delete();
    }
  }



Это рекурсивный метод: Он вызывает сам себя для каждого вложенного файла или директории, а затем удаляет и саму директорию. файл он удаляет сразу.
Копирование реализуется ненамного сложнее:

  public static void copy(String from, String to) throws FileNotFoundException, IOException {
    String fname = from.substring(from.lastIndexOf("/") + 1);
    File f = new File(from);
    if (f.isDirectory()) {
      if (new File(to + "/" + fname).exists()) {
        throw new IOException("Directory " + to + "/" + fname + " already exists");
      }
      mkdir(to + "/" + fname);
      String[] child = f.list();
      for (int i = 0; i < child.length; i++) {
        copy(from + "/" + child[i], to + "/" + fname);
      }
    } else {
      if (new File(to + "/" + fname).exists()) {
        throw new IOException("File " + to + "/" + fname + " already exists");
      }
      FileChannel srcChannel = new FileInputStream(f).getChannel();
      FileChannel destChannel = new FileOutputStream(to + "/" + fname).getChannel();
      srcChannel.transferTo(0, srcChannel.size(), destChannel);
    }
  }


Тут добавлены проверки на существование файла и директории перед их созданием. Копирование файла через FileChannel предпочтительнее ибо быстрее.

пятница, 26 августа 2011 г.

Android: делаем холст для рисования

Допустим вы решили сделать маленький Paint для Android. Например, для развития художественных талантов у своего ребёнка :). Основой такого приложения будет холст: некая View, которая может "ощущать" прикосновения и отображать на себе путь пальца. Для реализации таких функций создаём класс, расширяющий View и переопределяем у него два метода: onTouchEvent (позволит перехватывать движения пальца) и onDraw (позволит рисовать).

  1. public class PaintView extends View {
  2.  
  3.   
  4.   private int linecolor, alpha, lwidth;
  5.   private int WIDTH;
  6.   private int HEIGHT;
  7.   MainActivity act;
  8.  
  9.   public PaintView(MainActivity act, int linecolor, int alpha, int lwidth, int width, int height) {
  10.     super(act);
  11.     this.act = act;
  12.     this.linecolor = linecolor;
  13.     this.alpha = alpha;
  14.     this.WIDTH = width;
  15.     this.HEIGHT = height;
  16.     this.lwidth = lwidth;
  17.   }
  18.   
  19.   @Override
  20.   public boolean onTouchEvent(MotionEvent e) {
  21.  
  22.     int action = e.getAction();
  23.  
  24.     if (action == MotionEvent.ACTION_UP) {
  25.       act.addPoint(null);
  26.  
  27.     } else if (action == MotionEvent.ACTION_MOVE || action == MotionEvent.ACTION_DOWN) {
  28.       Point point = new Point(Math.round(e.getX()), Math.round(e.getY()), linecolor, alpha, lwidth);
  29.       act.addPoint(point);
  30.       postInvalidate();
  31.     }
  32.  
  33.     return true;
  34.   }
  35.  
  36.   @Override
  37.   protected void onDraw(Canvas c) {
  38.     Point curr = null;
  39.     for (Point data : act.getPoints()) {
  40.       if (curr != null && data != null) {
  41.         Paint paint = new Paint();
  42.         paint.setColor(data.getColor());
  43.         paint.setAlpha(data.getAlpha());
  44.         if (data.getWidth()==1) {
  45.           c.drawLine(curr.getX(), curr.getY(), data.getX(), data.getY(), paint);
  46.         } else {
  47.           c.drawRect(curr.getX(), curr.getY(), data.getX()+data.getWidth(), data.getY()+data.getWidth(), paint);
  48.         }
  49.       }
  50.       curr = data;
  51.     }
  52.   }
  53.  
  54.   @Override
  55.   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  56.     super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  57.     this.setMeasuredDimension(WIDTH, HEIGHT);
  58.   }
  59. }
Рассмотрим подробнее метод onTouchEvent. Для событий прикосновения и перемещения мы получаем координаты точки на экране и создаём экземпляр Point, добавляя его в ArrayList с помощью метода addPoint(point) в MainActivity. Почему не хранить коллекцию точек прямо в классе? Так проще обрабатывать события, требующие перерисовки холста. Просто создаём новый холст, а данные для отрисовки он возьмёт в MainActivity.
Класс Point выглядит просто:
public class Point {
  private int x, y, color, alpha, width;

  public Point(int x, int y, int color, int alpha, int width) {
    this.x = x;
    this.y = y;
    this.color = color;
    this.alpha = alpha;
    this.width = width;
  }

  public int getX() {
    return x;
  }

  public int getY() {
    return y;
  }

  public int getColor() {
    return color;
  }

  public int getAlpha() {
    return alpha;
  }

  public int getWidth() {
    return width;
  }
}

...это обычное хранилище координат точки и её характеристик.
Вызываем наш холст из MainActivity следующим образом:
PaintView paint = new PaintView(this, initColor, alpha, linew, w, h);
Тут:
this - экземпляр MainActivity, из которого, собственно делаем вызов
initColor, alpha, linew - настройки цвета, прозрачности и толщины линии
w и h - ширина и высота доступной для рисования области в пикселях. Получаем её так:
    Resources res = getResources();
    int h = res.getDisplayMetrics().heightPixels;
    int w = res.getDisplayMetrics().widthPixels;

Созданный экземпляр холста может быть добавлен в нашу структуру View с помощью метода addView родительского контейнера а контейнер верхнего уровня устанавливаем как контент окна методом setContentView.
Останется только добавить в наше приложение панель с кнопками для выбора цвета, толщины линий и т.п. и простенькая "рисовалка" готова :).

пятница, 15 июля 2011 г.

Android: Управляем виртуальной клавиатурой программно

В современных Android-смартфонах виртуальная (экранная) клавиатура очень полезный инструмент - это мало кто поставит под сомнение. А для разработчика она приятна ещё и своей гибкостью. Тут я приведу пару приёмов по управлению этим "девайсом".

Изменить вид клавиатуры для данного EditText-а:
EditText ipt = new EditText(this);
ipt.setInputType(InputType.TYPE_CLASS_PHONE);  - установит клавиатуру для ввода номера телефона. Другие варианты:
TYPE_CLASS_DATETIME - дата и время
TYPE_CLASS_NUMBER - цифры
TYPE_CLASS_TEXT - буквы

Убрать клавиатуру с экрана:
Context context = getApplicationContext();
InputMethodManager imm = (InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(edit.getWindowToken(), 0);
Тут edit - EditText для которого нужно скрыть клавиатуру.
Код так выглядит, если писать его в Activity. Если расположить его в другом классе, экземпляр Activity нужно передать туда как параметр и вызывать методы как  act.getApplicationContext(), где act - экземпляр Activity

вторник, 12 июля 2011 г.

Пишем виджет для Android

В сети есть достаточно много инструкций по написанию виджетов для Android. Большая часть из них "HelloWorld-ы", в остальных местах упущены какие-то важные мелочи... В общем давайте разберём создание виджета "по взрослому": с фоновыми процессами, обработкой кликов, локализацией. Пусть наш виджет получает курс валюты Bitcoin по отношению к доллару из API биржы Mt. Gox и отображает его текущее значение. В результате мы получим что-то вроде этого.

Во-первых, чем виджет отчается от Activity?
Виджет работает в рамках "рабочего стола" нашего смартфона и от этого имеет как плюсы так и минусы. Плюсы: можно настроить обновление средствами системы. Интервал обновления при этом не может быть меньше получаса (180000 ms). Приложение всегда на виду: клиенту можно что-то сообщать не дожидаясь его действий. Минусы: ограниченный набор компонентов, доступных для использования в интерфейсе (из компоновщиков можно использовать только  "FrameLayout", "LinearLayout" и "RelativeLayout". Из View: "AnalogClock", "Button", "Chromometer", "ImageButton", "ImageView", "ProgressBar" и "TextView".). Также ограничено время, которое отводится на исполнение запросов. Ну, и, само собой, ограниченный размер "рабочей площади". Также достаточно сложно реализовать обработку событий в виджете: onclickListener на кнопку в виджете "повесить" не получится.

Как регистрировать виджет?
Так же как и всё остальное в нашем приложении, виджет описывается в AndroidManifest.xml. В тег application добавляем структуру:

    <receiver android:name=".CourceWidget" android:label="@string/app_label">
      <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
      </intent-filter>
      <meta-data android:name="android.appwidget.provider" android:resource="@xml/widget_cource" />
    </receiver>

Тут .CourceWidget - класс, описывающий виджет, widget_cource - xml файл (фактически:  res/xml/widget_cource.xml), описывающий параметры виджета.

Как описать параметры виджета?
Вот, например, так:
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
  android:minWidth="146dip"
  android:minHeight="72dip"
  android:updatePeriodMillis="1800000"
  android:initialLayout="@layout/cource_message"
  />

Тут android:minWidth и android:minHeight соответственно ширина и высота виджета. Рекомендуется приводить размеры по формуле: число пикселей = (число ячеек * 74) – 2. Одну ячейку на "рабочем столе" занимает одна иконка. Значение android:updatePeriodMillis, как уже говорили, определяет период обновления виджета в миллисекундах. В атрибуте  android:initialLayout указываем ссылку на xml файл (res/layout/cource_message.xml) с описанием интерфейса виджета.

Как описать интерфейс виджета?
Вот, например, так:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="fill_parent"
  android:layout_height="wrap_content"
  android:gravity="center"
  android:background="@drawable/widget_bg">
  <LinearLayout
    android:orientation="vertical"
    android:gravity="center"
    android:layout_width="wrap_content"
    android:layout_height="fill_parent">
    <TextView
      android:layout_marginTop="10sp"
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"
      android:textColor="#000000"
      android:gravity="center"
      android:text="@string/buy" />
    <TextView
      android:id="@+id/message_b"
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"
      android:textColor="#ff0000"
      android:textSize="16sp"
      android:textStyle="bold"
      android:gravity="center"
      android:text="@string/loading" />
    <TextView
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"
      android:textColor="#000000"
      android:gravity="center"
      android:text="@string/sell" />
    <TextView
      android:id="@+id/message_s"
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"
      android:textColor="#ff0000"
      android:textSize="16sp"
      android:textStyle="bold"
      android:gravity="center"
      android:text="@string/loading" />
  </LinearLayout>    
</LinearLayout>

Это обычный LinearLayout с четырьмя TextView, два из которых содержат строковые константы ("покупка" и "продажа") а другие два заполняются данными, полученными из API. Строковые константы для поддержки "мультиязычности" выносим в файл res/values/strings.xml (значения по умолчанию - английская локализация) и res/values-ru/strings.xml (русская локализация).

Как обеспечить локализацию виджета?
Как сказано выше: вынести все константы в xml-файл вида
<resources>
  <string name="app_name">Exchange rate of Bitcoin</string>
  <string name="app_label">Bitcoin:USD exchange rate</string>
  <string name="buy">Buy:</string>
  <string name="sell">Sell:</string>
  <string name="loading">Loading</string>
  <string name="err_connect">Connect error</string>
</resources>

... и обращаться к ним как: @string/loading (вернёт "Loading"). Система сама выберет нужный файл с константами в зависимости от настроек языка в системе. В java-коде обращение к локализованным строковым константам будет выглядеть так: R.string.loading.

Как описать логику, которая должна выполняться при обновлении виджета?
Вот тут мы, наконец-то, начинаем писать java-код. Для описания логики обновления данных мы должны реализовать класс, имя которого указано в самом первом xml-конфиге, приведённом в этой статье. Это CourceWidget.java:

  1. public class CourceWidget extends AppWidgetProvider {
  2.  
  3.   @Override
  4.   public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
  5.     context.startService(new Intent(context, UpdateService.class));
  6.   }
  7.  
  8.   public static class UpdateService extends Service {
  9.  
  10.     @Override
  11.     public void onStart(Intent intent, int startId) {
  12.       RemoteViews updateViews = buildUpdate(this);
  13.       AppWidgetManager.getInstance(this).updateAppWidget(new ComponentName(this, CourceWidget.class), updateViews);
  14.     }
  15.  
  16.     /**
  17.      * Build a widget update
  18.      */
  19.     public RemoteViews buildUpdate(Context context) {
  20.  
  21.       RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.cource_message);
  22.  
  23.       String buy = "";
  24.       String sell = "";
  25.  
  26.       // get gata from JSON
  27.       try {
  28.         JSONObject resp = new JSONObject(Http.Request("https://mtgox.com/code/data/ticker.php"));
  29.         buy = resp.getJSONObject("ticker").getString("buy");
  30.         sell = resp.getJSONObject("ticker").getString("sell");
  31.       } catch (Exception e) {
  32.         buy = "...";
  33.         sell = "...";
  34.         e.printStackTrace(System.err);
  35.         Toast.makeText(this, R.string.err_connect, Toast.LENGTH_SHORT).show();
  36.       }
  37.       views.setTextViewText(R.id.message_b, "$" + buy);
  38.       views.setTextViewText(R.id.message_s, "$" + sell);
  39.       return views;
  40.     }
  41.  
  42.     @Override
  43.     public IBinder onBind(Intent intent) {
  44.       return null;
  45.     }
  46.   }
  47. }
Наш класс расширяет AppWidgetProvider, переопределяя его метод onUpdate. В этом методе мы ничего не делаем, кроме одного: мы запускаем сервис UpdateService, описанный тут же, как вложенный класс.

Что такое сервис и зачем он нам нужен?
В контексте виджета мы не можем обращаться к сетевым ресурсам: время обновления "рабочего стола" должно быть минимальным. Сервис - это фоновый процесс, который Android исполняет в отдельном потоке настолько долго, насколько это нам нужно. Чтобы зарегистрировать сервис в AndroidManifest.xml внутри тега application добавим:
<service android:name=".CourceWidget$UpdateService" />

Как сервис получит данные из API?
Очень просто. Отправив get-запрос, мы получаем строку, которая на самом деле представляет собой JSON-структуру. Парсинг JSON выполняем при помощи классов org.json.* которые доступны без подключения всяких сторонних библиотек. Передаём полученную из API строку в конструктор объекта JSONObject. Методами этого объекта мы получаем вложенные объекты и их поля, т.е. значения из ответа (строки 28-30). В случае любой ошибки присваиваем переменным значения по умолчанию.

Как сервис передаёт данные в виджет?
У виджета нельзя обратиться к отдельному элементу View, чтобы изменить его свойства. Можно только заменить всю иерархию компонентов целиком. Для этого создаём объект RemoteViews (строка 21) и "вкладываем" в него полученные из API данные с помощью метода setTextViewText. Потом иерархию с установленными свойствами выдаём в виджет с помощью метода updateAppWidget (строка 13).

И напоследок: как обрабатывать нажатие на виджет?
Тут мы с сожалением вспомним удобные Listener-ы из Activity. В виджетах всё существенно сложнее. Добавим в наш класс CourceWidget константу:
public static String ACTION_WIDGET_RELOAD = "reload";
Затем в нашем сервисе создадим событие:

Intent active = new Intent(context, CourceWidget.class);
active.setAction(ACTION_WIDGET_RELOAD);
PendingIntent actionPendingIntent = PendingIntent.getBroadcast(context, 0, active, 0);
... и зарегистрируем его (привязав к какому-нибудь id элемента из нашей иерархии RemoteViews):
views.setOnClickPendingIntent(R.id.reload, actionPendingIntent);
В данном случае мы "навешиваем" событие на элемент с id "reload".
Чтобы получить и обработать событие в классе CourceWidget переопределяем метод:
  @Override
  public void onReceive(Context context, Intent intent) {
    // Receive event
    final String action = intent.getAction();
    if (ACTION_WIDGET_RELOAD.equals(action)) {
      context.startService(new Intent(context, UpdateService.class));
    }
    super.onReceive(context, intent);
  }

Тут мы заново стартуем сервис прои нажатии на виджет, тем самым вызывая "внеочередное" обновление данных.

P.S: по многочисленным просьбам даю ссылку на исходники моего проекта, "по мотивам" которого была написана эта статья.

среда, 6 июля 2011 г.

Строим диаграмму в Android-приложении

Допустим, вы решили сделать виджет, который будет отображать текущий курс валют. Вы идёте на любой открытый источник, берёте данные и выводите клиенту. Ничего сложного. Но и ничего особенного - таких виджетов миллион. Вы идёте дальше: принимаете решение хранить полученные данные за предыдущие дни и отображать клиенту информацию в виде диаграммы. Вот тут-то мы и сталкиваемся с реализацией 2D-графики в Android.
Чтобы создать объект изображения, нужно переопределить View. Назовём наш объект ChartView:

  1. public class ChartView extends View {
  2.   private int width = 10;
  3.   ArrayList<ChartItem> charts;
  4.   int max = 0;
  5.   ShapeDrawable background;
  6.   
  7.   public ChartView(Context context, ArrayList<ChartItem> charts, int top, int bgColor) {
  8.     super(context);
  9.     Resources res = getResources();
  10.     int ws = res.getDisplayMetrics().widthPixels;
  11.     
  12.     ArrayList<ChartItem> gencharts = new ArrayList<ChartItem>();
  13.     for (ChartItem ci : charts) {
  14.       int[] points = scaleTo(ci.getPoints(), top);
  15.       ArrayList<ShapeDrawable> myDraws = new ArrayList<ShapeDrawable>();
  16.       for (int i=0; i<points.length; i++) {
  17.         int h = points[i];
  18.         ShapeDrawable mDrawable = new ShapeDrawable(new RectShape());
  19.         mDrawable.getPaint().setColor(ci.getColor());
  20.         mDrawable.getPaint().setAlpha(ci.getAlpha());
  21.         mDrawable.setBounds(width*i, top-h, width*(i+1), top);
  22.         mDrawable.getPaint().setShader(new LinearGradient(width*i, top-h, width*(i+1), top-h, ci.getColor(), Color.WHITE, Shader.TileMode.REPEAT));
  23.         myDraws.add(mDrawable);
  24.       }
  25.       ci.setDraws(myDraws);
  26.       gencharts.add(ci);
  27.     }
  28.     this.charts = gencharts;
  29.     background = new ShapeDrawable(new RectShape());
  30.     background.setBounds(0, 0, ws, top);
  31.     background.getPaint().setColor(bgColor);
  32.   }
  33.  
  34.   @Override
  35.   protected void onDraw(Canvas canvas) {
  36.     background.draw(canvas);
  37.     for (ChartItem ci : charts) {
  38.       ArrayList<ShapeDrawable> draws = ci.getDraws();
  39.       for (int i=0; i<draws.size(); i++) {
  40.         ShapeDrawable d = draws.get(i);
  41.         d.draw(canvas);
  42.       }
  43.     }
  44.   }
  45.   
  46.   private int getMax(int[] ii) {
  47.     for (int i : ii) {
  48.       if (max<i) max = i;
  49.     }
  50.     return max;
  51.   }
  52.   
  53.   private int[] scaleTo(int[] ii, int vmax) {
  54.     int[] scaled = new int[ii.length];
  55.     double k = (double) vmax / getMax(ii);
  56.     for(int i=0; i<ii.length; i++) {
  57.       scaled[i] = (int) (ii[i]*k);
  58.     }
  59.     return scaled;
  60.   }
  61. }
Рассмотрим наш класс подробнее.
В конструктор обязательно нужно передавать Context - его мы сразу же отдадим в конструктор предка. Кроме этого в конструктор мы передаём массив объектов ChartItem, которые представляют данные для каждой из диаграмм, которые мы собираемся построить на нашем изображении. Следующие два параметра общие для всех диаграмм на изображении: максимальная высота и фон. 
В строках 9-10 получаем текущую ширину экрана, чтобы полностью закрасить её фоном. 
Все графические примитивы создаются как объекты ShapeDrawable. В конструкторе мы устанавливаем параметры для прямоугольников, составляющих столбцы диаграмм: ширину, высоту, цвет. Делаем их симпатичнее с помощью градиента (строка 22). Все созданные объекты должны быть нарисованы на канве, для чего переопределяем метод protected void onDraw(Canvas canvas) и в нём для всех ShapeDrawable вызываем метод draw(canvas) (строки 35-44). Делать это нужно в правильном порядке: следующий объект будет рисоваться поверх предыдущего. 
Определённая сложность есть в расчёте высоты столбцов диаграммы: нужно чтобы они поместились в максимальную высоту и при этом сохранили пропорции. Это решаем с помощью метода  private int[] scaleTo(int[] ii, int vmax) в который передаём массив данных для конкретной диаграммы и максимальную высоту. На выходе получаем уже масштабированный массив (строки 53-60). 
Добавляя в setContentView вашего Activity созданный экземпляр СhartView, получаете результат вроде того, что на картинке. В данном случае выбран серый фон и две диаграммы с прозрачностью 50% синего и зелёного цвета. 

пятница, 1 июля 2011 г.

TabWidget в Android: простой способ построить окно с вкладками

Пожалуй, главная проблема при создании интерфейсов в Android-приложениях - нехватка места на экране. Прокрутка спасает не всегда. И часто выгодно бывает реализовать вкладки для "подразделов" содержимого. Вот код метода, который строит интерфейс с вкладками без какой-либо дополнительной XML-разметки:
public static void getTabs(final Activity act) {
TabHost tabHost = new TabHost(act);
tabHost.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));

LinearLayout lll = new LinearLayout(act);
lll.setOrientation(LinearLayout.VERTICAL);

TabWidget tabWidget = new TabWidget(act);
tabWidget.setId(android.R.id.tabs);
lll.addView(tabWidget, new LinearLayout.LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT));

FrameLayout frameLayout = new FrameLayout(act);
frameLayout.setId(android.R.id.tabcontent);
lll.addView(frameLayout, new LinearLayout.LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));

tabHost.addView(lll, LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);

tabHost.setup();

// первая вкладка
TabSpec firstTab = tabHost.newTabSpec("FIRST_TAB");
firstTab.setIndicator("Первая вкладка");
firstTab.setContent(new TabHost.TabContentFactory() {

public View createTabContent(String string) {

TextView in = new TextView(act);
in.setText("Текст в первой вкладке");

return in;
}
});
tabHost.addTab(firstTab);

// вторая вкладка
TabSpec nextTab = tabHost.newTabSpec("NEXT_TAB");
nextTab.setIndicator("Вторая вкладка");
nextTab.setContent(new TabHost.TabContentFactory() {

public View createTabContent(String string) {

TextView in = new TextView(act);
in.setText("Текст во второй вкладке");

return in;
}
});
tabHost.addTab(nextTab);

act.setContentView(tabHost);
}
В параметрах метода передаётся Activity в котором нужно построить вкладки. Туда же можно передать данные для отображения.
Преимущество такого подхода, думаю, очевидно: в зависимости от данных, которые нужно отобразить в окне можно "на лету" менять число вкладок.