Показаны сообщения с ярлыком j2me. Показать все сообщения
Показаны сообщения с ярлыком j2me. Показать все сообщения

среда, 12 ноября 2008 г.

Mobile Processing

Думала, мой следующий пост (вернее, следующая серия) будет опять про Андроид, ан нет :) Недавно попалась мне одна удивительная штука под названием Mobile Processing. Это инструмент, облегчающий процесс создания мобильных приложений. Обычно, если хочется написать мобильное приложение, надо учить Java, разбираться, как устроен мидлет, какие у него там события, каков жизненный цикл. И, если честно, не могу сказать, что копаться в этом очень приятно.

Другое дело Mobile Processing. Тут вообще необязательно что-либо знать про мидлеты. Есть собственный простой язык, похожий на Java, с помощью которого можно описать логику приложения. Есть IDE, которая выглядит таким образом:

Единственное, что надо настроить в IDE - указать путь к WTK.

После этого мы пишем код, в котором просто определяем функции типа setup(), draw() и т.п., сохраняем все это дело в файл %progname%.PDE потом нажимаем кнопку "Пыщь" - и среда генерит нам jar.

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

int x, y;
int r = 5;

void setup()
{
 x = 100;
 y = 100;
}

void draw()
{
 background(255, 204, 0);
 ellipse(x, y, 2 * r, 2 * r);switch (keyCode){
  case UP: y--; break;
  case DOWN: y++; break;
  case LEFT: x--; break;
  case RIGHT: x++; break;
  default: break;}
}

void keyReleased()
{
 keyCode = 0;
}

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

Полученный jar-файл весил 46 Кб, но с помощью имеющегося оптимизатора-обфускатора ProGuard его удалось ужать до 7 Кб. Программа запросто запустилась на моей Nokia 2600c, а также на Nokia N73.

В общем, данная тулза привела меня в восторг своей простотой (особенно после монстров типа NetBeans или IDEA, которую все хвалят, но в которой мне так и не удалось создать Mobility-проект). Позиционируется она как средство быстрого создания прототипов приложений, но, честно говоря, возможности у нее не такие уж и слабые. Есть библиотеки для работы с Bluetooth, с камерой и другими возможностями телефона. И самое интересное - оно работает. Единственное, что меня удивляет, так это практически отсутствие упоминаний о данном продукте в рунете. Вроде, и проект не совсем мертвый (последний релиз в июле 2008). Интересно, почему бы это?

Но я в ней еще поковыряюсь, дабы узнать, что там плохо или хорошо. Пока что все обнаруженные минусы относятся к редактору: отсутствие отладчика и IntelliSense, неидеальный keymapping (я люблю, чтобы работали Ctrl+Ins и Shift+Ins, а там только Ctrl+C и Ctrl+V. Хотя, возможно, это настраивается в конфиге, который там далеко спрятан).

Ссылки по теме

вторник, 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), потому что иначе при установке соединения будет возникать дедлок. А возникает он по следующей причине. Поток, инициирующий соединение, блокируются до тех пор, пока соединение не будет установлено. То есть, если код выполняется в системном потоке мидлета, то блокируется системный поток. И тут мидлет вдруг решает спросить у пользователя разрешения на выполнение операции соединения. Без разрешения соединение не установится, а разрешить не получится, потому что системный поток заблокирован. Отсюда дедлок. Статья по этому поводу