четверг, 16 октября 2008 г.

Знакомство с Android. Часть 1: Простое приложение для Android

Недавно заинтересовала меня платформа Android. Как-то много говорят о нем в последнее время, да и вообще хотелось узнать, такая же ли там ужасная Java, как в мидлетах. Так что потратила я некоторое время на копание в нем, написала простое приложение, и сейчас вот буду делиться опытом.

UPDATE

Серия статей по игре Life обновлена 03.11.2010. Исправлены ошибки и проведена адаптация под Android версии 2.2. Всем спасибо за замечания и дополнения!

Постановка задачи

Первым нашим приложением для Android будет реализация всем известной игры Life. Местом дейтвия будет прямоугольное клеточное поле, размеры которого запрашиваются у пользователя. Также у пользователя запрашивается начальное количество клеток. Первое поколение расставляется по карте случайным образом. Последующие поколения получаются по следующим правилам:
  • Если у живой клетки меньше двух или больше трёх соседей, то она погибает.
  • Если у пустой клетки ровно три соседки, она оживает.
Все входные параметры должны проверяться на правильность: столбцов должно быть не меньше 5 и не больше 25, строк должно быть не меньше 5 и не больше 35, начальное количество клеток должно быть не больше, чем ячеек на поле. Для реализации поля будет использован класс GridView.
Для разработки была использована среда Eclipse и Android plugin для неё.
Статья будет из четырех частей:
Итак, начнём.

В этой части

Мы создадим проект, рассмотрим его структуру и напишем простое приложение, состоящее из одной формы. На форме будет интерфейс для ввода данных и кнопка Run.

Создание и обзор проекта

На установке Android SDK и плагина для Eclipse останавливаться не будем, т.к. это достаточно подробно описано в официальном мануале. Создаем в Eclipse новый Android Project:
Создание нового проекта под Android
После нажатия на кнопку Finish создастся новый проект с такой структурой файлов:
Структура проекта
Рассмотрим эту структуру внимательнее.

/res/drawable-dpi

Сюда помещаются все графические файлы, используемые в приложении, для разных разрешений экрана. На данный момент там есть только файл icon.png - главная иконка приложения.

/res/layout

В эту папку помещаются файлы, в которых в формате XML описывается внешний вид форм, расположение контролов и т.д. (как dfm-ки в Дельфи). Плагин даже создал разметку для нашей единственной формы и назвал её main.xml. Позже мы рассмотрим ее подробнее.

/res/values

В этой папке хранятся общие константы для всего приложения, как то: текст, используемый элементами управления, цвета, стили и т.д.. Например, если мы хотим вывести "Hello World" в TextView, можно это сделать явно в разметке, как мы всю жизнь делали в тех же dfm-ках или aspx; либо создать в strings.xml константу hello со значением "Hello World", после чего пойти обратно в разметку и в атрибутах этого TextView прописать android:text="@string/hello".

/gen/R.java

Это такой специальный сгенерированный класс, посредством которого осуществляется доступ к ресурсам приложения (т.е. ко всему тому, что есть в папке res). Например, R.string.hello возвращает константу с именем hello из strings.xml.

StartActivity.java

Это нам плагин сгенерировал класс для главной (и пока что единственной) формы приложения. Там пока содержится единственный обработчик onCreate, и написано там только setContentView(R.layout.main);. С помощью этой строчки к данной форме привязывается разметка, описанная в файле /res/layout/main.xml

AndroidManifest.xml

В этом файле перечисляются общие свойства проекта (версия, package и прочее), а также все формы (Activities), входящие в проект.

Разметка формы (Layout)

Элементы управления в Android называются Views и наследуются от класса View или ViewGroup. Класс ViewGroup также унаследован от View, но его отличие в том, что в него могут быть вложены другие View или ViewGroup.
Иерархия View
Плагин создал простейшую разметку для нашей единственной формы (main.xml):

main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
<TextView  
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:text="@string/hello"
    />
</LinearLayout>
Вначале задаётся Layout, т.е. правило, согласно которому элементы управления следуют друг за другом. LinearLayout значит, что они идут друг за другом сверху вниз (android:orientation="vertical"). Бывают и другие Layout-ы: TableLayout, с помощью которого можно выстроить контролы в таблицу; FrameLayout, который ставит контролы один на другой; и т.д.
Мы воспользуемся TableLayout
Сделаем вот такую разметку:

