Ну вот, в этот раз вторая часть поучительной истории о моих попытках зохватить голактеку начать разрабатывать платные приложения для Palm WebOS и о продаже приложений в Palm App Catalog.

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

О всяком

Итак, прошел месяц продаж и наконец-то Palm опубликовали статистику загрузок/продаж за февраль, и отчеты с этой информацией стали доступны в Web-интерфейсе разработчика. Отчеты, как оказалось, неплохие, довольно подробные. Во0первых, можно посмотреть статистику по каждому приложению и по каждой версии приложения. В принципе, информация о загрузках вся публичная, цена приложения известна, так что не вижу смысла затирать цифры и выкладываю скрины как есть.

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

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

Как обычно, запостил новый бинарник, отдал на ревью. Прошло три дня и.. вот оно! Больше тысячи загрузок в за первые сутки после публикации! Я прям даже обрадовался.

Epic Fail

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

Подождав денек после публикации, решил посмотреть, что же пишут люди по поводу моего чудесного творения. А, как оказалось, пишут они странное. Наиболее странным мне показалась фраза о том что “Круто что в этой версии они снизили цену до $0”. Вобще, более тысячи загрузок в сутки мне тоже показалось странным, учитывая то, что за половину февраля было куплено всего 388 копий приложения. Пошел проверять в админку. И таки-да, уважаемые товарищи из Palm выложили новую версию моего приложения с ценой $0 вместо заявленных при сабмите $0.99.

В этом месте мне пришлось грязно выругаться стало не по себе, но, как показывает опыт, в таких случаях лучше не психовать, а просто планомерно решать проблему. Написал в саппорт Palm’у что вот, у меня проблема с последней версией приложения. Я заплатил $50 за сабмит, прошлая версия приложения была платной, а тут почему-то стала бесплатной. Можете мне откатить все загрузки текущей версии и снова сделать мое приложение платным? Также я описал последовательность действий по сабмиту приложения с самого начала. Рассказал о том, что первая версия была бесплатной бетой, потом я засабмитил платное приложение, потом обновил версию, после чего произошла бяка.

На самом деле, при общении с саппортом, я особо на результат не рассчитывал. Вернее даже не так, судя по прошлому общению, ответ я ожидал недели через полторы, кгда все, кому это надо, уже скачают бесплатную версию и смысла продавать приложение уже не будет совсем. Но в этот раз все было быстро. Первый ответ пришел через 4 часа с вопросами, действительно ли мое приложение было платное. Я ответил что да, и они могут посмотреть тчеты по продажам за февраль. Следующим письмом было уведомление о том, что мое приложение убрали из каталога (suspended) до выяснения источника проблем и до того момента, когда они смогут сделать приложение снова платным.

До того момента как приложение засаспендили, его успело скачать около 2900 леммингов, но вцелом в этот раз саппортом я доволен, порешали все в течении суток.

Еще через сутки мне сказали что кина не будет и засабмитить приложение заново как платное нельзя, только надо сабмитить приложение с новым package ID и платить $50.

В этом месте мне снова пришлось грязно выругаться задуматься. А как же мои денежки, которые я потратил на сабмит этого приложения? Ведь бяка произошла не по моей вине, а из-за бага у них в системе в системе. О своих размышлениях на эту тему я довольно деликатно намекнул саппорту.

И о чудо! Они пообещали попробовать обдумать возможность возврата мне денег за прошлый сабмит. Так и написали – попробовать обдумать возможноть.

Зная, как все делается у нас, я особо на результат не рассчитывал, ведь папуас папуасу друг, товарищ и.. корм. Поэтому я просто подготовил новую версию приложения, добавив к нему слово Pro в конце, а также вставив несколько полезных фич по настройке загружаемых PDF файлов, и… отдал еще $50 за новый сабмит 😉

А, да, еще +$20 отдал дизайнеру-фрилансеру за перевод иконки в вектор и за то что слепил мне ICO файл (глядя на все это, решил еще и для остальных мобильных платформ версии делать).

Ну хоть что-то хорошее

Еще чере сутки (пока проходило ревью нового приложения), получил радостную новость от Palm что таки-да, они сжалились надо мной и вернули $50. А откатывать бесплатные загрузки старого приложения никто так и не пытался. Итого получается что из-за бага в системе у Palm в мире стало на 3000 счастливых леммингов больше. Ну что же, ладно, будем считать что мне зачтется доброе дело и я получу +10 к пепяке.

И вот, настало сегодня. И сегодня у меня получилось сразу две радостные новости.

Первая – пришли документы, подтверждающие валидность EIN, который я поиспользовал в Google Checkout и Palm App Catalog.

Вторая – что мое приложение прошло ревью и доступно для загрузки.

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

