Вспомним основную задачу (часть 1):
Задача: Создать приложение, которое позволит осуществлять приемку товара в розничном магазине при перемещении со склада компании.
В этой части:
- Создадим приложение, кастомизированое под 1С
- Будем выполнять авторизацию в базе 1С.
Касательно веб-сервисов рассмотрим:
- Вызов операции сервиса без параметров.
- Вызов операции сервиса с параметрами примитивного типа.
Начнем
Для вызова операции веб-сервиса пользователю нужна роль, которой доступен вызов операций этого сервиса. Логично, что вызов операций проходит с теми же логином и паролем, что и вход в обыное приложение 1С. Если устройства персонализированы, то все достаточно просто - можно хранить в настройках тот самый пароль. Расммотим более тяжелый случай, когда с одного и того же устройства могут авторизоваться несколько человек.
Какие осложнения нам это дает:
1) Список пользователей должен быть динамическим
2) Пароли на устройстве хранить нельзя
3) Для обращению к списку пользователей ИБ нужны административные права
Тогда мы создадим отдельного пользователя с минимальным набором прав, который даст список пользователей, затем будем выбирать нужного, вводить пароль и начинать работу.
Создадим:
- Пользователя "WSuser", который получит логины.
- Регистр сведений "ПользователиМобильногоКлиента" с одним-единственным измерением "Пользователь", в котором будем хранить список пользователей для авторизации на мобильном клиенте
- XDTO-пакет "AcceptingOrdersPackage" с URI "AcceptingOrdersService"
- Веб-серис "AcceptingOrders", определим 2 операции "GetLoginList" и "Login" первая, соответственно, будет получать список пользователей, вторая проверять имя пользователя и пароль.
- Роль "СлужебныйПользовательМобильногоКлиента", у которой есть доступ к обеим операциям сервиса, право чтения регистра "ПользователиМобильногоКлиента" и справочника "Пользователи", право администрирования.
Теперь наш пакет и веб-сверис имеют такой вид:
Операция GetLoginList:
Функция GetLoginList()
Запрос = Новый Запрос("ВЫБРАТЬ РАЗЛИЧНЫЕ
| ПользователиМобильногоКлиента.Пользователь.ИдентификаторПользователяИБ КАК ИдентификаторПользователяИБ,
| ПользователиМобильногоКлиента.Пользователь.Наименование КАК Наименование
|ИЗ
| РегистрСведений.ПользователиМобильногоКлиента КАК ПользователиМобильногоКлиента");
Выборка = Запрос.Выполнить().Выбрать();
xdtoТипОтвета = ФабрикаXDTO.Тип("AcceptingOrdersService", "LoginList");
xdtoТипЛогин = ФабрикаXDTO.Тип("AcceptingOrdersService", "Login");
xdtoОтвет = ФабрикаXDTO.Создать(xdtoТипОтвета);
Пока Выборка.Следующий() Цикл
Если Не ЗначениеЗаполнено(Выборка.ИдентификаторПользователяИБ) Тогда
Продолжить;
КонецЕсли;
xdtoЛогин = ФабрикаXDTO.Создать(xdtoТипЛогин);
xdtoЛогин.Description = Выборка.Наименование;
xdtoЛогин.ID = Строка(Выборка.ИдентификаторПользователяИБ);
xdtoОтвет.Login.Добавить(xdtoЛогин);
КонецЦикла;
Возврат xdtoОтвет;
КонецФункции
Операция Login:
Функция Login(ID, Password)
xdtoТипОтвета = ФабрикаXDTO.Тип("AcceptingOrdersService", "LoginResult");
xdtoОтвет = ФабрикаXDTO.Создать(xdtoТипОтвета);
xdtoОтвет.Name = "";
ИдентификаторПользователя = Новый УникальныйИдентификатор(ID);
ПользовательИБ = ПользователиИнформационнойБазы.НайтиПоУникальномуИдентификатору(ИдентификаторПользователя);
Если ПользовательИБ <> Неопределено
И ПользовательИБ.СохраняемоеЗначениеПароля = СтрЗаменить(Password, Символы.ПС, "") Тогда
xdtoОтвет.Name = ПользовательИБ.Имя;
xdtoОтвет.Result = Истина;
Возврат xdtoОтвет;
КонецЕсли;
xdtoОтвет.Result = Ложь;
Возврат xdtoОтвет;
КонецФункции
Серверная часть готова, переходим к Android
В файлах есть архив с проектом для Android Studio, если необходимо. Весь код выкладывать будет излишним, буду разбирать только самое необходимое
Далее не претендую на грамотность объяснений и оптимальность кода, расскажу как понимаю сам, проводя иногда аналоги с 1С.
Что важно понимать при программировании обращения к сетевым ресурсам:
В андроид (Java) есть понятие потоков , параллельно работающих процессов. Основной поток программы управляет главной активностью (окно программы по-простому) и нельзя в основном потоке выполнять действия, которые будут "занимать" его. Таким образом, для обращения к веб-сервису мы должны создать дополнительный поток, Thread (я бы сказал, что это очень похоже на фоновое задание). Для того, чтобы получить ответ используется обработчик, Handler (как ОбработкаОповещения).
То есть мы создаем параллельный поток и просто ждем ответа от него. Это можно сравнить с подбором товара в табличную часть документа - при нажатии кнопки открывается общая форма, но при этом форма документа продолжает работать в обычном режиме. А при выборе строки в форме подбора срабатывает обработчик оповещения, в параметры которого передается строка.
Рассмотрим основной класс - MainActivity
Опустим описание переменных кроме трех:
public static final int ACTION_ConnectionError = 0;
public static final int ACTION_GetLoginList = 1;
public static final int ACTION_Login = 2;
Это вспомогательные переменные, имеющие фиксированное значение.
1) Обработчик создания
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Инициализируем вспомогательный класс
uiManager = new UIManager(this);
// Инициализируем менеджер настроек
preferences = PreferenceManager.getDefaultSharedPreferences(this);
// Читаем идентификатор последнего выбранного пользователя из настроек
wsParam_LoginID = preferences.getString("LoginID", "");
// Читаем настройки подключения
initiateConnectionSettings();
// Инициализируем обработчик ответа от сервиса
soapHandler = new incomingHandler(this);
if (soapParam_URL.equals(""))
// Первый запуск, открываем настройки
openSettings();
else {
// Выводим на экран форму авторизации
setContentView(R.layout.activity_main);
// Запрашиваем список пользователей
startExchange(ACTION_GetLoginList);
}
}
Думаю, комментарии излишни
2) Класс для обработки сообщений от сервиса (от параллельного потока)
private static class incomingHandler extends Handler {
private final WeakReference<MainActivity> mTarget;
// Конструктор
public incomingHandler(MainActivity context){
mTarget = new WeakReference<>(context);
}
@Override
public void handleMessage(Message msg) {
MainActivity target = mTarget.get();
switch (msg.what) {
case ACTION_ConnectionError:
uiManager.showToast("Ошибка" + getSoapErrorMessage());
break;
case ACTION_GetLoginList:
target.initiateLoginList();
break;
case ACTION_Login:
target.checkLoginResult();
break;
}
}
}
Метод "handleMessage" как раз выполняет обработку сообщения, тело сообщения - msg.what. В зависимости от значения выполняем обработку. Как мы видим, при получения сообщения об ошибке соединения, выводится сообщение. О том как оно формируется - ниже.
3) Обработчик заполнения списка пользователей после получения ответа от сервиса
protected void initiateLoginList(){
ArrayList<String> loginList = new ArrayList<>();
loginIDList = new ArrayList<>();
int count = soapParam_Response.getPropertyCount();
int position = 0;
for (int i = 0; i < count; i++) {
SoapObject login = (SoapObject) soapParam_Response.getProperty(i);
String name = login.getPropertyAsString("Description");
String id = login.getPropertyAsString("ID");
loginList.add(name);
loginIDList.add(id);
if (wsParam_LoginID.equals(id)){
position = i;
}
}
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, R.layout.spinner_user, loginList);
Spinner spinner = (Spinner) findViewById(R.id.spinner);
spinner.setPrompt("Выберите пользователя");
spinner.setAdapter(adapter);
spinner.setSelection(position);
spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
saveUserID(position);
}
@Override
public void onNothingSelected(AdapterView<?> arg0) {
}
});
}
Давайте обратим внимание на цикл:
int count = soapParam_Response.getPropertyCount();
int position = 0;
for (int i = 0; i < count; i++) {
SoapObject login = (SoapObject) soapParam_Response.getProperty(i);
String name = login.getPropertyAsString("Description");
String id = login.getPropertyAsString("ID");
loginList.add(name);
loginIDList.add(id);
if (wsParam_LoginID.equals(id)){
position = i;
}
}
В 1С в xdto пакете мы определили тип "LoginList" со свойством типа "Login" и переменная "soapParam_Response" - ответ от сервиса типа "LoginList". Если в отладке 1С мы попытаемся посмотреть свойства пакета, то будет одно свойство типа "Login" и в нем только несколько экземпляров. Это несколько искажает представление о том, как работать с xdto в Android. Тут же у пакета "soapParam_Response" столько свойств с именем "Login", сколько экземпляров мы в него добавили в 1С. То есть у нас count логинов и мы столько же свойств и получаем - getProperty(i). Если бы в пакете были свойства другого типа, нам пришлось бы проверять имя свойства. В дальнейшем такие моменты тоже разберем.
Сам же экземпляр списка, полученный методом getProperty(i) имеет тип так же SoapObject. Его поля имеют строковый тип и получаются так - getPropertyAsString("Description").
В данном обработчике мы заполняем выпадающий список пользователей и в качестве текущего устанавливаем последний выбранный.
4) Обработчик авторизации
public void checkLoginResult(){
Boolean isLoginSuccess = Boolean.parseBoolean(soapParam_Response.getPropertyAsString("Result"));
if (isLoginSuccess){
soapParam_user = soapParam_Response.getPropertyAsString("Name");
EditText wsParam_Password = (EditText) findViewById(R.id.wsParam_Password);
soapParam_pass = wsParam_Password.getText().toString();
setActivityTaskList();
}
else
uiManager.showToast("Ошибка! Неверно введен пароль");
}
Обратите внимание на то, как мы получаем реквизит типа булево - приходится парсить его из строки. Так же можно воспользоваться методом "getPrimitiveProperty". Аналогично с данными типа число.
5) Обработчик разбора ошибок сервиса
private static String getSoapErrorMessage () {
String errorMessage;
if (responseFault == null)
errorMessage = "Отсутствует соединение с сервером.";
else{
try {
errorMessage = responseFault.faultstring;
}
catch (Exception e) {
e.printStackTrace();
errorMessage = "Неизвестная ошибка.";
}
}
return errorMessage;
}
В случае ошибки при вычислении на стороне 1С в responseFault.faultstring будет полное описание ошибки.
6) Вызов операции веб-сервера
protected void startExchange(int ACTION){
SOAP_Dispatcher dispatcher = new SOAP_Dispatcher(soapParam_timeout, soapParam_URL, soapParam_user, soapParam_pass, ACTION);
dispatcher.start();
}
В качестве параметра передается переменная ACTION - номер операции для вызова сервиса.
Рассмотрим класс SOAP_Dispatcher - это основной класс для работы с сервисом 1С
public class SOAP_Dispatcher extends Thread {
int timeout;
String URL;
String user;
String pass;
int ACTION;
SoapObject soap_Response;
final String NAMESPACE = "AcceptingOrdersService";
public SOAP_Dispatcher(int soapParam_timeout, String soapParam_URL, String soapParam_user, String soapParam_pass, int SOAP_ACTION){
timeout = soapParam_timeout;
URL = soapParam_URL;
user = soapParam_user;
pass = soapParam_pass;
ACTION = SOAP_ACTION;
}
@Override
public void run() {
switch (ACTION) {
case MainActivity.ACTION_GetLoginList:
GetLoginList();
break;
case MainActivity.ACTION_Login:
Login();
break;
}
if (soap_Response != null) {
MainActivity.soapParam_Response = soap_Response;
MainActivity.soapHandler.sendEmptyMessage(ACTION);
} else {
MainActivity.soapHandler.sendEmptyMessage(MainActivity.ACTION_ConnectionError);
}
}
void GetLoginList(){
String method = "GetLoginList";
String action = NAMESPACE + "#AcceptingOrders:" + method;
SoapObject request = new SoapObject(NAMESPACE, method);
soap_Response = callWebService(request, action);
}
void Login(){
String method = "Login";
String action = NAMESPACE + "#AcceptingOrders:" + method;
SoapObject request = new SoapObject(NAMESPACE, method);
request.addProperty("ID", MainActivity.wsParam_LoginID);
request.addProperty("Password", MainActivity.wsParam_PassHash);
soap_Response = callWebService(request, action);
}
private SoapObject callWebService(SoapObject request, String action){
SoapSerializationEnvelope envelope = new SoapSerializationEnvelope(SoapEnvelope.VER11);
envelope.setOutputSoapObject(request);
envelope.dotNet = true;
envelope.implicitTypes = true;
HttpTransportSE androidHttpTransport = new HttpTransportBasicAuthSE(URL, user, pass, timeout);
androidHttpTransport.debug = true;
try {
androidHttpTransport.call(action, envelope);
return (SoapObject) envelope.getResponse();
} catch (Exception e) {
e.printStackTrace();
MainActivity.responseFault = (SoapFault) envelope.bodyIn;
}
return null;
}
}
Здесь:
NAMESPACE - пространство имен, заданное в 1С
timeout,URL,user,pass - параметры подключения
public SOAP_Dispatcher - конструктор класса
run() - обработчик, срабатываемый при вызове метода start() экземпляра класса (смотри 6) Вызов операции веб-сервера)
Далее следуют как раз вызовы операций веб-сервисов:
void GetLoginList(){
String method = "GetLoginList";
String action = NAMESPACE + "#AcceptingOrders:" + method;
SoapObject request = new SoapObject(NAMESPACE, method);
soap_Response = callWebService(request, action);
}
Здесь:
- method - имя операции веб сервиса как в 1С
- AcceptingOrders - имя сервиса
- request - вспомогательная переменная
- callWebService - служебный метод, его можно просто скопировать.
- soap_Response - ответ от сервиса.
Обратите внимание: переменная soap_Response имеет тип SoapObject, как и метод callWebService. Если операция веб-сервиса будет возвращать ответ примитивного типа - string или boolean, то ответ будет типа SoapPrimitive и для таких операций нужны отдельные методы и переменные.
void Login(){
String method = "Login";
String action = NAMESPACE + "#AcceptingOrders:" + method;
SoapObject request = new SoapObject(NAMESPACE, method);
request.addProperty("ID", MainActivity.wsParam_LoginID);
request.addProperty("Password", MainActivity.wsParam_PassHash);
soap_Response = callWebService(request, action);
}
Это уже вызов операции с параметрами. Ничего сложного нет, параметр вставляется так:
request.addProperty("ID", MainActivity.wsParam_LoginID);
Таким образом, класс SOAP_Dispatcher получился довольно универсальным и добавление операции на сервисе не создаст нам трудностей.
Пароли
Теперь еще раз о паролях. Повторим то, с чего начали - мы имеем устройство не персональное, то есть любой пользователь должен иметь возможность авторизоваться с любого устройства. Понятно, что для удобства список пользователей должен быть доступен на устройстве, для этого мы придумали служебного пользователя. Как же поступаем дальше?
Вот мы ввели пароль, теперь выполним проверку. В 1С у пользователя ИБ есть свойство "СохраняемоеЗначениеПароля" которое составляется следующим образом: вычисляется хеш-функция SHA1 от пароля и через запятую хеш-функция от пароля в верхнем регистре. Из соображений безопасности нам этот хеш надо вычислять на мобильном устройстве. Для работы с текстом есть отдельный класс Parser_Text
Вот метод, вычисляющий хеш от пароля:
public static String getPassHash(String text){
return base64string(sha1(text)) + "," + base64string(sha1(text.toUpperCase()));
}
И уже далее если хеш пароля совпадает с хранимым в 1С, мы продолжаем работу с сервисом, но не с логином WSuser, а уже под выбранным пользователем. Для этого операция Login возвращает нам результат проверки пароля и имя пользователя для входа.
Кастомизация
Далее не относящееся к веб-серсиам. Сделаем наше приложение более-менее похожим на 1С:
Чтобы все элементы управления были одинаковыми без вмешательства разработчика, в файле Styles.xml описываем стили.
<style name="AppTheme" parent="android:Theme.NoTitleBar.Fullscreen">
<item name="android:textViewStyle">@style/textViewStyle</item>
<item name="android:editTextStyle">@style/editTextStyle</item>
<item name="android:spinnerStyle">@style/spinnerStyle</item>
<item name="android:windowBackground">@color/form</item>
</style>
<style name="textViewStyle" parent="android:Widget.TextView">
<item name="android:textColor">@color/text</item>
<item name="android:textSize">@dimen/textsize</item>
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_marginTop">@dimen/layout_vertical_margin</item>
<item name="android:paddingBottom">@dimen/element_vertical_padding</item>
<item name="android:paddingTop">@dimen/element_vertical_padding</item>
<item name="android:paddingLeft">@dimen/element_horizontal_padding</item>
<item name="android:paddingRight">@dimen/element_horizontal_padding</item>
</style>
<style name="editTextStyle" parent="android:Widget.EditText">
<item name="android:textColor">@color/text</item>
<item name="android:textSize">@dimen/textsize</item>
<item name="android:background">@drawable/edit</item>
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_marginTop">@dimen/layout_vertical_margin</item>
<item name="android:singleLine">true</item>
</style>
<style name="spinnerStyle" parent="android:Widget.Spinner">
<item name="android:textColor">@color/text</item>
<item name="android:textSize">@dimen/textsize</item>
<item name="android:background">@drawable/spinner</item>
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:dropDownWidth">wrap_content</item>
</style>
Все необходимые ресурсы есть в архиве с проектом.
В следующей части:
- Поработаем с последовательностью xdto (ПоследовательностьXDTO в 1С)
- Посканируем при помощи камеры устройства
- Вызовем операцию сервиса с параметром типа xdtoОбъект.
Каждый раз я буду выкладывать архив проекта, если нет необходимости - не качайте