main.xml

<?xml version="1.0" encoding="utf-8"?>
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:stretchColumns="1"
    android:padding="10dip"
    >
    <TableRow android:paddingBottom="5dip">
        <TextView
            android:text="@string/columns_title"
            android:paddingRight="10dip"
            android:gravity="right"
            android:textStyle="bold"
        />
        <EditText
            android:id="@+id/columns_count"
            android:text="25"
            android:inputType="number"
        />
    </TableRow>
    <TableRow android:paddingBottom="5dip">
        <TextView
            android:text="@string/rows_title"
            android:paddingRight="10dip"
            android:gravity="right"
            android:textStyle="bold"
        />
        <EditText
            android:id="@+id/rows_count"
            android:text="35"
            android:inputType="number"
        />
    </TableRow>
    <TableRow android:paddingBottom="5dip">
        <TextView
            android:text="@string/cells_title"
            android:paddingRight="10dip"
            android:gravity="right"
            android:textStyle="bold"
        />
        <EditText
            android:id="@+id/cells_count"
            android:text="100"
            android:inputType="number"
        />
    </TableRow>
    <TableRow>
        <Button
            android:id="@+id/run"
            android:text="@string/run_title"
            android:textStyle="bold"
            android:layout_span="2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
        />
    </TableRow>
</TableLayout>
В файл res/values/strings.xml при этом нужно добавить следующие строки:

strings.xml

<string name="run_title">Run!</string>
<string name="columns_title">Columns:</string>
<string name="rows_title">Rows:</string>
<string name="cells_title">Cells:</string>
Форма при этом будет выглядеть так:
StartActivity
Рассмотрим некоторые атрибуты, использованные в разметке

android:id

Идентификатор элемента. Если он указан, то в дальнейшем его можно найти на форме с помощью метода findViewById(id). Для контролов, которых мы не планируем в дальнейшем трогать (например, для заголовков), можно это свойство и вовсе не указывать. Идентификаторы можно складывать в файл ids.xml, но вместо этого обычно применяется синтаксис @+id/View1. Это означает, что идентификатор View1 добавляется в константы прямо на ходу. В R.java соответствующие поля также добавляются автоматически.

android:layout_width и android:layout_heigth

Свойства layout_width и layout_heigth обозначают, какую часть родительского контрола будет занимать данный элемент управления: всю (fill_parent или match_parent, начиная с 2.2) или ровно столько, сколько требуется (wrap_content). Можно также задавать численные значения.

android:inputType

Это атрибут EditText. С его помощью можно устанавливать формат вводимого значения. Есть ряд предопределенных форматов (date, phone, etc). Значение number значит, что в это поле можно вводить только целые положительные числа.

android:gravity, android:layout_gravity

Устанавливает выравнивание в данном элементе управления. Отличие в том, что gravity задает выравнивание дочерних контролов, а layout_gravity задает выравнивание самого контрола.

Заключение

Итак, мы создали проект для Android, рассмотрели его структуру, составили разметку для нашей единственной формы.
Исходники примера

Знакомство с Android. Часть 2: Переходы между формами

Итак, продолжим. В этой части мы добавим в проект ещё одну форму и будем открывать её по нажатию кнопки Run. Также сделаем так, чтобы параметры, которые были введены в первой форме, передавались во вторую (они там ещё пригодятся). Однако, ничего страшного мы пока с ними делать не будем, а просто напишем "Введены такие-то числа"

Знакомство с Android. Часть 3: Использование диалогов

В этой части мы сделаем проверку вводимых параметров по следующим правилам:
  • Число столбцов должно быть не меньше 5 и не больше 25.
  • Число строк должно быть не меньше 5 и не больше 35.
  • Начальное количество клеток должно быть не больше, чем ячеек на поле.
Если какое-то из этих условий не выполняется, будем выводить соответствующее предупреждение
Кроме того, мы добавим кнопку Close, при нажатии на которую приложение будет закрываться, спрашивая вначале согласие пользователя.

Знакомство с Android. Часть 4: Использование GridView

Итак, в нашем приложении осталось всего ничего: реализовать собственно алгоритм игры Life и отобразить его в GridView. Этим-то мы сейчас и займёмся.

четверг, 7 августа 2008 г.

