Материал: Мобильное приложение для оценки эффективности мерчендайзинга торговой компании

Внимание! Если размещение файла нарушает Ваши авторские права, то обязательно сообщите нам

}(ACTION_LOGIN.equals(action)) {(intent);

} else if (ACTION_UPLOAD_PICTURE.(action)) {();

} else if (ACTION_SAVE_PICTURE.(action)) {(intent);

}

} else {(HOST_NOT_SPECIFIED);

}

} catch (UnknownHostException ex) {(UNKNOWN_HOST);

} catch (IOException ex) {.printStackTrace();(IO_ERROR);

}}

Метод on Handle Intent определён в базовом классе Intent Service и работает следующим образом: когда какой-либо компонент посылает сервису запрос с помощью интента, передаваемого в метод start Service, данный интент оборачивается в объект Message и добавляется в очередь обработки сообщений, ассоциированную с экземпляром класса Handler. Каждая очередь также ассоциирована с определённым потоком, в котором будет производится обработка сообщений. Если объект Handler определяет, что поток обработки простаивает, то он извлекает очередное сообщение (объект класса Message) из очереди и передаёт его в метод handle Message. Этот метод класса Handler не производит никакой обработки сообщения, поэтому для создания работоспособной очереди обработки используются наследники класса Handler. Класс Intent Service содержит ссылку на потомок класса Handler, который в методе handle Message извлекает интент из объекта Message и передаёт в метод on Handle Intent. Сам Intent Service, аналогично классу Handler, не обеспечивает никакой обработки переданных в on Handle Intent интентов, для чего также используются классы-наследники, коим и является класс Worker Service. Таким образом, метод on Handle Intent гарантированно вызывается в отличном от главного потоке, а блок catch с типом IO Exception обеспечивает обработку ошибок ввода/вывода.

В методе on Handle Intent нет прямых вызовов методов интерфейса IWeb Client, однако они имеются внутри методов log In, upload Picture и save Picture. Листинг этих методов приведён ниже вместе с листингом метода report Error, обеспечивающего информирование об ошибках, а также аналогичного ему метода send Pending, информирующего об успешном окончании операции.