Напоследок видео

И еще ссылка на Pre Central, где можно проголосовать за приложение и посмотреть статистику.

Previous ArticleNext Article
Технический директор IT-Dimension, компании-разработчика кросс-платформенного программного обеспечения

This post has 1 Comment

1

Leave a Reply

Your email address will not be published. Required fields are marked *

В.

Впечатления от вебкаста по Palm webOS

Palm PreПоучаствовал в вебкасте по разработке для Palm webOS.

Как это было? Это было ммм.. довольно странно. Видео тормозило просто жутчайшим образом, а звука вобще не было. Вернее вместо этого был только шум. Как вариант предлагалось позвонить по телефону и послушать таким образом о чем там идет речь, но… как-то межлународный звонок в течении часа.. ну не каждый осилит.

Но в общем ладно. Теперь о хорошем. Как-то так постараюсь тезисно свое мнение изложить:

  1. Palm OS это прикольно. 😉
  2. Т.к. приложения пишутся на JavaScript, то для разоаботки GUI кроме текстового редактора ничего не нужно.
  3. В вебкасте чудо-человек разработчик демонстрировал все под Mac OS но т.к. JavaScript отрабатывает на клиенте, то писать можно и под Windows и под Linux
  4. JavaScript как и реньше не имеет доступа к системным ресурсам, зато для этого предусмотрен серверный функционал. Для получения, например, данных о GPS координатах из JavaScript выполняется запрос к локальному Web-сервису (URL начинается как-то так с “palm://…”, целиком не запомнил)
  5. Архитектура приложения на вид напоминает MVC. В подробности особо не вдавались, вебкаст короткий.
  6. Форматирование и GUI-контролы реализуются с помощью предустановленных CSS-стилей. На вид выглядит довольно аккуратно и юзабельно.
  7. SDK будет включать (по крайней мере по обещаниям) какое-то подобие веб-сервера, реализующего эмуляцию серверной части API для webOS (тех веб-сервисов, которые доступны в самой OS и которые можно использовать из JS).
  8. Не очень понятно пока каким образом можно реализовывать свой серверный функционал (т.е. например какой-нибудь сервис, который могут дергать все приложения, запущенные на устройстве, это пока загадка).
  9. Не очень понятно как поучаствовать в закрытом бета-тестировании SDK (оно пока не доступно для широкой общественности).
  10. Записанный вариант вебкаста обещали выложить (не знаю пока, в публичном доступе или только для участников) и прислать адрес в почту. Если пришлют, обязательно выложу информацию здесь на сайте.

Ну вот как-то так. Жаль, конечно, что из-за недостаточного качества организации мероприятия не получилось узнать больше, но тема сама по себе очень интересная.

И.

Использование HTML5 Depot для хранения данных в Palm WebOS

Для начала давайте разберемся, что такое Depot и зачем он может понадобиться в приложениях для Palm WebOS.

Если ваше приложение должно хранить данные, то в таком случае есть три варианта:

  • Использование Cookies
  • Использование базы данных SQLite
  • Использование Depot

Но в каких же случаях есть смысл использовать каждый из перечисленных вариантов? Если необходимо сохранить небольшой объем данных, например настройки приложения, то лучше использовать Cookies. Если же объем данных относительно большой, то выбор должен быть сделан в пользу одного из оставшихся способов. Какой из них лучше?

Что лучше использовать – зависит от объема данных, которые нужно хранить, а также от того, где вы будете хранить эти данные. Если вы используете хранилище по умолчанию (кстати Depot переводится как “хранилище” или “склад”), то у вас объем данных будет ограничен одним мегабайтом.

Если вам необходимо хранить данные в структурированном виде, то лучше сделать выбор в пользу базы данных SQLite.

Кстати, ограничение на 1 МБ данных можно обойти, разместив хранилище на разделе с медиа. В этом случае размер хранилища будет ограничен только объемом свободной памяти на это мразделе.

В чем же отличие Depot от базы данных SQLite? Depot может хранить только объекты. Например сделать так чтобы хранилась только булевая переменная не получится, нужно обязательно сделать объект и эту булевую переменную указать в виде свойства.

{ "value" : true }

Тоесть, что получается: Depot – это неструктурированное хранилище объектов. На самом деле это просто еще один уровень абстракции над API по работе с базами данных SQLite, но он позволяет избавиться от необходимости писать SQL в коде, что довольно удобно.

Перед тем, как рассматривать пример использования Depot, давайте создадим каркас приложения и потом в него уже будем добавлять необходимый функционал:

app/views/main/main-scene.html

<div id="main" class="palm-hasheader">
  <div class="palm-header">Depot Test</div>
  <div id="AddSceneButton" x-mojo-element="Button"></div>
  <div id="info" x-mojo-element="x-body-text"></div>
</div>

app/assistants/main-assistant.js

function MainAssistant() {
}

MainAssistant.prototype.setup = function() {
  this.controller.setupWidget("AddSceneButton", {}, {label:"Add"});
  Mojo.Event.listen(this.controller.get("AddSceneButton"),
    Mojo.Event.tap,
    this.handleAddSceneButton.bind(this));
}

MainAssistant.prototype.handleAddSceneButton = function(event) {
  this.controller.stageController.pushScene("add");
}

app/views/add/add-scene.html

<div id="main" class="palm-hasheader">
  <div class="palm-header left" id="list-header">
    Add / replace data
  </div>
</div>
<div class="palm-group">
  <div class="palm-group-title"><span x-mojo-loc="">Database Info.</span></div>
    <div class="palm-list">
      <div class='palm-row first'>
        <div class="palm-row-wrapper textfield-group" x-mojo-focus-highlight="true">
          <div class="title">
            <div class="label">Key</div>
            <div id="keyField" name="keyField" x-mojo-element="TextField" ></div>
          </div>
        </div>
      </div>
      <div class='palm-row last'>
        <div class="palm-row-wrapper textfield-group" x-mojo-focus-highlight="true">
          <div class="title">
            <div class="label">Text</div>
            <div id="textField" name="textField" x-mojo-element="TextField" ></div>
          </div>
        </div>
      </div>
    </div>
</div>
</div>
<div id="depotResult" class="info-text"></div>
<div x-mojo-element="Button" id="add_button"></div>
<div x-mojo-element="Button" id="back_button"></div>

app/assistants/add-assistant.js

function AddAssistant() {
}

AddAssistant.prototype.setup = function() {
  this.controller.setupWidget('keyField',
    {
      hintText:       'Enter key here',
      modelProperty:  'originalValue',
      textCase:       Mojo.Widget.steModeLowerCase,
      focus:          true,
      maxLength:      30
    },
    {
      originalValue : ''
    });
  this.controller.setupWidget('textField',
    {
      hintText:       'and some text here',
      modelProperty:  'originalValue',
      textCase:       Mojo.Widget.steModeLowerCase,
      multiline:      false,
      focus:          false,
      maxLength:      30
    },
    {
      originalValue : ''
    });
  this.controller.setupWidget("add_button", {}, {label:"Add entry"});
  this.controller.setupWidget("back_button", {}, {label:"Back to main view"});

  Mojo.Event.listen(this.controller.get("add_button"),
    Mojo.Event.tap, this.handleAddButton.bind(this));
  Mojo.Event.listen(this.controller.get("back_button"),
    Mojo.Event.tap, this.handleBackButton.bind(this));
}

AddAssistant.prototype.cleanup = function() {
  this.controller.stopListening(
    this.controller.get('add_button'), Mojo.Event.tap,
    this.handleAddButton);
  this.controller.stopListening(
    this.controller.get('back_button'), Mojo.Event.tap,
    this.handleBackButton);
}

AddAssistant.prototype.deactivate = function(event) {
}

AddAssistant.prototype.handleAddButton = function(event) {
}

AddAssistant.prototype.handleBackButton = function(event) {
  this.controller.stageController.popScene();
}

На главной форме у нас находится кнопка Add, которая вызывает сцену, в которой можно добавить новые записи в Depot.
Форма добавления новых записей содержит заголовок, группу из двух текстовых полей и две кнопки – кнопку добавления новой записи и кнопку закрытия сцены.
WebOS Depot Test

Ну вот, GUI готово, теперь можем начинать работу с Depot.

function MainAssistant() {
  var options = {
    name: "depot_tutorial", //Название хранилища (обязательный параметр)
    version: 1, //Версия базы данных. (опционально, по умолчанию 1)
    replace: false // открывать существующее хранилище
  };
  this.depot = new Mojo.Depot(options,
    this.dbConnectionSuccess.bind(this),
    this.dbConnectionFailure.bind(this));
}

MainAssistant.prototype.dbConnectionSuccess = function() {
    this.controller.get("info").update('OK');
}

MainAssistant.prototype.dbConnectionFailure = function() {
    this.controller.get("info").update('Error');
}

При создании объекта Depot можно указать методы, которые будут отрабатывать при успешном подключении к хранилищу или же при ошибке подключения.

Если надо создать хранилище на разделе с медиа, то название хранилища будет выглядеть так:

name: "ext:depot_tutorial"

У нас есть сцена, в которой должно происходить добавление объектов в Depot. Нам надо передать в нее объект Depot в качестве параметра. Сделать это можно вот как:
app/assistants/main-assistant.js

MainAssistant.prototype.handleAddSceneButton = function(event)
{
    this.controller.stageController.pushScene("add", this.depot);
}

app/assistants/add-assistant.js

function AddAssistant(arg) {
    this.depot = arg;
}

Мы передаем объект Depot в качестве параметра pushScene() и этот объект будет получен сценой как параметр конструктора.
Теперь нам необходимо написать обработчик кнопки add_button, в котором значение из текстового поля TEXT будет добавляться в хранилище с ключем KEY.

AddAssistant.prototype.handleAddButton = function(event) {
    this.data = { "value" :this.textModel.originalValue };
    this.depot.simpleAdd(this.keyModel.originalValue,
        this.data, this.dbAddSuccess, this.dbAddFailure);
}

AddAssistant.prototype.dbAddSuccess = function(event) {
    this.depotResult.innerHTML = ("Add success!");
}

AddAssistant.prototype.dbAddFailure = function(event) {
    this.depotResult.innerHTML = ("Add failure!");
}

В обработчике кнопки Add мы создаем объект, в нем в свойстве value запоминаем введенный текст и затем записываем в Depot. Запись происходит путем вызова метода simpleAdd(), в котором мы указываем ключ, сохраняемый объект, а также два callback-метода, которые будут вызваны при удачном или неудачном завершении операции записи.

И вот, мы научились добавлять объекты в Depot. Теперь попробуем научиться их оттуда доставать.

Создадим новую сцену с названием get.

app/views/get/get-scene.html

<div id="main" class="palm-hasheader">
  <div class="palm-header left" id="list-header">
    Retrieve data
  </div>
</div>
<div class="info-text">Which data entry you want to get?</div>

<div class="palm-group">
<div class="palm-group-title">Enter key string</div>
  <div class="palm-list">
    <div class="palm-row single">
      <div class="palm-row-wrapper textfield-group" x-mojo-focus-highlight="true">
        <div class="title">
          <div id="entryNum" x-mojo-element="TextField" ></div>
        </div>
      </div>
    </div>
  </div>
</div>
<div id="response" class="info-text"></div>
<div x-mojo-element="Button" id="get_button"></div>
<div x-mojo-element="Button" id="back_button"></div>

app/assistants/get-assistant.js

function GetAssistant(arg) {
    this.depot = arg;
}

GetAssistant.prototype.setup = function() {
    this.controller.setupWidget("get_button", {}, {label:"Get value"});
    this.controller.setupWidget("back_button", {}, {label:"Back to main view"});
    this.controller.setupWidget('entryNum',
        {
            hintText:       'Enter key here',
            modelProperty:  'originalValue',
            textCase:       Mojo.Widget.steModeLowerCase,
            multiline:      false,
            focus:          true,
            maxLength: 30
        },
        this.keyModel =
        {
            originalValue : ''
        });

    Mojo.Event.listen(this.controller.get('get_button'),
        Mojo.Event.tap, this.handleGetButton.bind(this));
    Mojo.Event.listen(this.controller.get("back_button"),
        Mojo.Event.tap, this.handleBackButton.bind(this));
}

GetAssistant.prototype.cleanup = function(event) {
    this.controller.stopListening(
        this.controller.get('get_button'), Mojo.Event.tap,
        this.handleGetButton);
    this.controller.stopListening(
        this.controller.get('back_button'), Mojo.Event.tap,
        this.handleBackButton);
}

GetAssistant.prototype.handleGetButton = function(event) {
    this.depot.get(this.keyModel.originalValue,
        this.dbGetSuccess.bind(this),
        this.dbGetFailure.bind(this));
}

GetAssistant.prototype.dbGetSuccess = function(response) {
    var recordSize = Object.values(response).size();
    if(recordSize == 0) {
        this.controller.get("response").innerHTML = "No such record in the database";
    } else {
        this.controller.get("response").innerHTML = "Data entry is: " + response.value;
    }
}

GetAssistant.prototype.dbGetFailure = function(transaction, result) {
    this.controller.get("response").innerHTML = "Error: " + result.message;
}

GetAssistant.prototype.handleBackButton = function(event) {
    this.controller.stageController.popScene();
}

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

WebOS Depot Search GUI

Для получения объекта из хранилища мы используем метод get() объекта Depot. В этом методе указывается ключ и callback-методы, которые отработают при удачном или неудачном завершении транзакции.

На этом все. В этой статье мыузнали:

  • Чем отличается Depot от базы данных SQLite.
  • Как создать объект Depot в хранилище по умолчанию или на разделе с медиа
  • Как создать соединение с Depot
  • Как записать данные в Depot
  • Как получить данные из Depot
  • Как обрабатывать результаты транзакций в callback-методах.

Скачать исходный код примера.