URL rewriting в ASP.NET

URL rewriting - это такая штука, которая позволяет из некрасивых урлов с параметрами, вроде
www.somesite.ru/?cat=blogs&author=darja&year=2008&month=8
позволяет делать красивые, т.н. Человеку понятные урлы (ЧПУ), вроде
www.somesite.ru/blogs/darja/2008/8

Возникла у меня такая задача. Есть табличка (вернее, ListView), данные в которой располагаются на нескольких страницах. Пейджер у таблицы устроен так, что к N-ной странице можно обратиться http://localhost/Default.aspx?page=N И вот захотелось, чтобы к n-ной странице таблички можно было бы обратиться как http://localhost/N. Типа так ссылки приятнее выглядят и роботам хорошо. Итак, было найдено следующее решение.

URL rewriting

  1. Скачиваем библиотеку ThunderMain.URLRewriter.dll, кидаем в свой проект.
  2. В Global.asax добавляем:
    protected void Application_BeginRequest(Object sender, EventArgs e)
    {
      ThunderMain.URLRewriter.Rewriter.Process();
    }
    
  3. Вносим изменения в web.config. В configSections пишем:
    <sectionGroup name="system.web">
      <section name="urlrewrites" type="ThunderMain.URLRewriter.Rewriter, ThunderMain.URLRewriter,
     Version=1.0.783.30976, Culture=neutral, PublicKeyToken=7a95f6f4820c8dc3"/></sectionGroup>
    
    Далее, в system.web пишем:
    <urlrewrites><rule>
      <url>/(\d*)</url>
      <rewrite>Default.aspx?page=$1</rewrite></rule>
    </urlrewrites>
    
    Элементов rule может быть несколько. В узле url указывается регулярное выражение, которому должен удовлетворять url, который мы хотим переписывать, в rewrite - на что мы будем его заменять. Можно использовать стандартные для регулярных выражений группы ($1, $2 и т.д., как в перле). В нашем случае, по адресу http://localhost/N, где N - число, загрузится http://localhost/Default.aspx?page=N. Всё очень просто.
  4. Также потребуется некоторая донастройка IIS. А именно, нужно зайти в настройки сайта -> Home directory -> Configuration -> Mappings и добавить там обработчик для файлов .* такой же, как для aspx (ну там C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll). И обязательно снять галочку "Verify that file exists".
UPDATE: Кстати, у меня тут практика показала, что прописывать в IIS мэппинг для .* не есть хорошо, потому что при этом ломаются стили и прочие статические файлы, потому что IIS и их начинает спрашивать с asp.net-a. Я в итоге, дабы не париться, сделала адреса вида http://localhost/N.htm и добавила в IIS мэппинг только для .htm

Пейджер у ListView

Итак, к страницам уже можно обращаться по адресу http://localhost/N. Но в ссылки пейджера всё ещё выглядят по-прежнему. Делаем следующее.
  1. Пейджер выносим в отдельный div и ставим у него style="display:none".
  2. Под листвью добавляем панельку:
    <asp:Panel ID="Pager" runat="server" onprerender="Pager_PreRender">
    </asp:Panel>
    
  3. В коде пишем:
    protected void Pager_PreRender(object sender, EventArgs e)
    {
      Pager.Controls.Clear();
    
      DataPager pager = (DataPager)RatingListView.FindControl("RatingPager");
    
      int count = pager.TotalRowCount;
      int pageSize = pager.PageSize;
      int pagesCount = count / pageSize + (count % pageSize == 0 ? 0 : 1);
      int pageSelected = pager.StartRowIndex / pageSize + 1;
    
      for (int i = 1; i <= pagesCount; ++i)
      {
        if (pageSelected != i)
        {
          HyperLink link = new HyperLink();
          link.NavigateUrl = link.ResolveUrl(i.ToString());
          link.Text = i.ToString();
          Pager.Controls.Add(link);
        }
        else
        {
          Label label = new Label();
          label.Text = i.ToString();
          Pager.Controls.Add(label);
        }
    
        Literal space = new Literal();
        space.Text = " ";
        Pager.Controls.Add(space);
      }
    }
    
    Здесь RatingListView - наш листвью, RatingPager - его "родной" пейджер.

Литература

вторник, 29 июля 2008 г.

Не надо использовать ничего из com.sun.*

