|
|
@@ -1,4 +1,20 @@
|
|
|
-# Проект "база". Авторизация на сервере (Basic auth, token).
|
|
|
+<table style="width: 100%;"><tr><td style="width: 40%;">
|
|
|
+<a href="../articles/weather2.md">Проект погода (продолжение): SplashScreen (заставка). Выбор города. Выбор и отображение массива значений (почасовая, ежедневная). Разбор XML.
|
|
|
+</a></td><td style="width: 20%;">
|
|
|
+<a href="../readme.md">Содержание
|
|
|
+</a></td><td style="width: 40%;">
|
|
|
+<a href="../articles/android_auth.md">Проект "база". Авторизация на сервере (Basic auth, token). POST-запросы. API.
|
|
|
+</a></td><tr></table>
|
|
|
+
|
|
|
+# Проект "база". Авторизация на сервере (Basic auth, token). POST-запросы. API.
|
|
|
+
|
|
|
+**Содержание**
|
|
|
+
|
|
|
+* [API](#API)
|
|
|
+* [Первичная настройка приложения](#Первичная-настройка-приложения)
|
|
|
+* [Добавление альбомной ориентации](#Добавление-альбомной-ориентации)
|
|
|
+* [Модальный диалог авторизации](#Модальный-диалог-авторизации)
|
|
|
+* [HTTP-запросы, методы, форматы, заголовки.](#HTTP-запросы-методы-форматы-заголовки)
|
|
|
|
|
|
## API
|
|
|
|
|
|
@@ -120,6 +136,17 @@
|
|
|
|
|
|
То есть при получении ответа вы должны разобрать полученный JSON, если есть токен, то можно продолжать работать. Если ошибка, то показать **Alert** с ошибкой и остаться на экране авторизации.
|
|
|
|
|
|
+ >Маловероятно, но вдруг попадётся задача сделать "базовую авторизацию" (Basic Auth)
|
|
|
+ >При таком методе авторизации в запрос нужно добавить заголовок `Authorization: Basic <логин:пароль в кодировке base64>`
|
|
|
+ >
|
|
|
+ >```kt
|
|
|
+ >Base64.encodeToString(
|
|
|
+ > "$login:$password".toByteArray(),
|
|
|
+ > Base64.NO_WRAP)
|
|
|
+ >```
|
|
|
+ >
|
|
|
+ >Базовая авторизация позволяет использовать GET-запрос, т.к. в теле запроса ничего не предается
|
|
|
+
|
|
|
4. Для выхода нужно послать **POST** запрос **/logout** c параметом *username*:
|
|
|
|
|
|
```
|
|
|
@@ -167,3 +194,546 @@
|
|
|
```
|
|
|
GET {{url}}/img/tire_0.jpg
|
|
|
```
|
|
|
+
|
|
|
+ Обратите внимание, для загрузки картинок я использую не всю строку, которая у вас в базе (`\products\tire_0.jpg`), а только название файла.
|
|
|
+
|
|
|
+ Чтобы вытащить название из строки можно использовать метод *split* - он делит исходную строку на список подстрок по указанному разделителю:
|
|
|
+
|
|
|
+ ```kt
|
|
|
+ val fileName = imageNameFromDB.split("\\").lastOrNull()
|
|
|
+ if(fileName != null){
|
|
|
+ ...
|
|
|
+ }
|
|
|
+ ```
|
|
|
+
|
|
|
+ Если условие поиска в строке сложнее, то можно применить регулярные выражения:
|
|
|
+
|
|
|
+ ```kt
|
|
|
+ // то что мы хотим найти заключаем в круглые скобки (группы)
|
|
|
+ val re = Regex("""\\products\\(.*)""")
|
|
|
+ val res = re.find("""\products\tire_0.jpg""")
|
|
|
+ // если регулярное выражение ничего не найдет, то вернёт null
|
|
|
+ if(res != null){
|
|
|
+ // искомый текст в ПЕРВОЙ группе
|
|
|
+ // (в 0 группе находится вся строка совпавшая с регулярным выражением)
|
|
|
+ Log.d("KEILOG", res.groupValues[1])
|
|
|
+ }
|
|
|
+ ```
|
|
|
+
|
|
|
+## Первичная настройка приложения
|
|
|
+
|
|
|
+1. Создаем новый проект и сразу пытаемся его запустить.
|
|
|
+
|
|
|
+ Если при сборке проекта выходит подобная ошибка, то нужно "понизить" версию зависимости, на которую ругается сборщик. Вообще эта ошибка означает, что какой-то пакет (в нашем случае **androidx.appcompat:appcompat:1.4.0**) требует более новой SDK, чем установлена. Но в **AVD** пока нет версий новее 30.
|
|
|
+
|
|
|
+ ```
|
|
|
+ The minCompileSdk (31) specified in a
|
|
|
+ dependency's AAR metadata (META-INF/com/android/build/gradle/aar-metadata.properties)
|
|
|
+ is greater than this module's compileSdkVersion (android-30).
|
|
|
+ Dependency: androidx.appcompat:appcompat:1.4.0.
|
|
|
+ ```
|
|
|
+
|
|
|
+ Открываем `build.graddle (Module...)`, находим нужный пакет в зависимостях (секция **dependencies**) и уменьшаем минорную версию пакета. Например, если была версия **1.4.0**, то правим на **1.3.0**.
|
|
|
+
|
|
|
+2. Устанавливаем иконку и название проекта.
|
|
|
+
|
|
|
+ * Установка иконки:
|
|
|
+
|
|
|
+ В контекстном меню папки ресурсов (**res**) выбираем `New -> Image asset`
|
|
|
+
|
|
|
+ 
|
|
|
+
|
|
|
+ В появившемся окне в поле путь (**Path**) выбираем картинку (в нашем случае произвольную, а на демо-экзамене она должна быть в предоставленных ресурсах). Можно заодно задать имя ресурса в поле **Name**.
|
|
|
+
|
|
|
+ 
|
|
|
+
|
|
|
+ Открываем манифест (`manifest/AndroidManifest.xml`) и в теге **application** правим атрибут `android:icon`:
|
|
|
+
|
|
|
+ ```
|
|
|
+ android:icon="@mipmap/ico"
|
|
|
+ ```
|
|
|
+
|
|
|
+ * установка названия приложения
|
|
|
+
|
|
|
+ В принципе достаточно поменять в манифесте атрибут
|
|
|
+
|
|
|
+ ```
|
|
|
+ android:label="Название вашего приложения"
|
|
|
+ ```
|
|
|
+
|
|
|
+ Но на всякий случай можно завернуть его в ресурсы (вдруг на экзамене будет под это отдельный критерий):
|
|
|
+
|
|
|
+ В файле строковых ресурсов (`res/values/strings.xml`) добавить (исправить если уже есть) значение
|
|
|
+
|
|
|
+ ```xml
|
|
|
+ <string name="app_name">Восьмёрка</string>
|
|
|
+ ```
|
|
|
+
|
|
|
+ И в манифесте вставить указатель на строковый ресурс
|
|
|
+
|
|
|
+ ```
|
|
|
+ android:label="@string/app_name"
|
|
|
+ ```
|
|
|
+
|
|
|
+<!-- TODO перенести в тему про ImageView -->
|
|
|
+
|
|
|
+Раньше как-то не пришло в голову, но часто графические ресурсы таскают сразу в приложении. По крайней мере в том задании про банк иконки валют и стран прилагались к заданию.
|
|
|
+
|
|
|
+Это значит, что при получении, например, информации о валюте мы должны иконку тащить не из интернета, а из ресурсов.
|
|
|
+
|
|
|
+Простая загрузка ресурса с ИЗВЕСТНЫМ id не сложная:
|
|
|
+
|
|
|
+```kt
|
|
|
+findViewById<ImageView>(R.id.ico)
|
|
|
+ .setImageResource(
|
|
|
+ R.drawable.ic_launcher_background
|
|
|
+ )
|
|
|
+```
|
|
|
+
|
|
|
+Но при получении данных из интернета мы имеем НАЗВАНИЕ ресурса (файла), а не его id в приложении. Для поиска id по имени есть отдельный метод:
|
|
|
+
|
|
|
+```kt
|
|
|
+val icoId = resources
|
|
|
+ .getIdentifier(
|
|
|
+ "ic_launcher_background", // название ресурса
|
|
|
+ "drawable", // раздел, в котором находится ресурс
|
|
|
+ this.packageName // пакет
|
|
|
+ )
|
|
|
+// дальше как обычно
|
|
|
+findViewById<ImageView>(R.id.ico).setImageResource(icoId)
|
|
|
+```
|
|
|
+
|
|
|
+## Добавление альбомной ориентации
|
|
|
+
|
|
|
+В окне разметки (acticity_main.xml) перейдите в режим "design" и кликните кнопку "Orientation..." выбираем "Create Landscape Variation"
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+Система автоматически создаст Layout с альбомной ориентацией.
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+>Учитывайте, что конструктор общий для всех ориентаций - при обращении к несуществующему объекту произойдет исключение.
|
|
|
+
|
|
|
+Чтобы для разных ориентаций не рисовать одинаковую разметку (допустим список валют выводится в обеих ориентациях) можно вынести повторяющиеся куски разметки в отдельные файлы разметки (layout), а в нужные места вставить ссылку на них с помощью тега **include**
|
|
|
+
|
|
|
+```xml
|
|
|
+<include
|
|
|
+ android:layout_width="wrap_content"
|
|
|
+ android:layout_height="wrap_content"
|
|
|
+ layout="@layout/my_cool_layout" />
|
|
|
+```
|
|
|
+
|
|
|
+В паре с **include** используется тег **merge**. Если выделяемый кусок разметки содержит несколько отдельных элементов, то по стандартам XML мы должны завернуть их в один родительский. Как раз тег **merge** и можно использовать в таком случае. Он игнорируется при разборе разметки и ни как на неё не влияет.
|
|
|
+
|
|
|
+```xml
|
|
|
+<merge
|
|
|
+ xmlns:android="http://schemas.android.com/apk/res/android">
|
|
|
+
|
|
|
+ <Button
|
|
|
+ android:layout_width="fill_parent"
|
|
|
+ android:layout_height="wrap_content"
|
|
|
+ android:text="@string/add"/>
|
|
|
+
|
|
|
+ <Button
|
|
|
+ android:layout_width="fill_parent"
|
|
|
+ android:layout_height="wrap_content"
|
|
|
+ android:text="@string/delete"/>
|
|
|
+
|
|
|
+</merge>
|
|
|
+```
|
|
|
+
|
|
|
+Такие выделенные куски разметки можно использовать и в том случае, если один и тот же участок разметки используется в нескольких окнах.
|
|
|
+
|
|
|
+## Модальный диалог авторизации
|
|
|
+
|
|
|
+Работа со внешними ресурсами подразумевает авторизацию (ввод логина/пароля). Для отображения формы авторизации можно использовать отдельное окно (это вы можете сделать и сами) или модальный диалог на текущем окне. Второй вариант рассмотрим подробнее.
|
|
|
+
|
|
|
+>http://developer.alexanderklimov.ru/android/dialogfragment_alertdialog.php
|
|
|
+
|
|
|
+Самый распространённый вариант диалогового окна - это **AlertDialog**.
|
|
|
+
|
|
|
+В создаваемых диалоговых окнах можно задавать следующие элементы:
|
|
|
+
|
|
|
+* заголовок
|
|
|
+* текстовое сообщение
|
|
|
+* кнопки: от одной до трёх
|
|
|
+* список
|
|
|
+* флажки
|
|
|
+* переключатели
|
|
|
+
|
|
|
+**AlertDialog с одной кнопкой**
|
|
|
+
|
|
|
+Начнём с простого примера - покажем на экране диалоговое окно с одной кнопкой.
|
|
|
+
|
|
|
+>Собственно такой диалог у нас уже используется для вывода сообщений об ошибках, другое дело, что я вам не рассказывал как он работает.
|
|
|
+
|
|
|
+Создаём новый класс **LoginDialog**
|
|
|
+
|
|
|
+```kt
|
|
|
+// класс наследуется от DialogFragment
|
|
|
+class LoginDialog : DialogFragment() {
|
|
|
+ // и должен реализовать метод onCreateDialog
|
|
|
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
|
+ return activity?.let {
|
|
|
+ val builder = AlertDialog.Builder(it)
|
|
|
+
|
|
|
+ // для диалога мы задаём название, сообщение и кнопку
|
|
|
+ builder.setTitle("Важное сообщение!")
|
|
|
+ .setMessage("Покормите кота!")
|
|
|
+ // иконку я пока убрал, но как подцепить - понятно
|
|
|
+ //.setIcon(R.drawable.hungrycat)
|
|
|
+ // при добавлении кнопки сразу задается обработчик,
|
|
|
+ // которы просто закрывает диалог
|
|
|
+ .setPositiveButton("ОК, иду на кухню") {
|
|
|
+ dialog, id -> dialog.cancel()
|
|
|
+ }
|
|
|
+ builder.create()
|
|
|
+ } ?: throw IllegalStateException("Activity cannot be null")
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Для показа этого диалога нужно вызвать конструктор с методом *show*
|
|
|
+
|
|
|
+```kt
|
|
|
+LoginDialog().show(supportFragmentManager, "loginDialog")
|
|
|
+```
|
|
|
+
|
|
|
+**AlertDialog со списком**
|
|
|
+
|
|
|
+Расписывать его я не буду, просто замечу, что выбор города можно было сделать таким диалогом
|
|
|
+
|
|
|
+В билдере вместо задания текста и кнопки задается список с обработчиком клика:
|
|
|
+
|
|
|
+```kt
|
|
|
+.setItems(catNames) { dialog, which ->
|
|
|
+ Toast.makeText(
|
|
|
+ activity,
|
|
|
+ "Выбранный кот: ${catNames[which]}",
|
|
|
+ Toast.LENGTH_SHORT)
|
|
|
+ .show()
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Параметр *which* содержит номер позиции, которую выбрали в списке
|
|
|
+
|
|
|
+>Можно прицепить и кнопку "Отмена"
|
|
|
+
|
|
|
+**AlertDialog с собственной разметкой**
|
|
|
+
|
|
|
+Если стандартный вид **AlertDialog** нас не устраивает, то можем придумать свою разметку и подключить её через метод *setView()*
|
|
|
+
|
|
|
+Сделаем диалоговое окно для авторизации (приведу сразу готовый код с комментариями):
|
|
|
+
|
|
|
+*Сначала рисуем разметку.*
|
|
|
+
|
|
|
+>В разметке я рисую свою кнопку "Логин". Теоретически можно использовать стандартную "позитивную" кнопку диалога, но при клике на такую кнопку диалог автоматически закрывается и я никак не смог переопределить её поведение (а нам сразу закрываться нельзя - мы сначала должны проверить все ли поля заполнены)
|
|
|
+
|
|
|
+Создаем, как обычно, дополнительный layout. Корневой элемент мой любиный **LinearLayout**.
|
|
|
+
|
|
|
+Чтобы форма не "прилипала" к границам диалога задаю у **LinearLayout** отступы: `android:padding="20dp"`
|
|
|
+
|
|
|
+В качестве текстовых полей ввода я использую элемент **TextInputLayout** - он позволяет показать рядом с полем текст ошибки.
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+Для поля "**Логин**" задаём *тип ввода* `android:inputType="textPersonName"` и *подсказку* `android:hint="Введите логин"`.
|
|
|
+
|
|
|
+Для поля "**Пароль**" задаём *тип ввода* `android:inputType="textPassword"` и *подсказку*
|
|
|
+
|
|
|
+Вместо кнопки я использую обычный **TextView**, стилизовав его под стандартную кнопку диалога
|
|
|
+
|
|
|
+```
|
|
|
+android:layout_marginTop="20dp"
|
|
|
+android:gravity="right"
|
|
|
+android:textColor="@color/design_default_color_primary"
|
|
|
+```
|
|
|
+
|
|
|
+```xml
|
|
|
+<?xml version="1.0" encoding="utf-8"?>
|
|
|
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
|
+ xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
|
+ android:orientation="vertical"
|
|
|
+ android:layout_width="match_parent"
|
|
|
+ android:layout_height="match_parent"
|
|
|
+ android:padding="20dp">
|
|
|
+
|
|
|
+ <com.google.android.material.textfield.TextInputLayout
|
|
|
+ android:id="@+id/login_error"
|
|
|
+ android:layout_width="match_parent"
|
|
|
+ android:layout_height="match_parent"
|
|
|
+ app:errorEnabled="true"
|
|
|
+ >
|
|
|
+
|
|
|
+ <com.google.android.material.textfield.TextInputEditText
|
|
|
+ android:id="@+id/login"
|
|
|
+ android:layout_width="match_parent"
|
|
|
+ android:layout_height="wrap_content"
|
|
|
+ android:inputType="textPersonName"
|
|
|
+ android:hint="Введите логин"
|
|
|
+ />
|
|
|
+ </com.google.android.material.textfield.TextInputLayout>
|
|
|
+
|
|
|
+ <com.google.android.material.textfield.TextInputLayout
|
|
|
+ android:id="@+id/password_error"
|
|
|
+ android:layout_width="match_parent"
|
|
|
+ android:layout_height="match_parent"
|
|
|
+ app:errorEnabled="true"
|
|
|
+ >
|
|
|
+
|
|
|
+ <com.google.android.material.textfield.TextInputEditText
|
|
|
+ android:id="@+id/password"
|
|
|
+ android:layout_width="match_parent"
|
|
|
+ android:layout_height="wrap_content"
|
|
|
+ android:inputType="textPassword"
|
|
|
+ android:hint="Введите пароль"
|
|
|
+ />
|
|
|
+ </com.google.android.material.textfield.TextInputLayout>
|
|
|
+
|
|
|
+ <TextView
|
|
|
+ android:id="@+id/login_button"
|
|
|
+ android:layout_width="match_parent"
|
|
|
+ android:layout_height="wrap_content"
|
|
|
+ android:layout_marginTop="20dp"
|
|
|
+ android:gravity="right"
|
|
|
+ android:textColor="@color/design_default_color_primary"
|
|
|
+ android:text="ЛОГИН"
|
|
|
+ />
|
|
|
+
|
|
|
+</LinearLayout>
|
|
|
+```
|
|
|
+
|
|
|
+*Дальше переделываем класс диалога*
|
|
|
+
|
|
|
+**Во-первых**, в параметры конструктора я добавил лямбда-функцию обратного вызова, чтобы получать логин и пароль в том классе, где он нужны.
|
|
|
+
|
|
|
+**Во-вторых**, достаём из ресурсов наш *login_layout* и из него ссылки на все используемые элементы.
|
|
|
+
|
|
|
+Диалог создаём как обычно (но без кнопок)
|
|
|
+
|
|
|
+И, **в-третьих**, задаём обработчик кнопке "Логин". В этом обработчике мы просто проверяем заполнены ли поля, если нет, то показываем ошибки. Если же всё нормально, то закрываем модальное окно и вызываем функцию обратного вызова с заполненными логином и паролем.
|
|
|
+
|
|
|
+Обратите внимание, метод *onCreateDialog* должен вернуть указатель на созданные диалог - это последняя строчка лямбда выражения.
|
|
|
+
|
|
|
+```kt
|
|
|
+class LoginDialog(private val callback: (login: String, password: String)->Unit) : DialogFragment() {
|
|
|
+
|
|
|
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
|
+ return activity?.let {
|
|
|
+ val builder = AlertDialog.Builder(it)
|
|
|
+
|
|
|
+ val loginLayout = layoutInflater.inflate(R.layout.activity_login, null)
|
|
|
+
|
|
|
+ val loginText = loginLayout.findViewById<com.google.android.material.textfield.TextInputEditText>(R.id.login)
|
|
|
+ val loginError = loginLayout.findViewById<com.google.android.material.textfield.TextInputLayout>(R.id.login_error)
|
|
|
+ val passwordText = loginLayout.findViewById<com.google.android.material.textfield.TextInputEditText>(R.id.password)
|
|
|
+ val passwordError = loginLayout.findViewById<com.google.android.material.textfield.TextInputLayout>(R.id.password_error)
|
|
|
+ val loginButton = loginLayout.findViewById<TextView>(R.id.login_button)
|
|
|
+
|
|
|
+ val myDialog = builder.setView(loginLayout)
|
|
|
+ .setTitle("Авторизация!")
|
|
|
+ .setIcon(R.mipmap.ico)
|
|
|
+ .create()
|
|
|
+
|
|
|
+ loginButton.setOnClickListener {
|
|
|
+ var hasErrors = false
|
|
|
+ if(loginText.text.isNullOrEmpty()){
|
|
|
+ hasErrors = true
|
|
|
+ loginError.error = "Поле должно быть заполнено"
|
|
|
+ }
|
|
|
+ else
|
|
|
+ loginError.error = ""
|
|
|
+
|
|
|
+ if(passwordText.text.isNullOrEmpty()){
|
|
|
+ hasErrors = true
|
|
|
+ passwordError.error = "Поле должно быть заполнено"
|
|
|
+ } else
|
|
|
+ passwordError.error = ""
|
|
|
+
|
|
|
+ if(!hasErrors) {
|
|
|
+ myDialog.dismiss()
|
|
|
+ callback.invoke(
|
|
|
+ loginText.text.toString(),
|
|
|
+ passwordText.text.toString()
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ myDialog
|
|
|
+ } ?: throw IllegalStateException("Activity cannot be null")
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+В итоге должна получиться такая форма
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+Ну и не забываем в основном окне добавить в параметры конструктора лямбда-выражение, в котором мы получим логин и пароль.
|
|
|
+
|
|
|
+```kt
|
|
|
+LoginDialog { login, password ->
|
|
|
+ Log.d("KEILOG", "${login}/${password}")
|
|
|
+}.show(supportFragmentManager, null)
|
|
|
+```
|
|
|
+
|
|
|
+## Регулярные выражения
|
|
|
+
|
|
|
+Возможно будет задание сделать форму регистрации. В этом случае обычно нужно сделать проверку введенных данных. Например, электронной почты
|
|
|
+
|
|
|
+```kt
|
|
|
+val re = Regex("""^\S+\@\S+\.\w+$""")
|
|
|
+if(re.containsMatchIn("kolei@ya.ru")){
|
|
|
+
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## HTTP-запросы, методы, форматы, заголовки.
|
|
|
+
|
|
|
+>Класс [**HTTP**](../shpora/HttpHelper.kt) в шпаргалке я обновил.
|
|
|
+
|
|
|
+Итак, мы получили от формы авторизации логин и пароль.
|
|
|
+
|
|
|
+Напишем нормальную функцию обратного вызова (выделим её в отдельную переменную)
|
|
|
+
|
|
|
+Теперь вызов диалога авторизации будет выглядеть так:
|
|
|
+
|
|
|
+```kt
|
|
|
+LoginDialog(onLoginResponce)
|
|
|
+ .show(supportFragmentManager, null)
|
|
|
+```
|
|
|
+
|
|
|
+Ниже реализация лямбда функции *onLoginResponce*:
|
|
|
+
|
|
|
+```kt
|
|
|
+val onLoginResponce: (login: String, password: String)->Unit = { login, password ->
|
|
|
+ // первым делом сохраняем имя пользователя,
|
|
|
+ // чтобы при необходимости можно было разлогиниться
|
|
|
+ username = login
|
|
|
+
|
|
|
+ // затем формируем JSON объект с нужными полями
|
|
|
+ val json = JSONObject()
|
|
|
+ json.put("username", login)
|
|
|
+ json.put("password", password)
|
|
|
+
|
|
|
+ // и вызываем POST-запрос /login
|
|
|
+ // в параметрах не забываем указать заголовок Content-Type
|
|
|
+ HTTP.requestPOST(
|
|
|
+ "http://s4a.kolei.ru/login",
|
|
|
+ json,
|
|
|
+ mapOf(
|
|
|
+ "Content-Type" to "application/json"
|
|
|
+ )
|
|
|
+ ){result, error ->
|
|
|
+ if(result!=null){
|
|
|
+ try {
|
|
|
+ // анализируем ответ
|
|
|
+ val jsonResp = JSONObject(result)
|
|
|
+
|
|
|
+ // если нет объекта notice
|
|
|
+ if(!jsonResp.has("notice"))
|
|
|
+ throw Exception("Не верный формат ответа, ожидался объект notice")
|
|
|
+
|
|
|
+ // есть какая-то ошибка
|
|
|
+ if(jsonResp.getJSONObject("notice").has("answer"))
|
|
|
+ throw Exception(jsonResp.getJSONObject("notice").getString("answer"))
|
|
|
+
|
|
|
+ // есть токен!!!
|
|
|
+ if(jsonResp.getJSONObject("notice").has("token")) {
|
|
|
+ token = jsonResp.getJSONObject("notice").getString("token")
|
|
|
+ runOnUiThread {
|
|
|
+ // тут можно переходить на следующее окно
|
|
|
+ Toast.makeText(this, "Success get token: $token", Toast.LENGTH_LONG)
|
|
|
+ .show()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ throw Exception("Не верный формат ответа, ожидался объект token")
|
|
|
+ } catch (e: Exception){
|
|
|
+ runOnUiThread {
|
|
|
+ AlertDialog.Builder(this)
|
|
|
+ .setTitle("Ошибка")
|
|
|
+ .setMessage(e.message)
|
|
|
+ .setPositiveButton("OK", null)
|
|
|
+ .create()
|
|
|
+ .show()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else
|
|
|
+ runOnUiThread {
|
|
|
+ AlertDialog.Builder(this)
|
|
|
+ .setTitle("Ошибка http-запроса")
|
|
|
+ .setMessage(error)
|
|
|
+ .setPositiveButton("OK", null)
|
|
|
+ .create()
|
|
|
+ .show()
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Если мы уже авторизованы и получили в ответ соответсвующую ошибку, то нужно предусмотреть на форме кнопку "Выход" (Logout) и реализовать обработчик:
|
|
|
+
|
|
|
+```kt
|
|
|
+// тут я не делал отдельный json объект для параметров
|
|
|
+// можно создавать его на ходу
|
|
|
+HTTP.requestPOST(
|
|
|
+ "http://s4a.kolei.ru/logout",
|
|
|
+ JSONObject().put("username", username),
|
|
|
+ mapOf(
|
|
|
+ "Content-Type" to "application/json"
|
|
|
+ )
|
|
|
+){result, error ->
|
|
|
+ // при выходе не забываем стереть существующий токен
|
|
|
+ token = ""
|
|
|
+
|
|
|
+ // каких-то осмысленных действий дальше не предполагается
|
|
|
+ // разве что снова вызвать форму авторизации
|
|
|
+ runOnUiThread {
|
|
|
+ if(result!=null) {
|
|
|
+ Toast.makeText(this, "Logout success!", Toast.LENGTH_LONG).show()
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ AlertDialog.Builder(this)
|
|
|
+ .setTitle("Ошибка http-запроса")
|
|
|
+ .setMessage(error)
|
|
|
+ .setPositiveButton("OK", null)
|
|
|
+ .create()
|
|
|
+ .show()
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Ну и последний на сегодня запрос - запрос списка продукции (его нужно вызывать уже из другого **activity**, передав ему токен):
|
|
|
+
|
|
|
+```kt
|
|
|
+if(token.isNotEmpty()){
|
|
|
+ HTTP.requestGET(
|
|
|
+ "http://s4a.kolei.ru/Product",
|
|
|
+ mapOf(
|
|
|
+ "token" to token
|
|
|
+ )
|
|
|
+ ){result, error ->
|
|
|
+ runOnUiThread{
|
|
|
+ if(result!=null){
|
|
|
+ resultTextView.text = result
|
|
|
+ }
|
|
|
+ else
|
|
|
+ resultTextView.text = "ошибка: $error"
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+else
|
|
|
+ Toast.makeText(this, "Не найден токен, нужно залогиниться", Toast.LENGTH_LONG)
|
|
|
+ .show()
|
|
|
+```
|
|
|
+
|
|
|
+Как видите, в параметры GET-запроса я добавил заголовки, чтобы можно было добавить токен. Дальнейшая реализация уже с вас.
|
|
|
+
|
|
|
+# Задание
|
|
|
+
|
|
|
+* на первый экран добавить заставку (splash-screen) с таймером на пару секунд, только потом показывать диалог для ввода логина/пароля
|
|
|
+* после получения токена перейти на другое окно (activity), на котором получить и вывести в RecyclerView список продукции (с картинками)
|
|
|
+* на окне со списком продукции сделать кнопку "выход" (logout), после чего вернуться на экран авторизации. У нас после первой авторизации осталось имя, сделайте передачу имени в диалог авторизации, чтобы он был уже заполнен.
|