浏览代码

каршеринг

Евгений Колесников 2 年之前
父节点
当前提交
0dfd9bc14b
共有 11 个文件被更改,包括 345 次插入446 次删除
  1. 327 442
      articles/android_auth.md
  2. 2 0
      articles/android_profile.md
  3. 9 0
      articles/animation.md
  4. 二进制
      img/mad_03.png
  5. 二进制
      img/mad_04.png
  6. 二进制
      img/mad_05.png
  7. 二进制
      img/mad_06.png
  8. 二进制
      img/mas1_01.png
  9. 二进制
      img/mas1_02.png
  10. 二进制
      img/mas1_03.png
  11. 7 4
      readme.md

+ 327 - 442
articles/android_auth.md

@@ -1,228 +1,135 @@
-<table style="width: 100%;"><tr><td style="width: 40%;">
-<a href="../articles/android_bottom_navigation.md">Android Navigation. Знакомство с BottomNavigationView.
-</a></td><td style="width: 20%;">
-<a href="../readme.md">Содержание
-</a></td><td style="width: 40%;">
-<a href="../articles/map_yandex.md">Работа с картами
-</a></td><tr></table>
+Предыдущая лекция | &nbsp; | Следующая лекция
+:----------------:|:----------:|:----------------:
+[Анимация](./animation.md) | [Содержание](../readme.md#практика-разработка-мобильных-приложений) | [Проект "каршеринг" Часть 2. Профиль пользователя.](./android_profile.md)
 
-# Проект "база". Авторизация на сервере (Basic auth, token). POST-запросы. API.
+# Проект "каршеринг" Часть 1. Регистрация/Авторизация.
 
 **Содержание**
 
-* [API](#API)
+* [Техническое задание](#техническое-задание-задание-на-демо-экзамене)
+
 * [Первичная настройка приложения](#первичная-настройка-приложения)
-* [Добавление альбомной ориентации](#добавление-альбомной-ориентации)
-* [~~Модальный диалог авторизации~~](#модальный-диалог-авторизации)
-* [Регулярные выражения](#Регулярные-выражения)
-* [HTTP-запросы, методы, форматы, заголовки.](#HTTP-запросы-методы-форматы-заголовки)
-* [Сохранение данных при работе приложения](#сохранение-данных-при-работе-приложения)
-* [Фильтрация данных](#фильтрация-данных)
-* [Spinner (выпадающий список)](#spinner-выпадающий-список)
 
-## API
+* [Регистрация и авторизация. Регулярные выражения. POST-запросы. Сохранение данных при работе приложения](#регистрация-и-авторизация-регулярные-выражения-post-запросы-сохранение-данных-при-работе-приложения)
 
-Приведу пример описания API, которое было на моём демо-экзамене:
+## Техническое задание
 
->API  доступно по адресу http://car.areas.su/
->Для работы с API метод принимает в теле application/json
->Например:
->Метод входа. Ссылка http://cars.areas.su/login
->
->Request Body(application/json)
->```json
->{
->	"username":"serk",
->	"password":"123"
->}
->```
->
->Response
->
->```json
->{
->   "notice": 
->   {
->       "token":111520
->   }
->}
->```
->
->1 **/login** – метод для получения ключа для входа
->   - Используется метод POST
->   - Формат запроса JSON
->   - Принимает два ключа username и password
->   - ответ в формате JSON
->
->2 **/logout** – метод обнуления действия token
->   - Используется метод POST
->   - Формат запроса JSON
->   - Принимает ключ  username
->
->3 **/signup** – метод регистрации нового пользователя
->   - Используется метод POST
->   - Формат запроса JSON
->	- Принимает ключ  username, email, password
->
->4 **/cars** – метод для получения свободных автомобилей
->   - Используется метод GET
->   - Формат ответа JSON
->
->5 **/history** – метод для получения истории вождения
->   - Используется метод POST
->   - Формат запроса JSON
->   - **Принимает ключ  token**
->   - Формат ответа JSON
+>Сразу уточню: задание скомпилировано из нескольких демо-экзаменов, могут быть нестыковки.
 
-Несколько косноязычно и не расписаны "подводные камни". Логика авторизации такая, что если токен уже получен, то новый не выдается - нужно сначала разлогиниться.
+**Модуль 1** 
 
-Методы есть как публичные (**/cars**), так и требующие авторизации (**/history** - это видно по тому, что в параметрах требуется токен).
+Разработка мобильного приложения для бронирования автомобилей (каршеринг)
 
-Я на примере этого разработал АПИ для ваших баз:
+Необходимо разработать мобильное приложение для смартфона, удовлетворяющее следующим требованиям:
 
-1. Создаем файл `api.http`:
+Приложение должно поддерживать следующие версии ОС:
 
-    >Примеры запросов реализованы в формате плагина **REST Client** для Visual Studio Code
+* Android 9.0 и новее
+* iOS 13.0 и новее
 
-    Прописываем в нем URL сервера:
+В работе необходимо использовать систему контроля версий **Git**.
 
-    ```
-    @url=http://s4a.kolei.ru
-    ```
+Необходимо загрузить результаты выполнения модуля в отдельный репозиторий с именем `Module_X`, где `Х` – это номер модуля.
 
-2. Проверить работоспособность можно послав запрос **/about**:
+Необходимо корректно обрабатывать запросы к серверу. В случае получения ошибки от сервера или отсутствия соединения с сетью Интернет необходимо отобразить соответствующий текст ошибки с помощью диалогового окна.
 
-    ```
-    GET {{url}}/about
-    ```
+Необходимо строго следовать предложенному дизайну. Макеты приложения доступны по ссылке:
 
-3. Для авторизации нужно послать **POST** запрос **/login** в формате `application/json` с параметрами *username* и *password* (соответственно логин и пароль к ВАШЕЙ базе MySQL)
+>Пока нет фигмы
 
-    ```
-    ### логин
-    # @name login
-    POST {{url}}/login
-    Content-Type: application/json
+<!-- TODO добавить ссылку на фигму -->
 
-    {
-        "username": "esmirnov",
-        "password": "111103"
-    }
+Описание протокола API в формате OpenAPI (Swagger) доступно по ссылке:
+https://swagger.kolei.ru/?url=https://carsharing.kolei.ru/swagger/api.yml
 
-    ###
-    @token={{login.response.body.$.notice.token}}
-    ```
+Проект приложения должен быть структурирован по экранам, то есть исходные файлы конкретного экрана должны быть в соответствующей папке. Общие для нескольких экранов классы необходимо поместить в папку `common`.
 
-    Тут встречаются средства автоматизации **REST Client**-а:
+Необходимо реализовать следующий функционал:
 
-    * `# @name login` этой командой мы присваиваем имя текущему запросу
-    * `@token={{login.response.body.$.notice.token}}` а здесь, используя ответ команды достаём токен. Таким образом для последующих команд не надо выписывать на бумажке токен и править запросы.
+1. Создайте проект. Настройте иконку приложения согласно макету. Следует учесть разницу в отображении иконок на различных версиях операционной системы.
 
-    При успешном ответе придет токен авторизации:
+1. Реализуйте экран *Launch Screen* согласно макету. 
 
-    ```json
-    {
-        "notice": {
-            "token": 2200743
-        }
-    }
-    ```
+    ![](../img/mas1_01.png)
 
-    При ошибке текст ошибки в поле *answer*:
+    Текст должен быть отдельным элементом (на скрине нет, но добавьте что-нибудь сами). 
+    
+    Логотип приложения должен быть расположен по центру экрана. При первом запуске приложения после `Launch Screen` должен отображаться `SignUp Screen` (регистрация). При последующих - `SignIn Screen` (авторизация).
 
-    ```json
-    {
-        "notice": {
-            "answer": "Пользователь уже авторизован, используйте токен или перелогиньтесь"
-        }
-    }
-    ```
+    >Я создание загрузочного экрана расписывать не буду, вы уже должны справиться с этим самостоятельно.
 
-    То есть при получении ответа вы должны разобрать полученный JSON, если есть токен, то можно продолжать работать. Если ошибка, то показать **Alert** с ошибкой и остаться на экране авторизации.
+1. Реализуйте экран `SignUp Screen` согласно макету:
 
-    >Маловероятно, но вдруг попадётся задача сделать "базовую авторизацию" (Basic Auth)
-    >При таком методе авторизации в запрос нужно добавить заголовок `Authorization: Basic <логин:пароль в кодировке base64>`
-    >
-    >```kt
-    >Base64.encodeToString(
-    >   "$login:$password".toByteArray(), 
-    >   Base64.NO_WRAP)
-    >```
-    >
-    >Базовая авторизация позволяет использовать GET-запрос, т.к. в теле запроса ничего не предается
+    ![](../img/mas1_03.png)    
 
-4. Для выхода нужно послать **POST** запрос **/logout** c параметом *username*:
- 
-    ```
-    POST {{url}}/logout
-    Content-Type: application/json
+    >**Обратите внимание!** В оригинальном задании был вход по e-mail, но в моем АПИ нужно по телефону
 
-    {   
-        "username": "esmirnov"
-    }
-    ```
+    * При нажатии на кнопку "Зарегистрироваться" необходимо проверять поля для ввода на пустоту, а также телефон на корректность (требования к телефону описаны в документации к API). При некорректном заполнении необходимо отобразить ошибку с помощью диалогового окна. Так же необходимо проверять равенство пароля и его повтора.
 
-5. Для запроса данных из базы нужно послать **GET** запрос с названием таблицы. В заголовке запроса передать токен полученный при авторизации.
+    * При корректном заполнении формы необходимо отправлять запрос регистрации на сервер. При получении ошибки от сервера ее необходимо отобразить с помощью диалогового окна. При успешной регистрации нужно автоматически осуществить авторизацию и перейти на `Main Screen`.
 
-    ```
-    GET {{url}}/Product
-    Content-Type: application/json
-    Token: {{token}}
-    ```
+    * При нажатии на кнопку "У меня уже есть аккаунт" необходимо осуществлять переход на `SignIn Screen`. На скрине этой кнопки нет - добавьте.
 
-    В ответ должны получить содержимое таблицы:
+    * при нажатии на ссылку "Оферта" необходимо открыть web-страничку с текстом оферты. Этой кнопки тоже нет, тоже добавьте сами.
 
-    ```json
-    {
-        "notice": {
-            "data": [
-                {
-                    "ID": 1,
-                    "Title": "Колесо R18 Кованый",
-                    "ProductTypeID": 128,
-                    "ArticleNumber": "241659",
-                    "Description": null,
-                    "Image": "\\products\\tire_15.jpg",
-                    "ProductionPersonCount": 4,
-                    "ProductionWorkshopNumber": 10,
-                    "MinCostForAgent": 11509
-                },
-                ...
-            ]
-        }
-    }
-    ```
+    >Создание этого и следующего экранов я тоже расписывать не буду, тут всё элементарно. Остановлюсь только на создании POST-запросов и регулярных выражениях.
 
-6. Для получения изображений нужно выделить его название из поля *Image* и сформировать **GET** запрос:
+1. Реализуйте экран `SignIn Screen` согласно макету:
 
-    ```
-    GET {{url}}/img/tire_0.jpg
-    ```
+    ![](../img/mas1_02.png)    
 
-    Обратите внимание, для загрузки картинок я использую не всю строку, которая у вас в базе (`\products\tire_0.jpg`), а только название файла.
+    * При нажатии на кнопку "Войти" необходимо проверять поля для ввода на пустоту, а также телефон на корректность (требования к телефону описаны в документации к API). При некорректном заполнении необходимо отобразить ошибку с помощью диалогового окна. При корректном заполнении формы необходимо отправить на сервер соответствующий запрос.
 
-    Чтобы вытащить название из строки можно использовать метод *split* - он делит исходную строку на список подстрок по указанному разделителю:
+    * При нажатии на кнопку "Регистрация" необходимо осуществлять переход на `SignUp Screen`.
 
-    ```kt
-    val fileName = imageNameFromDB.split("\\").lastOrNull()
-    if(fileName != null){
-        ...
-    }
-    ```
+    * При успешной авторизации необходимо осуществлять переход на экран `Main Screen` или `Profile Screen`, в зависимости от состояния пользователя (загружены ли права и другие необходимые документы). При получении ошибки от сервера необходимо отобразить её с помощью диалогового окна.
 
-    Если условие поиска в строке сложнее, то можно применить регулярные выражения:
+1. Реализуйте экран `Profile Screen` согласно макету:
 
-    ```kt
-    // то что мы хотим найти заключаем в круглые скобки (группы)
-    val re = Regex("""\\products\\(.*)""")
-    val res = re.find("""\products\tire_0.jpg""")
-    // если регулярное выражение ничего не найдет, то вернёт null
-    if(res != null){
-        // искомый текст в ПЕРВОЙ группе 
-        // (в 0 группе находится вся строка совпавшая с регулярным выражением)
-        Log.d("KEILOG", res.groupValues[1])
-    }
-    ```
+    * На экране необходимо отобразить аватарку пользователя. Рамка вокруг аватарки должна быть разного цвета в зависимости от состояния профиля:
+        - *желтый*: не загружены фотографии водительских прав или паспорта
+        - *красный*: есть штрафы от ГИБДД или претензии от владельца каршеринга
+        - *зелёный*: всё OK (активный профиль)
+
+    * При нажатии на аватарку открыть приложение "Камера" и полученную миниатюру отправить на сервер и заменить ею существующую аватрку.
+
+    * При нажатии на кнопки "Загрузить фото водительских прав" или "Загрузить файл паспорта" открыть приложение "Галерея" и выбранный файл отправить на сервер.
+
+1. Реализуйте экран `Main Screen` согласно макету:
+
+    * На экране необходимо отобразить карту с текущей позицией и маркерами автомобилей (список доступных автомобилей получить с сервера).
+
+    * при клике на маркер автомобиля показывать (во всплывающем окне) краткую информацию об автомобиле: марка автомобиля, фото, кнопки "забронировать" и "маршрут". Кнопка "забронировать" должна быть только у активного пользователя
+
+    * при клике на кнопку "Забронировать" открыть окно `Booking Screen`
+
+    * ~~При клике на кнопку "маршрут" закрыть всплывающее окно и проложить маршрут от текущей позиции пользователя до выбранного автомобиля~~
+
+    * При нажатии на иконку профиля необходимо переходить на `Profile Screen`.
+
+1. Реализуйте экран `Booking Screen` согласно макету:
+
+## API
+
+Описание протокола API в формате OpenAPI (Swagger) доступно по ссылке:
+https://swagger.kolei.ru/?url=https://carsharing.kolei.ru/swagger/api.yml
+
+### Примечания
+
+Для запросов требующих авторизации необходимо добавлять заголовок `Authorization: Bearer <ваш токен>`
+
+В качестве токена используется поле `userId`, получаемое в ответ на запрос `/login`
+
+>Маловероятно, но вдруг попадётся задача сделать "базовую авторизацию" (Basic Auth)
+>При таком методе авторизации в запрос нужно добавить заголовок `Authorization: Basic <логин:пароль в кодировке base64>`
+>
+>```kt
+>Base64.encodeToString(
+>   "$login:$password".toByteArray(), 
+>   Base64.NO_WRAP)
+>```
+>
+>Базовая авторизация позволяет использовать GET-запрос, т.к. в теле запроса ничего не предается
 
 ## Первичная настройка приложения
 
@@ -281,11 +188,11 @@
 
 <!-- TODO перенести в тему про ImageView -->
 
-Раньше как-то не пришло в голову, но часто графические ресурсы таскают сразу в приложении. По крайней мере в одном из заданий про банк иконки валют и стран прилагались к заданию. 
+Раньше как-то не пришло в голову, но часто графические ресурсы хранят сразу в приложении (в ресурсах). По крайней мере в одном из заданий про банк иконки валют и стран прилагались к заданию. 
 
-Это значит, что при получении, например, информации о валюте мы должны иконку тащить не из интернета, а из ресурсов.
+Это значит, что при получении, например, информации о валюте мы должны иконку получать не из интернета, а из ресурсов.
 
-Простая загрузка ресурса с ИЗВЕСТНЫМ id не сложная:
+Простая загрузка ресурса с ИЗВЕСТНЫМ `id` не сложная:
 
 ```kt
 findViewById<ImageView>(R.id.ico)
@@ -294,7 +201,7 @@ findViewById<ImageView>(R.id.ico)
     )
 ```
 
-Но при получении данных из интернета мы имеем НАЗВАНИЕ ресурса (файла), а не его id в приложении. Для поиска id по имени есть отдельный метод:
+Но при получении данных из интернета мы имеем НАЗВАНИЕ ресурса (файла), а не его `id` в приложении. Для поиска `id` по имени есть отдельный метод:
 
 ```kt
 val icoId = resources
@@ -307,7 +214,9 @@ val icoId = resources
 findViewById<ImageView>(R.id.ico).setImageResource(icoId)
 ```
 
-## Добавление альбомной ориентации
+## Добавление альбомной ориентации (include/merge)
+
+<!-- TODO перенести в соответствующую лекцию -->
 
 В окне разметки (acticity_main.xml) перейдите в режим "design" и кликните кнопку "Orientation..." выбираем "Create Landscape Variation"   
 
@@ -349,269 +258,95 @@ findViewById<ImageView>(R.id.ico).setImageResource(icoId)
 
 Такие выделенные куски разметки можно использовать и в том случае, если один и тот же участок разметки используется в нескольких окнах.
 
-<details>
-
-<summary>Модальный диалог авторизации</summary>
-
-## ~~Модальный диалог авторизации~~
-
-Работа со внешними ресурсами подразумевает авторизацию (ввод логина/пароля). Для отображения формы авторизации можно использовать отдельное окно (это вы можете сделать и сами) или модальный диалог на текущем окне. Второй вариант рассмотрим подробнее.
-
->http://developer.alexanderklimov.ru/android/dialogfragment_alertdialog.php
+## Регистрация и авторизация. Регулярные выражения. POST-запросы. Сохранение данных при работе приложения
 
-Самый распространённый вариант диалогового окна - это **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")
+```xml
+<TextView
+    android:autoLink="web"
+    android:linksClickable="true"
+    android:text="Оферта: carsharing.kolei.ru/public/oferta.html"
+/>
 ```
 
-**AlertDialog со списком**
+Элемент **TextView** поддерживает автоматический переход по ссылкам, достаточно указать какие типы ссылок искать в тексте (у нас указано искать веб-адреса `android:autoLink="web"`, но можно в этот атрибут добавить ключевое слово `phone` и можно будет позвонить).
 
-Расписывать его я не буду, просто замечу, что выбор города можно было сделать таким диалогом
+### Регулярные выражения
 
-В билдере вместо задания текста и кнопки задается список с обработчиком клика:
+При создании формы регистрации/авторизации обычно нужно сделать проверку введенных данных. Например, электронной почты:
 
 ```kt
-.setItems(catNames) { dialog, which ->
-    Toast.makeText(
-            activity, 
-            "Выбранный кот: ${catNames[which]}",
-            Toast.LENGTH_SHORT)
-        .show()
-}
-```
-
-Параметр *which* содержит номер позиции, которую выбрали в списке
-
->Можно прицепить и кнопку "Отмена"
-
-**AlertDialog с собственной разметкой**
-
-Если стандартный вид **AlertDialog** нас не устраивает, то можем придумать свою разметку и подключить её через метод *setView()*
-
-Сделаем диалоговое окно для авторизации (приведу сразу готовый код с комментариями):
-
-*Сначала рисуем разметку.*
-
-СОЗДАЕМ НОВЫЙ ФАЙЛ РАЗМЕТКИ (в контекстном меню layout)
-
->В разметке я рисую свою кнопку "Логин". Теоретически можно использовать стандартную "позитивную" кнопку диалога, но при клике на такую кнопку диалог автоматически закрывается и я никак не смог переопределить её поведение (а нам сразу закрываться нельзя - мы сначала должны проверить все ли поля заполнены)
-
-Создаем, как обычно, дополнительный layout. Корневой элемент мой любиный **LinearLayout**.
-
-Чтобы форма не "прилипала" к границам диалога задаю у **LinearLayout** отступы: `android:padding="20dp"`
-
-В качестве текстовых полей ввода я использую элемент **TextInputLayout** - он позволяет показать рядом с полем текст ошибки.
-
-![](../img/04035.png)    
-
-Для поля "**Логин**" задаём *тип ввода* `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="ЛОГИН"
-        />
+val re = Regex("""^\S+\@\S+\.\w+$""")
+if(re.containsMatchIn("nothere@inthe.net")){
 
-</LinearLayout>
+}
 ```
 
-*Дальше переделываем класс диалога*
-
-**Во-первых**, в параметры конструктора я добавил лямбда-функцию обратного вызова, чтобы получать логин и пароль в том классе, где он нужны.
+1. Конструктор **Regex** создает объект "регулярное выражение". В параметрах конструктора передаётся текст регулярного выражения, где:
 
-**Во-вторых**, достаём из ресурсов наш *login_layout* и из него ссылки на все используемые элементы.
+    * `^` - начало строки
+    * `\S+` - один или более (`+`) любых НЕ пробельных символов (`\S`)
+    * `\@` - просто символ `@` (можно и не экранировать)
+    * `\.` - точку экранируем обязательно, иначе она воспринимается как "любой символ"
+    *  `w+` - домен первого уровня не может содержать цифры, поэтому только буквы (`\w`)
+    * `$` - конеу строки
 
-Диалог создаём как обычно (но без кнопок)
+1. Метод *containsMatchIn* проверяет, соответствует ли переданная строка шаблону (естественно, вместо `nothere@inthe.net` вы вставляете значание поля, в котором вводите электронную почту). 
 
-И, **в-третьих**, задаём обработчик кнопке "Логин". В этом обработчике мы просто проверяем заполнены ли поля, если нет, то показываем ошибки. Если же всё нормально, то закрываем модальное окно и вызываем функцию обратного вызова с заполненными логином и паролем.
-
-Обратите внимание, метод *onCreateDialog* должен вернуть указатель на созданные диалог - это последняя строчка лямбда выражения.
-
-```kt
-class LoginDialog(private val callback: (login: String, password: String)->Unit) : DialogFragment() {
+### API. Swagger.
 
-    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
-        return activity?.let {
-            val builder = AlertDialog.Builder(it)
+<!-- TODO сюда вставить описание АПИ и ссылку на swagger -->
 
-            val loginLayout = layoutInflater.inflate(R.layout.activity_login, null)
+**Swagger** - это фреймворк для спецификации *RESTful API*. Его прелесть заключается в том, что он дает возможность не только интерактивно просматривать спецификацию, но и отправлять запросы.
 
-            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)
+Открываем [ссылку](https://swagger.kolei.ru?url=https://carsharing.kolei.ru/swagger/api.yml) на описание АПИ и смотрим что там есть:
 
-            val myDialog = builder.setView(loginLayout)
-                .setTitle("Авторизация!")
-                .setIcon(R.mipmap.ico)
-                .create()
+![](../img/mad_03.png)
 
-            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()
-                    )
-                }
-            }
+В начале идёт общая информация. Нам тут пока интереснен только так называемый *базовый URL*. Здесь он находится в блоке **Servers** (`https://carsharing.kolei.ru`), но может быть написан и просто текстом (как базовый урл для картинок).
 
-            myDialog
-        } ?: throw IllegalStateException("Activity cannot be null")
-    }
-}
-```
+Дальше идут описания методов АПИ. Рассмотрим подробно метод "Регистрация":
 
-В итоге должна получиться такая форма
+![](../img/mad_04.png)
 
-![](../img/04036.png)
+1. В заголовке указано какой метод и "путь" используются для запроса. К пути надо добавить "базовый урл" и получим полный адрес запроса: `https://carsharing.kolei.ru/auth/register`
 
-Ну и не забываем в основном окне добавить в параметры конструктора лямбда-выражение, в котором мы получим логин и пароль.
+1. В параметрах (*Parameters*) указываются параметры GET-запросов, передаваемые в строке запроса. У нас тут пусто.
 
-```kt
-LoginDialog { login, password ->
-    Log.d("KEILOG", "${login}/${password}")
-}.show(supportFragmentManager, null)
-```
+1. Тело запроса (*Request body*). Тут указано что тело запроса обязательно должно присутсвовать (**required**) и формат `application/json`
 
-</details>
+    В теле запроса должна быть JSON-строка. Пример её можно посмотреть на вкладке **Example value**, но нам интереснее вкладка **schema** - на ней описаны типы данных (string), описание поля (что это такое вообще) и, возможно, обязательность использования поля. Например, для поля **phone** расписан шаблон, которому оно должно соответсвовать.
 
-## Регулярные выражения
+1. Коды ответов (*Responses*)
 
-Возможно будет задание сделать форму регистрации. В этом случае обычно нужно сделать проверку введенных данных. Например, электронной почты
+    Тут надо быть внимательным, коды могут отличаться.
 
-```kt
-val re = Regex("""^\S+\@\S+\.\w+$""")
-if(re.containsMatchIn("kolei@ya.ru")){
+Ну и самое приятное в **Swagger** - можно прямо в нём проверить результат работы. Кликаем кнопку "Try it out", вводим в открывшемся окне тело запроса и нажимаем выполнить (*Execute*). Таким образом нам не нужны ни **Postman** ни **VSCode** с плагинами
 
-}
-```
+![](../img/mad_05.png)
 
-## HTTP-запросы, методы, форматы, заголовки.
+### POST-запросы.
 
-В шпаргалке лежит доработанный класс [**HTTP**](../shpora/HttpHelper.kt).
+В шпаргалке лежит доработанный класс [**HTTP**](../shpora/Http.kt), который поддерживает передачу заголовков запроса.
 
 ```kt
 object Http {
     private val client = OkHttpClient()
 
-    fun buildRequest(url: String, data: String? = null, method: String = "GET", headers: Map<String, String>? = null): Request {
-        val json = "application/json; charset=utf-8".toMediaTypeOrNull()
+    fun buildRequest(
+        url: String, 
+        data: String? = null, 
+        method: String = "GET", 
+        headers: Map<String, String>? = null): Request 
+    {
+        val json = "application/json; charset=utf-8"
+            .toMediaTypeOrNull()
+
         val request = Request.Builder().url(url)
+
         if (data != null)
             request.post(data.toRequestBody(json))
         else
@@ -623,11 +358,13 @@ object Http {
             }
         }
 
-//            .addHeader("Authorization", "Bearer ${_token}")
         return request.build()
     }
 
-    fun call(url: Any, callback: (response: Response?, error: Exception?)->Unit) {
+    fun call(
+        url: Any, 
+        callback: (response: Response?, error: Exception?)->Unit) 
+    {
         var request: Request = when (url) {
             is String -> Request.Builder()
                 .url(url)
@@ -638,6 +375,7 @@ object Http {
                 return
             }
         }
+
         client.newCall(request).enqueue(object : Callback {
             override fun onFailure(call: Call, e: IOException) {
                 callback.invoke(null, Exception(e.message!!))
@@ -659,7 +397,7 @@ object Http {
 object Http {...}
 ```
 
->Ключевое слово **object** одновременно объявляет класс и создаёт его экземпляр (*singleton*).
+>Ключевое слово **object** одновременно объявляет класс и создаёт его экземпляр (шаблон проектироания *singleton (одиночка)*).
 
 ```kt
 fun call(
@@ -684,7 +422,11 @@ fun call(
 **call** - основной метод этого класса, он, собственно, и запускает запрос. Я сделал его универсальным: на входе ему можно передать либо просто строку URL, либо подготовленный объект **Request** (он нам понадобится, чтобы посылать запросы с методом POST, данными и заголовками)
 
 ```kt
-fun buildRequest(url: String, data: String? = null, headers: Map<String, String>? = null): Request {
+fun buildRequest(
+    url: String, 
+    data: String? = null,
+    headers: Map<String, String>? = null): Request 
+{
     val json = "application/json; charset=utf-8".toMediaTypeOrNull()
     val request = Request.Builder().url(url)
     if (data != null)
@@ -703,11 +445,39 @@ fun buildRequest(url: String, data: String? = null, headers: Map<String, String>
 
 **buildRequest** - вспомогательный метод, который облегчает построение объекта Request.
 
->Этот метод заточен на посылку GET или POST запросов с типом данных JSON (заголовок `Content-Type: application/json` добавляется автоматически). Если понадобится послать другой метод (PUT, PATCH, DELETE) или данные другого типа, то можно сформировать объект **Request** самим и передать его в метод **call**
+>Этот метод заточен на посылку POST запросов с типом данных JSON (заголовок `Content-Type: application/json` добавляется автоматически). Если понадобится послать другой метод (PUT, PATCH, DELETE) или данные другого типа, то можно сформировать объект **Request** самим и передать его в метод **call**
 
 Дальше по ходу лекции мы разберёмся с различными примерами использования этого метода (но они есть и в комментариях в начале файла)
 
-Итак, мы получили от формы авторизации логин и пароль.
+Пример отправки запроса регистрации из приложения Kotlin:
+
+```kt
+// создаём пустой JSON объект
+val json = JSONObject()
+
+// добавляем в него нужные поля
+json.put("phone", phone)
+json.put("password", password)
+json.put("firstName", firstName)
+json.put("lastName", lastName)
+
+Http.call(
+    Http.buildRequest(
+        "https://carsharing.kolei.ru/auth/register",
+        json.toString()
+    )
+) { response, error -> ... }
+```
+
+Так как нам нужно послать POST-запрос с данными для регистрации, то в методе *Http.call* первым параметром передаем "построитель запросов", в котором указываем URL и данные для регистрации.
+
+С запросом авторизации аналогично, только параметров ещё меньше.
+
+Ответ на запрос авторизации у меня возвращает информацию о пользователе, чтобы можно было принять решение на какой экран переходить после авторизации.
+
+![](../img/mad_06.png)
+
+<!-- Итак, мы получили от формы авторизации логин и пароль.
 
 По клику на кнопке "Авторизоваться" пытаемся получить токен авторизации
 
@@ -724,7 +494,7 @@ json.put("password", password)
 // и вызываем POST-запрос /login
 Http.call(
     Http.buildRequest(
-        "http://s4a.kolei.ru/login",
+        "https://s4a.kolei.ru/login",
         json.toString()
     )
 ) { response, error ->
@@ -780,7 +550,7 @@ Http.call(
 // тут для примера я формирую JSON-строку 
 // без использования класса JSONObject
 Http.call(Http.buildRequest(
-    "http://s4a.kolei.ru/logout", 
+    "https://s4a.kolei.ru/logout", 
     """{"username":"${userName}"}"""))
 {response, error ->
     try {
@@ -803,17 +573,70 @@ Http.call(Http.buildRequest(
 ```kt
 Http.call(
     Http.buildRequest(
-        "http://s4a.kolei.ru/Product",
+        "https://s4a.kolei.ru/Product",
         headers = mapOf("token" to token)
     )
 ) { response, error ->
     ...
 ```
 
-Как видите, в параметры метода **buildRequest** я добавил заголовки, чтобы можно было добавить токен. Дальнейшая реализация уже с вас.
+Как видите, в параметры метода **buildRequest** я добавил заголовки, чтобы можно было добавить токен. Дальнейшая реализация уже с вас. 
+
+-->
+
+### Хранение данных в памяти устройства (долговременное)
+
+Разберёмся как узнать: в первый раз мы запустили приложение или нет.
+
+Подробно [тут](https://startandroid.ru/ru/uroki/vse-uroki-spiskom/73-urok-33-hranenie-dannyh-preferences.html)
+
+Мы в качестве внешнего хранилища будем использовать **Preferences**. Это знакомый уже нам способ хранения данных в виде пары: **имя**, **значение**. Данные записываются в память телефона и доступны нам после перезапуска приложения.
+
+Для получения экземпляра хранилища мы должны вызвать метод *getSharedPreferences*:
+
+```kt
+val myPreferences = getSharedPreferences(
+    "settings", 
+    MODE_PRIVATE)
+```
+
+где:
+* "settings" - произвольное название вашего хранилища (их у вас может быть несколько)
+* Константа MODE_PRIVATE используется для настройки доступа и означает, что после сохранения, данные будут видны только этому приложению
+
+После получения экземпляра хранилища мы можем читать и писать в него значения.
+
+Чтение простое:
+
+```kt
+val isFirstEnter = myPreferences
+    .getBoolean(
+        "isFirstEnter", 
+        true)
+```
+
+Используя get-методы (*getString*, *getBoolean* и т.д.) мы можем получить сохранённые данные или значения по-умолчанию, если такого параметра нет в хранилище
 
+Таким образом при первом входе в приложение мы получим `isFirstEnter = true`. 
 
-## Сохранение данных при работе приложения
+Теперь нам необходимо записать в этот параметр значение `false`, чтобы при следующем входе знать, что это уже не первый вход в приложение.
+
+Процесс записи несколько сложнее:
+
+```kt
+val editor = myPreferences.edit()
+try {
+    editor.putBoolean(
+        "isFirstEnter", 
+        false )
+} finally {
+    editor.commit()
+}
+```
+
+Мы должны получить объект **Editor**, в котором реализованы методы сохранения данных (*putString*, *putBoolean* ...), вызвать нужный метод и после записи данных вызвать подтверждение записи в хранилище (метод *commit*)
+
+### Сохранение данных при работе приложения
 
 Специфика жизненного цикла **activity** такова, что при каждом чихе (смена ориентации, языка и т.п.) окно пересоздаётся То есть все наши данные теряются.
 
@@ -823,10 +646,25 @@ Http.call(
 
 Но хранить в таком объекте можно только скалярные данные (т.е. базовые типы типа "целое", "строка" и т.п.). Чтобы сохранить более-менее сложный объект, например, массив продукции, нужно использовать сериализацию/десериализацию.
 
-Есть ещё более модная концепция **LiveData** - но там даже у меня происходит вывих мозга (это самая правильная концепция и если вы не дай бог станете таки андроид-разработчиками, то разобраться в ней надо).
+Есть ещё более модная концепция **LiveData** - но там даже у меня происходит вывих мозга. Но это самая правильная концепция и если вы не дай бог станете таки андроид-разработчиками, то разобраться в ней надо.
+
+При желании вы можете с ним разобраться найдя статьи в интернете, например: 
+
+* [ViewModel и LiveData. Сохранение и передача состояния активити при повороте устройства](https://www.fandroid.info/urok-7-androiddev-na-kotlin-sohranenie-i-peredacha-sostoyaniya-aktiviti-pri-povorote-ustrojstva-onsaveinstancestate-ili-viewmodel-livedata-kotlin-android-extensions/)
+
+* [Использование ViewModel для хранения данных пользовательского интерфейса](https://swiftbook.ru/post/tutorials/android-lifecycle/)
+
 
 Но ещё есть простой рабоче-крестьянский вариант с глобальными переменными. Само понятие глобальные переменные противоречит одному из основных постулатов ООП - **инкапсуляция**, поэтому его не рекомендуется использовать в сколько-нибудь серъезных проектах.
 
+Мы будем использовать **хранение данных в классе приложения**.
+
+Этот метод редко упоминается в литературе, но достаточно прост в реализации.
+
+Рассматривая жизненный цикл андроид приложения все почему-то описывают только жизненный цикл отдельной активности (иногда ещё фрагмента), но не упоминают про само приложение. А оно между тем продолжает существовать и при смене активностей и при засыпании телефона. 
+
+Есть возможность определить свой класс приложения и хранить в нём нужные данные, которые будут доступны в любой активности:
+
 1. Создайте класс **MyApp**, который наследуется от класа **Application** и внутри опишите **публичные** переменные, которые можно будет использовать во всех **activity** нашего приложения
 
     ```kt
@@ -836,9 +674,9 @@ Http.call(
     }
     ```
 
-2. В манифесте в тег **application** добавьте атрибут `android:name=".MyApp"`, где `.MyApp` это имя созданного нами ранее класса
+1. В манифесте в тег **application** добавьте атрибут `android:name=".MyApp"`, где `.MyApp` это имя созданного нами ранее класса
 
-3. В классах, где нам нужны глобальные переменные создаем переменную, которая будет хранить указатель на **MyApp**
+1. В классах, где нам нужны глобальные переменные создаем переменную, которая будет хранить указатель на **MyApp**
 
     ```kt
     private lateinit var app: MyApp
@@ -853,7 +691,7 @@ Http.call(
     И дальше в коде можете её использовать
 
     ```kt
-    if(app.token.isEmpty()){
+    if(app?.token.isEmpty()){
         // 
     }
     else {
@@ -861,6 +699,53 @@ Http.call(
     }
     ```
 
+### Класс **User**
+
+При успешной авторизации мы получим в ответе данные пользователя - запишем их в экземпляр класса **User**:
+
+Класс **User** (поля берём из ответа сервера):
+
+```kt
+data class User(
+    var userId: Int,
+    var firstName: String,
+    var lastName: String,
+    var phone: String,
+    var valid: Boolean,
+    var avatar: String? = null,
+    var prava: String? = null,
+    var passport: String? = null
+)
+```
+
+Поля, которые в ответе сервера не обязательны (ссылки на фотографии), мы делаем *нуллабельными* и задаём значения по-умолчанию. 
+
+Пример получения профиля:
+
+```kt
+...
+
+val jsonObj = JSONObject(response.body!!.string())
+
+app.user = User(
+    jsonObj.getInt("userId"),
+    jsonObj.getString("firstName"),
+    jsonObj.getString("lastname"),
+    jsonObj.getString("phone"),
+    jsonObj.getBoolean("valid"),
+    if(jsonObj.has("avatar")) jsonObj.getString("avatar") else null
+)
+
+if(jsonObj.has("prava"))
+    app.user!!.prava = jsonObj.getString("avatar")
+```
+
+Обратите внимание, *нуллабельные* параметры *avatar* и *prava* я заполнил по-разному (оба варианта правильные):
+
+* аватар мы заполнили используя возможность использовать оператор `if` как функцию
+* права мы заполнили не при создании экземпляра класса, а отдельно
+
+<!-- 
 ## Фильтрация данных
 
 В нормальных АПИ должна быть предусмотрена фильтрация набора данных на сервере, то есть вам придут только те данные, которые нужны. Но на моём сервере АПИ простенькое (зато универсальное), оно просто отдаёт ту таблицу в базе, которую вы запросите (нам это не страшно, так как таблицы у нас коротенькие). 
@@ -928,8 +813,6 @@ productTypeSpinner.adapter = ArrayAdapter(
 
 И при выборе элемента списка сделать фильтрацию списка продукции (тоже сделайте сами)
 
-<!-- TODO вроде нигде не расписано как делать фильтрацию - написать -->
-
 ```kt
 productTypeSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener{
     override fun onItemSelected(
@@ -943,18 +826,20 @@ productTypeSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedL
 
     override fun onNothingSelected(parent: AdapterView<*>?) {}
 }
-```
+``` 
+
+-->
+
+---
+
+## Задание
+
+* Создать приложение с иконкой и названием.
 
-# Задание
+* Реализовать заставку (`launch screen`) с таймером на пару секунд, только потом показывать окно регистрации/авторизации
 
-* на первый экран добавить заставку (splash-screen) с таймером на пару секунд, только потом показывать диалог для ввода логина/пароля
-* после авторизации и получения токена перейти на другое окно (activity), на котором получить и вывести в вертикальный RecyclerView список продукции (с картинками)
-* на окне со списком продукции сделать кнопку "выход" (logout), после чего вернуться на экран авторизации. У нас после первой авторизации осталось имя, сделайте передачу имени в диалог авторизации, чтобы он был уже заполнен.
+* Реализовать окна регистрации и авторизации с запоминанием первого входа в приложение и POST-запросами в АПИ.
 
-<table style="width: 100%;"><tr><td style="width: 40%;">
-<a href="../articles/android_bottom_navigation.md">Android Navigation. Знакомство с BottomNavigationView.
-</a></td><td style="width: 20%;">
-<a href="../readme.md">Содержание
-</a></td><td style="width: 40%;">
-<a href="../articles/map_yandex.md">Работа с картами
-</a></td><tr></table>
+Предыдущая лекция | &nbsp; | Следующая лекция
+:----------------:|:----------:|:----------------:
+[Анимация](./animation.md) | [Содержание](../readme.md#практика-разработка-мобильных-приложений) | [Проект "каршеринг" Часть 2. Профиль пользователя.](./android_profile.md)

+ 2 - 0
articles/android_profile.md

@@ -0,0 +1,2 @@
+# Проект "каршеринг" Часть 2. Профиль пользователя.
+

+ 9 - 0
articles/animation.md

@@ -1,3 +1,8 @@
+Предыдущая лекция | &nbsp; | Следующая лекция
+:----------------:|:----------:|:----------------:
+[Android Navigation.](./android_bottom_navigation.md) | [Содержание](../readme.md#практика-разработка-мобильных-приложений) | [Проект "каршеринг"](./android_auth.md)
+
+
 # Анимация
 
 Базовые анимации. Взято [отсюда](https://habr.com/ru/post/347918/)
@@ -440,3 +445,7 @@ VectorDrawable состоит из Path и Group элементов. Созда
 
 Все примеры можно посмотреть и изучить [здесь](https://github.com/JuzTosS/AnimationsDemo).
 
+Предыдущая лекция | &nbsp; | Следующая лекция
+:----------------:|:----------:|:----------------:
+[Android Navigation.](./android_bottom_navigation.md) | [Содержание](../readme.md#практика-разработка-мобильных-приложений) | [Проект "каршеринг"](./android_auth.md)
+

二进制
img/mad_03.png


二进制
img/mad_04.png


二进制
img/mad_05.png


二进制
img/mad_06.png


二进制
img/mas1_01.png


二进制
img/mas1_02.png


二进制
img/mas1_03.png


+ 7 - 4
readme.md

@@ -175,8 +175,9 @@ http://sergeyteplyakov.blogspot.com/2014/01/microsoft-fakes-state-verification.h
     + [Фрагменты](./articles/fragments.md)
     + [Android Navigation. Знакомство с BottomNavigationView.](./articles/android_bottom_navigation.md)
     + [Анимация](./articles/animation.md)
-    + TODO по итогам курсов сюда сделать каршеринг [Проект "база". Авторизация на сервере (Basic auth, token).](./articles/android_auth.md)
-    + [Работа с картами](./articles/map_yandex.md)
+    + [Проект "каршеринг" Часть 1. Регистрация/Авторизация.](./articles/android_auth.md)
+    + [Проект "каршеринг" Часть 2. Профиль пользователя.](./articles/android_profile.md)
+    + [Проект "каршеринг" Часть 3. Работа с Яндекс картами](./articles/map_yandex.md)
     + [Wear OS (умные часы)](./articles/wear_os.md)
     + [Android TV](./articles/android_tv.md)
 
@@ -476,9 +477,11 @@ https://office-menu.ru/uroki-sql Уроки SQL
 
 1. [Анимация](./articles/animation.md)
 
-1. TODO по итогам курсов сюда сделать каршеринг [Проект "база". Авторизация на сервере (Basic auth, token).](./articles/android_auth.md)
+1. [Проект "каршеринг" Часть 1. Регистрация/Авторизация.](./articles/android_auth.md)
 
-1. [Работа с картами](./articles/map_yandex.md)
+1. [Проект "каршеринг" Часть 2. Профиль пользователя.](./articles/android_profile.md)
+
+1. [Проект "каршеринг" Часть 3. Работа с Яндекс картами](./articles/map_yandex.md)
 
 1. [Wear OS](./articles/wear_os.md)