В общем, я почти дописала свой мидлет. И оказалось, что на целевой телефон (Nokia N73) он вообще отказывается ставиться. Authorisation error и все. Погуглив, узнала, что во всем виноват пакет com.sun.midp.io.Base64. Люди пишут, что вообще не стоит использовать какие-либо возможности из пакетов, которые называются com.sun.*, ибо это приводит к несовместимости мидлета с некоторыми устройствами. А так как работа с Base64 все-таки нужна, пришлось добавить в код следующее:
private static char[] base64map = new char[64];

static
{
  int i = 0;
  for (char c = 'A'; c <= 'Z'; c++)
  {
    base64map[i++] = c;
  }
  for (char c = 'a'; c <= 'z'; c++)
  {
    base64map[i++] = c;
  }
  for (char c = '0'; c <= '9'; c++)
  {
    base64map[i++] = c;
  }
  base64map[i++] = '+';
  base64map[i++] = '/';
}

private static String base64Encode(byte[] in, int offset, int length)
{
  byte[] src = new byte[length];

  for (int i = 0; i < length; ++i)
  {
    src[i] = in[offset + i];
  }
  return base64Encode(src);
}

private static String base64Encode(byte[] in)
{
  int iLen = in.length;
  int oDataLen = (iLen * 4 + 2) / 3;// output length without padding
  int oLen = ((iLen + 2) / 3) * 4;// output length including padding
  char[] out = new char[oLen];
  int ip = 0;
  int op = 0;
  int i0;
  int i1;
  int i2;
  int o0;
  int o1;
  int o2;
  int o3;
  while (ip < iLen)
  {
    i0 = in[ip++] & 0xff;
    i1 = ip < iLen ? in[ip++] & 0xff : 0;
    i2 = ip < iLen ? in[ip++] & 0xff : 0;
    o0 = i0 >>> 2;
    o1 = ((i0 & 3) << 4) | (i1 >>> 4);
    o2 = ((i1 & 0xf) << 2) | (i2 >>> 6);
    o3 = i2 & 0x3F;
    out[op++] = base64map[o0];
    out[op++] = base64map[o1];
    out[op] = op < oDataLen ? base64map[o2] : '=';
    op++;
    out[op] = op < oDataLen ? base64map[o3] : '=';
    op++;
  }
  return new String(out);
}
Теперь вместо
content = Base64.encode(src, i * CHUNK_SIZE, CHUNK_SIZE);
нужно писать
content = base64Encode(src, i * CHUNK_SIZE, CHUNK_SIZE);

вторник, 15 июля 2008 г.

Upload картинки из мидлета на сервер

Сначала следует отметить, что у мидлета есть ограничение: он может передать по HTTP файл размером не больше чем в 2Кб. Так что будем передавать файл кусочками, а потом на сервере склеивать. Кроме того, картинку следует передавать не абы как, а в кодировке base64, иначе, опять же, будут возникать непонятные проблемы передачи (по крайней мере, у меня возникали).

Клиент

Пусть мы каким-то образом получили файл как byte[]. Напишем специальный класс Uploader, с помощью которого будем заливать картинку на сервер. Передавать файл будем фрагментами по 1 Кб. Для каждого фрагмента будем открывать отдельный коннекшен и составлять POST-запрос, содержащий следующие данные:
  • Номер фрагмента (number)
  • Общее количество фрагментов (count)
  • Собственно содержание (content) в виде вложенного файла
Добавим в наш мидлет следующие поля:
/**
* Размер передаваемого фрагмента картинки
*/
private static int CHUNK_SIZE = 1024;

/**
* Граница между разделами в HTTP-запросе
*/
private static String BOUNDARY = "42hjkiqp4279h";

/**
* Адрес, на который мы посылаем запросы
*/
private static String HOST = "http://some_url/Handler.ashx";