private void reportError(String message) {{.send(this, SERVICE_ERROR, Intent().(CommonConstants.ERROR_FIELD, message));

} catch (PendingIntent.CanceledException e) {.printStackTrace();

}void sendPending(Intent intent) {{.send(this, Activity.RESULT_OK, intent);

} catch (PendingIntent.CanceledException e) {.printStackTrace();

}

Нетрудно заметить, что метод send Pending отличается от report Error тем, что использует код Activity.RESULT_OK и не передаёт никакой дополнительной информации об ошибках.

private void logIn(Intent intent) throws IOException {login = intent.getStringExtra(.LOGIN_FIELD);password = intent.getStringExtra(.PASSWORD_FIELD);role = webClient.logIn(login, password);(new Intent().(CommonConstants.ROLE_FIELD, role));(AuthorizationHelper.isValidRole(role)) {provider =

DataProvider.getInstance(this);.updateStores(webClient.updateStores(.getLastStoresVersion()));.updateCategories(webClient.(provider.

getLastCategoriesVersion()));

}

Данный метод инкапсулирует извлечение из интента необходимых параметров, а также осуществляет обновление кэша при успешной авторизации. Такое совмещение действий оказывается весьма удобным, если принять во внимание то, что, во-первых, метод logIn выполняется в фоновом потоке, и, следовательно, длительные и ресурсоёмкие операции с базой данных не будут занимать основной поток выполнения. Во-вторых, приложение не должно допускать пользователя к редактированию метаданных до тех пор, пока кэш находится в неактуальном состоянии. Таким образом, операции загрузки и сохранения фотографии гарантированно не будут извлечены из очереди обработки сообщений до тех пор, пока не закончится обновление БД.

private void uploadPicture() throws IOException {(SERVICE_UPLOADING,());[] picture = ImageStore.getImage();fileId = webClient.uploadPicture(picture);(true);(fileId != null) {(new Intent().putExtra(.FILE_ID_FIELD, fileId));;

}("Uploading error");

}

Метод upload Picture, осуществляющий загрузку фото на сервер, показывает сразу два характерных для Android-приложений решения. Во-первых, поскольку операция загрузки снимка занимает очень большое количество времени и при этом не должна прерываться, для её выполнения сервис входит в привилегированный режим с помощью вызова start Foreground. Это означает, что пользователь может спокойно свернуть приложение дожидаясь окончания загрузки, не опасаясь того, что ОС завершит приложение с целью экономии ресурсов. По завершению загрузки, сервис выходит из привилегированного режима используя метод stop Foreground. В качестве одного из параметров метод start Foreground принимает специальный объект-нотификацию, представляющую собой сообщение в строке состояния телефона, указывающее на то, что в данный момент происходит некоторая длительная высокоприоритетная операция. Метод stop Foreground, в свою очередь, принимает в качестве параметра флаг, указывающий на то, следует ли автоматически убрать нотификацию, созданную при входе в привилегированный режим или же пользователь должен будет сделать это сам.

Во-вторых, в метод upload Picture можно видеть, что изображение для загрузки передаётся не в качестве параметра интента, как это сделано с логином и паролем в методе logIn, а через статическое поле в классе Image Store. Это сделано для того, чтобы обойти ограничение, накладываемое интентом на размер своих параметров. Изображения зачастую превышают определённый для параметров лимит байт, что приводит к возникновению исключений во время выполнения программы.

private void savePicture(Intent intent) throws IOException {metadata = intent.getParcelableExtra(.METADATA_FIELD);result = webClient.sendMetadata(metadata);(!result) {("Save metadata error");

} else {.clean();(new Intent());

}

Метод save Picture, аналогично методу logIn, инкапсулирует извлечение данных из интента, однако если логин и пароль передавались как простые строковые параметры, то здесь используется сложная структура Metadata. Для обеспечения пересылки сложных структур данных можно воспользоваться предоставляемой Java бинарной сериализацией объектов, однако это не является эффективным способом конверси данных. Вместо этого, Android SDK предоставляет куда более гибкий и быстрый способ основанный на интерфейсе Parcelable. Каждый Parcelable-объект должен реализовывать метод void write To Parcel (Parcel parcel, int i), который вызывается каждый раз при преобразовании данных для пересылки. Объект parcel представляет собой очередь, способную хранить строки, примитивные типы и их массивы. Помимо этого, реализующие Parcelable классы должны содержать статическое поле CREATOR с объектом класса наследника типа Parcelable. Creator<Т>, где Т - тип, который содержит поле CREATOR. Инетрфейс Parcelable. Creator<Т> определяет метод Т create From Parcel (Parcel in), который вызывается на принимающей стороне. Этот метод извлекает из очереди in данные и создаёт из них копию пересылаемого Parcelable-объекта. Пример реализации интерфейса Parcelable представлен ниже с помощью фрагментов класса Metadata:

public class Metadata implements Parcelable {

<Объявление свойств и констант>

@Overridevoid writeToParcel(Parcel parcel, int i) {.writeInt(storeId)

< Запись остальных свойств в очередь parcel>

parcel.writeString(wasTaken);

}static final Parcelable.Creator<Metadata> CREATOR=Parcelable.Creator<Metadata>() {Metadata createFromParcel(Parcel in) {metadata = new Metadata();.storeId = in.readInt();

< Извлечение остальных свойств из очереди in>

metadata.wasTaken = in.readString();metadata;

}Metadata[] newArray(int size) { new Metadata[size];

}

В данном случае, объект CREATOR представлен анонимны классом, что упрощает инициализацию создаваемого им объекта класса Metadata.

Реализацию методов интерфейса I Web Client рассмотрим на примере метода logIn класса Web Client, листинг которого приведён ниже:

@OverrideString logIn(String login, String password)IOException {= "Basic "+ Base64.encodeToString(.format("%s:%s", login, password).(), Base64.NO_WRAP);response =(API_LOGIN, new JsonHelper().toStringEntity());(response != null) {status = response.().getStatusCode();(status == HttpStatus.SC_UNAUTHORIZED) {AuthorizationHelper.AUTHORIZATION_ERROR;

} else if (status == HttpStatus.SC_OK){reader =.getReaderForResponse(response);role = null;(reader != null) {{.beginObject();.nextName();= reader.nextString();.close();

} catch (IOException e) {.printStackTrace();

}role;

}response.getStatusLine().toString();

}"Error: Unable to get response";

}

Метод log In хорошо показывает особенности реализации веб-клиента в данном проекте. В нём можно выделить несколько основных элементов, также присутствующих во всех остальных методах веб-клиента:

- Вызов метода post. Данный метод формирует HTTP POST запрос с телом, переданным во втором параметре, и отправляет его по адресу веб-метода, указанного первым параметром. Листинг этого метода будет приведён далее по тексту.

-   Использование объекта Json Helper для формирования тела запроса. Остальные методы сначала использую несколько вызовов метода Json Helper.add для инициализации объекта, а затем приводят его к необходимому типу вызовом метода to String Entity, также приведённого ниже. Исключением является метод upload Picture, сразу использующий объект Byte Array Entity, поскольку телом запроса для этого метода не является JSON-объект.

-   Получение из HTTP-ответа потокового JSON-парсера, с помощью статического метода Json Helper. Get Reader For Response.

-   Последовательное извлечение данных из HTTP-потока с помощью ранее полученного JSON-парсера.

Листинг метода post:

HttpResponse post(String method, HttpEntity entity)IOException {request = new HttpPost(baseUrl + method);.setEntity(entity);.setHeader("Accept", "application/json");(entity instanceof StringEntity) {.setHeader("Content-type",

"application/json; charset=UTF-8");

}.setHeader("Authorization", authHeader); client.execute(request);

}

В методе осуществляется объединение базового адреса веб API с именем конкретного веб-метода, создание нового экземпляра HTTP POST запроса, установка его тела, а также HTTP-заголовков, содержащих тип содержимого, тип ответа и токен авторизации. В качестве возвращаемого значения выступает объект типа Http Response, представляющий собой HTTP-ответ на созданный HTTP POST запрос.

Логика методов to String Entity и get Reader For Response весьма прямолинейна, однако хорошо иллюстрирует типичные способы конвертации данных при взаимодействии с REST API в Android-приложениях:

public StringEntity toStringEntity() {{new StringEntity(.toString(), HTTP.UTF_8);

} catch (UnsupportedEncodingException e) {.printStackTrace();

}null;

}static JsonReader getReaderForResponse(response) {(response != null) {{new JsonReader(InputStreamReader(.getEntity().getContent()));

} catch (IOException e) {.printStackTrace();

}

}

return null;}

5. Программа и методика испытаний

Система тестирования Android предоставляет архитектуру и мощные инструменты для тестирования всех частей приложения на всех уровнях разработки (от отдельной части программы до приложения в целом). Система тестирования включает в себя следующие особенности:

.        Наборы тестов Android основаных на JUnit. Для тестирования класса, который не использует Android API, можно использовать обычные тесты из JUnit, или использовать расширения Android JUnit, для тестирования Android компонентов.

.        Android расширения JUnit предоставляют компоненто-ориентированные классы тестов. Эти классы предоставляют вспомогательные методы для создания mock-объектов и методов, которые помогают управлять жизненным циклом компонентов.

.        Наборы тестов содержатся в тестовых пакетах, которые похожи на основные пакеты программ

.        Инструменты SDK для создания и тестирования доступны в виде плагинов к IDE, а также в виде консольных приложений. Эти инструменты получают информацию из проекта тестируемого приложения и используют ее для автоматического создания build-файлов, файла манифеста и структуры каталогов для тестового пакета.

.        SDK также предоставляет monkeyrunner - API тестирования устройств с программами на Python, и UI/Application Exerciser Monkey (консольный инструмент для стресс-тестирования пользовательских интерфейсов путем отправки псевдослучайных событий на устройство.

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

В основном тестирование производилось на смартфоне Samsung Galaxy S II с операционной системой Android 4.1.2 и разрешением экрана в 800х480 точек. Смартфон обладает всеми особенностями, необходимыми для полноценной работы всех возможностей приложения, поэтому дополнительно проводились тесты на других устройствах. В некоторых тестах использовался смартфон Huawei U8230 с установленной на него модифицированной версией ОС Android - CyanogenMod 9, однако должной вариативности это не принесло, хотя и позволило проверить отличное от 800х480 разрешение на реальном устройстве.

Для того, чтобы проверить работу приложения в различных условиях, использовался встроенный в SDK эмулятор ОС Android. Эмулятор позволяет создавать виртуальные устройства с практически любыми конфигурациями оборудования, однако не может эмулировать некоторые функции камеры, например, такие как вспышка и автофокус. Исходя из этого, эмулятор использовался в первую очередь для тестирования пользовательского интерфейса на различных разрешениях экрана. Для этого был создан один экземпляр предустановленной виртуальной машины «Nexus 4», обладающей характеристиками одноимённого смартфона, для которой, в последствии, менялось разрешения экрана, не затрагивая остальных технических параметров. Поскольку эмулятор, входящий в Android SDK обладает очень плохим быстродействием, побочным положительным эффектом такого тестирования явилась проверка на работоспособность приложения на «слабых» устройствах, не обладающих достаточно мощными аппаратными характеристиками.

Для того, чтобы решить проблему с быстродействием стандартного эмулятора, использующего микроархитектуру ARM для виртуальных машин, для тестов критических состояний, таких как обрыв сети или недоступность GPS, использовался сторонний эмулятор Genymotion. В отличие от стандартного эмулятора, Genymotion использует х86 архитектуру, что позволяет использовать технологии аппаратной виртуализации и в десятки раз ускоряет работу эмулятора. Помимо этого, Genymotion обладает значительно более удобной системой эмуляции местоположения. К недостаткам данного эмулятора можно отнести крайне нестабильную работу: Genymotion работает в паре с системой виртуализацииVirtual Box и при этом довольно часто теряет с ней соединение, что приводит к рестарту эмулятора. Также не редки и «тихие» вылеты на рабочий стол без выдачи каких-либо сообщений об ошибках. Несмотря на это, Genymotion является гораздо более удобным в работе, в основном благодаря невероятно высокой скорости работы, по сравнению со стандартным эмулятором. Стоит отметить, что поскольку Genymotion базируется на микроархитектуре х86, он не всегда в полной мере позволяет протестировать приложения, использующие нативные библиотеки, написанные на С/С++ и собранные с помощью Android NDK, поскольку, в их случае, такие библиотеки собираются отдельно под каждый конкретный тип архитектуры. Так как данный проект не использует таких библиотек, то эта особенность несущественна.

Говоря о программной поддержке тестирования приложения, стоит отметить несколько классов, которые использовались на различных этапах разработки проекта для обеспечения работоспособности в условиях не до конца готовой системы, а также для проверки экстремальных ситуаций на реальном устройстве. Ниже приведён частичный листинг класса FakeWebClient, использовавшийся в проекте для тестирования функциональности в то время как сервер был ещё не готов:

public class FakeWebClient implements IWebClient {

@OverrideString logIn(String login, String password) {"user";

}

@OverrideList<StoreEntity> updateCache(int version) {null;

}

@OverrideString uploadPicture(byte[] picture) {{out =FileOutputStream(String.format(

"/storage/extSdCard/X/screen%d.jpg",.getInstance().getTimeInMillis()));.write(picture);.close();

} catch (Exception e) {.printStackTrace();

}"some_random_generated_id";

}

@Overrideboolean sendMetadata(Metadata data) { true;

}

Данная версия вместо отправки фото на сервер, сохраняет его на SD-карту телефона для проверки качества получаемых фотографий.

Помимо нескольких классов, наподобие Fake Web Client, которые использовались для проверки остальных веб-методов, следует отметить класс Location Helper, содержащий несколько методов для симуляции ошибочных ситуаций. В качестве примера, ниже приведён листинг метода emulate Location Error, имитирующего помехи в работе GPS и возвращающего геокоординаты каждый третий запрос:

public static void emulateLocationError(Context context,PendingIntent pendingIntent) {++;(counter > 2) {= 0;(context, pendingIntent);;

}Looper myLooper = Looper.myLooper();Handler handler = new Handler(myLooper);.postDelayed(new Runnable() {void run() {(context, pendingIntent);

}

}, TIMEOUT - 3000);

Таким образом, несмотря на отсутствие unit- и monkey- тестов, в проекте были созданы условия для довольно информативного, хоть и не автоматизированного тестирования. Кроме этого итоговый продукт был протестирован на достаточно большом количестве конфигураций, как виртуальных, так и физических.