/**
* Сообщение, которое мы получаем в случае успешной отправки запроса
*/
private static String UPLOAD_OK_MSG = "Upload OK";
И следующие методы:
/**
* Залитие картинки на сервер
* @param src Массив символов, содержащий картинку
* @return Ответ от сервера: удалось залить, или нет
*/
public String Upload(byte[] src)
{
  String response = "";
  try
  {
    int nchunks = src.length / CHUNK_SIZE;
    System.out.println("size = " + src.length + ", chunks count = " + nchunks);

    String fileId = GetNextPhotoName(5);

    for (int i = 0; i < nchunks; ++i)
    {
      String content = Base64.encode(src, i * CHUNK_SIZE, CHUNK_SIZE);
      response = UploadChunk(fileId, i + 1, nchunks, content);
      if (!IsUploadOk(response))
      {
        break;
      }
    }
    System.out.println("Uploading file " + fileId + ": " + response);
  }
  catch (Exception ex)
  {
    System.out.println(ex.getClass().getName() + " while uploading file: " + ex.getMessage());
  }
  finally
  {
  }
  return response;
}

/**
* Формирование раздела HTTP-запроса, содержащего значение некоторого параметра
* @param name Имя параметра
* @param value Значение параметра
* @return Строка - раздел HTTP-запроса
*/
private static String FormData(String name, Object value)
{
  return "--" + BOUNDARY + "\r\n" +
      "Content-Disposition: form-data; name=\"" + name + "\"\r\n\r\n" + value + "\r\n";
}

/**
* Заливает на сервер фрагмента картинки
* @param fileId Идентификатор картинки
* @param number Номер фрагмента
* @param count Общее количество фрагментов
* @param content Содержаение фрагмента
* @return Ответ от сервера: удалось залить или нет
*/  
private String UploadChunk(String fileId, int number, int count, String content)
{
  String message =
      FormData("number", new Integer(number)) +
      FormData("count", new Integer(count)) +
      FormData("sessionId", sessionId) +
      "--" + BOUNDARY + "\r\n" +
      "Content-Disposition: form-data; name=\"imagefile\"; filename=\"" + fileId + "\"\r\n" +
      "Content-Transfer-Encoding: base64\r\n" +
      "Content-Type: image/png\r\n\r\n" + content + "\r\n" +
      "--" + BOUNDARY + "--\r\n";
  String response = SendPostData(message);

  System.out.println("Chunk " + fileId + "_" + number + " " + response);
  return response;
}

/**
* Отправляет на сервер POST-запрос
* @param message Текст запроса
* @return Ответ сервера
*/
private static String SendPostData(String message)
{
  HttpConnection conn = null;
  InputStream is = null;
  OutputStream os = null;
  String response = null;
  try
  {          
    conn = (HttpConnection) Connector.open(HOST);

    // Задаем параметры запроса
    conn.setRequestMethod(HttpConnection.POST);
    conn.setRequestProperty("Connection", "Keep-Alive");
    conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + BOUNDARY);

    byte[] messageBody = message.getBytes();          
    conn.setRequestProperty("Content-Length", Integer.toString(messageBody.length));

    // Посылаем запрос
    os = conn.openOutputStream();
    os.write(messageBody);

    int rc = conn.getResponseCode();          

    if (rc != HttpConnection.HTTP_OK)
    {
      throw new IOException("HTTP responce code: " + rc);
    }

    // Получаем ответ
    is = conn.openInputStream();
    int ch;
    StringBuffer data = new StringBuffer();
    while ((ch = is.read()) != -1)          
    {
      data.append((char) ch);
    }
    response = data.toString();
  }
  catch (Exception e)
  {
    response = "Error while connecting:\n\n" + e.getMessage();
  }
  finally
  {
    try
    {
      if (is != null)
      {
        is.close();
      }              
      if (os != null)
      {
        os.close();
      }
      if (conn != null)
      {
        conn.close();
      }
    }
    catch (Exception e)
    {
    }
  }
  return response;
}

/**
* Проверка, успешно ли произошел Upload на сервер
* @param response Ответ сервера
* @return
*/
private boolean IsUploadOk(String response)
{
  return response.equals(UPLOAD_OK_MSG);
}

Сервер

Сервер, как и раньше, написан на .NET. Есть хэндлер Handler.ashx, и в нем написано следующее:
namespace MobileServer
{
  [WebService(Namespace = "http://tempuri.org/")]
  [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
  public class Handler : IHttpHandler
  {
    private const string UPLOAD_PATH = "...";
    
    /// <summary>
    /// Получение пути ко временному файлу с фрагментом картинки
    /// </summary>
    private static string GetChunkFileName(string fileName, int number)
    {
    return String.Format("{0}\{1}_{2}", UPLOAD_PATH, f.FileName, number);
    }

    /// <summary>
    /// Получение пути к файлу с картинкой
    /// </summary>
    private static string GetFileName(string fileName)
    {
    return String.Format("{0}\{1}.png", UPLOAD_PATH, f.FileName);
    }
    
    /// <summary>
    /// Сохранение фрагмента картинки в файл
    /// </summary>
    /// <param name="number">Номер фрагмента
    /// <param name="f">Файл, полученный по HTTP
    private static void SaveChunkToFile(int number, HttpPostedFile f)
    {
      byte[] buf = new byte[f.ContentLength];
      f.InputStream.Read(buf, 0, f.ContentLength);
      String content = String.Empty;

      for (int i = 0; i < buf.Length; ++i)
      {
        if (buf[i] == 0)
          break;

        content += ((char)buf[i]).ToString();
      }

      byte[] rez = Convert.FromBase64String(content);

      string fileName = GetChunkFileName(f.FileName, number);

      FileStream fs = new FileStream(fileName, FileMode.CreateNew);
      BinaryWriter bw = new BinaryWriter(fs);
      bw.Write(rez, 0, rez.Length);
      bw.Flush();
      bw.Close();
      fs.Close();
    }
    
    /// <summary>
    /// Проверка, все ли фрагменты данной картинки залились
    /// </summary>
    /// <param name="fileName">Имя файла с картинкой
    /// <param name="count">Общее количество фрагментов
    private static bool AreAllChunksUploaded(string fileName, int count)
    {
      for (int i = 1; i <= count; ++i)
      {
        if (!File.Exists(GetChunkFileName(fileName, i)))
        {
          return false;
        }
      }
      return true;
    }

    /// <summary>
    /// Объединение всех фрагментов файла, и сохранение в отдельный файл
    /// </summary>
    /// <param name="fileName">Название файла
    private static void MergeChunks(string fileName)
    {
      for (int i = 1; i <= imageInfo.ChunksCount; ++i)
      {
        FileStream fs = new FileStream(GetChunkFileName(fileName, i), FileMode.Open);
        BinaryReader br = new BinaryReader(fs);
        byte[] buf = br.ReadBytes(2000);
        br.Close();
        fs.Close();

        fs = new FileStream(GetFileName(fileName), FileMode.Append);
        BinaryWriter bw = new BinaryWriter(fs);
        bw.Write(buf);
        bw.Close();
        fs.Close();
      }
    }

    #region IHttpHandler members
    public void ProcessRequest(HttpContext context)
    {
      try
      {
        int number = Convert.ToInt32(context.Request["number"]);
        int count = Convert.ToInt32(context.Request["count"]);
        string sessionId = context.Request["sessionId"];

        HttpPostedFile f = (HttpPostedFile)context.Request.Files[0];

        SaveChunkToFile(number, f);

        if (AreAllChunksUploaded(f.FileName, count))
        {
          MergeChunks(f.FileName);
          //TODO: код для удаления временных файлов
        }
        context.Response.Write("Upload OK");
      }
      catch (Exception e)
      {
        context.Response.Write("Upload failed:\n" + e.Message);
      }
    }

    public bool IsReusable
    {
      get
      {
        return false;
      }
    }
    #endregion
  }
}
В общем, как-то так.

четверг, 26 июня 2008 г.

Простейший мидлет с возможностью работы с HTTP

Задача: написать мидлет, который будет посылать некоторый POST-запрос на сервер и выводить ответ.

Аппаратное обеспечение

Проверялось на Sony Ericsson k320i и на Nokia 2600 classic.

Сервер

На сервере у нас простое Web-приложение, написанное на ASP.NET. Для этого приложения напишем хэндлер Handler.ashx, который будет принимать и обрабатывать запросы. К примеру, он будет получать значение переменной message, и возвращать строку "Message is <значение>"
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class Handler : IHttpHandler
{
  public void ProcessRequest(HttpContext context)
  {
    string msg = context.Request["message"];
    context.Response.ContentType = "text/plain";
    context.Response.Write(String.Format("Message is: {0}", msg));
  }

  public bool IsReusable
  {
    get
    {
      return false;
    }
  }
}

Создание мидлета

Для работы использовалась среда NetBeans 6.1. Создаем Mobile Application с профилем MIDP 2.0 (c 2.1 на указанных телефонах мидлет не заработал). На форму мидлета добавим StringItem и назовем MessageString. Также добавим на форму команду OkCommand, и в ее обработчик (не знаю, как это по-явовски правильно называется) добавим вызов функции CallServer Сама функция выглядит примерно таким образом:
public void CallServer()
{
  Runnable r = new Runnable()
  {
    public void run()
    {
      HttpConnection conn = null;
      InputStream is = null;
      OutputStream os = null;
      try
      {
        conn = (HttpConnection) Connector.open("http://localhost:1988/Handler.ashx");

        // Настройки соединения
        conn.setRequestMethod(HttpConnection.POST);
        conn.setRequestProperty("User-Agent", "Profile/MIDP-2.0 Configuration/CLDC-1.0");
        conn.setRequestProperty("Content-Language", "en-US");
        conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");

        // GenerateString - некоторая функция, возвращающая строку
        String str = "message=" + GenerateString() + "\r\n";

        // Посылка сообщения
        os = conn.openOutputStream();
        os.write(str.getBytes());
        //os.flush();

        // Смотрим, какой код пришел с сервера
        int rc = conn.getResponseCode();
        if (rc != HttpConnection.HTTP_OK)
        {
          throw new IOException("HTTP responce code: " + rc);
        }                 

        // Получение ответа и вывод его в строке MessageString
        is = conn.openInputStream();
        int ch;
        StringBuffer data = new StringBuffer();
        while ((ch = is.read()) != -1)
        {
          data.append((char) ch);
        }
        MessageString.setText(data.toString());
      }
      catch (Exception ex)
      {
        MessageString.setText("something failed: " + ex.getMessage());
      }
      finally
      {
          // Закрытие соединений
        try
        {
          if (is != null)
          {
            is.close();
          }
          if (os != null)
          {
            os.close();
          }
          if (conn != null)
          {
            conn.close();
          }
        }
        catch (Exception e)
        {
        }
      }
    }
  };
  new Thread(r).start();
}
При работе с HttpConnection обязательно нужно создавать отдельный поток (Runnable), потому что иначе при установке соединения будет возникать дедлок. А возникает он по следующей причине. Поток, инициирующий соединение, блокируются до тех пор, пока соединение не будет установлено. То есть, если код выполняется в системном потоке мидлета, то блокируется системный поток. И тут мидлет вдруг решает спросить у пользователя разрешения на выполнение операции соединения. Без разрешения соединение не установится, а разрешить не получится, потому что системный поток заблокирован. Отсюда дедлок. Статья по этому поводу

четверг, 12 июня 2008 г.

Коллекции в конфиге приложения

Задача была следующая. Нужно было написать робота, который будет ходить по тестовому сайту, делать там хиты, прикидываться разными пользователями, делать несколько сессий, да еще и работать на нескольких браузерах. Робот реализован в виде консольного приложения, для работы с браузерами используется Selenium, параметры (количество пользователей, сессий и хитов, а также список браузеров) хранятся в App.config. Собственно, здесь я напишу о том, как работать с параметрами из конфига.

Добавление в конфиг своей секции

Итак, мы хотим, чтобы в конфиге можно было написать так:
<VisitorSettings hitsPerDay="300" sessionsPerDay="21" duniqsPerDay="15"><Browsers>
  <browser name="chrome" />
  <browser name="iexplore"/>
  <browser name="opera" path="C:\Program Files\Opera\Opera.exe" />
  <browser name="custom" path="C:\Program Files\Safari\Safari.exe" /></Browsers>
</VisitorSettings>
Пишем следующие классы:

Класс для хранения конфигурации браузера

public class Browser : ConfigurationSection
{
  public const string SectionName = "Browsers";
  private const string name = "name";
  private const string path = "path";

  [ConfigurationProperty(name, IsRequired = true)]
  public string Name
  {
    get { return (string)base[name]; }
    set { base[name] = value; }
  }

  [ConfigurationProperty(path, IsRequired = false)]
  public string Path
  {
    get { return (string)base[path]; }
    set { base[path] = value; }
  }

  public string GetBrowserString()
  {
    return String.Format(
      "*{0}{1}{2}",
      Name,
      String.IsNullOrEmpty(Path) ? String.Empty : " ",
      Path
    );
  }
}

Класс для хранения конфигурации списка браузеров

[ConfigurationCollection(typeof(Browser), CollectionType = ConfigurationElementCollectionType.AddRemoveClearMap)]
public class BrowserElementCollection : ConfigurationElementCollection
{
  #region Constructor
  static BrowserElementCollection()
  {
    browsers = new ConfigurationPropertyCollection();
  }
  #endregion

  #region Fields
  private static ConfigurationPropertyCollection browsers;
  #endregion

  #region Properties
  protected override ConfigurationPropertyCollection Properties
  {
    get { return browsers; }
  }

  public override ConfigurationElementCollectionType CollectionType
  {
    get { return ConfigurationElementCollectionType.BasicMap; }
  }

  protected override string ElementName
  {
    get
    {
      return "browser";
    }
  }
  #endregion

  #region Indexers
  public Browser this[int index]
  {
    get { return (Browser)base.BaseGet(index); }
    set
    {
      if (base.BaseGet(index) != null)
      {
        base.BaseRemoveAt(index);
      }
      base.BaseAdd(index, value);
    }
  }
  #endregion

  #region Overrides
  protected override ConfigurationElement CreateNewElement()
  {
    return new Browser();
  }

  protected override object GetElementKey(ConfigurationElement element)
  {
    return (element as Browser).GetBrowserString();
  }
  #endregion
}

Класс для самой секции конфига:

public class VisitorSettings : ConfigurationSection
{
  static public string SectionName = "VisitorSettings";

  const string hitsPerDay = "hitsPerDay";
  const string duniqsPerDay = "duniqsPerDay";
  const string sessionsPerDay = "sessionsPerDay";
  const string browsers = "Browsers";

  [ConfigurationProperty(hitsPerDay, IsRequired = true)]
  public int HitsPerDay
  {
    get { return (int)base[hitsPerDay]; }
    set { base[hitsPerDay] = value; }
  }

  [ConfigurationProperty(sessionsPerDay, IsRequired = true)]
  public int SessionsPerDay
  {
    get { return (int)base[sessionsPerDay]; }
    set { base[sessionsPerDay] = value; }
  }

  [ConfigurationProperty(duniqsPerDay, IsRequired = true)]
  public int DuniqsPerDay
  {
    get { return (int)base[duniqsPerDay]; }
    set { base[duniqsPerDay] = value; }
  }
  [ConfigurationProperty(browsers, IsRequired = true)]
  public BrowserElementCollection Browsers
  {
    get { return (BrowserElementCollection)base[browsers]; }
    set { base[browsers] = value; }
  }
}
Готово! Теперь для полного счастья в App.config configSections осталось прописать:
<section name="VisitorSettings" type="Visitor.VisitorSettings, Visitor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />

Чтение данных из конфига

Теперь можно писать так:
int uniques = WatcherConfiguration.VisitorSettings.DuniqsPerDay;
int sessions = WatcherConfiguration.VisitorSettings.SessionsPerDay;
int hits = WatcherConfiguration.VisitorSettings.HitsPerDay;
BrowserElementCollection browsers = WatcherConfiguration.VisitorSettings.Browsers;

Редактирование конфига из программы

Пусть теперь у нас есть админка, из которой мы должны иметь возможность добавлять и удалять браузеры. Сначала нужно добавить в класс BrowserElementCollection следующие методы:
#region Методы
public void Add(Browser browser)
{
  base.BaseAdd(browser);
}

public void Remove(string key)
{
  base.BaseRemove(key);
}

public void Remove(Browser browser)
{
  base.BaseRemove(GetElementKey(browser));
}

public void Clear()
{
  base.BaseClear();
}

public void RemoveAt(int index)
{
  base.BaseRemoveAt(index);
}

public string GetKey(int index)
{
  return (string)base.BaseGetKey(index);
}
#endregion
Теперь добавлять браузеры мы можем так:
protected void AddBrowser(string name, string path)
{
  Browser browser = new Browser()
  {
    Name = name;
    Path = path;
  };

  WatcherConfiguration.VisitorSettings.Browsers.Add(browser);
  WatcherConfiguration.SaveAll();
}
А удалять - так:
protected void AddBrowser(string name, string path)
{
  WatcherConfiguration.VisitorSettings.Browsers.RemoveAt(index);
  WatcherConfiguration.SaveAll();
}